diff --git a/abprime/addon/adblockplus.js b/abprime/addon/adblockplus.js
new file mode 100644
index 00000000..c3c05bbb
--- /dev/null
+++ b/abprime/addon/adblockplus.js
@@ -0,0 +1,32 @@
+#filter substitution
+
+pref("extensions.@ADDON_CHROME_NAME@.currentVersion", "0.0");
+pref("extensions.@ADDON_CHROME_NAME@.enabled", true);
+pref("extensions.@ADDON_CHROME_NAME@.frameobjects", true);
+pref("extensions.@ADDON_CHROME_NAME@.fastcollapse", false);
+pref("extensions.@ADDON_CHROME_NAME@.showinstatusbar", false);
+pref("extensions.@ADDON_CHROME_NAME@.detachsidebar", false);
+pref("extensions.@ADDON_CHROME_NAME@.defaulttoolbaraction", 0);
+pref("extensions.@ADDON_CHROME_NAME@.defaultstatusbaraction", 0);
+pref("extensions.@ADDON_CHROME_NAME@.sidebar_key", "Accel Shift V, Accel Shift U");
+pref("extensions.@ADDON_CHROME_NAME@.sendReport_key", "");
+pref("extensions.@ADDON_CHROME_NAME@.filters_key", "Accel Shift E, Accel Shift F, Accel Shift O");
+pref("extensions.@ADDON_CHROME_NAME@.enable_key", "");
+pref("extensions.@ADDON_CHROME_NAME@.flash_scrolltoitem", true);
+pref("extensions.@ADDON_CHROME_NAME@.previewimages", true);
+pref("extensions.@ADDON_CHROME_NAME@.data_directory", "adblockplus");
+pref("extensions.@ADDON_CHROME_NAME@.patternsbackups", 5);
+pref("extensions.@ADDON_CHROME_NAME@.patternsbackupinterval", 24);
+pref("extensions.@ADDON_CHROME_NAME@.whitelistschemes", "about chrome file irc moz-safe-about news resource snews x-jsd addbook cid imap mailbox nntp pop data javascript moz-icon");
+pref("extensions.@ADDON_CHROME_NAME@.hideimagemanager", true);
+pref("extensions.@ADDON_CHROME_NAME@.subscriptions_autoupdate", true);
+pref("extensions.@ADDON_CHROME_NAME@.subscriptions_listurl", "https://adblockplus.org/subscriptions2.xml");
+pref("extensions.@ADDON_CHROME_NAME@.subscriptions_fallbackurl", "https://adblockplus.org/getSubscription?version=%VERSION%&url=%SUBSCRIPTION%&downloadURL=%URL%&error=%ERROR%&channelStatus=%CHANNELSTATUS%&responseStatus=%RESPONSESTATUS%");
+pref("extensions.@ADDON_CHROME_NAME@.subscriptions_fallbackerrors", 5);
+pref("extensions.@ADDON_CHROME_NAME@.documentation_link", "https://adblockplus.org/redirect?link=%LINK%&lang=%LANG%");
+pref("extensions.@ADDON_CHROME_NAME@.savestats", true);
+pref("extensions.@ADDON_CHROME_NAME@.composer_default", 2);
+pref("extensions.@ADDON_CHROME_NAME@.clearStatsOnHistoryPurge", true);
+pref("extensions.@ADDON_CHROME_NAME@.report_submiturl", "");
+pref("extensions.@ADDON_CHROME_NAME@.recentReports", "[]");
+pref("services.sync.engine.@ADDON_CHROME_NAME@", false);
diff --git a/abprime/addon/icon.png b/abprime/addon/icon.png
new file mode 100644
index 00000000..59c427df
Binary files /dev/null and b/abprime/addon/icon.png differ
diff --git a/abprime/addon/icon64.png b/abprime/addon/icon64.png
new file mode 100644
index 00000000..f840f408
Binary files /dev/null and b/abprime/addon/icon64.png differ
diff --git a/abprime/addon/install.rdf b/abprime/addon/install.rdf
new file mode 100644
index 00000000..918630e6
--- /dev/null
+++ b/abprime/addon/install.rdf
@@ -0,0 +1,58 @@
+
+
+
+
+#filter substitution
+
+
+
+
+ @ADDON_ID@
+ @ADDON_VERSION@
+ @ADDON_NAME@
+ @ADDON_SHORT_DESC@
+ @ADDON_AUTHOR@
+#ifdef ADDON_URL
+ @ADDON_URL@
+#endif
+ 2
+
+
+
+ @ADDON_TARGET_APP_ID@
+ @ADDON_TARGET_APP_MINVER@
+ @ADDON_TARGET_APP_MAXVER@
+
+
+#ifdef ADDON_TARGET_BASILISK
+
+
+
+ {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+ 52.0
+ 55.*
+ true
+
+
+#endif
+
+
+
+ {a3210b97-8e8a-4737-9aa0-aa0e607640b9}
+ 1.0.0a1
+ 2.*
+
+
+
+
+
+ {3550f703-e582-4d05-9a08-453d09bdfdc6}
+ 52.9.6884
+ 52.9.*
+
+
+
+
\ No newline at end of file
diff --git a/abprime/addon/jar.mn b/abprime/addon/jar.mn
new file mode 100644
index 00000000..a0fc8158
--- /dev/null
+++ b/abprime/addon/jar.mn
@@ -0,0 +1,12 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% resource @ADDON_CHROME_NAME@ /
+
+% component {d32a3c00-4ed3-11de-8a39-0800200c9a66} components/Initializer.js
+% contract @adblockplus.org/abp/startup;1 {d32a3c00-4ed3-11de-8a39-0800200c9a66}
+% category profile-after-change @adblockplus.org/abp/startup;1 @adblockplus.org/abp/startup;1
\ No newline at end of file
diff --git a/abprime/addon/moz.build b/abprime/addon/moz.build
new file mode 100644
index 00000000..b8894980
--- /dev/null
+++ b/abprime/addon/moz.build
@@ -0,0 +1,16 @@
+# vim: set filetype=python:
+# 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/.
+
+FINAL_TARGET_FILES += [
+ 'icon.png',
+ 'icon64.png',
+]
+
+FINAL_TARGET_PP_FILES += ['install.rdf']
+
+FINAL_TARGET_PP_FILES.defaults += ['patterns.ini']
+FINAL_TARGET_PP_FILES.defaults.preferences += ['adblockplus.js']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/abprime/addon/patterns.ini b/abprime/addon/patterns.ini
new file mode 100644
index 00000000..afb67743
--- /dev/null
+++ b/abprime/addon/patterns.ini
@@ -0,0 +1,2 @@
+# Adblock Plus preferences
+version=4
diff --git a/abprime/app.mozbuild b/abprime/app.mozbuild
new file mode 100644
index 00000000..b921a9f6
--- /dev/null
+++ b/abprime/app.mozbuild
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+# Never add tier dirs after the application srcdir because they
+# apparently won't get packaged properly on Mac.
+DIRS += ['/abprime']
diff --git a/abprime/build.mk b/abprime/build.mk
new file mode 100644
index 00000000..92fe1b14
--- /dev/null
+++ b/abprime/build.mk
@@ -0,0 +1,7 @@
+# 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/.
+
+package:
+ cd $(DIST)/bin; \
+ zip -Dr9X ../${ADDON_XPI_NAME}-${ADDON_VERSION}.xpi * -x \*/.mkdir.done; \
diff --git a/abprime/components/Initializer.js b/abprime/components/Initializer.js
new file mode 100644
index 00000000..8cf78457
--- /dev/null
+++ b/abprime/components/Initializer.js
@@ -0,0 +1,94 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Gecko 1.9.0/1.9.1 compatibility - add XPCOMUtils.defineLazyServiceGetter
+if (!("defineLazyServiceGetter" in XPCOMUtils))
+{
+ XPCOMUtils.defineLazyServiceGetter = function XPCU_defineLazyServiceGetter(obj, prop, contract, iface)
+ {
+ obj.__defineGetter__(prop, function XPCU_serviceGetter()
+ {
+ delete obj[prop];
+ return obj[prop] = Cc[contract].getService(Ci[iface]);
+ });
+ };
+}
+
+/**
+ * Application startup/shutdown observer, triggers init()/shutdown() methods in Bootstrap.jsm module.
+ * @constructor
+ */
+function Initializer() {}
+Initializer.prototype =
+{
+ classDescription: "Adblock Plus initializer",
+ contractID: "@adblockplus.org/abp/startup;1",
+ classID: Components.ID("{d32a3c00-4ed3-11de-8a39-0800200c9a66}"),
+ _xpcom_categories: [{ category: "app-startup", service: true }],
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe: function(subject, topic, data)
+ {
+ let observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+ switch (topic)
+ {
+ case "app-startup":
+ observerService.addObserver(this, "profile-after-change", true);
+ break;
+ case "profile-after-change":
+ observerService.addObserver(this, "quit-application", true);
+
+ // Don't init in Fennec, initialization will happen when UI is ready
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
+ if (appInfo.ID != "{a23983c0-fd0e-11dc-95ff-0800200c9a66}" && appInfo.ID != "{aa3c5121-dab2-40e2-81ca-7ea25febc110}")
+ {
+ try
+ {
+ // Gecko 2.0 and higher - chrome URLs can be loaded directly
+ Cu.import("resource://@ADDON_CHROME_NAME@/modules/Bootstrap.jsm");
+ }
+ catch (e)
+ {
+ // Gecko 1.9.x - have to convert chrome URLs to file URLs first
+ let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ let bootstrapURL = chromeRegistry.convertChromeURL(ioService.newURI("resource://@ADDON_CHROME_NAME@/modules/Bootstrap.jsm", null, null));
+ Cu.import(bootstrapURL.spec);
+ }
+ Bootstrap.startup();
+ }
+ break;
+ case "quit-application":
+ try {
+ // This will fail if component was added via chrome.manifest (Gecko 2.0)
+ observerService.removeObserver(this, "profile-after-change");
+ }catch(e) {}
+ observerService.removeObserver(this, "quit-application");
+ if ("@adblockplus.org/abp/private;1" in Cc)
+ {
+ let baseURL = Cc["@adblockplus.org/abp/private;1"].getService(Ci.nsIURI);
+ Cu.import(baseURL.spec + "Bootstrap.jsm");
+ Bootstrap.shutdown(false);
+ }
+ break;
+ }
+ }
+};
+
+if (XPCOMUtils.generateNSGetFactory)
+ var NSGetFactory = XPCOMUtils.generateNSGetFactory([Initializer]);
+else
+ var NSGetModule = XPCOMUtils.generateNSGetModule([Initializer]);
diff --git a/abprime/components/moz.build b/abprime/components/moz.build
new file mode 100644
index 00000000..948abeb3
--- /dev/null
+++ b/abprime/components/moz.build
@@ -0,0 +1,9 @@
+# vim: set filetype=python:
+# 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/.
+
+# Don't use sub-manifest files
+NO_JS_MANIFEST = True
+
+EXTRA_PP_COMPONENTS += ['Initializer.js']
diff --git a/abprime/content/about.js b/abprime/content/about.js
new file mode 100644
index 00000000..5a6b9e03
--- /dev/null
+++ b/abprime/content/about.js
@@ -0,0 +1,164 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+try
+{
+ Cu.import("resource://gre/modules/AddonManager.jsm");
+}
+catch (e) {}
+
+function init()
+{
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ if (typeof AddonManager != "undefined")
+ {
+ let addon = AddonManager.getAddonByID(Utils.addonID, function(addon)
+ {
+ loadInstallManifest(addon.getResourceURI("install.rdf"), addon.name, addon.homepageURL);
+ });
+ }
+ else if ("@mozilla.org/extensions/manager;1" in Cc)
+ {
+ let extensionManager = Cc["@mozilla.org/extensions/manager;1"].getService(Ci.nsIExtensionManager);
+ let rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
+ let root = rdf.GetResource("urn:mozilla:item:" + Utils.addonID);
+
+ function emResource(prop)
+ {
+ return rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + prop);
+ }
+
+ function getTarget(prop)
+ {
+ let target = extensionManager.datasource.GetTarget(root, emResource(prop), true);
+ if (target)
+ return target.QueryInterface(Ci.nsIRDFLiteral).Value;
+ else
+ return null;
+ }
+
+ let installLocation = extensionManager.getInstallLocation(Utils.addonID);
+ let installManifestFile = installLocation.getItemFile(Utils.addonID, "install.rdf");
+ loadInstallManifest(ioService.newFileURI(installManifestFile), getTarget("name"), getTarget("homepageURL"));
+ }
+ else
+ {
+ // No add-on manager, no extension manager - we must be running in K-Meleon.
+ // Load Manifest.jsm as last solution.
+ Cu.import(baseURL + "Manifest.jsm");
+ setExtensionData(manifest.name, manifest.version, manifest.homepage, [manifest.creator], manifest.contributors, manifest.translators);
+ }
+}
+
+function loadInstallManifest(installManifestURI, name, homepage)
+{
+ let rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService);
+ let ds = rdf.GetDataSource(installManifestURI.spec);
+ let root = rdf.GetResource("urn:mozilla:install-manifest");
+
+ function emResource(prop)
+ {
+ return rdf.GetResource("http://www.mozilla.org/2004/em-rdf#" + prop);
+ }
+
+ function getTargets(prop)
+ {
+ let targets = ds.GetTargets(root, emResource(prop), true);
+ let result = [];
+ while (targets.hasMoreElements())
+ result.push(targets.getNext().QueryInterface(Ci.nsIRDFLiteral).Value);
+ return result;
+ }
+
+ function dataSourceLoaded()
+ {
+ setExtensionData(name, getTargets("version")[0],
+ homepage, getTargets("creator"),
+ getTargets("contributor"), getTargets("translator"));
+ }
+
+ if (ds instanceof Ci.nsIRDFRemoteDataSource && ds.loaded)
+ dataSourceLoaded();
+ else
+ {
+ let sink = ds.QueryInterface(Ci.nsIRDFXMLSink);
+ sink.addXMLSinkObserver({
+ onBeginLoad: function() {},
+ onInterrupt: function() {},
+ onResume: function() {},
+ onEndLoad: function() {
+ sink.removeXMLSinkObserver(this);
+ dataSourceLoaded();
+ },
+ onError: function() {},
+ });
+ }
+}
+
+function cmpNoCase(a, b)
+{
+ let aLC = a.toLowerCase();
+ let bLC = b.toLowerCase();
+ if (aLC < bLC)
+ return -1;
+ else if (aLC > bLC)
+ return 1;
+ else
+ return 0;
+}
+
+function setExtensionData(name, version, homepage, authors, contributors, translators)
+{
+ authors.sort(cmpNoCase);
+ contributors.sort(cmpNoCase);
+ translators.sort(cmpNoCase);
+
+ E("title").value = name;
+ E("version").value = version;
+ E("homepage").value = homepage;
+ E("authors").textContent = authors.join(", ");
+ E("contributors").textContent = contributors.join(", ");
+ E("translators").textContent = translators.join(", ");
+
+ let request = new XMLHttpRequest();
+ request.open("GET", "chrome://@ADDON_CHROME_NAME@/content/subscriptions.xml");
+ request.addEventListener("load", setSubscriptionAuthors, false);
+ request.send(null);
+}
+
+function setSubscriptionAuthors()
+{
+ let doc = this.responseXML;
+ if (!doc || doc.documentElement.localName != "subscriptions")
+ return;
+
+ let authors = {__proto__: null};
+ for (let node = doc.documentElement.firstChild; node; node = node.nextSibling)
+ {
+ if (node.localName != "subscription" || !node.hasAttribute("author"))
+ continue;
+
+ for each (let author in node.getAttribute("author").split(","))
+ {
+ author = author.replace(/^\s+/, "").replace(/\s+$/, "");
+ if (author == "")
+ continue;
+
+ authors[author] = true;
+ }
+ }
+
+ let list = [];
+ for (let author in authors)
+ list.push(author);
+
+ list.sort(cmpNoCase)
+ E("subscriptionAuthors").textContent = list.join(", ");
+
+ E("mainBox").setAttribute("loaded", "true");
+}
diff --git a/abprime/content/about.xul b/abprime/content/about.xul
new file mode 100644
index 00000000..772d7851
--- /dev/null
+++ b/abprime/content/about.xul
@@ -0,0 +1,62 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/composer.js b/abprime/content/composer.js
new file mode 100644
index 00000000..7c6cae28
--- /dev/null
+++ b/abprime/content/composer.js
@@ -0,0 +1,401 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+let nodes = null;
+let item = null;
+let advancedMode = false;
+
+function init()
+{
+ [nodes, item] = window.arguments;
+
+ E("filterType").value = (!item.filter || item.filter.disabled || item.filter instanceof WhitelistFilter ? "filterlist" : "whitelist");
+ E("customPattern").value = item.location;
+
+ let insertionPoint = E("customPatternBox");
+ let addSuggestion = function(address)
+ {
+ // Always drop protocol and www. from the suggestion
+ address = address.replace(/^[\w\-]+:\/+(?:www\.)?/, "");
+
+ let suggestion = document.createElement("radio");
+ suggestion.setAttribute("value", address);
+ suggestion.setAttribute("label", address);
+ suggestion.setAttribute("crop", "center");
+ suggestion.setAttribute("class", "suggestion");
+ insertionPoint.parentNode.insertBefore(suggestion, insertionPoint);
+
+ return address;
+ }
+
+ let ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+ try
+ {
+ let suggestions = [""];
+
+ let url = ioService.newURI(item.location, null, null)
+ .QueryInterface(Ci.nsIURL);
+ let suffix = (url.query ? "?*" : "");
+ url.query = "";
+ suggestions[1] = addSuggestion(url.spec + suffix);
+
+ let parentURL = ioService.newURI(url.fileName == "" ? ".." : ".", null, url);
+ if (!parentURL.equals(url))
+ suggestions[2] = addSuggestion(parentURL.spec + "*");
+ else
+ suggestions[2] = suggestions[1];
+
+ let rootURL = ioService.newURI("/", null, url);
+ if (!rootURL.equals(parentURL) && !rootURL.equals(url))
+ suggestions[3] = addSuggestion(rootURL.spec + "*");
+ else
+ suggestions[3] = suggestions[2];
+
+ try
+ {
+ suggestions[4] = addSuggestion(url.host.replace(/^www\./, "") + "^");
+
+ // Prefer example.com^ to example.com/*
+ let undesired = suggestions[4].replace(/\^$/, "/*");
+ for (let i = 0; i < suggestions.length - 1; i++)
+ if (suggestions[i] == undesired)
+ suggestions[i] = suggestions[4];
+
+ for (let child = insertionPoint.parentNode.firstChild; child; child = child.nextSibling)
+ {
+ if (child.localName == "radio" && child.getAttribute("value") == undesired)
+ {
+ child.parentNode.removeChild(child);
+ break;
+ }
+ }
+ }
+ catch (e)
+ {
+ suggestions[4] = suggestions[3];
+ }
+
+ try
+ {
+ let effectiveTLD = Cc["@mozilla.org/network/effective-tld-service;1"].getService(Ci.nsIEffectiveTLDService);
+ let host = url.host;
+ let baseDomain = effectiveTLD.getBaseDomainFromHost(host);
+ if (baseDomain != host.replace(/^www\./, ""))
+ suggestions[5] = addSuggestion(baseDomain + "^");
+ else
+ suggestions[5] = suggestions[4];
+ }
+ catch (e)
+ {
+ suggestions[5] = suggestions[4];
+ }
+
+ E("patternGroup").value = (Prefs.composer_default in suggestions ? suggestions[Prefs.composer_default] : suggestions[1]);
+ }
+ catch (e)
+ {
+ // IOService returned nsIURI - not much we can do with it
+ addSuggestion(item.location);
+ E("patternGroup").value = "";
+ }
+ if (Prefs.composer_default == 0)
+ E("customPattern").focus();
+ else
+ E("patternGroup").focus();
+
+ let types = [];
+ for (let type in Policy.localizedDescr)
+ {
+ types.push(parseInt(type));
+ }
+ types.sort(function(a, b) {
+ if (a < b)
+ return -1;
+ else if (a > b)
+ return 1;
+ else
+ return 0;
+ });
+
+ let docDomain = item.docDomain;
+ let thirdParty = item.thirdParty;
+
+ if (docDomain)
+ docDomain = docDomain.replace(/^www\./i, "").replace(/\.+$/, "");
+ if (docDomain)
+ E("domainRestriction").value = docDomain;
+
+ E("thirdParty").hidden = !thirdParty;
+ E("firstParty").hidden = thirdParty;
+
+ let typeGroup = E("typeGroup");
+ let defaultTypes = RegExpFilter.prototype.contentType & ~RegExpFilter.typeMap.DOCUMENT;
+ let isDefaultType = (RegExpFilter.typeMap[item.typeDescr] & defaultTypes) != 0;
+ for each (let type in types)
+ {
+ if (type == Policy.type.ELEMHIDE)
+ continue;
+
+ let typeNode = document.createElement("checkbox");
+ typeNode.setAttribute("value", Policy.typeDescr[type].toLowerCase().replace(/\_/g, "-"));
+ typeNode.setAttribute("label", Policy.localizedDescr[type].toLowerCase());
+
+ let typeMask = RegExpFilter.typeMap[Policy.typeDescr[type]];
+ typeNode._defaultType = (typeMask & defaultTypes) != 0;
+ if ((isDefaultType && typeNode._defaultType) || (!isDefaultType && item.type == type))
+ typeNode.setAttribute("checked", "true");
+
+ if (item.type == type)
+ typeNode.setAttribute("disabled", "true");
+ typeNode.addEventListener("command", function() checkboxUpdated(this), false);
+ typeGroup.appendChild(typeNode);
+ }
+
+ let collapseDefault = E("collapseDefault");
+ collapseDefault.label = collapseDefault.getAttribute(Prefs.fastcollapse ? "label_no" : "label_yes");
+ E("collapse").value = "";
+ E("collapse").setAttribute("label", collapseDefault.label);
+
+ let warning = E("disabledWarning");
+ generateLinkText(warning);
+ warning.hidden = Prefs.enabled;
+
+ updatePatternSelection();
+}
+
+function checkboxUpdated(checkbox)
+{
+ checkbox._lastChange = Date.now();
+ updateFilter();
+}
+
+function updateFilter()
+{
+ let filter = "";
+
+ let type = E("filterType").value
+ if (type == "whitelist")
+ filter += "@@";
+
+ let pattern = E("patternGroup").value;
+ if (pattern == "")
+ pattern = E("customPattern").value;
+
+ if (E("anchorStart").checked)
+ filter += E("anchorStart").flexibleAnchor ? "||" : "|";
+
+ filter += pattern;
+
+ if (E("anchorEnd").checked)
+ filter += "|";
+
+ if (advancedMode)
+ {
+ let options = [];
+
+ if (E("domainRestrictionEnabled").checked)
+ {
+ let domainRestriction = E("domainRestriction").value.replace(/[,\s]/g, "").replace(/\.+$/, "");
+ if (domainRestriction)
+ options.push([E("domainRestrictionEnabled")._lastChange || 0, "domain=" + domainRestriction]);
+ }
+
+ if (E("firstParty").checked)
+ options.push([E("firstParty")._lastChange || 0, "~third-party"]);
+ if (E("thirdParty").checked)
+ options.push([E("thirdParty")._lastChange || 0, "third-party"]);
+
+ if (E("matchCase").checked)
+ options.push([E("matchCase")._lastChange || 0, "match-case"]);
+
+ let collapse = E("collapse");
+ disableElement(collapse, type == "whitelist", "value", "");
+ if (collapse.value != "")
+ options.push([collapse._lastChange, collapse.value]);
+
+ let enabledTypes = [];
+ let disabledTypes = [];
+ let forceEnabledTypes = [];
+ for (let typeNode = E("typeGroup").firstChild; typeNode; typeNode = typeNode.nextSibling)
+ {
+ let value = typeNode.getAttribute("value");
+ if (value == "document")
+ disableElement(typeNode, type != "whitelist", "checked", false);
+
+ if (!typeNode._defaultType)
+ {
+ if (typeNode.getAttribute("checked") == "true")
+ forceEnabledTypes.push([typeNode._lastChange || 0, value]);
+ }
+ else if (typeNode.getAttribute("checked") == "true")
+ enabledTypes.push([typeNode._lastChange || 0, value]);
+ else
+ disabledTypes.push([typeNode._lastChange || 0, "~" + value]);
+ }
+ if (!forceEnabledTypes.length && disabledTypes.length < enabledTypes.length)
+ options.push.apply(options, disabledTypes);
+ else
+ options.push.apply(options, enabledTypes);
+ options.push.apply(options, forceEnabledTypes);
+
+ if (options.length)
+ {
+ options.sort(function(a, b) a[0] - b[0]);
+ filter += "$" + options.map(function(o) o[1]).join(",");
+ }
+ }
+ else
+ {
+ let defaultTypes = RegExpFilter.prototype.contentType & ~RegExpFilter.typeMap.DOCUMENT;
+ let isDefaultType = (RegExpFilter.typeMap[item.typeDescr] & defaultTypes) != 0;
+ if (!isDefaultType)
+ filter += "$" + item.typeDescr.toLowerCase().replace(/\_/g, "-");
+ }
+
+ filter = Filter.normalize(filter);
+ E("regexpWarning").hidden = !Filter.regexpRegExp.test(filter);
+
+ let isSlow = false;
+ let compiledFilter = Filter.fromText(filter);
+ if (E("regexpWarning").hidden)
+ {
+ if (compiledFilter instanceof RegExpFilter && defaultMatcher.isSlowFilter(compiledFilter))
+ isSlow = true;
+ }
+ E("shortpatternWarning").hidden = !isSlow;
+
+ E("matchWarning").hidden = compiledFilter instanceof RegExpFilter && compiledFilter.matches(item.location, item.typeDescr, item.docDomain, item.thirdParty);
+
+ E("filter").value = filter;
+}
+
+function generateLinkText(element, replacement)
+{
+ let template = element.getAttribute("textTemplate");
+ if (typeof replacement != "undefined")
+ template = template.replace(/\?1\?/g, replacement)
+
+ let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
+ while (element.firstChild && element.firstChild.nodeType != Node.ELEMENT_NODE)
+ element.removeChild(element.firstChild);
+ while (element.lastChild && element.lastChild.nodeType != Node.ELEMENT_NODE)
+ element.removeChild(element.lastChild);
+ if (!element.firstChild)
+ return;
+
+ element.firstChild.textContent = linkText;
+ element.insertBefore(document.createTextNode(beforeLink), element.firstChild);
+ element.appendChild(document.createTextNode(afterLink));
+}
+
+function updatePatternSelection()
+{
+ let pattern = E("patternGroup").value;
+ if (pattern == "")
+ {
+ pattern = E("customPattern").value;
+ }
+ else
+ {
+ E("anchorStart").checked = true;
+ E("anchorEnd").checked = false;
+ }
+
+ function testFilter(/**String*/ filter) /**Boolean*/
+ {
+ return RegExpFilter.fromText(filter + "$" + item.typeDescr).matches(item.location, item.typeDescr, item.docDomain, item.thirdParty);
+ }
+
+ let anchorStartCheckbox = E("anchorStart");
+ if (!/^\*/.test(pattern) && testFilter("||" + pattern))
+ {
+ disableElement(anchorStartCheckbox, false, "checked", false);
+ anchorStartCheckbox.setAttribute("label", anchorStartCheckbox.getAttribute("labelFlexible"));
+ anchorStartCheckbox.accessKey = anchorStartCheckbox.getAttribute("accesskeyFlexible");
+ anchorStartCheckbox.flexibleAnchor = true;
+ }
+ else
+ {
+ disableElement(anchorStartCheckbox, /^\*/.test(pattern) || !testFilter("|" + pattern), "checked", false);
+ anchorStartCheckbox.setAttribute("label", anchorStartCheckbox.getAttribute("labelRegular"));
+ anchorStartCheckbox.accessKey = anchorStartCheckbox.getAttribute("accesskeyRegular");
+ anchorStartCheckbox.flexibleAnchor = false;
+ }
+ disableElement(E("anchorEnd"), /[\*\^]$/.test(pattern) || !testFilter(pattern + "|"), "checked", false);
+
+ updateFilter();
+ setAdvancedMode(document.documentElement.getAttribute("advancedMode") == "true");
+}
+
+function updateCustomPattern()
+{
+ E("patternGroup").value = "";
+ updatePatternSelection();
+}
+
+function addFilter() {
+ let filter = Filter.fromText(document.getElementById("filter").value);
+ filter.disabled = false;
+
+ FilterStorage.addFilter(filter);
+
+ if (nodes)
+ Policy.refilterNodes(nodes, item);
+
+ return true;
+}
+
+function setAdvancedMode(mode) {
+ advancedMode = mode;
+
+ var dialog = document.documentElement;
+ dialog.setAttribute("advancedMode", advancedMode);
+
+ var button = dialog.getButton("disclosure");
+ button.setAttribute("label", dialog.getAttribute(advancedMode ? "buttonlabeldisclosure_off" : "buttonlabeldisclosure_on"));
+
+ updateFilter();
+}
+
+function disableElement(element, disable, valueProperty, disabledValue) {
+ if ((element.getAttribute("disabled") == "true") == disable)
+ return;
+
+ if (disable)
+ {
+ element.setAttribute("disabled", "true");
+ element._abpStoredValue = element[valueProperty];
+ element[valueProperty] = disabledValue;
+ }
+ else
+ {
+ element.removeAttribute("disabled");
+ if ("_abpStoredValue" in element)
+ element[valueProperty] = element._abpStoredValue;
+ delete element._abpStoredValue;
+ }
+}
+
+function openPreferences() {
+ Utils.openFiltersDialog(Filter.fromText(E("filter").value));
+}
+
+function doEnable() {
+ Prefs.enabled = true;
+ E("disabledWarning").hidden = true;
+}
+
+/**
+ * Selects or unselects all type checkboxes except those
+ * that are disabled.
+ */
+function selectAllTypes(/**Boolean*/ select)
+{
+ for (let typeNode = E("typeGroup").firstChild; typeNode; typeNode = typeNode.nextSibling)
+ if (typeNode.getAttribute("disabled") != "true")
+ typeNode.checked = select;
+ updateFilter();
+}
diff --git a/abprime/content/composer.xul b/abprime/content/composer.xul
new file mode 100644
index 00000000..ace683cb
--- /dev/null
+++ b/abprime/content/composer.xul
@@ -0,0 +1,107 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &pattern.explanation;
+ ®exp.warning;
+ &shortpattern.warning;
+ &match.warning;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/errors.html b/abprime/content/errors.html
new file mode 100644
index 00000000..14e01809
--- /dev/null
+++ b/abprime/content/errors.html
@@ -0,0 +1,96 @@
+
+
+ Adblock Plus Errors
+
+
+
+ Refresh
+ Clear errors
+
+
+
+
diff --git a/abprime/content/filters-backup.js b/abprime/content/filters-backup.js
new file mode 100644
index 00000000..648670b8
--- /dev/null
+++ b/abprime/content/filters-backup.js
@@ -0,0 +1,331 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+/**
+ * Implementation of backup and restore functionality.
+ * @class
+ */
+var Backup =
+{
+ /**
+ * Template for menu items to be displayed in the Restore menu (for automated
+ * backups).
+ * @type Element
+ */
+ restoreTemplate: null,
+
+ /**
+ * Element after which restore items should be inserted.
+ * @type Element
+ */
+ restoreInsertionPoint: null,
+
+ /**
+ * Regular expression to recognize checksum comments.
+ */
+ CHECKSUM_REGEXP: /^!\s*checksum[\s\-:]+([\w\+\/]+)/i,
+
+ /**
+ * Regular expression to recognize group title comments.
+ */
+ GROUPTITLE_REGEXP: /^!\s*\[(.*)\]((?:\/\w+)*)\s*$/,
+
+
+ /**
+ * Initializes backup UI.
+ */
+ init: function()
+ {
+ this.restoreTemplate = E("restoreBackupTemplate");
+ this.restoreInsertionPoint = this.restoreTemplate.previousSibling;
+ this.restoreTemplate.parentNode.removeChild(this.restoreTemplate);
+ this.restoreTemplate.removeAttribute("id");
+ this.restoreTemplate.removeAttribute("hidden");
+ },
+
+ /**
+ * Gets the default download dir, as used by the browser itself.
+ */
+ getDefaultDir: function() /**nsIFile*/
+ {
+ try
+ {
+ return Utils.prefService.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
+ }
+ catch (e)
+ {
+ // No default download location. Default to desktop.
+ return FileUtils.getDir("Desk", [], false);
+ }
+ },
+
+ /**
+ * Saves new default download dir after the user chose a different directory to
+ * save his files to.
+ */
+ saveDefaultDir: function(/**nsIFile*/ dir)
+ {
+ try
+ {
+ Utils.prefService.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, dir);
+ } catch(e) {};
+ },
+
+ /**
+ * Called when the Restore menu is being opened, fills in "Automated backup"
+ * entries.
+ */
+ fillRestorePopup: function()
+ {
+ while (this.restoreInsertionPoint.nextSibling && !this.restoreInsertionPoint.nextSibling.id)
+ this.restoreInsertionPoint.parentNode.removeChild(this.restoreInsertionPoint.nextSibling);
+
+ let files = FilterStorage.getBackupFiles().reverse();
+ for (let i = 0; i < files.length; i++)
+ {
+ let file = files[i];
+ let item = this.restoreTemplate.cloneNode(true);
+ let label = item.getAttribute("label");
+ label = label.replace(/\?1\?/, Utils.formatTime(file.lastModifiedTime));
+ item.setAttribute("label", label);
+ item.addEventListener("command", function()
+ {
+ Backup.restoreAllData(file);
+ }, false);
+ this.restoreInsertionPoint.parentNode.insertBefore(item, this.restoreInsertionPoint.nextSibling);
+ }
+ },
+
+ /**
+ * Lets the user choose a file to restore filters from.
+ */
+ restoreFromFile: function()
+ {
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ picker.init(window, E("backupButton").getAttribute("_restoreDialogTitle"), picker.modeOpen);
+ picker.defaultExtension = ".ini";
+ picker.appendFilter(E("backupButton").getAttribute("_fileFilterComplete"), "*.ini");
+ picker.appendFilter(E("backupButton").getAttribute("_fileFilterCustom"), "*.txt");
+
+ if (picker.show() != picker.returnCancel)
+ {
+ this.saveDefaultDir(picker.file.parent);
+ if (picker.filterIndex == 0)
+ this.restoreAllData(picker.file);
+ else
+ this.restoreCustomFilters(picker.file);
+ }
+ },
+
+ /**
+ * Restores patterns.ini from a file.
+ */
+ restoreAllData: function(/**nsIFile*/ file)
+ {
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+ stream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ stream.QueryInterface(Ci.nsILineInputStream);
+
+ let lines = [];
+ let line = {value: null};
+ if (stream.readLine(line))
+ lines.push(line.value);
+ if (stream.readLine(line))
+ lines.push(line.value);
+ stream.close();
+
+ let match;
+ if (lines.length < 2 || lines[0] != "# Adblock Plus preferences" || !(match = /version=(\d+)/.exec(lines[1])))
+ {
+ Utils.alert(window, E("backupButton").getAttribute("_restoreError"), E("backupButton").getAttribute("_restoreDialogTitle"));
+ return;
+ }
+
+ let warning = E("backupButton").getAttribute("_restoreCompleteWarning");
+ let minVersion = parseInt(match[1], 10);
+ if (minVersion > FilterStorage.formatVersion)
+ warning += "\n\n" + E("backupButton").getAttribute("_restoreVersionWarning");
+
+ if (!Utils.confirm(window, warning, E("backupButton").getAttribute("_restoreDialogTitle")))
+ return;
+
+ FilterStorage.loadFromDisk(file);
+ },
+
+ /**
+ * Restores custom filters from a file.
+ */
+ restoreCustomFilters: function(/**nsIFile*/ file)
+ {
+ IO.readFromFile(file, true, {
+ seenHeader: false,
+ subscription: null,
+ process: function(line)
+ {
+ if (!this.seenHeader)
+ {
+ // This should be a header
+ this.seenHeader = true;
+ let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(line);
+ if (match)
+ {
+ let warning = E("backupButton").getAttribute("_restoreCustomWarning");
+ let minVersion = match[1];
+ if (minVersion && Utils.versionComparator.compare(minVersion, Utils.addonVersion) > 0)
+ warning += "\n\n" + E("backupButton").getAttribute("_restoreVersionWarning");
+
+ if (Utils.confirm(window, warning, E("backupButton").getAttribute("_restoreDialogTitle")))
+ {
+ let subscriptions = FilterStorage.subscriptions.filter(function(s) s instanceof SpecialSubscription);
+ for (let i = 0; i < subscriptions.length; i++)
+ FilterStorage.removeSubscription(subscriptions[i]);
+
+ return;
+ }
+ else
+ throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
+ }
+ else
+ throw new Error("Invalid file");
+ }
+ else if (line === null)
+ {
+ // End of file
+ if (this.subscription)
+ FilterStorage.addSubscription(this.subscription);
+ E("tabs").selectedIndex = 1;
+ }
+ else if (Backup.CHECKSUM_REGEXP.test(line))
+ {
+ // Ignore checksums
+ }
+ else if (Backup.GROUPTITLE_REGEXP.test(line))
+ {
+ // New group start
+ if (this.subscription)
+ FilterStorage.addSubscription(this.subscription);
+
+ let [, title, options] = Backup.GROUPTITLE_REGEXP.exec(line);
+ this.subscription = SpecialSubscription.create(title);
+
+ let defaults = [];
+ if (options)
+ options = options.split("/");
+ for (let j = 0; j < options.length; j++)
+ if (options[j] in SpecialSubscription.defaultsMap)
+ defaults.push(options[j]);
+ if (defaults.length)
+ this.subscription.defaults = defaults;
+ }
+ else
+ {
+ // Regular filter
+ let filter = Filter.fromText(Filter.normalize(line));
+ if (filter)
+ {
+ if (!this.subscription)
+ this.subscription = SpecialSubscription.create(Utils.getString("newGroup_title"));
+ this.subscription.filters.push(filter);
+ }
+ }
+ }
+ }, function(e)
+ {
+ if (e && e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK)
+ {
+ Cu.reportError(e);
+ Utils.alert(window, E("backupButton").getAttribute("_restoreError"), E("backupButton").getAttribute("_restoreDialogTitle"));
+ }
+ });
+ },
+
+ /**
+ * Lets the user choose a file to backup filters to.
+ */
+ backupToFile: function()
+ {
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ picker.init(window, E("backupButton").getAttribute("_backupDialogTitle"), picker.modeSave);
+ picker.defaultExtension = ".ini";
+ picker.appendFilter(E("backupButton").getAttribute("_fileFilterComplete"), "*.ini");
+ picker.appendFilter(E("backupButton").getAttribute("_fileFilterCustom"), "*.txt");
+
+ if (picker.show() != picker.returnCancel)
+ {
+ this.saveDefaultDir(picker.file.parent);
+ if (picker.filterIndex == 0)
+ this.backupAllData(picker.file);
+ else
+ this.backupCustomFilters(picker.file);
+ }
+ },
+
+ /**
+ * Writes all patterns.ini data to a file.
+ */
+ backupAllData: function(/**nsIFile*/ file)
+ {
+ FilterStorage.saveToDisk(file);
+ },
+
+ /**
+ * Writes user's custom filters to a file.
+ */
+ backupCustomFilters: function(/**nsIFile*/ file)
+ {
+ let subscriptions = FilterStorage.subscriptions.filter(function(s) s instanceof SpecialSubscription);
+ let list = ["[Adblock Plus 2.0]"];
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+ let typeAddition = "";
+ if (subscription.defaults)
+ typeAddition = "/" + subscription.defaults.join("/");
+ list.push("! [" + subscription.title + "]" + typeAddition);
+ for (let j = 0; j < subscription.filters.length; j++)
+ {
+ let filter = subscription.filters[j];
+ // Skip checksums
+ if (filter instanceof CommentFilter && this.CHECKSUM_REGEXP.test(filter.text))
+ continue;
+ // Skip group headers
+ if (filter instanceof CommentFilter && this.GROUPTITLE_REGEXP.test(filter.text))
+ continue;
+ list.push(filter.text);
+ }
+ }
+
+ // Insert checksum. Have to add an empty line to the end of the list to
+ // account for the trailing newline in the file.
+ list.push("");
+ let checksum = Utils.generateChecksum(list);
+ list.pop();
+ if (checksum)
+ list.splice(1, 0, "! Checksum: " + checksum);
+
+ function generator()
+ {
+ for (let i = 0; i < list.length; i++)
+ yield list[i];
+ }
+
+ IO.writeToFile(file, true, generator(), function(e)
+ {
+ if (e)
+ {
+ Cu.reportError(e);
+ Utils.alert(window, E("backupButton").getAttribute("_backupError"), E("backupButton").getAttribute("_backupDialogTitle"));
+ }
+ });
+ }
+};
+
+window.addEventListener("load", function()
+{
+ Backup.init();
+}, false);
diff --git a/abprime/content/filters-filteractions.js b/abprime/content/filters-filteractions.js
new file mode 100644
index 00000000..7dd5d6b4
--- /dev/null
+++ b/abprime/content/filters-filteractions.js
@@ -0,0 +1,539 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Implementation of the various actions performed on the filters.
+ * @class
+ */
+var FilterActions =
+{
+ /**
+ * Initializes filter actions.
+ */
+ init: function()
+ {
+ let me = this;
+ this.treeElement.parentNode.addEventListener("keypress", function(event)
+ {
+ me.keyPress(event);
+ }, true);
+ this.treeElement.view = FilterView;
+
+ this.treeElement.inputField.addEventListener("keypress", function(event)
+ {
+ // Prevent the tree from capturing cursor keys pressed in the input field
+ if (event.keyCode >= event.DOM_VK_PAGE_UP && event.keyCode <= event.DOM_VK_DOWN)
+ event.stopPropagation();
+ }, false);
+
+ // Create a copy of the view menu
+ function fixId(node, newId)
+ {
+ if (node.nodeType == node.ELEMENT_NODE)
+ {
+ if (node.hasAttribute("id"))
+ node.setAttribute("id", node.getAttribute("id").replace(/\d+$/, newId));
+
+ for (let i = 0, len = node.childNodes.length; i < len; i++)
+ fixId(node.childNodes[i], newId);
+ }
+ return node;
+ }
+ E("viewMenu").appendChild(fixId(E("filters-view-menu1").cloneNode(true), "2"));
+ },
+
+ /**
+ * element containing the filters.
+ * @type XULElement
+ */
+ get treeElement() E("filtersTree"),
+
+ /**
+ * Tests whether the tree is currently visible.
+ */
+ get visible()
+ {
+ return !this.treeElement.parentNode.collapsed;
+ },
+
+ /**
+ * Tests whether the tree is currently focused.
+ * @type Boolean
+ */
+ get focused()
+ {
+ let focused = document.commandDispatcher.focusedElement;
+ while (focused)
+ {
+ if ("treeBoxObject" in focused && focused.treeBoxObject == FilterView.boxObject)
+ return true;
+ focused = focused.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * Updates visible filter commands whenever the selected subscription changes.
+ */
+ updateCommands: function()
+ {
+ E("filters-add-command").setAttribute("disabled", !FilterView.editable);
+ },
+
+ /**
+ * Called whenever filter actions menu is opened, initializes menu items.
+ */
+ fillActionsPopup: function()
+ {
+ let editable = FilterView.editable;
+ let items = FilterView.selectedItems.filter(function(i) !i.filter.dummy);
+ items.sort(function(entry1, entry2) entry1.index - entry2.index);
+ let activeItems = items.filter(function(i) i.filter instanceof ActiveFilter);
+
+ E("filters-edit-command").setAttribute("disabled", !editable || !items.length);
+ E("filters-delete-command").setAttribute("disabled", !editable || !items.length);
+ E("filters-resetHitCounts-command").setAttribute("disabled", !activeItems.length);
+ E("filters-moveUp-command").setAttribute("disabled", !editable || FilterView.isSorted() || !items.length || items[0].index == 0);
+ E("filters-moveDown-command").setAttribute("disabled", !editable || FilterView.isSorted() || !items.length || items[items.length - 1].index == FilterView.rowCount - 1);
+ E("filters-copy-command").setAttribute("disabled", !items.length);
+ E("filters-cut-command").setAttribute("disabled", !editable || !items.length);
+ E("filters-paste-command").setAttribute("disabled", !editable || !Utils.clipboard.hasDataMatchingFlavors(["text/unicode"], 1, Utils.clipboard.kGlobalClipboard));
+ },
+
+ /**
+ * Changes sort current order for the tree. Sorts by filter column if the list is unsorted.
+ * @param {String} order either "ascending" or "descending"
+ */
+ setSortOrder: function(sortOrder)
+ {
+ let col = (FilterView.sortColumn ? FilterView.sortColumn.id : "col-filter");
+ FilterView.sortBy(col, sortOrder);
+ },
+
+ /**
+ * Toggles the visibility of a tree column.
+ */
+ toggleColumn: function(/**String*/ id)
+ {
+ let col = E(id);
+ col.setAttribute("hidden", col.hidden ? "false" : "true");
+ },
+
+ /**
+ * Enables or disables all filters in the current selection.
+ */
+ selectionToggleDisabled: function()
+ {
+ if (this.treeElement.editingColumn)
+ return;
+
+ let items = FilterView.selectedItems.filter(function(i) i.filter instanceof ActiveFilter);
+ if (items.length)
+ {
+ FilterView.boxObject.beginUpdateBatch();
+ let newValue = !items[0].filter.disabled;
+ for (let i = 0; i < items.length; i++)
+ items[i].filter.disabled = newValue;
+ FilterView.boxObject.endUpdateBatch();
+ }
+ },
+
+ /**
+ * Selects all entries in the list.
+ */
+ selectAll: function()
+ {
+ if (this.treeElement.editingColumn)
+ return;
+
+ FilterView.selection.selectAll();
+ this.treeElement.focus();
+ },
+
+ /**
+ * Starts editing the current filter.
+ */
+ startEditing: function()
+ {
+ if (this.treeElement.editingColumn)
+ return;
+
+ this.treeElement.startEditing(FilterView.selection.currentIndex, FilterView.boxObject.columns.getNamedColumn("col-filter"));
+ },
+
+ /**
+ * Starts editing a new filter at the current position.
+ */
+ insertFilter: function()
+ {
+ if (!FilterView.editable || this.treeElement.editingColumn)
+ return;
+
+ FilterView.insertEditDummy();
+ this.startEditing();
+
+ let tree = this.treeElement;
+ let listener = function(event)
+ {
+ if (event.attrName == "editing" && tree.editingRow < 0)
+ {
+ tree.removeEventListener("DOMAttrModified", listener, false);
+ FilterView.removeEditDummy();
+ }
+ }
+ tree.addEventListener("DOMAttrModified", listener, false);
+ },
+
+ /**
+ * Deletes items from the list.
+ */
+ deleteItems: function(/**Array*/ items)
+ {
+ let oldIndex = FilterView.selection.currentIndex;
+ items.sort(function(entry1, entry2) entry2.index - entry1.index);
+
+ for (let i = 0; i < items.length; i++)
+ FilterStorage.removeFilter(items[i].filter, FilterView._subscription, items[i].index);
+
+ FilterView.selectRow(oldIndex);
+ },
+
+ /**
+ * Deletes selected filters.
+ */
+ deleteSelected: function()
+ {
+ if (!FilterView.editable || this.treeElement.editingColumn)
+ return;
+
+ let items = FilterView.selectedItems;
+ if (items.length == 0 || (items.length >= 2 && !Utils.confirm(window, this.treeElement.getAttribute("_removewarning"))))
+ return;
+
+ this.deleteItems(items)
+ },
+
+ /**
+ * Resets hit counts of the selected filters.
+ */
+ resetHitCounts: function()
+ {
+ if (this.treeElement.editingColumn)
+ return;
+
+ let items = FilterView.selectedItems.filter(function(i) i.filter instanceof ActiveFilter);
+ if (items.length)
+ FilterStorage.resetHitCounts(items.map(function(i) i.filter));
+ },
+
+ /**
+ * Moves items to a different position in the list.
+ * @param {Array} items
+ * @param {Integer} offset negative offsets move the items up, positive down
+ */
+ _moveItems: function(/**Array*/ items, /**Integer*/ offset)
+ {
+ if (!items.length)
+ return;
+
+ if (offset < 0)
+ {
+ items.sort(function(entry1, entry2) entry1.index - entry2.index);
+ let position = items[0].index + offset;
+ if (position < 0)
+ return;
+
+ for (let i = 0; i < items.length; i++)
+ FilterStorage.moveFilter(items[i].filter, FilterView._subscription, items[i].index, position++);
+ FilterView.selection.rangedSelect(position - items.length, position - 1, false);
+ }
+ else if (offset > 0)
+ {
+ items.sort(function(entry1, entry2) entry2.index - entry1.index);
+ let position = items[0].index + offset;
+ if (position >= FilterView.rowCount)
+ return;
+
+ for (let i = 0; i < items.length; i++)
+ FilterStorage.moveFilter(items[i].filter, FilterView._subscription, items[i].index, position--);
+ FilterView.selection.rangedSelect(position + 1, position + items.length, false);
+ }
+ },
+
+ /**
+ * Moves selected filters one line up.
+ */
+ moveUp: function()
+ {
+ if (!FilterView.editable || FilterView.isEmpty || FilterView.isSorted() || this.treeElement.editingColumn)
+ return;
+
+ this._moveItems(FilterView.selectedItems, -1);
+ },
+
+ /**
+ * Moves selected filters one line down.
+ */
+ moveDown: function()
+ {
+ if (!FilterView.editable || FilterView.isEmpty || FilterView.isSorted() || this.treeElement.editingColumn)
+ return;
+
+ this._moveItems(FilterView.selectedItems, 1);
+ },
+
+ /**
+ * Fills the context menu of the filters columns.
+ */
+ fillColumnPopup: function(/**Element*/ element)
+ {
+ let suffix = element.id.match(/\d+$/)[0] || "1";
+
+ E("filters-view-filter" + suffix).setAttribute("checked", !E("col-filter").hidden);
+ E("filters-view-slow" + suffix).setAttribute("checked", !E("col-slow").hidden);
+ E("filters-view-enabled" + suffix).setAttribute("checked", !E("col-enabled").hidden);
+ E("filters-view-hitcount" + suffix).setAttribute("checked", !E("col-hitcount").hidden);
+ E("filters-view-lasthit" + suffix).setAttribute("checked", !E("col-lasthit").hidden);
+
+ let sortColumn = FilterView.sortColumn;
+ let sortColumnID = (sortColumn ? sortColumn.id : null);
+ let sortDir = (sortColumn ? sortColumn.getAttribute("sortDirection") : "natural");
+ E("filters-sort-none" + suffix).setAttribute("checked", sortColumn == null);
+ E("filters-sort-filter" + suffix).setAttribute("checked", sortColumnID == "col-filter");
+ E("filters-sort-enabled" + suffix).setAttribute("checked", sortColumnID == "col-enabled");
+ E("filters-sort-hitcount" + suffix).setAttribute("checked", sortColumnID == "col-hitcount");
+ E("filters-sort-lasthit" + suffix).setAttribute("checked", sortColumnID == "col-lasthit");
+ E("filters-sort-asc" + suffix).setAttribute("checked", sortDir == "ascending");
+ E("filters-sort-desc" + suffix).setAttribute("checked", sortDir == "descending");
+ },
+
+ /**
+ * Fills tooltip with the item data.
+ */
+ fillTooltip: function(event)
+ {
+ let item = FilterView.getItemAt(event.clientX, event.clientY);
+ if (!item || item.filter.dummy)
+ {
+ event.preventDefault();
+ return;
+ }
+
+ function setMultilineContent(box, text)
+ {
+ while (box.firstChild)
+ box.removeChild(box.firstChild);
+
+ for (var i = 0; i < text.length; i += 80)
+ {
+ var description = document.createElement("description");
+ description.setAttribute("value", text.substr(i, 80));
+ box.appendChild(description);
+ }
+ }
+
+ setMultilineContent(E("tooltip-filter"), item.filter.text);
+
+ E("tooltip-hitcount-row").hidden = !(item.filter instanceof ActiveFilter);
+ E("tooltip-lasthit-row").hidden = !(item.filter instanceof ActiveFilter) || !item.filter.lastHit;
+ if (item.filter instanceof ActiveFilter)
+ {
+ E("tooltip-hitcount").setAttribute("value", item.filter.hitCount)
+ E("tooltip-lasthit").setAttribute("value", Utils.formatTime(item.filter.lastHit))
+ }
+
+ E("tooltip-additional").hidden = false;
+ if (item.filter instanceof InvalidFilter && item.filter.reason)
+ E("tooltip-additional").textContent = item.filter.reason;
+ else if (item.filter instanceof RegExpFilter && defaultMatcher.isSlowFilter(item.filter))
+ E("tooltip-additional").textContent = Utils.getString("filter_regexp_tooltip");
+ else
+ E("tooltip-additional").hidden = true;
+ },
+
+ /**
+ * Called whenever a key is pressed on the list.
+ */
+ keyPress: function(/**Event*/ event)
+ {
+ if (event.target != E("filtersTree"))
+ return;
+
+ let modifiers = 0;
+ if (event.altKey)
+ modifiers |= SubscriptionActions._altMask;
+ if (event.ctrlKey)
+ modifiers |= SubscriptionActions._ctrlMask;
+ if (event.metaKey)
+ modifiers |= SubscriptionActions._metaMask;
+
+ if (event.charCode == " ".charCodeAt(0) && modifiers == 0 && !E("col-enabled").hidden)
+ this.selectionToggleDisabled();
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && modifiers == SubscriptionActions._accelMask)
+ {
+ E("filters-moveUp-command").doCommand();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN && modifiers == SubscriptionActions._accelMask)
+ {
+ E("filters-moveDown-command").doCommand();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Copies selected items to clipboard and optionally removes them from the
+ * list after that.
+ */
+ copySelected: function(/**Boolean*/ keep)
+ {
+ let items = FilterView.selectedItems;
+ if (!items.length)
+ return;
+
+ items.sort(function(entry1, entry2) entry1.index - entry2.index);
+ let text = items.map(function(i) i.filter.text).join(IO.lineBreak);
+ Utils.clipboardHelper.copyString(text);
+
+ if (!keep && FilterView.editable && !this.treeElement.editingColumn)
+ this.deleteItems(items);
+ },
+
+ /**
+ * Pastes text from clipboard as filters at the current position.
+ */
+ paste: function()
+ {
+ if (!FilterView.editable || this.treeElement.editingColumn)
+ return;
+
+ let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
+ transferable.addDataFlavor("text/unicode");
+
+ let data;
+ try
+ {
+ data = {};
+ Utils.clipboard.getData(transferable, Utils.clipboard.kGlobalClipboard);
+ transferable.getTransferData("text/unicode", data, {});
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ }
+ catch (e) {
+ return;
+ }
+
+ let item = FilterView.currentItem;
+ let position = (item ? item.index : FilterView.data.length);
+
+ let lines = data.replace(/\r/g, "").split("\n");
+ for (let i = 0; i < lines.length; i++)
+ {
+ let filter = Filter.fromText(lines[i]);
+ if (filter)
+ FilterStorage.addFilter(filter, FilterView._subscription, position++);
+ }
+ },
+
+ dragItems: null,
+
+ /**
+ * Called whenever the user starts a drag operation.
+ */
+ startDrag: function(/**Event*/ event)
+ {
+ let items = FilterView.selectedItems;
+ if (!items.length)
+ return;
+
+ items.sort(function(entry1, entry2) entry1.index - entry2.index);
+ event.dataTransfer.setData("text/plain", items.map(function(i) i.filter.text).join(IO.lineBreak));
+ this.dragItems = items;
+ event.stopPropagation();
+ },
+
+ /**
+ * Called to check whether moving the items to the given position is possible.
+ */
+ canDrop: function(/**Integer*/ newPosition, /**nsIDOMDataTransfer*/ dataTransfer)
+ {
+ if (!FilterView.editable || this.treeElement.editingColumn)
+ return false;
+
+ // If we aren't dragging items then maybe we got filters as plain text
+ if (!this.dragItems)
+ return dataTransfer && dataTransfer.getData("text/plain");
+
+ if (FilterView.isEmpty || FilterView.isSorted())
+ return false;
+
+ if (newPosition < this.dragItems[0].index)
+ return true;
+ else if (newPosition > this.dragItems[this.dragItems.length - 1].index + 1)
+ return true;
+ else
+ return false;
+ },
+
+ /**
+ * Called when the user decides to drop the items.
+ */
+ drop: function(/**Integer*/ newPosition, /**nsIDOMDataTransfer*/ dataTransfer)
+ {
+ if (!FilterView.editable || this.treeElement.editingColumn)
+ return;
+
+ if (!this.dragItems)
+ {
+ // We got filters as plain text, insert them into the list
+ let data = (dataTransfer ? dataTransfer.getData("text/plain") : null);
+ if (data)
+ {
+ let lines = data.replace(/\r/g, "").split("\n");
+ for (let i = 0; i < lines.length; i++)
+ {
+ let filter = Filter.fromText(lines[i]);
+ if (filter)
+ FilterStorage.addFilter(filter, FilterView._subscription, newPosition++);
+ }
+ }
+ return;
+ }
+
+ if (FilterView.isEmpty || FilterView.isSorted())
+ return;
+
+ if (newPosition < this.dragItems[0].index)
+ this._moveItems(this.dragItems, newPosition - this.dragItems[0].index);
+ else if (newPosition > this.dragItems[this.dragItems.length - 1].index + 1)
+ this._moveItems(this.dragItems, newPosition - this.dragItems[this.dragItems.length - 1].index - 1);
+ },
+
+ /**
+ * Called whenever the a drag operation finishes.
+ */
+ endDrag: function(/**Event*/ event)
+ {
+ this.dragItems = null;
+ },
+
+ /**
+ * Called if filters have been dragged into a subscription and need to be removed.
+ */
+ removeDraggedFilters: function()
+ {
+ if (!this.dragItems)
+ return;
+
+ this.deleteItems(this.dragItems);
+ }
+};
+
+window.addEventListener("load", function()
+{
+ FilterActions.init();
+}, false);
diff --git a/abprime/content/filters-filterview.js b/abprime/content/filters-filterview.js
new file mode 100644
index 00000000..e07e8b41
--- /dev/null
+++ b/abprime/content/filters-filterview.js
@@ -0,0 +1,820 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * nsITreeView implementation to display filters of a particular filter
+ * subscription.
+ * @class
+ */
+var FilterView =
+{
+ /**
+ * Initialization function.
+ */
+ init: function()
+ {
+ if (this.sortProcs)
+ return;
+
+ function compareText(/**Filter*/ filter1, /**Filter*/ filter2)
+ {
+ if (filter1.text < filter2.text)
+ return -1;
+ else if (filter1.text > filter2.text)
+ return 1;
+ else
+ return 0;
+ }
+ function compareSlow(/**Filter*/ filter1, /**Filter*/ filter2)
+ {
+ let isSlow1 = filter1 instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter1);
+ let isSlow2 = filter2 instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter2);
+ return isSlow1 - isSlow2;
+ }
+ function compareEnabled(/**Filter*/ filter1, /**Filter*/ filter2)
+ {
+ let hasEnabled1 = (filter1 instanceof ActiveFilter ? 1 : 0);
+ let hasEnabled2 = (filter2 instanceof ActiveFilter ? 1 : 0);
+ if (hasEnabled1 != hasEnabled2)
+ return hasEnabled1 - hasEnabled2;
+ else if (hasEnabled1)
+ return (filter2.disabled - filter1.disabled);
+ else
+ return 0;
+ }
+ function compareHitCount(/**Filter*/ filter1, /**Filter*/ filter2)
+ {
+ let hasHitCount1 = (filter1 instanceof ActiveFilter ? 1 : 0);
+ let hasHitCount2 = (filter2 instanceof ActiveFilter ? 1 : 0);
+ if (hasHitCount1 != hasHitCount2)
+ return hasHitCount1 - hasHitCount2;
+ else if (hasHitCount1)
+ return filter1.hitCount - filter2.hitCount;
+ else
+ return 0;
+ }
+ function compareLastHit(/**Filter*/ filter1, /**Filter*/ filter2)
+ {
+ let hasLastHit1 = (filter1 instanceof ActiveFilter ? 1 : 0);
+ let hasLastHit2 = (filter2 instanceof ActiveFilter ? 1 : 0);
+ if (hasLastHit1 != hasLastHit2)
+ return hasLastHit1 - hasLastHit2;
+ else if (hasLastHit1)
+ return filter1.lastHit - filter2.lastHit;
+ else
+ return 0;
+ }
+
+ /**
+ * Creates a sort function from a primary and a secondary comparison function.
+ * @param {Function} cmpFunc comparison function to be called first
+ * @param {Function} fallbackFunc (optional) comparison function to be called if primary function returns 0
+ * @param {Boolean} desc if true, the result of the primary function (not the secondary function) will be reversed - sorting in descending order
+ * @result {Function} comparison function to be used
+ */
+ function createSortFunction(cmpFunc, fallbackFunc, desc)
+ {
+ let factor = (desc ? -1 : 1);
+
+ return function(entry1, entry2)
+ {
+ // Comment replacements not bound to a filter always go last
+ let isLast1 = ("origFilter" in entry1 && entry1.filter == null);
+ let isLast2 = ("origFilter" in entry2 && entry2.filter == null);
+ if (isLast1)
+ return (isLast2 ? 0 : 1)
+ else if (isLast2)
+ return -1;
+
+ let ret = cmpFunc(entry1.filter, entry2.filter);
+ if (ret == 0 && fallbackFunc)
+ return fallbackFunc(entry1.filter, entry2.filter);
+ else
+ return factor * ret;
+ }
+ }
+
+ this.sortProcs = {
+ filter: createSortFunction(compareText, null, false),
+ filterDesc: createSortFunction(compareText, null, true),
+ slow: createSortFunction(compareSlow, compareText, true),
+ slowDesc: createSortFunction(compareSlow, compareText, false),
+ enabled: createSortFunction(compareEnabled, compareText, false),
+ enabledDesc: createSortFunction(compareEnabled, compareText, true),
+ hitcount: createSortFunction(compareHitCount, compareText, false),
+ hitcountDesc: createSortFunction(compareHitCount, compareText, true),
+ lasthit: createSortFunction(compareLastHit, compareText, false),
+ lasthitDesc: createSortFunction(compareLastHit, compareText, true)
+ };
+
+ let me = this;
+ let proxy = function()
+ {
+ return me._onChange.apply(me, arguments);
+ };
+ FilterNotifier.addListener(proxy);
+ window.addEventListener("unload", function()
+ {
+ FilterNotifier.removeListener(proxy);
+ }, false);
+ },
+
+ /**
+ * Filter change processing.
+ * @see FilterNotifier.addListener()
+ */
+ _onChange: function(action, item, param1, param2, param3)
+ {
+ switch (action)
+ {
+ case "subscription.updated":
+ {
+ if (item == this._subscription)
+ this.refresh(true);
+ break;
+ }
+ case "filter.disabled":
+ case "filter.hitCount":
+ case "filter.lastHit":
+ {
+ this.updateFilter(item);
+ break;
+ }
+ case "filter.added":
+ {
+ let subscription = param1;
+ let position = param2;
+ if (subscription == this._subscription)
+ this.addFilterAt(position, item);
+ break;
+ }
+ case "filter.removed":
+ {
+ let subscription = param1;
+ let position = param2;
+ if (subscription == this._subscription)
+ this.removeFilterAt(position);
+ break;
+ }
+ case "filter.moved":
+ {
+ let subscription = param1;
+ let oldPosition = param2;
+ let newPosition = param3;
+ if (subscription == this._subscription)
+ this.moveFilterAt(oldPosition, newPosition);
+ break;
+ }
+ }
+ },
+
+ /**
+ * Box object of the tree that this view is attached to.
+ * @type nsITreeBoxObject
+ */
+ boxObject: null,
+
+ /**
+ * Map of used cell properties to the corresponding nsIAtom representations.
+ */
+ atoms: null,
+
+ /**
+ * "Filter" to be displayed if no filter group is selected.
+ */
+ noGroupDummy: null,
+
+ /**
+ * "Filter" to be displayed if the selected group is empty.
+ */
+ noFiltersDummy: null,
+
+ /**
+ * "Filter" to be displayed for a new filter being edited.
+ */
+ editDummy: null,
+
+ /**
+ * Displayed list of filters, might be sorted.
+ * @type Filter[]
+ */
+ data: [],
+
+ /**
+ * element that the view is attached to.
+ * @type XULElement
+ */
+ get treeElement() this.boxObject ? this.boxObject.treeBody.parentNode : null,
+
+ /**
+ * Checks whether the list is currently empty (regardless of dummy entries).
+ * @type Boolean
+ */
+ get isEmpty()
+ {
+ return !this._subscription || !this._subscription.filters.length;
+ },
+
+ /**
+ * Checks whether the filters in the view can be changed.
+ * @type Boolean
+ */
+ get editable()
+ {
+ return (FilterView._subscription instanceof SpecialSubscription);
+ },
+
+ /**
+ * Returns current item of the list.
+ * @type Object
+ */
+ get currentItem()
+ {
+ let index = this.selection.currentIndex;
+ if (index >= 0 && index < this.data.length)
+ return this.data[index];
+ return null;
+ },
+
+ /**
+ * Returns items that are currently selected in the list.
+ * @type Object[]
+ */
+ get selectedItems()
+ {
+ let items = []
+ for (let i = 0; i < this.selection.getRangeCount(); i++)
+ {
+ let min = {};
+ let max = {};
+ this.selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; j++)
+ if (j >= 0 && j < this.data.length)
+ items.push(this.data[j]);
+ }
+ return items;
+ },
+
+ getItemAt: function(x, y)
+ {
+ let row = this.boxObject.getRowAt(x, y);
+ if (row >= 0 && row < this.data.length)
+ return this.data[row];
+ else
+ return null;
+ },
+
+ _subscription: 0,
+
+ /**
+ * Filter subscription being displayed.
+ * @type Subscription
+ */
+ get subscription() this._subscription,
+ set subscription(value)
+ {
+ if (value == this._subscription)
+ return;
+
+ // Make sure the editor is done before we update the list.
+ if (this.treeElement)
+ this.treeElement.stopEditing(true);
+
+ this._subscription = value;
+ this.refresh(true);
+ },
+
+ /**
+ * Will be true if updates are outstanding because the list was hidden.
+ */
+ _dirty: false,
+
+ /**
+ * Updates internal view data after a change.
+ * @param {Boolean} force if false, a refresh will only happen if previous
+ * changes were suppressed because the list was hidden
+ */
+ refresh: function(force)
+ {
+ if (FilterActions.visible)
+ {
+ if (!force && !this._dirty)
+ return;
+ this._dirty = false;
+ this.updateData();
+ this.selectRow(0);
+ }
+ else
+ this._dirty = true;
+ },
+
+ /**
+ * Map of comparison functions by column ID or column ID + "Desc" for
+ * descending sort order.
+ * @const
+ */
+ sortProcs: null,
+
+ /**
+ * Column that the list is currently sorted on.
+ * @type Element
+ */
+ sortColumn: null,
+
+ /**
+ * Sorting function currently in use.
+ * @type Function
+ */
+ sortProc: null,
+
+ /**
+ * Resorts the list.
+ * @param {String} col ID of the column to sort on. If null, the natural order is restored.
+ * @param {String} direction "ascending" or "descending", if null the sort order is toggled.
+ */
+ sortBy: function(col, direction)
+ {
+ let newSortColumn = null;
+ if (col)
+ {
+ newSortColumn = this.boxObject.columns.getNamedColumn(col).element;
+ if (!direction)
+ {
+ if (this.sortColumn == newSortColumn)
+ direction = (newSortColumn.getAttribute("sortDirection") == "ascending" ? "descending" : "ascending");
+ else
+ direction = "ascending";
+ }
+ }
+
+ if (this.sortColumn && this.sortColumn != newSortColumn)
+ this.sortColumn.removeAttribute("sortDirection");
+
+ this.sortColumn = newSortColumn;
+ if (this.sortColumn)
+ {
+ this.sortColumn.setAttribute("sortDirection", direction);
+ this.sortProc = this.sortProcs[col.replace(/^col-/, "") + (direction == "descending" ? "Desc" : "")];
+ }
+ else
+ this.sortProc = null;
+
+ if (this.data.length > 1)
+ {
+ this.updateData();
+ this.boxObject.invalidate();
+ }
+ },
+
+ /**
+ * Inserts dummy entry into the list if necessary.
+ */
+ addDummyRow: function()
+ {
+ if (this.boxObject && this.data.length == 0)
+ {
+ if (this._subscription)
+ this.data.splice(0, 0, this.noFiltersDummy);
+ else
+ this.data.splice(0, 0, this.noGroupDummy);
+ this.boxObject.rowCountChanged(0, 1);
+ }
+ },
+
+ /**
+ * Removes dummy entry from the list if present.
+ */
+ removeDummyRow: function()
+ {
+ if (this.boxObject && this.isEmpty && this.data.length)
+ {
+ this.data.splice(0, 1);
+ this.boxObject.rowCountChanged(0, -1);
+ }
+ },
+
+ /**
+ * Inserts dummy row when a new filter is being edited.
+ */
+ insertEditDummy: function()
+ {
+ FilterView.removeDummyRow();
+ let position = this.selection.currentIndex;
+ if (position >= this.data.length)
+ position = this.data.length - 1;
+ if (position < 0)
+ position = 0;
+
+ this.editDummy.index = (position < this.data.length ? this.data[position].index : this.data.length);
+ this.editDummy.position = position;
+ this.data.splice(position, 0, this.editDummy);
+ this.boxObject.rowCountChanged(position, 1);
+ this.selectRow(position);
+ },
+
+ /**
+ * Removes dummy row once the edit is finished.
+ */
+ removeEditDummy: function()
+ {
+ let position = this.editDummy.position;
+ if (typeof position != "undefined" && position < this.data.length && this.data[position] == this.editDummy)
+ {
+ this.data.splice(position, 1);
+ this.boxObject.rowCountChanged(position, -1);
+ FilterView.addDummyRow();
+
+ this.selectRow(position);
+ }
+ },
+
+ /**
+ * Selects a row in the tree and makes sure it is visible.
+ */
+ selectRow: function(row)
+ {
+ if (this.selection)
+ {
+ row = Math.min(Math.max(row, 0), this.data.length - 1);
+ this.selection.select(row);
+ this.boxObject.ensureRowIsVisible(row);
+ }
+ },
+
+ /**
+ * Finds a particular filter in the list and selects it.
+ */
+ selectFilter: function(/**Filter*/ filter)
+ {
+ let index = -1;
+ for (let i = 0; i < this.data.length; i++)
+ {
+ if (this.data[i].filter == filter)
+ {
+ index = i;
+ break;
+ }
+ }
+ if (index >= 0)
+ {
+ this.selectRow(index);
+ this.treeElement.focus();
+ }
+ },
+
+ /**
+ * Updates value of data property on sorting or filter subscription changes.
+ */
+ updateData: function()
+ {
+ let oldCount = this.rowCount;
+ if (this._subscription && this._subscription.filters.length)
+ {
+ this.data = this._subscription.filters.map(function(f, i) ({index: i, filter: f}));
+ if (this.sortProc)
+ {
+ // Hide comments in the list, they should be sorted like the filter following them
+ let followingFilter = null;
+ for (let i = this.data.length - 1; i >= 0; i--)
+ {
+ if (this.data[i].filter instanceof CommentFilter)
+ {
+ this.data[i].origFilter = this.data[i].filter;
+ this.data[i].filter = followingFilter;
+ }
+ else
+ followingFilter = this.data[i].filter;
+ }
+
+ this.data.sort(this.sortProc);
+
+ // Restore comments
+ for (let i = 0; i < this.data.length; i++)
+ {
+ if ("origFilter" in this.data[i])
+ {
+ this.data[i].filter = this.data[i].origFilter;
+ delete this.data[i].origFilter;
+ }
+ }
+ }
+ }
+ else
+ this.data = [];
+
+ if (this.boxObject)
+ {
+ this.boxObject.rowCountChanged(0, -oldCount);
+ this.boxObject.rowCountChanged(0, this.rowCount);
+ }
+
+ this.addDummyRow();
+ },
+
+ /**
+ * Called to update the view when a filter property is changed.
+ */
+ updateFilter: function(/**Filter*/ filter)
+ {
+ for (let i = 0; i < this.data.length; i++)
+ if (this.data[i].filter == filter)
+ this.boxObject.invalidateRow(i);
+ },
+
+ /**
+ * Called if a filter has been inserted at the specified position.
+ */
+ addFilterAt: function(/**Integer*/ position, /**Filter*/ filter)
+ {
+ if (this.data.length == 1 && this.data[0].filter.dummy)
+ {
+ this.data.splice(0, 1);
+ this.boxObject.rowCountChanged(0, -1);
+ }
+
+ if (this.sortProc)
+ {
+ this.updateData();
+ for (let i = 0; i < this.data.length; i++)
+ {
+ if (this.data[i].index == position)
+ {
+ position = i;
+ break;
+ }
+ }
+ }
+ else
+ {
+ for (let i = 0; i < this.data.length; i++)
+ if (this.data[i].index >= position)
+ this.data[i].index++;
+ this.data.splice(position, 0, {index: position, filter: filter});
+ }
+ this.boxObject.rowCountChanged(position, 1);
+ this.selectRow(position);
+ },
+
+ /**
+ * Called if a filter has been removed at the specified position.
+ */
+ removeFilterAt: function(/**Integer*/ position)
+ {
+ for (let i = 0; i < this.data.length; i++)
+ {
+ if (this.data[i].index == position)
+ {
+ this.data.splice(i, 1);
+ this.boxObject.rowCountChanged(i, -1);
+ i--;
+ }
+ else if (this.data[i].index > position)
+ this.data[i].index--;
+ }
+ this.addDummyRow();
+ },
+
+ /**
+ * Called if a filter has been moved within the list.
+ */
+ moveFilterAt: function(/**Integer*/ oldPosition, /**Integer*/ newPosition)
+ {
+ let dir = (oldPosition < newPosition ? 1 : -1);
+ for (let i = 0; i < this.data.length; i++)
+ {
+ if (this.data[i].index == oldPosition)
+ this.data[i].index = newPosition;
+ else if (dir * this.data[i].index > dir * oldPosition && dir * this.data[i].index <= dir * newPosition)
+ this.data[i].index -= dir;
+ }
+
+ if (!this.sortProc)
+ {
+ let item = this.data[oldPosition];
+ this.data.splice(oldPosition, 1);
+ this.data.splice(newPosition, 0, item);
+ this.boxObject.invalidateRange(Math.min(oldPosition, newPosition), Math.max(oldPosition, newPosition));
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
+
+ setTree: function(boxObject)
+ {
+ this.init();
+ this.boxObject = boxObject;
+
+ if (this.boxObject)
+ {
+ this.noGroupDummy = {index: 0, filter: {text: this.boxObject.treeBody.getAttribute("noGroupText"), dummy: true}};
+ this.noFiltersDummy = {index: 0, filter: {text: this.boxObject.treeBody.getAttribute("noFiltersText"), dummy: true}};
+ this.editDummy = {filter: {text: ""}};
+
+ let atomService = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
+ let stringAtoms = ["col-filter", "col-enabled", "col-hitcount", "col-lasthit", "type-comment", "type-filterlist", "type-whitelist", "type-elemhide", "type-invalid"];
+ let boolAtoms = ["selected", "dummy", "slow", "disabled"];
+
+ this.atoms = {};
+ for each (let atom in stringAtoms)
+ this.atoms[atom] = atomService.getAtom(atom);
+ for each (let atom in boolAtoms)
+ {
+ this.atoms[atom + "-true"] = atomService.getAtom(atom + "-true");
+ this.atoms[atom + "-false"] = atomService.getAtom(atom + "-false");
+ }
+
+ let columns = this.boxObject.columns;
+ for (let i = 0; i < columns.length; i++)
+ if (columns[i].element.hasAttribute("sortDirection"))
+ this.sortBy(columns[i].id, columns[i].element.getAttribute("sortDirection"));
+
+ this.refresh(true);
+ }
+ },
+
+ selection: null,
+
+ get rowCount() this.data.length,
+
+ getCellText: function(row, col)
+ {
+ if (row < 0 || row >= this.data.length)
+ return null;
+
+ col = col.id;
+ if (col != "col-filter" && col != "col-slow" && col != "col-hitcount" && col != "col-lasthit")
+ return null;
+
+ let filter = this.data[row].filter;
+ if (col == "col-filter")
+ return filter.text;
+ else if (col == "col-slow")
+ return (filter instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter) ? "!" : null);
+ else if (filter instanceof ActiveFilter)
+ {
+ if (col == "col-hitcount")
+ return filter.hitCount;
+ else if (col == "col-lasthit")
+ return (filter.lastHit ? Utils.formatTime(filter.lastHit) : null);
+ }
+
+ return null;
+ },
+
+ generateProperties: function(list, properties)
+ {
+ if (properties)
+ {
+ // Gecko 21 and below: we have an nsISupportsArray parameter, add atoms
+ // to that.
+ for (let i = 0; i < list.length; i++)
+ if (list[i] in this.atoms)
+ properties.AppendElement(this.atoms[list[i]]);
+ return null;
+ }
+ else
+ {
+ // Gecko 22+: no parameter, just return a string
+ return list.join(" ");
+ }
+ },
+
+ getColumnProperties: function(col, properties)
+ {
+ return this.generateProperties(["col-" + col.id], properties);
+ },
+
+ getRowProperties: function(row, properties)
+ {
+ if (row < 0 || row >= this.data.length)
+ return "";
+
+ let list = [];
+ let filter = this.data[row].filter;
+ list.push("selected-" + this.selection.isSelected(row));
+ list.push("slow-" + (filter instanceof RegExpFilter && defaultMatcher.isSlowFilter(filter)));
+ if (filter instanceof ActiveFilter)
+ list.push("disabled-" + filter.disabled);
+ list.push("dummy-" + ("dummy" in filter));
+
+ if (filter instanceof CommentFilter)
+ list.push("type-comment");
+ else if (filter instanceof BlockingFilter)
+ list.push("type-filterlist");
+ else if (filter instanceof WhitelistFilter)
+ list.push("type-whitelist");
+ else if (filter instanceof ElemHideFilter)
+ list.push("type-elemhide");
+ else if (filter instanceof InvalidFilter)
+ list.push("type-invalid");
+
+ return this.generateProperties(list, properties);
+ },
+
+ getCellProperties: function(row, col, properties)
+ {
+ return this.getRowProperties(row, properties) + " " + this.getColumnProperties(col, properties);
+ },
+
+ cycleHeader: function(col)
+ {
+ let oldDirection = col.element.getAttribute("sortDirection");
+ if (oldDirection == "ascending")
+ this.sortBy(col.id, "descending");
+ else if (oldDirection == "descending")
+ this.sortBy(null, null);
+ else
+ this.sortBy(col.id, "ascending");
+ },
+
+ isSorted: function()
+ {
+ return (this.sortProc != null);
+ },
+
+ canDrop: function(row, orientation, dataTransfer)
+ {
+ if (orientation == Ci.nsITreeView.DROP_ON || row < 0 || row >= this.data.length || !this.editable)
+ return false;
+
+ let item = this.data[row];
+ let position = (orientation == Ci.nsITreeView.DROP_BEFORE ? item.index : item.index + 1);
+ return FilterActions.canDrop(position, dataTransfer);
+ },
+
+ drop: function(row, orientation, dataTransfer)
+ {
+ if (orientation == Ci.nsITreeView.DROP_ON || row < 0 || row >= this.data.length || !this.editable)
+ return;
+
+ let item = this.data[row];
+ let position = (orientation == Ci.nsITreeView.DROP_BEFORE ? item.index : item.index + 1);
+ FilterActions.drop(position, dataTransfer);
+ },
+
+ isEditable: function(row, col)
+ {
+ if (row < 0 || row >= this.data.length || !this.editable)
+ return false;
+
+ let filter = this.data[row].filter;
+ if (col.id == "col-filter")
+ return !("dummy" in filter);
+ else
+ return false;
+ },
+
+ setCellText: function(row, col, value)
+ {
+ if (row < 0 || row >= this.data.length || col.id != "col-filter")
+ return;
+
+ let oldFilter = this.data[row].filter;
+ let position = this.data[row].index;
+ value = Filter.normalize(value);
+ if (!value || value == oldFilter.text)
+ return;
+
+ // Make sure we don't get called recursively (see https://adblockplus.org/forum/viewtopic.php?t=9003)
+ this.treeElement.stopEditing();
+
+ let newFilter = Filter.fromText(value);
+ if (this.data[row] == this.editDummy)
+ this.removeEditDummy();
+ else
+ FilterStorage.removeFilter(oldFilter, this._subscription, position);
+ FilterStorage.addFilter(newFilter, this._subscription, position);
+ },
+
+ cycleCell: function(row, col)
+ {
+ if (row < 0 || row >= this.data.length || col.id != "col-enabled")
+ return;
+
+ let filter = this.data[row].filter;
+ if (filter instanceof ActiveFilter)
+ filter.disabled = !filter.disabled;
+ },
+
+ isContainer: function(row) false,
+ isContainerOpen: function(row) false,
+ isContainerEmpty: function(row) true,
+ getLevel: function(row) 0,
+ getParentIndex: function(row) -1,
+ hasNextSibling: function(row, afterRow) false,
+ toggleOpenState: function(row) {},
+ getProgressMode: function() null,
+ getImageSrc: function() null,
+ isSeparator: function() false,
+ performAction: function() {},
+ performActionOnRow: function() {},
+ performActionOnCell: function() {},
+ getCellValue: function() null,
+ setCellValue: function() {},
+ selectionChanged: function() {},
+};
diff --git a/abprime/content/filters-search.js b/abprime/content/filters-search.js
new file mode 100644
index 00000000..dabc3208
--- /dev/null
+++ b/abprime/content/filters-search.js
@@ -0,0 +1,263 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Implementation of the filter search functionality.
+ * @class
+ */
+var FilterSearch =
+{
+ /**
+ * Initializes findbar widget.
+ */
+ init: function()
+ {
+ let filters = E("filtersTree");
+ for (let prop in FilterSearch.fakeBrowser)
+ filters[prop] = FilterSearch.fakeBrowser[prop];
+ Object.defineProperty(filters, "_lastSearchString", {
+ get: function()
+ {
+ return this.finder.searchString;
+ },
+ enumerable: true,
+ configurable: true
+ });
+
+ let findbar = E("findbar");
+ findbar.browser = filters;
+
+ findbar.addEventListener("keypress", function(event)
+ {
+ // Work-around for bug 490047
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN)
+ event.preventDefault();
+ }, false);
+
+ // Hack to prevent "highlight all" from getting enabled
+ findbar.toggleHighlight = function() {};
+ },
+
+ /**
+ * Performs a text search.
+ * @param {String} text text to be searched
+ * @param {Integer} direction search direction: -1 (backwards), 0 (forwards
+ * starting with current), 1 (forwards starting with next)
+ * @param {Boolean} caseSensitive if true, a case-sensitive search is performed
+ * @result {Integer} one of the nsITypeAheadFind constants
+ */
+ search: function(text, direction, caseSensitive)
+ {
+ function normalizeString(string) caseSensitive ? string : string.toLowerCase();
+
+ function findText(text, direction, startIndex)
+ {
+ let list = E("filtersTree");
+ let col = list.columns.getNamedColumn("col-filter");
+ let count = list.view.rowCount;
+ for (let i = startIndex + direction; i >= 0 && i < count; i += (direction || 1))
+ {
+ let filter = normalizeString(list.view.getCellText(i, col));
+ if (filter.indexOf(text) >= 0)
+ {
+ FilterView.selectRow(i);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ text = normalizeString(text);
+
+ // First try to find the entry in the current list
+ if (findText(text, direction, E("filtersTree").currentIndex))
+ return Ci.nsITypeAheadFind.FIND_FOUND;
+
+ // Now go through the other subscriptions
+ let result = Ci.nsITypeAheadFind.FIND_FOUND;
+ let subscriptions = FilterStorage.subscriptions.slice();
+ subscriptions.sort((s1, s2) => (s1 instanceof SpecialSubscription) - (s2 instanceof SpecialSubscription));
+ let current = subscriptions.indexOf(FilterView.subscription);
+ direction = direction || 1;
+ for (let i = current + direction; ; i+= direction)
+ {
+ if (i < 0)
+ {
+ i = subscriptions.length - 1;
+ result = Ci.nsITypeAheadFind.FIND_WRAPPED;
+ }
+ else if (i >= subscriptions.length)
+ {
+ i = 0;
+ result = Ci.nsITypeAheadFind.FIND_WRAPPED;
+ }
+ if (i == current)
+ break;
+
+ let subscription = subscriptions[i];
+ for (let j = 0; j < subscription.filters.length; j++)
+ {
+ let filter = normalizeString(subscription.filters[j].text);
+ if (filter.indexOf(text) >= 0)
+ {
+ let list = E(subscription instanceof SpecialSubscription ? "groups" : "subscriptions");
+ let node = Templater.getNodeForData(list, "subscription", subscription);
+ if (!node)
+ break;
+
+ // Select subscription in its list and restore focus after that
+ let oldFocus = document.commandDispatcher.focusedElement;
+ E("tabs").selectedIndex = (subscription instanceof SpecialSubscription ? 1 : 0);
+ list.ensureElementIsVisible(node);
+ list.selectItem(node);
+ if (oldFocus)
+ {
+ oldFocus.focus();
+ Utils.runAsync(() => oldFocus.focus());
+ }
+
+ Utils.runAsync(() => findText(text, direction, direction == 1 ? -1 : subscription.filters.length));
+ return result;
+ }
+ }
+ }
+
+ return Ci.nsITypeAheadFind.FIND_NOTFOUND;
+ }
+};
+
+/**
+ * Fake browser implementation to make findbar widget happy - searches in
+ * the filter list.
+ */
+FilterSearch.fakeBrowser =
+{
+ finder:
+ {
+ _resultListeners: [],
+ searchString: null,
+ caseSensitive: false,
+ lastResult: null,
+
+ _notifyResultListeners: function(result, findBackwards)
+ {
+ this.lastResult = result;
+ for (let listener of this._resultListeners)
+ {
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=958101, starting
+ // with Gecko 29 only one parameter is expected.
+ try
+ {
+ if (listener.onFindResult.length == 1)
+ {
+ listener.onFindResult({searchString: this.searchString,
+ result: result, findBackwards: findBackwards});
+ }
+ else
+ listener.onFindResult(result, findBackwards);
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ fastFind: function(searchString, linksOnly, drawOutline)
+ {
+ this.searchString = searchString;
+ let result = FilterSearch.search(this.searchString, 0,
+ this.caseSensitive);
+ this._notifyResultListeners(result, false);
+ },
+
+ findAgain: function(findBackwards, linksOnly, drawOutline)
+ {
+ let result = FilterSearch.search(this.searchString,
+ findBackwards ? -1 : 1,
+ this.caseSensitive);
+ this._notifyResultListeners(result, findBackwards);
+ },
+
+ addResultListener: function(listener)
+ {
+ if (this._resultListeners.indexOf(listener) === -1)
+ this._resultListeners.push(listener);
+ },
+
+ removeResultListener: function(listener)
+ {
+ let index = this._resultListeners.indexOf(listener);
+ if (index !== -1)
+ this._resultListeners.splice(index, 1);
+ },
+
+ // Irrelevant for us
+ requestMatchesCount: function(searchString, matchLimit, linksOnly) {},
+ highlight: function(highlight, word) {},
+ enableSelection: function() {},
+ removeSelection: function() {},
+ focusContent: function() {},
+ keyPress: function() {}
+ },
+
+ currentURI: Utils.makeURI("http://example.com/"),
+ contentWindow:
+ {
+ focus: function()
+ {
+ E("filtersTree").focus();
+ },
+ scrollByLines: function(num)
+ {
+ E("filtersTree").boxObject.scrollByLines(num);
+ },
+ scrollByPages: function(num)
+ {
+ E("filtersTree").boxObject.scrollByPages(num);
+ },
+ },
+
+ messageManager:
+ {
+ _messageMap: {
+ "Findbar:Mouseup": "mouseup",
+ "Findbar:Keypress": "keypress"
+ },
+
+ _messageFromEvent: function(event)
+ {
+ for (let message in this._messageMap)
+ if (this._messageMap[message] == event.type)
+ return {target: event.currentTarget, name: message, data: event};
+ return null;
+ },
+
+ addMessageListener: function(message, listener)
+ {
+ if (!this._messageMap.hasOwnProperty(message))
+ return;
+
+ if (!("_ABPHandler" in listener))
+ listener._ABPHandler = (event) => listener.receiveMessage(this._messageFromEvent(event));
+
+ E("filtersTree").addEventListener(this._messageMap[message], listener._ABPHandler, false);
+ },
+
+ removeMessageListener: function(message, listener)
+ {
+ if (this._messageMap.hasOwnProperty(message) && listener._ABPHandler)
+ E("filtersTree").removeEventListener(this._messageMap[message], listener._ABPHandler, false);
+ },
+
+ sendAsyncMessage: function() {}
+ }
+};
+
+window.addEventListener("load", function()
+{
+ FilterSearch.init();
+}, false);
diff --git a/abprime/content/filters-subscriptionactions.js b/abprime/content/filters-subscriptionactions.js
new file mode 100644
index 00000000..84a91736
--- /dev/null
+++ b/abprime/content/filters-subscriptionactions.js
@@ -0,0 +1,592 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Implemetation of the various actions that can be performed on subscriptions.
+ * @class
+ */
+var SubscriptionActions =
+{
+ /**
+ * Returns the subscription list currently having focus if any.
+ * @type Element
+ */
+ get focusedList()
+ {
+ return E("tabs").selectedPanel.getElementsByTagName("richlistbox")[0];
+ },
+
+ /**
+ * Returns the currently selected and focused subscription item if any.
+ * @type Element
+ */
+ get selectedItem()
+ {
+ let list = this.focusedList;
+ return (list ? list.selectedItem : null);
+ },
+
+ /**
+ * Finds the subscription for a particular filter, selects it and selects the
+ * filter.
+ */
+ selectFilter: function(/**Filter*/ filter)
+ {
+ let node = null;
+ let tabIndex = -1;
+ let subscriptions = filter.subscriptions.slice();
+ subscriptions.sort(function(s1, s2) s1.disabled - s2.disabled);
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+ let list = E(subscription instanceof SpecialSubscription ? "groups" : "subscriptions");
+ tabIndex = (subscription instanceof SpecialSubscription ? 1 : 0);
+ node = Templater.getNodeForData(list, "subscription", subscription);
+ if (node)
+ break;
+ }
+ if (node)
+ {
+ E("tabs").selectedIndex = tabIndex;
+ Utils.runAsync(function()
+ {
+ node.parentNode.ensureElementIsVisible(node);
+ node.parentNode.selectItem(node);
+ if (!FilterActions.visible)
+ E("subscription-showHideFilters-command").doCommand();
+ Utils.runAsync(FilterView.selectFilter, FilterView, filter);
+ });
+ }
+ },
+
+ /**
+ * Updates subscription commands whenever the selected subscription changes.
+ * Note: this method might be called with a wrong "this" value.
+ */
+ updateCommands: function()
+ {
+ let node = SubscriptionActions.selectedItem;
+ let data = Templater.getDataForNode(node);
+ let subscription = (data ? data.subscription : null)
+ E("subscription-editTitle-command").setAttribute("disabled", !subscription ||
+ subscription.fixedTitle);
+ E("subscription-update-command").setAttribute("disabled", !subscription ||
+ !(subscription instanceof DownloadableSubscription) ||
+ Synchronizer.isExecuting(subscription.url));
+ E("subscription-moveUp-command").setAttribute("disabled", !subscription ||
+ !node || !node.previousSibling || !!node.previousSibling.id);
+ E("subscription-moveDown-command").setAttribute("disabled", !subscription ||
+ !node || !node.nextSibling || !!node.nextSibling.id);
+ },
+
+ /**
+ * Starts title editing for the selected subscription.
+ */
+ editTitle: function()
+ {
+ let node = this.selectedItem;
+ if (node)
+ TitleEditor.start(node);
+ },
+
+ /**
+ * Triggers re-download of a filter subscription.
+ */
+ updateFilters: function(/**Node*/ node)
+ {
+ let data = Templater.getDataForNode(node || this.selectedItem);
+ if (data && data.subscription instanceof DownloadableSubscription)
+ Synchronizer.execute(data.subscription, true, true);
+ },
+
+ /**
+ * Triggers re-download of all filter subscriptions.
+ */
+ updateAllFilters: function()
+ {
+ for (let i = 0; i < FilterStorage.subscriptions.length; i++)
+ {
+ let subscription = FilterStorage.subscriptions[i];
+ if (subscription instanceof DownloadableSubscription)
+ Synchronizer.execute(subscription, true, true);
+ }
+ },
+
+ /**
+ * Sets Subscription.disabled field to a new value.
+ */
+ setDisabled: function(/**Element*/ node, /**Boolean*/ value)
+ {
+ let data = Templater.getDataForNode(node || this.selectedItem);
+ if (data)
+ data.subscription.disabled = value;
+ },
+
+ /**
+ * Enables all disabled filters in a subscription.
+ */
+ enableFilters: function(/**Element*/ node)
+ {
+ let data = Templater.getDataForNode(node);
+ if (!data)
+ return;
+
+ let filters = data.subscription.filters;
+ for (let i = 0, l = filters.length; i < l; i++)
+ if (filters[i] instanceof ActiveFilter && filters[i].disabled)
+ filters[i].disabled = false;
+ },
+
+ /**
+ * Removes a filter subscription from the list (after a warning).
+ */
+ remove: function(/**Node*/ node)
+ {
+ let data = Templater.getDataForNode(node || this.selectedItem);
+ if (data && Utils.confirm(window, Utils.getString(data.subscription instanceof SpecialSubscription ? "remove_group_warning" : "remove_subscription_warning")))
+ FilterStorage.removeSubscription(data.subscription);
+ },
+
+ /**
+ * Adds a new filter group and allows the user to change its title.
+ */
+ addGroup: function()
+ {
+ let subscription = SpecialSubscription.create();
+ FilterStorage.addSubscription(subscription);
+
+ let list = E("groups");
+ let node = Templater.getNodeForData(list, "subscription", subscription);
+ if (node)
+ {
+ list.focus();
+ list.ensureElementIsVisible(node);
+ list.selectedItem = node;
+ this.editTitle();
+ }
+ },
+
+ /**
+ * Moves a filter subscription one line up.
+ */
+ moveUp: function(/**Node*/ node)
+ {
+ node = Templater.getDataNode(node || this.selectedItem);
+ let data = Templater.getDataForNode(node);
+ if (!data)
+ return;
+
+ let previousData = Templater.getDataForNode(node.previousSibling);
+ if (!previousData)
+ return;
+
+ FilterStorage.moveSubscription(data.subscription, previousData.subscription);
+ },
+
+ /**
+ * Moves a filter subscription one line down.
+ */
+ moveDown: function(/**Node*/ node)
+ {
+ node = Templater.getDataNode(node || this.selectedItem);
+ let data = Templater.getDataForNode(node);
+ if (!data)
+ return;
+
+ let nextNode = node.nextSibling;
+ if (!Templater.getDataForNode(nextNode))
+ return;
+
+ let nextData = Templater.getDataForNode(nextNode.nextSibling);
+ FilterStorage.moveSubscription(data.subscription, nextData ? nextData.subscription : null);
+ },
+
+ /**
+ * Opens the context menu for a subscription node.
+ */
+ openMenu: function(/**Event*/ event, /**Node*/ node)
+ {
+ node.getElementsByClassName("actionMenu")[0].openPopupAtScreen(event.screenX, event.screenY, true);
+ },
+
+ _altMask: 2,
+ _ctrlMask: 4,
+ _metaMask: 8,
+ get _accelMask()
+ {
+ let result = this._ctrlMask;
+ try {
+ let accelKey = Utils.prefService.getIntPref("ui.key.accelKey");
+ if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META)
+ result = this._metaMask;
+ else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT)
+ result = this._altMask;
+ } catch(e) {}
+ this.__defineGetter__("_accelMask", function() result);
+ return result;
+ },
+
+ /**
+ * Called when a key is pressed on the subscription list.
+ */
+ keyPress: function(/**Event*/ event)
+ {
+ let modifiers = 0;
+ if (event.altKey)
+ modifiers |= this._altMask;
+ if (event.ctrlKey)
+ modifiers |= this._ctrlMask;
+ if (event.metaKey)
+ modifiers |= this._metaMask;
+
+ if (event.charCode == " ".charCodeAt(0) && modifiers == 0)
+ {
+ // Ignore if Space is pressed on a button
+ for (let node = event.target; node; node = node.parentNode)
+ if (node.localName == "button")
+ return;
+
+ let data = Templater.getDataForNode(this.selectedItem);
+ if (data)
+ data.subscription.disabled = !data.subscription.disabled;
+ }
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && modifiers == this._accelMask)
+ {
+ E("subscription-moveUp-command").doCommand();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ else if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN && modifiers == this._accelMask)
+ {
+ E("subscription-moveDown-command").doCommand();
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Subscription currently being dragged if any.
+ * @type Subscription
+ */
+ dragSubscription: null,
+
+ /**
+ * Called when a subscription entry is dragged.
+ */
+ startDrag: function(/**Event*/ event, /**Node*/ node)
+ {
+ let data = Templater.getDataForNode(node);
+ if (!data)
+ return;
+
+ event.dataTransfer.addElement(node);
+ event.dataTransfer.setData("text/x-moz-url", data.subscription.url);
+ event.dataTransfer.setData("text/plain", data.subscription.title);
+ this.dragSubscription = data.subscription;
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when something is dragged over a subscription entry or subscriptions list.
+ */
+ dragOver: function(/**Event*/ event)
+ {
+ // Don't allow dragging onto a scroll bar
+ for (let node = event.originalTarget; node; node = node.parentNode)
+ if (node.localName == "scrollbar")
+ return;
+
+ // Don't allow dragging onto element's borders
+ let target = event.originalTarget;
+ while (target && target.localName != "richlistitem")
+ target = target.parentNode;
+ if (!target)
+ target = event.originalTarget;
+
+ let styles = window.getComputedStyle(target, null);
+ let rect = target.getBoundingClientRect();
+ if (event.clientX < rect.left + parseInt(styles.borderLeftWidth, 10) ||
+ event.clientY < rect.top + parseInt(styles.borderTopWidth, 10) ||
+ event.clientX > rect.right - parseInt(styles.borderRightWidth, 10) - 1 ||
+ event.clientY > rect.bottom - parseInt(styles.borderBottomWidth, 10) - 1)
+ {
+ return;
+ }
+
+ // If not dragging a subscription check whether we can accept plain text
+ if (!this.dragSubscription)
+ {
+ let data = Templater.getDataForNode(event.target);
+ if (!data || !(data.subscription instanceof SpecialSubscription) || !event.dataTransfer.getData("text/plain"))
+ return;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when something is dropped on a subscription entry or subscriptions list.
+ */
+ drop: function(/**Event*/ event, /**Node*/ node)
+ {
+ if (!this.dragSubscription)
+ {
+ // Not dragging a subscription, maybe this is plain text that we can add as filters?
+ let data = Templater.getDataForNode(node);
+ if (data && data.subscription instanceof SpecialSubscription)
+ {
+ let lines = event.dataTransfer.getData("text/plain").replace(/\r/g, "").split("\n");
+ for (let i = 0; i < lines.length; i++)
+ {
+ let filter = Filter.fromText(lines[i]);
+ if (filter)
+ FilterStorage.addFilter(filter, data.subscription);
+ }
+ FilterActions.removeDraggedFilters();
+ event.stopPropagation();
+ }
+ return;
+ }
+
+ // When dragging down we need to insert after the drop node, otherwise before it.
+ node = Templater.getDataNode(node);
+ if (node)
+ {
+ let dragNode = Templater.getNodeForData(node.parentNode, "subscription", this.dragSubscription);
+ if (node.compareDocumentPosition(dragNode) & node.DOCUMENT_POSITION_PRECEDING)
+ node = node.nextSibling;
+ }
+
+ let data = Templater.getDataForNode(node);
+ FilterStorage.moveSubscription(this.dragSubscription, data ? data.subscription : null);
+ event.stopPropagation();
+ },
+
+ /**
+ * Called when the drag operation for a subscription is finished.
+ */
+ endDrag: function()
+ {
+ this.dragSubscription = null;
+ }
+};
+
+/**
+ * Subscription title editing functionality.
+ * @class
+ */
+var TitleEditor =
+{
+ /**
+ * List item corresponding with the currently edited subscription if any.
+ * @type Node
+ */
+ subscriptionEdited: null,
+
+ /**
+ * Starts editing of a subscription title.
+ * @param {Node} node subscription list entry or a child node
+ * @param {Boolean} [checkSelection] if true the editor will not start if the
+ * item was selected in the preceding mousedown event
+ */
+ start: function(node, checkSelection)
+ {
+ if (this.subscriptionEdited)
+ this.end(true);
+
+ let subscriptionNode = Templater.getDataNode(node);
+ if (!subscriptionNode || (checkSelection && !subscriptionNode._wasSelected))
+ return;
+
+ let subscription = Templater.getDataForNode(subscriptionNode).subscription;
+ if (!subscription || subscription.fixedTitle)
+ return;
+
+ subscriptionNode.getElementsByClassName("titleBox")[0].selectedIndex = 1;
+ let editor = subscriptionNode.getElementsByClassName("titleEditor")[0];
+ editor.value = subscription.title;
+ editor.setSelectionRange(0, editor.value.length);
+ this.subscriptionEdited = subscriptionNode;
+ editor.focus();
+ },
+
+ /**
+ * Stops editing of a subscription title.
+ * @param {Boolean} save if true the entered value will be saved, otherwise dismissed
+ */
+ end: function(save)
+ {
+ if (!this.subscriptionEdited)
+ return;
+
+ let subscriptionNode = this.subscriptionEdited;
+ this.subscriptionEdited = null;
+
+ let newTitle = null;
+ if (save)
+ {
+ newTitle = subscriptionNode.getElementsByClassName("titleEditor")[0].value;
+ newTitle = newTitle.replace(/^\s+/, "").replace(/\s+$/, "");
+ }
+
+ let subscription = Templater.getDataForNode(subscriptionNode).subscription
+ if (newTitle && newTitle != subscription.title)
+ subscription.title = newTitle;
+ else
+ {
+ subscriptionNode.getElementsByClassName("titleBox")[0].selectedIndex = 0;
+ subscriptionNode.parentNode.focus();
+ }
+ },
+
+ /**
+ * Processes keypress events on the subscription title editor field.
+ */
+ keyPress: function(/**Event*/ event)
+ {
+ // Prevent any key presses from triggering outside actions
+ event.stopPropagation();
+
+ if (event.keyCode == event.DOM_VK_RETURN || event.keyCode == event.DOM_VK_ENTER)
+ {
+ event.preventDefault();
+ this.end(true);
+ }
+ else if (event.keyCode == event.DOM_VK_CANCEL || event.keyCode == event.DOM_VK_ESCAPE)
+ {
+ event.preventDefault();
+ this.end(false);
+ }
+ }
+};
+
+/**
+ * Methods called when choosing and adding a new filter subscription.
+ * @class
+ */
+var SelectSubscription =
+{
+ /**
+ * Starts selection of a filter subscription to add.
+ */
+ start: function(/**Event*/ event)
+ {
+ let panel = E("selectSubscriptionPanel");
+ let list = E("selectSubscription");
+ let template = E("selectSubscriptionTemplate");
+ let parent = list.menupopup;
+
+ if (panel.state == "open")
+ {
+ list.focus();
+ return;
+ }
+
+ // Remove existing entries if any
+ while (parent.lastChild)
+ parent.removeChild(parent.lastChild);
+
+ // Load data
+ let request = new XMLHttpRequest();
+ request.open("GET", "subscriptions.xml");
+ request.onload = function()
+ {
+ // Avoid race condition if two downloads are started in parallel
+ if (panel.state == "open")
+ return;
+
+ // Add subscription entries to the list
+ let subscriptions = request.responseXML.getElementsByTagName("subscription");
+ let listedSubscriptions = [];
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+ let url = subscription.getAttribute("url");
+ if (!url || url in FilterStorage.knownSubscriptions)
+ continue;
+
+ let localePrefix = Utils.checkLocalePrefixMatch(subscription.getAttribute("prefixes"));
+ let node = Templater.process(template, {
+ __proto__: null,
+ node: subscription,
+ localePrefix: localePrefix
+ });
+ parent.appendChild(node);
+ listedSubscriptions.push(subscription);
+ }
+ let selectedNode = Utils.chooseFilterSubscription(listedSubscriptions);
+ list.selectedItem = Templater.getNodeForData(parent, "node", selectedNode) || parent.firstChild;
+
+ // Show panel and focus list
+ let position = "bottomcenter topleft";
+ panel.openPopup(E("selectSubscriptionButton"), position, 0, 0, false, false, event);
+ Utils.runAsync(list.focus, list);
+ };
+ request.send();
+ },
+
+ /**
+ * Adds filter subscription that is selected.
+ */
+ add: function()
+ {
+ E("selectSubscriptionPanel").hidePopup();
+
+ let data = Templater.getDataForNode(E("selectSubscription").selectedItem);
+ if (!data)
+ return;
+
+ let subscription = Subscription.fromURL(data.node.getAttribute("url"));
+ if (!subscription)
+ return;
+
+ FilterStorage.addSubscription(subscription);
+ subscription.disabled = false;
+ subscription.title = data.node.getAttribute("title");
+ subscription.homepage = data.node.getAttribute("homepage");
+
+ // Make sure the subscription is visible and selected
+ let list = E("subscriptions");
+ let node = Templater.getNodeForData(list, "subscription", subscription);
+ if (node)
+ {
+ list.ensureElementIsVisible(node);
+ list.selectedItem = node;
+ list.focus();
+ }
+
+ // Trigger download if necessary
+ if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
+ Synchronizer.execute(subscription);
+ },
+
+ /**
+ * Called if the user chooses to view the complete subscriptions list.
+ */
+ chooseOther: function()
+ {
+ E("selectSubscriptionPanel").hidePopup();
+ window.openDialog("subscriptionSelection.xul", "_blank", "chrome,centerscreen,modal,resizable,dialog=no", null, null);
+ },
+
+ /**
+ * Called for keys pressed on the subscription selection panel.
+ */
+ keyPress: function(/**Event*/ event)
+ {
+ // Buttons and text links handle Enter key themselves
+ if (event.target.localName == "button" || event.target.localName == "label")
+ return;
+
+ if (event.keyCode == event.DOM_VK_RETURN || event.keyCode == event.DOM_VK_ENTER)
+ {
+ // This shouldn't accept our dialog, only the panel
+ event.preventDefault();
+ E("selectSubscriptionAccept").doCommand();
+ }
+ }
+};
diff --git a/abprime/content/filters-subscriptionview.js b/abprime/content/filters-subscriptionview.js
new file mode 100644
index 00000000..bdcd50c3
--- /dev/null
+++ b/abprime/content/filters-subscriptionview.js
@@ -0,0 +1,288 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Fills a list of filter groups and keeps it updated.
+ * @param {Element} list richlistbox element to be filled
+ * @param {Node} template template to use for the groups
+ * @param {Function} filter filter to decide which lists should be included
+ * @param {Function} listener function to be called on changes
+ * @constructor
+ */
+function ListManager(list, template, filter, listener)
+{
+ this._list = list;
+ this._template = template;
+ this._filter = filter;
+ this._listener = listener || function(){};
+
+ this._deck = this._list.parentNode;
+
+ this._list.listManager = this;
+ this.reload();
+
+ let me = this;
+ let proxy = function()
+ {
+ return me._onChange.apply(me, arguments);
+ };
+ FilterNotifier.addListener(proxy);
+ window.addEventListener("unload", function()
+ {
+ FilterNotifier.removeListener(proxy);
+ }, false);
+}
+ListManager.prototype =
+{
+ /**
+ * List element being managed.
+ * @type Element
+ */
+ _list: null,
+ /**
+ * Template used for the groups.
+ * @type Node
+ */
+ _template: null,
+ /**
+ * Filter function to decide which subscriptions should be included.
+ * @type Function
+ */
+ _filter: null,
+ /**
+ * Function to be called whenever list contents change.
+ * @type Function
+ */
+ _listener: null,
+ /**
+ * Deck switching between list display and "no entries" message.
+ * @type Element
+ */
+ _deck: null,
+
+ /**
+ * Completely rebuilds the list.
+ */
+ reload: function()
+ {
+ // Remove existing entries if any
+ while (this._list.firstChild)
+ this._list.removeChild(this._list.firstChild);
+
+ // Now add all subscriptions
+ let subscriptions = FilterStorage.subscriptions.filter(this._filter, this);
+ if (subscriptions.length)
+ {
+ for each (let subscription in subscriptions)
+ this.addSubscription(subscription, null);
+
+ // Make sure first list item is selected after list initialization
+ Utils.runAsync(function()
+ {
+ this._list.selectItem(this._list.getItemAtIndex(this._list.getIndexOfFirstVisibleRow()));
+ }, this);
+ }
+
+ this._deck.selectedIndex = (subscriptions.length ? 1 : 0);
+ this._listener();
+ },
+
+ /**
+ * Adds a filter subscription to the list.
+ */
+ addSubscription: function(/**Subscription*/ subscription, /**Node*/ insertBefore) /**Node*/
+ {
+ let disabledFilters = 0;
+ for (let i = 0, l = subscription.filters.length; i < l; i++)
+ if (subscription.filters[i] instanceof ActiveFilter && subscription.filters[i].disabled)
+ disabledFilters++;
+
+ let node = Templater.process(this._template, {
+ __proto__: null,
+ subscription: subscription,
+ isExternal: subscription instanceof ExternalSubscription,
+ downloading: Synchronizer.isExecuting(subscription.url),
+ disabledFilters: disabledFilters
+ });
+ if (insertBefore)
+ this._list.insertBefore(node, insertBefore);
+ else
+ this._list.appendChild(node);
+ return node;
+ },
+
+ /**
+ * Map indicating subscriptions that need their "disabledFilters" property to
+ * be updated by next updateDisabled() call.
+ * @type Object
+ */
+ _scheduledUpdateDisabled: null,
+
+ /**
+ * Updates subscriptions that had some of their filters enabled/disabled.
+ */
+ updateDisabled: function()
+ {
+ let list = this._scheduledUpdateDisabled;
+ this._scheduledUpdateDisabled = null;
+ for (let url in list)
+ {
+ let subscription = Subscription.fromURL(url);
+ let subscriptionNode = Templater.getNodeForData(this._list, "subscription", subscription);
+ if (subscriptionNode)
+ {
+ let data = Templater.getDataForNode(subscriptionNode);
+ let disabledFilters = 0;
+ for (let i = 0, l = subscription.filters.length; i < l; i++)
+ if (subscription.filters[i] instanceof ActiveFilter && subscription.filters[i].disabled)
+ disabledFilters++;
+
+ if (disabledFilters != data.disabledFilters)
+ {
+ data.disabledFilters = disabledFilters;
+ Templater.update(this._template, subscriptionNode);
+
+ if (!document.commandDispatcher.focusedElement)
+ this._list.focus();
+ }
+ }
+ }
+ },
+
+ /**
+ * Subscriptions change processing.
+ * @see FilterNotifier.addListener()
+ */
+ _onChange: function(action, item, param1, param2)
+ {
+
+ if (action == "filter.disabled")
+ {
+ if (this._scheduledUpdateDisabled == null)
+ {
+ this._scheduledUpdateDisabled = {__proto__: null};
+ Utils.runAsync(this.updateDisabled, this);
+ }
+ for (let i = 0; i < item.subscriptions.length; i++)
+ this._scheduledUpdateDisabled[item.subscriptions[i].url] = true;
+ return;
+ }
+
+ if (action != "load" && !this._filter(item))
+ return;
+
+ switch (action)
+ {
+ case "load":
+ {
+ this.reload();
+ break;
+ }
+ case "subscription.added":
+ {
+ let index = FilterStorage.subscriptions.indexOf(item);
+ if (index >= 0)
+ {
+ let insertBefore = null;
+ for (index++; index < FilterStorage.subscriptions.length && !insertBefore; index++)
+ insertBefore = Templater.getNodeForData(this._list, "subscription", FilterStorage.subscriptions[index]);
+ this.addSubscription(item, insertBefore);
+ this._deck.selectedIndex = 1;
+ this._listener();
+ }
+ break;
+ }
+ case "subscription.removed":
+ {
+ let node = Templater.getNodeForData(this._list, "subscription", item);
+ if (node)
+ {
+ let newSelection = node.nextSibling || node.previousSibling;
+ node.parentNode.removeChild(node);
+ if (!this._list.firstChild)
+ {
+ this._deck.selectedIndex = 0;
+ this._list.selectedIndex = -1;
+ }
+ else if (newSelection)
+ {
+ this._list.ensureElementIsVisible(newSelection);
+ this._list.selectedItem = newSelection;
+ }
+ this._listener();
+ }
+ break
+ }
+ case "subscription.moved":
+ {
+ let node = Templater.getNodeForData(this._list, "subscription", item);
+ if (node)
+ {
+ node.parentNode.removeChild(node);
+ let insertBefore = null;
+ let index = FilterStorage.subscriptions.indexOf(item);
+ if (index >= 0)
+ for (index++; index < FilterStorage.subscriptions.length && !insertBefore; index++)
+ insertBefore = Templater.getNodeForData(this._list, "subscription", FilterStorage.subscriptions[index]);
+ this._list.insertBefore(node, insertBefore);
+ this._list.ensureElementIsVisible(node);
+ this._listener();
+ }
+ break;
+ }
+ case "subscription.title":
+ case "subscription.disabled":
+ case "subscription.homepage":
+ case "subscription.lastDownload":
+ case "subscription.downloadStatus":
+ {
+ let subscriptionNode = Templater.getNodeForData(this._list, "subscription", item);
+ if (subscriptionNode)
+ {
+ Templater.getDataForNode(subscriptionNode).downloading = Synchronizer.isExecuting(item.url);
+ Templater.update(this._template, subscriptionNode);
+
+ if (!document.commandDispatcher.focusedElement)
+ this._list.focus();
+ this._listener();
+ }
+ break;
+ }
+ case "subscription.fixedTitle":
+ {
+ SubscriptionActions.updateCommands();
+ break;
+ }
+ case "subscription.updated":
+ {
+ if (this._scheduledUpdateDisabled == null)
+ {
+ this._scheduledUpdateDisabled = {__proto__: null};
+ Utils.runAsync(this.updateDisabled, this);
+ }
+ this._scheduledUpdateDisabled[item.url] = true;
+ break;
+ }
+ }
+ }
+};
+
+/**
+ * Attaches list managers to the lists.
+ */
+ListManager.init = function()
+{
+ new ListManager(E("subscriptions"),
+ E("subscriptionTemplate"),
+ function(s) s instanceof RegularSubscription,
+ SubscriptionActions.updateCommands);
+ new ListManager(E("groups"),
+ E("groupTemplate"),
+ function(s) s instanceof SpecialSubscription,
+ SubscriptionActions.updateCommands);
+};
+
+window.addEventListener("load", ListManager.init, false);
diff --git a/abprime/content/filters.js b/abprime/content/filters.js
new file mode 100644
index 00000000..c535eba8
--- /dev/null
+++ b/abprime/content/filters.js
@@ -0,0 +1,210 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Initialization function, called when the window is loaded.
+ */
+function init()
+{
+ if (window.arguments && window.arguments.length)
+ {
+ let filter = window.arguments[0].wrappedJSObject;
+ if (filter instanceof Filter)
+ Utils.runAsync(SubscriptionActions.selectFilter, SubscriptionActions, filter);
+ }
+}
+
+/**
+ * Called whenever the currently selected tab changes.
+ */
+function onTabChange(/**Element*/ tabbox)
+{
+ updateSelectedSubscription();
+
+ Utils.runAsync(function()
+ {
+ let panel = tabbox.selectedPanel;
+ if (panel)
+ panel.getElementsByClassName("initialFocus")[0].focus();
+ SubscriptionActions.updateCommands();
+ });
+}
+
+/**
+ * Called whenever the selected subscription changes.
+ */
+function onSelectionChange(/**Element*/ list)
+{
+ SubscriptionActions.updateCommands();
+ updateSelectedSubscription();
+ list.focus();
+
+ // Take elements of the previously selected item out of the tab order
+ if ("previousSelection" in list && list.previousSelection)
+ {
+ let elements = list.previousSelection.getElementsByClassName("tabable");
+ for (let i = 0; i < elements.length; i++)
+ elements[i].setAttribute("tabindex", "-1");
+ }
+ // Put elements of the selected item into tab order
+ if (list.selectedItem)
+ {
+ let elements = list.selectedItem.getElementsByClassName("tabable");
+ for (let i = 0; i < elements.length; i++)
+ elements[i].removeAttribute("tabindex");
+ }
+ list.previousSelection = list.selectedItem;
+}
+
+/**
+ * Called when splitter state changes to make sure it is persisted properly.
+ */
+function onSplitterStateChange(/**Element*/ splitter)
+{
+ let state = splitter.getAttribute("state");
+ if (!state)
+ {
+ splitter.setAttribute("state", "open");
+ document.persist(splitter.id, "state");
+ }
+}
+
+/**
+ * Updates filter list when selected subscription changes.
+ */
+function updateSelectedSubscription()
+{
+ let panel = E("tabs").selectedPanel;
+ if (!panel)
+ return;
+
+ let list = panel.getElementsByTagName("richlistbox")[0];
+ if (!list)
+ return;
+
+ let data = Templater.getDataForNode(list.selectedItem);
+ FilterView.subscription = (data ? data.subscription : null);
+ FilterActions.updateCommands();
+}
+
+/**
+ * Template processing functions.
+ * @class
+ */
+var Templater =
+{
+ /**
+ * Processes a template node using given data object.
+ */
+ process: function(/**Node*/ template, /**Object*/ data) /**Node*/
+ {
+ // Use a sandbox to resolve attributes (for convenience, not security)
+ let sandbox = Cu.Sandbox(window);
+ for (let key in data)
+ sandbox[key] = data[key];
+ sandbox.formatTime = Utils.formatTime;
+
+ // Clone template but remove id/hidden attributes from it
+ let result = template.cloneNode(true);
+ result.removeAttribute("id");
+ result.removeAttribute("hidden");
+ result._data = data;
+
+ // Resolve any attributes of the for attr="{obj.foo}"
+ let conditionals = [];
+ let nodeIterator = document.createNodeIterator(result, NodeFilter.SHOW_ELEMENT, null, false);
+ for (let node = nodeIterator.nextNode(); node; node = nodeIterator.nextNode())
+ {
+ if (node.localName == "if")
+ conditionals.push(node);
+ for (let i = 0; i < node.attributes.length; i++)
+ {
+ let attribute = node.attributes[i];
+ let len = attribute.value.length;
+ if (len >= 2 && attribute.value[0] == "{" && attribute.value[len - 1] == "}")
+ attribute.value = Cu.evalInSandbox(attribute.value.substr(1, len - 2), sandbox);
+ }
+ }
+
+ // Process tags - remove if condition is false, replace by their children
+ // if it is true
+ for each (let node in conditionals)
+ {
+ let fragment = document.createDocumentFragment();
+ let condition = node.getAttribute("condition");
+ if (condition == "false")
+ condition = false;
+ for (let i = 0; i < node.childNodes.length; i++)
+ {
+ let child = node.childNodes[i];
+ if (child.localName == "elif" || child.localName == "else")
+ {
+ if (condition)
+ break;
+ condition = (child.localName == "elif" ? child.getAttribute("condition") : true);
+ if (condition == "false")
+ condition = false;
+ }
+ else if (condition)
+ fragment.appendChild(node.childNodes[i--]);
+ }
+ node.parentNode.replaceChild(fragment, node);
+ }
+
+ return result;
+ },
+
+ /**
+ * Updates first child of a processed template if the underlying data changed.
+ */
+ update: function(/**Node*/ template, /**Node*/ node)
+ {
+ if (!("_data" in node))
+ return;
+ let newChild = Templater.process(template.firstChild, node._data);
+ delete newChild._data;
+ node.replaceChild(newChild, node.firstChild);
+ },
+
+ /**
+ * Walks up the parent chain for a node until the node corresponding with a
+ * template is found.
+ */
+ getDataNode: function(/**Node*/ node) /**Node*/
+ {
+ while (node)
+ {
+ if ("_data" in node)
+ return node;
+ node = node.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the data used to generate the node from a template.
+ */
+ getDataForNode: function(/**Node*/ node) /**Object*/
+ {
+ node = Templater.getDataNode(node);
+ if (node)
+ return node._data;
+ else
+ return null;
+ },
+
+ /**
+ * Returns a node that has been generated from a template using a particular
+ * data object.
+ */
+ getNodeForData: function(/**Node*/ parent, /**String*/ property, /**Object*/ data) /**Node*/
+ {
+ for (let child = parent.firstChild; child; child = child.nextSibling)
+ if ("_data" in child && property in child._data && child._data[property] == data)
+ return child;
+ return null;
+ }
+};
diff --git a/abprime/content/filters.xul b/abprime/content/filters.xul
new file mode 100644
index 00000000..8600284f
--- /dev/null
+++ b/abprime/content/filters.xul
@@ -0,0 +1,395 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &subscription.minVersion.warning;
+
+ &subscription.disabledFilters.warning;
+
+
+
+
+
+
+ &noSubscriptions.text;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &noFilters.text;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/firstRun.js b/abprime/content/firstRun.js
new file mode 100644
index 00000000..1c8881a3
--- /dev/null
+++ b/abprime/content/firstRun.js
@@ -0,0 +1,98 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+function init()
+{
+
+ if (Utils.isFennec)
+ {
+ let topWnd = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if (topWnd.wrappedJSObject)
+ topWnd = topWnd.wrappedJSObject;
+
+ // window.close() closes the entire window (bug 642604), make sure to close
+ // only a single tab instead.
+ if ("BrowserUI" in topWnd)
+ {
+ window.close = function()
+ {
+ topWnd.BrowserUI.closeTab();
+ };
+ }
+ }
+
+ generateLinkText(E("changeDescription"));
+
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (subscription instanceof DownloadableSubscription && subscription.url)
+ {
+ E("listName").textContent = subscription.title;
+
+ let link = E("listHomepage");
+ link.setAttribute("_url", subscription.homepage);
+ link.setAttribute("tooltiptext", subscription.homepage);
+
+ E("listNameContainer").hidden = false;
+ E("listNone").hidden = true;
+ break;
+ }
+ }
+}
+
+function generateLinkText(element)
+{
+ let template = element.getAttribute("_textTemplate");
+
+ let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
+ while (element.firstChild && element.firstChild.nodeType != Node.ELEMENT_NODE)
+ element.removeChild(element.firstChild);
+ while (element.lastChild && element.lastChild.nodeType != Node.ELEMENT_NODE)
+ element.removeChild(element.lastChild);
+ if (!element.firstChild)
+ return;
+
+ element.firstChild.textContent = linkText;
+ element.insertBefore(document.createTextNode(beforeLink), element.firstChild);
+ element.appendChild(document.createTextNode(afterLink));
+}
+
+function openFilters()
+{
+ if (Utils.isFennec)
+ {
+ let topWnd = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if (topWnd.wrappedJSObject)
+ topWnd = topWnd.wrappedJSObject;
+
+ // window.close() closes the entire window (bug 642604), make sure to close
+ // only a single tab instead.
+ if ("BrowserUI" in topWnd)
+ {
+ topWnd.BrowserUI.showPanel("addons-container");
+ function showOptions()
+ {
+ if (!topWnd.ExtensionsView.getElementForAddon(Utils.addonID))
+ Utils.runAsync(showOptions);
+ else
+ topWnd.ExtensionsView.showOptions(Utils.addonID);
+ }
+ showOptions();
+ }
+ }
+ else
+ Utils.openFiltersDialog();
+}
diff --git a/abprime/content/firstRun.xul b/abprime/content/firstRun.xul
new file mode 100644
index 00000000..1ae098ef
--- /dev/null
+++ b/abprime/content/firstRun.xul
@@ -0,0 +1,43 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+ &confirmation;
+
+
+ &advancedSection;
+
+ &listSelection1;
+
+
+
+
+
+ &noList;
+
+
+
+
+
+
+
diff --git a/abprime/content/flasher.js b/abprime/content/flasher.js
new file mode 100644
index 00000000..4304a0b9
--- /dev/null
+++ b/abprime/content/flasher.js
@@ -0,0 +1,104 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Draws a blinking border for a list of matching nodes.
+ */
+
+var flasher = {
+ nodes: null,
+ count: 0,
+ timer: null,
+
+ flash: function(nodes)
+ {
+ this.stop();
+ if (nodes)
+ nodes = nodes.filter(function(node) node.nodeType == Node.ELEMENT_NODE);
+ if (!nodes || !nodes.length)
+ return;
+
+ if (Prefs.flash_scrolltoitem && nodes[0].ownerDocument)
+ {
+ // Ensure that at least one node is visible when flashing
+ let wnd = nodes[0].ownerDocument.defaultView;
+ try
+ {
+ let hooks = wnd.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .document.getElementById("abp-hooks");
+ if (hooks.wrappedJSObject)
+ hooks = hooks.wrappedJSObject;
+
+ let viewer = hooks.getBrowser().markupDocumentViewer;
+ viewer.scrollToNode(nodes[0]);
+ }
+ catch(e)
+ {
+ Cu.reportError(e);
+ }
+ }
+
+ this.nodes = nodes;
+ this.count = 0;
+
+ this.doFlash();
+ },
+
+ doFlash: function() {
+ if (this.count >= 12) {
+ this.stop();
+ return;
+ }
+
+ if (this.count % 2)
+ this.switchOff();
+ else
+ this.switchOn();
+
+ this.count++;
+
+ this.timer = window.setTimeout(function() {flasher.doFlash()}, 300);
+ },
+
+ stop: function() {
+ if (this.timer) {
+ window.clearTimeout(this.timer);
+ this.timer = null;
+ }
+
+ if (this.nodes) {
+ this.switchOff();
+ this.nodes = null;
+ }
+ },
+
+ setOutline: function(outline, offset)
+ {
+ for (var i = 0; i < this.nodes.length; i++)
+ {
+ if ("style" in this.nodes[i])
+ {
+ this.nodes[i].style.outline = outline;
+ this.nodes[i].style.outlineOffset = offset;
+ }
+ }
+ },
+
+ switchOn: function()
+ {
+ this.setOutline("#CC0000 dotted 2px", "-2px");
+ },
+
+ switchOff: function()
+ {
+ this.setOutline("", "");
+ }
+};
diff --git a/abprime/content/jar.mn b/abprime/content/jar.mn
new file mode 100644
index 00000000..ca65577b
--- /dev/null
+++ b/abprime/content/jar.mn
@@ -0,0 +1,53 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% content @ADDON_CHROME_NAME@ chrome/content/
+* content/about.js
+* content/about.xul
+ content/composer.js
+* content/composer.xul
+ content/errors.html
+ content/filters-backup.js
+ content/filters-filteractions.js
+ content/filters-filterview.js
+ content/filters-search.js
+ content/filters-subscriptionactions.js
+ content/filters-subscriptionview.js
+ content/filters.js
+* content/filters.xul
+ content/firstRun.js
+* content/firstRun.xul
+ content/flasher.js
+* content/mailOverlay.xul
+* content/navigatorOverlay.xul
+ content/objtabs.css
+* content/overlay.js
+* content/overlayGeneral.xul
+* content/phoenixOverlay.xul
+ content/progressBar.xml
+ content/sendReport.js
+* content/sendReport.xul
+* content/settings.xul
+* content/sidebar.js
+* content/sidebar.xul
+* content/sidebarDetached.xul
+ content/subscriptions.xml
+ content/subscriptionSelection.js
+* content/subscriptionSelection.xul
+* content/utils.js
+
+#ifdef ADDON_TARGET_BASILISK
+% overlay chrome://browser/content/browser.xul chrome://@ADDON_CHROME_NAME@/content/phoenixOverlay.xul application=@ADDON_TARGET_APP_ID@ application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+#else
+% overlay chrome://browser/content/browser.xul chrome://@ADDON_CHROME_NAME@/content/phoenixOverlay.xul application=@ADDON_TARGET_APP_ID@
+#endif
+
+% overlay chrome://navigator/content/navigator.xul chrome://@ADDON_CHROME_NAME@/content/navigatorOverlay.xul application={a3210b97-8e8a-4737-9aa0-aa0e607640b9}
+% overlay chrome://navigator/content/navigator.xul chrome://@ADDON_CHROME_NAME@/content/mailOverlay.xul application={3550f703-e582-4d05-9a08-453d09bdfdc6}
+
+# Hack to prevent .Net Framework Assistant from messing up the browser
+% override chrome://dotnetassistant/content/bootstrap.xul data:text/xml,
diff --git a/abprime/content/mailOverlay.xul b/abprime/content/mailOverlay.xul
new file mode 100644
index 00000000..da163499
--- /dev/null
+++ b/abprime/content/mailOverlay.xul
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/communicator/moz.build b/abprime/content/moz.build
similarity index 89%
rename from communicator/moz.build
rename to abprime/content/moz.build
index 9bc39c29..e0eb66aa 100644
--- a/communicator/moz.build
+++ b/abprime/content/moz.build
@@ -3,5 +3,4 @@
# 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/.
-
-
+JAR_MANIFESTS += ['jar.mn']
diff --git a/abprime/content/navigatorOverlay.xul b/abprime/content/navigatorOverlay.xul
new file mode 100644
index 00000000..76ebd65e
--- /dev/null
+++ b/abprime/content/navigatorOverlay.xul
@@ -0,0 +1,59 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/objtabs.css b/abprime/content/objtabs.css
new file mode 100644
index 00000000..03c1ed13
--- /dev/null
+++ b/abprime/content/objtabs.css
@@ -0,0 +1,71 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.w3.org/1999/xhtml");
+
+.%%CLASSVISIBLETOP%%, .%%CLASSVISIBLEBOTTOM%%, .%%CLASSHIDDEN%%
+{
+ position: fixed !important;
+ display: block !important;
+
+ width: auto !important;
+ height: auto !important;
+ right: auto !important;
+ bottom: auto !important;
+ z-index: 65535 !important;
+ float: left !important;
+ border-color: black !important;
+ border-style: solid !important;
+ background: white !important;
+ color: black !important;
+ cursor: pointer !important;
+ white-space: nowrap !important;
+ font-family: Arial,Helvetica,Sans-Serif !important;
+ font-size: 10px !important;
+ font-style: normal !important;
+ font-variant: normal !important;
+ font-weight: normal !important;
+ letter-spacing: normal !important;
+ line-height: normal !important;
+ text-align: center !important;
+ text-decoration: none !important;
+ text-indent: 0px !important;
+ text-transform: none !important;
+ direction: ltr !important;
+ padding: 0px 5px !important;
+ -moz-binding: none !important;
+ -moz-user-focus: none !important;
+ -moz-user-input: none !important;
+ -moz-user-select: none !important;
+}
+
+.%%CLASSVISIBLETOP%%, .%%CLASSHIDDEN%%
+{
+ border-width: 1px 1px 0px 1px !important;
+ border-top-left-radius: 10px !important;
+ border-top-right-radius: 10px !important;
+ border-bottom-left-radius: 0px !important;
+ border-bottom-right-radius: 0px !important;
+}
+
+.%%CLASSVISIBLEBOTTOM%%
+{
+ border-width: 0px 1px 1px 1px !important;
+ border-top-left-radius: 0px !important;
+ border-top-right-radius: 0px !important;
+ border-bottom-left-radius: 10px !important;
+ border-bottom-right-radius: 10px !important;
+}
+
+.%%CLASSVISIBLETOP%%, .%%CLASSVISIBLEBOTTOM%%
+{
+ visibility: visible !important;
+}
+
+.%%CLASSHIDDEN%%
+{
+ visibility: hidden !important;
+}
diff --git a/abprime/content/overlay.js b/abprime/content/overlay.js
new file mode 100644
index 00000000..099740f5
--- /dev/null
+++ b/abprime/content/overlay.js
@@ -0,0 +1,30 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+{
+ let Cc = Components.classes;
+ let Ci = Components.interfaces;
+ let Cr = Components.results;
+ let Cu = Components.utils;
+
+ // Use UIReady event to initialize in Fennec (bug 531071)
+ let eventName = Cu.import("resource://@ADDON_CHROME_NAME@/modules/Utils.jsm", null).Utils.isFennec ? "UIReady" : "load";
+
+ window.addEventListener(eventName, function()
+ {
+ window.removeEventListener(eventName, arguments.callee, false);
+
+ if (!("@adblockplus.org/abp/public;1" in Cc))
+ {
+ // Force initialization (in Fennec we won't be initialized at this point)
+ Cu.import("resource://@ADDON_CHROME_NAME@/modules/Bootstrap.jsm", null).Bootstrap.startup();
+ }
+
+ Cu.import("resource://@ADDON_CHROME_NAME@/modules/AppIntegration.jsm", null).AppIntegration.addWindow(window);
+ }, false);
+}
diff --git a/abprime/content/overlayGeneral.xul b/abprime/content/overlayGeneral.xul
new file mode 100644
index 00000000..377fb666
--- /dev/null
+++ b/abprime/content/overlayGeneral.xul
@@ -0,0 +1,97 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/phoenixOverlay.xul b/abprime/content/phoenixOverlay.xul
new file mode 100644
index 00000000..95045a2c
--- /dev/null
+++ b/abprime/content/phoenixOverlay.xul
@@ -0,0 +1,56 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/progressBar.xml b/abprime/content/progressBar.xml
new file mode 100644
index 00000000..c1842424
--- /dev/null
+++ b/abprime/content/progressBar.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+ 5
+ document.getAnonymousElementByAttribute(this, "anonid", "canvas")
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/sendReport.js b/abprime/content/sendReport.js
new file mode 100644
index 00000000..5ce79c31
--- /dev/null
+++ b/abprime/content/sendReport.js
@@ -0,0 +1,1467 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+//
+// Report data template, more data will be added during data collection
+//
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+const MILLISECONDS_IN_SECOND = 1000;
+const SECONDS_IN_MINUTE = 60;
+const SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE;
+const SECONDS_IN_DAY = 24 * SECONDS_IN_HOUR;
+
+let reportData =
+
+
+
+
+
+ {Prefs.enabled}
+ {Prefs.frameobjects}
+ {!Prefs.fastcollapse}
+ {Prefs.privateBrowsing}
+ {Prefs.subscriptions_autoupdate}
+
+
+
+
+
+
+ ;
+
+//
+// Data collectors
+//
+
+let reportsListDataSource =
+{
+ list: [],
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ let data = null;
+ try
+ {
+ data = JSON.parse(Prefs.recentReports);
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+
+ if (data && "length" in data)
+ {
+ for (let i = 0; i < data.length; i++)
+ {
+ let entry = data[i];
+ if (typeof entry.reportURL == "string" && entry.reportURL &&
+ typeof entry.time == "number" && Date.now() - entry.time < 30*24*60*60*1000)
+ {
+ let newEntry = {site: null, reportURL: entry.reportURL, time: entry.time};
+ if (typeof entry.site == "string" && entry.site)
+ newEntry.site = entry.site;
+ this.list.push(newEntry);
+ }
+ }
+ }
+
+ if (this.list.length > 10)
+ this.list.splice(10);
+
+ E("recentReports").hidden = !this.list.length;
+ if (this.list.length)
+ {
+ let rows = E("recentReportsRows")
+ for (let i = 0; i < this.list.length; i++)
+ {
+ let entry = this.list[i];
+ let row = document.createElement("row");
+
+ let link = document.createElement("description");
+ link.setAttribute("class", "text-link");
+ link.setAttribute("url", entry.reportURL);
+ link.textContent = entry.reportURL.replace(/^.*\/(?=[^\/])/, "");
+ row.appendChild(link);
+
+ let site = document.createElement("description");
+ if (entry.site)
+ site.textContent = entry.site;
+ row.appendChild(site);
+
+ let time = document.createElement("description");
+ time.textContent = Utils.formatTime(entry.time);
+ row.appendChild(time);
+
+ rows.appendChild(row);
+ }
+ }
+
+ callback();
+ },
+
+ addReport: function(site, reportURL)
+ {
+ this.list.unshift({site: site, reportURL: reportURL, time: Date.now()});
+ try
+ {
+ Prefs.recentReports = JSON.stringify(this.list);
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ },
+
+ clear: function()
+ {
+ this.list = [];
+ Prefs.recentReports = JSON.stringify(this.list);
+ E("recentReports").hidden = true;
+ },
+
+ handleClick: function(event)
+ {
+ if (event.button != 0 || !event.target || !event.target.hasAttribute("url"))
+ return;
+
+ Utils.loadInBrowser(event.target.getAttribute("url"));
+ }
+};
+
+let requestsDataSource =
+{
+ requests: reportData.requests,
+ origRequests: [],
+ requestNotifier: null,
+ callback: null,
+ nodeByKey: {__proto__: null},
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ this.callback = callback;
+ this.requestNotifier = new RequestNotifier(wnd, this.onRequestFound, this);
+ },
+
+ onRequestFound: function(frame, node, entry, scanComplete)
+ {
+ if (entry)
+ {
+ let key = entry.location + " " + entry.typeDescr + " " + entry.docDomain;
+ let requestXML
+ if (key in this.nodeByKey)
+ {
+ requestXML = this.nodeByKey[key];
+ requestXML.@count = parseInt(requestXML.@count, 10) + 1;
+ }
+ else
+ {
+ requestXML = ;
+ this.nodeByKey[key] = requestXML;
+ this.requests.appendChild(requestXML);
+ }
+
+ // Location is meaningless for element hiding hits
+ if (entry.filter && entry.filter instanceof ElemHideFilter)
+ delete requestXML.@location;
+
+ if (entry.filter)
+ requestXML.@filter = entry.filter.text;
+
+ if (node instanceof Element)
+ {
+ requestXML.@node = node.localName;
+ if (node.namespaceURI)
+ requestXML.@node = node.namespaceURI + "#" + requestXML.@node;
+
+ try
+ {
+ requestXML.@size = node.offsetWidth + "x" + node.offsetHeight;
+ } catch(e) {}
+ }
+ this.origRequests.push(entry);
+ }
+
+ if (scanComplete)
+ {
+ this.requestNotifier.shutdown();
+ this.requestNotifier = null;
+ this.callback();
+ }
+ }
+};
+
+let filtersDataSource =
+{
+ origFilters: [],
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ let wndStats = RequestNotifier.getWindowStatistics(wnd);
+ if (wndStats)
+ {
+ let filters = reportData.filters;
+ for (let f in wndStats.filters)
+ {
+ let filter = Filter.fromText(f)
+ let hitCount = wndStats.filters[f];
+ filters.appendChild();
+ this.origFilters.push(filter);
+ }
+ }
+ callback();
+ }
+};
+
+let subscriptionsDataSource =
+{
+ subscriptionFilter: function(s)
+ {
+ if (s.disabled || !(s instanceof RegularSubscription))
+ return false;
+ if (s instanceof DownloadableSubscription && !/^(http|https|ftp):/i.test(s.url))
+ return false;
+ return true;
+ },
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ let subscriptions = reportData.subscriptions;
+ let now = Math.round(Date.now() / 1000);
+ for (let i = 0; i < FilterStorage.subscriptions.length; i++)
+ {
+ let subscription = FilterStorage.subscriptions[i];
+ if (!this.subscriptionFilter(subscription))
+ continue;
+
+ let subscriptionXML = ;
+ if (subscription.lastDownload)
+ subscriptionXML.@lastDownloadAttempt = subscription.lastDownload - now;
+ if (subscription instanceof DownloadableSubscription)
+ {
+ if (subscription.lastSuccess)
+ subscriptionXML.@lastDownloadSuccess = subscription.lastSuccess - now;
+ if (subscription.softExpiration)
+ subscriptionXML.@softExpiration = subscription.softExpiration - now;
+ if (subscription.expires)
+ subscriptionXML.@hardExpiration = subscription.expires - now;
+ subscriptionXML.@downloadStatus = subscription.downloadStatus;
+ }
+ subscriptions.appendChild(subscriptionXML);
+ }
+ callback();
+ }
+};
+
+let screenshotDataSource =
+{
+ imageOffset: 10,
+
+ // Fields used for color reduction
+ _mapping: [0x00, 0x55, 0xAA, 0xFF],
+ _i: null,
+ _max: null,
+ _pixelData: null,
+ _callback: null,
+
+ // Fields used for user interaction
+ _enabled: true,
+ _canvas: null,
+ _context: null,
+ _selectionType: "mark",
+ _currentData: null,
+ _undoQueue: [],
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ this._callback = callback;
+ this._canvas = E("screenshotCanvas");
+ this._canvas.width = this._canvas.offsetWidth;
+
+ // Do not resize canvas any more (no idea why Gecko requires both to be set)
+ this._canvas.parentNode.style.MozBoxAlign = "center";
+ this._canvas.parentNode.align = "center";
+
+ this._context = this._canvas.getContext("2d");
+ let wndWidth = wnd.document.documentElement.scrollWidth;
+ let wndHeight = wnd.document.documentElement.scrollHeight;
+
+ // Copy scaled screenshot of the webpage. We scale the webpage by width
+ // but leave 10px on each side for easier selecting.
+
+ // Gecko doesn't like sizes more than 64k, restrict to 30k to be on the safe side.
+ // Also, make sure height is at most five times the width to keep image size down.
+ let copyWidth = Math.min(wndWidth, 30000);
+ let copyHeight = Math.min(wndHeight, 30000, copyWidth * 5);
+ let copyX = Math.max(Math.min(wnd.scrollX - copyWidth / 2, wndWidth - copyWidth), 0);
+ let copyY = Math.max(Math.min(wnd.scrollY - copyHeight / 2, wndHeight - copyHeight), 0);
+
+ let scalingFactor = (this._canvas.width - this.imageOffset * 2) / copyWidth;
+ this._canvas.height = copyHeight * scalingFactor + this.imageOffset * 2;
+
+ this._context.save();
+ this._context.translate(this.imageOffset, this.imageOffset);
+ this._context.scale(scalingFactor, scalingFactor);
+ this._context.drawWindow(wnd, copyX, copyY, copyWidth, copyHeight, "rgb(255,255,255)");
+ this._context.restore();
+
+ // Init canvas settings
+ this._context.fillStyle = "rgb(0, 0, 0)";
+ this._context.strokeStyle = "rgba(255, 0, 0, 0.7)";
+ this._context.lineWidth = 3;
+ this._context.lineJoin = "round";
+
+ // Reduce colors asynchronously
+ this._pixelData = this._context.getImageData(this.imageOffset, this.imageOffset,
+ this._canvas.width - this.imageOffset * 2,
+ this._canvas.height - this.imageOffset * 2);
+ this._max = this._pixelData.width * this._pixelData.height * 4;
+ this._i = 0;
+ Utils.threadManager.currentThread.dispatch(this, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ },
+
+ run: function()
+ {
+ // Process only 5000 bytes at a time to prevent browser hangs
+ let endIndex = Math.min(this._i + 5000, this._max);
+ let i = this._i;
+ for (; i < endIndex; i++)
+ this._pixelData.data[i] = this._mapping[this._pixelData.data[i] >> 6];
+
+ if (i >= this._max)
+ {
+ // Save data back and we are done
+ this._context.putImageData(this._pixelData, this.imageOffset, this.imageOffset);
+ this._callback();
+ }
+ else
+ {
+ this._i = i;
+ Utils.threadManager.currentThread.dispatch(this, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ }
+ },
+
+ get enabled() this._enabled,
+ set enabled(enabled)
+ {
+ if (this._enabled == enabled)
+ return;
+
+ this._enabled = enabled;
+ this._canvas.style.opacity = this._enabled ? "" : "0.3"
+ E("screenshotMarkButton").disabled = !this._enabled;
+ E("screenshotRemoveButton").disabled = !this._enabled;
+ E("screenshotUndoButton").disabled = !this._enabled || !this._undoQueue.length;
+ },
+
+ get selectionType() this._selectionType,
+ set selectionType(type)
+ {
+ if (this._selectionType == type)
+ return;
+
+ // Abort selection already in progress
+ this.abortSelection();
+
+ this._selectionType = type;
+ },
+
+ exportData: function()
+ {
+ if (this.enabled)
+ {
+ reportData.screenshot = this._canvas.toDataURL();
+ reportData.screenshot.@edited = (this._undoQueue.length ? 'true' : 'false');
+ }
+ else
+ delete reportData.screenshot;
+ },
+
+ abortSelection: function()
+ {
+ if (this._currentData && this._currentData.data)
+ {
+ this._context.putImageData(this._currentData.data,
+ Math.min(this._currentData.anchorX, this._currentData.currentX),
+ Math.min(this._currentData.anchorY, this._currentData.currentY));
+ }
+ document.removeEventListener("keypress", this.handleKeyPress, true);
+ this._currentData = null;
+ },
+
+ handleKeyPress: function(event)
+ {
+ if (event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE)
+ {
+ event.stopPropagation();
+ event.preventDefault();
+ screenshotDataSource.abortSelection();
+ }
+ },
+
+ startSelection: function(event)
+ {
+ if (event.button == 2)
+ this.abortSelection(); // Right mouse button aborts selection
+
+ if (event.button != 0 || !this.enabled)
+ return;
+
+ // Abort selection already in progress
+ this.abortSelection();
+
+ let boxObject = document.getBoxObjectFor(this._canvas);
+ let [x, y] = [event.screenX - boxObject.screenX, event.screenY - boxObject.screenY];
+ this._currentData = {
+ data: null,
+ anchorX: x,
+ anchorY: y,
+ currentX: -1,
+ currentY: -1
+ };
+ this.updateSelection(event);
+
+ document.addEventListener("keypress", this.handleKeyPress, true);
+ },
+
+ updateSelection: function(event)
+ {
+ if (event.button != 0 || !this._currentData)
+ return;
+
+ let boxObject = document.getBoxObjectFor(this._canvas);
+ let [x, y] = [event.screenX - boxObject.screenX, event.screenY - boxObject.screenY];
+ if (this._currentData.currentX == x && this._currentData.currentY == y)
+ return;
+
+ if (this._currentData.data)
+ {
+ this._context.putImageData(this._currentData.data,
+ Math.min(this._currentData.anchorX, this._currentData.currentX),
+ Math.min(this._currentData.anchorY, this._currentData.currentY));
+ }
+
+ this._currentData.currentX = x;
+ this._currentData.currentY = y;
+
+ let left = Math.min(this._currentData.anchorX, this._currentData.currentX);
+ let right = Math.max(this._currentData.anchorX, this._currentData.currentX);
+ let top = Math.min(this._currentData.anchorY, this._currentData.currentY);
+ let bottom = Math.max(this._currentData.anchorY, this._currentData.currentY);
+
+ let minDiff = (this._selectionType == "mark" ? 3 : 1);
+ if (right - left >= minDiff && bottom - top >= minDiff)
+ this._currentData.data = this._context.getImageData(left, top, right - left, bottom - top);
+ else
+ this._currentData.data = null;
+
+ if (this._selectionType == "mark")
+ {
+ // all coordinates need to be moved 1.5px inwards to get the desired result
+ left += 1.5;
+ right -= 1.5;
+ top += 1.5;
+ bottom -= 1.5;
+ if (left < right && top < bottom)
+ this._context.strokeRect(left, top, right - left, bottom - top);
+ }
+ else if (this._selectionType == "remove")
+ this._context.fillRect(left, top, right - left, bottom - top);
+ },
+
+ stopSelection: function(event)
+ {
+ if (event.button != 0 || !this._currentData)
+ return;
+
+ if (this._currentData.data)
+ {
+ this._undoQueue.push(this._currentData);
+ E("screenshotUndoButton").disabled = false;
+ }
+
+ this._currentData = null;
+ document.removeEventListener("keypress", this.handleKeyPress, true);
+ },
+
+ undo: function()
+ {
+ let op = this._undoQueue.pop();
+ if (!op)
+ return;
+
+ this._context.putImageData(op.data,
+ Math.min(op.anchorX, op.currentX),
+ Math.min(op.anchorY, op.currentY));
+
+ if (!this._undoQueue.length)
+ E("screenshotUndoButton").disabled = true;
+ }
+};
+
+let framesDataSource =
+{
+ site: null,
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ try
+ {
+ this.site = windowURI.host;
+ if (this.site)
+ document.title += " (" + this.site + ")";
+ }
+ catch (e)
+ {
+ // Expected exception - not all URL schemes have a host name
+ }
+
+ reportData.window.@url = censorURL(windowURI ? windowURI.spec : wnd.location.href);
+ if (wnd.opener && wnd.opener.location.href)
+ reportData.window.@opener = censorURL(wnd.opener.location.href);
+ if (wnd.document.referrer)
+ reportData.window.@referrer = censorURL(wnd.document.referrer);
+ this.scanFrames(wnd, reportData.window);
+
+ callback();
+ },
+
+ scanFrames: function(wnd, xmlList)
+ {
+ try
+ {
+ for (let i = 0; i < wnd.frames.length; i++)
+ {
+ let frame = wnd.frames[i];
+ let frameXML = ;
+ this.scanFrames(frame, frameXML);
+ xmlList.appendChild(frameXML);
+ }
+ }
+ catch (e)
+ {
+ // Don't break if something goes wrong
+ Cu.reportError(e);
+ }
+ }
+};
+
+let errorsDataSource =
+{
+ collectData: function(wnd, windowURI, callback)
+ {
+ let messages = {};
+ Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).getMessageArray(messages, {});
+ messages = messages.value || [];
+ messages = messages.filter(function(message)
+ {
+ return (message instanceof Ci.nsIScriptError &&
+ !/^https?:/i.test(message.sourceName) &&
+ (/adblock/i.test(message.errorMessage) || /adblock/i.test(message.sourceName)));
+ });
+ if (messages.length > 10) // Only the last 10 messages
+ messages = messages.slice(messages.length - 10, messages.length);
+
+ // Censor app and profile paths in error messages
+ let censored = {__proto__: null};
+ let pathList = [["ProfD", "%PROFILE%"], ["GreD", "%GRE%"], ["CurProcD", "%APP%"]];
+ for (let i = 0; i < pathList.length; i++)
+ {
+ let [pathID, placeholder] = pathList[i];
+ try
+ {
+ let file = FileUtils.getDir(pathID, [], false);
+ censored[file.path.replace(/[\\\/]+$/, '')] = placeholder;
+ let uri = Utils.ioService.newFileURI(file);
+ censored[uri.spec.replace(/[\\\/]+$/, '')] = placeholder;
+ } catch(e) {}
+ }
+
+ let errors = reportData.errors;
+ for (let i = 0; i < messages.length; i++)
+ {
+ let message = messages[i];
+
+ let text = message.errorMessage;
+ for (let path in censored)
+ text = text.replace(path, censored[path], "gi");
+ if (text.length > 256)
+ text = text.substr(0, 256) + "...";
+
+ let file = message.sourceName;
+ for (let path in censored)
+ file = file.replace(path, censored[path], "gi");
+ if (file.length > 256)
+ file = file.substr(0, 256) + "...";
+
+ let sourceLine = message.sourceLine;
+ if (sourceLine.length > 256)
+ sourceLine = sourceLine.substr(0, 256) + "...";
+
+ let errorXML = ;
+ errors.appendChild(errorXML);
+ }
+
+ callback();
+ }
+};
+
+let extensionsDataSource =
+{
+ data: ,
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ try
+ {
+ let AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", null).AddonManager;
+ AddonManager.getAddonsByTypes(["extension", "plugin"], function(items)
+ {
+ for (let i = 0; i < items.length; i++)
+ {
+ let item = items[i];
+ if (!item.isActive)
+ continue;
+ this.data.appendChild();
+ }
+ callback();
+ }.bind(this));
+ }
+ catch (e)
+ {
+ // No add-on manager, what's going on? Skip this step.
+ callback();
+ }
+ },
+
+ exportData: function(doExport)
+ {
+ if (doExport)
+ reportData.extensions = this.data;
+ else
+ delete reportData.extensions;
+ }
+};
+
+let subscriptionUpdateDataSource =
+{
+ contentWnd: null,
+ type: null,
+ outdated: null,
+ needUpdate: null,
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ this.contentWnd = wnd;
+ let now = Date.now() / MILLISECONDS_IN_SECOND;
+ let outdatedThreshold = now - 14 * SECONDS_IN_DAY;
+ let needUpdateThreshold = now - 1 * SECONDS_IN_HOUR;
+
+ this.outdated = [];
+ this.needUpdate = [];
+
+ let subscriptions = FilterStorage.subscriptions.filter(issuesDataSource.subscriptionFilter);
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let lastSuccess = subscriptions[i].lastSuccess;
+ if (lastSuccess < outdatedThreshold)
+ this.outdated.push(subscriptions[i]);
+ if (lastSuccess < needUpdateThreshold)
+ this.needUpdate.push(subscriptions[i]);
+ }
+
+ callback();
+ },
+
+ updatePage: function(type)
+ {
+ this.type = type;
+ E("updateInProgress").hidden = (type != "false positive" || this.needUpdate.length == 0);
+ E("outdatedSubscriptions").hidden = !E("updateInProgress").hidden || this.outdated.length == 0;
+ if (!E("outdatedSubscriptions").hidden)
+ {
+ let template = E("outdatedSubscriptionTemplate");
+ let list = E("outdatedSubscriptionsList");
+ while (list.lastChild)
+ list.removeChild(list.lastChild);
+
+ for (let i = 0; i < this.outdated.length; i++)
+ {
+ let subscription = this.outdated[i];
+ let entry = template.cloneNode(true);
+ entry.removeAttribute("id");
+ entry.removeAttribute("hidden");
+ entry.setAttribute("_url", subscription.url);
+ entry.setAttribute("tooltiptext", subscription.url);
+ entry.textContent = subscription.title;
+ list.appendChild(entry);
+ }
+ }
+ return !E("updateInProgress").hidden || !E("outdatedSubscriptions").hidden;
+ },
+
+ showPage: function()
+ {
+ document.documentElement.canAdvance = false;
+
+ if (!E("updateInProgress").hidden)
+ {
+ document.documentElement.canRewind = false;
+
+ for (let i = 0; i < this.needUpdate.length; i++)
+ Synchronizer.execute(this.needUpdate[i], true, true);
+
+ let listener = function(action)
+ {
+ if (!/^subscription\./.test(action))
+ return;
+
+ for (let i = 0; i < this.needUpdate.length; i++)
+ if (Synchronizer.isExecuting(this.needUpdate[i].url))
+ return;
+
+ FilterNotifier.removeListener(listener);
+ E("updateInProgress").hidden = "true";
+
+ let filtersRemoved = false;
+ let requests = requestsDataSource.origRequests;
+ for (let i = 0; i < requests.length; i++)
+ if (requests[i].filter && !requests[i].filter.subscriptions.filter(function(s) !s.disabled).length)
+ filtersRemoved = true;
+
+ if (filtersRemoved)
+ {
+ // Force the user to reload the page
+ E("updateFixedIssue").hidden = false;
+ document.documentElement.canAdvance = true;
+
+ let nextButton = document.documentElement.getButton("next");
+ nextButton.label = E("updatePage").getAttribute("reloadButtonLabel");
+ nextButton.accessKey = E("updatePage").getAttribute("reloadButtonAccesskey");
+ document.documentElement.addEventListener("wizardnext", function(event)
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ window.close();
+ this.contentWnd.location.reload();
+ }.bind(this), true);
+ }
+ else
+ {
+ this.collectData(null, null, function() {});
+ this.needUpdate = [];
+ if (this.outdated.length)
+ {
+ document.documentElement.canRewind = true;
+
+ this.updatePage(this.type);
+ this.showPage();
+ }
+ else
+ {
+ // No more issues, make sure to remove this page from history and
+ // advance to the next page.
+ document.documentElement.canRewind = true;
+ document.documentElement.canAdvance = true;
+
+ let next = document.documentElement.currentPage.next;
+ document.documentElement.rewind();
+ document.documentElement.currentPage.next = next;
+
+ document.documentElement.advance();
+ }
+ }
+ }.bind(this);
+
+ FilterNotifier.addListener(listener);
+ window.addEventListener("unload", function()
+ {
+ FilterNotifier.removeListener(listener);
+ });
+ }
+ },
+
+ updateOutdated: function()
+ {
+ for (let i = 0; i < this.outdated.length; i++)
+ Synchronizer.execute(this.outdated[i], true, true);
+ }
+}
+
+let issuesDataSource =
+{
+ contentWnd: null,
+ isEnabled: Prefs.enabled,
+ whitelistFilter: null,
+ disabledFilters: [],
+ disabledSubscriptions: [],
+ ownFilters: [],
+ numSubscriptions: 0,
+ numAppliedFilters: Infinity,
+
+ subscriptionFilter: function(s)
+ {
+ if (s instanceof DownloadableSubscription)
+ return subscriptionsDataSource.subscriptionFilter(s);
+ else
+ return false;
+ },
+
+ collectData: function(wnd, windowURI, callback)
+ {
+ this.contentWnd = wnd;
+ this.whitelistFilter = Policy.isWindowWhitelisted(wnd);
+
+ if (!this.whitelistFilter && this.isEnabled)
+ {
+ // Find disabled filters in active subscriptions matching any of the requests
+ let disabledMatcher = new CombinedMatcher();
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (subscription.disabled)
+ continue;
+
+ for each (let filter in subscription.filters)
+ if (filter instanceof BlockingFilter && filter.disabled)
+ disabledMatcher.add(filter);
+ }
+
+ let seenFilters = {__proto__: null};
+ for each (let request in requestsDataSource.origRequests)
+ {
+ if (request.filter)
+ continue;
+
+ let filter = disabledMatcher.matchesAny(request.location, request.typeDescr, request.docDomain, request.thirdParty);
+ if (filter && !(filter.text in seenFilters))
+ {
+ this.disabledFilters.push(filter);
+ seenFilters[filter.text] = true;
+ }
+ }
+
+ // Find disabled subscriptions with filters matching any of the requests
+ let seenSubscriptions = {__proto__: null};
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (!subscription.disabled)
+ continue;
+
+ disabledMatcher.clear();
+ for each (let filter in subscription.filters)
+ if (filter instanceof BlockingFilter)
+ disabledMatcher.add(filter);
+
+ for each (let request in requestsDataSource.origRequests)
+ {
+ if (request.filter)
+ continue;
+
+ let filter = disabledMatcher.matchesAny(request.location, request.typeDescr, request.docDomain, request.thirdParty);
+ if (filter && !(subscription.url in seenSubscriptions))
+ {
+ this.disabledSubscriptions.push(subscription);
+ seenSubscriptions[subscription.text] = true;
+ break;
+ }
+ }
+ }
+
+ this.numSubscriptions = FilterStorage.subscriptions.filter(this.subscriptionFilter).length;
+ this.numAppliedFilters = 0;
+ for each (let filter in filtersDataSource.origFilters)
+ {
+ if (filter instanceof WhitelistFilter)
+ continue;
+
+ this.numAppliedFilters++;
+ if (filter.subscriptions.some(function(subscription) subscription instanceof SpecialSubscription))
+ this.ownFilters.push(filter);
+ }
+ }
+
+ callback();
+ },
+
+ updateIssues: function(type)
+ {
+ if (type == "other")
+ {
+ E("typeSelectorPage").next = "typeWarning";
+ return;
+ }
+
+ E("issuesWhitelistBox").hidden = !this.whitelistFilter;
+ E("issuesDisabledBox").hidden = this.isEnabled;
+ E("issuesNoFiltersBox").hidden = (type != "false positive" || this.numAppliedFilters > 0);
+ E("issuesNoSubscriptionsBox").hidden = (type != "false negative" || this.numAppliedFilters > 0 || this.numSubscriptions > 0);
+ E("issuesSubscriptionCountBox").hidden = (this.numSubscriptions < 5);
+
+ let ownFiltersBox = E("issuesOwnFilters");
+ if (this.ownFilters.length && !ownFiltersBox.firstChild)
+ {
+ let template = E("issuesOwnFiltersTemplate");
+ for each (let filter in this.ownFilters)
+ {
+ let element = template.cloneNode(true);
+ element.removeAttribute("id");
+ element.removeAttribute("hidden");
+ element.firstChild.setAttribute("value", filter.text);
+ element.firstChild.setAttribute("tooltiptext", filter.text);
+ element.abpFilter = filter;
+ ownFiltersBox.appendChild(element);
+ }
+ }
+ E("issuesOwnFiltersBox").hidden = (type != "false positive" || this.ownFilters.length == 0);
+
+ let disabledSubscriptionsBox = E("issuesDisabledSubscriptions");
+ if (this.disabledSubscriptions.length && !disabledSubscriptionsBox.firstChild)
+ {
+ let template = E("issuesDisabledSubscriptionsTemplate");
+ for each (let subscription in this.disabledSubscriptions)
+ {
+ let element = template.cloneNode(true);
+ element.removeAttribute("id");
+ element.removeAttribute("hidden");
+ element.firstChild.setAttribute("value", subscription.title);
+ element.setAttribute("tooltiptext", subscription instanceof DownloadableSubscription ? subscription.url : subscription.title);
+ element.abpSubscription = subscription;
+ disabledSubscriptionsBox.appendChild(element);
+ }
+ }
+ E("issuesDisabledSubscriptionsBox").hidden = (type != "false negative" || this.disabledSubscriptions.length == 0);
+
+ let disabledFiltersBox = E("issuesDisabledFilters");
+ if (this.disabledFilters.length && !disabledFiltersBox.firstChild)
+ {
+ let template = E("issuesDisabledFiltersTemplate");
+ for each (let filter in this.disabledFilters)
+ {
+ let element = template.cloneNode(true);
+ element.removeAttribute("id");
+ element.removeAttribute("hidden");
+ element.firstChild.setAttribute("value", filter.text);
+ element.setAttribute("tooltiptext", filter.text);
+ element.abpFilter = filter;
+ disabledFiltersBox.appendChild(element);
+ }
+ }
+ E("issuesDisabledFiltersBox").hidden = (type != "false negative" || this.disabledFilters.length == 0);
+
+ // Don't allow sending report if the page is whitelisted - we need the data.
+ // Also disallow reports without matching filters or without subscriptions,
+ // subscription authors cannot do anything about those.
+ E("issuesOverride").hidden = !E("issuesWhitelistBox").hidden ||
+ !E("issuesDisabledBox").hidden ||
+ !E("issuesNoFiltersBox").hidden ||
+ !E("issuesNoSubscriptionsBox").hidden ||
+ !E("issuesSubscriptionCountBox").hidden;
+
+ let page = E("typeSelectorPage");
+ if (subscriptionUpdateDataSource.updatePage(type))
+ {
+ page.next = "update";
+ page = E("updatePage");
+ }
+
+ if (E("issuesWhitelistBox").hidden && E("issuesDisabledBox").hidden &&
+ E("issuesNoFiltersBox").hidden && E("issuesNoSubscriptionsBox").hidden &&
+ E("issuesOwnFiltersBox").hidden && E("issuesDisabledFiltersBox").hidden &&
+ E("issuesDisabledSubscriptionsBox").hidden && E("issuesSubscriptionCountBox").hidden)
+ {
+ page.next = "screenshot";
+ }
+ else
+ {
+ page.next = "issues";
+ }
+ },
+
+ forceReload: function()
+ {
+ // User changed configuration, don't allow sending report now - page needs
+ // to be reloaded
+ E("issuesOverride").hidden = true;
+ E("issuesChangeMessage").hidden = false;
+ document.documentElement.canRewind = false;
+ document.documentElement.canAdvance = true;
+
+ let contentWnd = this.contentWnd;
+ let nextButton = document.documentElement.getButton("next");
+ nextButton.label = E("issuesPage").getAttribute("reloadButtonLabel");
+ nextButton.accessKey = E("issuesPage").getAttribute("reloadButtonAccesskey");
+ document.documentElement.addEventListener("wizardnext", function(event)
+ {
+ event.preventDefault();
+ event.stopPropagation();
+ window.close();
+ contentWnd.location.reload();
+ }, true);
+ },
+
+ removeWhitelist: function()
+ {
+ if (this.whitelistFilter && this.whitelistFilter.subscriptions.length)
+ this.whitelistFilter.disabled = true;
+ E("issuesWhitelistBox").hidden = true;
+ this.forceReload();
+ },
+
+ enable: function()
+ {
+ Prefs.enabled = true;
+ E("issuesDisabledBox").hidden = true;
+ this.forceReload();
+ },
+
+ addSubscription: function()
+ {
+ let result = {};
+ openDialog("subscriptionSelection.xul", "_blank", "chrome,centerscreen,modal,resizable,dialog=no", null, result);
+ if (!("url" in result))
+ return;
+
+ let subscriptionResults = [[result.url, result.title]];
+ if ("mainSubscriptionURL" in result)
+ subscriptionResults.push([result.mainSubscriptionURL, result.mainSubscriptionTitle]);
+
+ for each (let [url, title] in subscriptionResults)
+ {
+ let subscription = Subscription.fromURL(url);
+ if (!subscription)
+ continue;
+
+ FilterStorage.addSubscription(subscription);
+
+ subscription.disabled = false;
+ subscription.title = title;
+
+ if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
+ Synchronizer.execute(subscription);
+ }
+
+ E("issuesNoSubscriptionsBox").hidden = true;
+ this.forceReload();
+ },
+
+ disableFilter: function(node)
+ {
+ let filter = node.abpFilter;
+ if (filter && filter.subscriptions.length)
+ filter.disabled = true;
+
+ node.parentNode.removeChild(node);
+ if (!E("issuesOwnFilters").firstChild)
+ E("issuesOwnFiltersBox").hidden = true;
+ this.forceReload();
+ },
+
+ enableFilter: function(node)
+ {
+ let filter = node.abpFilter;
+ if (filter && filter.subscriptions.length)
+ filter.disabled = false;
+
+ node.parentNode.removeChild(node);
+ if (!E("issuesDisabledFilters").firstChild)
+ E("issuesDisabledFiltersBox").hidden = true;
+ this.forceReload();
+ },
+
+
+ enableSubscription: function(node)
+ {
+ let subscription = node.abpSubscription;
+ if (subscription)
+ subscription.disabled = false;
+
+ node.parentNode.removeChild(node);
+ if (!E("issuesDisabledSubscriptions").firstChild)
+ E("issuesDisabledSubscriptionsBox").hidden = true;
+ this.forceReload();
+ }
+};
+
+let dataCollectors = [reportsListDataSource, requestsDataSource, filtersDataSource, subscriptionsDataSource,
+ screenshotDataSource, framesDataSource, errorsDataSource, extensionsDataSource,
+ subscriptionUpdateDataSource, issuesDataSource];
+
+//
+// Wizard logic
+//
+
+function initWizard()
+{
+ // Make sure no issue type is selected by default
+ E("typeGroup").selectedItem = null;
+ document.documentElement.addEventListener("pageshow", updateNextButton, false);
+
+ // Move privacy link
+ let extraButton = document.documentElement.getButton("extra1");
+ extraButton.parentNode.insertBefore(E("privacyLink"), extraButton);
+}
+
+function updateNextButton()
+{
+ let nextButton = document.documentElement.getButton("next");
+ if (!nextButton)
+ return;
+
+ if (document.documentElement.currentPage.id == "commentPage")
+ {
+ if (!nextButton.hasAttribute("_origLabel"))
+ {
+ nextButton.setAttribute("_origLabel", nextButton.getAttribute("label"));
+ nextButton.setAttribute("label", document.documentElement.getAttribute("sendbuttonlabel"));
+ nextButton.setAttribute("_origAccessKey", nextButton.getAttribute("accesskey"));
+ nextButton.setAttribute("accesskey", document.documentElement.getAttribute("sendbuttonaccesskey"));
+ }
+ }
+ else
+ {
+ if (nextButton.hasAttribute("_origLabel"))
+ {
+ nextButton.setAttribute("label", nextButton.getAttribute("_origLabel"));
+ nextButton.removeAttribute("_origLabel");
+ nextButton.setAttribute("accesskey", nextButton.getAttribute("_origAccessKey"));
+ nextButton.removeAttribute("_origAccessKey");
+ }
+ }
+}
+
+function initDataCollectorPage()
+{
+ document.documentElement.canAdvance = false;
+
+ let contentWindow = window.arguments[0];
+ let windowURI = (window.arguments[1] instanceof Ci.nsIURI ? window.arguments[1] : null);
+ let totalSteps = dataCollectors.length;
+ let initNextDataSource = function()
+ {
+ if (!dataCollectors.length)
+ {
+ // We are done, continue to next page
+ document.documentElement.canAdvance = true;
+ document.documentElement.advance();
+ return;
+ }
+
+ let progress = (totalSteps - dataCollectors.length) / totalSteps * 100;
+ if (progress > 0)
+ {
+ let progressMeter = E("dataCollectorProgress");
+ progressMeter.mode = "determined";
+ progressMeter.value = progress;
+ }
+
+ // Continue with the next data source, asynchronously to allow progress meter to update
+ let dataSource = dataCollectors.shift();
+ Utils.runAsync(function()
+ {
+ dataSource.collectData(contentWindow, windowURI, initNextDataSource);
+ });
+ };
+
+ initNextDataSource();
+}
+
+function initTypeSelectorPage()
+{
+ E("progressBar").activeItem = E("typeSelectorHeader");
+ let header = document.getAnonymousElementByAttribute(document.documentElement, "class", "wizard-header");
+ if (header)
+ header.setAttribute("viewIndex", "1");
+
+ document.documentElement.canRewind = false;
+ typeSelectionUpdated();
+}
+
+function typeSelectionUpdated()
+{
+ let selection = E("typeGroup").selectedItem;
+ document.documentElement.canAdvance = (selection != null);
+ if (selection)
+ {
+ if (reportData.@type != selection.value)
+ {
+ E("screenshotCheckbox").checked = (selection.value != "other");
+ E("screenshotCheckbox").doCommand();
+ E("extensionsCheckbox").checked = (selection.value == "other");
+ E("extensionsCheckbox").doCommand();
+ }
+ reportData.@type = selection.value;
+
+ issuesDataSource.updateIssues(selection.value);
+ }
+}
+
+function initIssuesPage()
+{
+ updateIssuesOverride();
+}
+
+function updateIssuesOverride()
+{
+ document.documentElement.canAdvance = E("issuesOverride").checked;
+}
+
+function initTypeWarningPage()
+{
+ updateTypeWarningOverride();
+
+ let textElement = E("typeWarningText");
+ if ("abpInitialized" in textElement)
+ return;
+
+ let template = textElement.textContent.replace(/[\r\n\s]+/g, " ");
+
+ let [, beforeLink, linkText, afterLink] = /(.*)\[link\](.*)\[\/link\](.*)/.exec(template) || [null, "", template, ""];
+ while (textElement.firstChild && textElement.firstChild.nodeType != Node.ELEMENT_NODE)
+ textElement.removeChild(textElement.firstChild);
+ while (textElement.lastChild && textElement.lastChild.nodeType != Node.ELEMENT_NODE)
+ textElement.removeChild(textElement.lastChild);
+
+ if (textElement.firstChild)
+ textElement.firstChild.textContent = linkText;
+ textElement.insertBefore(document.createTextNode(beforeLink), textElement.firstChild);
+ textElement.appendChild(document.createTextNode(afterLink));
+ textElement.abpInitialized = true;
+}
+
+function updateTypeWarningOverride()
+{
+ document.documentElement.canAdvance = E("typeWarningOverride").checked;
+}
+
+function initScreenshotPage()
+{
+ E("progressBar").activeItem = E("screenshotHeader");
+}
+
+function initCommentPage()
+{
+ E("progressBar").activeItem = E("commentPageHeader");
+
+ screenshotDataSource.exportData();
+ updateDataField();
+}
+
+function showDataField()
+{
+ E('dataDeck').selectedIndex = 1;
+ updateDataField();
+ E('data').focus();
+}
+
+let _dataFieldUpdateTimeout = null;
+
+function _updateDataField()
+{
+ let dataField = E("data");
+ let [selectionStart, selectionEnd] = [dataField.selectionStart, dataField.selectionEnd];
+ dataField.value = reportData.toXMLString();
+ dataField.setSelectionRange(selectionStart, selectionEnd);
+}
+
+function updateDataField()
+{
+ // Don't do anything if data field is hidden
+ if (E('dataDeck').selectedIndex != 1)
+ return;
+
+ if (_dataFieldUpdateTimeout)
+ {
+ window.clearTimeout(_dataFieldUpdateTimeout);
+ _dataFieldUpdateTimeout = null;
+ }
+
+ _dataFieldUpdateTimeout = window.setTimeout(_updateDataField, 200);
+}
+
+function updateComment()
+{
+ let value = E("comment").value;
+ reportData.comment = value.substr(0, 1000);
+ E("commentLengthWarning").setAttribute("visible", value.length > 1000);
+ updateDataField();
+}
+
+function updateEmail()
+{
+ reportData.email = E("email").value.replace(/\@/g, " at ").replace(/\./g, " dot ");
+ updateDataField();
+}
+
+function updateExtensions(attach)
+{
+ extensionsDataSource.exportData(attach);
+ updateDataField();
+}
+
+function initSendPage()
+{
+ E("progressBar").activeItem = E("sendPageHeader");
+
+ E("result").hidden = true;
+ E("sendReportErrorBox").hidden = true;
+ E("sendReportMessage").hidden = false;
+ E("sendReportProgress").hidden = false;
+ E("sendReportProgress").mode = "undetermined";
+
+ document.documentElement.canRewind = false;
+ document.documentElement.getButton("finish").disabled = true;
+
+ let guid = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString().replace(/[\{\}]/g, "");
+ let url = Prefs.report_submiturl.replace(/%GUID%/g, guid).replace(/%LANG%/g, Utils.appLocale);
+ let request = new XMLHttpRequest();
+ request.open("POST", url);
+ request.setRequestHeader("Content-Type", "text/xml");
+ request.setRequestHeader("X-Adblock-Plus", "1");
+ request.addEventListener("load", reportSent, false);
+ request.addEventListener("error", reportSent, false);
+ if ("upload" in request && request.upload)
+ request.upload.addEventListener("progress", updateReportProgress, false);
+ request.send(reportData.toXMLString());
+}
+
+function updateReportProgress(event)
+{
+ if (!event.lengthComputable)
+ return;
+
+ let progress = Math.round(event.loaded / event.total * 100);
+ if (progress > 0)
+ {
+ let progressMeter = E("sendReportProgress");
+ progressMeter.mode = "determined";
+ progressMeter.value = progress;
+ }
+}
+
+function reportSent(event)
+{
+ let request = event.target;
+ let success = false;
+ let errorMessage = E("sendReportError").getAttribute("defaultError");
+ try
+ {
+ let status = request.channel.status;
+ if (Components.isSuccessCode(status))
+ {
+ success = (request.status == 200 || request.status == 0);
+ errorMessage = request.status + " " + request.statusText;
+ }
+ else
+ {
+ errorMessage = "0x" + status.toString(16);
+
+ // Try to find the name for the status code
+ let exception = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
+ exception.initialize(null, status, null, null, null, null);
+ if (exception.name)
+ errorMessage = exception.name;
+ }
+ } catch (e) {}
+
+ let result = "";
+ try
+ {
+ result = request.responseText;
+ } catch (e) {}
+
+ result = result.replace(/%CONFIRMATION%/g, encodeHTML(E("result").getAttribute("confirmationMessage")));
+ result = result.replace(/%KNOWNISSUE%/g, encodeHTML(E("result").getAttribute("knownIssueMessage")));
+ result = result.replace(/( /g, ">").replace(/"/g, """);
+}
diff --git a/abprime/content/sendReport.xul b/abprime/content/sendReport.xul
new file mode 100644
index 00000000..98eaeb78
--- /dev/null
+++ b/abprime/content/sendReport.xul
@@ -0,0 +1,248 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+%reporterDTD;
+
+%filtersDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &dataCollector.description;
+
+
+
+
+
+ &typeSelector.description;
+
+
+
+ &typeSelector.falsePositive.description;
+
+ &typeSelector.falseNegative.description;
+
+ &typeSelector.other.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &update.inProgress.description;
+
+
+
+ &update.fixed.description;
+
+
+ &outdatedSubscriptions.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &issues.description;
+
+
+
+ &issues.whitelist.description;
+
+
+
+
+
+ &issues.disabled.description;
+
+
+
+
+
+ &issues.nofilters.description;
+
+
+ &issues.nosubscriptions.description;
+
+
+
+
+
+ &issues.subscriptionCount.description;
+
+
+
+
+
+ &issues.ownfilters.description;
+
+
+
+
+
+
+
+ &issues.disabledgroups.description;
+
+
+
+
+
+
+
+ &issues.disabledfilters.description;
+
+
+
+
+
+
+
+
+
+ &issues.change.description;
+
+
+
+
+ &typeWarning.description;
+
+
+
+
+
+
+
+ &screenshot.description;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &sendPage.waitMessage;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/settings.xul b/abprime/content/settings.xul
new file mode 100644
index 00000000..f8348384
--- /dev/null
+++ b/abprime/content/settings.xul
@@ -0,0 +1,29 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/content/sidebar.js b/abprime/content/sidebar.js
new file mode 100644
index 00000000..8ca1d1db
--- /dev/null
+++ b/abprime/content/sidebar.js
@@ -0,0 +1,1226 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Main browser window
+var mainWin = parent;
+
+// The window handler currently in use
+var requestNotifier = null;
+
+var cacheSession = null;
+var noFlash = false;
+
+// Matcher for disabled filters
+var disabledMatcher = new CombinedMatcher();
+
+// Cached string values
+var docDomainThirdParty = null;
+var docDomainFirstParty = null;
+
+var abpHooks = null;
+
+function init() {
+ docDomainThirdParty = document.documentElement.getAttribute("docDomainThirdParty");
+ docDomainFirstParty = document.documentElement.getAttribute("docDomainFirstParty");
+
+ var list = E("list");
+ list.view = treeView;
+
+ // Restore previous state
+ var params = Utils.getParams();
+ if (params && params.filter)
+ {
+ E("searchField").value = params.filter;
+ treeView.setFilter(params.filter);
+ }
+ if (params && params.focus && E(params.focus))
+ E(params.focus).focus();
+ else
+ E("searchField").focus();
+
+ var selected = null;
+ if (/sidebarDetached\.xul$/.test(parent.location.href)) {
+ mainWin = parent.opener;
+ mainWin.addEventListener("unload", mainUnload, false);
+ E("detachButton").hidden = true;
+ E("reattachButton").hidden = false;
+ if (!mainWin.document.getElementById("abp-sidebar"))
+ E("reattachButton").setAttribute("disabled", "true");
+ if (mainWin.document.getElementById("abp-key-sidebar")) {
+ var sidebarKey = mainWin.document.getElementById("abp-key-sidebar").cloneNode(true);
+ parent.document.getElementById("detached-keyset").appendChild(parent.document.importNode(sidebarKey, true));
+ }
+
+ // Set default size/position unless already persisted
+ let defaults = {screenX: 0, screenY: 0, width: 600, height: 300};
+ if (params && params.position)
+ defaults = params.position;
+
+ let wnd = parent.document.documentElement;
+ for (let attr in defaults)
+ if (!wnd.hasAttribute(attr))
+ wnd.setAttribute(attr, defaults[attr]);
+ }
+
+ abpHooks = mainWin.document.getElementById("abp-hooks");
+ window.__defineGetter__("content", function() {return abpHooks.getBrowser().contentWindow;});
+
+ // Initialize matcher for disabled filters
+ reloadDisabledFilters();
+ FilterNotifier.addListener(reloadDisabledFilters);
+ Prefs.addListener(onPrefChange);
+
+ // Activate flasher
+ list.addEventListener("select", onSelectionChange, false);
+
+ // Initialize data
+ handleLocationChange();
+
+ // Install a progress listener to catch location changes
+ abpHooks.getBrowser().addProgressListener(progressListener);
+}
+
+// To be called for a detached window when the main window has been closed
+function mainUnload() {
+ parent.close();
+}
+
+// To be called on unload
+function cleanUp() {
+ flasher.stop();
+ requestNotifier.shutdown();
+ FilterNotifier.removeListener(reloadDisabledFilters);
+ Prefs.removeListener(onPrefChange);
+ E("list").view = null;
+
+ abpHooks.getBrowser().removeProgressListener(progressListener);
+ mainWin.removeEventListener("unload", mainUnload, false);
+}
+
+/**
+ * Tracks preference changes, calls reloadDisabledFilters whenever Adblock Plus
+ * is enabled/disabled.
+ */
+function onPrefChange(name)
+{
+ if (name == "enabled")
+ reloadDisabledFilters();
+}
+
+let reloadDisabledScheduled = false;
+
+/**
+ * Updates matcher for disabled filters (global disabledMatcher variable),
+ * called on each filter change. Execute delayed to prevent multiple subsequent
+ * invocations.
+ */
+function reloadDisabledFilters()
+{
+ if (reloadDisabledScheduled)
+ return;
+
+ Utils.runAsync(reloadDisabledFiltersInternal);
+ reloadDisabledScheduled = true;
+}
+
+function reloadDisabledFiltersInternal()
+{
+ reloadDisabledScheduled = false;
+ disabledMatcher.clear();
+
+ if (Prefs.enabled)
+ {
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (subscription.disabled)
+ continue;
+
+ for each (let filter in subscription.filters)
+ if (filter instanceof RegExpFilter && filter.disabled)
+ disabledMatcher.add(filter);
+ }
+ }
+
+ treeView.updateFilters();
+}
+
+// Called whenever list selection changes - triggers flasher
+function onSelectionChange() {
+ var item = treeView.getSelectedItem();
+ if (item)
+ E("copy-command").removeAttribute("disabled");
+ else
+ E("copy-command").setAttribute("disabled", "true");
+
+ if (item && window.content)
+ {
+ let key = item.location + " " + item.type + " " + item.docDomain;
+ RequestNotifier.storeSelection(window.content, key);
+ treeView.itemToSelect = null;
+ }
+
+ if (!noFlash)
+ flasher.flash(item ? item.nodes : null);
+}
+
+function handleLocationChange()
+{
+ if (requestNotifier)
+ requestNotifier.shutdown();
+
+ treeView.clearData();
+ treeView.itemToSelect = RequestNotifier.getSelection(window.content);
+ requestNotifier = new RequestNotifier(window.content, function(wnd, node, item, scanComplete)
+ {
+ if (item)
+ treeView.addItem(node, item, scanComplete);
+ });
+}
+
+// Fills a box with text splitting it up into multiple lines if necessary
+function setMultilineContent(box, text, noRemove)
+{
+ if (!noRemove)
+ while (box.firstChild)
+ box.removeChild(box.firstChild);
+
+ for (var i = 0; i < text.length; i += 80)
+ {
+ var description = document.createElement("description");
+ description.setAttribute("value", text.substr(i, 80));
+ box.appendChild(description);
+ }
+}
+
+// Fill in tooltip data before showing it
+function fillInTooltip(e) {
+ // Prevent tooltip from overlapping menu
+ if (E("context").state == "open")
+ {
+ e.preventDefault();
+ return;
+ }
+
+ var item;
+ if (treeView.data && !treeView.data.length)
+ item = treeView.getDummyTooltip();
+ else
+ item = treeView.getItemAt(e.clientX, e.clientY);
+
+ if (!item)
+ {
+ e.preventDefault();
+ return;
+ }
+
+ let filter = ("filter" in item && item.filter ? item.filter : null);
+ let size = ("tooltip" in item ? null : getItemSize(item));
+ let subscriptions = (filter ? filter.subscriptions.filter(function(subscription) { return !subscription.disabled; }) : []);
+
+ E("tooltipDummy").hidden = !("tooltip" in item);
+ E("tooltipAddressRow").hidden = ("tooltip" in item);
+ E("tooltipTypeRow").hidden = ("tooltip" in item);
+ E("tooltipSizeRow").hidden = !size;
+ E("tooltipDocDomainRow").hidden = ("tooltip" in item || !item.docDomain);
+ E("tooltipFilterRow").hidden = !filter;
+ E("tooltipFilterSourceRow").hidden = !subscriptions.length;
+
+ if ("tooltip" in item)
+ E("tooltipDummy").setAttribute("value", item.tooltip);
+ else
+ {
+ E("tooltipAddress").parentNode.hidden = (item.typeDescr == "ELEMHIDE");
+ setMultilineContent(E("tooltipAddress"), item.location);
+
+ var type = item.localizedDescr;
+ if (filter && filter instanceof WhitelistFilter)
+ type += " " + E("tooltipType").getAttribute("whitelisted");
+ else if (filter && item.typeDescr != "ELEMHIDE")
+ type += " " + E("tooltipType").getAttribute("filtered");
+ E("tooltipType").setAttribute("value", type);
+
+ if (size)
+ E("tooltipSize").setAttribute("value", size.join(" x "));
+
+ E("tooltipDocDomain").setAttribute("value", item.docDomain + " " + (item.thirdParty ? docDomainThirdParty : docDomainFirstParty));
+ }
+
+ if (filter)
+ {
+ let filterField = E("tooltipFilter");
+ setMultilineContent(filterField, filter.text);
+ if (filter.disabled)
+ {
+ let disabledText = document.createElement("description");
+ disabledText.className = "disabledTextLabel";
+ disabledText.textContent = filterField.getAttribute("disabledText");
+ filterField.appendChild(disabledText);
+ }
+
+ if (subscriptions.length)
+ {
+ let sourceElement = E("tooltipFilterSource");
+ while (sourceElement.firstChild)
+ sourceElement.removeChild(sourceElement.firstChild);
+ for (let i = 0; i < subscriptions.length; i++)
+ setMultilineContent(sourceElement, subscriptions[i].title, true);
+ }
+ }
+
+ var showPreview = Prefs.previewimages && !("tooltip" in item);
+ showPreview = showPreview && item.typeDescr == "IMAGE";
+ showPreview = showPreview && (!item.filter || item.filter.disabled || item.filter instanceof WhitelistFilter);
+ if (showPreview) {
+ // Check whether image is in cache (stolen from ImgLikeOpera)
+ if (!cacheSession) {
+ var cacheService = Cc["@mozilla.org/network/cache-service;1"].getService(Ci.nsICacheService);
+ cacheSession = cacheService.createSession("HTTP", Ci.nsICache.STORE_ANYWHERE, true);
+ }
+
+ try {
+ var descriptor = cacheSession.openCacheEntry(item.location, Ci.nsICache.ACCESS_READ, false);
+ descriptor.close();
+ }
+ catch (e) {
+ showPreview = false;
+ }
+ }
+
+ if (showPreview) {
+ E("tooltipPreviewBox").hidden = false;
+ E("tooltipPreview").setAttribute("src", "");
+ E("tooltipPreview").setAttribute("src", item.location);
+ }
+ else
+ E("tooltipPreviewBox").hidden = true;
+}
+
+const visual = {
+ OTHER: true,
+ IMAGE: true,
+ SUBDOCUMENT: true
+}
+
+/**
+ * Updates context menu before it is shown.
+ */
+function fillInContext(/**Event*/ e)
+{
+ let item, allItems;
+ if (treeView.data && !treeView.data.length)
+ {
+ item = treeView.getDummyTooltip();
+ allItems = [item];
+ }
+ else
+ {
+ item = treeView.getItemAt(e.clientX, e.clientY);
+ allItems = treeView.getAllSelectedItems();
+ }
+
+ if (!item || ("tooltip" in item && !("filter" in item)))
+ return false;
+
+ E("contextDisableFilter").hidden = true;
+ E("contextEnableFilter").hidden = true;
+ E("contextDisableOnSite").hidden = true;
+ if ("filter" in item && item.filter)
+ {
+ let filter = item.filter;
+ let menuItem = E(filter.disabled ? "contextEnableFilter" : "contextDisableFilter");
+ menuItem.setAttribute("label", menuItem.getAttribute("labeltempl").replace(/\?1\?/, filter.text));
+ menuItem.hidden = false;
+
+ if (filter instanceof ActiveFilter && !filter.disabled && filter.subscriptions.length && !filter.subscriptions.some(function(subscription) !(subscription instanceof SpecialSubscription)))
+ {
+ let domain = null;
+ try {
+ domain = Utils.effectiveTLD.getBaseDomainFromHost(item.docDomain);
+ } catch (e) {}
+
+ if (domain && !filter.isActiveOnlyOnDomain(domain))
+ {
+ menuItem = E("contextDisableOnSite");
+ menuItem.setAttribute("label", menuItem.getAttribute("labeltempl").replace(/\?1\?/, domain));
+ menuItem.hidden = false;
+ }
+ }
+ }
+
+ E("contextWhitelist").hidden = ("tooltip" in item || !item.filter || item.filter.disabled || item.filter instanceof WhitelistFilter || item.typeDescr == "ELEMHIDE");
+ E("contextBlock").hidden = !E("contextWhitelist").hidden;
+ E("contextBlock").setAttribute("disabled", "filter" in item && item.filter && !item.filter.disabled);
+ E("contextEditFilter").setAttribute("disabled", !("filter" in item && item.filter));
+ E("contextOpen").setAttribute("disabled", "tooltip" in item || item.typeDescr == "ELEMHIDE");
+ E("contextFlash").setAttribute("disabled", "tooltip" in item || !(item.typeDescr in visual) || (item.filter && !item.filter.disabled && !(item.filter instanceof WhitelistFilter)));
+ E("contextCopyFilter").setAttribute("disabled", !allItems.some(function(item) {return "filter" in item && item.filter}));
+
+ return true;
+}
+
+/**
+ * Resets context menu data once the context menu is closed.
+ */
+function clearContextMenu(/**Event*/ event)
+{
+ if (event.eventPhase != event.AT_TARGET)
+ return;
+
+ {
+ let menuItem = E("contextDisableOnSite");
+ menuItem.item = item;
+ menuItem.filter = filter;
+ menuItem.domain = domain;
+ }
+}
+
+/**
+ * Processed mouse clicks on the item list.
+ * @param {Event} event
+ */
+function handleClick(event)
+{
+ let item = treeView.getItemAt(event.clientX, event.clientY);
+ if (event.button == 0 && treeView.getColumnAt(event.clientX, event.clientY) == "state")
+ {
+ if (item.filter)
+ enableFilter(item.filter, item.filter.disabled);
+ event.preventDefault();
+ }
+ else if (event.button == 1)
+ {
+ openInTab(item, event);
+ event.preventDefault();
+ }
+}
+
+/**
+ * Processes double-clicks on the item list.
+ * @param {Event} event
+ */
+function handleDblClick(event)
+{
+ if (event.button != 0 || treeView.getColumnAt(event.clientX, event.clientY) == "state")
+ return;
+
+ doBlock();
+}
+
+/**
+ * Opens the item in a new tab.
+ */
+function openInTab(item, /**Event*/ event)
+{
+ let items = (item ? [item] : treeView.getAllSelectedItems());
+ for each (let item in items)
+ {
+ if (item && item.typeDescr != "ELEMHIDE")
+ Utils.loadInBrowser(item.location, mainWin, event);
+ }
+}
+
+function doBlock() {
+ var item = treeView.getSelectedItem();
+ if (!item || item.typeDescr == "ELEMHIDE")
+ return;
+
+ var filter = null;
+ if (item.filter && !item.filter.disabled)
+ filter = item.filter;
+
+ if (filter && filter instanceof WhitelistFilter)
+ return;
+
+ openDialog("chrome://@ADDON_CHROME_NAME@/content/composer.xul", "_blank", "chrome,centerscreen,resizable,dialog=no,dependent", item.nodes, item.orig);
+}
+
+function editFilter()
+{
+ var item = treeView.getSelectedItem();
+ if (treeView.data && !treeView.data.length)
+ item = treeView.getDummyTooltip();
+
+ if (!("filter" in item) || !item.filter)
+ return;
+
+ if (!("location") in item)
+ item.location = undefined
+
+ Utils.openFiltersDialog(item.filter);
+}
+
+function enableFilter(filter, enable) {
+ filter.disabled = !enable;
+
+ treeView.boxObject.invalidate();
+}
+
+/**
+ * Edits the filter to disable it on a particular domain.
+ */
+function disableOnSite()
+{
+ let item = treeView.getSelectedItem();
+ let filter = item.filter;
+ if (!(filter instanceof ActiveFilter) || filter.disabled || !filter.subscriptions.length || filter.subscriptions.some(function(subscription) !(subscription instanceof SpecialSubscription)))
+ return;
+
+ let domain;
+ try {
+ domain = Utils.effectiveTLD.getBaseDomainFromHost(item.docDomain).toUpperCase();
+ }
+ catch (e)
+ {
+ return;
+ }
+
+ // Generate text for new filter that excludes current domain
+ let text = filter.text;
+ if (filter instanceof RegExpFilter)
+ {
+ let match = Filter.optionsRegExp.exec(text);
+ if (match)
+ {
+ let found = false;
+ let options = match[1].toUpperCase().split(",");
+ for (let i = 0; i < options.length; i++)
+ {
+ let match = /^DOMAIN=(.*)/.exec(options[i]);
+ if (match)
+ {
+ let domains = match[1].split("|").filter(function(d) d != domain && d != "~" + domain && (d.length <= domain.length || d.lastIndexOf("." + domain) != d.length - domain.length - 1));
+ domains.push("~" + domain);
+ options[i] = "DOMAIN=" + domains.join("|");
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ options.push("DOMAIN=~" + domain);
+
+ text = text.replace(Filter.optionsRegExp, "$" + options.join(",").toLowerCase());
+ }
+ else
+ text += "$domain=~" + domain.toLowerCase();
+ }
+ else if (filter instanceof ElemHideFilter)
+ {
+ let match = /^([^#]+)(#.*)/.exec(text);
+ if (match)
+ {
+ let selector = match[2];
+ let domains = match[1].toUpperCase().split(",").filter(function(d) d != domain && (d.length <= domain.length || d != "~" + domain && d.lastIndexOf("." + domain) != d.length - domain.length - 1));
+ domains.push("~" + domain);
+ text = domains.join(",").toLowerCase() + selector;
+ }
+ else
+ text = "~" + domain.toLowerCase() + text;
+ }
+
+ if (text == filter.text)
+ return; // Just in case, shouldn't happen
+
+ // Insert new filter before the old one and remove the old one then
+ let newFilter = Filter.fromText(text);
+ if (newFilter.disabled && newFilter.subscriptions.length)
+ newFilter.disabled = false;
+ else if (!newFilter.subscriptions.length)
+ {
+ newFilter.disabled = false;
+ let subscription = filter.subscriptions.filter(function(s) s instanceof SpecialSubscription)[0];
+ if (subscription)
+ FilterStorage.addFilter(newFilter, subscription, subscription.filters.indexOf(filter));
+ }
+ FilterStorage.removeFilter(filter);
+
+ // Update display
+ for (let i = 0; i < treeView.allData.length; i++)
+ if (treeView.allData[i].filter == filter)
+ treeView.allData[i].filter = null;
+ treeView.boxObject.invalidate();
+}
+
+function copyToClipboard() {
+ var items = treeView.getAllSelectedItems();
+ if (!items.length)
+ return;
+
+ Utils.clipboardHelper.copyString(items.map(function(item) {return item.location}).join(IO.lineBreak));
+}
+
+function copyFilter() {
+ var items = treeView.getAllSelectedItems().filter(function(item) {return item.filter});
+ if (treeView.data && !treeView.data.length)
+ items = [treeView.getDummyTooltip()];
+
+ if (!items.length)
+ return;
+
+ Utils.clipboardHelper.copyString(items.map(function(item) {return item.filter.text}).join(IO.lineBreak));
+}
+
+function selectAll() {
+ treeView.selectAll();
+}
+
+// Saves sidebar's state before detaching/reattaching
+function saveState() {
+ var focused = document.commandDispatcher.focusedElement;
+ while (focused && (!focused.id || !("focus" in focused)))
+ focused = focused.parentNode;
+
+ // Calculate default position for the detached window
+ var boxObject = document.documentElement.boxObject;
+ var position = {screenX: boxObject.screenX, screenY: boxObject.screenY, width: boxObject.width, height: boxObject.height};
+
+ var params = {
+ filter: treeView.filter,
+ focus: (focused ? focused.id : null),
+ position: position
+ };
+ Utils.setParams(params);
+}
+
+// closes the sidebar
+function doClose()
+{
+ mainWin.document.getElementById("abp-command-sidebar").doCommand();
+}
+
+// detaches/reattaches the sidebar
+function detach(doDetach)
+{
+ saveState();
+
+ // Store variables locally, global variables will go away when we are closed
+ let myPrefs = Prefs;
+ let myMainWin = mainWin;
+
+ // Close sidebar and open detached window
+ myMainWin.document.getElementById("abp-command-sidebar").doCommand();
+ myPrefs.detachsidebar = doDetach;
+ myMainWin.document.getElementById("abp-command-sidebar").doCommand();
+}
+
+// Returns items size in the document if available
+function getItemSize(item)
+{
+ if (item.filter && !item.filter.disabled && item.filter instanceof BlockingFilter)
+ return null;
+
+ for each (let node in item.nodes)
+ {
+ if (node instanceof HTMLImageElement && (node.naturalWidth || node.naturalHeight))
+ return [node.naturalWidth, node.naturalHeight];
+ else if (node instanceof HTMLElement && (node.offsetWidth || node.offsetHeight))
+ return [node.offsetWidth, node.offsetHeight];
+ }
+ return null;
+}
+
+// Sort functions for the item list
+function sortByAddress(item1, item2) {
+ if (item1.location < item2.location)
+ return -1;
+ else if (item1.location > item2.location)
+ return 1;
+ else
+ return 0;
+}
+
+function sortByAddressDesc(item1, item2) {
+ return -sortByAddress(item1, item2);
+}
+
+function compareType(item1, item2) {
+ if (item1.localizedDescr < item2.localizedDescr)
+ return -1;
+ else if (item1.localizedDescr > item2.localizedDescr)
+ return 1;
+ else
+ return 0;
+}
+
+function compareFilter(item1, item2) {
+ var hasFilter1 = (item1.filter ? 1 : 0);
+ var hasFilter2 = (item2.filter ? 1 : 0);
+ if (hasFilter1 != hasFilter2)
+ return hasFilter1 - hasFilter2;
+ else if (hasFilter1 && item1.filter.text < item2.filter.text)
+ return -1;
+ else if (hasFilter1 && item1.filter.text > item2.filter.text)
+ return 1;
+ else
+ return 0;
+}
+
+function compareState(item1, item2) {
+ var state1 = (!item1.filter ? 0 : (item1.filter.disabled ? 1 : (item1.filter instanceof WhitelistFilter ? 2 : 3)));
+ var state2 = (!item2.filter ? 0 : (item2.filter.disabled ? 1 : (item2.filter instanceof WhitelistFilter ? 2 : 3)));
+ return state1 - state2;
+}
+
+function compareSize(item1, item2) {
+ var size1 = getItemSize(item1);
+ size1 = size1 ? size1[0] * size1[1] : 0;
+
+ var size2 = getItemSize(item2);
+ size2 = size2 ? size2[0] * size2[1] : 0;
+ return size1 - size2;
+}
+
+function compareDocDomain(item1, item2)
+{
+ if (item1.docDomain < item2.docDomain)
+ return -1;
+ else if (item1.docDomain > item2.docDomain)
+ return 1;
+ else if (item1.thirdParty && !item2.thirdParty)
+ return -1;
+ else if (!item1.thirdParty && item2.thirdParty)
+ return 1;
+ else
+ return 0;
+}
+
+function compareFilterSource(item1, item2)
+{
+ let subs1 = item1.filter ? item1.filter.subscriptions.map(function(s) s.title).join(", ") : "";
+ let subs2 = item2.filter ? item2.filter.subscriptions.map(function(s) s.title).join(", ") : "";
+ if (subs1 < subs2)
+ return -1;
+ else if (subs1 > subs2)
+ return 1;
+ else
+ return 0;
+}
+
+function createSortWithFallback(cmpFunc, fallbackFunc, desc) {
+ var factor = (desc ? -1 : 1);
+ return function(item1, item2) {
+ var ret = cmpFunc(item1, item2);
+ if (ret == 0)
+ return fallbackFunc(item1, item2);
+ else
+ return factor * ret;
+ }
+}
+
+var progressListener =
+{
+ onLocationChange: function(progress, request, uri, flags)
+ {
+ if (!flags || !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT))
+ handleLocationChange()
+ },
+ onProgressChange: function() {},
+ onSecurityChange: function() {},
+ onStateChange: function() {},
+ onStatusChange: function() {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
+};
+
+// Item list's tree view object
+var treeView = {
+ //
+ // nsISupports implementation
+ //
+
+ QueryInterface: function(uuid) {
+ if (!uuid.equals(Ci.nsISupports) &&
+ !uuid.equals(Ci.nsITreeView))
+ {
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+
+ return this;
+ },
+
+ //
+ // nsITreeView implementation
+ //
+
+ selection: null,
+
+ setTree: function(boxObject) {
+ if (!boxObject)
+ return;
+ this.boxObject = boxObject;
+ this.itemsDummy = boxObject.treeBody.getAttribute("noitemslabel");
+ this.whitelistDummy = boxObject.treeBody.getAttribute("whitelistedlabel");
+ var stringAtoms = ["col-address", "col-type", "col-filter", "col-state", "col-size", "col-docDomain", "col-filterSource", "state-regular", "state-filtered", "state-whitelisted", "state-hidden"];
+ var boolAtoms = ["selected", "dummy", "filter-disabled"];
+ var atomService = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);
+ this.atoms = {};
+ for each (let atom in stringAtoms)
+ this.atoms[atom] = atomService.getAtom(atom);
+ for each (let atom in boolAtoms)
+ {
+ this.atoms[atom + "-true"] = atomService.getAtom(atom + "-true");
+ this.atoms[atom + "-false"] = atomService.getAtom(atom + "-false");
+ }
+
+ this.itemsDummyTooltip = Utils.getString("no_blocking_suggestions");
+ this.whitelistDummyTooltip = Utils.getString("whitelisted_page");
+
+ // Check current sort direction
+ var cols = document.getElementsByTagName("treecol");
+ var sortDir = null;
+ for (let i = 0; i < cols.length; i++) {
+ var col = cols[i];
+ var dir = col.getAttribute("sortDirection");
+ if (dir && dir != "natural") {
+ this.sortColumn = col;
+ sortDir = dir;
+ }
+ }
+ if (!this.sortColumn)
+ {
+ let defaultSort = E("list").getAttribute("defaultSort");
+ let match = /^(\w+)\s+(ascending|descending)$/.exec(defaultSort);
+ if (match)
+ {
+ this.sortColumn = E(match[1]);
+ if (this.sortColumn)
+ {
+ sortDir = match[2];
+ this.sortColumn.setAttribute("sortDirection", sortDir);
+ }
+ }
+ }
+
+ if (sortDir)
+ {
+ this.sortProc = this.sortProcs[this.sortColumn.id + (sortDir == "descending" ? "Desc" : "")];
+ E("list").setAttribute("defaultSort", " ");
+ }
+
+ // Make sure to update the dummy row every two seconds
+ setInterval(function(view) {
+ if (!view.data || !view.data.length)
+ view.boxObject.invalidateRow(0);
+ }, 2000, this);
+
+ // Prevent a reference through closures
+ boxObject = null;
+ },
+ get rowCount() {
+ return (this.data && this.data.length ? this.data.length : 1);
+ },
+ getCellText: function(row, col) {
+ col = col.id;
+ if (col != "type" && col != "address" && col != "filter" && col != "size" && col != "docDomain" && col != "filterSource")
+ return "";
+ if (this.data && this.data.length) {
+ if (row >= this.data.length)
+ return "";
+ if (col == "type")
+ return this.data[row].localizedDescr;
+ else if (col == "filter")
+ return (this.data[row].filter ? this.data[row].filter.text : "");
+ else if (col == "size")
+ {
+ let size = getItemSize(this.data[row]);
+ return (size ? size.join(" x ") : "");
+ }
+ else if (col == "docDomain")
+ return this.data[row].docDomain + " " + (this.data[row].thirdParty ? docDomainThirdParty : docDomainFirstParty);
+ else if (col == "filterSource")
+ {
+ if (!this.data[row].filter)
+ return "";
+
+ return this.data[row].filter.subscriptions.filter(function(s) !s.disabled).map(function(s) s.title).join(", ");
+ }
+ else
+ return this.data[row].location;
+ }
+ else {
+ // Empty list, show dummy
+ if (row > 0 || (col != "address" && col != "filter"))
+ return "";
+ if (col == "filter") {
+ var filter = Policy.isWindowWhitelisted(window.content);
+ return filter ? filter.text : "";
+ }
+
+ return (Policy.isWindowWhitelisted(window.content) ? this.whitelistDummy : this.itemsDummy);
+ }
+ },
+
+ generateProperties: function(list, properties)
+ {
+ if (properties)
+ {
+ // Gecko 21 and below: we have an nsISupportsArray parameter, add atoms
+ // to that.
+ for (let i = 0; i < list.length; i++)
+ if (list[i] in this.atoms)
+ properties.AppendElement(this.atoms[list[i]]);
+ return null;
+ }
+ else
+ {
+ // Gecko 22+: no parameter, just return a string
+ return list.join(" ");
+ }
+ },
+
+ getColumnProperties: function(col, properties)
+ {
+ return this.generateProperties(["col-" + col.id], properties);
+ },
+
+ getRowProperties: function(row, properties)
+ {
+ if (row >= this.rowCount)
+ return "";
+
+ let list = [];
+ list.push("selected-" + this.selection.isSelected(row));
+
+ let state;
+ if (this.data && this.data.length) {
+ list.push("dummy-false");
+
+ let filter = this.data[row].filter;
+ if (filter)
+ list.push("filter-disabled-" + filter.disabled);
+
+ state = "state-regular";
+ if (filter && !filter.disabled)
+ {
+ if (filter instanceof WhitelistFilter)
+ state = "state-whitelisted";
+ else if (filter instanceof BlockingFilter)
+ state = "state-filtered";
+ else if (filter instanceof ElemHideFilter)
+ state = "state-hidden";
+ }
+ }
+ else {
+ list.push("dummy-true");
+
+ state = "state-filtered";
+ if (this.data && Policy.isWindowWhitelisted(window.content))
+ state = "state-whitelisted";
+ }
+ list.push(state);
+ return this.generateProperties(list, properties);
+ },
+
+ getCellProperties: function(row, col, properties)
+ {
+ return this.getRowProperties(row, properties) + " " + this.getColumnProperties(col, properties);
+ },
+
+ cycleHeader: function(col) {
+ col = col.id;
+
+ col = E(col);
+ if (!col)
+ return;
+
+ var cycle = {
+ natural: 'ascending',
+ ascending: 'descending',
+ descending: 'natural'
+ };
+
+ var curDirection = "natural";
+ if (this.sortColumn == col)
+ curDirection = col.getAttribute("sortDirection");
+ else if (this.sortColumn)
+ this.sortColumn.removeAttribute("sortDirection");
+
+ curDirection = cycle[curDirection];
+
+ if (curDirection == "natural")
+ this.sortProc = null;
+ else
+ this.sortProc = this.sortProcs[col.id + (curDirection == "descending" ? "Desc" : "")];
+
+ if (this.data)
+ this.refilter();
+
+ col.setAttribute("sortDirection", curDirection);
+ this.sortColumn = col;
+
+ this.boxObject.invalidate();
+ },
+
+ isSorted: function() {
+ return this.sortProc;
+ },
+
+ isContainer: function() {return false},
+ isContainerOpen: function() {return false},
+ isContainerEmpty: function() {return false},
+ getLevel: function() {return 0},
+ getParentIndex: function() {return -1},
+ hasNextSibling: function() {return false},
+ toggleOpenState: function() {},
+ canDrop: function() {return false},
+ drop: function() {},
+ getCellValue: function() {return null},
+ getProgressMode: function() {return null},
+ getImageSrc: function() {return null},
+ isSeparator: function() {return false},
+ isEditable: function() {return false},
+ cycleCell: function() {},
+ performAction: function() {},
+ performActionOnRow: function() {},
+ performActionOnCell: function() {},
+ selectionChanged: function() {},
+
+ //
+ // Custom properties and methods
+ //
+
+ boxObject: null,
+ atoms: null,
+ filter: "",
+ data: null,
+ allData: [],
+ dataMap: {__proto__: null},
+ sortColumn: null,
+ sortProc: null,
+ resortTimeout: null,
+ itemsDummy: null,
+ whitelistDummy: null,
+ itemsDummyTooltip: null,
+ whitelistDummyTooltip: null,
+ itemToSelect: null,
+
+ sortProcs: {
+ address: sortByAddress,
+ addressDesc: sortByAddressDesc,
+ type: createSortWithFallback(compareType, sortByAddress, false),
+ typeDesc: createSortWithFallback(compareType, sortByAddress, true),
+ filter: createSortWithFallback(compareFilter, sortByAddress, false),
+ filterDesc: createSortWithFallback(compareFilter, sortByAddress, true),
+ state: createSortWithFallback(compareState, sortByAddress, false),
+ stateDesc: createSortWithFallback(compareState, sortByAddress, true),
+ size: createSortWithFallback(compareSize, sortByAddress, false),
+ sizeDesc: createSortWithFallback(compareSize, sortByAddress, true),
+ docDomain: createSortWithFallback(compareDocDomain, sortByAddress, false),
+ docDomainDesc: createSortWithFallback(compareDocDomain, sortByAddress, true),
+ filterSource: createSortWithFallback(compareFilterSource, sortByAddress, false),
+ filterSourceDesc: createSortWithFallback(compareFilterSource, sortByAddress, true)
+ },
+ clearData: function(data) {
+ var oldRows = this.rowCount;
+ this.allData = [];
+ this.dataMap = {__proto__: null};
+ this.refilter();
+
+ this.boxObject.rowCountChanged(0, -oldRows);
+ this.boxObject.rowCountChanged(0, this.rowCount);
+ },
+
+ addItem: function(/**Node*/ node, /**RequestEntry*/ item, /**Boolean*/ scanComplete)
+ {
+ // Merge duplicate entries
+ let key = item.location + " " + item.type + " " + item.docDomain;
+ if (key in this.dataMap)
+ {
+ // We know this item already - take over the filter if any and be done with it
+ let existing = this.dataMap[key];
+ if (item.filter)
+ existing.filter = item.filter;
+
+ existing.nodes.push(node);
+ this.invalidateItem(existing);
+ return;
+ }
+
+ // Add new item to the list
+ // Store original item in orig property - reading out prototype is messed up in Gecko 1.9.2
+ item = {__proto__: item, orig: item, nodes: [node]};
+ this.allData.push(item);
+ this.dataMap[key] = item;
+
+ // Show disabled filters if no other filter applies
+ if (!item.filter)
+ item.filter = disabledMatcher.matchesAny(item.location, item.typeDescr, item.docDomain, item.thirdParty);
+
+ if (!this.matchesFilter(item))
+ return;
+
+ let index = -1;
+ if (this.sortProc && this.sortColumn && this.sortColumn.id == "size")
+ {
+ // Sorting by size requires accessing content document, and that's
+ // dangerous from a content policy (and we are likely called directly
+ // from a content policy call). Size data will be inaccurate anyway,
+ // delay sorting until later.
+ if (this.resortTimeout)
+ clearTimeout(this.resortTimeout);
+ this.resortTimeout = setTimeout(function(me)
+ {
+ if (me.sortProc)
+ me.data.sort(me.sortProc);
+ me.boxObject.invalidate();
+ }, 500, this);
+ }
+ else if (this.sortProc)
+ for (var i = 0; index < 0 && i < this.data.length; i++)
+ if (this.sortProc(item, this.data[i]) < 0)
+ index = i;
+
+ if (index >= 0)
+ this.data.splice(index, 0, item);
+ else {
+ this.data.push(item);
+ index = this.data.length - 1;
+ }
+
+ if (this.data.length == 1)
+ this.boxObject.invalidateRow(0);
+ else
+ this.boxObject.rowCountChanged(index, 1);
+
+ if (this.itemToSelect == key)
+ {
+ this.selection.select(index);
+ this.boxObject.ensureRowIsVisible(index);
+ this.itemToSelect = null;
+ }
+ else if (!scanComplete && this.selection.currentIndex >= 0) // Keep selected row visible while scanning
+ this.boxObject.ensureRowIsVisible(this.selection.currentIndex);
+ },
+
+ updateFilters: function()
+ {
+ for each (let item in this.allData)
+ {
+ if (item.filter instanceof RegExpFilter && item.filter.disabled)
+ delete item.filter;
+ if (!item.filter)
+ item.filter = disabledMatcher.matchesAny(item.location, item.typeDescr, item.docDomain, item.thirdParty);
+ }
+ this.refilter();
+ },
+
+ /**
+ * Updates the list after a filter or sorting change.
+ */
+ refilter: function()
+ {
+ if (this.resortTimeout)
+ clearTimeout(this.resortTimeout);
+
+ this.data = this.allData.filter(this.matchesFilter, this);
+
+ if (this.sortProc)
+ this.data.sort(this.sortProc);
+ },
+
+ /**
+ * Tests whether an item matches current list filter.
+ * @return {Boolean} true if the item should be shown
+ */
+ matchesFilter: function(item)
+ {
+ if (!this.filter)
+ return true;
+
+ return (item.location.toLowerCase().indexOf(this.filter) >= 0 ||
+ (item.filter && item.filter.text.toLowerCase().indexOf(this.filter) >= 0) ||
+ item.typeDescr.toLowerCase().indexOf(this.filter.replace(/-/g, "_")) >= 0 ||
+ item.localizedDescr.toLowerCase().indexOf(this.filter) >= 0 ||
+ (item.docDomain && item.docDomain.toLowerCase().indexOf(this.filter) >= 0) ||
+ (item.docDomain && item.thirdParty && docDomainThirdParty.toLowerCase().indexOf(this.filter) >= 0) ||
+ (item.docDomain && !item.thirdParty && docDomainFirstParty.toLowerCase().indexOf(this.filter) >= 0));
+ },
+
+ setFilter: function(filter) {
+ var oldRows = this.rowCount;
+
+ this.filter = filter.toLowerCase();
+ this.refilter();
+
+ var newRows = this.rowCount;
+ if (oldRows != newRows)
+ this.boxObject.rowCountChanged(oldRows < newRows ? oldRows : newRows, this.rowCount - oldRows);
+ this.boxObject.invalidate();
+ },
+
+ selectAll: function() {
+ this.selection.selectAll();
+ },
+
+ getSelectedItem: function() {
+ if (!this.data || this.selection.currentIndex < 0 || this.selection.currentIndex >= this.data.length)
+ return null;
+
+ return this.data[this.selection.currentIndex];
+ },
+
+ getAllSelectedItems: function() {
+ let result = [];
+ if (!this.data)
+ return result;
+
+ let numRanges = this.selection.getRangeCount();
+ for (let i = 0; i < numRanges; i++)
+ {
+ let min = {};
+ let max = {};
+ let range = this.selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; j++)
+ {
+ if (j >= 0 && j < this.data.length)
+ result.push(this.data[j]);
+ }
+ }
+ return result;
+ },
+
+ getItemAt: function(x, y)
+ {
+ if (!this.data)
+ return null;
+
+ var row = this.boxObject.getRowAt(x, y);
+ if (row < 0 || row >= this.data.length)
+ return null;
+
+ return this.data[row];
+ },
+
+ getColumnAt: function(x, y)
+ {
+ if (!this.data)
+ return null;
+
+ let col = {};
+ this.boxObject.getCellAt(x, y, {}, col, {});
+ return (col.value ? col.value.id : null);
+ },
+
+ getDummyTooltip: function() {
+ if (!this.data || this.data.length)
+ return null;
+
+ var filter = Policy.isWindowWhitelisted(window.content);
+ if (filter)
+ return {tooltip: this.whitelistDummyTooltip, filter: filter};
+ else
+ return {tooltip: this.itemsDummyTooltip};
+ },
+
+ invalidateItem: function(item)
+ {
+ let row = this.data.indexOf(item);
+ if (row >= 0)
+ this.boxObject.invalidateRow(row);
+ }
+}
diff --git a/abprime/content/sidebar.xul b/abprime/content/sidebar.xul
new file mode 100644
index 00000000..626615be
--- /dev/null
+++ b/abprime/content/sidebar.xul
@@ -0,0 +1,128 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
diff --git a/abprime/content/sidebarDetached.xul b/abprime/content/sidebarDetached.xul
new file mode 100644
index 00000000..49a24088
--- /dev/null
+++ b/abprime/content/sidebarDetached.xul
@@ -0,0 +1,39 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
diff --git a/abprime/content/subscriptionSelection.js b/abprime/content/subscriptionSelection.js
new file mode 100644
index 00000000..390b2060
--- /dev/null
+++ b/abprime/content/subscriptionSelection.js
@@ -0,0 +1,296 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let subscriptionListLoading = false;
+
+function init()
+{
+ if (window.arguments && window.arguments.length && window.arguments[0])
+ {
+ let source = window.arguments[0];
+ setCustomSubscription(source.title, source.url,
+ source.mainSubscriptionTitle, source.mainSubscriptionURL);
+
+ E("all-subscriptions-container").hidden = true;
+ E("fromWebText").hidden = false;
+ }
+ else
+ loadSubscriptionList();
+}
+
+function updateSubscriptionInfo()
+{
+ let selectedSubscription = E("all-subscriptions").selectedItem;
+
+ E("subscriptionInfo").setAttribute("invisible", !selectedSubscription);
+ if (selectedSubscription)
+ {
+ let url = selectedSubscription.getAttribute("_url");
+ let homePage = selectedSubscription.getAttribute("_homepage")
+
+ let viewLink = E("view-list");
+ viewLink.setAttribute("_url", url);
+ viewLink.setAttribute("tooltiptext", url);
+
+ let homePageLink = E("visit-homepage");
+ homePageLink.hidden = !homePage;
+ if (homePage)
+ {
+ homePageLink.setAttribute("_url", homePage);
+ homePageLink.setAttribute("tooltiptext", homePage);
+ }
+ }
+}
+
+function reloadSubscriptionList()
+{
+ subscriptionListLoading = false;
+ loadSubscriptionList();
+}
+
+function loadSubscriptionList()
+{
+ if (subscriptionListLoading)
+ return;
+
+ E("all-subscriptions-container").selectedIndex = 0;
+ E("all-subscriptions-loading").hidden = false;
+
+ let request = new XMLHttpRequest();
+ let errorHandler = function()
+ {
+ E("all-subscriptions-container").selectedIndex = 2;
+ E("all-subscriptions-loading").hidden = true;
+ };
+ let successHandler = function()
+ {
+ if (!request.responseXML || request.responseXML.documentElement.localName != "subscriptions")
+ {
+ errorHandler();
+ return;
+ }
+
+ try
+ {
+ processSubscriptionList(request.responseXML);
+ E("all-subscriptions").selectedIndex = 0;
+ E("all-subscriptions").focus();
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ errorHandler();
+ }
+ };
+
+ request.open("GET", Prefs.subscriptions_listurl);
+ request.addEventListener("error", errorHandler, false);
+ request.addEventListener("load", successHandler, false);
+ request.send(null);
+
+ subscriptionListLoading = true;
+}
+
+function processSubscriptionList(doc)
+{
+ let list = E("all-subscriptions");
+ while (list.firstChild)
+ list.removeChild(list.firstChild);
+
+ addSubscriptions(list, doc.documentElement, 0, null, null);
+ E("all-subscriptions-container").selectedIndex = 1;
+ E("all-subscriptions-loading").hidden = true;
+}
+
+function addSubscriptions(list, parent, level, parentTitle, parentURL)
+{
+ for (let i = 0; i < parent.childNodes.length; i++)
+ {
+ let node = parent.childNodes[i];
+ if (node.nodeType != Node.ELEMENT_NODE || node.localName != "subscription")
+ continue;
+
+ if (node.getAttribute("type") != "ads" || node.getAttribute("deprecated") == "true")
+ continue;
+
+ let variants = node.getElementsByTagName("variants");
+ if (!variants.length || !variants[0].childNodes.length)
+ continue;
+ variants = variants[0].childNodes;
+
+ let isFirst = true;
+ let mainTitle = null;
+ let mainURL = null;
+ for (let j = 0; j < variants.length; j++)
+ {
+ let variant = variants[j];
+ if (variant.nodeType != Node.ELEMENT_NODE || variant.localName != "variant")
+ continue;
+
+ let item = document.createElement("richlistitem");
+ item.setAttribute("_title", variant.getAttribute("title"));
+ item.setAttribute("_url", variant.getAttribute("url"));
+ if (parentTitle && parentURL && variant.getAttribute("complete") != "true")
+ {
+ item.setAttribute("_supplementForTitle", parentTitle);
+ item.setAttribute("_supplementForURL", parentURL);
+ }
+ item.setAttribute("tooltiptext", variant.getAttribute("url"));
+ item.setAttribute("_homepage", node.getAttribute("homepage"));
+
+ let title = document.createElement("description");
+ if (isFirst)
+ {
+ if (Utils.checkLocalePrefixMatch(node.getAttribute("prefixes")))
+ title.setAttribute("class", "subscriptionTitle localeMatch");
+ else
+ title.setAttribute("class", "subscriptionTitle");
+ title.textContent = node.getAttribute("title") + " (" + node.getAttribute("specialization") + ")";
+ mainTitle = variant.getAttribute("title");
+ mainURL = variant.getAttribute("url");
+ isFirst = false;
+ }
+ title.setAttribute("flex", "1");
+ title.style.marginLeft = (20 * level) + "px";
+ item.appendChild(title);
+
+ let variantTitle = document.createElement("description");
+ variantTitle.setAttribute("class", "variant");
+ variantTitle.textContent = variant.getAttribute("title");
+ variantTitle.setAttribute("crop", "end");
+ item.appendChild(variantTitle);
+
+ list.appendChild(item);
+ }
+
+ let supplements = node.getElementsByTagName("supplements");
+ if (supplements.length)
+ addSubscriptions(list, supplements[0], level + 1, mainTitle, mainURL);
+ }
+}
+
+function onSelectionChange()
+{
+ let selectedItem = E("all-subscriptions").selectedItem;
+ if (!selectedItem)
+ return;
+
+ setCustomSubscription(selectedItem.getAttribute("_title"), selectedItem.getAttribute("_url"),
+ selectedItem.getAttribute("_supplementForTitle"), selectedItem.getAttribute("_supplementForURL"));
+
+ updateSubscriptionInfo();
+}
+
+function setCustomSubscription(title, url, mainSubscriptionTitle, mainSubscriptionURL)
+{
+ E("title").value = title;
+ E("location").value = url;
+
+ let messageElement = E("supplementMessage");
+ let addMainCheckbox = E("addMainSubscription");
+ if (mainSubscriptionURL && !hasSubscription(mainSubscriptionURL))
+ {
+ messageElement.removeAttribute("invisible");
+ addMainCheckbox.removeAttribute("invisible");
+
+ let [, beforeLink, afterLink] = /(.*)\?1\?(.*)/.exec(messageElement.getAttribute("_textTemplate")) || [null, messageElement.getAttribute("_textTemplate"), ""];
+ while (messageElement.firstChild)
+ messageElement.removeChild(messageElement.firstChild);
+ messageElement.appendChild(document.createTextNode(beforeLink));
+ let link = document.createElement("label");
+ link.className = "text-link";
+ link.setAttribute("tooltiptext", mainSubscriptionURL);
+ link.addEventListener("click", function() Utils.loadInBrowser(mainSubscriptionURL), false);
+ link.textContent = mainSubscriptionTitle;
+ messageElement.appendChild(link);
+ messageElement.appendChild(document.createTextNode(afterLink));
+
+ addMainCheckbox.value = mainSubscriptionURL;
+ addMainCheckbox.setAttribute("_mainSubscriptionTitle", mainSubscriptionTitle)
+ addMainCheckbox.label = addMainCheckbox.getAttribute("_labelTemplate").replace(/\?1\?/g, mainSubscriptionTitle);
+ addMainCheckbox.accessKey = addMainCheckbox.accessKey;
+ }
+ else
+ {
+ messageElement.setAttribute("invisible", "true");
+ addMainCheckbox.setAttribute("invisible", "true");
+ }
+}
+
+function validateURL(url)
+{
+ if (!url)
+ return null;
+ url = url.replace(/^\s+/, "").replace(/\s+$/, "");
+
+ // Is this a file path?
+ try {
+ let file = new FileUtils.File(url);
+ return Services.io.newFileURI(file).spec;
+ } catch (e) {}
+
+ // Is this a valid URL?
+ let uri = Utils.makeURI(url);
+ if (uri)
+ return uri.spec;
+
+ return null;
+}
+
+function addSubscription()
+{
+ let url = E("location").value;
+ url = validateURL(url);
+ if (!url)
+ {
+ Utils.alert(window, Utils.getString("subscription_invalid_location"));
+ E("location").focus();
+ return false;
+ }
+
+ let title = E("title").value.replace(/^\s+/, "").replace(/\s+$/, "");
+ if (!title)
+ title = url;
+
+ doAddSubscription(url, title);
+
+ let addMainCheckbox = E("addMainSubscription")
+ if (addMainCheckbox.getAttribute("invisible") != "true" && addMainCheckbox.checked)
+ {
+ let mainSubscriptionTitle = addMainCheckbox.getAttribute("_mainSubscriptionTitle");
+ let mainSubscriptionURL = validateURL(addMainCheckbox.value);
+ if (mainSubscriptionURL)
+ doAddSubscription(mainSubscriptionURL, mainSubscriptionTitle);
+ }
+
+ return true;
+}
+
+/**
+ * Adds a new subscription to the list.
+ */
+function doAddSubscription(/**String*/ url, /**String*/ title)
+{
+ let subscription = Subscription.fromURL(url);
+ if (!subscription)
+ return;
+
+ FilterStorage.addSubscription(subscription);
+
+ subscription.disabled = false;
+ subscription.title = title;
+
+ if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
+ Synchronizer.execute(subscription);
+}
+
+function hasSubscription(url)
+{
+ return FilterStorage.subscriptions.some(function(subscription) subscription instanceof DownloadableSubscription && subscription.url == url);
+}
diff --git a/abprime/content/subscriptionSelection.xul b/abprime/content/subscriptionSelection.xul
new file mode 100644
index 00000000..fb23917e
--- /dev/null
+++ b/abprime/content/subscriptionSelection.xul
@@ -0,0 +1,64 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &fromWeb.description;
+
+
+
+
+
+
+
+
+
+
+ &supplementMessage;
+ dummy dummy dummy dummy dummy dummy dummy dummy dummy dummy
+
+
+
+
diff --git a/abprime/content/subscriptions.xml b/abprime/content/subscriptions.xml
new file mode 100644
index 00000000..0addbfdf
--- /dev/null
+++ b/abprime/content/subscriptions.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/abprime/content/utils.js b/abprime/content/utils.js
new file mode 100644
index 00000000..5df6c13f
--- /dev/null
+++ b/abprime/content/utils.js
@@ -0,0 +1,36 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "AppIntegration.jsm");
+Cu.import(baseURL + "ContentPolicy.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "FilterListener.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "IO.jsm");
+Cu.import(baseURL + "Matcher.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "RequestNotifier.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+Cu.import(baseURL + "Synchronizer.jsm");
+/* Cu.import(baseURL + "Sync.jsm"); */
+Cu.import(baseURL + "Utils.jsm");
+
+/**
+ * Shortcut for document.getElementById(id)
+ */
+function E(id)
+{
+ return document.getElementById(id);
+}
diff --git a/abprime/locale/about.dtd b/abprime/locale/about.dtd
new file mode 100644
index 00000000..a8614a31
--- /dev/null
+++ b/abprime/locale/about.dtd
@@ -0,0 +1,16 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/composer.dtd b/abprime/locale/composer.dtd
new file mode 100644
index 00000000..e464f9d5
--- /dev/null
+++ b/abprime/locale/composer.dtd
@@ -0,0 +1,53 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/filters.dtd b/abprime/locale/filters.dtd
new file mode 100644
index 00000000..850f4253
--- /dev/null
+++ b/abprime/locale/filters.dtd
@@ -0,0 +1,116 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Z sort order">
+
+ A sort order">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/firstRun.dtd b/abprime/locale/firstRun.dtd
new file mode 100644
index 00000000..7174132d
--- /dev/null
+++ b/abprime/locale/firstRun.dtd
@@ -0,0 +1,27 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/global.properties b/abprime/locale/global.properties
new file mode 100644
index 00000000..9870579a
--- /dev/null
+++ b/abprime/locale/global.properties
@@ -0,0 +1,60 @@
+#filter substitution
+
+default_dialog_title=@ADDON_NAME@
+
+action0_tooltip=Click to bring up context menu, middle-click to enable/disable.
+action1_tooltip=Click to open/close blockable items, middle-click to enable/disable.
+action2_tooltip=Click to open preferences, middle-click to enable/disable.
+action3_tooltip=Click to enable/disable @ADDON_NAME@.
+
+disabled_tooltip=@ADDON_NAME@ is disabled.
+# Note: the placeholder ?1? will be replaced by the number of active filter subscriptions, the placeholder ?2? by the number of custom filters
+active_tooltip=@ADDON_NAME@ is enabled, ?1? filter subscription(s) and ?2? custom filter(s) in use.
+whitelisted_tooltip=@ADDON_NAME@ is disabled on current page.
+
+# Note: the placeholder ?1? will be replaced by the number of blocked items, the placeholder ?2? by the total number of items on current page
+blocked_count_tooltip=?1? out of ?2?
+# Note: the placeholder ?1? will be replaced by the number of whitelisted items, the placeholder ?2? by the number of hidden items on current page
+blocked_count_addendum=(also whitelisted: ?1?, hidden: ?2?)
+
+no_blocking_suggestions=No blockable items on the current page
+whitelisted_page=@ADDON_NAME@ has been disabled for the current page
+
+newGroup_title=New filter group
+whitelistGroup_title=Exception Rules
+blockingGroup_title=Ad Blocking Rules
+elemhideGroup_title=Element Hiding Rules
+
+remove_subscription_warning=Do you really want to remove this subscription?
+remove_group_warning=Do you really want to remove this group?
+clearStats_warning=This will reset all filter hit statistics and disable counting filter hits. Do you want to proceed?
+
+filter_regexp_tooltip=This filter is either a regular expression or too short to be optimized. Too many of these filters might slow down your browsing.
+filter_elemhide_duplicate_id=Only one ID of the element to be hidden can be specified
+filter_elemhide_nocriteria=No criteria specified to recognize the element to be hidden
+
+subscription_invalid_location=Filter list location is neither a valid URL nor a valid file name.
+
+type_label_other=other
+type_label_script=script
+type_label_image=image
+type_label_stylesheet=stylesheet
+type_label_object=object
+type_label_subdocument=frame
+type_label_document=document
+type_label_elemhide=hidden
+type_label_popup=pop-up window
+type_label_websocket=websocket
+type_label_webrtc=webtrc
+type_label_csp=csp
+
+type_label_xmlhttprequest=XML request
+type_label_object_subrequest=object subrequest
+type_label_media=audio/video
+type_label_font=font
+
+mobile_menu_enable=ABP: Enable
+# Note: the placeholder ?1? will be replaced by the site name. Ideally it should be at the end of the string (space is limited and site names can be long).
+mobile_menu_enable_site=ABP: Enable on ?1?
+# Note: the placeholder ?1? will be replaced by the site name. Ideally it should be at the end of the string (space is limited and site names can be long).
+mobile_menu_disable_site=ABP: Disable on ?1?
diff --git a/abprime/locale/jar.mn b/abprime/locale/jar.mn
new file mode 100644
index 00000000..f2c29aa2
--- /dev/null
+++ b/abprime/locale/jar.mn
@@ -0,0 +1,17 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% locale @ADDON_CHROME_NAME@ en-US chrome/locale/
+* locale/about.dtd
+* locale/composer.dtd
+* locale/filters.dtd
+* locale/firstRun.dtd
+* locale/global.properties
+* locale/overlay.dtd
+* locale/sendReport.dtd
+* locale/sidebar.dtd
+* locale/subscriptionSelection.dtd
\ No newline at end of file
diff --git a/abprime/locale/moz.build b/abprime/locale/moz.build
new file mode 100644
index 00000000..e0eb66aa
--- /dev/null
+++ b/abprime/locale/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/abprime/locale/overlay.dtd b/abprime/locale/overlay.dtd
new file mode 100644
index 00000000..1d60ab1d
--- /dev/null
+++ b/abprime/locale/overlay.dtd
@@ -0,0 +1,54 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/sendReport.dtd b/abprime/locale/sendReport.dtd
new file mode 100644
index 00000000..65eecad9
--- /dev/null
+++ b/abprime/locale/sendReport.dtd
@@ -0,0 +1,201 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/sidebar.dtd b/abprime/locale/sidebar.dtd
new file mode 100644
index 00000000..3753195d
--- /dev/null
+++ b/abprime/locale/sidebar.dtd
@@ -0,0 +1,44 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/locale/subscriptionSelection.dtd b/abprime/locale/subscriptionSelection.dtd
new file mode 100644
index 00000000..e905309b
--- /dev/null
+++ b/abprime/locale/subscriptionSelection.dtd
@@ -0,0 +1,27 @@
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/abprime/modules/AppIntegration.jsm b/abprime/modules/AppIntegration.jsm
new file mode 100644
index 00000000..1bc03262
--- /dev/null
+++ b/abprime/modules/AppIntegration.jsm
@@ -0,0 +1,1667 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Application integration module, will keep track of application
+ * windows and handle the necessary events.
+ */
+
+var EXPORTED_SYMBOLS = ["AppIntegration"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "ContentPolicy.jsm");
+Cu.import(baseURL + "FilterListener.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+Cu.import(baseURL + "RequestNotifier.jsm");
+Cu.import(baseURL + "Synchronizer.jsm");
+/* Cu.import(baseURL + "Sync.jsm"); */
+
+/**
+ * Wrappers for tracked application windows.
+ * @type Array of WindowWrapper
+ */
+let wrappers = [];
+
+/**
+ * Stores the selected hotkeys, initialized when the first browser window opens.
+ */
+let hotkeys = null;
+
+/**
+ * Object observing add-on manager notifications about add-on options being initialized.
+ * @type nsIObserver
+ */
+let optionsObserver =
+{
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+ observe: function(subject, topic, data)
+ {
+ if (data != Utils.addonID)
+ return;
+
+ initOptionsDoc(subject.QueryInterface(Ci.nsIDOMDocument));
+ }
+};
+
+/**
+ * Initializes app integration module
+ */
+function init()
+{
+ // Process preferences
+ reloadPrefs();
+
+ // Listen for pref and filters changes
+ Prefs.addListener(function(name)
+ {
+ if (name == "enabled" || name == "showinstatusbar" || name == "defaulttoolbaraction" || name == "defaultstatusbaraction")
+ reloadPrefs();
+ });
+ FilterNotifier.addListener(function(action)
+ {
+ if (/^(filter|subscription)\.(added|removed|disabled|updated)$/.test(action) || action == "load")
+ reloadPrefs();
+ });
+ Services.obs.addObserver(optionsObserver, "addon-options-displayed", true);
+}
+
+/**
+ * Exported app integration functions.
+ * @class
+ */
+var AppIntegration =
+{
+ /**
+ * Adds an application window to the tracked list.
+ */
+ addWindow: function(/**Window*/ window)
+ {
+ let hooks = window.document.getElementById("abp-hooks");
+ if (!hooks)
+ return;
+
+ TimeLine.enter("Entered AppIntegration.addWindow()")
+ // Execute first-run actions
+ if (!("lastVersion" in Prefs))
+ {
+ Prefs.lastVersion = Prefs.currentVersion;
+
+ // Show subscriptions dialog if the user doesn't have any subscriptions yet
+ if (Prefs.currentVersion != Utils.addonVersion)
+ {
+ Prefs.currentVersion = Utils.addonVersion;
+
+ if ("nsISessionStore" in Ci)
+ {
+ // Have to wait for session to be restored
+ let observer =
+ {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+ observe: function(subject, topic, data)
+ {
+ Services.obs.removeObserver(observer, "sessionstore-windows-restored");
+ timer.cancel();
+ timer = null;
+ addSubscription();
+ }
+ };
+
+ Services.obs.addObserver(observer, "sessionstore-windows-restored", false);
+
+ // Just in case, don't wait more than a second
+ let timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
+ timer.init(observer, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ else
+ addSubscription();
+ }
+ }
+ TimeLine.log("App-wide first-run actions done")
+
+ let wrapper = new WindowWrapper(window, hooks);
+ wrappers.push(wrapper);
+ TimeLine.leave("AppIntegration.addWindow() done")
+ },
+
+ /**
+ * Retrieves the wrapper object corresponding to a particular application window.
+ */
+ getWrapperForWindow: function(/**Window*/ wnd) /**WindowWrapper*/
+ {
+ for each (let wrapper in wrappers)
+ if (wrapper.window == wnd)
+ return wrapper;
+
+ return null;
+ },
+
+ /**
+ * Toggles the value of a boolean preference.
+ */
+ togglePref: function(/**String*/ pref)
+ {
+ Prefs[pref] = !Prefs[pref];
+ },
+
+ /**
+ * Toggles the pref for the Adblock Plus sync engine.
+ * @return {Boolean} new state of the sync engine
+ */
+ /** toggleSync: function()
+ {
+ let syncEngine = Sync.getEngine();
+ syncEngine.enabled = !syncEngine.enabled;
+ return syncEngine.enabled;
+ },
+ */
+
+ /**
+ * Adds or removes the Adblock Plus toolbar icon.
+ * @return {Boolean} new state of the toolbar button
+ */
+ toggleToolbarIcon: function()
+ {
+ if (!wrappers.length)
+ return false;
+
+ let newVal = !wrappers[0].isToolbarIconVisible();
+ for (let i = 0; i < wrappers.length; i++)
+ {
+ if (newVal)
+ wrappers[i].installToolbarIcon();
+ else
+ wrappers[i].hideToolbarIcon();
+ }
+ return wrappers[0].isToolbarIconVisible();
+ },
+
+ /**
+ * If the given filter is already in user's list, removes it from the list. Otherwise adds it.
+ */
+ toggleFilter: function(/**Filter*/ filter)
+ {
+ if (filter.subscriptions.length)
+ {
+ if (filter.disabled || filter.subscriptions.some(function(subscription) !(subscription instanceof SpecialSubscription)))
+ filter.disabled = !filter.disabled;
+ else
+ FilterStorage.removeFilter(filter);
+ }
+ else
+ FilterStorage.addFilter(filter);
+ },
+
+ /**
+ * Opens ABP menu.
+ */
+ openMenu: function(window)
+ {
+ let wrapper = AppIntegration.getWrapperForWindow(window.top);
+ if (!wrapper)
+ {
+ // Maybe we got a content window
+ window = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ if (window.wrappedJSObject)
+ window = window.wrappedJSObject;
+ wrapper = AppIntegration.getWrapperForWindow(window);
+ }
+ if (!wrapper)
+ {
+ // Try to find any known window
+ let enumerator = Utils.windowMediator.getZOrderDOMWindowEnumerator(null, true);
+ if (!enumerator.hasMoreElements())
+ {
+ // On Linux the list returned will be empty, see bug 156333. Fall back to random order.
+ enumerator = Utils.windowMediator.getEnumerator(null);
+ }
+ while (enumerator.hasMoreElements())
+ {
+ window = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
+ wrapper = AppIntegration.getWrapperForWindow(window);
+ if (wrapper)
+ break;
+ }
+ }
+
+ if (wrapper)
+ Utils.runAsync(wrapper.openMenu, wrapper);
+ }
+};
+
+/**
+ * Removes an application window from the tracked list.
+ */
+function removeWindow()
+{
+ let wnd = this;
+
+ for (let i = 0; i < wrappers.length; i++)
+ if (wrappers[i].window == wnd)
+ wrappers.splice(i--, 1);
+}
+
+/**
+ * Class providing various functions related to application windows.
+ * @constructor
+ */
+function WindowWrapper(window, hooks)
+{
+ TimeLine.enter("Entered WindowWrapper constructor")
+ this.window = window;
+
+ this.initializeHooks(hooks);
+ TimeLine.log("Hooks element initialized")
+
+ this.fixupMenus();
+ TimeLine.log("Context menu copying done")
+
+ this.configureKeys();
+ TimeLine.log("Shortcut keys configured")
+
+ this.initContextMenu();
+ TimeLine.log("Context menu initialized")
+
+ let browser = this.getBrowser();
+ if (browser && browser.currentURI)
+ {
+ this.updateState();
+ }
+ else
+ {
+ // Update state asynchronously, the Thunderbird window won't be initialized yet for non-default window layouts
+ Utils.runAsync(this.updateState, this);
+ }
+ TimeLine.log("Icon state updated")
+
+ // Some people actually switch off browser.frames.enabled and are surprised
+ // that things stop working...
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .allowSubframes = true;
+
+ this.registerEventListeners();
+ TimeLine.log("Added event listeners")
+
+ this.executeFirstRunActions();
+ TimeLine.log("Window-specific first-run actions done")
+
+ TimeLine.leave("WindowWrapper constructor done")
+}
+WindowWrapper.prototype =
+{
+ /**
+ * Application window this object belongs to.
+ * @type Window
+ */
+ window: null,
+
+ /**
+ * Current state as displayed for this window.
+ * @type String
+ */
+ state: null,
+
+ /**
+ * Methods that can be defined at attributes of the hooks element.
+ * @type Array of String
+ */
+ customMethods: ["getBrowser", "addTab", "getContextMenu", "getToolbox", "getDefaultToolbar", "toolbarInsertBefore"],
+
+ /**
+ * Progress listener used to watch for location changes, if any.
+ * @type nsIProgressListener
+ */
+ progressListener: null,
+
+ /**
+ * Filter corresponding with "disable on site" menu item (set in fillPopup()).
+ * @type Filter
+ */
+ siteWhitelist: null,
+ /**
+ * Filter corresponding with "disable on site" menu item (set in fillPopup()).
+ * @type Filter
+ */
+ pageWhitelist: null,
+
+ /**
+ * Data associated with the node currently under mouse pointer (set in updateContextMenu()).
+ * @type RequestEntry
+ */
+ nodeData: null,
+ /**
+ * The document node that nodeData belongs to.
+ */
+ currentNode: null,
+ /**
+ * Data associated with the background image currently under mouse pointer (set in updateContextMenu()).
+ * @type RequestEntry
+ */
+ backgroundData: null,
+ /**
+ * Data associated with the frame currently under mouse pointer (set in updateContextMenu()).
+ * @type RequestEntry
+ */
+ frameData: null,
+ /**
+ * The frame that frameData belongs to.
+ */
+ currentFrame: null,
+
+ /**
+ * Window of the detached list of blockable items (might be null or closed).
+ * @type Window
+ */
+ detachedSidebar: null,
+
+ /**
+ * Binds a function to the object, ensuring that "this" pointer is always set
+ * correctly.
+ */
+ _bindMethod: function(/**Function*/ method) /**Function*/
+ {
+ let me = this;
+ return function() method.apply(me, arguments);
+ },
+
+ /**
+ * Retrieves an element by its ID.
+ */
+ E: function(/**String*/ id)
+ {
+ let doc = this.window.document;
+ this.E = function(id) doc.getElementById(id);
+ return this.E(id);
+ },
+
+ /**
+ * Initializes abp-hooks element, converts any function attributes to actual
+ * functions.
+ */
+ initializeHooks: function(hooks)
+ {
+ for each (let hook in this.customMethods)
+ {
+ let handler = hooks.getAttribute(hook);
+ this[hook] = hooks[hook] = (handler ? this._bindMethod(new Function(handler)) : null);
+ }
+ },
+
+ /**
+ * Makes a copy of the ABP icon's context menu for the toolbar button.
+ */
+ fixupMenus: function()
+ {
+ function fixId(node, newId)
+ {
+ if (node.nodeType == node.ELEMENT_NODE)
+ {
+ if (node.hasAttribute("id"))
+ node.setAttribute("id", node.getAttribute("id").replace(/abp-status/, newId));
+
+ for (let i = 0, len = node.childNodes.length; i < len; i++)
+ fixId(node.childNodes[i], newId);
+ }
+ return node;
+ }
+
+ let menuSource = this.E("abp-status-popup");
+ let paletteButton = this.getPaletteButton();
+ let toolbarButton = this.E("abp-toolbarbutton");
+ let menuItem = this.E("abp-menuitem");
+ if (toolbarButton)
+ toolbarButton.appendChild(fixId(menuSource.cloneNode(true), "abp-toolbar"));
+ if (paletteButton && paletteButton != toolbarButton)
+ paletteButton.appendChild(fixId(menuSource.cloneNode(true), "abp-toolbar"));
+ if (menuItem)
+ menuItem.appendChild(fixId(menuSource.cloneNode(true), "abp-menuitem"));
+ },
+
+ /**
+ * Attaches event listeners to a window represented by hooks element
+ */
+ registerEventListeners: function()
+ {
+ // Palette button elements aren't reachable by ID, create a lookup table
+ let paletteButtonIDs = {};
+ let paletteButton = this.getPaletteButton();
+ if (paletteButton)
+ {
+ function getElementIds(element)
+ {
+ if (element.hasAttribute("id"))
+ paletteButtonIDs[element.getAttribute("id")] = element;
+
+ for (let child = element.firstChild; child; child = child.nextSibling)
+ if (child.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
+ getElementIds(child);
+ }
+ getElementIds(paletteButton);
+ }
+
+ // Go on and register listeners
+ this.window.addEventListener("unload", removeWindow, false);
+ for each (let [id, event, handler] in this.eventHandlers)
+ {
+ handler = this._bindMethod(handler);
+
+ let element = this.E(id);
+ if (element)
+ element.addEventListener(event, handler, false);
+
+ if (id in paletteButtonIDs)
+ paletteButtonIDs[id].addEventListener(event, handler, false);
+ }
+
+ let browser = this.getBrowser();
+ browser.addEventListener("click", this._bindMethod(this.handleLinkClick), true);
+
+ // Register progress listener as well if requested
+ if (!("isDummy" in this.updateState))
+ {
+ let dummy = function() {};
+ this.progressListener =
+ {
+ onLocationChange: this._bindMethod(this.updateState),
+ onProgressChange: dummy,
+ onSecurityChange: dummy,
+ onStateChange: dummy,
+ onStatusChange: dummy,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference])
+ };
+ browser.addProgressListener(this.progressListener);
+ }
+ },
+
+ /**
+ * Retrieves the current location of the browser (might return null on failure).
+ */
+ getCurrentLocation: function() /**nsIURI*/
+ {
+ if ("currentHeaderData" in this.window && "content-base" in this.window.currentHeaderData)
+ {
+ // Thunderbird blog entry
+ return Utils.unwrapURL(this.window.currentHeaderData["content-base"].headerValue);
+ }
+ else if ("currentHeaderData" in this.window && "from" in this.window.currentHeaderData)
+ {
+ // Thunderbird mail/newsgroup entry
+ try
+ {
+ let headerParser = Cc["@mozilla.org/messenger/headerparser;1"].getService(Ci.nsIMsgHeaderParser);
+ let emailAddress = headerParser.extractHeaderAddressMailboxes(this.window.currentHeaderData.from.headerValue);
+ return Utils.makeURI("mailto:" + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, "%20"));
+ }
+ catch(e)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ // Regular browser
+ return Utils.unwrapURL(this.getBrowser().currentURI.clone());
+ }
+ },
+
+ /**
+ * Executes window-specific first-run actions if necessary.
+ */
+ executeFirstRunActions: function()
+ {
+ // Only execute first-run actions for this window once
+ if ("doneFirstRunActions " + this.window.location.href in Prefs)
+ return;
+ Prefs["doneFirstRunActions " + this.window.location.href] = true;
+
+ // Check version we previously executed first-run actions for;
+ let hooks = this.E("abp-hooks");
+ let lastVersion = hooks.getAttribute("currentVersion") || "0.0";
+ if (lastVersion != Prefs.currentVersion)
+ {
+ hooks.setAttribute("currentVersion", Prefs.currentVersion);
+ this.window.document.persist("abp-hooks", "currentVersion");
+
+ let needInstall = (Utils.versionComparator.compare(lastVersion, "0.0") <= 0);
+ if (!needInstall)
+ {
+ // Before version 1.1 we didn't add toolbar icon in SeaMonkey, do it now
+ needInstall = Utils.appID == "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}" &&
+ Utils.versionComparator.compare(lastVersion, "1.1") < 0;
+ }
+
+ // Add ABP icon to toolbar if necessary
+ if (needInstall)
+ Utils.runAsync(this.installToolbarIcon, this);
+ }
+ },
+
+ /**
+ * Finds the toolbar button in the toolbar palette.
+ */
+ getPaletteButton: function()
+ {
+ let toolbox = (this.getToolbox ? this.getToolbox() : null);
+ if (!toolbox || !("palette" in toolbox) || !toolbox.palette)
+ return null;
+
+ for (var child = toolbox.palette.firstChild; child; child = child.nextSibling)
+ if (child.id == "abp-toolbarbutton")
+ return child;
+
+ return null;
+ },
+
+ /**
+ * Updates displayed state for an application window.
+ */
+ updateState: function()
+ {
+ let state = (Prefs.enabled ? "active" : "disabled");
+
+ if (state == "active")
+ {
+ let location = this.getCurrentLocation();
+ if (location && Policy.isWhitelisted(location.spec))
+ state = "whitelisted";
+ }
+ this.state = state;
+
+ function updateElement(element)
+ {
+ if (!element)
+ return;
+
+ if (element.tagName == "statusbarpanel")
+ element.hidden = !Prefs.showinstatusbar;
+ else
+ {
+ if (element.hasAttribute("context") && Prefs.defaulttoolbaraction == 0)
+ element.setAttribute("type", "menu");
+ else
+ element.setAttribute("type", "menu-button");
+ }
+
+ element.setAttribute("abpstate", state);
+ };
+
+ let status = this.E("abp-status");
+ if (status)
+ {
+ updateElement.call(this, status);
+ if (Prefs.defaultstatusbaraction == 0)
+ status.setAttribute("popup", status.getAttribute("context"));
+ else
+ status.removeAttribute("popup");
+ }
+
+ let button = this.E("abp-toolbarbutton");
+ if (button)
+ updateElement.call(this, button);
+
+ updateElement.call(this, this.getPaletteButton());
+ },
+
+ /**
+ * Sets up hotkeys for the window.
+ */
+ configureKeys: function()
+ {
+ if (!hotkeys)
+ {
+ hotkeys = {__proto__: null};
+
+ let validModifiers =
+ {
+ accel: 1,
+ shift: 2,
+ ctrl: 4,
+ control: 4,
+ alt: 8,
+ meta: 16,
+ __proto__: null
+ };
+
+ try
+ {
+ let accelKey = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch).getIntPref("ui.key.accelKey");
+ if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_CONTROL)
+ validModifiers.ctrl = validModifiers.control = validModifiers.accel;
+ else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT)
+ validModifiers.alt = validModifiers.accel;
+ else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META)
+ validModifiers.meta = validModifiers.accel;
+ }
+ catch(e)
+ {
+ Cu.reportError(e);
+ }
+
+ // Find which hotkeys are already taken, convert them to canonical form
+ let existing = {};
+ let keys = this.window.document.getElementsByTagName("key");
+ for (let i = 0; i < keys.length; i++)
+ {
+ let key = keys[i];
+ let keyChar = key.getAttribute("key");
+ let keyCode = key.getAttribute("keycode");
+ if (!keyChar && !keyCode)
+ continue;
+
+ let modifiers = 0;
+ let keyModifiers = key.getAttribute("modifiers");
+ if (keyModifiers)
+ {
+ for each (let modifier in keyModifiers.match(/\w+/g))
+ {
+ modifier = modifier.toLowerCase();
+ if (modifier in validModifiers)
+ modifiers |= validModifiers[modifier]
+ }
+
+ let canonical = modifiers + " " + (keyChar || keyCode).toUpperCase();
+ existing[canonical] = true;
+ }
+ }
+
+ // Find available keys for our prefs
+ for (let pref in Prefs)
+ {
+ let match = /_key$/.exec(pref);
+ if (match && typeof Prefs[pref] == "string")
+ {
+ try
+ {
+ let id = match.input.substr(0, match.index);
+ let result = this.findAvailableKey(id, Prefs[pref], validModifiers, existing);
+ if (result)
+ hotkeys[id] = result;
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+
+ // Add elements for all configured hotkeys
+ for (let id in hotkeys)
+ {
+ let [keychar, keycode, modifierString] = hotkeys[id];
+
+ let element = this.window.document.createElement("key");
+ element.setAttribute("id", "abp-key-" + id);
+ element.setAttribute("command", "abp-command-" + id);
+ if (keychar)
+ element.setAttribute("key", keychar);
+ else
+ element.setAttribute("keycode", keycode);
+ element.setAttribute("modifiers", modifierString);
+
+ this.E("abp-keyset").appendChild(element);
+ }
+ },
+
+ /**
+ * Finds an available hotkey for a value defined in preferences.
+ */
+ findAvailableKey: function(/**String*/ id, /**String*/ value, /**Object*/ validModifiers, /**Object*/ existing) /**Array*/
+ {
+ let command = this.E("abp-command-" + id);
+ if (!command)
+ return;
+
+ for each (let variant in value.split(/\s*,\s*/))
+ {
+ if (!variant)
+ continue;
+
+ let modifiers = 0;
+ let keychar = null;
+ let keycode = null;
+ for each (let part in variant.split(/\s+/))
+ {
+ if (part.toLowerCase() in validModifiers)
+ modifiers |= validModifiers[part.toLowerCase()];
+ else if (part.length == 1)
+ keychar = part.toUpperCase();
+ else if ("DOM_VK_" + part.toUpperCase() in Ci.nsIDOMKeyEvent)
+ keycode = "VK_" + part.toUpperCase();
+ }
+
+ if (!keychar && !keycode)
+ continue;
+
+ let canonical = modifiers + " " + (keychar || keycode);
+ if (canonical in existing)
+ continue;
+
+ let modifierString = "";
+ for each (let modifier in ["accel", "shift", "control", "alt", "meta"])
+ {
+ if (modifiers & validModifiers[modifier])
+ {
+ modifierString += modifier + " ";
+ modifiers &= ~validModifiers[modifier];
+ }
+ }
+ return [keychar, keycode, modifierString];
+ }
+ return null;
+ },
+
+ /**
+ * Initializes window's context menu.
+ */
+ initContextMenu: function()
+ {
+ let contextMenu = this.getContextMenu();
+ if (contextMenu)
+ {
+ contextMenu.addEventListener("popupshowing", this._bindMethod(this.updateContextMenu), false);
+ contextMenu.addEventListener("popuphidden", this._bindMethod(this.clearContextMenu), false);
+
+ // Make sure our context menu items are at the bottom
+ contextMenu.appendChild(this.E("abp-removeWhitelist-menuitem"));
+ contextMenu.appendChild(this.E("abp-frame-menuitem"));
+ contextMenu.appendChild(this.E("abp-object-menuitem"));
+ contextMenu.appendChild(this.E("abp-media-menuitem"));
+ contextMenu.appendChild(this.E("abp-image-menuitem"));
+ }
+ },
+
+ /**
+ * Checks whether the toolbar icon is currently displayed.
+ */
+ isToolbarIconVisible: function()
+ {
+ let tb = this.E("abp-toolbarbutton");
+ if (!tb || tb.parentNode.localName == "toolbarpalette")
+ return false;
+
+ if (tb.parentNode.collapsed)
+ return false;
+
+ return true;
+ },
+
+ /**
+ * Makes sure the toolbar button is displayed.
+ */
+ installToolbarIcon: function()
+ {
+ let tb = this.E("abp-toolbarbutton");
+ if (!tb || tb.parentNode.localName == "toolbarpalette")
+ {
+ let toolbar = (this.getDefaultToolbar ? this.getDefaultToolbar() : null);
+ if (!toolbar || typeof toolbar.insertItem != "function")
+ return;
+
+ let insertBefore = (this.toolbarInsertBefore ? this.toolbarInsertBefore() : null);
+ if (insertBefore && insertBefore.parentNode != toolbar)
+ insertBefore = null;
+
+ toolbar.insertItem("abp-toolbarbutton", insertBefore, null, false);
+
+ toolbar.setAttribute("currentset", toolbar.currentSet);
+ this.window.document.persist(toolbar.id, "currentset");
+ }
+
+ tb = this.E("abp-toolbarbutton");
+ if (tb && tb.parentNode.collapsed)
+ {
+ tb.parentNode.setAttribute("collapsed", "false");
+ this.window.document.persist(tb.parentNode.id, "collapsed");
+ }
+ },
+
+ /**
+ * Removes toolbar button from the toolbar.
+ */
+ hideToolbarIcon: function()
+ {
+ let tb = this.E("abp-toolbarbutton");
+ if (!tb || tb.parentNode.localName != "toolbar")
+ return;
+
+ let toolbar = tb.parentNode;
+ toolbar.currentSet = toolbar.currentSet.split(",").filter(function(id) id != "abp-toolbarbutton").join(",");
+
+ toolbar.setAttribute("currentset", toolbar.currentSet);
+ this.window.document.persist(toolbar.id, "currentset");
+ },
+
+ /**
+ * Opens Adblock Plus menu.
+ */
+ openMenu: function()
+ {
+ this.installToolbarIcon();
+
+ let button = this.E("abp-toolbarbutton");
+ if (!button)
+ return;
+
+ let toolbar = button.parentNode;
+ if (toolbar.collapsed)
+ {
+ toolbar.setAttribute("collapsed", "false");
+ this.window.document.persist(toolbar.id, "collapsed");
+ }
+
+ Utils.runAsync(function()
+ {
+ button.open = true;
+ });
+ },
+
+ /**
+ * Handles browser clicks to intercept clicks on abp: links. This can be
+ * called either with an event object or with the link target (if it is the
+ * former then link target will be retrieved from event target).
+ */
+ handleLinkClick: function (/**Event*/ event, /**String*/ linkTarget)
+ {
+ if (event)
+ {
+ // Ignore right-clicks
+ if (event.button == 2)
+ return;
+
+ // Search the link associated with the click
+ let link = event.target;
+ while (link && !(link instanceof Ci.nsIDOMHTMLAnchorElement))
+ link = link.parentNode;
+
+ if (!link || link.protocol != "abp:")
+ return;
+
+ // This is our link - make sure the browser doesn't handle it
+ event.preventDefault();
+ event.stopPropagation();
+
+ linkTarget = link.href;
+ }
+
+ let match = /^abp:\/*subscribe\/*\?(.*)/i.exec(linkTarget);
+ if (!match)
+ return;
+
+ // Decode URL parameters
+ let title = null;
+ let url = null;
+ let mainSubscriptionTitle = null;
+ let mainSubscriptionURL = null;
+ for each (let param in match[1].split('&'))
+ {
+ let parts = param.split("=", 2);
+ if (parts.length != 2 || !/\S/.test(parts[1]))
+ continue;
+ switch (parts[0])
+ {
+ case "title":
+ title = decodeURIComponent(parts[1]);
+ break;
+ case "location":
+ url = decodeURIComponent(parts[1]);
+ break;
+ case "requiresTitle":
+ mainSubscriptionTitle = decodeURIComponent(parts[1]);
+ break;
+ case "requiresLocation":
+ mainSubscriptionURL = decodeURIComponent(parts[1]);
+ break;
+ }
+ }
+ if (!url)
+ return;
+
+ // Default title to the URL
+ if (!title)
+ title = url;
+
+ // Main subscription needs both title and URL
+ if (mainSubscriptionTitle && !mainSubscriptionURL)
+ mainSubscriptionTitle = null;
+ if (mainSubscriptionURL && !mainSubscriptionTitle)
+ mainSubscriptionURL = null;
+
+ // Trim spaces in title and URL
+ title = title.replace(/^\s+/, "").replace(/\s+$/, "");
+ url = url.replace(/^\s+/, "").replace(/\s+$/, "");
+ if (mainSubscriptionURL)
+ {
+ mainSubscriptionTitle = mainSubscriptionTitle.replace(/^\s+/, "").replace(/\s+$/, "");
+ mainSubscriptionURL = mainSubscriptionURL.replace(/^\s+/, "").replace(/\s+$/, "");
+ }
+
+ // Verify that the URL is valid
+ url = Utils.makeURI(url);
+ if (!url || (url.scheme != "http" && url.scheme != "https" && url.scheme != "ftp"))
+ return;
+ url = url.spec;
+
+ if (mainSubscriptionURL)
+ {
+ mainSubscriptionURL = Utils.makeURI(mainSubscriptionURL);
+ if (!mainSubscriptionURL || (mainSubscriptionURL.scheme != "http" && mainSubscriptionURL.scheme != "https" && mainSubscriptionURL.scheme != "ftp"))
+ mainSubscriptionURL = mainSubscriptionTitle = null;
+ else
+ mainSubscriptionURL = mainSubscriptionURL.spec;
+ }
+
+ // Open dialog
+ let subscription = {url: url, title: title, disabled: false, external: false,
+ mainSubscriptionTitle: mainSubscriptionTitle, mainSubscriptionURL: mainSubscriptionURL};
+ this.window.openDialog("chrome://@ADDON_CHROME_NAME@/content/subscriptionSelection.xul", "_blank",
+ "chrome,centerscreen,resizable,dialog=no", subscription, null);
+ },
+
+ /**
+ * Updates state of the icon tooltip.
+ */
+ fillTooltip: function(/**Event*/ event)
+ {
+ let node = this.window.document.tooltipNode;
+ if (!node || !node.hasAttribute("tooltip"))
+ {
+ event.preventDefault();
+ return;
+ }
+
+ // Prevent tooltip from overlapping menu
+ for each (let id in ["abp-toolbar-popup", "abp-status-popup"])
+ {
+ let element = this.E(id);
+ if (element && element.state == "open")
+ {
+ event.preventDefault();
+ return;
+ }
+ }
+
+ let type = (node.id == "abp-toolbarbutton" ? "toolbar" : "statusbar");
+ let action = parseInt(Prefs["default" + type + "action"]);
+ if (isNaN(action))
+ action = -1;
+
+ let actionDescr = this.E("abp-tooltip-action");
+ actionDescr.hidden = (action < 0 || action > 3);
+ if (!actionDescr.hidden)
+ actionDescr.setAttribute("value", Utils.getString("action" + action + "_tooltip"));
+
+ let statusDescr = this.E("abp-tooltip-status");
+ let statusStr = Utils.getString(this.state + "_tooltip");
+ if (this.state == "active")
+ {
+ let [activeSubscriptions, activeFilters] = FilterStorage.subscriptions.reduce(function([subscriptions, filters], current)
+ {
+ if (current instanceof SpecialSubscription)
+ return [subscriptions, filters + current.filters.filter(function(filter) !filter.disabled).length];
+ else if (!current.disabled && !(Prefs.subscriptions_exceptionscheckbox && current.url == Prefs.subscriptions_exceptionsurl))
+ return [subscriptions + 1, filters];
+ else
+ return [subscriptions, filters]
+ }, [0, 0]);
+
+ statusStr = statusStr.replace(/\?1\?/, activeSubscriptions).replace(/\?2\?/, activeFilters);
+ }
+ statusDescr.setAttribute("value", statusStr);
+
+ let activeFilters = [];
+ this.E("abp-tooltip-blocked-label").hidden = (this.state != "active");
+ this.E("abp-tooltip-blocked").hidden = (this.state != "active");
+ if (this.state == "active")
+ {
+ let stats = RequestNotifier.getWindowStatistics(this.getBrowser().contentWindow);
+
+ let blockedStr = Utils.getString("blocked_count_tooltip");
+ blockedStr = blockedStr.replace(/\?1\?/, stats ? stats.blocked : 0).replace(/\?2\?/, stats ? stats.items : 0);
+
+ if (stats && stats.whitelisted + stats.hidden)
+ {
+ blockedStr += " " + Utils.getString("blocked_count_addendum");
+ blockedStr = blockedStr.replace(/\?1\?/, stats.whitelisted).replace(/\?2\?/, stats.hidden);
+ }
+
+ this.E("abp-tooltip-blocked").setAttribute("value", blockedStr);
+
+ if (stats)
+ {
+ let filterSort = function(a, b)
+ {
+ return stats.filters[b] - stats.filters[a];
+ };
+ for (let filter in stats.filters)
+ activeFilters.push(filter);
+ activeFilters = activeFilters.sort(filterSort);
+ }
+
+ if (activeFilters.length > 0)
+ {
+ let filtersContainer = this.E("abp-tooltip-filters");
+ while (filtersContainer.firstChild)
+ filtersContainer.removeChild(filtersContainer.firstChild);
+
+ for (let i = 0; i < activeFilters.length && i < 3; i++)
+ {
+ let descr = filtersContainer.ownerDocument.createElement("description");
+ descr.setAttribute("value", activeFilters[i] + " (" + stats.filters[activeFilters[i]] + ")");
+ filtersContainer.appendChild(descr);
+ }
+ }
+ }
+
+ this.E("abp-tooltip-filters-label").hidden = (activeFilters.length == 0);
+ this.E("abp-tooltip-filters").hidden = (activeFilters.length == 0);
+ this.E("abp-tooltip-more-filters").hidden = (activeFilters.length <= 3);
+ },
+
+ /**
+ * Updates state of the icon context menu.
+ */
+ fillPopup: function(/**Event*/ event)
+ {
+ let popup = event.target;
+
+ // Submenu being opened - ignore
+ let match = /^(abp-(?:toolbar|status|menuitem)-)popup$/.exec(popup.getAttribute("id"));
+ if (!match)
+ return;
+ let prefix = match[1];
+
+ let sidebarOpen = this.isSidebarOpen();
+ this.E(prefix + "opensidebar").hidden = sidebarOpen;
+ this.E(prefix + "closesidebar").hidden = !sidebarOpen;
+
+ let whitelistItemSite = this.E(prefix + "whitelistsite");
+ let whitelistItemPage = this.E(prefix + "whitelistpage");
+ whitelistItemSite.hidden = whitelistItemPage.hidden = true;
+
+ let location = this.getCurrentLocation();
+ if (location && Policy.isBlockableScheme(location))
+ {
+ let host = null;
+ try
+ {
+ host = location.host.replace(/^www\./, "");
+ } catch (e) {}
+
+ if (host)
+ {
+ let ending = "|";
+ if (location instanceof Ci.nsIURL && location.ref)
+ location.ref = "";
+ if (location instanceof Ci.nsIURL && location.query)
+ {
+ location.query = "";
+ ending = "?";
+ }
+
+ this.siteWhitelist = Filter.fromText("@@||" + host + "^$document");
+ whitelistItemSite.setAttribute("checked", this.siteWhitelist.subscriptions.length && !this.siteWhitelist.disabled);
+ whitelistItemSite.setAttribute("label", whitelistItemSite.getAttribute("labeltempl").replace(/\?1\?/, host));
+ whitelistItemSite.hidden = false;
+
+ this.pageWhitelist = Filter.fromText("@@|" + location.spec + ending + "$document");
+ whitelistItemPage.setAttribute("checked", this.pageWhitelist.subscriptions.length && !this.pageWhitelist.disabled);
+ whitelistItemPage.hidden = false;
+ }
+ else
+ {
+ this.siteWhitelist = Filter.fromText("@@|" + location.spec + "|");
+ whitelistItemSite.setAttribute("checked", this.siteWhitelist.subscriptions.length && !this.siteWhitelist.disabled);
+ whitelistItemSite.setAttribute("label", whitelistItemSite.getAttribute("labeltempl").replace(/\?1\?/, location.spec.replace(/^mailto:/, "")));
+ whitelistItemSite.hidden = false;
+ }
+ }
+
+ this.E(prefix + "disabled").setAttribute("checked", !Prefs.enabled);
+ this.E(prefix + "frameobjects").setAttribute("checked", Prefs.frameobjects);
+ this.E(prefix + "slowcollapse").setAttribute("checked", !Prefs.fastcollapse);
+ this.E(prefix + "savestats").setAttribute("checked", Prefs.savestats);
+
+ let hasToolbar = this.getDefaultToolbar && this.getDefaultToolbar();
+ let hasStatusBar = this.E("abp-status");
+ this.E(prefix + "showintoolbar").hidden = !hasToolbar || prefix == "abp-toolbar-";
+ this.E(prefix + "showinstatusbar").hidden = !hasStatusBar;
+ this.E(prefix + "iconSettingsSeparator").hidden = this.E(prefix + "showintoolbar").hidden && this.E(prefix + "showinstatusbar").hidden;
+
+ this.E(prefix + "showintoolbar").setAttribute("checked", this.isToolbarIconVisible());
+ this.E(prefix + "showinstatusbar").setAttribute("checked", Prefs.showinstatusbar);
+
+ /** let syncEngine = Sync.getEngine();
+ this.E(prefix + "sync").hidden = !syncEngine;
+ this.E(prefix + "sync").setAttribute("checked", syncEngine && syncEngine.enabled); */
+
+ let defAction = (!this.window.document.popupNode || this.window.document.popupNode.id == "abp-toolbarbutton" ?
+ Prefs.defaulttoolbaraction :
+ Prefs.defaultstatusbaraction);
+ this.E(prefix + "opensidebar").setAttribute("default", defAction == 1);
+ this.E(prefix + "closesidebar").setAttribute("default", defAction == 1);
+ this.E(prefix + "filters").setAttribute("default", defAction == 2);
+ this.E(prefix + "disabled").setAttribute("default", defAction == 3);
+ },
+
+ /**
+ * Opens report wizard for the current page.
+ */
+ openReportDialog: function()
+ {
+ let wnd = Utils.windowMediator.getMostRecentWindow("abp:sendReport");
+ if (wnd)
+ wnd.focus();
+ else
+ this.window.openDialog("chrome://@ADDON_CHROME_NAME@/content/sendReport.xul", "_blank", "chrome,centerscreen,resizable=no", this.window.content, this.getCurrentLocation());
+ },
+
+ /**
+ * Opens our contribution page.
+ */
+ openContributePage: function()
+ {
+ Utils.loadDocLink("contribute");
+ },
+
+ /**
+ * Hide contribute button and persist this choice.
+ */
+ hideContributeButton: function(event)
+ {
+ for each (let button in [this.E("abp-status-contributebutton"), this.E("abp-toolbar-contributebutton"), this.E("abp-menuitem-contributebutton")])
+ {
+ if (button)
+ {
+ button.setAttribute("hidden", "true");
+ this.window.document.persist(button.id, "hidden");
+ }
+ }
+ },
+
+ /**
+ * Tests whether blockable items list is currently open.
+ */
+ isSidebarOpen: function() /**Boolean*/
+ {
+ if (this.detachedSidebar && !this.detachedSidebar.closed)
+ return true;
+
+ let sidebar = this.E("abp-sidebar");
+ return (sidebar ? !sidebar.hidden : false);
+ },
+
+ /**
+ * Toggles open/closed state of the blockable items list.
+ */
+ toggleSidebar: function()
+ {
+ if (this.detachedSidebar && !this.detachedSidebar.closed)
+ {
+ this.detachedSidebar.close();
+ this.detachedSidebar = null;
+ }
+ else
+ {
+ let sidebar = this.E("abp-sidebar");
+ if (sidebar && (!Prefs.detachsidebar || !sidebar.hidden))
+ {
+ this.E("abp-sidebar-splitter").hidden = !sidebar.hidden;
+ this.E("abp-sidebar-browser").setAttribute("src", sidebar.hidden ? "chrome://@ADDON_CHROME_NAME@/content/sidebar.xul" : "about:blank");
+ sidebar.hidden = !sidebar.hidden;
+ if (sidebar.hidden)
+ this.getBrowser().contentWindow.focus();
+ }
+ else
+ this.detachedSidebar = this.window.openDialog("chrome://@ADDON_CHROME_NAME@/content/sidebarDetached.xul", "_blank", "chrome,resizable,dependent,dialog=no");
+ }
+ },
+
+ /**
+ * Removes/disables the exception rule applying for the current page.
+ */
+ removeWhitelist: function()
+ {
+ let location = this.getCurrentLocation();
+ let filter = null;
+ if (location)
+ filter = Policy.isWhitelisted(location.spec);
+ if (filter && filter.subscriptions.length && !filter.disabled)
+ {
+ AppIntegration.toggleFilter(filter);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Toggles "Count filter hits" option.
+ */
+ toggleSaveStats: function()
+ {
+ if (Prefs.savestats)
+ {
+ if (!Utils.confirm(this.window, Utils.getString("clearStats_warning")))
+ return;
+
+ FilterStorage.resetHitCounts();
+ FilterListener.setDirty(0); // Force saving to disk
+ Prefs.savestats = false;
+ }
+ else
+ Prefs.savestats = true;
+ },
+
+ /**
+ * Handles command events on toolbar icon.
+ */
+ handleToolbarCommand: function(event)
+ {
+ if (event.eventPhase != event.AT_TARGET)
+ return;
+
+ if (Prefs.defaulttoolbaraction == 0)
+ event.target.open = true;
+ else
+ this.executeAction(Prefs.defaulttoolbaraction);
+ },
+
+ /**
+ * Handles click events on toolbar icon.
+ */
+ handleToolbarClick: function(/**Event*/ event)
+ {
+ if (event.eventPhase != event.AT_TARGET)
+ return;
+
+ if (event.button == 1)
+ this.executeAction(3);
+ },
+
+ /**
+ * Handles click events on status bar icon.
+ */
+ handleStatusClick: function(/**Event*/ event)
+ {
+ if (event.eventPhase != event.AT_TARGET)
+ return;
+
+ if (event.button == 0)
+ this.executeAction(Prefs.defaultstatusbaraction);
+ else if (event.button == 1)
+ this.executeAction(3);
+ },
+
+ // Executes default action for statusbar/toolbar by its number
+ executeAction: function (action)
+ {
+ if (action == 1)
+ this.toggleSidebar();
+ else if (action == 2)
+ Utils.openFiltersDialog();
+ else if (action == 3)
+ {
+ // If there is a whitelisting rule for current page - remove it (reenable).
+ // Otherwise flip "enabled" pref.
+ if (!this.removeWhitelist())
+ AppIntegration.togglePref("enabled");
+ }
+ },
+
+ /**
+ * Updates context menu, in particularly controls the visibility of context
+ * menu items like "Block image".
+ */
+ updateContextMenu: function(event)
+ {
+ if (event.eventPhase != event.AT_TARGET)
+ return;
+
+ let contextMenu = this.getContextMenu();
+ let target = this.window.document.popupNode;
+ if (target instanceof Ci.nsIDOMHTMLMapElement || target instanceof Ci.nsIDOMHTMLAreaElement)
+ {
+ // HTML image maps will usually receive events when the mouse pointer is
+ // over a different element, get the real event target.
+ let rect = target.getClientRects()[0];
+ target = target.ownerDocument.elementFromPoint(Math.max(rect.left, 0), Math.max(rect.top, 0));
+ }
+
+ let nodeType = null;
+ this.nodeData = null;
+ this.currentNode = null;
+ this.backgroundData = null;
+ this.frameData = null;
+ this.currentFrame = null;
+ if (target)
+ {
+ // Lookup the node in our stored data
+ let data = RequestNotifier.getDataForNode(target);
+ if (data && !data[1].filter)
+ {
+ [this.currentNode, this.nodeData] = data;
+ nodeType = this.nodeData.typeDescr;
+ }
+
+ let wnd = Utils.getWindow(target);
+
+ if (wnd.frameElement)
+ {
+ let data = RequestNotifier.getDataForNode(wnd.frameElement, true);
+ if (data && !data[1].filter)
+ [this.currentFrame, this.frameData] = data;
+ }
+
+ if (nodeType != "IMAGE")
+ {
+ // Look for a background image
+ let imageNode = target;
+ while (imageNode)
+ {
+ if (imageNode.nodeType == imageNode.ELEMENT_NODE)
+ {
+ let style = wnd.getComputedStyle(imageNode, "");
+ let bgImage = extractImageURL(style, "background-image") || extractImageURL(style, "list-style-image");
+ if (bgImage)
+ {
+ let data = RequestNotifier.getDataForNode(wnd.document, true, Policy.type.IMAGE, bgImage);
+ if (data && !data[1].filter)
+ {
+ this.backgroundData = data[1];
+ break;
+ }
+ }
+ }
+
+ imageNode = imageNode.parentNode;
+ }
+ }
+
+ // Hide "Block Images from ..." if hideimagemanager pref is true and the image manager isn't already blocking something
+ let imgManagerContext = this.E("context-blockimage");
+ if (imgManagerContext && shouldHideImageManager())
+ {
+ // Don't use "hidden" attribute - it might be overridden by the default popupshowing handler
+ imgManagerContext.collapsed = true;
+ }
+ }
+
+ this.E("abp-image-menuitem").hidden = (nodeType != "IMAGE" && this.backgroundData == null);
+ this.E("abp-object-menuitem").hidden = (nodeType != "OBJECT");
+ this.E("abp-media-menuitem").hidden = (nodeType != "MEDIA");
+ this.E("abp-frame-menuitem").hidden = (this.frameData == null);
+
+ let location = this.getCurrentLocation();
+ this.E("abp-removeWhitelist-menuitem").hidden = (!location || !Policy.isWhitelisted(location.spec));
+ },
+
+ /**
+ * Clears context menu data once the menu is closed.
+ */
+ clearContextMenu: function(event)
+ {
+ if (event.eventPhase != event.AT_TARGET)
+ return;
+
+ this.nodeData = null;
+ this.currentNode = null;
+ this.backgroundData = null;
+ this.frameData = null;
+ this.currentFrame = null;
+ },
+
+ /**
+ * Brings up the filter composer dialog to block an item.
+ */
+ blockItem: function(/**Node*/ node, /**RequestEntry*/ item)
+ {
+ if (!item)
+ return;
+
+ this.window.openDialog("chrome://@ADDON_CHROME_NAME@/content/composer.xul", "_blank", "chrome,centerscreen,resizable,dialog=no,dependent", [node], item);
+ }
+};
+
+/**
+ * List of event handers to be registered. For each event handler the element ID,
+ * event and the actual event handler are listed.
+ * @type Array
+ */
+WindowWrapper.prototype.eventHandlers = [
+ ["abp-tooltip", "popupshowing", WindowWrapper.prototype.fillTooltip],
+ ["abp-status-popup", "popupshowing", WindowWrapper.prototype.fillPopup],
+ ["abp-toolbar-popup", "popupshowing", WindowWrapper.prototype.fillPopup],
+ ["abp-menuitem-popup", "popupshowing", WindowWrapper.prototype.fillPopup],
+ ["abp-command-sendReport", "command", WindowWrapper.prototype.openReportDialog],
+ ["abp-command-filters", "command", function() {Utils.openFiltersDialog();}],
+ ["abp-command-sidebar", "command", WindowWrapper.prototype.toggleSidebar],
+ ["abp-command-togglesitewhitelist", "command", function() { AppIntegration.toggleFilter(this.siteWhitelist); }],
+ ["abp-command-togglepagewhitelist", "command", function() { AppIntegration.toggleFilter(this.pageWhitelist); }],
+ ["abp-command-toggleobjtabs", "command", function() { AppIntegration.togglePref("frameobjects"); }],
+ ["abp-command-togglecollapse", "command", function() { AppIntegration.togglePref("fastcollapse"); }],
+ ["abp-command-togglesavestats", "command", WindowWrapper.prototype.toggleSaveStats],
+ ["abp-command-togglesync", "command", AppIntegration.toggleSync],
+ ["abp-command-toggleshowintoolbar", "command", AppIntegration.toggleToolbarIcon],
+ ["abp-command-toggleshowinstatusbar", "command", function() { AppIntegration.togglePref("showinstatusbar"); }],
+ ["abp-command-enable", "command", function() { AppIntegration.togglePref("enabled"); }],
+ ["abp-command-contribute", "command", WindowWrapper.prototype.openContributePage],
+ ["abp-command-contribute-hide", "command", WindowWrapper.prototype.hideContributeButton],
+ ["abp-toolbarbutton", "command", WindowWrapper.prototype.handleToolbarCommand],
+ ["abp-toolbarbutton", "click", WindowWrapper.prototype.handleToolbarClick],
+ ["abp-status", "click", WindowWrapper.prototype.handleStatusClick],
+ ["abp-image-menuitem", "command", function() { this.backgroundData ? this.blockItem(null, this.backgroundData) : this.blockItem(this.currentNode, this.nodeData); }],
+ ["abp-object-menuitem", "command", function() { this.blockItem(this.currentNode, this.nodeData); }],
+ ["abp-media-menuitem", "command", function() { this.blockItem(this.currentNode, this.nodeData); }],
+ ["abp-frame-menuitem", "command", function() { this.blockItem(this.currentFrame, this.frameData); }],
+ ["abp-removeWhitelist-menuitem", "command", WindowWrapper.prototype.removeWhitelist]
+];
+
+/**
+ * Updates displayed status for all application windows (on prefs or filters
+ * change).
+ */
+function reloadPrefs()
+{
+ for each (let wrapper in wrappers)
+ wrapper.updateState();
+}
+
+/**
+ * Initializes options in add-on manager when they show up.
+ */
+function initOptionsDoc(/**Document*/ doc)
+{
+ function E(id) doc.getElementById(id);
+
+ E("@ADDON_CHROME_NAME@-filters").addEventListener("command", Utils.openFiltersDialog, false);
+
+ let wrapper = wrappers.length ? wrappers[0] : null;
+ let hasToolbar = wrapper && wrapper.getDefaultToolbar && wrapper.getDefaultToolbar();
+ let hasStatusBar = wrapper && wrapper.E("abp-status");
+
+ let syncEngine = Sync.getEngine();
+ E("@ADDON_CHROME_NAME@-sync").collapsed = !syncEngine;
+
+ E("@ADDON_CHROME_NAME@-showintoolbar").collapsed = !hasToolbar;
+ E("@ADDON_CHROME_NAME@-showinstatusbar").collapsed = !hasStatusBar;
+
+ function initCheckboxes()
+ {
+
+ E("@ADDON_CHROME_NAME@-savestats").value = Prefs.savestats;
+ E("@ADDON_CHROME_NAME@-savestats").addEventListener("command", function()
+ {
+ wrapper.toggleSaveStats.call({window: doc.defaultView});
+ E("@ADDON_CHROME_NAME@-savestats").value = Prefs.savestats;
+ }, false);
+
+ E("@ADDON_CHROME_NAME@-sync").value = syncEngine && syncEngine.enabled;
+ E("@ADDON_CHROME_NAME@-sync").addEventListener("command", function()
+ {
+ E("@ADDON_CHROME_NAME@-sync").value = AppIntegration.toggleSync();
+ }, false);
+
+ if (wrapper)
+ {
+ E("@ADDON_CHROME_NAME@-showintoolbar").value =
+ wrapper.isToolbarIconVisible();
+ let handler = function()
+ {
+ E("@ADDON_CHROME_NAME@-showintoolbar").value =
+ AppIntegration.toggleToolbarIcon();
+ };
+ E("@ADDON_CHROME_NAME@-showintoolbar").addEventListener("command", handler, false);
+ }
+ }
+ initCheckboxes();
+}
+
+/**
+ * Tests whether image manager context menu entry should be hidden with user's current preferences.
+ * @return Boolean
+ */
+function shouldHideImageManager()
+{
+ let result = false;
+ if (Prefs.hideimagemanager && "@mozilla.org/permissionmanager;1" in Cc)
+ {
+ try
+ {
+ result = true;
+ let enumerator = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager).enumerator;
+ while (enumerator.hasMoreElements())
+ {
+ let item = enumerator.getNext().QueryInterface(Ci.nsIPermission);
+ if (item.type == "image" && item.capability == Ci.nsIPermissionManager.DENY_ACTION)
+ {
+ result = false;
+ break;
+ }
+ }
+ }
+ catch(e)
+ {
+ result = false;
+ }
+ }
+
+ shouldHideImageManager = function() result;
+ return result;
+}
+
+/**
+ * Executed on first run, adds a filter subscription and notifies that user
+ * about that.
+ */
+function addSubscription()
+{
+ // Add "acceptable ads" subscription for new users and user updating from old ABP versions.
+ // Don't add it for users of privacy subscriptions (use a hardcoded list for now).
+ let addAcceptable = false;
+ let privacySubscriptions = {
+ "https://easylist-downloads.adblockplus.org/easyprivacy+easylist.txt": true,
+ "https://easylist-downloads.adblockplus.org/easyprivacy.txt": true,
+ "https://secure.fanboy.co.nz/fanboy-tracking.txt": true,
+ "https://fanboy-adblock-list.googlecode.com/hg/fanboy-adblocklist-stats.txt": true,
+ "https://bitbucket.org/fanboy/fanboyadblock/raw/tip/fanboy-adblocklist-stats.txt": true,
+ "https://hg01.codeplex.com/fanboyadblock/raw-file/tip/fanboy-adblocklist-stats.txt": true,
+ "https://adversity.googlecode.com/hg/Adversity-Tracking.txt": true
+ };
+
+ // Don't add subscription if the user has a subscription already
+ let addSubscription = true;
+ if (FilterStorage.subscriptions.some(function(subscription) subscription instanceof DownloadableSubscription && subscription.url != Prefs.subscriptions_exceptionsurl))
+ addSubscription = false;
+
+ // Only add subscription if this is the first run or the user has no filters
+ if (addSubscription)
+ {
+ let hasFilters = FilterStorage.subscriptions.some(function(subscription) subscription.filters.length);
+ if (hasFilters && Utils.versionComparator.compare(Prefs.lastVersion, "0.0") > 0)
+ addSubscription = false;
+ }
+
+ if (!addSubscription && !addAcceptable)
+ return;
+
+ function notifyUser()
+ {
+ let wrapper = (wrappers.length ? wrappers[0] : null);
+ if (wrapper && wrapper.addTab)
+ {
+ wrapper.addTab("chrome://@ADDON_CHROME_NAME@/content/firstRun.xul");
+ }
+ else
+ {
+ Utils.windowWatcher.openWindow(wrapper ? wrapper.window : null,
+ "chrome://@ADDON_CHROME_NAME@/content/firstRun.xul",
+ "_blank", "chrome,centerscreen,resizable,dialog=no", null);
+ }
+ }
+
+ if (addSubscription)
+ {
+ // Load subscriptions data
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ request.open("GET", "chrome://@ADDON_CHROME_NAME@/content/subscriptions.xml");
+ request.addEventListener("load", function()
+ {
+ let node = Utils.chooseFilterSubscription(request.responseXML.getElementsByTagName("subscription"));
+ let subscription = (node ? Subscription.fromURL(node.getAttribute("url")) : null);
+ if (subscription)
+ {
+ FilterStorage.addSubscription(subscription);
+ subscription.disabled = false;
+ subscription.title = node.getAttribute("title");
+ subscription.homepage = node.getAttribute("homepage");
+ if (subscription instanceof DownloadableSubscription && !subscription.lastDownload)
+ Synchronizer.execute(subscription);
+
+ notifyUser();
+ }
+ }, false);
+ request.send();
+ }
+ else
+ notifyUser();
+}
+
+/**
+ * Extracts the URL of the image from a CSS property.
+ */
+function extractImageURL(/**CSSStyleDeclaration*/ computedStyle, /**String*/ property)
+{
+ let value = computedStyle.getPropertyCSSValue(property);
+ if (value instanceof Ci.nsIDOMCSSValueList && value.length >= 1)
+ value = value[0];
+ if (value instanceof Ci.nsIDOMCSSPrimitiveValue && value.primitiveType == Ci.nsIDOMCSSPrimitiveValue.CSS_URI)
+ return Utils.unwrapURL(value.getStringValue()).spec;
+
+ return null;
+}
+
+init();
diff --git a/abprime/modules/Bootstrap.jsm b/abprime/modules/Bootstrap.jsm
new file mode 100644
index 00000000..b50aa394
--- /dev/null
+++ b/abprime/modules/Bootstrap.jsm
@@ -0,0 +1,167 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Bootstrap module, will initialize Adblock Plus when loaded
+ */
+
+var EXPORTED_SYMBOLS = ["Bootstrap"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+
+let publicURL = Services.io.newURI(baseURL + "Public.jsm", null, null);
+if (publicURL instanceof Ci.nsIMutable)
+ publicURL.mutable = false;
+
+const cidPublic = Components.ID("5e447bce-1dd2-11b2-b151-ec21c2b6a135");
+const contractIDPublic = "@adblockplus.org/abp/public;1";
+let factoryPublic =
+{
+ createInstance: function(outer, iid)
+ {
+ if (outer)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return publicURL.QueryInterface(iid);
+ }
+};
+
+let defaultModules = [
+ baseURL + "Prefs.jsm",
+ baseURL + "FilterListener.jsm",
+ baseURL + "ContentPolicy.jsm",
+ baseURL + "Synchronizer.jsm",
+// baseURL + "Sync.jsm"
+];
+
+let loadedModules = {__proto__: null};
+
+let initialized = false;
+
+/**
+ * Allows starting up and shutting down Adblock Plus functions.
+ * @class
+ */
+var Bootstrap =
+{
+ /**
+ * Initializes add-on, loads and initializes all modules.
+ */
+ startup: function()
+ {
+ if (initialized)
+ return;
+ initialized = true;
+
+ TimeLine.enter("Entered Bootstrap.startup()");
+
+ // Register component to allow retrieving public URL
+
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(cidPublic, "Adblock Plus public module URL", contractIDPublic, factoryPublic);
+
+ TimeLine.log("done registering URL components");
+
+ // Load and initialize modules
+
+ TimeLine.log("started initializing modules");
+
+ for each (let url in defaultModules)
+ Bootstrap.loadModule(url);
+
+ TimeLine.leave("Bootstrap.startup() done");
+ },
+
+ /**
+ * Shuts down add-on.
+ */
+ shutdown: function()
+ {
+ if (!initialized)
+ return;
+
+ TimeLine.enter("Entered Bootstrap.shutdown()");
+
+ // Shut down modules
+ for (let url in loadedModules)
+ Bootstrap.shutdownModule(url);
+
+ TimeLine.leave("Bootstrap.shutdown() done");
+ },
+
+ /**
+ * Loads and initializes a module.
+ */
+ loadModule: function(/**String*/ url)
+ {
+ if (url in loadedModules)
+ return;
+
+ let module = {};
+ try
+ {
+ Cu.import(url, module);
+ }
+ catch (e)
+ {
+ Cu.reportError("Adblock Plus: Failed to load module " + url + ": " + e);
+ return;
+ }
+
+ for each (let obj in module)
+ {
+ if ("startup" in obj)
+ {
+ try
+ {
+ obj.startup();
+ loadedModules[url] = obj;
+ }
+ catch (e)
+ {
+ Cu.reportError("Adblock Plus: Calling method startup() for module " + url + " failed: " + e);
+ }
+ return;
+ }
+ }
+
+ Cu.reportError("Adblock Plus: No exported object with startup() method found for module " + url);
+ },
+
+ /**
+ * Shuts down a module.
+ */
+ shutdownModule: function(/**String*/ url)
+ {
+ if (!(url in loadedModules))
+ return;
+
+ let obj = loadedModules[url];
+ if ("shutdown" in obj)
+ {
+ try
+ {
+ obj.shutdown();
+ }
+ catch (e)
+ {
+ Cu.reportError("Adblock Plus: Calling method shutdown() for module " + url + " failed: " + e);
+ }
+ return;
+ }
+ }
+};
diff --git a/abprime/modules/ContentPolicy.jsm b/abprime/modules/ContentPolicy.jsm
new file mode 100644
index 00000000..4bae6419
--- /dev/null
+++ b/abprime/modules/ContentPolicy.jsm
@@ -0,0 +1,634 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Content policy implementation, responsible for blocking things.
+ */
+
+var EXPORTED_SYMBOLS = ["Policy"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "Matcher.jsm");
+Cu.import(baseURL + "ObjectTabs.jsm");
+Cu.import(baseURL + "RequestNotifier.jsm");
+
+/**
+ * List of explicitly supported content types
+ * @type Array of String
+ */
+const contentTypes = ["OTHER", "SCRIPT", "IMAGE", "STYLESHEET", "OBJECT", "SUBDOCUMENT", "DOCUMENT", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT", "MEDIA", "WEBSOCKET", "WEBRTC", "CSP"];
+
+/**
+ * List of content types that aren't associated with a visual document area
+ * @type Array of String
+ */
+const nonVisualTypes = ["SCRIPT", "STYLESHEET", "XMLHTTPREQUEST", "OBJECT_SUBREQUEST", "FONT", "WEBSOCKET", "WEBRTC", "CSP"];
+
+/**
+ * Public policy checking functions and auxiliary objects
+ * @class
+ */
+var Policy =
+{
+ /**
+ * Map of content type identifiers by their name.
+ * @type Object
+ */
+ type: {},
+
+ /**
+ * Map of content type names by their identifiers (reverse of type map).
+ * @type Object
+ */
+ typeDescr: {},
+
+ /**
+ * Map of localized content type names by their identifiers.
+ * @type Object
+ */
+ localizedDescr: {},
+
+ /**
+ * Lists the non-visual content types.
+ * @type Object
+ */
+ nonVisual: {},
+
+ /**
+ * Map containing all schemes that should be ignored by content policy.
+ * @type Object
+ */
+ whitelistSchemes: {},
+
+ /**
+ * Called on module startup.
+ */
+ startup: function()
+ {
+ TimeLine.enter("Entered ContentPolicy.startup()");
+
+ // type constant by type description and type description by type constant
+ var iface = Ci.nsIContentPolicy;
+ for each (let typeName in contentTypes)
+ {
+ if ("TYPE_" + typeName in iface)
+ {
+ let id = iface["TYPE_" + typeName];
+ Policy.type[typeName] = id;
+ Policy.typeDescr[id] = typeName;
+ Policy.localizedDescr[id] = Utils.getString("type_label_" + typeName.toLowerCase());
+ }
+ }
+
+ Policy.type.ELEMHIDE = 0xFFFD;
+ Policy.typeDescr[0xFFFD] = "ELEMHIDE";
+ Policy.localizedDescr[0xFFFD] = Utils.getString("type_label_elemhide");
+
+ Policy.type.POPUP = 0xFFFE;
+ Policy.typeDescr[0xFFFE] = "POPUP";
+ Policy.localizedDescr[0xFFFE] = Utils.getString("type_label_popup");
+
+ for each (let type in nonVisualTypes)
+ Policy.nonVisual[Policy.type[type]] = true;
+
+ // whitelisted URL schemes
+ for each (var scheme in Prefs.whitelistschemes.toLowerCase().split(" "))
+ Policy.whitelistSchemes[scheme] = true;
+
+ TimeLine.log("done initializing types");
+
+ // Generate class identifier used to collapse node and register corresponding
+ // stylesheet.
+ TimeLine.log("registering global stylesheet");
+
+ let offset = "a".charCodeAt(0);
+ Utils.collapsedClass = "";
+ for (let i = 0; i < 20; i++)
+ Utils.collapsedClass += String.fromCharCode(offset + Math.random() * 26);
+
+ let collapseStyle = Utils.makeURI("data:text/css," +
+ encodeURIComponent("." + Utils.collapsedClass +
+ "{-moz-binding: url(chrome://global/content/bindings/general.xml#foobarbazdummy) !important;}"));
+ Utils.styleService.loadAndRegisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET);
+ TimeLine.log("done registering stylesheet");
+
+ // Register our content policy
+ TimeLine.log("registering component");
+
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ try
+ {
+ registrar.registerFactory(PolicyPrivate.classID, PolicyPrivate.classDescription, PolicyPrivate.contractID, PolicyPrivate);
+ }
+ catch (e)
+ {
+ // Don't stop on errors - the factory might already be registered
+ Cu.reportError(e);
+ }
+
+ let catMan = Utils.categoryManager;
+ for each (let category in PolicyPrivate.xpcom_categories)
+ catMan.addCategoryEntry(category, PolicyPrivate.classDescription, PolicyPrivate.contractID, false, true);
+
+ Services.obs.addObserver(PolicyPrivate, "http-on-modify-request", true);
+ Services.obs.addObserver(PolicyPrivate, "content-document-global-created", true);
+
+ TimeLine.leave("ContentPolicy.startup() done");
+ },
+
+ shutdown: function()
+ {
+ PolicyPrivate.previousRequest = null;
+ },
+
+ /**
+ * Checks whether a node should be blocked, hides it if necessary
+ * @param wnd {nsIDOMWindow}
+ * @param node {nsIDOMElement}
+ * @param contentType {String}
+ * @param location {nsIURI}
+ * @param collapse {Boolean} true to force hiding of the node
+ * @return {Boolean} false if the node should be blocked
+ */
+ processNode: function(wnd, node, contentType, location, collapse)
+ {
+ let topWnd = wnd.top;
+ if (!topWnd || !topWnd.location || !topWnd.location.href)
+ return true;
+
+ let originWindow = Utils.getOriginWindow(wnd);
+ let wndLocation = originWindow.location.href;
+ let docDomain = getHostname(wndLocation);
+ let match = null;
+ if (!match && Prefs.enabled)
+ {
+ let testWnd = wnd;
+ let parentWndLocation = getWindowLocation(testWnd);
+ while (true)
+ {
+ let testWndLocation = parentWndLocation;
+ parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent));
+ match = Policy.isWhitelisted(testWndLocation, parentWndLocation);
+
+ if (!(match instanceof WhitelistFilter))
+
+ if (match instanceof WhitelistFilter)
+ {
+ FilterStorage.increaseHitCount(match);
+ RequestNotifier.addNodeData(testWnd.document, topWnd, Policy.type.DOCUMENT, getHostname(parentWndLocation), false, testWndLocation, match);
+ return true;
+ }
+
+ if (testWnd.parent == testWnd)
+ break;
+ else
+ testWnd = testWnd.parent;
+ }
+ }
+
+ // Data loaded by plugins should be attached to the document
+ if (contentType == Policy.type.OBJECT_SUBREQUEST && node instanceof Ci.nsIDOMElement)
+ node = node.ownerDocument;
+
+ // Fix type for objects misrepresented as frames or images
+ if (contentType != Policy.type.OBJECT && (node instanceof Ci.nsIDOMHTMLObjectElement || node instanceof Ci.nsIDOMHTMLEmbedElement))
+ contentType = Policy.type.OBJECT;
+
+ let locationText = location.spec;
+ if (!match && contentType == Policy.type.ELEMHIDE)
+ {
+ let testWnd = wnd;
+ let parentWndLocation = getWindowLocation(testWnd);
+ while (true)
+ {
+ let testWndLocation = parentWndLocation;
+ parentWndLocation = (testWnd == testWnd.parent ? testWndLocation : getWindowLocation(testWnd.parent));
+ let parentDocDomain = getHostname(parentWndLocation);
+ match = defaultMatcher.matchesAny(testWndLocation, "ELEMHIDE", parentDocDomain, false);
+ if (match instanceof WhitelistFilter)
+ {
+ FilterStorage.increaseHitCount(match);
+ RequestNotifier.addNodeData(testWnd.document, topWnd, contentType, parentDocDomain, false, testWndLocation, match);
+ return true;
+ }
+
+ if (testWnd.parent == testWnd)
+ break;
+ else
+ testWnd = testWnd.parent;
+ }
+
+ match = location;
+ locationText = match.text.replace(/^.*?#/, '#');
+ location = locationText;
+
+ if (!match.isActiveOnDomain(docDomain))
+ return true;
+ }
+
+ let thirdParty = (contentType == Policy.type.ELEMHIDE ? false : isThirdParty(location, docDomain));
+
+ if (!match && Prefs.enabled)
+ {
+ match = defaultMatcher.matchesAny(locationText, Policy.typeDescr[contentType] || "", docDomain, thirdParty);
+ if (match instanceof BlockingFilter && node.ownerDocument && !(contentType in Policy.nonVisual))
+ {
+ let prefCollapse = (match.collapse != null ? match.collapse : !Prefs.fastcollapse);
+ if (collapse || prefCollapse)
+ Utils.schedulePostProcess(node);
+ }
+
+ // Track mouse events for objects
+ if (!match && contentType == Policy.type.OBJECT && node.nodeType == Ci.nsIDOMNode.ELEMENT_NODE)
+ {
+ node.addEventListener("mouseover", objectMouseEventHander, true);
+ node.addEventListener("mouseout", objectMouseEventHander, true);
+ }
+ }
+
+ // Store node data
+ RequestNotifier.addNodeData(node, topWnd, contentType, docDomain, thirdParty, locationText, match);
+ if (match)
+ FilterStorage.increaseHitCount(match);
+
+ return !match || match instanceof WhitelistFilter;
+ },
+
+ /**
+ * Checks whether the location's scheme is blockable.
+ * @param location {nsIURI}
+ * @return {Boolean}
+ */
+ isBlockableScheme: function(location)
+ {
+ return !(location.scheme in Policy.whitelistSchemes);
+ },
+
+ /**
+ * Checks whether a page is whitelisted.
+ * @param {String} url
+ * @param {String} [parentUrl] location of the parent page
+ * @return {Filter} filter that matched the URL or null if not whitelisted
+ */
+ isWhitelisted: function(url, parentUrl)
+ {
+ if (!url)
+ return null;
+
+ // Do not apply exception rules to schemes on our whitelistschemes list.
+ let match = /^([\w\-]+):/.exec(url);
+ if (match && match[1] in Policy.whitelistSchemes)
+ return null;
+
+ if (!parentUrl)
+ parentUrl = url;
+
+ // Ignore fragment identifier
+ let index = url.indexOf("#");
+ if (index >= 0)
+ url = url.substring(0, index);
+
+ let result = defaultMatcher.matchesAny(url, "DOCUMENT", getHostname(parentUrl), false);
+ return (result instanceof WhitelistFilter ? result : null);
+ },
+
+ /**
+ * Checks whether the page loaded in a window is whitelisted.
+ * @param wnd {nsIDOMWindow}
+ * @return {Filter} matching exception rule or null if not whitelisted
+ */
+ isWindowWhitelisted: function(wnd)
+ {
+ return Policy.isWhitelisted(getWindowLocation(wnd));
+ },
+
+
+ /**
+ * Asynchronously re-checks filters for given nodes.
+ */
+ refilterNodes: function(/**Node[]*/ nodes, /**RequestEntry*/ entry)
+ {
+ // Ignore nodes that have been blocked already
+ if (entry.filter && !(entry.filter instanceof WhitelistFilter))
+ return;
+
+ for each (let node in nodes)
+ Utils.runAsync(refilterNode, this, node, entry);
+ }
+};
+
+/**
+ * Private nsIContentPolicy and nsIChannelEventSink implementation
+ * @class
+ */
+var PolicyPrivate =
+{
+ classDescription: "Adblock Plus content policy",
+ classID: Components.ID("cfeaabe6-1dd1-11b2-a0c6-cb5c268894c9"),
+ contractID: "@adblockplus.org/abp/policy;1",
+ xpcom_categories: ["content-policy", "net-channel-event-sinks"],
+
+ //
+ // nsISupports interface implementation
+ //
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
+ Ci.nsIChannelEventSink, Ci.nsIFactory, Ci.nsISupportsWeakReference]),
+
+ //
+ // nsIContentPolicy interface implementation
+ //
+
+ shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra)
+ {
+ // Ignore requests without context and top-level documents
+ if (!node || contentType == Policy.type.DOCUMENT)
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ // Ignore standalone objects
+ if (contentType == Policy.type.OBJECT && node.ownerDocument && !/^text\/|[+\/]xml$/.test(node.ownerDocument.contentType))
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ let wnd = Utils.getWindow(node);
+ if (!wnd)
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ // Ignore whitelisted schemes
+ let location = Utils.unwrapURL(contentLocation);
+ if (!Policy.isBlockableScheme(location))
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ // Interpret unknown types as "other"
+ if (!(contentType in Policy.typeDescr))
+ contentType = Policy.type.OTHER;
+
+ let result = Policy.processNode(wnd, node, contentType, location, false);
+ if (result)
+ {
+ // We didn't block this request so we will probably see it again in
+ // http-on-modify-request. Keep it so that we can associate it with the
+ // channel there - will be needed in case of redirect.
+ PolicyPrivate.previousRequest = [location, contentType];
+ }
+ return (result ? Ci.nsIContentPolicy.ACCEPT : Ci.nsIContentPolicy.REJECT_REQUEST);
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra)
+ {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ //
+ // nsIObserver interface implementation
+ //
+ observe: function(subject, topic, data, additional)
+ {
+ switch (topic)
+ {
+ case "content-document-global-created":
+ {
+ if (!(subject instanceof Ci.nsIDOMWindow) || !subject.opener)
+ return;
+
+ let uri = additional || Utils.makeURI(subject.location.href);
+ if (!Policy.processNode(subject.opener, subject.opener.document, Policy.type.POPUP, uri, false))
+ {
+ subject.stop();
+ Utils.runAsync(subject.close, subject);
+ }
+ else if (uri.spec == "about:blank")
+ {
+ // An about:blank pop-up most likely means that a load will be
+ // initiated synchronously. Set a flag for our "http-on-modify-request"
+ // handler.
+ PolicyPrivate.expectingPopupLoad = true;
+ Utils.runAsync(function()
+ {
+ PolicyPrivate.expectingPopupLoad = false;
+ });
+ }
+ break;
+ }
+ case "http-on-modify-request":
+ {
+ if (!(subject instanceof Ci.nsIHttpChannel))
+ return;
+
+ if (Prefs.enabled)
+ {
+ let match = defaultMatcher.matchesAny(subject.URI.spec, "DONOTTRACK", null, false);
+ if (match && match instanceof BlockingFilter)
+ {
+ FilterStorage.increaseHitCount(match);
+ subject.setRequestHeader("DNT", "1", false);
+
+ // Bug 23845 - Some routers are broken and cannot handle DNT header
+ // following Connection header. Make sure Connection header is last.
+ try
+ {
+ let connection = subject.getRequestHeader("Connection");
+ subject.setRequestHeader("Connection", null, false);
+ subject.setRequestHeader("Connection", connection, false);
+ } catch(e) {}
+ }
+ }
+
+ if (PolicyPrivate.previousRequest && subject.URI == PolicyPrivate.previousRequest[0] &&
+ subject instanceof Ci.nsIWritablePropertyBag)
+ {
+ // We just handled a content policy call for this request - associate
+ // the data with the channel so that we can find it in case of a redirect.
+ subject.setProperty("abpRequestType", PolicyPrivate.previousRequest[1]);
+ PolicyPrivate.previousRequest = null;
+ }
+
+ if (PolicyPrivate.expectingPopupLoad)
+ {
+ let wnd = Utils.getRequestWindow(subject);
+ if (wnd && wnd.opener && wnd.location.href == "about:blank")
+ PolicyPrivate.observe(wnd, "content-document-global-created", null, subject.URI);
+ }
+
+ break;
+ }
+ }
+ },
+
+ //
+ // nsIChannelEventSink interface implementation
+ //
+
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
+ {
+ let result = Cr.NS_OK;
+ try
+ {
+ // Try to retrieve previously stored request data from the channel
+ let contentType;
+ if (oldChannel instanceof Ci.nsIWritablePropertyBag)
+ {
+ try
+ {
+ contentType = oldChannel.getProperty("abpRequestType");
+ }
+ catch(e)
+ {
+ // No data attached, ignore this redirect
+ return;
+ }
+ }
+
+ let newLocation = null;
+ try
+ {
+ newLocation = newChannel.URI;
+ } catch(e2) {}
+ if (!newLocation)
+ return;
+
+ let wnd = Utils.getRequestWindow(newChannel);
+ if (!wnd)
+ return;
+
+ if (!Policy.processNode(wnd, wnd.document, contentType, newLocation, false))
+ result = Cr.NS_BINDING_ABORTED;
+ }
+ catch (e)
+ {
+ // We shouldn't throw exceptions here - this will prevent the redirect.
+ Cu.reportError(e);
+ }
+ finally
+ {
+ callback.onRedirectVerifyCallback(result);
+ }
+ },
+
+ //
+ // nsIFactory interface implementation
+ //
+
+ createInstance: function(outer, iid)
+ {
+ if (outer)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this.QueryInterface(iid);
+ }
+};
+
+/**
+ * Extracts the hostname from a URL (might return null).
+ */
+function getHostname(/**String*/ url) /**String*/
+{
+ try
+ {
+ return Utils.unwrapURL(url).host;
+ }
+ catch(e)
+ {
+ return null;
+ }
+}
+
+/**
+ * Retrieves the location of a window.
+ * @param wnd {nsIDOMWindow}
+ * @return {String} window location or null on failure
+ */
+function getWindowLocation(wnd)
+{
+ if ("name" in wnd && wnd.name == "messagepane")
+ {
+ // Thunderbird branch
+ try
+ {
+ let mailWnd = wnd.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+
+ // Typically we get a wrapped mail window here, need to unwrap
+ try
+ {
+ mailWnd = mailWnd.wrappedJSObject;
+ } catch(e) {}
+
+ if ("currentHeaderData" in mailWnd && "content-base" in mailWnd.currentHeaderData)
+ {
+ return mailWnd.currentHeaderData["content-base"].headerValue;
+ }
+ else if ("currentHeaderData" in mailWnd && "from" in mailWnd.currentHeaderData)
+ {
+ let emailAddress = Utils.headerParser.extractHeaderAddressMailboxes(mailWnd.currentHeaderData.from.headerValue);
+ if (emailAddress)
+ return 'mailto:' + emailAddress.replace(/^[\s"]+/, "").replace(/[\s"]+$/, "").replace(/\s/g, '%20');
+ }
+ } catch(e) {}
+ }
+ else
+ {
+ // Firefox branch
+ return wnd.location.href;
+ }
+}
+
+/**
+ * Checks whether the location's origin is different from document's origin.
+ */
+function isThirdParty(/**nsIURI*/location, /**String*/ docDomain) /**Boolean*/
+{
+ if (!location || !docDomain)
+ return true;
+
+ try
+ {
+ return Utils.effectiveTLD.getBaseDomain(location) != Utils.effectiveTLD.getBaseDomainFromHost(docDomain);
+ }
+ catch (e)
+ {
+ // EffectiveTLDService throws on IP addresses, just compare the host name
+ let host = "";
+ try
+ {
+ host = location.host;
+ } catch (e) {}
+ return host != docDomain;
+ }
+}
+
+/**
+ * Re-checks filters on an element.
+ */
+function refilterNode(/**Node*/ node, /**RequestEntry*/ entry)
+{
+ let wnd = Utils.getWindow(node);
+ if (!wnd || wnd.closed)
+ return;
+
+ if (entry.type == Policy.type.OBJECT)
+ {
+ node.removeEventListener("mouseover", objectMouseEventHander, true);
+ node.removeEventListener("mouseout", objectMouseEventHander, true);
+ }
+ Policy.processNode(wnd, node, entry.type, Utils.makeURI(entry.location), true);
+}
diff --git a/abprime/modules/ContentPolicyRemote.jsm b/abprime/modules/ContentPolicyRemote.jsm
new file mode 100644
index 00000000..6c260122
--- /dev/null
+++ b/abprime/modules/ContentPolicyRemote.jsm
@@ -0,0 +1,265 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Content policy to be loaded in the content process for a multi-process setup (currently only Fennec)
+ */
+
+var EXPORTED_SYMBOLS = ["PolicyRemote"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://@ADDON_CHROME_NAME@/modules/Utils.jsm");
+
+/**
+ * nsIContentPolicy and nsIChannelEventSink implementation
+ * @class
+ */
+var PolicyRemote =
+{
+ classDescription: "Adblock Plus content policy",
+ classID: Components.ID("094560a0-4fed-11e0-b8af-0800200c9a66"),
+ contractID: "@adblockplus.org/abp/policy-remote;1",
+ xpcom_categories: ["content-policy", "net-channel-event-sinks"],
+
+ cache: new Cache(512),
+
+ startup: function()
+ {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ try
+ {
+ registrar.registerFactory(PolicyRemote.classID, PolicyRemote.classDescription, PolicyRemote.contractID, PolicyRemote);
+ }
+ catch (e)
+ {
+ // Don't stop on errors - the factory might already be registered
+ Cu.reportError(e);
+ }
+
+ let catMan = Utils.categoryManager;
+ for each (let category in PolicyRemote.xpcom_categories)
+ catMan.addCategoryEntry(category, PolicyRemote.classDescription, PolicyRemote.contractID, false, true);
+
+ Services.obs.addObserver(PolicyRemote, "http-on-modify-request", true);
+ Services.obs.addObserver(PolicyRemote, "content-document-global-created", true);
+
+ // Generate class identifier used to collapse node and register corresponding
+ // stylesheet.
+ let offset = "a".charCodeAt(0);
+ Utils.collapsedClass = "";
+ for (let i = 0; i < 20; i++)
+ Utils.collapsedClass += String.fromCharCode(offset + Math.random() * 26);
+
+ let collapseStyle = Utils.makeURI("data:text/css," +
+ encodeURIComponent("." + Utils.collapsedClass +
+ "{-moz-binding: url(chrome://global/content/bindings/general.xml#foobarbazdummy) !important;}"));
+ Utils.styleService.loadAndRegisterSheet(collapseStyle, Ci.nsIStyleSheetService.USER_SHEET);
+
+ // Get notified if we need to invalidate our matching cache
+ Utils.childMessageManager.addMessageListener("AdblockPlus:Matcher:clearCache", function(message)
+ {
+ PolicyRemote.cache.clear();
+ });
+ },
+
+ //
+ // nsISupports interface implementation
+ //
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver,
+ Ci.nsIChannelEventSink, Ci.nsIFactory, Ci.nsISupportsWeakReference]),
+
+ //
+ // nsIContentPolicy interface implementation
+ //
+
+ shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra)
+ {
+ // Ignore requests without context and top-level documents
+ if (!node || contentType == Ci.nsIContentPolicy.TYPE_DOCUMENT)
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ let wnd = Utils.getWindow(node);
+ if (!wnd)
+ return Ci.nsIContentPolicy.ACCEPT;
+
+ wnd = Utils.getOriginWindow(wnd);
+
+ let locations = [];
+ let testWnd = wnd;
+ while (true)
+ {
+ locations.push(testWnd.location.href);
+ if (testWnd.parent == testWnd)
+ break;
+ else
+ testWnd = testWnd.parent;
+ }
+
+ let key = contentType + " " + contentLocation.spec + " " + locations.join(" ");
+ if (!(key in this.cache.data))
+ {
+ this.cache.add(key, Utils.childMessageManager.sendSyncMessage("AdblockPlus:Policy:shouldLoad", {
+ contentType: contentType,
+ contentLocation: contentLocation.spec,
+ locations: locations})[0]);
+ }
+
+ let result = this.cache.data[key];
+ if (result.value == Ci.nsIContentPolicy.ACCEPT)
+ {
+ // We didn't block this request so we will probably see it again in
+ // http-on-modify-request. Keep it so that we can associate it with the
+ // channel there - will be needed in case of redirect.
+ PolicyRemote.previousRequest = [Utils.unwrapURL(contentLocation), contentType];
+ }
+ else if (result.postProcess)
+ Utils.schedulePostProcess(node);
+ return result.value;
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, insecNode, mimeType, extra)
+ {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ //
+ // nsIObserver interface implementation
+ //
+ observe: function(subject, topic, data, additional)
+ {
+ switch (topic)
+ {
+ case "content-document-global-created":
+ {
+ if (!(subject instanceof Ci.nsIDOMWindow) || !subject.opener)
+ return;
+
+ let uri = additional || Utils.makeURI(subject.location.href);
+ if (PolicyRemote.shouldLoad(0xFFFE /*Policy.type.POPUP*/, uri, null, subject.opener.document, null, null) != Ci.nsIContentPolicy.ACCEPT)
+ {
+ subject.stop();
+ Utils.runAsync(subject.close, subject);
+ }
+ else if (uri.spec == "about:blank")
+ {
+ // An about:blank pop-up most likely means that a load will be
+ // initiated synchronously. Set a flag for our "http-on-modify-request"
+ // handler.
+ PolicyRemote.expectingPopupLoad = true;
+ Utils.runAsync(function()
+ {
+ PolicyRemote.expectingPopupLoad = false;
+ });
+ }
+
+ break;
+ }
+ case "http-on-modify-request":
+ {
+ if (!(subject instanceof Ci.nsIHttpChannel))
+ return;
+
+ // TODO: Do-not-track header
+
+ if (PolicyRemote.previousRequest && subject.URI == PolicyRemote.previousRequest[0] &&
+ subject instanceof Ci.nsIWritablePropertyBag)
+ {
+ // We just handled a content policy call for this request - associate
+ // the data with the channel so that we can find it in case of a redirect.
+ subject.setProperty("abpRequestType", PolicyRemote.previousRequest[1]);
+ PolicyRemote.previousRequest = null;
+ }
+
+ if (PolicyRemote.expectingPopupLoad)
+ {
+ let wnd = Utils.getRequestWindow(subject);
+ if (wnd && wnd.opener && wnd.location.href == "about:blank")
+ PolicyRemote.observe(wnd, "content-document-global-created", null, subject.URI);
+ }
+
+ break;
+ }
+ }
+ },
+
+ //
+ // nsIChannelEventSink interface implementation
+ //
+
+ onChannelRedirect: function(oldChannel, newChannel, flags)
+ {
+ try
+ {
+ // Try to retrieve previously stored request data from the channel
+ let contentType;
+ if (oldChannel instanceof Ci.nsIWritablePropertyBag)
+ {
+ try
+ {
+ contentType = oldChannel.getProperty("abpRequestType");
+ }
+ catch(e)
+ {
+ // No data attached, ignore this redirect
+ return;
+ }
+ }
+
+ let newLocation = null;
+ try
+ {
+ newLocation = newChannel.URI;
+ } catch(e2) {}
+ if (!newLocation)
+ return;
+
+ let wnd = Utils.getRequestWindow(newChannel);
+ if (!wnd)
+ return;
+
+ // HACK: NS_BINDING_ABORTED would be proper error code to throw but this will show up in error console (bug 287107)
+ if (PolicyRemote.shouldLoad(contentType, newLocation, null, wnd.document) != Ci.nsIContentPolicy.ACCEPT)
+ throw Cr.NS_BASE_STREAM_WOULD_BLOCK;
+ else
+ return;
+ }
+ catch (e if (e != Cr.NS_BASE_STREAM_WOULD_BLOCK))
+ {
+ // We shouldn't throw exceptions here - this will prevent the redirect.
+ Cu.reportError(e);
+ }
+ },
+
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
+ {
+ this.onChannelRedirect(oldChannel, newChannel, flags);
+
+ // If onChannelRedirect didn't throw an exception indicate success
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ },
+
+ //
+ // nsIFactory interface implementation
+ //
+
+ createInstance: function(outer, iid)
+ {
+ if (outer)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this.QueryInterface(iid);
+ }
+};
+
+PolicyRemote.startup();
diff --git a/abprime/modules/ElemHide.jsm b/abprime/modules/ElemHide.jsm
new file mode 100644
index 00000000..88ce60fe
--- /dev/null
+++ b/abprime/modules/ElemHide.jsm
@@ -0,0 +1,447 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Element hiding implementation.
+ */
+
+var EXPORTED_SYMBOLS = ["ElemHide"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "IO.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "ContentPolicy.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+
+/**
+ * Lookup table, filters by their associated key
+ * @type Object
+ */
+let filterByKey = {__proto__: null};
+
+/**
+ * Lookup table, keys of the filters by filter text
+ * @type Object
+ */
+let keyByFilter = {__proto__: null};
+
+/**
+ * Currently applied stylesheet URL
+ * @type nsIURI
+ */
+let styleURL = null;
+
+/**
+ * Element hiding component
+ * @class
+ */
+var ElemHide =
+{
+ /**
+ * Indicates whether filters have been added or removed since the last apply() call.
+ * @type Boolean
+ */
+ isDirty: false,
+
+ /**
+ * Inidicates whether the element hiding stylesheet is currently applied.
+ * @type Boolean
+ */
+ applied: false,
+
+ /**
+ * Called on module startup.
+ */
+ init: function()
+ {
+ TimeLine.enter("Entered ElemHide.init()");
+ Prefs.addListener(function(name)
+ {
+ if (name == "enabled")
+ ElemHide.apply();
+ });
+
+ TimeLine.log("done adding prefs listener");
+
+ let styleFile = IO.resolveFilePath(Prefs.data_directory);
+ styleFile.append("elemhide.css");
+ styleURL = Utils.ioService.newFileURI(styleFile).QueryInterface(Ci.nsIFileURL);
+ TimeLine.log("done determining stylesheet URL");
+
+ TimeLine.log("registering component");
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(ElemHidePrivate.classID, ElemHidePrivate.classDescription,
+ "@mozilla.org/network/protocol/about;1?what=" + ElemHidePrivate.aboutPrefix, ElemHidePrivate);
+
+ TimeLine.leave("ElemHide.init() done");
+ },
+
+ /**
+ * Removes all known filters
+ */
+ clear: function()
+ {
+ filterByKey = {__proto__: null};
+ keyByFilter = {__proto__: null};
+ ElemHide.isDirty = false;
+ ElemHide.unapply();
+ },
+
+ /**
+ * Add a new element hiding filter
+ * @param {ElemHideFilter} filter
+ */
+ add: function(filter)
+ {
+ if (filter.text in keyByFilter)
+ return;
+
+ let key;
+ do {
+ key = Math.random().toFixed(15).substr(5);
+ } while (key in filterByKey);
+
+ filterByKey[key] = filter;
+ keyByFilter[filter.text] = key;
+ ElemHide.isDirty = true;
+ },
+
+ /**
+ * Removes an element hiding filter
+ * @param {ElemHideFilter} filter
+ */
+ remove: function(filter)
+ {
+ if (!(filter.text in keyByFilter))
+ return;
+
+ let key = keyByFilter[filter.text];
+ delete filterByKey[key];
+ delete keyByFilter[filter.text];
+ ElemHide.isDirty = true;
+ },
+
+ /**
+ * Will be set to true if apply() is running (reentrance protection).
+ * @type Boolean
+ */
+ _applying: false,
+
+ /**
+ * Will be set to true if an apply() call arrives while apply() is already
+ * running (delayed execution).
+ * @type Boolean
+ */
+ _needsApply: false,
+
+ /**
+ * Generates stylesheet URL and applies it globally
+ */
+ apply: function()
+ {
+ if (this._applying)
+ {
+ this._needsApply = true;
+ return;
+ }
+
+ TimeLine.enter("Entered ElemHide.apply()");
+
+ if (!ElemHide.isDirty || !Prefs.enabled)
+ {
+ // Nothing changed, looks like we merely got enabled/disabled
+ if (Prefs.enabled && !ElemHide.applied)
+ {
+ try
+ {
+ Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
+ ElemHide.applied = true;
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ TimeLine.log("Applying existing stylesheet finished");
+ }
+ else if (!Prefs.enabled && ElemHide.applied)
+ {
+ ElemHide.unapply();
+ TimeLine.log("ElemHide.unapply() finished");
+ }
+
+ TimeLine.leave("ElemHide.apply() done (no file changes)");
+ return;
+ }
+
+ IO.writeToFile(styleURL.file, false, this._generateCSSContent(), function(e)
+ {
+ TimeLine.enter("ElemHide.apply() write callback");
+ this._applying = false;
+
+ if (e && e.result == Cr.NS_ERROR_NOT_AVAILABLE)
+ {
+ e = null;
+ try
+ {
+ styleURL.file.remove(false);
+ } catch (e2) {}
+ }
+ else if (e)
+ Cu.reportError(e);
+
+ if (this._needsApply)
+ {
+ this._needsApply = false;
+ this.apply();
+ }
+ else if (!e)
+ {
+ ElemHide.isDirty = false;
+
+ ElemHide.unapply();
+ TimeLine.log("ElemHide.unapply() finished");
+
+ if (styleURL.file.exists())
+ {
+ try
+ {
+ Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
+ ElemHide.applied = true;
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ TimeLine.log("Applying stylesheet finished");
+ }
+
+ FilterNotifier.triggerListeners("elemhideupdate");
+ }
+ TimeLine.leave("ElemHide.apply() write callback done");
+ }.bind(this), "ElemHideWrite");
+
+ this._applying = true;
+
+ TimeLine.leave("ElemHide.apply() done", "ElemHideWrite");
+ },
+
+ _generateCSSContent: function()
+ {
+ // Grouping selectors by domains
+ TimeLine.log("start grouping selectors");
+ let domains = {__proto__: null};
+ let hasFilters = false;
+ for (let key in filterByKey)
+ {
+ let filter = filterByKey[key];
+ let domain = filter.selectorDomain || "";
+
+ let list;
+ if (domain in domains)
+ list = domains[domain];
+ else
+ {
+ list = {__proto__: null};
+ domains[domain] = list;
+ }
+ list[filter.selector] = key;
+ hasFilters = true;
+ }
+ TimeLine.log("done grouping selectors");
+
+ if (!hasFilters)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ function escapeChar(match)
+ {
+ return "\\" + match.charCodeAt(0).toString(16) + " ";
+ }
+
+ // Return CSS data
+ let cssTemplate = "-moz-binding: url(about:" + ElemHidePrivate.aboutPrefix + "?%ID%#dummy) !important;";
+ for (let domain in domains)
+ {
+ let rules = [];
+ let list = domains[domain];
+
+ if (domain)
+ yield ('@-moz-document domain("' + domain.split(",").join('"),domain("') + '"){').replace(/[^\x01-\x7F]/g, escapeChar);
+ else
+ {
+ // Only allow unqualified rules on a few protocols to prevent them from blocking chrome
+ yield '@-moz-document url-prefix("http://"),url-prefix("https://"),'
+ + 'url-prefix("mailbox://"),url-prefix("imap://"),'
+ + 'url-prefix("news://"),url-prefix("snews://"){';
+ }
+
+ for (let selector in list)
+ yield selector.replace(/[^\x01-\x7F]/g, escapeChar) + "{" + cssTemplate.replace("%ID%", list[selector]) + "}";
+ yield '}';
+ }
+ },
+
+ /**
+ * Unapplies current stylesheet URL
+ */
+ unapply: function()
+ {
+ if (ElemHide.applied)
+ {
+ try
+ {
+ Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ ElemHide.applied = false;
+ }
+ },
+
+ /**
+ * Retrieves the currently applied stylesheet URL
+ * @type String
+ */
+ get styleURL() ElemHide.applied ? styleURL.spec : null,
+
+ /**
+ * Retrieves an element hiding filter by the corresponding protocol key
+ */
+ getFilterByKey: function(/**String*/ key) /**Filter*/
+ {
+ return (key in filterByKey ? filterByKey[key] : null);
+ }
+};
+
+/**
+ * Private nsIAboutModule implementation
+ * @class
+ */
+var ElemHidePrivate =
+{
+ classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"),
+ classDescription: "Element hiding hit registration protocol handler",
+ aboutPrefix: "abp-elemhidehit",
+
+ //
+ // Factory implementation
+ //
+
+ createInstance: function(outer, iid)
+ {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ return this.QueryInterface(iid);
+ },
+
+ //
+ // About module implementation
+ //
+
+ getURIFlags: function(uri)
+ {
+ return ("HIDE_FROM_ABOUTABOUT" in Ci.nsIAboutModule ? Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT : 0);
+ },
+
+ newChannel: function(uri)
+ {
+ let match = /\?(\d+)/.exec(uri.path)
+ if (!match)
+ throw Cr.NS_ERROR_FAILURE;
+
+ return new HitRegistrationChannel(uri, match[1]);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
+};
+
+/**
+ * Channel returning data for element hiding hits.
+ * @constructor
+ */
+function HitRegistrationChannel(uri, key)
+{
+ this.key = key;
+ this.URI = this.originalURI = uri;
+}
+HitRegistrationChannel.prototype = {
+ key: null,
+ URI: null,
+ originalURI: null,
+ contentCharset: "utf-8",
+ contentLength: 0,
+ contentType: "text/xml",
+ owner: Utils.systemPrincipal,
+ securityInfo: null,
+ notificationCallbacks: null,
+ loadFlags: 0,
+ loadGroup: null,
+ name: null,
+ status: Cr.NS_OK,
+
+ asyncOpen: function(listener, context)
+ {
+ let stream = this.open();
+ Utils.runAsync(function()
+ {
+ try {
+ listener.onStartRequest(this, context);
+ } catch(e) {}
+ try {
+ listener.onDataAvailable(this, context, stream, 0, stream.available());
+ } catch(e) {}
+ try {
+ listener.onStopRequest(this, context, Cr.NS_OK);
+ } catch(e) {}
+ }, this);
+ },
+
+ open: function()
+ {
+ let data = " ";
+ if (this.key in filterByKey)
+ {
+ let wnd = Utils.getRequestWindow(this);
+ if (wnd && wnd.document && !Policy.processNode(wnd, wnd.document, Policy.type.ELEMHIDE, filterByKey[this.key]))
+ data = " ";
+ }
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+ return stream;
+ },
+ isPending: function()
+ {
+ return false;
+ },
+ cancel: function()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ suspend: function()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ resume: function()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+};
diff --git a/abprime/modules/ElemHideRemote.jsm b/abprime/modules/ElemHideRemote.jsm
new file mode 100644
index 00000000..ae22966a
--- /dev/null
+++ b/abprime/modules/ElemHideRemote.jsm
@@ -0,0 +1,186 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Element hiding protocol to be loaded in the content process for a multi-process setup (currently only Fennec)
+ */
+
+var EXPORTED_SYMBOLS = ["ElemHideRemote"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://@ADDON_CHROME_NAME@/modules/Utils.jsm");
+
+/**
+ * Currently applied stylesheet URL
+ * @type nsIURI
+ */
+let styleURL = null;
+
+/**
+ * nsIAboutModule implementation
+ * @class
+ */
+var ElemHideRemote =
+{
+ classID: Components.ID("{55fb7be0-1dd2-11b2-98e6-9e97caf8ba67}"),
+ classDescription: "Element hiding hit registration protocol handler",
+ aboutPrefix: "abp-elemhidehit",
+
+ startup: function()
+ {
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(ElemHideRemote.classID, ElemHideRemote.classDescription,
+ "@mozilla.org/network/protocol/about;1?what=" + ElemHideRemote.aboutPrefix, ElemHideRemote);
+
+ styleURL = Utils.makeURI(Utils.childMessageManager.sendSyncMessage("AdblockPlus:ElemHide:styleURL"));
+ if (styleURL)
+ Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
+
+ // Get notified about style URL changes
+ Utils.childMessageManager.addMessageListener("AdblockPlus:ElemHide:updateStyleURL", function(message)
+ {
+ if (styleURL)
+ Utils.styleService.unregisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
+
+ styleURL = Utils.makeURI(message.json);
+ if (styleURL)
+ Utils.styleService.loadAndRegisterSheet(styleURL, Ci.nsIStyleSheetService.USER_SHEET);
+ });
+ },
+
+ //
+ // Factory implementation
+ //
+
+ createInstance: function(outer, iid)
+ {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+
+ return this.QueryInterface(iid);
+ },
+
+ //
+ // About module implementation
+ //
+
+ getURIFlags: function(uri)
+ {
+ return Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+ },
+
+ newChannel: function(uri)
+ {
+ let match = /\?(\d+)/.exec(uri.path)
+ if (!match)
+ throw Cr.NS_ERROR_FAILURE;
+
+ return new HitRegistrationChannel(uri, match[1]);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory, Ci.nsIAboutModule])
+};
+
+/**
+ * Channel returning data for element hiding hits.
+ * @constructor
+ */
+function HitRegistrationChannel(uri, key)
+{
+ this.key = key;
+ this.URI = this.originalURI = uri;
+}
+HitRegistrationChannel.prototype = {
+ key: null,
+ URI: null,
+ originalURI: null,
+ contentCharset: "utf-8",
+ contentLength: 0,
+ contentType: "text/xml",
+ owner: Utils.systemPrincipal,
+ securityInfo: null,
+ notificationCallbacks: null,
+ loadFlags: 0,
+ loadGroup: null,
+ name: null,
+ status: Cr.NS_OK,
+
+ asyncOpen: function(listener, context)
+ {
+ let stream = this.open();
+ Utils.runAsync(function()
+ {
+ try {
+ listener.onStartRequest(this, context);
+ } catch(e) {}
+ try {
+ listener.onDataAvailable(this, context, stream, 0, stream.available());
+ } catch(e) {}
+ try {
+ listener.onStopRequest(this, context, Cr.NS_OK);
+ } catch(e) {}
+ }, this);
+ },
+
+ open: function()
+ {
+ let data = " ";
+ let wnd = Utils.getRequestWindow(this);
+
+ if (wnd)
+ {
+ wnd = Utils.getOriginWindow(wnd);
+
+ let locations = [];
+ let testWnd = wnd;
+ while (true)
+ {
+ locations.push(testWnd.location.href);
+ if (testWnd.parent == testWnd)
+ break;
+ else
+ testWnd = testWnd.parent;
+ }
+
+ let result = Utils.childMessageManager.sendSyncMessage("AdblockPlus:ElemHide:checkHit", {
+ key: this.key,
+ locations: locations})[0];
+ if (result)
+ data = " ";
+ }
+
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ stream.setData(data, data.length);
+ return stream;
+ },
+ isPending: function()
+ {
+ return false;
+ },
+ cancel: function()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ suspend: function()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ resume: function()
+ {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannel, Ci.nsIRequest])
+};
+
+ElemHideRemote.startup();
diff --git a/abprime/modules/FilterClasses.jsm b/abprime/modules/FilterClasses.jsm
new file mode 100644
index 00000000..4d1e03b8
--- /dev/null
+++ b/abprime/modules/FilterClasses.jsm
@@ -0,0 +1,813 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Definition of Filter class and its subclasses.
+ */
+
+var EXPORTED_SYMBOLS = ["Filter", "InvalidFilter", "CommentFilter", "ActiveFilter", "RegExpFilter", "BlockingFilter", "WhitelistFilter", "ElemHideFilter"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+
+/**
+ * Abstract base class for filters
+ *
+ * @param {String} text string representation of the filter
+ * @constructor
+ */
+function Filter(text)
+{
+ this.text = text;
+ this.subscriptions = [];
+}
+Filter.prototype =
+{
+ /**
+ * String representation of the filter
+ * @type String
+ */
+ text: null,
+
+ /**
+ * Filter subscriptions the filter belongs to
+ * @type Array of Subscription
+ */
+ subscriptions: null,
+
+ /**
+ * Serializes the filter to an array of strings for writing out on the disk.
+ * @param {Array of String} buffer buffer to push the serialization results into
+ */
+ serialize: function(buffer)
+ {
+ buffer.push("[Filter]");
+ buffer.push("text=" + this.text);
+ },
+
+ toString: function()
+ {
+ return this.text;
+ }
+};
+
+/**
+ * Cache for known filters, maps string representation to filter objects.
+ * @type Object
+ */
+Filter.knownFilters = {__proto__: null};
+
+/**
+ * Regular expression that element hiding filters should match
+ * @type RegExp
+ */
+Filter.elemhideRegExp = /^([^\/\*\|\@"!]*?)#(?:([\w\-]+|\*)((?:\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\))*)|#([^{}]+))$/;
+/**
+ * Regular expression that RegExp filters specified as RegExps should match
+ * @type RegExp
+ */
+Filter.regexpRegExp = /^(@@)?\/.*\/(?:\$~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)?$/;
+/**
+ * Regular expression that options on a RegExp filter should match
+ * @type RegExp
+ */
+Filter.optionsRegExp = /\$(~?[\w\-]+(?:=[^,\s]+)?(?:,~?[\w\-]+(?:=[^,\s]+)?)*)$/;
+
+/**
+ * Creates a filter of correct type from its text representation - does the basic parsing and
+ * calls the right constructor then.
+ *
+ * @param {String} text as in Filter()
+ * @return {Filter} filter or null if the filter couldn't be created
+ */
+Filter.fromText = function(text)
+{
+ if (text in Filter.knownFilters)
+ return Filter.knownFilters[text];
+
+ if (!/\S/.test(text))
+ return null;
+
+ let ret;
+ let match = Filter.elemhideRegExp.exec(text);
+ if (match)
+ ret = ElemHideFilter.fromText(text, match[1], match[2], match[3], match[4]);
+ else if (text[0] == "!")
+ ret = new CommentFilter(text);
+ else
+ ret = RegExpFilter.fromText(text);
+
+ Filter.knownFilters[ret.text] = ret;
+ return ret;
+}
+
+/**
+ * Deserializes a filter
+ *
+ * @param {Object} obj map of serialized properties and their values
+ * @return {Filter} filter or null if the filter couldn't be created
+ */
+Filter.fromObject = function(obj)
+{
+ let ret = Filter.fromText(obj.text);
+ if (ret instanceof ActiveFilter)
+ {
+ if ("disabled" in obj)
+ ret._disabled = (obj.disabled == "true");
+ if ("hitCount" in obj)
+ ret._hitCount = parseInt(obj.hitCount) || 0;
+ if ("lastHit" in obj)
+ ret._lastHit = parseInt(obj.lastHit) || 0;
+ }
+ return ret;
+}
+
+/**
+ * Removes unnecessary whitespaces from filter text, will only return null if
+ * the input parameter is null.
+ */
+Filter.normalize = function(/**String*/ text) /**String*/
+{
+ if (!text)
+ return text;
+
+ // Remove line breaks and such
+ text = text.replace(/[^\S ]/g, "");
+
+ if (/^\s*!/.test(text))
+ {
+ // Don't remove spaces inside comments
+ return text.replace(/^\s+/, "").replace(/\s+$/, "");
+ }
+ else if (Filter.elemhideRegExp.test(text))
+ {
+ // Special treatment for element hiding filters, right side is allowed to contain spaces
+ let [, domain, separator, selector] = /^(.*?)(#+)(.*)$/.exec(text); // .split(..., 2) will cut off the end of the string
+ return domain.replace(/\s/g, "") + separator + selector.replace(/^\s+/, "").replace(/\s+$/, "");
+ }
+ else
+ return text.replace(/\s/g, "");
+}
+
+/**
+ * Class for invalid filters
+ * @param {String} text see Filter()
+ * @param {String} reason Reason why this filter is invalid
+ * @constructor
+ * @augments Filter
+ */
+function InvalidFilter(text, reason)
+{
+ Filter.call(this, text);
+
+ this.reason = reason;
+}
+InvalidFilter.prototype =
+{
+ __proto__: Filter.prototype,
+
+ /**
+ * Reason why this filter is invalid
+ * @type String
+ */
+ reason: null,
+
+ /**
+ * See Filter.serialize()
+ */
+ serialize: function(buffer) {}
+};
+
+/**
+ * Class for comments
+ * @param {String} text see Filter()
+ * @constructor
+ * @augments Filter
+ */
+function CommentFilter(text)
+{
+ Filter.call(this, text);
+}
+CommentFilter.prototype =
+{
+ __proto__: Filter.prototype,
+
+ /**
+ * See Filter.serialize()
+ */
+ serialize: function(buffer) {}
+};
+
+/**
+ * Abstract base class for filters that can get hits
+ * @param {String} text see Filter()
+ * @param {String} domains (optional) Domains that the filter is restricted to separated by domainSeparator e.g. "foo.com|bar.com|~baz.com"
+ * @constructor
+ * @augments Filter
+ */
+function ActiveFilter(text, domains)
+{
+ Filter.call(this, text);
+
+ if (domains)
+ {
+ this.domainSource = domains;
+ this.__defineGetter__("domains", this._getDomains);
+ }
+}
+ActiveFilter.prototype =
+{
+ __proto__: Filter.prototype,
+
+ _disabled: false,
+ _hitCount: 0,
+ _lastHit: 0,
+
+ /**
+ * Defines whether the filter is disabled
+ * @type Boolean
+ */
+ get disabled() this._disabled,
+ set disabled(value)
+ {
+ if (value != this._disabled)
+ {
+ let oldValue = this._disabled;
+ this._disabled = value;
+ FilterNotifier.triggerListeners("filter.disabled", this, value, oldValue);
+ }
+ return this._disabled;
+ },
+
+ /**
+ * Number of hits on the filter since the last reset
+ * @type Number
+ */
+ get hitCount() this._hitCount,
+ set hitCount(value)
+ {
+ if (value != this._hitCount)
+ {
+ let oldValue = this._hitCount;
+ this._hitCount = value;
+ FilterNotifier.triggerListeners("filter.hitCount", this, value, oldValue);
+ }
+ return this._hitCount;
+ },
+
+ /**
+ * Last time the filter had a hit (in milliseconds since the beginning of the epoch)
+ * @type Number
+ */
+ get lastHit() this._lastHit,
+ set lastHit(value)
+ {
+ if (value != this._lastHit)
+ {
+ let oldValue = this._lastHit;
+ this._lastHit = value;
+ FilterNotifier.triggerListeners("filter.lastHit", this, value, oldValue);
+ }
+ return this._lastHit;
+ },
+
+ /**
+ * String that the domains property should be generated from
+ * @type String
+ */
+ domainSource: null,
+
+ /**
+ * Separator character used in domainSource property, must be overridden by subclasses
+ * @type String
+ */
+ domainSeparator: null,
+
+ /**
+ * Map containing domains that this filter should match on/not match on or null if the filter should match on all domains
+ * @type Object
+ */
+ domains: null,
+
+ /**
+ * Called first time domains property is requested, triggers _generateDomains method.
+ */
+ _getDomains: function()
+ {
+ this._generateDomains();
+ return this.domains;
+ },
+
+ /**
+ * Generates domains property when it is requested for the first time.
+ */
+ _generateDomains: function()
+ {
+ let domains = this.domainSource.split(this.domainSeparator);
+
+ delete this.domainSource;
+ delete this.domains;
+
+ if (domains.length == 1 && domains[0][0] != "~")
+ {
+ // Fast track for the common one-domain scenario
+ this.domains = {__proto__: null, "": false};
+ this.domains[domains[0]] = true;
+ }
+ else
+ {
+ let hasIncludes = false;
+ for (let i = 0; i < domains.length; i++)
+ {
+ let domain = domains[i];
+ if (domain == "")
+ continue;
+
+ let include;
+ if (domain[0] == "~")
+ {
+ include = false;
+ domain = domain.substr(1);
+ }
+ else
+ {
+ include = true;
+ hasIncludes = true;
+ }
+
+ if (!this.domains)
+ this.domains = {__proto__: null};
+
+ this.domains[domain] = include;
+ }
+ this.domains[""] = !hasIncludes;
+ }
+ },
+
+ /**
+ * Checks whether this filter is active on a domain.
+ */
+ isActiveOnDomain: function(/**String*/ docDomain) /**Boolean*/
+ {
+ // If no domains are set the rule matches everywhere
+ if (!this.domains)
+ return true;
+
+ // If the document has no host name, match only if the filter isn't restricted to specific domains
+ if (!docDomain)
+ return this.domains[""];
+
+ docDomain = docDomain.replace(/\.+$/, "").toUpperCase();
+
+ while (true)
+ {
+ if (docDomain in this.domains)
+ return this.domains[docDomain];
+
+ let nextDot = docDomain.indexOf(".");
+ if (nextDot < 0)
+ break;
+ docDomain = docDomain.substr(nextDot + 1);
+ }
+ return this.domains[""];
+ },
+
+ /**
+ * Checks whether this filter is active only on a domain and its subdomains.
+ */
+ isActiveOnlyOnDomain: function(/**String*/ docDomain) /**Boolean*/
+ {
+ if (!docDomain || !this.domains || this.domains[""])
+ return false;
+
+ docDomain = docDomain.replace(/\.+$/, "").toUpperCase();
+
+ for (let domain in this.domains)
+ if (this.domains[domain] && domain != docDomain && (domain.length <= docDomain.length || domain.indexOf("." + docDomain) != domain.length - docDomain.length - 1))
+ return false;
+
+ return true;
+ },
+
+ /**
+ * See Filter.serialize()
+ */
+ serialize: function(buffer)
+ {
+ if (this._disabled || this._hitCount || this._lastHit)
+ {
+ Filter.prototype.serialize.call(this, buffer);
+ if (this._disabled)
+ buffer.push("disabled=true");
+ if (this._hitCount)
+ buffer.push("hitCount=" + this._hitCount);
+ if (this._lastHit)
+ buffer.push("lastHit=" + this._lastHit);
+ }
+ }
+};
+
+/**
+ * Abstract base class for RegExp-based filters
+ * @param {String} text see Filter()
+ * @param {String} regexpSource filter part that the regular expression should be build from
+ * @param {Number} contentType (optional) Content types the filter applies to, combination of values from RegExpFilter.typeMap
+ * @param {Boolean} matchCase (optional) Defines whether the filter should distinguish between lower and upper case letters
+ * @param {String} domains (optional) Domains that the filter is restricted to, e.g. "foo.com|bar.com|~baz.com"
+ * @param {Boolean} thirdParty (optional) Defines whether the filter should apply to third-party or first-party content only
+ * @constructor
+ * @augments ActiveFilter
+ */
+function RegExpFilter(text, regexpSource, contentType, matchCase, domains, thirdParty)
+{
+ ActiveFilter.call(this, text, domains);
+
+ if (contentType != null)
+ this.contentType = contentType;
+ if (matchCase)
+ this.matchCase = matchCase;
+ if (thirdParty != null)
+ this.thirdParty = thirdParty;
+
+ if (regexpSource.length >= 2 && regexpSource[0] == "/" && regexpSource[regexpSource.length - 1] == "/")
+ {
+ // The filter is a regular expression - convert it immediately to catch syntax errors
+ this.regexp = new RegExp(regexpSource.substr(1, regexpSource.length - 2), this.matchCase ? "" : "i");
+ }
+ else
+ {
+ // No need to convert this filter to regular expression yet, do it on demand
+ this.regexpSource = regexpSource;
+ this.__defineGetter__("regexp", this._generateRegExp);
+ }
+}
+RegExpFilter.prototype =
+{
+ __proto__: ActiveFilter.prototype,
+
+ /**
+ * Number of filters contained, will always be 1 (required to optimize Matcher).
+ * @type Integer
+ */
+ length: 1,
+
+ /**
+ * @see ActiveFilter.domainSeparator
+ */
+ domainSeparator: "|",
+
+ /**
+ * Expression from which a regular expression should be generated - for delayed creation of the regexp property
+ * @type String
+ */
+ regexpSource: null,
+ /**
+ * Regular expression to be used when testing against this filter
+ * @type RegExp
+ */
+ regexp: null,
+ /**
+ * Content types the filter applies to, combination of values from RegExpFilter.typeMap
+ * @type Number
+ */
+ contentType: 0x7FFFFFFF,
+ /**
+ * Defines whether the filter should distinguish between lower and upper case letters
+ * @type Boolean
+ */
+ matchCase: false,
+ /**
+ * Defines whether the filter should apply to third-party or first-party content only. Can be null (apply to all content).
+ * @type Boolean
+ */
+ thirdParty: null,
+
+ /**
+ * Generates regexp property when it is requested for the first time.
+ * @return {RegExp}
+ */
+ _generateRegExp: function()
+ {
+ // Remove multiple wildcards
+ let source = this.regexpSource.replace(/\*+/g, "*");
+
+ // Remove leading wildcards
+ if (source[0] == "*")
+ source = source.substr(1);
+
+ // Remove trailing wildcards
+ let pos = source.length - 1;
+ if (pos >= 0 && source[pos] == "*")
+ source = source.substr(0, pos);
+
+ source = source.replace(/\^\|$/, "^") // remove anchors following separator placeholder
+ .replace(/\W/g, "\\$&") // escape special symbols
+ .replace(/\\\*/g, ".*") // replace wildcards by .*
+ // process separator placeholders (all ANSI charaters but alphanumeric characters and _%.-)
+ .replace(/\\\^/g, "(?:[\\x00-\\x24\\x26-\\x2C\\x2F\\x3A-\\x40\\x5B-\\x5E\\x60\\x7B-\\x80]|$)")
+ .replace(/^\\\|\\\|/, "^[\\w\\-]+:\\/+(?!\\/)(?:[^.\\/]+\\.)*?") // process extended anchor at expression start
+ .replace(/^\\\|/, "^") // process anchor at expression start
+ .replace(/\\\|$/, "$"); // process anchor at expression end
+
+ let regexp = new RegExp(source, this.matchCase ? "" : "i");
+
+ delete this.regexp;
+ delete this.regexpSource;
+ return (this.regexp = regexp);
+ },
+
+ /**
+ * Tests whether the URL matches this filter
+ * @param {String} location URL to be tested
+ * @param {String} contentType content type identifier of the URL
+ * @param {String} docDomain domain name of the document that loads the URL
+ * @param {Boolean} thirdParty should be true if the URL is a third-party request
+ * @return {Boolean} true in case of a match
+ */
+ matches: function(location, contentType, docDomain, thirdParty)
+ {
+ if (this.regexp.test(location) &&
+ (RegExpFilter.typeMap[contentType] & this.contentType) != 0 &&
+ (this.thirdParty == null || this.thirdParty == thirdParty) &&
+ this.isActiveOnDomain(docDomain))
+ {
+ return true;
+ }
+
+ return false;
+ }
+};
+
+RegExpFilter.prototype.__defineGetter__("0", function()
+{
+ return this;
+});
+
+/**
+ * Creates a RegExp filter from its text representation
+ * @param {String} text same as in Filter()
+ */
+RegExpFilter.fromText = function(text)
+{
+ let blocking = true;
+ let origText = text;
+ if (text.indexOf("@@") == 0)
+ {
+ blocking = false;
+ text = text.substr(2);
+ }
+
+ let contentType = null;
+ let matchCase = null;
+ let domains = null;
+ let thirdParty = null;
+ let collapse = null;
+ let options;
+ let match = Filter.optionsRegExp.exec(text);
+ if (match)
+ {
+ options = match[1].toUpperCase().split(",");
+ text = match.input.substr(0, match.index);
+ for each (let option in options)
+ {
+ let value = null;
+ let separatorIndex = option.indexOf("=");
+ if (separatorIndex >= 0)
+ {
+ value = option.substr(separatorIndex + 1);
+ option = option.substr(0, separatorIndex);
+ }
+ option = option.replace(/-/, "_");
+ if (option in RegExpFilter.typeMap)
+ {
+ if (contentType == null)
+ contentType = 0;
+ contentType |= RegExpFilter.typeMap[option];
+ }
+ else if (option[0] == "~" && option.substr(1) in RegExpFilter.typeMap)
+ {
+ if (contentType == null)
+ contentType = RegExpFilter.prototype.contentType;
+ contentType &= ~RegExpFilter.typeMap[option.substr(1)];
+ }
+ else if (option == "MATCH_CASE")
+ matchCase = true;
+ else if (option == "DOMAIN" && typeof value != "undefined")
+ domains = value;
+ else if (option == "THIRD_PARTY")
+ thirdParty = true;
+ else if (option == "~THIRD_PARTY")
+ thirdParty = false;
+ else if (option == "COLLAPSE")
+ collapse = true;
+ else if (option == "~COLLAPSE")
+ collapse = false;
+ }
+ }
+
+ if (!blocking && (contentType == null || (contentType & RegExpFilter.typeMap.DOCUMENT)) &&
+ (!options || options.indexOf("DOCUMENT") < 0) && !/^\|?[\w\-]+:/.test(text))
+ {
+ // Exception filters shouldn't apply to pages by default unless they start with a protocol name
+ if (contentType == null)
+ contentType = RegExpFilter.prototype.contentType;
+ contentType &= ~RegExpFilter.typeMap.DOCUMENT;
+ }
+
+ try
+ {
+ if (blocking)
+ return new BlockingFilter(origText, text, contentType, matchCase, domains, thirdParty, collapse);
+ else
+ return new WhitelistFilter(origText, text, contentType, matchCase, domains, thirdParty);
+ }
+ catch (e)
+ {
+ return new InvalidFilter(text, e);
+ }
+}
+
+/**
+ * Maps type strings like "SCRIPT" or "OBJECT" to bit masks
+ */
+RegExpFilter.typeMap = {
+ OTHER: 1,
+ SCRIPT: 2,
+ IMAGE: 4,
+ STYLESHEET: 8,
+ OBJECT: 16,
+ SUBDOCUMENT: 32,
+ DOCUMENT: 64,
+ XBL: 1,
+ PING: 1,
+ XMLHTTPREQUEST: 2048,
+ OBJECT_SUBREQUEST: 4096,
+ DTD: 1,
+ MEDIA: 16384,
+ FONT: 32768,
+ WEBSOCKET: 1,
+ WEBRTC: 1,
+ CSP: 1,
+
+ BACKGROUND: 4, // Backwards compat, same as IMAGE
+
+ POPUP: 0x10000000,
+ DONOTTRACK: 0x20000000,
+ ELEMHIDE: 0x40000000
+};
+
+// ELEMHIDE, DONOTTRACK, POPUP option shouldn't be there by default
+RegExpFilter.prototype.contentType &= ~(
+ RegExpFilter.typeMap.ELEMHIDE |
+ RegExpFilter.typeMap.DONOTTRACK |
+ RegExpFilter.typeMap.POPUP |
+ RegExpFilter.typeMap.WEBSOCKET |
+ RegExpFilter.typeMap.WEBRTC |
+ RegExpFilter.typeMap.CSP
+);
+
+/**
+ * Class for blocking filters
+ * @param {String} text see Filter()
+ * @param {String} regexpSource see RegExpFilter()
+ * @param {Number} contentType see RegExpFilter()
+ * @param {Boolean} matchCase see RegExpFilter()
+ * @param {String} domains see RegExpFilter()
+ * @param {Boolean} thirdParty see RegExpFilter()
+ * @param {Boolean} collapse defines whether the filter should collapse blocked content, can be null
+ * @constructor
+ * @augments RegExpFilter
+ */
+function BlockingFilter(text, regexpSource, contentType, matchCase, domains, thirdParty, collapse)
+{
+ RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty);
+
+ this.collapse = collapse;
+}
+BlockingFilter.prototype =
+{
+ __proto__: RegExpFilter.prototype,
+
+ /**
+ * Defines whether the filter should collapse blocked content. Can be null (use the global preference).
+ * @type Boolean
+ */
+ collapse: null
+};
+
+/**
+ * Class for whitelist filters
+ * @param {String} text see Filter()
+ * @param {String} regexpSource see RegExpFilter()
+ * @param {Number} contentType see RegExpFilter()
+ * @param {Boolean} matchCase see RegExpFilter()
+ * @param {String} domains see RegExpFilter()
+ * @param {Boolean} thirdParty see RegExpFilter()
+ * @constructor
+ * @augments RegExpFilter
+ */
+function WhitelistFilter(text, regexpSource, contentType, matchCase, domains, thirdParty)
+{
+ RegExpFilter.call(this, text, regexpSource, contentType, matchCase, domains, thirdParty);
+}
+
+WhitelistFilter.prototype =
+{
+ __proto__: RegExpFilter.prototype,
+
+}
+
+/**
+ * Class for element hiding filters
+ * @param {String} text see Filter()
+ * @param {String} domains (optional) Host names or domains the filter should be restricted to
+ * @param {String} selector CSS selector for the HTML elements that should be hidden
+ * @constructor
+ * @augments ActiveFilter
+ */
+function ElemHideFilter(text, domains, selector)
+{
+ ActiveFilter.call(this, text, domains ? domains.toUpperCase() : null);
+
+ if (domains)
+ this.selectorDomain = domains.replace(/,~[^,]+/g, "").replace(/^~[^,]+,?/, "").toLowerCase();
+ this.selector = selector;
+}
+ElemHideFilter.prototype =
+{
+ __proto__: ActiveFilter.prototype,
+
+ /**
+ * @see ActiveFilter.domainSeparator
+ */
+ domainSeparator: ",",
+
+ /**
+ * Host name or domain the filter should be restricted to (can be null for no restriction)
+ * @type String
+ */
+ selectorDomain: null,
+ /**
+ * CSS selector for the HTML elements that should be hidden
+ * @type String
+ */
+ selector: null
+};
+
+/**
+ * Creates an element hiding filter from a pre-parsed text representation
+ *
+ * @param {String} text same as in Filter()
+ * @param {String} domain domain part of the text representation (can be empty)
+ * @param {String} tagName tag name part (can be empty)
+ * @param {String} attrRules attribute matching rules (can be empty)
+ * @param {String} selector raw CSS selector (can be empty)
+ * @return {ElemHideFilter or InvalidFilter}
+ */
+ElemHideFilter.fromText = function(text, domain, tagName, attrRules, selector)
+{
+ if (!selector)
+ {
+ if (tagName == "*")
+ tagName = "";
+
+ let id = null;
+ let additional = "";
+ if (attrRules) {
+ attrRules = attrRules.match(/\([\w\-]+(?:[$^*]?=[^\(\)"]*)?\)/g);
+ for each (let rule in attrRules) {
+ rule = rule.substr(1, rule.length - 2);
+ let separatorPos = rule.indexOf("=");
+ if (separatorPos > 0) {
+ rule = rule.replace(/=/, '="') + '"';
+ additional += "[" + rule + "]";
+ }
+ else {
+ if (id)
+ return new InvalidFilter(text, Utils.getString("filter_elemhide_duplicate_id"));
+ else
+ id = rule;
+ }
+ }
+ }
+
+ if (id)
+ selector = tagName + "." + id + additional + "," + tagName + "#" + id + additional;
+ else if (tagName || additional)
+ selector = tagName + additional;
+ else
+ return new InvalidFilter(text, Utils.getString("filter_elemhide_nocriteria"));
+ }
+ return new ElemHideFilter(text, domain, selector);
+}
diff --git a/abprime/modules/FilterListener.jsm b/abprime/modules/FilterListener.jsm
new file mode 100644
index 00000000..545bed60
--- /dev/null
+++ b/abprime/modules/FilterListener.jsm
@@ -0,0 +1,269 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Component synchronizing filter storage with Matcher instances and ElemHide.
+ */
+
+var EXPORTED_SYMBOLS = ["FilterListener"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "ElemHide.jsm");
+Cu.import(baseURL + "Matcher.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "Utils.jsm");
+
+/**
+ * Value of the FilterListener.batchMode property.
+ * @type Boolean
+ */
+let batchMode = false;
+
+/**
+ * Increases on filter changes, filters will be saved if it exceeds 1.
+ * @type Integer
+ */
+let isDirty = 0;
+
+/**
+ * This object can be used to change properties of the filter change listeners.
+ * @class
+ */
+var FilterListener =
+{
+ /**
+ * Called on module initialization, registers listeners for FilterStorage changes
+ */
+ startup: function()
+ {
+ TimeLine.enter("Entered FilterListener.startup()");
+
+ FilterNotifier.addListener(function(action, item, newValue, oldValue)
+ {
+ let match = /^(\w+)\.(.*)/.exec(action);
+ if (match && match[1] == "filter")
+ onFilterChange(match[2], item, newValue, oldValue);
+ else if (match && match[1] == "subscription")
+ onSubscriptionChange(match[2], item, newValue, oldValue);
+ else
+ onGenericChange(action, item);
+ });
+
+ ElemHide.init();
+ FilterStorage.loadFromDisk();
+
+ TimeLine.log("done initializing data structures");
+
+ Services.obs.addObserver(FilterListenerPrivate, "browser:purge-session-history", true);
+ TimeLine.log("done adding observers");
+
+ TimeLine.leave("FilterListener.startup() done");
+ },
+
+ /**
+ * Set to true when executing many changes, changes will only be fully applied after this variable is set to false again.
+ * @type Boolean
+ */
+ get batchMode()
+ {
+ return batchMode;
+ },
+ set batchMode(value)
+ {
+ batchMode = value;
+ flushElemHide();
+ },
+
+ /**
+ * Increases "dirty factor" of the filters and calls FilterStorage.saveToDisk()
+ * if it becomes 1 or more. Save is executed delayed to prevent multiple
+ * subsequent calls. If the parameter is 0 it forces saving filters if any
+ * changes were recorded after the previous save.
+ */
+ setDirty: function(/**Integer*/ factor)
+ {
+ if (factor == 0 && isDirty > 0)
+ isDirty = 1;
+ else
+ isDirty += factor;
+ if (isDirty >= 1)
+ FilterStorage.saveToDisk();
+ }
+};
+
+/**
+ * Private nsIObserver implementation.
+ * @class
+ */
+var FilterListenerPrivate =
+{
+ observe: function(subject, topic, data)
+ {
+ if (topic == "browser:purge-session-history" && Prefs.clearStatsOnHistoryPurge)
+ {
+ FilterStorage.resetHitCounts();
+ FilterListener.setDirty(0); // Force saving to disk
+
+ Prefs.recentReports = "[]";
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
+};
+
+/**
+ * Calls ElemHide.apply() if necessary.
+ */
+function flushElemHide()
+{
+ if (!batchMode && ElemHide.isDirty)
+ ElemHide.apply();
+}
+
+/**
+ * Notifies Matcher instances or ElemHide object about a new filter
+ * if necessary.
+ * @param {Filter} filter filter that has been added
+ */
+function addFilter(filter)
+{
+ if (!(filter instanceof ActiveFilter) || filter.disabled)
+ return;
+
+ let hasEnabled = false;
+ for (let i = 0; i < filter.subscriptions.length; i++)
+ if (!filter.subscriptions[i].disabled)
+ hasEnabled = true;
+ if (!hasEnabled)
+ return;
+
+ if (filter instanceof RegExpFilter)
+ defaultMatcher.add(filter);
+ else if (filter instanceof ElemHideFilter)
+ ElemHide.add(filter);
+}
+
+/**
+ * Notifies Matcher instances or ElemHide object about removal of a filter
+ * if necessary.
+ * @param {Filter} filter filter that has been removed
+ */
+function removeFilter(filter)
+{
+ if (!(filter instanceof ActiveFilter))
+ return;
+
+ if (!filter.disabled)
+ {
+ let hasEnabled = false;
+ for (let i = 0; i < filter.subscriptions.length; i++)
+ if (!filter.subscriptions[i].disabled)
+ hasEnabled = true;
+ if (hasEnabled)
+ return;
+ }
+
+ if (filter instanceof RegExpFilter)
+ defaultMatcher.remove(filter);
+ else if (filter instanceof ElemHideFilter)
+ ElemHide.remove(filter);
+}
+
+/**
+ * Subscription change listener
+ */
+function onSubscriptionChange(action, subscription, newValue, oldValue)
+{
+ FilterListener.setDirty(1);
+
+ if (action != "added" && action != "removed" && action != "disabled" && action != "updated")
+ return;
+
+ if (action != "removed" && !(subscription.url in FilterStorage.knownSubscriptions))
+ {
+ // Ignore updates for subscriptions not in the list
+ return;
+ }
+
+ if ((action == "added" || action == "removed" || action == "updated") && subscription.disabled)
+ {
+ // Ignore adding/removing/updating of disabled subscriptions
+ return;
+ }
+
+ if (action == "added" || action == "removed" || action == "disabled")
+ {
+ let method = (action == "added" || (action == "disabled" && newValue == false) ? addFilter : removeFilter);
+ if (subscription.filters)
+ subscription.filters.forEach(method);
+ }
+ else if (action == "updated")
+ {
+ subscription.oldFilters.forEach(removeFilter);
+ subscription.filters.forEach(addFilter);
+ }
+
+ flushElemHide();
+}
+
+/**
+ * Filter change listener
+ */
+function onFilterChange(action, filter, newValue, oldValue)
+{
+ if (action == "hitCount" || action == "lastHit")
+ FilterListener.setDirty(0.002);
+ else
+ FilterListener.setDirty(1);
+
+ if (action != "added" && action != "removed" && action != "disabled")
+ return;
+
+ if ((action == "added" || action == "removed") && filter.disabled)
+ {
+ // Ignore adding/removing of disabled filters
+ return;
+ }
+
+ if (action == "added" || (action == "disabled" && newValue == false))
+ addFilter(filter);
+ else
+ removeFilter(filter);
+ flushElemHide();
+}
+
+/**
+ * Generic notification listener
+ */
+function onGenericChange(action)
+{
+ if (action == "load")
+ {
+ isDirty = 0;
+
+ defaultMatcher.clear();
+ ElemHide.clear();
+ for each (let subscription in FilterStorage.subscriptions)
+ if (!subscription.disabled)
+ subscription.filters.forEach(addFilter);
+ flushElemHide();
+ }
+ else if (action == "save")
+ isDirty = 0;
+}
diff --git a/abprime/modules/FilterNotifier.jsm b/abprime/modules/FilterNotifier.jsm
new file mode 100644
index 00000000..89bdc7ae
--- /dev/null
+++ b/abprime/modules/FilterNotifier.jsm
@@ -0,0 +1,70 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview FilterNotifier class manages listeners and distributes messages
+ * about filter changes to them.
+ */
+
+var EXPORTED_SYMBOLS = ["FilterNotifier"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+/**
+ * List of registered listeners
+ * @type Array of function(action, item, newValue, oldValue)
+ */
+let listeners = [];
+
+/**
+ * This class allows registering and triggering listeners for filter events.
+ * @class
+ */
+var FilterNotifier =
+{
+ /**
+ * Adds a listener
+ */
+ addListener: function(/**function(action, item, newValue, oldValue)*/ listener)
+ {
+ if (listeners.indexOf(listener) >= 0)
+ return;
+
+ listeners.push(listener);
+ },
+
+ /**
+ * Removes a listener that was previosly added via addListener
+ */
+ removeListener: function(/**function(action, item, newValue, oldValue)*/ listener)
+ {
+ let index = listeners.indexOf(listener);
+ if (index >= 0)
+ listeners.splice(index, 1);
+ },
+
+ /**
+ * Notifies listeners about an event
+ * @param {String} action event code ("load", "save", "elemhideupdate",
+ * "subscription.added", "subscription.removed",
+ * "subscription.disabled", "subscription.title",
+ * "subscription.lastDownload", "subscription.downloadStatus",
+ * "subscription.homepage", "subscription.updated",
+ * "filter.added", "filter.removed", "filter.moved",
+ * "filter.disabled", "filter.hitCount", "filter.lastHit")
+ * @param {Subscription|Filter} item item that the change applies to
+ */
+ triggerListeners: function(action, item, param1, param2, param3)
+ {
+ for each (let listener in listeners)
+ listener(action, item, param1, param2, param3);
+ }
+};
diff --git a/abprime/modules/FilterStorage.jsm b/abprime/modules/FilterStorage.jsm
new file mode 100644
index 00000000..d11c4606
--- /dev/null
+++ b/abprime/modules/FilterStorage.jsm
@@ -0,0 +1,776 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview FilterStorage class responsible to managing user's subscriptions and filters.
+ */
+
+var EXPORTED_SYMBOLS = ["FilterStorage"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "IO.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+
+/**
+ * Version number of the filter storage file format.
+ * @type Integer
+ */
+const formatVersion = 4;
+
+/**
+ * This class reads user's filters from disk, manages them in memory and writes them back.
+ * @class
+ */
+var FilterStorage =
+{
+ /**
+ * Version number of the patterns.ini format used.
+ * @type Integer
+ */
+ get formatVersion() formatVersion,
+
+ /**
+ * File that the filter list has been loaded from and should be saved to
+ * @type nsIFile
+ */
+ get sourceFile()
+ {
+ let file = null;
+ if (Prefs.patternsfile)
+ {
+ // Override in place, use it instead of placing the file in the regular data dir
+ file = IO.resolveFilePath(Prefs.patternsfile);
+ }
+ if (!file)
+ {
+ // Place the file in the data dir
+ file = IO.resolveFilePath(Prefs.data_directory);
+ if (file)
+ file.append("patterns.ini");
+ }
+ if (!file)
+ {
+ // Data directory pref misconfigured? Try the default value
+ try
+ {
+ file = IO.resolveFilePath(Prefs.defaultBranch.getCharPref("data_directory"));
+ if (file)
+ file.append("patterns.ini");
+ } catch(e) {}
+ }
+
+ if (!file)
+ Cu.reportError("Adblock Plus: Failed to resolve filter file location from extensions.@ADDON_CHROME_NAME@.patternsfile preference");
+
+ this.__defineGetter__("sourceFile", function() file);
+ return this.sourceFile;
+ },
+
+ /**
+ * Map of properties listed in the filter storage file before the sections
+ * start. Right now this should be only the format version.
+ */
+ fileProperties: {__proto__: null},
+
+ /**
+ * List of filter subscriptions containing all filters
+ * @type Array of Subscription
+ */
+ subscriptions: [],
+
+ /**
+ * Map of subscriptions already on the list, by their URL/identifier
+ * @type Object
+ */
+ knownSubscriptions: {__proto__: null},
+
+ /**
+ * Finds the filter group that a filter should be added to by default. Will
+ * return null if this group doesn't exist yet.
+ */
+ getGroupForFilter: function(/**Filter*/ filter) /**SpecialSubscription*/
+ {
+ let generalSubscription = null;
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (subscription instanceof SpecialSubscription && !subscription.disabled)
+ {
+ // Always prefer specialized subscriptions
+ if (subscription.isDefaultFor(filter))
+ return subscription;
+
+ // If this is a general subscription - store it as fallback
+ if (!generalSubscription && (!subscription.defaults || !subscription.defaults.length))
+ generalSubscription = subscription;
+ }
+ }
+ return generalSubscription;
+ },
+
+ /**
+ * Adds a filter subscription to the list
+ * @param {Subscription} subscription filter subscription to be added
+ * @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded)
+ */
+ addSubscription: function(subscription, silent)
+ {
+ if (subscription.url in FilterStorage.knownSubscriptions)
+ return;
+
+ FilterStorage.subscriptions.push(subscription);
+ FilterStorage.knownSubscriptions[subscription.url] = subscription;
+ addSubscriptionFilters(subscription);
+
+ if (!silent)
+ FilterNotifier.triggerListeners("subscription.added", subscription);
+ },
+
+ /**
+ * Removes a filter subscription from the list
+ * @param {Subscription} subscription filter subscription to be removed
+ * @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded)
+ */
+ removeSubscription: function(subscription, silent)
+ {
+ for (let i = 0; i < FilterStorage.subscriptions.length; i++)
+ {
+ if (FilterStorage.subscriptions[i].url == subscription.url)
+ {
+ removeSubscriptionFilters(subscription);
+
+ FilterStorage.subscriptions.splice(i--, 1);
+ delete FilterStorage.knownSubscriptions[subscription.url];
+ if (!silent)
+ FilterNotifier.triggerListeners("subscription.removed", subscription);
+ return;
+ }
+ }
+ },
+
+ /**
+ * Moves a subscription in the list to a new position.
+ * @param {Subscription} subscription filter subscription to be moved
+ * @param {Subscription} [insertBefore] filter subscription to insert before
+ * (if omitted the subscription will be put at the end of the list)
+ */
+ moveSubscription: function(subscription, insertBefore)
+ {
+ let currentPos = FilterStorage.subscriptions.indexOf(subscription);
+ if (currentPos < 0)
+ return;
+
+ let newPos = insertBefore ? FilterStorage.subscriptions.indexOf(insertBefore) : -1;
+ if (newPos < 0)
+ newPos = FilterStorage.subscriptions.length;
+
+ if (currentPos < newPos)
+ newPos--;
+ if (currentPos == newPos)
+ return;
+
+ FilterStorage.subscriptions.splice(currentPos, 1);
+ FilterStorage.subscriptions.splice(newPos, 0, subscription);
+ FilterNotifier.triggerListeners("subscription.moved", subscription);
+ },
+
+ /**
+ * Replaces the list of filters in a subscription by a new list
+ * @param {Subscription} subscription filter subscription to be updated
+ * @param {Array of Filter} filters new filter lsit
+ */
+ updateSubscriptionFilters: function(subscription, filters)
+ {
+ removeSubscriptionFilters(subscription);
+ subscription.oldFilters = subscription.filters;
+ subscription.filters = filters;
+ addSubscriptionFilters(subscription);
+ FilterNotifier.triggerListeners("subscription.updated", subscription);
+ delete subscription.oldFilters;
+
+ // Do not keep empty subscriptions disabled
+ if (subscription instanceof SpecialSubscription && !subscription.filters.length && subscription.disabled)
+ subscription.disabled = false;
+ },
+
+ /**
+ * Adds a user-defined filter to the list
+ * @param {Filter} filter
+ * @param {SpecialSubscription} [subscription] particular group that the filter should be added to
+ * @param {Integer} [position] position within the subscription at which the filter should be added
+ * @param {Boolean} silent if true, no listeners will be triggered (to be used when filter list is reloaded)
+ */
+ addFilter: function(filter, subscription, position, silent)
+ {
+ if (!subscription)
+ {
+ if (filter.subscriptions.some(function(s) s instanceof SpecialSubscription && !s.disabled))
+ return; // No need to add
+ subscription = FilterStorage.getGroupForFilter(filter);
+ }
+ if (!subscription)
+ {
+ // No group for this filter exists, create one
+ subscription = SpecialSubscription.createForFilter(filter);
+ this.addSubscription(subscription);
+ return;
+ }
+
+ if (typeof position == "undefined")
+ position = subscription.filters.length;
+
+ if (filter.subscriptions.indexOf(subscription) < 0)
+ filter.subscriptions.push(subscription);
+ subscription.filters.splice(position, 0, filter);
+ if (!silent)
+ FilterNotifier.triggerListeners("filter.added", filter, subscription, position);
+ },
+
+ /**
+ * Removes a user-defined filter from the list
+ * @param {Filter} filter
+ * @param {SpecialSubscription} [subscription] a particular filter group that
+ * the filter should be removed from (if ommited will be removed from all subscriptions)
+ * @param {Integer} [position] position inside the filter group at which the
+ * filter should be removed (if ommited all instances will be removed)
+ */
+ removeFilter: function(filter, subscription, position)
+ {
+ let subscriptions = (subscription ? [subscription] : filter.subscriptions.slice());
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+ if (subscription instanceof SpecialSubscription)
+ {
+ let positions = [];
+ if (typeof position == "undefined")
+ {
+ let index = -1;
+ do
+ {
+ index = subscription.filters.indexOf(filter, index + 1);
+ if (index >= 0)
+ positions.push(index);
+ } while (index >= 0);
+ }
+ else
+ positions.push(position);
+
+ for (let j = positions.length - 1; j >= 0; j--)
+ {
+ let position = positions[j];
+ if (subscription.filters[position] == filter)
+ {
+ subscription.filters.splice(position, 1);
+ if (subscription.filters.indexOf(filter) < 0)
+ {
+ let index = filter.subscriptions.indexOf(subscription);
+ if (index >= 0)
+ filter.subscriptions.splice(index, 1);
+ }
+ FilterNotifier.triggerListeners("filter.removed", filter, subscription, position);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Moves a user-defined filter to a new position
+ * @param {Filter} filter
+ * @param {SpecialSubscription} subscription filter group where the filter is located
+ * @param {Integer} oldPosition current position of the filter
+ * @param {Integer} newPosition new position of the filter
+ */
+ moveFilter: function(filter, subscription, oldPosition, newPosition)
+ {
+ if (!(subscription instanceof SpecialSubscription) || subscription.filters[oldPosition] != filter)
+ return;
+
+ newPosition = Math.min(Math.max(newPosition, 0), subscription.filters.length - 1);
+ if (oldPosition == newPosition)
+ return;
+
+ subscription.filters.splice(oldPosition, 1);
+ subscription.filters.splice(newPosition, 0, filter);
+ FilterNotifier.triggerListeners("filter.moved", filter, subscription, oldPosition, newPosition);
+ },
+
+ /**
+ * Increases the hit count for a filter by one
+ * @param {Filter} filter
+ */
+ increaseHitCount: function(filter)
+ {
+ if (!Prefs.savestats || Prefs.privateBrowsing || !(filter instanceof ActiveFilter))
+ return;
+
+ filter.hitCount++;
+ filter.lastHit = Date.now();
+ },
+
+ /**
+ * Resets hit count for some filters
+ * @param {Array of Filter} filters filters to be reset, if null all filters will be reset
+ */
+ resetHitCounts: function(filters)
+ {
+ if (!filters)
+ {
+ filters = [];
+ for each (let filter in Filter.knownFilters)
+ filters.push(filter);
+ }
+ for each (let filter in filters)
+ {
+ filter.hitCount = 0;
+ filter.lastHit = 0;
+ }
+ },
+
+ _loading: false,
+
+ /**
+ * Loads all subscriptions from the disk
+ * @param {nsIFile} [sourceFile] File to read from
+ */
+ loadFromDisk: function(sourceFile)
+ {
+ if (this._loading)
+ return;
+
+ TimeLine.enter("Entered FilterStorage.loadFromDisk()");
+
+ let explicitFile = true;
+ if (!sourceFile)
+ {
+ sourceFile = FilterStorage.sourceFile;
+ explicitFile = false;
+
+ if (!sourceFile || !sourceFile.exists())
+ sourceFile = Utils.makeURI("resource://@ADDON_CHROME_NAME@/defaults/patterns.ini");
+ }
+
+ let readFile = function(sourceFile, backupIndex)
+ {
+ TimeLine.enter("FilterStorage.loadFromDisk() -> readFile()");
+
+ let parser = new INIParser();
+ IO.readFromFile(sourceFile, true, parser, function(e)
+ {
+ TimeLine.enter("FilterStorage.loadFromDisk() read callback");
+ if (!e && parser.subscriptions.length == 0)
+ {
+ // No filter subscriptions in the file, this isn't right.
+ e = new Error("No data in the file");
+ }
+
+ if (e)
+ Cu.reportError(e);
+
+ if (e && !explicitFile)
+ {
+ // Attempt to load a backup
+ sourceFile = this.sourceFile;
+ if (sourceFile)
+ {
+ let [, part1, part2] = /^(.*)(\.\w+)$/.exec(sourceFile.leafName) || [null, sourceFile.leafName, ""];
+
+ sourceFile = sourceFile.clone();
+ sourceFile.leafName = part1 + "-backup" + (++backupIndex) + part2;
+
+ if (sourceFile.exists())
+ {
+ readFile(sourceFile, backupIndex);
+ TimeLine.leave("FilterStorage.loadFromDisk() read callback done");
+ return;
+ }
+ }
+ }
+
+ // Old special groups might have been converted, remove them if they are empty
+ let specialMap = {"~il~": true, "~wl~": true, "~fl~": true, "~eh~": true};
+ let knownSubscriptions = {__proto__: null};
+ for (let i = 0; i < parser.subscriptions.length; i++)
+ {
+ let subscription = parser.subscriptions[i];
+ if (subscription instanceof SpecialSubscription && subscription.filters.length == 0 && subscription.url in specialMap)
+ parser.subscriptions.splice(i--, 1);
+ else
+ knownSubscriptions[subscription.url] = subscription;
+ }
+
+ this.fileProperties = parser.fileProperties;
+ this.subscriptions = parser.subscriptions;
+ this.knownSubscriptions = knownSubscriptions;
+ Filter.knownFilters = parser.knownFilters;
+ Subscription.knownSubscriptions = parser.knownSubscriptions;
+
+ if (parser.userFilters)
+ {
+ for (let i = 0; i < parser.userFilters.length; i++)
+ {
+ let filter = Filter.fromText(parser.userFilters[i]);
+ if (filter)
+ this.addFilter(filter, null, undefined, true);
+ }
+ }
+ TimeLine.log("Initializing data done, triggering observers")
+
+ this._loading = false;
+ FilterNotifier.triggerListeners("load");
+
+ if (sourceFile != this.sourceFile)
+ this.saveToDisk();
+
+ TimeLine.leave("FilterStorage.loadFromDisk() read callback done");
+ }.bind(this), "FilterStorageRead");
+
+ TimeLine.leave("FilterStorage.loadFromDisk() <- readFile()", "FilterStorageRead");
+ }.bind(this);
+
+ this._loading = true;
+ readFile(sourceFile, 0);
+
+ TimeLine.leave("FilterStorage.loadFromDisk() done");
+ },
+
+ _generateFilterData: function(subscriptions)
+ {
+ yield "# Adblock Plus preferences";
+ yield "version=" + formatVersion;
+
+ let saved = {__proto__: null};
+ let buf = [];
+
+ // Save filter data
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+ for (let j = 0; j < subscription.filters.length; j++)
+ {
+ let filter = subscription.filters[j];
+ if (!(filter.text in saved))
+ {
+ filter.serialize(buf);
+ saved[filter.text] = filter;
+ for (let k = 0; k < buf.length; k++)
+ yield buf[k];
+ buf.splice(0);
+ }
+ }
+ }
+
+ // Save subscriptions
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+
+ yield "";
+
+ subscription.serialize(buf);
+ if (subscription.filters.length)
+ {
+ buf.push("", "[Subscription filters]")
+ subscription.serializeFilters(buf);
+ }
+ for (let k = 0; k < buf.length; k++)
+ yield buf[k];
+ buf.splice(0);
+ }
+ },
+
+ /**
+ * Will be set to true if saveToDisk() is running (reentrance protection).
+ * @type Boolean
+ */
+ _saving: false,
+
+ /**
+ * Will be set to true if a saveToDisk() call arrives while saveToDisk() is
+ * already running (delayed execution).
+ * @type Boolean
+ */
+ _needsSave: false,
+
+ /**
+ * Saves all subscriptions back to disk
+ * @param {nsIFile} [targetFile] File to be written
+ */
+ saveToDisk: function(targetFile)
+ {
+ let explicitFile = true;
+ if (!targetFile)
+ {
+ targetFile = FilterStorage.sourceFile;
+ explicitFile = false;
+ }
+ if (!targetFile)
+ return;
+
+ if (!explicitFile && this._saving)
+ {
+ this._needsSave = true;
+ return;
+ }
+
+ TimeLine.enter("Entered FilterStorage.saveToDisk()");
+
+ try {
+ targetFile.normalize();
+ } catch (e) {}
+
+ // Make sure the file's parent directory exists
+ try {
+ targetFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+ } catch (e) {}
+
+ let backupFileParts = null;
+ if (!explicitFile && targetFile.exists() && Prefs.patternsbackups > 0)
+ {
+ // Check whether we need to backup the file
+ let [, part1, part2] = /^(.*)(\.\w+)$/.exec(targetFile.leafName) || [null, targetFile.leafName, ""];
+
+ let newestBackup = targetFile.clone();
+ newestBackup.leafName = part1 + "-backup1" + part2;
+ if (!newestBackup.exists() || (Date.now() - newestBackup.lastModifiedTime) / 3600000 >= Prefs.patternsbackupinterval)
+ backupFileParts = [part1 + "-backup", part2];
+ }
+
+ let writeFilters = function()
+ {
+ TimeLine.enter("FilterStorage.saveToDisk() -> writeFilters()");
+ IO.writeToFile(targetFile, true, this._generateFilterData(subscriptions), function(e)
+ {
+ TimeLine.enter("FilterStorage.saveToDisk() write callback");
+ if (!explicitFile)
+ this._saving = false;
+
+ if (e)
+ reportError(e);
+
+ if (!explicitFile && this._needsSave)
+ {
+ this._needsSave = false;
+ this.saveToDisk();
+ }
+ else
+ FilterNotifier.triggerListeners("save");
+ TimeLine.leave("FilterStorage.saveToDisk() write callback done");
+ }.bind(this), "FilterStorageWrite");
+ TimeLine.leave("FilterStorage.saveToDisk() -> writeFilters()", "FilterStorageWrite");
+ }.bind(this);
+
+ let removeLastBackup = function()
+ {
+ TimeLine.enter("FilterStorage.saveToDisk() -> removeLastBackup()");
+ let file = targetFile.clone();
+ file.leafName = backupFileParts.join(Prefs.patternsbackups);
+ IO.removeFile(file, function(e) renameBackup(Prefs.patternsbackups - 1));
+ TimeLine.leave("FilterStorage.saveToDisk() <- removeLastBackup()");
+ }.bind(this);
+
+ let renameBackup = function(index)
+ {
+ TimeLine.enter("FilterStorage.saveToDisk() -> renameBackup()");
+ if (index > 0)
+ {
+ let fromFile = targetFile.clone();
+ fromFile.leafName = backupFileParts.join(index);
+
+ let toName = backupFileParts.join(index + 1);
+
+ IO.renameFile(fromFile, toName, function(e) renameBackup(index - 1));
+ }
+ else
+ {
+ let toFile = targetFile.clone();
+ toFile.leafName = backupFileParts.join(index + 1);
+
+ IO.copyFile(targetFile, toFile, writeFilters);
+ }
+ TimeLine.leave("FilterStorage.saveToDisk() <- renameBackup()");
+ }.bind(this);
+
+ // Do not persist external subscriptions
+ let subscriptions = this.subscriptions.filter(function(s) !(s instanceof ExternalSubscription));
+ if (!explicitFile)
+ this._saving = true;
+
+ if (backupFileParts)
+ removeLastBackup();
+ else
+ writeFilters();
+
+ TimeLine.leave("FilterStorage.saveToDisk() done");
+ },
+
+ /**
+ * Returns the list of existing backup files.
+ */
+ getBackupFiles: function() /**nsIFile[]*/
+ {
+ let result = [];
+
+ let [, part1, part2] = /^(.*)(\.\w+)$/.exec(FilterStorage.sourceFile.leafName) || [null, FilterStorage.sourceFile.leafName, ""];
+ for (let i = 1; ; i++)
+ {
+ let file = FilterStorage.sourceFile.clone();
+ file.leafName = part1 + "-backup" + i + part2;
+ if (file.exists())
+ result.push(file);
+ else
+ break;
+ }
+ return result;
+ }
+};
+
+/**
+ * Joins subscription's filters to the subscription without any notifications.
+ * @param {Subscription} subscription filter subscription that should be connected to its filters
+ */
+function addSubscriptionFilters(subscription)
+{
+ if (!(subscription.url in FilterStorage.knownSubscriptions))
+ return;
+
+ for each (let filter in subscription.filters)
+ filter.subscriptions.push(subscription);
+}
+
+/**
+ * Removes subscription's filters from the subscription without any notifications.
+ * @param {Subscription} subscription filter subscription to be removed
+ */
+function removeSubscriptionFilters(subscription)
+{
+ if (!(subscription.url in FilterStorage.knownSubscriptions))
+ return;
+
+ for each (let filter in subscription.filters)
+ {
+ let i = filter.subscriptions.indexOf(subscription);
+ if (i >= 0)
+ filter.subscriptions.splice(i, 1);
+ }
+}
+
+/**
+ * IO.readFromFile() listener to parse filter data.
+ */
+function INIParser()
+{
+ this.fileProperties = this.curObj = {};
+ this.subscriptions = [];
+ this.knownFilters = {__proto__: null};
+ this.knownSubscriptions = {__proto__: null};
+}
+INIParser.prototype =
+{
+ subscriptions: null,
+ knownFilters: null,
+ knownSubscrptions : null,
+ wantObj: true,
+ fileProperties: null,
+ curObj: null,
+ curSection: null,
+ userFilters: null,
+
+ process: function(val)
+ {
+ let origKnownFilters = Filter.knownFilters;
+ Filter.knownFilters = this.knownFilters;
+ let origKnownSubscriptions = Subscription.knownSubscriptions;
+ Subscription.knownSubscriptions = this.knownSubscriptions;
+ let match;
+ try
+ {
+ if (this.wantObj === true && (match = /^(\w+)=(.*)$/.exec(val)))
+ this.curObj[match[1]] = match[2];
+ else if (val === null || (match = /^\s*\[(.+)\]\s*$/.exec(val)))
+ {
+ if (this.curObj)
+ {
+ // Process current object before going to next section
+ switch (this.curSection)
+ {
+ case "filter":
+ case "pattern":
+ if ("text" in this.curObj)
+ Filter.fromObject(this.curObj);
+ break;
+ case "subscription":
+ let subscription = Subscription.fromObject(this.curObj);
+ if (subscription)
+ this.subscriptions.push(subscription);
+ break;
+ case "subscription filters":
+ case "subscription patterns":
+ if (this.subscriptions.length)
+ {
+ let subscription = this.subscriptions[this.subscriptions.length - 1];
+ for each (let text in this.curObj)
+ {
+ let filter = Filter.fromText(text);
+ if (filter)
+ {
+ subscription.filters.push(filter);
+ filter.subscriptions.push(subscription);
+ }
+ }
+ }
+ break;
+ case "user patterns":
+ this.userFilters = this.curObj;
+ break;
+ }
+ }
+
+ if (val === null)
+ return;
+
+ this.curSection = match[1].toLowerCase();
+ switch (this.curSection)
+ {
+ case "filter":
+ case "pattern":
+ case "subscription":
+ this.wantObj = true;
+ this.curObj = {};
+ break;
+ case "subscription filters":
+ case "subscription patterns":
+ case "user patterns":
+ this.wantObj = false;
+ this.curObj = [];
+ break;
+ default:
+ this.wantObj = undefined;
+ this.curObj = null;
+ }
+ }
+ else if (this.wantObj === false && val)
+ this.curObj.push(val.replace(/\\\[/g, "["));
+ }
+ finally
+ {
+ Filter.knownFilters = origKnownFilters;
+ Subscription.knownSubscriptions = origKnownSubscriptions;
+ }
+ }
+};
diff --git a/abprime/modules/IO.jsm b/abprime/modules/IO.jsm
new file mode 100644
index 00000000..92c8a098
--- /dev/null
+++ b/abprime/modules/IO.jsm
@@ -0,0 +1,317 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Module containing file I/O helpers.
+ */
+
+var EXPORTED_SYMBOLS = ["IO"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "TimeLine.jsm");
+
+var IO =
+{
+ /**
+ * Retrieves the platform-dependent line break string.
+ */
+ get lineBreak()
+ {
+ let lineBreak = (Services.appinfo.OS == "WINNT" ? "\r\n" : "\n");
+ delete IO.lineBreak;
+ IO.__defineGetter__("lineBreak", function() lineBreak);
+ return IO.lineBreak;
+ },
+
+ /**
+ * Tries to interpret a file path as an absolute path or a path relative to
+ * user's profile. Returns a file or null on failure.
+ */
+ resolveFilePath: function(/**String*/ path) /**nsIFile*/
+ {
+ if (!path)
+ return null;
+
+ try {
+ // Assume an absolute path first
+ return new FileUtils.File(path);
+ } catch (e) {}
+
+ try {
+ // Try relative path now
+ return FileUtils.getFile("ProfD", path.split("/"));
+ } catch (e) {}
+
+ return null;
+ },
+
+ /**
+ * Reads strings from a file asynchronously, calls listener.process() with
+ * each line read and with a null parameter once the read operation is done.
+ * The callback will be called when the operation is done.
+ */
+ readFromFile: function(/**nsIFile|nsIURI*/ file, /**Boolean*/ decode, /**Object*/ listener, /**Function*/ callback, /**String*/ timeLineID)
+ {
+ try
+ {
+ let uri = file instanceof Ci.nsIFile ? Services.io.newFileURI(file) : file;
+ let channel = Services.io.newChannelFromURI(uri);
+ let converter = null;
+ if (decode)
+ {
+ converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "utf-8";
+ }
+
+ channel.asyncOpen({
+ buffer: "",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver, Ci.nsIStreamListener]),
+ onStartRequest: function(request, context) {},
+ onDataAvailable: function(request, context, stream, offset, count)
+ {
+ if (timeLineID)
+ {
+ TimeLine.asyncStart(timeLineID);
+ }
+
+ let data = this.buffer + NetUtil.readInputStreamToString(stream, count);
+ let index = Math.max(data.lastIndexOf("\n"), data.lastIndexOf("\r"));
+ if (index >= 0)
+ {
+ this.buffer = data.substr(index + 1);
+ data = data.substr(0, index + 1);
+ if (converter)
+ data = converter.ConvertToUnicode(data);
+
+ let lines = data.split(/[\r\n]+/);
+ lines.pop();
+ for (let i = 0; i < lines.length; i++)
+ listener.process(lines[i]);
+ }
+ else
+ this.buffer = data;
+
+ if (timeLineID)
+ {
+ TimeLine.asyncEnd(timeLineID);
+ }
+ },
+ onStopRequest: function(request, context, result)
+ {
+ if (timeLineID)
+ {
+ TimeLine.asyncStart(timeLineID);
+ }
+
+ if (Components.isSuccessCode(result) && this.buffer.length)
+ listener.process(this.buffer);
+ listener.process(null);
+
+ if (timeLineID)
+ {
+ TimeLine.asyncEnd(timeLineID);
+ TimeLine.asyncDone(timeLineID);
+ }
+
+ if (!Components.isSuccessCode(result))
+ {
+ let e = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
+ e.initialize("File read operation failed", result, null, Components.stack, file, null);
+ callback(e);
+ }
+ else
+ callback(null);
+ }
+ }, null);
+ }
+ catch (e)
+ {
+ callback(e);
+ }
+ },
+
+ /**
+ * Writes string data to a file asynchronously, optionally encodes it into
+ * UTF-8 first. The callback will be called when the write operation is done.
+ */
+ writeToFile: function(/**nsIFile*/ file, /**Boolean*/ encode, /**Iterator*/ data, /**Function*/ callback, /**String*/ timeLineID)
+ {
+ try
+ {
+ let fileStream = FileUtils.openSafeFileOutputStream(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
+
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0x8000, null);
+
+ let outStream = pipe.outputStream;
+ if (encode)
+ {
+ outStream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
+ outStream.init(pipe.outputStream, "UTF-8", 0, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+ }
+
+ let copier = Cc["@mozilla.org/network/async-stream-copier;1"].createInstance(Ci.nsIAsyncStreamCopier);
+ copier.init(pipe.inputStream, fileStream, null, true, false, 0x8000, true, true);
+ copier.asyncCopy({
+ onStartRequest: function(request, context) {},
+ onStopRequest: function(request, context, result)
+ {
+ if (timeLineID)
+ {
+ TimeLine.asyncDone(timeLineID);
+ }
+
+ if (!Components.isSuccessCode(result))
+ {
+ let e = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
+ e.initialize("File write operation failed", result, null, Components.stack, file, null);
+ callback(e);
+ }
+ else
+ callback(null);
+ }
+ }, null);
+
+ let lineBreak = this.lineBreak;
+ function writeNextChunk()
+ {
+ let buf = [];
+ bufLen = 0;
+ while (bufLen < 0x4000)
+ {
+ try
+ {
+ let str = data.next();
+ buf.push(str);
+ bufLen += str.length;
+ }
+ catch (e)
+ {
+ if (e instanceof StopIteration)
+ break;
+ else if (typeof e == "number")
+ pipe.outputStream.closeWithStatus(e);
+ else if (e instanceof Ci.nsIException)
+ pipe.outputStream.closeWithStatus(e.result);
+ else
+ {
+ Cu.reportError(e);
+ pipe.outputStream.closeWithStatus(Cr.NS_ERROR_FAILURE);
+ }
+ return;
+ }
+ }
+
+ pipe.outputStream.asyncWait({
+ onOutputStreamReady: function()
+ {
+ if (timeLineID)
+ {
+ TimeLine.asyncStart(timeLineID);
+ }
+
+ if (buf.length)
+ {
+ let str = buf.join(lineBreak) + lineBreak;
+ if (encode)
+ outStream.writeString(str);
+ else
+ outStream.write(str, str.length);
+ writeNextChunk();
+ }
+ else
+ outStream.close();
+
+ if (timeLineID)
+ {
+ TimeLine.asyncEnd(timeLineID);
+ }
+ }
+ }, 0, 0, Services.tm.currentThread);
+ }
+ writeNextChunk();
+ }
+ catch (e)
+ {
+ callback(e);
+ }
+ },
+
+ /**
+ * Copies a file asynchronously. The callback will be called when the copy
+ * operation is done.
+ */
+ copyFile: function(/**nsIFile*/ fromFile, /**nsIFile*/ toFile, /**Function*/ callback)
+ {
+ try
+ {
+ let inStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
+ inStream.init(fromFile, FileUtils.MODE_RDONLY, 0, Ci.nsIFile.DEFER_OPEN);
+
+ let outStream = FileUtils.openFileOutputStream(toFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
+
+ NetUtil.asyncCopy(inStream, outStream, function(result)
+ {
+ if (!Components.isSuccessCode(result))
+ {
+ let e = Cc["@mozilla.org/js/xpc/Exception;1"].createInstance(Ci.nsIXPCException);
+ e.initialize("File write operation failed", result, null, Components.stack, file, null);
+ callback(e);
+ }
+ else
+ callback(null);
+ });
+ }
+ catch (e)
+ {
+ callback(e);
+ }
+ },
+
+ /**
+ * Renames a file within the same directory, will call callback when done.
+ */
+ renameFile: function(/**nsIFile*/ fromFile, /**String*/ newName, /**Function*/ callback)
+ {
+ try
+ {
+ fromFile.moveTo(null, newName);
+ callback(null);
+ }
+ catch(e)
+ {
+ callback(e);
+ }
+ },
+
+ /**
+ * Removes a file, will call callback when done.
+ */
+ removeFile: function(/**nsIFile*/ file, /**Function*/ callback)
+ {
+ try
+ {
+ file.remove(false);
+ callback(null);
+ }
+ catch(e)
+ {
+ callback(e);
+ }
+ }
+}
diff --git a/abprime/modules/Matcher.jsm b/abprime/modules/Matcher.jsm
new file mode 100644
index 00000000..04f79d16
--- /dev/null
+++ b/abprime/modules/Matcher.jsm
@@ -0,0 +1,410 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Matcher class implementing matching addresses against a list of filters.
+ */
+
+var EXPORTED_SYMBOLS = ["Matcher", "CombinedMatcher", "defaultMatcher"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "FilterClasses.jsm");
+
+/**
+ * Blacklist/whitelist filter matching
+ * @constructor
+ */
+function Matcher()
+{
+ this.clear();
+}
+
+Matcher.prototype = {
+ /**
+ * Lookup table for filters by their associated keyword
+ * @type Object
+ */
+ filterByKeyword: null,
+
+ /**
+ * Lookup table for keywords by the filter text
+ * @type Object
+ */
+ keywordByFilter: null,
+
+ /**
+ * Removes all known filters
+ */
+ clear: function()
+ {
+ this.filterByKeyword = {__proto__: null};
+ this.keywordByFilter = {__proto__: null};
+ },
+
+ /**
+ * Adds a filter to the matcher
+ * @param {RegExpFilter} filter
+ */
+ add: function(filter)
+ {
+ if (filter.text in this.keywordByFilter)
+ return;
+
+ // Look for a suitable keyword
+ let keyword = this.findKeyword(filter);
+ let oldEntry = this.filterByKeyword[keyword];
+ if (typeof oldEntry == "undefined")
+ this.filterByKeyword[keyword] = filter;
+ else if (oldEntry.length == 1)
+ this.filterByKeyword[keyword] = [oldEntry, filter];
+ else
+ oldEntry.push(filter);
+ this.keywordByFilter[filter.text] = keyword;
+ },
+
+ /**
+ * Removes a filter from the matcher
+ * @param {RegExpFilter} filter
+ */
+ remove: function(filter)
+ {
+ if (!(filter.text in this.keywordByFilter))
+ return;
+
+ let keyword = this.keywordByFilter[filter.text];
+ let list = this.filterByKeyword[keyword];
+ if (list.length <= 1)
+ delete this.filterByKeyword[keyword];
+ else
+ {
+ let index = list.indexOf(filter);
+ if (index >= 0)
+ {
+ list.splice(index, 1);
+ if (list.length == 1)
+ this.filterByKeyword[keyword] = list[0];
+ }
+ }
+
+ delete this.keywordByFilter[filter.text];
+ },
+
+ /**
+ * Chooses a keyword to be associated with the filter
+ * @param {String} text text representation of the filter
+ * @return {String} keyword (might be empty string)
+ */
+ findKeyword: function(filter)
+ {
+ // For donottrack filters use "donottrack" as keyword if nothing else matches
+ let defaultResult = (filter.contentType & RegExpFilter.typeMap.DONOTTRACK ? "donottrack" : "");
+
+ let text = filter.text;
+ if (Filter.regexpRegExp.test(text))
+ return defaultResult;
+
+ // Remove options
+ let match = Filter.optionsRegExp.exec(text);
+ if (match)
+ text = match.input.substr(0, match.index);
+
+ // Remove whitelist marker
+ if (text.substr(0, 2) == "@@")
+ text = text.substr(2);
+
+ let candidates = text.toLowerCase().match(/[^a-z0-9%*][a-z0-9%]{3,}(?=[^a-z0-9%*])/g);
+ if (!candidates)
+ return defaultResult;
+
+ let hash = this.filterByKeyword;
+ let result = defaultResult;
+ let resultCount = 0xFFFFFF;
+ let resultLength = 0;
+ for (let i = 0, l = candidates.length; i < l; i++)
+ {
+ let candidate = candidates[i].substr(1);
+ let count = (candidate in hash ? hash[candidate].length : 0);
+ if (count < resultCount || (count == resultCount && candidate.length > resultLength))
+ {
+ result = candidate;
+ resultCount = count;
+ resultLength = candidate.length;
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Checks whether a particular filter is being matched against.
+ */
+ hasFilter: function(/**RegExpFilter*/ filter) /**Boolean*/
+ {
+ return (filter.text in this.keywordByFilter);
+ },
+
+ /**
+ * Returns the keyword used for a filter, null for unknown filters.
+ */
+ getKeywordForFilter: function(/**RegExpFilter*/ filter) /**String*/
+ {
+ if (filter.text in this.keywordByFilter)
+ return this.keywordByFilter[filter.text];
+ else
+ return null;
+ },
+
+ /**
+ * Checks whether the entries for a particular keyword match a URL
+ */
+ _checkEntryMatch: function(keyword, location, contentType, docDomain, thirdParty)
+ {
+ let list = this.filterByKeyword[keyword];
+ for (let i = 0; i < list.length; i++)
+ {
+ let filter = list[i];
+ if (filter.matches(location, contentType, docDomain, thirdParty))
+ return filter;
+ }
+ return null;
+ },
+
+ /**
+ * Tests whether the URL matches any of the known filters
+ * @param {String} location URL to be tested
+ * @param {String} contentType content type identifier of the URL
+ * @param {String} docDomain domain name of the document that loads the URL
+ * @param {Boolean} thirdParty should be true if the URL is a third-party request
+ * @return {RegExpFilter} matching filter or null
+ */
+ matchesAny: function(location, contentType, docDomain, thirdParty)
+ {
+ let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
+ if (candidates === null)
+ candidates = [];
+ if (contentType == "DONOTTRACK")
+ candidates.unshift("donottrack");
+ else
+ candidates.push("");
+ for (let i = 0, l = candidates.length; i < l; i++)
+ {
+ let substr = candidates[i];
+ if (substr in this.filterByKeyword)
+ {
+ let result = this._checkEntryMatch(substr, location, contentType, docDomain, thirdParty);
+ if (result)
+ return result;
+ }
+ }
+
+ return null;
+ }
+};
+
+/**
+ * Combines a matcher for blocking and exception rules, automatically sorts
+ * rules into two Matcher instances.
+ * @constructor
+ */
+function CombinedMatcher()
+{
+ this.blacklist = new Matcher();
+ this.whitelist = new Matcher();
+ this.resultCache = {__proto__: null};
+}
+
+/**
+ * Maximal number of matching cache entries to be kept
+ * @type Number
+ */
+CombinedMatcher.maxCacheEntries = 1000;
+
+CombinedMatcher.prototype =
+{
+ /**
+ * Matcher for blocking rules.
+ * @type Matcher
+ */
+ blacklist: null,
+
+ /**
+ * Matcher for exception rules.
+ * @type Matcher
+ */
+ whitelist: null,
+
+ /**
+ * Lookup table of previous matchesAny results
+ * @type Object
+ */
+ resultCache: null,
+
+ /**
+ * Number of entries in resultCache
+ * @type Number
+ */
+ cacheEntries: 0,
+
+ /**
+ * @see Matcher#clear
+ */
+ clear: function()
+ {
+ this.blacklist.clear();
+ this.whitelist.clear();
+ this.resultCache = {__proto__: null};
+ this.cacheEntries = 0;
+ },
+
+ /**
+ * @see Matcher#add
+ */
+ add: function(filter)
+ {
+ if (filter instanceof WhitelistFilter)
+ {
+ this.whitelist.add(filter);
+ }
+ else
+ this.blacklist.add(filter);
+
+ if (this.cacheEntries > 0)
+ {
+ this.resultCache = {__proto__: null};
+ this.cacheEntries = 0;
+ }
+ },
+
+ /**
+ * @see Matcher#remove
+ */
+ remove: function(filter)
+ {
+ if (filter instanceof WhitelistFilter)
+ {
+ this.whitelist.remove(filter);
+ }
+ else
+ this.blacklist.remove(filter);
+
+ if (this.cacheEntries > 0)
+ {
+ this.resultCache = {__proto__: null};
+ this.cacheEntries = 0;
+ }
+ },
+
+ /**
+ * @see Matcher#findKeyword
+ */
+ findKeyword: function(filter)
+ {
+ if (filter instanceof WhitelistFilter)
+ return this.whitelist.findKeyword(filter);
+ else
+ return this.blacklist.findKeyword(filter);
+ },
+
+ /**
+ * @see Matcher#hasFilter
+ */
+ hasFilter: function(filter)
+ {
+ if (filter instanceof WhitelistFilter)
+ return this.whitelist.hasFilter(filter);
+ else
+ return this.blacklist.hasFilter(filter);
+ },
+
+ /**
+ * @see Matcher#getKeywordForFilter
+ */
+ getKeywordForFilter: function(filter)
+ {
+ if (filter instanceof WhitelistFilter)
+ return this.whitelist.getKeywordForFilter(filter);
+ else
+ return this.blacklist.getKeywordForFilter(filter);
+ },
+
+ /**
+ * Checks whether a particular filter is slow
+ */
+ isSlowFilter: function(/**RegExpFilter*/ filter) /**Boolean*/
+ {
+ let matcher = (filter instanceof WhitelistFilter ? this.whitelist : this.blacklist);
+ if (matcher.hasFilter(filter))
+ return !matcher.getKeywordForFilter(filter);
+ else
+ return !matcher.findKeyword(filter);
+ },
+
+ /**
+ * Optimized filter matching testing both whitelist and blacklist matchers
+ * simultaneously. For parameters see Matcher.matchesAny().
+ * @see Matcher#matchesAny
+ */
+ matchesAnyInternal: function(location, contentType, docDomain, thirdParty)
+ {
+ let candidates = location.toLowerCase().match(/[a-z0-9%]{3,}/g);
+ if (candidates === null)
+ candidates = [];
+ if (contentType == "DONOTTRACK")
+ candidates.unshift("donottrack");
+ else
+ candidates.push("");
+
+ let blacklistHit = null;
+ for (let i = 0, l = candidates.length; i < l; i++)
+ {
+ let substr = candidates[i];
+ if (substr in this.whitelist.filterByKeyword)
+ {
+ let result = this.whitelist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty);
+ if (result)
+ return result;
+ }
+ if (substr in this.blacklist.filterByKeyword && blacklistHit === null)
+ blacklistHit = this.blacklist._checkEntryMatch(substr, location, contentType, docDomain, thirdParty);
+ }
+ return blacklistHit;
+ },
+
+ /**
+ * @see Matcher#matchesAny
+ */
+ matchesAny: function(location, contentType, docDomain, thirdParty)
+ {
+ let key = location + " " + contentType + " " + docDomain + " " + thirdParty;
+ if (key in this.resultCache)
+ return this.resultCache[key];
+
+ let result = this.matchesAnyInternal(location, contentType, docDomain, thirdParty);
+
+ if (this.cacheEntries >= CombinedMatcher.maxCacheEntries)
+ {
+ this.resultCache = {__proto__: null};
+ this.cacheEntries = 0;
+ }
+
+ this.resultCache[key] = result;
+ this.cacheEntries++;
+
+ return result;
+ },
+}
+
+/**
+ * Shared CombinedMatcher instance that should usually be used.
+ * @type CombinedMatcher
+ */
+var defaultMatcher = new CombinedMatcher();
diff --git a/abprime/modules/ObjectTabs.jsm b/abprime/modules/ObjectTabs.jsm
new file mode 100644
index 00000000..f38ab93e
--- /dev/null
+++ b/abprime/modules/ObjectTabs.jsm
@@ -0,0 +1,504 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * @fileOverview Code responsible for showing and hiding object tabs.
+ */
+
+#filter substitution
+
+var EXPORTED_SYMBOLS = ["objectMouseEventHander"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+Cu.import(baseURL + "RequestNotifier.jsm");
+
+// Run asynchronously to prevent cyclic module loads
+Utils.runAsync(function()
+{
+ Cu.import(baseURL + "ContentPolicy.jsm");
+});
+
+/**
+ * Class responsible for showing and hiding object tabs.
+ * @class
+ */
+var objTabs =
+{
+ /**
+ * Number of milliseconds to wait until hiding tab after the mouse moves away.
+ * @type Integer
+ */
+ HIDE_DELAY: 1000,
+
+ /**
+ * Flag used to trigger object tabs initialization first time object tabs are
+ * used.
+ * @type Boolean
+ */
+ initialized: false,
+
+ /**
+ * Will be set to true while initialization is in progress.
+ * @type Boolean
+ */
+ initializing: false,
+
+ /**
+ * Parameters for _showTab, to be called once initialization is complete.
+ */
+ delayedShowParams: null,
+
+ /**
+ * Randomly generated class to be used for visible object tabs on top of object.
+ * @type String
+ */
+ objTabClassVisibleTop: null,
+
+ /**
+ * Randomly generated class to be used for visible object tabs at the bottom of the object.
+ * @type String
+ */
+ objTabClassVisibleBottom: null,
+
+ /**
+ * Randomly generated class to be used for invisible object tabs.
+ * @type String
+ */
+ objTabClassHidden: null,
+
+ /**
+ * Document element the object tab is currently being displayed for.
+ * @type Element
+ */
+ currentElement: null,
+
+ /**
+ * Windows that the window event handler is currently registered for.
+ * @type Array of Window
+ */
+ windowListeners: null,
+
+ /**
+ * Panel element currently used as object tab.
+ * @type Element
+ */
+ objtabElement: null,
+
+ /**
+ * Time of previous position update.
+ * @type Integer
+ */
+ prevPositionUpdate: 0,
+
+ /**
+ * Timer used to update position of the object tab.
+ * @type nsITimer
+ */
+ positionTimer: null,
+
+ /**
+ * Timer used to delay hiding of the object tab.
+ * @type nsITimer
+ */
+ hideTimer: null,
+
+ /**
+ * Used when hideTimer is running, time when the tab should be hidden.
+ * @type Integer
+ */
+ hideTargetTime: 0,
+
+ /**
+ * Initializes object tabs (generates random classes and registers stylesheet).
+ */
+ _initCSS: function()
+ {
+ this.delayedShowParams = arguments;
+
+ if (!this.initializing)
+ {
+ this.initializing = true;
+
+ function processCSSData(data)
+ {
+ let rnd = [];
+ let offset = "a".charCodeAt(0);
+ for (let i = 0; i < 60; i++)
+ rnd.push(offset + Math.random() * 26);
+
+ this.objTabClassVisibleTop = String.fromCharCode.apply(String, rnd.slice(0, 20));
+ this.objTabClassVisibleBottom = String.fromCharCode.apply(String, rnd.slice(20, 40));
+ this.objTabClassHidden = String.fromCharCode.apply(String, rnd.slice(40, 60));
+
+ let url = Utils.makeURI("data:text/css," + encodeURIComponent(data.replace(/%%CLASSVISIBLETOP%%/g, this.objTabClassVisibleTop)
+ .replace(/%%CLASSVISIBLEBOTTOM%%/g, this.objTabClassVisibleBottom)
+ .replace(/%%CLASSHIDDEN%%/g, this.objTabClassHidden)));
+ Utils.styleService.loadAndRegisterSheet(url, Ci.nsIStyleSheetService.USER_SHEET);
+
+ this.initializing = false;
+ this.initialized = true;
+
+ if (this.delayedShowParams)
+ this._showTab.apply(this, this.delayedShowParams);
+ }
+
+ // Load CSS asynchronously
+ try {
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ request.open("GET", "chrome://@ADDON_CHROME_NAME@/content/objtabs.css");
+ request.overrideMimeType("text/plain");
+
+ let me = this;
+ request.addEventListener("load", function()
+ {
+ processCSSData.call(me, request.responseText);
+ }, false);
+ request.send(null);
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ this.initializing = false;
+ }
+ }
+ },
+
+ /**
+ * Called to show object tab for an element.
+ */
+ showTabFor: function(/**Element*/ element)
+ {
+ if (!Prefs.frameobjects)
+ return;
+
+ if (this.hideTimer)
+ {
+ this.hideTimer.cancel();
+ this.hideTimer = null;
+ }
+
+ if (this.objtabElement)
+ this.objtabElement.style.setProperty("opacity", "1", "important");
+
+ if (this.currentElement != element)
+ {
+ this._hideTab();
+
+ let data = RequestNotifier.getDataForNode(element, true, Policy.type.OBJECT);
+ if (data)
+ {
+ let hooks = this.getHooksForElement(element);
+ if (hooks)
+ {
+ if (this.initialized)
+ this._showTab(hooks, element, data[1]);
+ else
+ this._initCSS(hooks, element, data[1]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Looks up the chrome window containing an element and returns abp-hooks
+ * element for this window if any.
+ */
+ getHooksForElement: function(/**Element*/ element) /**Element*/
+ {
+ let doc = element.ownerDocument.defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .document;
+ let hooks = doc.getElementById("abp-hooks");
+ if (hooks && hooks.wrappedJSObject)
+ hooks = hooks.wrappedJSObject;
+ return hooks;
+ },
+
+ /**
+ * Called to hide object tab for an element (actual hiding happens delayed).
+ */
+ hideTabFor: function(/**Element*/ element)
+ {
+ if (element != this.currentElement || this.hideTimer)
+ return;
+
+ this.hideTargetTime = Date.now() + this.HIDE_DELAY;
+ this.hideTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.hideTimer.init(this, 40, Ci.nsITimer.TYPE_REPEATING_SLACK);
+ },
+
+ /**
+ * Makes the tab element visible.
+ */
+ _showTab: function(/**Element*/ hooks, /**Element*/ element, /**RequestEntry*/ data)
+ {
+ let doc = element.ownerDocument.defaultView.top.document;
+
+ this.objtabElement = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
+ this.objtabElement.textContent = hooks.getAttribute("objtabtext");
+ this.objtabElement.setAttribute("title", hooks.getAttribute("objtabtooltip"));
+ this.objtabElement.setAttribute("href", data.location);
+ this.objtabElement.setAttribute("class", this.objTabClassHidden);
+ this.objtabElement.style.setProperty("opacity", "1", "important");
+ this.objtabElement.nodeData = data;
+ this.objtabElement.hooks = hooks;
+
+ this.currentElement = element;
+
+ // Register paint listeners for the relevant windows
+ this.windowListeners = [];
+ let wnd = element.ownerDocument.defaultView;
+ while (wnd)
+ {
+ wnd.addEventListener("MozAfterPaint", objectWindowEventHandler, false);
+ this.windowListeners.push(wnd);
+ wnd = (wnd.parent != wnd ? wnd.parent : null);
+ }
+
+ // Register mouse listeners on the object tab
+ this.objtabElement.addEventListener("mouseover", objectTabEventHander, false);
+ this.objtabElement.addEventListener("mouseout", objectTabEventHander, false);
+ this.objtabElement.addEventListener("click", objectTabEventHander, true);
+
+ // Insert the tab into the document and adjust its position
+ doc.documentElement.appendChild(this.objtabElement);
+ if (!this.positionTimer)
+ {
+ this.positionTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.positionTimer.init(this, 200, Ci.nsITimer.TYPE_REPEATING_SLACK);
+ }
+ this._positionTab();
+ },
+
+ /**
+ * Hides the tab element.
+ */
+ _hideTab: function()
+ {
+ this.delayedShowParams = null;
+
+ if (this.objtabElement)
+ {
+ // Prevent recursive calls via popuphidden handler
+ let objtab = this.objtabElement;
+ this.objtabElement = null;
+ this.currentElement = null;
+
+ if (this.hideTimer)
+ {
+ this.hideTimer.cancel();
+ this.hideTimer = null;
+ }
+
+ if (this.positionTimer)
+ {
+ this.positionTimer.cancel();
+ this.positionTimer = null;
+ }
+
+ try {
+ objtab.parentNode.removeChild(objtab);
+ } catch (e) {}
+ objtab.removeEventListener("mouseover", objectTabEventHander, false);
+ objtab.removeEventListener("mouseout", objectTabEventHander, false);
+ objtab.nodeData = null;
+
+ for each (let wnd in this.windowListeners)
+ wnd.removeEventListener("MozAfterPaint", objectWindowEventHandler, false);
+ this.windowListeners = null;
+ }
+ },
+
+ /**
+ * Updates position of the tab element.
+ */
+ _positionTab: function()
+ {
+ // Test whether element is still in document
+ let elementDoc = null;
+ try
+ {
+ elementDoc = this.currentElement.ownerDocument;
+ } catch (e) {} // Ignore "can't access dead object" error
+ if (!elementDoc || !this.currentElement.offsetWidth || !this.currentElement.offsetHeight ||
+ !elementDoc.defaultView || !elementDoc.documentElement)
+ {
+ this._hideTab();
+ return;
+ }
+
+ let objRect = this._getElementPosition(this.currentElement);
+
+ let className = this.objTabClassVisibleTop;
+ let left = objRect.right - this.objtabElement.offsetWidth;
+ let top = objRect.top - this.objtabElement.offsetHeight;
+ if (top < 0)
+ {
+ top = objRect.bottom;
+ className = this.objTabClassVisibleBottom;
+ }
+
+ if (this.objtabElement.style.left != left + "px")
+ this.objtabElement.style.setProperty("left", left + "px", "important");
+ if (this.objtabElement.style.top != top + "px")
+ this.objtabElement.style.setProperty("top", top + "px", "important");
+
+ if (this.objtabElement.getAttribute("class") != className)
+ this.objtabElement.setAttribute("class", className);
+
+ this.prevPositionUpdate = Date.now();
+ },
+
+ /**
+ * Calculates element's position relative to the top frame and considering
+ * clipping due to scrolling.
+ * @return {left: Number, top: Number, right: Number, bottom: Number}
+ */
+ _getElementPosition: function(/**Element*/ element)
+ {
+ // Restrict rectangle coordinates by the boundaries of a window's client area
+ function intersectRect(rect, wnd)
+ {
+ // Cannot use wnd.innerWidth/Height because they won't account for scrollbars
+ let doc = wnd.document;
+ let wndWidth = doc.documentElement.clientWidth;
+ let wndHeight = doc.documentElement.clientHeight;
+ if (doc.compatMode == "BackCompat") // clientHeight will be bogus in quirks mode
+ wndHeight = Math.max(doc.documentElement.offsetHeight, doc.body.offsetHeight) - wnd.scrollMaxY - 1;
+
+ rect.left = Math.max(rect.left, 0);
+ rect.top = Math.max(rect.top, 0);
+ rect.right = Math.min(rect.right, wndWidth);
+ rect.bottom = Math.min(rect.bottom, wndHeight);
+ }
+
+ let rect = element.getBoundingClientRect();
+ let wnd = element.ownerDocument.defaultView;
+
+ let style = wnd.getComputedStyle(element, null);
+ let offsets = [
+ parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft),
+ parseFloat(style.borderTopWidth) + parseFloat(style.paddingTop),
+ parseFloat(style.borderRightWidth) + parseFloat(style.paddingRight),
+ parseFloat(style.borderBottomWidth) + parseFloat(style.paddingBottom)
+ ];
+
+ rect = {left: rect.left + offsets[0], top: rect.top + offsets[1],
+ right: rect.right - offsets[2], bottom: rect.bottom - offsets[3]};
+ while (true)
+ {
+ intersectRect(rect, wnd);
+
+ if (!wnd.frameElement)
+ break;
+
+ // Recalculate coordinates to be relative to frame's parent window
+ let frameElement = wnd.frameElement;
+ wnd = frameElement.ownerDocument.defaultView;
+
+ let frameRect = frameElement.getBoundingClientRect();
+ let frameStyle = wnd.getComputedStyle(frameElement, null);
+ let relLeft = frameRect.left + parseFloat(frameStyle.borderLeftWidth) + parseFloat(frameStyle.paddingLeft);
+ let relTop = frameRect.top + parseFloat(frameStyle.borderTopWidth) + parseFloat(frameStyle.paddingTop);
+
+ rect.left += relLeft;
+ rect.right += relLeft;
+ rect.top += relTop;
+ rect.bottom += relTop;
+ }
+
+ return rect;
+ },
+
+ doBlock: function()
+ {
+ Cu.import(baseURL + "AppIntegration.jsm");
+ let wrapper = AppIntegration.getWrapperForWindow(this.objtabElement.hooks.ownerDocument.defaultView);
+ if (wrapper)
+ wrapper.blockItem(this.currentElement, this.objtabElement.nodeData);
+ },
+
+ /**
+ * Called whenever a timer fires.
+ */
+ observe: function(/**nsISupport*/ subject, /**String*/ topic, /**String*/ data)
+ {
+ if (subject == this.positionTimer)
+ {
+ // Don't update position if it was already updated recently (via MozAfterPaint)
+ if (Date.now() - this.prevPositionUpdate > 100)
+ this._positionTab();
+ }
+ else if (subject == this.hideTimer)
+ {
+ let now = Date.now();
+ if (now >= this.hideTargetTime)
+ this._hideTab();
+ else if (this.hideTargetTime - now < this.HIDE_DELAY / 2)
+ this.objtabElement.style.setProperty("opacity", (this.hideTargetTime - now) * 2 / this.HIDE_DELAY, "important");
+ }
+ }
+};
+
+/**
+ * Function called whenever the mouse enters or leaves an object.
+ */
+function objectMouseEventHander(/**Event*/ event)
+{
+ if (!event.isTrusted)
+ return;
+
+ if (event.type == "mouseover")
+ objTabs.showTabFor(event.target);
+ else if (event.type == "mouseout")
+ objTabs.hideTabFor(event.target);
+}
+
+/**
+ * Function called for paint events of the object tab window.
+ */
+function objectWindowEventHandler(/**Event*/ event)
+{
+ if (!event.isTrusted)
+ return;
+
+ // Don't trigger update too often, avoid overusing CPU on frequent page updates
+ if (event.type == "MozAfterPaint" && Date.now() - objTabs.prevPositionUpdate > 20)
+ objTabs._positionTab();
+}
+
+/**
+ * Function called whenever the mouse enters or leaves an object tab.
+ */
+function objectTabEventHander(/**Event*/ event)
+{
+ if (!event.isTrusted)
+ return;
+
+ if (event.type == "click" && event.button == 0)
+ {
+ event.preventDefault();
+ event.stopPropagation();
+
+ objTabs.doBlock();
+ }
+ else if (event.type == "mouseover")
+ objTabs.showTabFor(objTabs.currentElement);
+ else if (event.type == "mouseout")
+ objTabs.hideTabFor(objTabs.currentElement);
+}
diff --git a/abprime/modules/Prefs.jsm b/abprime/modules/Prefs.jsm
new file mode 100644
index 00000000..70adc9a4
--- /dev/null
+++ b/abprime/modules/Prefs.jsm
@@ -0,0 +1,313 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Manages Adblock Plus preferences.
+ */
+
+var EXPORTED_SYMBOLS = ["Prefs"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+Cu.import(baseURL + "Utils.jsm");
+
+const prefRoot = "extensions.@ADDON_CHROME_NAME@.";
+
+/**
+ * Will be set to true if Adblock Plus is scheduled to be uninstalled on
+ * browser restart.
+ * @type Boolean
+ */
+let willBeUninstalled = false;
+
+/**
+ * Preferences branch containing Adblock Plus preferences.
+ * @type nsIPrefBranch
+ */
+let branch = Utils.prefService.getBranch(prefRoot);
+
+/**
+ * List of listeners to be notified whenever preferences are updated
+ * @type Array of Function
+ */
+let listeners = [];
+
+/**
+ * This object allows easy access to Adblock Plus preferences, all defined
+ * preferences will be available as its members.
+ * @class
+ */
+var Prefs =
+{
+ /**
+ * Will be set to true if the user enters private browsing mode.
+ * @type Boolean
+ */
+ privateBrowsing: false,
+
+ /**
+ * Called on module startup.
+ */
+ startup: function()
+ {
+ TimeLine.enter("Entered Prefs.startup()");
+
+ // Initialize prefs list
+ let defaultBranch = this.defaultBranch;
+ for each (let name in defaultBranch.getChildList("", {}))
+ {
+ let type = defaultBranch.getPrefType(name);
+ switch (type)
+ {
+ case Ci.nsIPrefBranch.PREF_INT:
+ defineIntegerProperty(name);
+ break;
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ defineBooleanProperty(name);
+ break;
+ case Ci.nsIPrefBranch.PREF_STRING:
+ defineStringProperty(name);
+ break;
+ }
+ if ("_update_" + name in PrefsPrivate)
+ PrefsPrivate["_update_" + name]();
+ }
+
+ // Always disable object tabs in Fennec, they aren't usable
+ if (Utils.isFennec)
+ Prefs.frameobjects = false;
+
+ TimeLine.log("done loading initial values");
+
+ // Register observers
+ TimeLine.log("registering observers");
+ registerObservers();
+
+ TimeLine.leave("Prefs.startup() done");
+ },
+
+ /**
+ * Backwards compatibility, this pref is optional
+ */
+ get patternsfile() /**String*/
+ {
+ let result = null;
+ try
+ {
+ result = branch.getCharPref("patternsfile");
+ } catch(e) {}
+ this.__defineGetter__("patternsfile", function() result);
+ return this.patternsfile;
+ },
+
+ /**
+ * Retrieves the preferences branch containing default preference values.
+ */
+ get defaultBranch() /**nsIPreferenceBranch*/
+ {
+ return Utils.prefService.getDefaultBranch(prefRoot);
+ },
+
+ /**
+ * Called on module shutdown.
+ */
+ shutdown: function()
+ {
+ TimeLine.enter("Entered Prefs.shutdown()");
+
+ if (willBeUninstalled)
+ {
+ // Make sure that a new installation after uninstall will be treated like
+ // an update.
+ try {
+ branch.clearUserPref("currentVersion");
+ } catch(e) {}
+ }
+
+ TimeLine.leave("Prefs.shutdown() done");
+ },
+
+ /**
+ * Adds a preferences listener that will be fired whenever preferences are
+ * reloaded
+ */
+ addListener: function(/**Function*/ listener)
+ {
+ let index = listeners.indexOf(listener);
+ if (index < 0)
+ listeners.push(listener);
+ },
+ /**
+ * Removes a preferences listener
+ */
+ removeListener: function(/**Function*/ listener)
+ {
+ let index = listeners.indexOf(listener);
+ if (index >= 0)
+ listeners.splice(index, 1);
+ }
+};
+
+/**
+ * Private nsIObserver implementation
+ * @class
+ */
+var PrefsPrivate =
+{
+ /**
+ * If set to true notifications about preference changes will no longer cause
+ * a reload. This is to prevent unnecessary reloads while saving.
+ * @type Boolean
+ */
+ ignorePrefChanges: false,
+
+ /**
+ * nsIObserver implementation
+ */
+ observe: function(subject, topic, data)
+ {
+ if (topic == "private-browsing")
+ {
+ if (data == "enter")
+ Prefs.privateBrowsing = true;
+ else if (data == "exit")
+ Prefs.privateBrowsing = false;
+ }
+ else if (topic == "em-action-requested")
+ {
+ if (subject instanceof Ci.nsIUpdateItem && subject.id == Utils.addonID)
+ willBeUninstalled = (data == "item-uninstalled");
+ }
+ else if (topic == "nsPref:changed" && !this.ignorePrefChanges && "_update_" + data in PrefsPrivate)
+ PrefsPrivate["_update_" + data]();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
+}
+
+/**
+ * Adds observers to keep various properties of Prefs object updated.
+ */
+function registerObservers()
+{
+ // Observe preferences changes
+ try {
+ branch.QueryInterface(Ci.nsIPrefBranchInternal)
+ .addObserver("", PrefsPrivate, true);
+ }
+ catch (e) {
+ Cu.reportError(e);
+ }
+
+ Services.obs.addObserver(PrefsPrivate, "em-action-requested", true);
+
+ // Add Private Browsing observer
+ if ("@mozilla.org/privatebrowsing;1" in Cc)
+ {
+ try
+ {
+ Prefs.privateBrowsing = Cc["@mozilla.org/privatebrowsing;1"].getService(Ci.nsIPrivateBrowsingService).privateBrowsingEnabled;
+ Services.obs.addObserver(PrefsPrivate, "private-browsing", true);
+ }
+ catch(e)
+ {
+ Cu.reportError(e);
+ }
+ }
+}
+
+/**
+ * Triggers preference listeners whenever a preference is changed.
+ */
+function triggerListeners(/**String*/ name)
+{
+ for each (let listener in listeners)
+ listener(name);
+}
+
+/**
+ * Sets up getter/setter on Prefs object for preference.
+ */
+function defineProperty(/**String*/ name, defaultValue, /**Function*/ readFunc, /**Function*/ writeFunc)
+{
+ let value = defaultValue;
+ PrefsPrivate["_update_" + name] = function()
+ {
+ try
+ {
+ value = readFunc();
+ triggerListeners(name);
+ }
+ catch(e)
+ {
+ Cu.reportError(e);
+ }
+ }
+ Prefs.__defineGetter__(name, function() value);
+ Prefs.__defineSetter__(name, function(newValue)
+ {
+ if (value == newValue)
+ return value;
+
+ try
+ {
+ PrefsPrivate.ignorePrefChanges = true;
+ writeFunc(newValue);
+ value = newValue;
+ triggerListeners(name);
+ }
+ catch(e)
+ {
+ Cu.reportError(e);
+ }
+ finally
+ {
+ PrefsPrivate.ignorePrefChanges = false;
+ }
+ return value;
+ });
+}
+
+/**
+ * Sets up getter/setter on Prefs object for an integer preference.
+ */
+function defineIntegerProperty(/**String*/ name)
+{
+ defineProperty(name, 0, function() branch.getIntPref(name),
+ function(newValue) branch.setIntPref(name, newValue));
+}
+
+/**
+ * Sets up getter/setter on Prefs object for a boolean preference.
+ */
+function defineBooleanProperty(/**String*/ name)
+{
+ defineProperty(name, false, function() branch.getBoolPref(name),
+ function(newValue) branch.setBoolPref(name, newValue));
+}
+
+/**
+ * Sets up getter/setter on Prefs object for a string preference.
+ */
+function defineStringProperty(/**String*/ name)
+{
+ defineProperty(name, "", function() branch.getComplexValue(name, Ci.nsISupportsString).data,
+ function(newValue)
+ {
+ let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ str.data = newValue;
+ branch.setComplexValue(name, Ci.nsISupportsString, str);
+ });
+}
diff --git a/abprime/modules/Public.jsm b/abprime/modules/Public.jsm
new file mode 100644
index 00000000..0ac9eebb
--- /dev/null
+++ b/abprime/modules/Public.jsm
@@ -0,0 +1,185 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Public Adblock Plus API.
+ */
+
+var EXPORTED_SYMBOLS = ["AdblockPlus"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+
+const externalPrefix = "~external~";
+
+/**
+ * Class implementing public Adblock Plus API
+ * @class
+ */
+var AdblockPlus =
+{
+ /**
+ * Returns current subscription count
+ * @type Integer
+ */
+ get subscriptionCount()
+ {
+ return FilterStorage.subscriptions.length;
+ },
+
+ /**
+ * Gets a subscription by its URL
+ */
+ getSubscription: function(/**String*/ id) /**IAdblockPlusSubscription*/
+ {
+ if (id in FilterStorage.knownSubscriptions)
+ return createSubscriptionWrapper(FilterStorage.knownSubscriptions[id]);
+
+ return null;
+ },
+
+ /**
+ * Gets a subscription by its position in the list
+ */
+ getSubscriptionAt: function(/**Integer*/ index) /**IAdblockPlusSubscription*/
+ {
+ if (index < 0 || index >= FilterStorage.subscriptions.length)
+ return null;
+
+ return createSubscriptionWrapper(FilterStorage.subscriptions[index]);
+ },
+
+ /**
+ * Updates an external subscription and creates it if necessary
+ */
+ updateExternalSubscription: function(/**String*/ id, /**String*/ title, /**Array of Filter*/ filters) /**String*/
+ {
+ if (id.substr(0, externalPrefix.length) != externalPrefix)
+ id = externalPrefix + id;
+ let subscription = Subscription.fromURL(id);
+ if (!subscription)
+ subscription = new ExternalSubscription(id, title);
+
+ subscription.lastDownload = parseInt(new Date().getTime() / 1000);
+
+ let newFilters = [];
+ for each (let filter in filters)
+ {
+ filter = Filter.fromText(Filter.normalize(filter));
+ if (filter)
+ newFilters.push(filter);
+ }
+
+ if (id in FilterStorage.knownSubscriptions)
+ FilterStorage.updateSubscriptionFilters(subscription, newFilters);
+ else
+ {
+ subscription.filters = newFilters;
+ FilterStorage.addSubscription(subscription);
+ }
+
+ return id;
+ },
+
+ /**
+ * Removes an external subscription by its identifier
+ */
+ removeExternalSubscription: function(/**String*/ id) /**Boolean*/
+ {
+ if (id.substr(0, externalPrefix.length) != externalPrefix)
+ id = externalPrefix + id;
+ if (!(id in FilterStorage.knownSubscriptions))
+ return false;
+
+ FilterStorage.removeSubscription(FilterStorage.knownSubscriptions[id]);
+ return true;
+ },
+
+ /**
+ * Adds user-defined filters to the list
+ */
+ addPatterns: function(/**Array of String*/ filters)
+ {
+ for each (let filter in filters)
+ {
+ filter = Filter.fromText(Filter.normalize(filter));
+ if (filter)
+ {
+ filter.disabled = false;
+ FilterStorage.addFilter(filter);
+ }
+ }
+ },
+
+ /**
+ * Removes user-defined filters from the list
+ */
+ removePatterns: function(/**Array of String*/ filters)
+ {
+ for each (let filter in filters)
+ {
+ filter = Filter.fromText(Filter.normalize(filter));
+ if (filter)
+ FilterStorage.removeFilter(filter);
+ }
+ },
+
+ /**
+ * Returns installed Adblock Plus version
+ */
+ getInstalledVersion: function() /**String*/
+ {
+ return Utils.addonVersion;
+ },
+
+ /**
+ * Returns source code revision this Adblock Plus build was created from (if available)
+ */
+ getInstalledBuild: function() /**String*/
+ {
+ return Utils.addonBuild;
+ },
+};
+
+/**
+ * Wraps a subscription into IAdblockPlusSubscription structure.
+ */
+function createSubscriptionWrapper(/**Subscription*/ subscription) /**IAdblockPlusSubscription*/
+{
+ if (!subscription)
+ return null;
+
+ return {
+ url: subscription.url,
+ special: subscription instanceof SpecialSubscription,
+ title: subscription.title,
+ autoDownload: true,
+ disabled: subscription.disabled,
+ external: subscription instanceof ExternalSubscription,
+ lastDownload: subscription instanceof RegularSubscription ? subscription.lastDownload : 0,
+ downloadStatus: subscription instanceof DownloadableSubscription ? subscription.downloadStatus : "synchronize_ok",
+ lastModified: subscription instanceof DownloadableSubscription ? subscription.lastModified : null,
+ expires: subscription instanceof DownloadableSubscription ? subscription.expires : 0,
+ getPatterns: function()
+ {
+ let result = subscription.filters.map(function(filter)
+ {
+ return filter.text;
+ });
+ return result;
+ }
+ };
+}
diff --git a/abprime/modules/RequestNotifier.jsm b/abprime/modules/RequestNotifier.jsm
new file mode 100644
index 00000000..a152d31c
--- /dev/null
+++ b/abprime/modules/RequestNotifier.jsm
@@ -0,0 +1,346 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Stores Adblock Plus data to be attached to a window.
+ */
+
+var EXPORTED_SYMBOLS = ["RequestNotifier"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Utils.runAsync(Cu.import, Cu, baseURL + "ContentPolicy.jsm"); // delay to avoid circular imports
+
+let nodeData = new WeakMap();
+let windowStats = new WeakMap();
+let windowSelection = new WeakMap();
+
+/**
+ * List of notifiers in use - these notifiers need to receive notifications on
+ * new requests.
+ * @type RequestNotifier[]
+ */
+let activeNotifiers = [];
+
+/**
+ * Creates a notifier object for a particular window. After creation the window
+ * will first be scanned for previously saved requests. Once that scan is
+ * complete only new requests for this window will be reported.
+ * @param {Window} wnd window to attach the notifier to
+ * @param {Function} listener listener to be called whenever a new request is found
+ * @param {Object} [listenerObj] "this" pointer to be used when calling the listener
+ */
+function RequestNotifier(wnd, listener, listenerObj)
+{
+ this.window = wnd;
+ this.listener = listener;
+ this.listenerObj = listenerObj || null;
+ activeNotifiers.push(this);
+ if (wnd)
+ this.startScan(wnd);
+ else
+ this.scanComplete = true;
+}
+RequestNotifier.prototype =
+{
+ /**
+ * The window this notifier is associated with.
+ * @type Window
+ */
+ window: null,
+
+ /**
+ * The listener to be called when a new request is found.
+ * @type Function
+ */
+ listener: null,
+
+ /**
+ * "this" pointer to be used when calling the listener.
+ * @type Object
+ */
+ listenerObj: null,
+
+ /**
+ * Will be set to true once the initial window scan is complete.
+ * @type Boolean
+ */
+ scanComplete: false,
+
+ /**
+ * Shuts down the notifier once it is no longer used. The listener
+ * will no longer be called after that.
+ */
+ shutdown: function()
+ {
+ delete this.window;
+ delete this.listener;
+ delete this.listenerObj;
+
+ for (let i = activeNotifiers.length - 1; i >= 0; i--)
+ if (activeNotifiers[i] == this)
+ activeNotifiers.splice(i, 1);
+ },
+
+ /**
+ * Notifies listener about a new request.
+ */
+ notifyListener: function(/**Window*/ wnd, /**Node*/ node, /**RequestEntry*/ entry)
+ {
+ this.listener.call(this.listenerObj, wnd, node, entry, this.scanComplete);
+ },
+
+ /**
+ * Number of currently posted scan events (will be 0 when the scan finishes
+ * running).
+ */
+ eventsPosted: 0,
+
+ /**
+ * Starts the initial scan of the window (will recurse into frames).
+ * @param {Window} wnd the window to be scanned
+ */
+ startScan: function(wnd)
+ {
+ let currentThread = Utils.threadManager.currentThread;
+
+ let doc = wnd.document;
+ let walker = doc.createTreeWalker(doc, Ci.nsIDOMNodeFilter.SHOW_ELEMENT, null, false);
+
+ let runnable =
+ {
+ notifier: null,
+
+ run: function()
+ {
+ if (!this.notifier.listener)
+ return;
+
+ let node = walker.currentNode;
+ let data = nodeData.get(node);
+ if (typeof data != "undefined")
+ for (let i = data.length - 1; i >= 0; i--)
+ this.notifier.notifyListener(wnd, node, data[i]);
+
+ if (walker.nextNode())
+ currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ else
+ {
+ // Done with the current window, start the scan for its frames
+ for (let i = 0; i < wnd.frames.length; i++)
+ this.notifier.startScan(wnd.frames[i]);
+
+ this.notifier.eventsPosted--;
+ if (!this.notifier.eventsPosted)
+ {
+ this.notifier.scanComplete = true;
+ this.notifier.notifyListener(wnd, null, null);
+ }
+
+ this.notifier = null;
+ }
+ }
+ };
+ runnable.notifier = this;
+
+ // Process each node in a separate event on current thread to allow other
+ // events to process
+ this.eventsPosted++;
+ currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ }
+};
+
+RequestNotifier.storeSelection = function(/**Window*/ wnd, /**String*/ selection)
+{
+ windowSelection.set(wnd, selection);
+};
+RequestNotifier.getSelection = function(/**Window*/ wnd) /**String*/
+{
+ if (windowSelection.has(wnd))
+ return windowSelection.get(wnd);
+ else
+ return null;
+};
+
+/**
+ * Attaches request data to a DOM node.
+ * @param {Node} node node to attach data to
+ * @param {Window} topWnd top-level window the node belongs to
+ * @param {Integer} contentType request type, one of the Policy.type.* constants
+ * @param {String} docDomain domain of the document that initiated the request
+ * @param {Boolean} thirdParty will be true if a third-party server has been requested
+ * @param {String} location the address that has been requested
+ * @param {Filter} filter filter applied to the request or null if none
+ */
+RequestNotifier.addNodeData = function(/**Node*/ node, /**Window*/ topWnd, /**Integer*/ contentType, /**String*/ docDomain, /**Boolean*/ thirdParty, /**String*/ location, /**Filter*/ filter)
+{
+ return new RequestEntry(node, topWnd, contentType, docDomain, thirdParty, location, filter);
+}
+
+/**
+ * Retrieves the statistics for a window.
+ * @result {Object} Object with the properties items, blocked, whitelisted, hidden, filters containing statistics for the window (might be null)
+ */
+RequestNotifier.getWindowStatistics = function(/**Window*/ wnd)
+{
+ if (windowStats.has(wnd.document))
+ return windowStats.get(wnd.document);
+ else
+ return null;
+}
+
+/**
+ * Retrieves the request entry associated with a DOM node.
+ * @param {Node} node
+ * @param {Boolean} noParent if missing or false, the search will extend to the parent nodes until one is found that has data associated with it
+ * @param {Integer} [type] request type to be looking for
+ * @param {String} [location] request location to be looking for
+ * @result {[Node, RequestEntry]}
+ * @static
+ */
+RequestNotifier.getDataForNode = function(node, noParent, type, location)
+{
+ while (node)
+ {
+ let data = nodeData.get(node);
+ if (typeof data != "undefined")
+ {
+ // Look for matching entry starting at the end of the list (most recent first)
+ for (let i = data.length - 1; i >= 0; i--)
+ {
+ let entry = data[i];
+ if ((typeof type == "undefined" || entry.type == type) &&
+ (typeof location == "undefined" || entry.location == location))
+ {
+ return [node, entry];
+ }
+ }
+ }
+
+ // If we don't have any match on this node then maybe its parent will do
+ if ((typeof noParent != "boolean" || !noParent) &&
+ node.parentNode instanceof Ci.nsIDOMElement)
+ {
+ node = node.parentNode;
+ }
+ else
+ {
+ node = null;
+ }
+ }
+
+ return null;
+};
+
+function RequestEntry(node, topWnd, contentType, docDomain, thirdParty, location, filter)
+{
+ this.type = contentType;
+ this.docDomain = docDomain;
+ this.thirdParty = thirdParty;
+ this.location = location;
+ this.filter = filter;
+
+ this.attachToNode(node);
+
+ // Update window statistics
+ if (!windowStats.has(topWnd.document))
+ {
+ windowStats.set(topWnd.document, {
+ items: 0,
+ hidden: 0,
+ blocked: 0,
+ whitelisted: 0,
+ filters: {}
+ });
+ }
+
+ let stats = windowStats.get(topWnd.document);
+ if (filter && filter instanceof ElemHideFilter)
+ stats.hidden++;
+ else
+ stats.items++;
+ if (filter)
+ {
+ if (filter instanceof BlockingFilter)
+ stats.blocked++;
+ else if (filter instanceof WhitelistFilter)
+ stats.whitelisted++;
+
+ if (filter.text in stats.filters)
+ stats.filters[filter.text]++;
+ else
+ stats.filters[filter.text] = 1;
+ }
+
+ // Notify listeners
+ for each (let notifier in activeNotifiers)
+ if (!notifier.window || notifier.window == topWnd)
+ notifier.notifyListener(topWnd, node, this);
+}
+RequestEntry.prototype =
+{
+ /**
+ * Content type of the request (one of the nsIContentPolicy constants)
+ * @type Integer
+ */
+ type: null,
+ /**
+ * Domain name of the requesting document
+ * @type String
+ */
+ docDomain: null,
+ /**
+ * True if the request goes to a different domain than the domain of the containing document
+ * @type Boolean
+ */
+ thirdParty: false,
+ /**
+ * Address being requested
+ * @type String
+ */
+ location: null,
+ /**
+ * Filter that was applied to this request (if any)
+ * @type Filter
+ */
+ filter: null,
+ /**
+ * String representation of the content type, e.g. "subdocument"
+ * @type String
+ */
+ get typeDescr() Policy.typeDescr[this.type],
+ /**
+ * User-visible localized representation of the content type, e.g. "frame"
+ * @type String
+ */
+ get localizedDescr() Policy.localizedDescr[this.type],
+
+ /**
+ * Attaches this request object to a DOM node.
+ */
+ attachToNode: function(/**Node*/ node)
+ {
+ let existingData = nodeData.get(node);
+ if (typeof existingData != "undefined")
+ {
+ // Add the new entry to the existing data
+ existingData.push(this);
+ }
+ else
+ {
+ // Associate the node with a new array
+ nodeData.set(node, [this]);
+ }
+ }
+};
diff --git a/abprime/modules/SubscriptionClasses.jsm b/abprime/modules/SubscriptionClasses.jsm
new file mode 100644
index 00000000..32aa7436
--- /dev/null
+++ b/abprime/modules/SubscriptionClasses.jsm
@@ -0,0 +1,560 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Definition of Subscription class and its subclasses.
+ */
+
+var EXPORTED_SYMBOLS = ["Subscription", "SpecialSubscription", "RegularSubscription", "ExternalSubscription", "DownloadableSubscription"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+
+/**
+ * Abstract base class for filter subscriptions
+ *
+ * @param {String} url download location of the subscription
+ * @param {String} [title] title of the filter subscription
+ * @constructor
+ */
+function Subscription(url, title)
+{
+ this.url = url;
+ this.filters = [];
+ this._title = title || Utils.getString("newGroup_title");
+ Subscription.knownSubscriptions[url] = this;
+}
+Subscription.prototype =
+{
+ /**
+ * Download location of the subscription
+ * @type String
+ */
+ url: null,
+
+ /**
+ * Filters contained in the filter subscription
+ * @type Array of Filter
+ */
+ filters: null,
+
+ _title: null,
+ _fixedTitle: false,
+ _disabled: false,
+
+ /**
+ * Title of the filter subscription
+ * @type String
+ */
+ get title() this._title,
+ set title(value)
+ {
+ if (value != this._title)
+ {
+ let oldValue = this._title;
+ this._title = value;
+ FilterNotifier.triggerListeners("subscription.title", this, value, oldValue);
+ }
+ return this._title;
+ },
+
+ /**
+ * Determines whether the title should be editable
+ * @type Boolean
+ */
+ get fixedTitle() this._fixedTitle,
+ set fixedTitle(value)
+ {
+ if (value != this._fixedTitle)
+ {
+ let oldValue = this._fixedTitle;
+ this._fixedTitle = value;
+ FilterNotifier.triggerListeners("subscription.fixedTitle", this, value, oldValue);
+ }
+ return this._fixedTitle;
+ },
+
+ /**
+ * Defines whether the filters in the subscription should be disabled
+ * @type Boolean
+ */
+ get disabled() this._disabled,
+ set disabled(value)
+ {
+ if (value != this._disabled)
+ {
+ let oldValue = this._disabled;
+ this._disabled = value;
+ FilterNotifier.triggerListeners("subscription.disabled", this, value, oldValue);
+ }
+ return this._disabled;
+ },
+
+ /**
+ * Serializes the filter to an array of strings for writing out on the disk.
+ * @param {Array of String} buffer buffer to push the serialization results into
+ */
+ serialize: function(buffer)
+ {
+ buffer.push("[Subscription]");
+ buffer.push("url=" + this.url);
+ buffer.push("title=" + this._title);
+ if (this._fixedTitle)
+ buffer.push("fixedTitle=true");
+ if (this._disabled)
+ buffer.push("disabled=true");
+ },
+
+ serializeFilters: function(buffer)
+ {
+ for each (let filter in this.filters)
+ buffer.push(filter.text.replace(/\[/g, "\\["));
+ },
+
+ toString: function()
+ {
+ let buffer = [];
+ this.serialize(buffer);
+ return buffer.join("\n");
+ }
+};
+
+/**
+ * Cache for known filter subscriptions, maps URL to subscription objects.
+ * @type Object
+ */
+Subscription.knownSubscriptions = {__proto__: null};
+
+/**
+ * Returns a subscription from its URL, creates a new one if necessary.
+ * @param {String} url URL of the subscription
+ * @return {Subscription} subscription or null if the subscription couldn't be created
+ */
+Subscription.fromURL = function(url)
+{
+ if (url in Subscription.knownSubscriptions)
+ return Subscription.knownSubscriptions[url];
+
+ try
+ {
+ // Test URL for validity
+ url = Utils.ioService.newURI(url, null, null).spec;
+ return new DownloadableSubscription(url, null);
+ }
+ catch (e)
+ {
+ return new SpecialSubscription(url);
+ }
+}
+
+/**
+ * Deserializes a subscription
+ *
+ * @param {Object} obj map of serialized properties and their values
+ * @return {Subscription} subscription or null if the subscription couldn't be created
+ */
+Subscription.fromObject = function(obj)
+{
+ let result;
+ try
+ {
+ obj.url = Utils.ioService.newURI(obj.url, null, null).spec;
+
+ // URL is valid - this is a downloadable subscription
+ result = new DownloadableSubscription(obj.url, obj.title);
+ if ("nextURL" in obj)
+ result.nextURL = obj.nextURL;
+ if ("downloadStatus" in obj)
+ result._downloadStatus = obj.downloadStatus;
+ if ("lastModified" in obj)
+ result.lastModified = obj.lastModified;
+ if ("lastSuccess" in obj)
+ result.lastSuccess = parseInt(obj.lastSuccess) || 0;
+ if ("lastCheck" in obj)
+ result._lastCheck = parseInt(obj.lastCheck) || 0;
+ if ("expires" in obj)
+ result.expires = parseInt(obj.expires) || 0;
+ if ("softExpiration" in obj)
+ result.softExpiration = parseInt(obj.softExpiration) || 0;
+ if ("errors" in obj)
+ result._errors = parseInt(obj.errors) || 0;
+ if ("requiredVersion" in obj)
+ {
+ result.requiredVersion = obj.requiredVersion;
+ if (Utils.versionComparator.compare(result.requiredVersion, "3.5.0") > 0)
+ result.upgradeRequired = true;
+ }
+ if ("alternativeLocations" in obj)
+ result.alternativeLocations = obj.alternativeLocations;
+ if ("homepage" in obj)
+ result._homepage = obj.homepage;
+ if ("lastDownload" in obj)
+ result._lastDownload = parseInt(obj.lastDownload) || 0;
+ }
+ catch (e)
+ {
+ // Invalid URL - custom filter group
+ if (!("title" in obj))
+ {
+ // Backwards compatibility - titles and filter types were originally
+ // determined by group identifier.
+ if (obj.url == "~wl~")
+ obj.defaults = "whitelist";
+ else if (obj.url == "~fl~")
+ obj.defaults = "blocking";
+ else if (obj.url == "~eh~")
+ obj.defaults = "elemhide";
+ if ("defaults" in obj)
+ obj.title = Utils.getString(obj.defaults + "Group_title");
+ }
+ result = new SpecialSubscription(obj.url, obj.title);
+ if ("defaults" in obj)
+ result.defaults = obj.defaults.split(" ");
+ }
+ if ("fixedTitle" in obj)
+ result._fixedTitle = (obj.fixedTitle == "true");
+ if ("disabled" in obj)
+ result._disabled = (obj.disabled == "true");
+
+ return result;
+}
+
+/**
+ * Class for special filter subscriptions (user's filters)
+ * @param {String} url see Subscription()
+ * @param {String} [title] see Subscription()
+ * @constructor
+ * @augments Subscription
+ */
+function SpecialSubscription(url, title)
+{
+ Subscription.call(this, url, title);
+}
+SpecialSubscription.prototype =
+{
+ __proto__: Subscription.prototype,
+
+ /**
+ * Filter types that should be added to this subscription by default
+ * (entries should correspond to keys in SpecialSubscription.defaultsMap).
+ * @type Array of String
+ */
+ defaults: null,
+
+ /**
+ * Tests whether a filter should be added to this group by default
+ * @param {Filter} filter filter to be tested
+ * @return {Boolean}
+ */
+ isDefaultFor: function(filter)
+ {
+ if (this.defaults && this.defaults.length)
+ {
+ for each (let type in this.defaults)
+ {
+ if (filter instanceof SpecialSubscription.defaultsMap[type])
+ return true;
+ if (!(filter instanceof ActiveFilter) && type == "blacklist")
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * See Subscription.serialize()
+ */
+ serialize: function(buffer)
+ {
+ Subscription.prototype.serialize.call(this, buffer);
+ if (this.defaults && this.defaults.length)
+ buffer.push("defaults=" + this.defaults.filter(function(type) type in SpecialSubscription.defaultsMap).join(" "));
+ if (this._lastDownload)
+ buffer.push("lastDownload=" + this._lastDownload);
+ }
+};
+
+SpecialSubscription.defaultsMap = {
+ __proto__: null,
+ "whitelist": WhitelistFilter,
+ "blocking": BlockingFilter,
+ "elemhide": ElemHideFilter
+};
+
+/**
+ * Creates a new user-defined filter group.
+ * @param {String} [title] title of the new filter group
+ * @result {SpecialSubscription}
+ */
+SpecialSubscription.create = function(title)
+{
+ let url;
+ do
+ {
+ url = "~user~" + Math.round(Math.random()*1000000);
+ } while (url in Subscription.knownSubscriptions);
+ return new SpecialSubscription(url, title)
+};
+
+/**
+ * Creates a new user-defined filter group and adds the given filter to it.
+ * This group will act as the default group for this filter type.
+ */
+SpecialSubscription.createForFilter = function(/**Filter*/ filter) /**SpecialSubscription*/
+{
+ let subscription = SpecialSubscription.create();
+ subscription.filters.push(filter);
+ for (let type in SpecialSubscription.defaultsMap)
+ {
+ if (filter instanceof SpecialSubscription.defaultsMap[type])
+ subscription.defaults = [type];
+ }
+ if (!subscription.defaults)
+ subscription.defaults = ["blocking"];
+ subscription.title = Utils.getString(subscription.defaults[0] + "Group_title");
+ return subscription;
+};
+
+/**
+ * Abstract base class for regular filter subscriptions (both internally and externally updated)
+ * @param {String} url see Subscription()
+ * @param {String} [title] see Subscription()
+ * @constructor
+ * @augments Subscription
+ */
+function RegularSubscription(url, title)
+{
+ Subscription.call(this, url, title || url);
+}
+RegularSubscription.prototype =
+{
+ __proto__: Subscription.prototype,
+
+ _homepage: null,
+ _lastDownload: 0,
+
+ /**
+ * Filter subscription homepage if known
+ * @type String
+ */
+ get homepage() this._homepage,
+ set homepage(value)
+ {
+ if (value != this._homepage)
+ {
+ let oldValue = this._homepage;
+ this._homepage = value;
+ FilterNotifier.triggerListeners("subscription.homepage", this, value, oldValue);
+ }
+ return this._homepage;
+ },
+
+ /**
+ * Time of the last subscription download (in seconds since the beginning of the epoch)
+ * @type Number
+ */
+ get lastDownload() this._lastDownload,
+ set lastDownload(value)
+ {
+ if (value != this._lastDownload)
+ {
+ let oldValue = this._lastDownload;
+ this._lastDownload = value;
+ FilterNotifier.triggerListeners("subscription.lastDownload", this, value, oldValue);
+ }
+ return this._lastDownload;
+ },
+
+ /**
+ * See Subscription.serialize()
+ */
+ serialize: function(buffer)
+ {
+ Subscription.prototype.serialize.call(this, buffer);
+ if (this._homepage)
+ buffer.push("homepage=" + this._homepage);
+ if (this._lastDownload)
+ buffer.push("lastDownload=" + this._lastDownload);
+ }
+};
+
+/**
+ * Class for filter subscriptions updated by externally (by other extension)
+ * @param {String} url see Subscription()
+ * @param {String} [title] see Subscription()
+ * @constructor
+ * @augments RegularSubscription
+ */
+function ExternalSubscription(url, title)
+{
+ RegularSubscription.call(this, url, title);
+}
+ExternalSubscription.prototype =
+{
+ __proto__: RegularSubscription.prototype,
+
+ /**
+ * See Subscription.serialize()
+ */
+ serialize: function(buffer)
+ {
+ throw new Error("Unexpected call, external subscriptions should not be serialized");
+ }
+};
+
+/**
+ * Class for filter subscriptions updated by externally (by other extension)
+ * @param {String} url see Subscription()
+ * @param {String} [title] see Subscription()
+ * @constructor
+ * @augments RegularSubscription
+ */
+function DownloadableSubscription(url, title)
+{
+ RegularSubscription.call(this, url, title);
+}
+DownloadableSubscription.prototype =
+{
+ __proto__: RegularSubscription.prototype,
+
+ _downloadStatus: null,
+ _lastCheck: 0,
+ _errors: 0,
+
+ /**
+ * Next URL the downloaded should be attempted from (in case of redirects)
+ * @type String
+ */
+ nextURL: null,
+
+ /**
+ * Status of the last download (ID of a string)
+ * @type String
+ */
+ get downloadStatus() this._downloadStatus,
+ set downloadStatus(value)
+ {
+ let oldValue = this._downloadStatus;
+ this._downloadStatus = value;
+ FilterNotifier.triggerListeners("subscription.downloadStatus", this, value, oldValue);
+ return this._downloadStatus;
+ },
+
+ /**
+ * Value of the Last-Modified header returned by the server on last download
+ * @type String
+ */
+ lastModified: null,
+
+ /**
+ * Time of the last successful download (in seconds since the beginning of the
+ * epoch).
+ */
+ lastSuccess: 0,
+
+ /**
+ * Time when the subscription was considered for an update last time (in seconds
+ * since the beginning of the epoch). This will be used to increase softExpiration
+ * if the user doesn't use Adblock Plus for some time.
+ * @type Number
+ */
+ get lastCheck() this._lastCheck,
+ set lastCheck(value)
+ {
+ if (value != this._lastCheck)
+ {
+ let oldValue = this._lastCheck;
+ this._lastCheck = value;
+ FilterNotifier.triggerListeners("subscription.lastCheck", this, value, oldValue);
+ }
+ return this._lastCheck;
+ },
+
+ /**
+ * Hard expiration time of the filter subscription (in seconds since the beginning of the epoch)
+ * @type Number
+ */
+ expires: 0,
+
+ /**
+ * Soft expiration time of the filter subscription (in seconds since the beginning of the epoch)
+ * @type Number
+ */
+ softExpiration: 0,
+
+ /**
+ * Number of download failures since last success
+ * @type Number
+ */
+ get errors() this._errors,
+ set errors(value)
+ {
+ if (value != this._errors)
+ {
+ let oldValue = this._errors;
+ this._errors = value;
+ FilterNotifier.triggerListeners("subscription.errors", this, value, oldValue);
+ }
+ return this._errors;
+ },
+
+ /**
+ * Minimal Adblock Plus version required for this subscription
+ * @type String
+ */
+ requiredVersion: null,
+
+ /**
+ * Should be true if requiredVersion is higher than current Adblock Plus version
+ * @type Boolean
+ */
+ upgradeRequired: false,
+
+ /**
+ * Value of the X-Alternative-Locations header: comma-separated list of URLs
+ * with their weighting factors, e.g.: http://foo.example.com/;q=0.5,http://bar.example.com/;q=2
+ * @type String
+ */
+ alternativeLocations: null,
+
+ /**
+ * See Subscription.serialize()
+ */
+ serialize: function(buffer)
+ {
+ RegularSubscription.prototype.serialize.call(this, buffer);
+ if (this.nextURL)
+ buffer.push("nextURL=" + this.nextURL);
+ if (this.downloadStatus)
+ buffer.push("downloadStatus=" + this.downloadStatus);
+ if (this.lastModified)
+ buffer.push("lastModified=" + this.lastModified);
+ if (this.lastSuccess)
+ buffer.push("lastSuccess=" + this.lastSuccess);
+ if (this.lastCheck)
+ buffer.push("lastCheck=" + this.lastCheck);
+ if (this.expires)
+ buffer.push("expires=" + this.expires);
+ if (this.softExpiration)
+ buffer.push("softExpiration=" + this.softExpiration);
+ if (this.errors)
+ buffer.push("errors=" + this.errors);
+ if (this.requiredVersion)
+ buffer.push("requiredVersion=" + this.requiredVersion);
+ if (this.alternativeLocations)
+ buffer.push("alternativeLocations=" + this.alternativeLocations);
+ }
+};
diff --git a/abprime/modules/Sync.jsm b/abprime/modules/Sync.jsm
new file mode 100644
index 00000000..7acf286f
--- /dev/null
+++ b/abprime/modules/Sync.jsm
@@ -0,0 +1,425 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Module containing a bunch of utility functions.
+ */
+
+var EXPORTED_SYMBOLS = ["Sync"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "Synchronizer.jsm");
+
+/**
+ * ID of the only record stored
+ * @type String
+ */
+const filtersRecordID = "6fad6286-8207-46b6-aa39-8e0ce0bd7c49";
+
+/**
+ * Weave tracker class (is set when Weave is initialized).
+ */
+var Tracker = null;
+
+var Sync =
+{
+ /**
+ * Will be set to true if/when Weave starts up.
+ * @type Boolean
+ */
+ initialized: false,
+
+ /**
+ * Whether Weave requested us to track changes.
+ * @type Boolean
+ */
+ trackingEnabled: false,
+
+ /**
+ * Called on module startup.
+ */
+ startup: function()
+ {
+ Services.obs.addObserver(SyncPrivate, "weave:service:ready", true);
+ Services.obs.addObserver(SyncPrivate, "weave:engine:start-tracking", true);
+ Services.obs.addObserver(SyncPrivate, "weave:engine:stop-tracking", true);
+ },
+
+ /**
+ * Returns Adblock Plus sync engine.
+ * @result Engine
+ */
+ getEngine: function()
+ {
+ if (this.initialized)
+ return Weave.Engines.get("adblockplus");
+ else
+ return null;
+ }
+};
+
+var SyncPrivate =
+{
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe: function(subject, topic, data)
+ {
+ switch (topic)
+ {
+ case "weave:service:ready":
+ Cu.import("resource://services-sync/main.js");
+
+ Tracker = Weave.SyncEngine.prototype._trackerObj;
+ ABPEngine.prototype.__proto__ = Weave.SyncEngine.prototype;
+ ABPStore.prototype.__proto__ = Weave.Store.prototype;
+ ABPTracker.prototype.__proto__ = Tracker.prototype;
+
+ Weave.Engines.register(ABPEngine);
+ Sync.initialized = true;
+ break;
+ case "weave:engine:start-tracking":
+ Sync.trackingEnabled = true;
+ if (trackerInstance)
+ trackerInstance.startTracking();
+ break;
+ case "weave:engine:stop-tracking":
+ Sync.trackingEnabled = false;
+ if (trackerInstance)
+ trackerInstance.stopTracking();
+ break;
+ }
+ }
+};
+
+function ABPEngine()
+{
+ Weave.SyncEngine.call(this, "AdblockPlus");
+}
+ABPEngine.prototype =
+{
+ _storeObj: ABPStore,
+ _trackerObj: ABPTracker,
+ version: 1,
+
+ _reconcile: function(item)
+ {
+ // Always process server data, we will do the merging ourselves
+ return true;
+ }
+};
+
+function ABPStore(name)
+{
+ Weave.Store.call(this, name);
+}
+ABPStore.prototype =
+{
+ getAllIDs: function()
+ {
+ let result = {}
+ result[filtersRecordID] = true;
+ return result;
+ },
+
+ changeItemID: function(oldId, newId)
+ {
+ // This should not be called, our engine doesn't implement _findDupe
+ throw Cr.NS_ERROR_UNEXPECTED;
+ },
+
+ itemExists: function(id)
+ {
+ // Only one id exists so far
+ return (id == filtersRecordID);
+ },
+
+ createRecord: function(id, collection)
+ {
+ let record = new ABPEngine.prototype._recordObj(collection, id);
+ if (id == filtersRecordID)
+ {
+ record.cleartext = {
+ id: id,
+ subscriptions: [],
+ };
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (subscription instanceof ExternalSubscription)
+ continue;
+
+ let subscriptionEntry =
+ {
+ url: subscription.url,
+ disabled: subscription.disabled
+ };
+ if (subscription instanceof SpecialSubscription)
+ {
+ subscriptionEntry.filters = [];
+ for each (let filter in subscription.filters)
+ {
+ let filterEntry = {text: filter.text};
+ if (filter instanceof ActiveFilter)
+ filterEntry.disabled = filter.disabled;
+ subscriptionEntry.filters.push(filterEntry);
+ }
+ }
+ else
+ subscriptionEntry.title = subscription.title;
+ record.cleartext.subscriptions.push(subscriptionEntry);
+ }
+
+ // Data sent, forget about local changes now
+ trackerInstance.clearPrivateChanges()
+ }
+ else
+ record.deleted = true;
+
+ return record;
+ },
+
+ create: function(record)
+ {
+ // This should not be called because our record list doesn't change but
+ // call update just in case.
+ this.update(record);
+ },
+
+ update: function(record)
+ {
+ if (record.id != filtersRecordID)
+ return;
+
+ this._log.trace("Merging in remote data");
+
+ let data = record.cleartext.subscriptions;
+
+ // First make sure we have the same subscriptions on both sides
+ let seenSubscription = {__proto__: null};
+ for each (let remoteSubscription in data)
+ {
+ seenSubscription[remoteSubscription.url] = true;
+ if (remoteSubscription.url in FilterStorage.knownSubscriptions)
+ {
+ let subscription = FilterStorage.knownSubscriptions[remoteSubscription.url];
+ if (!trackerInstance.didSubscriptionChange(remoteSubscription))
+ {
+ // Only change local subscription if there were no changes, otherwise dismiss remote changes
+ subscription.disabled = remoteSubscription.disabled;
+ if (subscription instanceof DownloadableSubscription)
+ subscription.title = remoteSubscription.title;
+ }
+ }
+ else if (!trackerInstance.didSubscriptionChange(remoteSubscription))
+ {
+ // Subscription was added remotely, add it locally as well
+ let subscription = Subscription.fromURL(remoteSubscription.url);
+ if (!subscription)
+ continue;
+
+ subscription.disabled = remoteSubscription.disabled;
+ if (subscription instanceof DownloadableSubscription)
+ {
+ subscription.title = remoteSubscription.title;
+ FilterStorage.addSubscription(subscription);
+ Synchronizer.execute(subscription);
+ }
+ }
+ }
+
+ for each (let subscription in FilterStorage.subscriptions.slice())
+ {
+ if (!(subscription.url in seenSubscription) && subscription instanceof DownloadableSubscription && !trackerInstance.didSubscriptionChange(subscription))
+ {
+ // Subscription was removed remotely, remove it locally as well
+ FilterStorage.removeSubscription(subscription);
+ }
+ }
+
+ // Now sync the custom filters
+ let seenFilter = {__proto__: null};
+ for each (let remoteSubscription in data)
+ {
+ if (!("filters" in remoteSubscription))
+ continue;
+
+ for each (let remoteFilter in remoteSubscription.filters)
+ {
+ seenFilter[remoteFilter.text] = true;
+
+ let filter = Filter.fromText(remoteFilter.text);
+ if (!filter || trackerInstance.didFilterChange(filter))
+ continue;
+
+ if (filter.subscriptions.some(function(subscription) subscription instanceof SpecialSubscription))
+ {
+ // Filter might have been changed remotely
+ if (filter instanceof ActiveFilter)
+ filter.disabled = remoteFilter.disabled;
+ }
+ else
+ {
+ // Filter was added remotely, add it locally as well
+ FilterStorage.addFilter(filter);
+ }
+ }
+ }
+
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (!(subscription instanceof SpecialSubscription))
+ continue;
+
+ for each (let filter in subscription.filters.slice())
+ {
+ if (!(filter.text in seenFilter) && !trackerInstance.didFilterChange(filter))
+ {
+ // Filter was removed remotely, remove it locally as well
+ FilterStorage.removeFilter(filter);
+ }
+ }
+ }
+
+ // Merge done, forget about local changes now
+ trackerInstance.clearPrivateChanges()
+ },
+
+ remove: function(record)
+ {
+ // Shouldn't be called but if it is - ignore
+ },
+
+ wipe: function()
+ {
+ this._log.trace("Got wipe command, removing all data");
+
+ for each (let subscription in FilterStorage.subscriptions.slice())
+ {
+ if (subscription instanceof DownloadableSubscription)
+ FilterStorage.removeSubscription(subscription);
+ else if (subscription instanceof SpecialSubscription)
+ {
+ for each (let filter in subscription.filters.slice())
+ FilterStorage.removeFilter(filter);
+ }
+ }
+
+ // Data wiped, forget about local changes now
+ trackerInstance.clearPrivateChanges()
+ }
+};
+
+/**
+ * Hack to allow store to use the tracker - store tracker pointer globally.
+ */
+let trackerInstance = null;
+
+function ABPTracker(name)
+{
+ Tracker.call(this, name);
+
+ this.privateTracker = new Tracker(name + ".private");
+ trackerInstance = this;
+
+ this.onChange = this._bindMethod(this.onChange);
+
+ if (Sync.trackingEnabled)
+ this.startTracking();
+}
+ABPTracker.prototype =
+{
+ privateTracker: null,
+
+ _bindMethod: function(method)
+ {
+ let me = this;
+ return function() method.apply(me, arguments);
+ },
+
+ startTracking: function()
+ {
+ FilterNotifier.addListener(this.onChange);
+ },
+
+ stopTracking: function()
+ {
+ FilterNotifier.removeListener(this.onChange);
+ },
+
+ clearPrivateChanges: function()
+ {
+ this.privateTracker.clearChangedIDs();
+ },
+
+ addPrivateChange: function(id)
+ {
+ // Ignore changes during syncing
+ if (this.ignoreAll)
+ return;
+
+ this.addChangedID(filtersRecordID);
+ this.privateTracker.addChangedID(id);
+ this.score += 10;
+ },
+
+ didSubscriptionChange: function(subscription)
+ {
+ return ("subscription " + subscription.url) in this.privateTracker.changedIDs;
+ },
+
+ didFilterChange: function(filter)
+ {
+ return ("filter " + filter.text) in this.privateTracker.changedIDs;
+ },
+
+ onChange: function(action, item)
+ {
+ switch (action)
+ {
+ case "subscription.updated":
+ if ("oldSubscription" in item)
+ {
+ // Subscription moved to a new address
+ this.addPrivateChange("subscription " + item.url);
+ this.addPrivateChange("subscription " + item.oldSubscription.url);
+ }
+ else if (item instanceof SpecialSubscription)
+ {
+ // User's filters changed via Preferences window
+ for each (let filter in item.filters)
+ this.addPrivateChange("filter " + filter.text);
+ for each (let filter in item.oldFilters)
+ this.addPrivateChange("filter " + filter.text);
+ }
+ break;
+ case "subscription.added":
+ case "subscription.removed":
+ case "subscription.disabled":
+ case "subscription.title":
+ this.addPrivateChange("subscription " + item.url);
+ break;
+ case "filter.added":
+ case "filter.removed":
+ case "filter.disabled":
+ this.addPrivateChange("filter " + item.text);
+ break;
+ }
+ }
+};
diff --git a/abprime/modules/Synchronizer.jsm b/abprime/modules/Synchronizer.jsm
new file mode 100644
index 00000000..74fb4318
--- /dev/null
+++ b/abprime/modules/Synchronizer.jsm
@@ -0,0 +1,594 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Manages synchronization of filter subscriptions.
+ */
+
+var EXPORTED_SYMBOLS = ["Synchronizer"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import(baseURL + "TimeLine.jsm");
+Cu.import(baseURL + "Utils.jsm");
+Cu.import(baseURL + "FilterStorage.jsm");
+Cu.import(baseURL + "FilterNotifier.jsm");
+Cu.import(baseURL + "FilterClasses.jsm");
+Cu.import(baseURL + "SubscriptionClasses.jsm");
+Cu.import(baseURL + "Prefs.jsm");
+
+const MILLISECONDS_IN_SECOND = 1000;
+const SECONDS_IN_MINUTE = 60;
+const SECONDS_IN_HOUR = 60 * SECONDS_IN_MINUTE;
+const SECONDS_IN_DAY = 24 * SECONDS_IN_HOUR;
+const INITIAL_DELAY = 6 * SECONDS_IN_MINUTE;
+const CHECK_INTERVAL = SECONDS_IN_HOUR;
+const MIN_EXPIRATION_INTERVAL = 1 * SECONDS_IN_DAY;
+const MAX_EXPIRATION_INTERVAL = 14 * SECONDS_IN_DAY;
+const MAX_ABSENSE_INTERVAL = 1 * SECONDS_IN_DAY;
+
+let timer = null;
+
+/**
+ * Map of subscriptions currently being downloaded, all currently downloaded
+ * URLs are keys of that map.
+ */
+let executing = {__proto__: null};
+
+/**
+ * This object is responsible for downloading filter subscriptions whenever
+ * necessary.
+ * @class
+ */
+var Synchronizer =
+{
+ /**
+ * Called on module startup.
+ */
+ startup: function()
+ {
+ TimeLine.enter("Entered Synchronizer.startup()");
+
+ let callback = function()
+ {
+ timer.delay = CHECK_INTERVAL * MILLISECONDS_IN_SECOND;
+ checkSubscriptions();
+ };
+
+ timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(callback, INITIAL_DELAY * MILLISECONDS_IN_SECOND, Ci.nsITimer.TYPE_REPEATING_SLACK);
+
+ TimeLine.leave("Synchronizer.startup() done");
+ },
+
+ /**
+ * Checks whether a subscription is currently being downloaded.
+ * @param {String} url URL of the subscription
+ * @return {Boolean}
+ */
+ isExecuting: function(url)
+ {
+ return url in executing;
+ },
+
+ /**
+ * Starts the download of a subscription.
+ * @param {DownloadableSubscription} subscription Subscription to be downloaded
+ * @param {Boolean} manual true for a manually started download (should not trigger fallback requests)
+ * @param {Boolean} forceDownload if true, the subscription will even be redownloaded if it didn't change on the server
+ */
+ execute: function(subscription, manual, forceDownload)
+ {
+ // Delay execution, SeaMonkey 2.1 won't fire request's event handlers
+ // otherwise if the window that called us is closed.
+ Utils.runAsync(this.executeInternal, this, subscription, manual, forceDownload);
+ },
+
+ executeInternal: function(subscription, manual, forceDownload)
+ {
+ let url = subscription.url;
+ if (url in executing)
+ return;
+
+ let newURL = subscription.nextURL;
+ let hadTemporaryRedirect = false;
+ subscription.nextURL = null;
+
+ let curVersion = "3.5.0";
+ let loadFrom = newURL;
+ let isBaseLocation = true;
+ if (!loadFrom)
+ loadFrom = url;
+ if (loadFrom == url)
+ {
+ if (subscription.alternativeLocations)
+ {
+ // We have alternative download locations, choose one. "Regular"
+ // subscription URL always goes in with weight 1.
+ let options = [[1, url]];
+ let totalWeight = 1;
+ for each (let alternative in subscription.alternativeLocations.split(','))
+ {
+ if (!/^https?:\/\//.test(alternative))
+ continue;
+
+ let weight = 1;
+ let match = /;q=([\d\.]+)$/.exec(alternative);
+ if (match)
+ {
+ weight = parseFloat(match[1]);
+ if (isNaN(weight) || !isFinite(weight) || weight < 0)
+ weight = 1;
+ if (weight > 10)
+ weight = 10;
+
+ alternative = alternative.substr(0, match.index);
+ }
+ options.push([weight, alternative]);
+ totalWeight += weight;
+ }
+
+ let choice = Math.random() * totalWeight;
+ for each (let [weight, alternative] in options)
+ {
+ choice -= weight;
+ if (choice < 0)
+ {
+ loadFrom = alternative;
+ break;
+ }
+ }
+
+ isBaseLocation = (loadFrom == url);
+ }
+ }
+ else
+ {
+ // Ignore modification date if we are downloading from a different location
+ forceDownload = true;
+ }
+ loadFrom = loadFrom.replace(/%VERSION%/, "ABP" + "3.5.0");
+
+ let request = null;
+ function errorCallback(error)
+ {
+ let channelStatus = -1;
+ try
+ {
+ channelStatus = request.channel.status;
+ } catch (e) {}
+ let responseStatus = "";
+ try
+ {
+ responseStatus = request.channel.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+ } catch (e) {}
+ setError(subscription, error, channelStatus, responseStatus, loadFrom, isBaseLocation, manual);
+ }
+
+ try
+ {
+ request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ request.mozBackgroundRequest = true;
+ request.open("GET", loadFrom);
+ }
+ catch (e)
+ {
+ errorCallback("synchronize_invalid_url");
+ return;
+ }
+
+ try {
+ request.overrideMimeType("text/plain");
+ request.channel.loadFlags = request.channel.loadFlags |
+ request.channel.INHIBIT_CACHING |
+ request.channel.VALIDATE_ALWAYS;
+
+ // Override redirect limit from preferences, user might have set it to 1
+ if (request.channel instanceof Ci.nsIHttpChannel)
+ request.channel.redirectionLimit = 5;
+
+ var oldNotifications = request.channel.notificationCallbacks;
+ var oldEventSink = null;
+ request.channel.notificationCallbacks =
+ {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSink]),
+
+ getInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsIChannelEventSink))
+ {
+ try {
+ oldEventSink = oldNotifications.QueryInterface(iid);
+ } catch(e) {}
+ return this;
+ }
+
+ if (oldNotifications)
+ return oldNotifications.QueryInterface(iid);
+ else
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback)
+ {
+ if (isBaseLocation && !hadTemporaryRedirect && oldChannel instanceof Ci.nsIHttpChannel)
+ {
+ try
+ {
+ subscription.alternativeLocations = oldChannel.getResponseHeader("X-Alternative-Locations");
+ }
+ catch (e)
+ {
+ subscription.alternativeLocations = null;
+ }
+ }
+
+ if (flags & Ci.nsIChannelEventSink.REDIRECT_TEMPORARY)
+ hadTemporaryRedirect = true;
+ else if (!hadTemporaryRedirect)
+ newURL = newChannel.URI.spec;
+
+ if (oldEventSink)
+ oldEventSink.asyncOnChannelRedirect(oldChannel, newChannel, flags, callback);
+ else
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+ }
+ }
+ catch (e)
+ {
+ Cu.reportError(e)
+ }
+
+ if (subscription.lastModified && !forceDownload)
+ request.setRequestHeader("If-Modified-Since", subscription.lastModified);
+
+ request.addEventListener("error", function(ev)
+ {
+ delete executing[url];
+ try {
+ request.channel.notificationCallbacks = null;
+ } catch (e) {}
+
+ errorCallback("synchronize_connection_error");
+ }, false);
+
+ request.addEventListener("load", function(ev)
+ {
+ delete executing[url];
+ try {
+ request.channel.notificationCallbacks = null;
+ } catch (e) {}
+
+ // Status will be 0 for non-HTTP requests
+ if (request.status && request.status != 200 && request.status != 304)
+ {
+ errorCallback("synchronize_connection_error");
+ return;
+ }
+
+ let newFilters = null;
+ if (request.status != 304)
+ {
+ newFilters = readFilters(subscription, request.responseText, errorCallback);
+ if (!newFilters)
+ return;
+
+ subscription.lastModified = request.getResponseHeader("Last-Modified");
+ }
+
+ if (isBaseLocation && !hadTemporaryRedirect)
+ subscription.alternativeLocations = request.getResponseHeader("X-Alternative-Locations");
+ subscription.lastSuccess = subscription.lastDownload = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
+ subscription.downloadStatus = "synchronize_ok";
+ subscription.errors = 0;
+
+ // Expiration header is relative to server time - use Date header if it exists, otherwise local time
+ let now = Math.round((new Date(request.getResponseHeader("Date")).getTime() || Date.now()) / MILLISECONDS_IN_SECOND);
+ let expires = Math.round(new Date(request.getResponseHeader("Expires")).getTime() / MILLISECONDS_IN_SECOND) || 0;
+ let expirationInterval = (expires ? expires - now : 0);
+ for each (let filter in newFilters || subscription.filters)
+ {
+ if (!(filter instanceof CommentFilter))
+ continue;
+
+ let match = /\bExpires\s*(?::|after)\s*(\d+)\s*(h)?/i.exec(filter.text);
+ if (match)
+ {
+ let interval = parseInt(match[1], 10);
+ if (match[2])
+ interval *= SECONDS_IN_HOUR;
+ else
+ interval *= SECONDS_IN_DAY;
+
+ if (interval > expirationInterval)
+ expirationInterval = interval;
+ }
+ }
+
+ // Expiration interval should be within allowed range
+ expirationInterval = Math.min(Math.max(expirationInterval, MIN_EXPIRATION_INTERVAL), MAX_EXPIRATION_INTERVAL);
+
+ // Hard expiration: download immediately after twice the expiration interval
+ subscription.expires = (subscription.lastDownload + expirationInterval * 2);
+
+ // Soft expiration: use random interval factor between 0.8 and 1.2
+ subscription.softExpiration = (subscription.lastDownload + Math.round(expirationInterval * (Math.random() * 0.4 + 0.8)));
+
+ // Process some special filters and remove them
+ if (newFilters)
+ {
+ let fixedTitle = false;
+ for (let i = 0; i < newFilters.length; i++)
+ {
+ let filter = newFilters[i];
+ if (!(filter instanceof CommentFilter))
+ continue;
+
+ let match = /^!\s*(\w+)\s*:\s*(.*)/.exec(filter.text);
+ if (match)
+ {
+ let keyword = match[1].toLowerCase();
+ let value = match[2];
+ let known = true;
+ if (keyword == "redirect")
+ {
+ if (isBaseLocation && value != url)
+ subscription.nextURL = value;
+ }
+ else if (keyword == "homepage")
+ {
+ let uri = Utils.makeURI(value);
+ if (uri && (uri.scheme == "http" || uri.scheme == "https"))
+ subscription.homepage = uri.spec;
+ }
+ else if (keyword == "title")
+ {
+ if (value)
+ {
+ subscription.title = value;
+ fixedTitle = true;
+ }
+ }
+ else
+ known = false;
+
+ if (known)
+ newFilters.splice(i--, 1);
+ }
+ }
+ subscription.fixedTitle = fixedTitle;
+ }
+
+ if (isBaseLocation && newURL && newURL != url)
+ {
+ let listed = (subscription.url in FilterStorage.knownSubscriptions);
+ if (listed)
+ FilterStorage.removeSubscription(subscription);
+
+ url = newURL;
+
+ let newSubscription = Subscription.fromURL(url);
+ for (let key in newSubscription)
+ delete newSubscription[key];
+ for (let key in subscription)
+ newSubscription[key] = subscription[key];
+
+ delete Subscription.knownSubscriptions[subscription.url];
+ newSubscription.oldSubscription = subscription;
+ subscription = newSubscription;
+ subscription.url = url;
+
+ if (!(subscription.url in FilterStorage.knownSubscriptions) && listed)
+ FilterStorage.addSubscription(subscription);
+ }
+
+ if (newFilters)
+ FilterStorage.updateSubscriptionFilters(subscription, newFilters);
+ delete subscription.oldSubscription;
+ }, false);
+
+ executing[url] = true;
+ FilterNotifier.triggerListeners("subscription.downloadStatus", subscription);
+
+ try
+ {
+ request.send(null);
+ }
+ catch (e)
+ {
+ delete executing[url];
+ errorCallback("synchronize_connection_error");
+ return;
+ }
+ }
+};
+
+/**
+ * Checks whether any subscriptions need to be downloaded and starts the download
+ * if necessary.
+ */
+function checkSubscriptions()
+{
+ if (!Prefs.subscriptions_autoupdate)
+ return;
+
+ let time = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
+ for each (let subscription in FilterStorage.subscriptions)
+ {
+ if (!(subscription instanceof DownloadableSubscription))
+ continue;
+
+ if (subscription.lastCheck && time - subscription.lastCheck > MAX_ABSENSE_INTERVAL)
+ {
+ // No checks for a long time interval - user must have been offline, e.g.
+ // during a weekend. Increase soft expiration to prevent load peaks on the
+ // server.
+ subscription.softExpiration += time - subscription.lastCheck;
+ }
+ subscription.lastCheck = time;
+
+ // Sanity check: do expiration times make sense? Make sure people changing
+ // system clock don't get stuck with outdated subscriptions.
+ if (subscription.expires - time > MAX_EXPIRATION_INTERVAL)
+ subscription.expires = time + MAX_EXPIRATION_INTERVAL;
+ if (subscription.softExpiration - time > MAX_EXPIRATION_INTERVAL)
+ subscription.softExpiration = time + MAX_EXPIRATION_INTERVAL;
+
+ if (subscription.softExpiration > time && subscription.expires > time)
+ continue;
+
+ // Do not retry downloads more often than MIN_EXPIRATION_INTERVAL
+ if (time - subscription.lastDownload >= MIN_EXPIRATION_INTERVAL)
+ Synchronizer.execute(subscription, false);
+ }
+}
+
+/**
+ * Extracts a list of filters from text returned by a server.
+ * @param {DownloadableSubscription} subscription subscription the info should be placed into
+ * @param {String} text server response
+ * @param {Function} errorCallback function to be called on error
+ * @return {Array of Filter}
+ */
+function readFilters(subscription, text, errorCallback)
+{
+ let lines = text.split(/[\r\n]+/);
+ let match = /\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.exec(lines[0]);
+ if (!match)
+ {
+ errorCallback("synchronize_invalid_data");
+ return null;
+ }
+ let minVersion = match[1];
+
+ for (let i = 0; i < lines.length; i++)
+ {
+ let match = /!\s*checksum[\s\-:]+([\w\+\/]+)/i.exec(lines[i]);
+ if (match)
+ {
+ lines.splice(i, 1);
+ let checksum = Utils.generateChecksum(lines);
+
+ if (checksum && checksum != match[1])
+ {
+ errorCallback("synchronize_checksum_mismatch");
+ return null;
+ }
+
+ break;
+ }
+ }
+
+ delete subscription.requiredVersion;
+ delete subscription.upgradeRequired;
+ if (minVersion)
+ {
+ subscription.requiredVersion = minVersion;
+ if (Utils.versionComparator.compare(minVersion, "3.5.0") > 0)
+ subscription.upgradeRequired = true;
+ }
+
+ lines.shift();
+ let result = [];
+ for each (let line in lines)
+ {
+ let filter = Filter.fromText(Filter.normalize(line));
+ if (filter)
+ result.push(filter);
+ }
+
+ return result;
+}
+
+/**
+ * Handles an error during a subscription download.
+ * @param {DownloadableSubscription} subscription subscription that failed to download
+ * @param {Integer} channelStatus result code of the download channel
+ * @param {String} responseStatus result code as received from server
+ * @param {String} downloadURL the URL used for download
+ * @param {String} error error ID in global.properties
+ * @param {Boolean} isBaseLocation false if the subscription was downloaded from a location specified in X-Alternative-Locations header
+ * @param {Boolean} manual true for a manually started download (should not trigger fallback requests)
+ */
+function setError(subscription, error, channelStatus, responseStatus, downloadURL, isBaseLocation, manual)
+{
+ // If download from an alternative location failed, reset the list of
+ // alternative locations - have to get an updated list from base location.
+ if (!isBaseLocation)
+ subscription.alternativeLocations = null;
+
+ try {
+ Cu.reportError("Adblock Plus: Downloading filter subscription " + subscription.title + " failed (" + Utils.getString(error) + ")\n" +
+ "Download address: " + downloadURL + "\n" +
+ "Channel status: " + channelStatus + "\n" +
+ "Server response: " + responseStatus);
+ } catch(e) {}
+
+ subscription.lastDownload = Math.round(Date.now() / MILLISECONDS_IN_SECOND);
+ subscription.downloadStatus = error;
+
+ // Request fallback URL if necessary - for automatic updates only
+ if (!manual)
+ {
+ if (error == "synchronize_checksum_mismatch")
+ {
+ // No fallback for successful download with checksum mismatch, reset error counter
+ subscription.errors = 0;
+ }
+ else
+ subscription.errors++;
+
+ if (subscription.errors >= Prefs.subscriptions_fallbackerrors && /^https?:\/\//i.test(subscription.url))
+ {
+ subscription.errors = 0;
+
+ let fallbackURL = Prefs.subscriptions_fallbackurl;
+ fallbackURL = fallbackURL.replace(/%VERSION%/g, encodeURIComponent("3.5.0"));
+ fallbackURL = fallbackURL.replace(/%SUBSCRIPTION%/g, encodeURIComponent(subscription.url));
+ fallbackURL = fallbackURL.replace(/%URL%/g, encodeURIComponent(downloadURL));
+ fallbackURL = fallbackURL.replace(/%ERROR%/g, encodeURIComponent(error));
+ fallbackURL = fallbackURL.replace(/%CHANNELSTATUS%/g, encodeURIComponent(channelStatus));
+ fallbackURL = fallbackURL.replace(/%RESPONSESTATUS%/g, encodeURIComponent(responseStatus));
+
+ let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ request.mozBackgroundRequest = true;
+ request.open("GET", fallbackURL);
+ request.overrideMimeType("text/plain");
+ request.channel.loadFlags = request.channel.loadFlags |
+ request.channel.INHIBIT_CACHING |
+ request.channel.VALIDATE_ALWAYS;
+ request.addEventListener("load", function(ev)
+ {
+ if (!(subscription.url in FilterStorage.knownSubscriptions))
+ return;
+
+ let match = /^(\d+)(?:\s+(\S+))?$/.exec(request.responseText);
+ if (match && match[1] == "301" && match[2]) // Moved permanently
+ subscription.nextURL = match[2];
+ else if (match && match[1] == "410") // Gone
+ {
+ let data = "[Adblock]\n" + subscription.filters.map(function(f) f.text).join("\n");
+ let url = "data:text/plain," + encodeURIComponent(data);
+ let newSubscription = Subscription.fromURL(url);
+ newSubscription.title = subscription.title;
+ newSubscription.disabled = subscription.disabled;
+ FilterStorage.removeSubscription(subscription);
+ FilterStorage.addSubscription(newSubscription);
+ Synchronizer.execute(newSubscription);
+ }
+ }, false);
+ request.send(null);
+ }
+ }
+}
diff --git a/abprime/modules/TimeLine.jsm b/abprime/modules/TimeLine.jsm
new file mode 100644
index 00000000..0efa8090
--- /dev/null
+++ b/abprime/modules/TimeLine.jsm
@@ -0,0 +1,153 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Debugging module used for load time measurements.
+ */
+
+var EXPORTED_SYMBOLS = ["TimeLine"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+let nestingCounter = 0;
+let firstTimeStamp = null;
+let lastTimeStamp = null;
+
+let asyncActions = {__proto__: null};
+
+/**
+ * Time logging module, used to measure startup time of Adblock Plus (development builds only).
+ * @class
+ */
+var TimeLine = {
+ /**
+ * Logs an event to console together with the time it took to get there.
+ */
+ log: function(/**String*/ message, /**Boolean*/ _forceDisplay)
+ {
+ if (!_forceDisplay && nestingCounter <= 0)
+ return;
+
+ let now = Date.now();
+ let diff = lastTimeStamp ? Math.round(now - lastTimeStamp) : "first event";
+ lastTimeStamp = now;
+
+ // Indent message depending on current nesting level
+ for (let i = 0; i < nestingCounter; i++)
+ message = "* " + message;
+
+ // Pad message with spaces
+ let padding = [];
+ for (let i = message.toString().length; i < 80; i++)
+ padding.push(" ");
+ dump("[" + now + "] ABP timeline: " + message + padding.join("") + "\t (" + diff + ")\n");
+ },
+
+ /**
+ * Called to indicate that application entered a block that needs to be timed.
+ */
+ enter: function(/**String*/ message)
+ {
+ if (nestingCounter <= 0)
+ firstTimeStamp = Date.now();
+
+ this.log(message, true);
+ nestingCounter = (nestingCounter <= 0 ? 1 : nestingCounter + 1);
+ },
+
+ /**
+ * Called when application exited a block that TimeLine.enter() was called for.
+ * @param {String} message message to be logged
+ * @param {String} [asyncAction] identifier of a pending async action
+ */
+ leave: function(message, asyncAction)
+ {
+ if (typeof asyncAction != "undefined")
+ message += " (async action pending)";
+
+ nestingCounter--;
+ this.log(message, true);
+
+ if (nestingCounter <= 0)
+ {
+ if (firstTimeStamp !== null)
+ dump("ABP timeline: Total time elapsed: " + Math.round(Date.now() - firstTimeStamp) + "\n");
+ firstTimeStamp = null;
+ lastTimeStamp = null;
+ }
+
+ if (typeof asyncAction != "undefined")
+ {
+ if (asyncAction in asyncActions)
+ dump("ABP timeline: Warning: Async action " + asyncAction + " already executing\n");
+ asyncActions[asyncAction] = {start: Date.now(), total: 0};
+ }
+ },
+
+ /**
+ * Called when the application starts processing of an async action.
+ */
+ asyncStart: function(/**String*/ asyncAction)
+ {
+ if (asyncAction in asyncActions)
+ {
+ let action = asyncActions[asyncAction];
+ if ("currentStart" in action)
+ dump("ABP timeline: Warning: Processing reentered for async action " + asyncAction + "\n");
+ action.currentStart = Date.now();
+ }
+ else
+ dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n");
+ },
+
+ /**
+ * Called when the application finishes processing of an async action.
+ */
+ asyncEnd: function(/**String*/ asyncAction)
+ {
+ if (asyncAction in asyncActions)
+ {
+ let action = asyncActions[asyncAction];
+ if ("currentStart" in action)
+ {
+ action.total += Date.now() - action.currentStart;
+ delete action.currentStart;
+ }
+ else
+ dump("ABP timeline: Warning: Processing not entered for async action " + asyncAction + "\n");
+ }
+ else
+ dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n");
+ },
+
+ /**
+ * Called when an async action is done and its time can be logged.
+ */
+ asyncDone: function(/**String*/ asyncAction)
+ {
+ if (asyncAction in asyncActions)
+ {
+ let action = asyncActions[asyncAction];
+ let now = Date.now();
+ let diff = now - action.start;
+ if ("currentStart" in action)
+ dump("ABP timeline: Warning: Still processing for async action " + asyncAction + "\n");
+
+ let message = "Async action " + asyncAction + " done";
+ let padding = [];
+ for (let i = message.toString().length; i < 80; i++)
+ padding.push(" ");
+ dump("[" + now + "] ABP timeline: " + message + padding.join("") + "\t (" + action.total + "/" + diff + ")\n");
+ }
+ else
+ dump("ABP timeline: Warning: Async action " + asyncAction + " is unknown\n");
+ }
+};
diff --git a/abprime/modules/Utils.jsm b/abprime/modules/Utils.jsm
new file mode 100644
index 00000000..64e6ab62
--- /dev/null
+++ b/abprime/modules/Utils.jsm
@@ -0,0 +1,820 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+#filter substitution
+
+/**
+ * @fileOverview Module containing a bunch of utility functions.
+ */
+
+var EXPORTED_SYMBOLS = ["Utils", "Cache"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+let sidebarParams = null;
+
+/**
+ * Provides a bunch of utility functions.
+ * @class
+ */
+var Utils =
+{
+ /**
+ * Returns the add-on ID used by Adblock Plus
+ */
+ get addonID()
+ {
+ return "@ADDON_ID@";
+ },
+
+ /**
+ * Returns the installed Adblock Plus version
+ */
+ get addonVersion()
+ {
+ let version = "@ADDON_VERSION@";
+ return (version[0] == "{" ? "99.9" : version);
+ },
+
+ /**
+ * Returns the VCS revision used for this Adblock Plus build
+ */
+ get addonBuild()
+ {
+ let build = "";
+ return (build[0] == "{" ? "" : build);
+ },
+
+ /**
+ * Returns ID of the application
+ */
+ get appID()
+ {
+ let id = Services.appinfo.ID;
+ Utils.__defineGetter__("appID", function() id);
+ return Utils.appID;
+ },
+
+ /**
+ * Returns whether we are running in Fennec, for Fennec-specific hacks
+ * @type Boolean
+ */
+ get isFennec()
+ {
+ let result = (this.appID == "{a23983c0-fd0e-11dc-95ff-0800200c9a66}" || this.appID == "{aa3c5121-dab2-40e2-81ca-7ea25febc110}");
+ Utils.__defineGetter__("isFennec", function() result);
+ return result;
+ },
+
+ /**
+ * Returns the user interface locale selected for adblockplus chrome package.
+ */
+ get appLocale()
+ {
+ let locale = "en-US";
+ try
+ {
+ locale = Utils.chromeRegistry.getSelectedLocale("@ADDON_CHROME_NAME@");
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+ Utils.__defineGetter__("appLocale", function() locale);
+ return Utils.appLocale;
+ },
+
+ /**
+ * Returns version of the Gecko platform
+ */
+ get platformVersion()
+ {
+ let platformVersion = Services.appinfo.platformVersion;
+ Utils.__defineGetter__("platformVersion", function() platformVersion);
+ return Utils.platformVersion;
+ },
+
+ /**
+ * Retrieves a string from global.properties string bundle, will throw if string isn't found.
+ *
+ * @param {String} name string name
+ * @return {String}
+ */
+ getString: function(name)
+ {
+ let stringBundle = Services.strings.createBundle("chrome://@ADDON_CHROME_NAME@/locale/global.properties");
+ Utils.getString = function(name)
+ {
+ return stringBundle.GetStringFromName(name);
+ }
+ return Utils.getString(name);
+ },
+
+ /**
+ * Shows an alert message like window.alert() but with a custom title.
+ *
+ * @param {Window} parentWindow parent window of the dialog (can be null)
+ * @param {String} message message to be displayed
+ * @param {String} [title] dialog title, default title will be used if omitted
+ */
+ alert: function(parentWindow, message, title)
+ {
+ if (!title)
+ title = Utils.getString("default_dialog_title");
+ Utils.promptService.alert(parentWindow, title, message);
+ },
+
+ /**
+ * Asks the user for a confirmation like window.confirm() but with a custom title.
+ *
+ * @param {Window} parentWindow parent window of the dialog (can be null)
+ * @param {String} message message to be displayed
+ * @param {String} [title] dialog title, default title will be used if omitted
+ * @return {Bool}
+ */
+ confirm: function(parentWindow, message, title)
+ {
+ if (!title)
+ title = Utils.getString("default_dialog_title");
+ return Utils.promptService.confirm(parentWindow, title, message);
+ },
+
+ /**
+ * Retrieves the window for a document node.
+ * @return {Window} will be null if the node isn't associated with a window
+ */
+ getWindow: function(/**Node*/ node)
+ {
+ if ("ownerDocument" in node && node.ownerDocument)
+ node = node.ownerDocument;
+
+ if ("defaultView" in node)
+ return node.defaultView;
+
+ return null;
+ },
+
+ /**
+ * If the window doesn't have its own security context (e.g. about:blank or
+ * data: URL) walks up the parent chain until a window is found that has a
+ * security context.
+ */
+ getOriginWindow: function(/**Window*/ wnd) /**Window*/
+ {
+ while (wnd != wnd.parent)
+ {
+ let uri = Utils.makeURI(wnd.location.href);
+ if (uri.spec != "about:blank" && uri.spec != "moz-safe-about:blank" &&
+ !Utils.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT))
+ {
+ break;
+ }
+ wnd = wnd.parent;
+ }
+ return wnd;
+ },
+
+ /**
+ * If a protocol using nested URIs like jar: is used - retrieves innermost
+ * nested URI.
+ */
+ unwrapURL: function(/**nsIURI or String*/ url) /**nsIURI*/
+ {
+ if (!(url instanceof Ci.nsIURI))
+ url = Utils.makeURI(url);
+
+ if (url instanceof Ci.nsINestedURI)
+ return url.innermostURI;
+ else
+ return url;
+ },
+
+ /**
+ * Translates a string URI into its nsIURI representation, will return null for
+ * invalid URIs.
+ */
+ makeURI: function(/**String*/ url) /**nsIURI*/
+ {
+ try
+ {
+ return Utils.ioService.newURI(url, null, null);
+ }
+ catch (e) {
+ return null;
+ }
+ },
+
+ /**
+ * Posts an action to the event queue of the current thread to run it
+ * asynchronously. Any additional parameters to this function are passed
+ * as parameters to the callback.
+ */
+ runAsync: function(/**Function*/ callback, /**Object*/ thisPtr)
+ {
+ let params = Array.prototype.slice.call(arguments, 2);
+ let runnable = {
+ run: function()
+ {
+ callback.apply(thisPtr, params);
+ }
+ };
+ Utils.threadManager.currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL);
+ },
+
+ /**
+ * Gets the DOM window associated with a particular request (if any).
+ */
+ getRequestWindow: function(/**nsIChannel*/ channel) /**nsIDOMWindow*/
+ {
+ try
+ {
+ if (channel.notificationCallbacks)
+ return channel.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
+ } catch(e) {}
+
+ try
+ {
+ if (channel.loadGroup && channel.loadGroup.notificationCallbacks)
+ return channel.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow;
+ } catch(e) {}
+
+ return null;
+ },
+
+ /**
+ * Generates filter subscription checksum.
+ *
+ * @param {Array of String} lines filter subscription lines (with checksum line removed)
+ * @return {String} checksum or null
+ */
+ generateChecksum: function(lines)
+ {
+ let stream = null;
+ try
+ {
+ // Checksum is an MD5 checksum (base64-encoded without the trailing "=") of
+ // all lines in UTF-8 without the checksum line, joined with "\n".
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ stream = converter.convertToInputStream(lines.join("\n"));
+
+ let hashEngine = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ hashEngine.init(hashEngine.MD5);
+ hashEngine.updateFromStream(stream, stream.available());
+ return hashEngine.finish(true).replace(/=+$/, "");
+ }
+ catch (e)
+ {
+ return null;
+ }
+ finally
+ {
+ if (stream)
+ stream.close();
+ }
+ },
+
+ /**
+ * Opens filter preferences dialog or focuses an already open dialog.
+ * @param {Filter} [filter] filter to be selected
+ */
+ openFiltersDialog: function(filter)
+ {
+ var dlg = Utils.windowMediator.getMostRecentWindow("abp:filters");
+ if (dlg)
+ {
+ try
+ {
+ dlg.focus();
+ }
+ catch (e) {}
+ if (filter)
+ dlg.SubscriptionActions.selectFilter(filter);
+ }
+ else
+ {
+ Utils.windowWatcher.openWindow(null, "chrome://@ADDON_CHROME_NAME@/content/filters.xul", "_blank", "chrome,centerscreen,resizable,dialog=no", {wrappedJSObject: filter});
+ }
+ },
+
+ /**
+ * Opens a URL in the browser window. If browser window isn't passed as parameter,
+ * this function attempts to find a browser window. If an event is passed in
+ * it should be passed in to the browser if possible (will e.g. open a tab in
+ * background depending on modifiers keys).
+ */
+ loadInBrowser: function(/**String*/ url, /**Window*/ currentWindow, /**Event*/ event)
+ {
+ let abpHooks = currentWindow ? currentWindow.document.getElementById("abp-hooks") : null;
+ if (!abpHooks || !abpHooks.addTab)
+ {
+ let enumerator = Utils.windowMediator.getZOrderDOMWindowEnumerator(null, true);
+ if (!enumerator.hasMoreElements())
+ {
+ // On Linux the list returned will be empty, see bug 156333. Fall back to random order.
+ enumerator = Utils.windowMediator.getEnumerator(null);
+ }
+ while (enumerator.hasMoreElements())
+ {
+ let window = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
+ abpHooks = window.document.getElementById("abp-hooks");
+ if (abpHooks && abpHooks.addTab)
+ {
+ if (!currentWindow)
+ window.focus();
+ break;
+ }
+ }
+ }
+
+ if (abpHooks && abpHooks.addTab)
+ abpHooks.addTab(url, event);
+ else
+ {
+ let protocolService = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
+ protocolService.loadURI(Utils.makeURI(url), null);
+ }
+ },
+
+ /**
+ * Opens a pre-defined documentation link in the browser window. This will
+ * send the UI language to adblockplus.org so that the correct language
+ * version of the page can be selected.
+ */
+ loadDocLink: function(/**String*/ linkID)
+ {
+ let baseURL = "resource://@ADDON_CHROME_NAME@/modules/";
+ Cu.import(baseURL + "Prefs.jsm");
+
+ let link = Prefs.documentation_link.replace(/%LINK%/g, linkID).replace(/%LANG%/g, Utils.appLocale);
+ Utils.loadInBrowser(link);
+ },
+
+ /**
+ * Formats a unix time according to user's locale.
+ * @param {Integer} time unix time in milliseconds
+ * @return {String} formatted date and time
+ */
+ formatTime: function(time)
+ {
+ try
+ {
+ let date = new Date(time);
+ return Utils.dateFormatter.FormatDateTime("", Ci.nsIScriptableDateFormat.dateFormatShort,
+ Ci.nsIScriptableDateFormat.timeFormatNoSeconds,
+ date.getFullYear(), date.getMonth() + 1, date.getDate(),
+ date.getHours(), date.getMinutes(), date.getSeconds());
+ }
+ catch(e)
+ {
+ // Make sure to return even on errors
+ Cu.reportError(e);
+ return "";
+ }
+ },
+
+ /**
+ * Checks whether any of the prefixes listed match the application locale,
+ * returns matching prefix if any.
+ */
+ checkLocalePrefixMatch: function(/**String*/ prefixes) /**String*/
+ {
+ if (!prefixes)
+ return null;
+
+ let appLocale = Utils.appLocale;
+ for each (let prefix in prefixes.split(/,/))
+ if (new RegExp("^" + prefix + "\\b").test(appLocale))
+ return prefix;
+
+ return null;
+ },
+
+ /**
+ * Chooses the best filter subscription for user's language.
+ * XXX: Removed code that will select at random
+ */
+ chooseFilterSubscription: function(/**NodeList*/ subscriptions) /**Node*/
+ {
+ let selectedItem = null;
+ let selectedPrefix = null;
+ let matchCount = 0;
+ for (let i = 0; i < subscriptions.length; i++)
+ {
+ let subscription = subscriptions[i];
+ if (!selectedItem)
+ selectedItem = subscription;
+
+ let prefix = Utils.checkLocalePrefixMatch(subscription.getAttribute("prefixes"));
+ if (prefix)
+ {
+ if (!selectedPrefix || selectedPrefix.length < prefix.length)
+ {
+ selectedItem = subscription;
+ selectedPrefix = prefix;
+ matchCount = 1;
+ }
+ }
+ }
+ return selectedItem;
+ },
+
+ /**
+ * Saves sidebar state before detaching/reattaching
+ */
+ setParams: function(params)
+ {
+ sidebarParams = params;
+ },
+
+ /**
+ * Retrieves and removes sidebar state after detaching/reattaching
+ */
+ getParams: function()
+ {
+ let ret = sidebarParams;
+ sidebarParams = null;
+ return ret;
+ },
+
+ /**
+ * Randomly generated class for collapsed nodes.
+ * @type String
+ */
+ collapsedClass: null,
+
+ /**
+ * Nodes scheduled for post-processing (might be null).
+ * @type Array of Node
+ */
+ scheduledNodes: null,
+
+ /**
+ * Schedules a node for post-processing.
+ */
+ schedulePostProcess: function(node)
+ {
+ if (Utils.scheduledNodes)
+ Utils.scheduledNodes.push(node);
+ else
+ {
+ Utils.scheduledNodes = [node];
+ Utils.runAsync(Utils.postProcessNodes);
+ }
+ },
+
+ /**
+ * Processes nodes scheduled for post-processing (typically hides them).
+ */
+ postProcessNodes: function()
+ {
+ let nodes = Utils.scheduledNodes;
+ Utils.scheduledNodes = null;
+
+ for each (let node in nodes)
+ {
+ // adjust frameset's cols/rows for frames
+ let parentNode = node.parentNode;
+ if (parentNode && parentNode instanceof Ci.nsIDOMHTMLFrameSetElement)
+ {
+ let hasCols = (parentNode.cols && parentNode.cols.indexOf(",") > 0);
+ let hasRows = (parentNode.rows && parentNode.rows.indexOf(",") > 0);
+ if ((hasCols || hasRows) && !(hasCols && hasRows))
+ {
+ let index = -1;
+ for (let frame = node; frame; frame = frame.previousSibling)
+ if (frame instanceof Ci.nsIDOMHTMLFrameElement || frame instanceof Ci.nsIDOMHTMLFrameSetElement)
+ index++;
+
+ let property = (hasCols ? "cols" : "rows");
+ let weights = parentNode[property].split(",");
+ weights[index] = "0";
+ parentNode[property] = weights.join(",");
+ }
+ }
+ else
+ node.className += " " + Utils.collapsedClass;
+ }
+ },
+
+ /**
+ * Verifies RSA signature. The public key and signature should be base64-encoded.
+ */
+ verifySignature: function(/**String*/ key, /**String*/ signature, /**String*/ data) /**Boolean*/
+ {
+ if (!Utils.crypto)
+ return false;
+
+ // Maybe we did the same check recently, look it up in the cache
+ if (!("_cache" in Utils.verifySignature))
+ Utils.verifySignature._cache = new Cache(5);
+ let cache = Utils.verifySignature._cache;
+ let cacheKey = key + " " + signature + " " + data;
+ if (cacheKey in cache.data)
+ return cache.data[cacheKey];
+ else
+ cache.add(cacheKey, false);
+
+ let keyInfo, pubKey, context;
+ try
+ {
+ let keyItem = Utils.crypto.getSECItem(atob(key));
+ keyInfo = Utils.crypto.SECKEY_DecodeDERSubjectPublicKeyInfo(keyItem.address());
+ if (keyInfo.isNull())
+ throw new Error("SECKEY_DecodeDERSubjectPublicKeyInfo failed");
+
+ pubKey = Utils.crypto.SECKEY_ExtractPublicKey(keyInfo);
+ if (pubKey.isNull())
+ throw new Error("SECKEY_ExtractPublicKey failed");
+
+ let signatureItem = Utils.crypto.getSECItem(atob(signature));
+
+ context = Utils.crypto.VFY_CreateContext(pubKey, signatureItem.address(), Utils.crypto.SEC_OID_ISO_SHA_WITH_RSA_SIGNATURE, null);
+ if (context.isNull())
+ return false; // This could happen if the signature is invalid
+
+ let error = Utils.crypto.VFY_Begin(context);
+ if (error < 0)
+ throw new Error("VFY_Begin failed");
+
+ error = Utils.crypto.VFY_Update(context, data, data.length);
+ if (error < 0)
+ throw new Error("VFY_Update failed");
+
+ error = Utils.crypto.VFY_End(context);
+ if (error < 0)
+ return false;
+
+ cache.data[cacheKey] = true;
+ return true;
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ return false;
+ }
+ finally
+ {
+ if (keyInfo && !keyInfo.isNull())
+ Utils.crypto.SECKEY_DestroySubjectPublicKeyInfo(keyInfo);
+ if (pubKey && !pubKey.isNull())
+ Utils.crypto.SECKEY_DestroyPublicKey(pubKey);
+ if (context && !context.isNull())
+ Utils.crypto.VFY_DestroyContext(context, true);
+ }
+ }
+};
+
+/**
+ * A cache with a fixed capacity, newer entries replace entries that have been
+ * stored first.
+ * @constructor
+ */
+function Cache(/**Integer*/ size)
+{
+ this._ringBuffer = new Array(size);
+ this.data = {__proto__: null};
+}
+Cache.prototype =
+{
+ /**
+ * Ring buffer storing hash keys, allows determining which keys need to be
+ * evicted.
+ * @type Array
+ */
+ _ringBuffer: null,
+
+ /**
+ * Index in the ring buffer to be written next.
+ * @type Integer
+ */
+ _bufferIndex: 0,
+
+ /**
+ * Cache data, maps values to the keys. Read-only access, for writing use
+ * add() method.
+ * @type Object
+ */
+ data: null,
+
+ /**
+ * Adds a key and the corresponding value to the cache.
+ */
+ add: function(/**String*/ key, value)
+ {
+ if (!(key in this.data))
+ {
+ // This is a new key - we need to add it to the ring buffer and evict
+ // another entry instead.
+ let oldKey = this._ringBuffer[this._bufferIndex];
+ if (typeof oldKey != "undefined")
+ delete this.data[oldKey];
+ this._ringBuffer[this._bufferIndex] = key;
+
+ this._bufferIndex++;
+ if (this._bufferIndex >= this._ringBuffer.length)
+ this._bufferIndex = 0;
+ }
+
+ this.data[key] = value;
+ },
+
+ /**
+ * Clears cache contents.
+ */
+ clear: function()
+ {
+ this._ringBuffer = new Array(this._ringBuffer.length);
+ this.data = {__proto__: null};
+ }
+}
+
+// Getters for common services, this should be replaced by Services.jsm in future
+
+XPCOMUtils.defineLazyServiceGetter(Utils, "categoryManager", "@mozilla.org/categorymanager;1", "nsICategoryManager");
+XPCOMUtils.defineLazyServiceGetter(Utils, "ioService", "@mozilla.org/network/io-service;1", "nsIIOService");
+XPCOMUtils.defineLazyServiceGetter(Utils, "threadManager", "@mozilla.org/thread-manager;1", "nsIThreadManager");
+XPCOMUtils.defineLazyServiceGetter(Utils, "promptService", "@mozilla.org/embedcomp/prompt-service;1", "nsIPromptService");
+XPCOMUtils.defineLazyServiceGetter(Utils, "effectiveTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService");
+XPCOMUtils.defineLazyServiceGetter(Utils, "netUtils", "@mozilla.org/network/util;1", "nsINetUtil");
+XPCOMUtils.defineLazyServiceGetter(Utils, "styleService", "@mozilla.org/content/style-sheet-service;1", "nsIStyleSheetService");
+XPCOMUtils.defineLazyServiceGetter(Utils, "prefService", "@mozilla.org/preferences-service;1", "nsIPrefService");
+XPCOMUtils.defineLazyServiceGetter(Utils, "versionComparator", "@mozilla.org/xpcom/version-comparator;1", "nsIVersionComparator");
+XPCOMUtils.defineLazyServiceGetter(Utils, "windowMediator", "@mozilla.org/appshell/window-mediator;1", "nsIWindowMediator");
+XPCOMUtils.defineLazyServiceGetter(Utils, "windowWatcher", "@mozilla.org/embedcomp/window-watcher;1", "nsIWindowWatcher");
+XPCOMUtils.defineLazyServiceGetter(Utils, "chromeRegistry", "@mozilla.org/chrome/chrome-registry;1", "nsIXULChromeRegistry");
+XPCOMUtils.defineLazyServiceGetter(Utils, "systemPrincipal", "@mozilla.org/systemprincipal;1", "nsIPrincipal");
+XPCOMUtils.defineLazyServiceGetter(Utils, "dateFormatter", "@mozilla.org/intl/scriptabledateformat;1", "nsIScriptableDateFormat");
+XPCOMUtils.defineLazyServiceGetter(Utils, "childMessageManager", "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender");
+XPCOMUtils.defineLazyServiceGetter(Utils, "parentMessageManager", "@mozilla.org/parentprocessmessagemanager;1", "nsIFrameMessageManager");
+XPCOMUtils.defineLazyServiceGetter(Utils, "httpProtocol", "@mozilla.org/network/protocol;1?name=http", "nsIHttpProtocolHandler");
+XPCOMUtils.defineLazyServiceGetter(Utils, "clipboard", "@mozilla.org/widget/clipboard;1", "nsIClipboard");
+XPCOMUtils.defineLazyServiceGetter(Utils, "clipboardHelper", "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+XPCOMUtils.defineLazyGetter(Utils, "crypto", function()
+{
+ try
+ {
+ let ctypes = Components.utils.import("resource://gre/modules/ctypes.jsm", null).ctypes;
+
+ let nsslib = ctypes.open(ctypes.libraryName("nss3"));
+
+ let result = {};
+
+ // seccomon.h
+ result.siUTF8String = 14;
+
+ // secoidt.h
+ result.SEC_OID_ISO_SHA_WITH_RSA_SIGNATURE = 15;
+
+ // The following types are opaque to us
+ result.VFYContext = ctypes.void_t;
+ result.SECKEYPublicKey = ctypes.void_t;
+ result.CERTSubjectPublicKeyInfo = ctypes.void_t;
+
+ /*
+ * seccomon.h
+ * struct SECItemStr {
+ * SECItemType type;
+ * unsigned char *data;
+ * unsigned int len;
+ * };
+ */
+ result.SECItem = ctypes.StructType("SECItem", [
+ {type: ctypes.int},
+ {data: ctypes.unsigned_char.ptr},
+ {len: ctypes.int}
+ ]);
+
+ /*
+ * cryptohi.h
+ * extern VFYContext *VFY_CreateContext(SECKEYPublicKey *key, SECItem *sig,
+ * SECOidTag sigAlg, void *wincx);
+ */
+ result.VFY_CreateContext = nsslib.declare(
+ "VFY_CreateContext",
+ ctypes.default_abi, result.VFYContext.ptr,
+ result.SECKEYPublicKey.ptr,
+ result.SECItem.ptr,
+ ctypes.int,
+ ctypes.voidptr_t
+ );
+
+ /*
+ * cryptohi.h
+ * extern void VFY_DestroyContext(VFYContext *cx, PRBool freeit);
+ */
+ result.VFY_DestroyContext = nsslib.declare(
+ "VFY_DestroyContext",
+ ctypes.default_abi, ctypes.void_t,
+ result.VFYContext.ptr,
+ ctypes.bool
+ );
+
+ /*
+ * cryptohi.h
+ * extern SECStatus VFY_Begin(VFYContext *cx);
+ */
+ result.VFY_Begin = nsslib.declare("VFY_Begin",
+ ctypes.default_abi, ctypes.int,
+ result.VFYContext.ptr
+ );
+
+ /*
+ * cryptohi.h
+ * extern SECStatus VFY_Update(VFYContext *cx, const unsigned char *input,
+ * unsigned int inputLen);
+ */
+ result.VFY_Update = nsslib.declare(
+ "VFY_Update",
+ ctypes.default_abi, ctypes.int,
+ result.VFYContext.ptr,
+ ctypes.unsigned_char.ptr,
+ ctypes.int
+ );
+
+ /*
+ * cryptohi.h
+ * extern SECStatus VFY_End(VFYContext *cx);
+ */
+ result.VFY_End = nsslib.declare(
+ "VFY_End",
+ ctypes.default_abi, ctypes.int,
+ result.VFYContext.ptr
+ );
+
+ /*
+ * keyhi.h
+ * extern CERTSubjectPublicKeyInfo *
+ * SECKEY_DecodeDERSubjectPublicKeyInfo(SECItem *spkider);
+ */
+ result.SECKEY_DecodeDERSubjectPublicKeyInfo = nsslib.declare(
+ "SECKEY_DecodeDERSubjectPublicKeyInfo",
+ ctypes.default_abi, result.CERTSubjectPublicKeyInfo.ptr,
+ result.SECItem.ptr
+ );
+
+ /*
+ * keyhi.h
+ * extern void SECKEY_DestroySubjectPublicKeyInfo(CERTSubjectPublicKeyInfo *spki);
+ */
+ result.SECKEY_DestroySubjectPublicKeyInfo = nsslib.declare(
+ "SECKEY_DestroySubjectPublicKeyInfo",
+ ctypes.default_abi, ctypes.void_t,
+ result.CERTSubjectPublicKeyInfo.ptr
+ );
+
+ /*
+ * keyhi.h
+ * extern SECKEYPublicKey *
+ * SECKEY_ExtractPublicKey(CERTSubjectPublicKeyInfo *);
+ */
+ result.SECKEY_ExtractPublicKey = nsslib.declare(
+ "SECKEY_ExtractPublicKey",
+ ctypes.default_abi, result.SECKEYPublicKey.ptr,
+ result.CERTSubjectPublicKeyInfo.ptr
+ );
+
+ /*
+ * keyhi.h
+ * extern void SECKEY_DestroyPublicKey(SECKEYPublicKey *key);
+ */
+ result.SECKEY_DestroyPublicKey = nsslib.declare(
+ "SECKEY_DestroyPublicKey",
+ ctypes.default_abi, ctypes.void_t,
+ result.SECKEYPublicKey.ptr
+ );
+
+ // Convenience method
+ result.getSECItem = function(data)
+ {
+ var dataArray = new ctypes.ArrayType(ctypes.unsigned_char, data.length)();
+ for (let i = 0; i < data.length; i++)
+ dataArray[i] = data.charCodeAt(i) % 256;
+ return new result.SECItem(result.siUTF8String, dataArray, dataArray.length);
+ };
+
+ return result;
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ // Expected, ctypes isn't supported in Gecko 1.9.2
+ return null;
+ }
+});
+
+if ("@mozilla.org/messenger/headerparser;1" in Cc)
+ XPCOMUtils.defineLazyServiceGetter(Utils, "headerParser", "@mozilla.org/messenger/headerparser;1", "nsIMsgHeaderParser");
+else
+ Utils.headerParser = null;
diff --git a/abprime/modules/moz.build b/abprime/modules/moz.build
new file mode 100644
index 00000000..fbe3f6e1
--- /dev/null
+++ b/abprime/modules/moz.build
@@ -0,0 +1,29 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_PP_JS_MODULES += [
+ 'AppIntegration.jsm',
+ 'Bootstrap.jsm',
+ 'ContentPolicy.jsm',
+ 'ContentPolicyRemote.jsm',
+ 'ElemHide.jsm',
+ 'ElemHideRemote.jsm',
+ 'FilterClasses.jsm',
+ 'FilterListener.jsm',
+ 'FilterNotifier.jsm',
+ 'FilterStorage.jsm',
+ 'IO.jsm',
+ 'Matcher.jsm',
+ 'ObjectTabs.jsm',
+ 'Prefs.jsm',
+ 'Public.jsm',
+ 'RequestNotifier.jsm',
+ 'SubscriptionClasses.jsm',
+ 'Sync.jsm',
+ 'Synchronizer.jsm',
+ 'TimeLine.jsm',
+ 'Utils.jsm',
+]
+
diff --git a/abprime/moz.build b/abprime/moz.build
new file mode 100644
index 00000000..fc271f72
--- /dev/null
+++ b/abprime/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'addon',
+ 'components',
+ 'content',
+ 'locale',
+ 'modules',
+ 'skin',
+]
+
diff --git a/abprime/moz.configure b/abprime/moz.configure
new file mode 100644
index 00000000..94fa6229
--- /dev/null
+++ b/abprime/moz.configure
@@ -0,0 +1,68 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# We could use confvars.sh and configure.in but for some reason it gets a bit messy...
+# Just use what we have.. While UXP may reduce the importance of moz.configure we
+# likely will never be able to eliminate it entirely... Oh well.
+
+# Disables building the platform code
+# NOTE: This negates any possiblity of an XPIDL or Binary XPCOM Components
+set_config('MOZ_DISABLE_PLATFORM', '1')
+
+# Minimum required by moz.configure since we don't want everything in
+# toolkit/moz.configure when the platform isn't built
+set_config('MOZ_PACKAGER_FORMAT', 'omni')
+set_config('MOZ_JAR_MAKER_FILE_FORMAT', 'flat')
+
+# moz.configure won't allow setting normal variables in a global context so
+# create a function that can be called to get specific info
+def confvars(key):
+ metadata = {
+ 'name': 'ABPrime',
+ 'id': 'abprime@projects.binaryoutcast.com',
+ 'version': '1.0.7',
+ 'creator': 'Binary Outcast',
+ 'description': 'Bootstrapped adblocking is yesterday!',
+ 'slug': 'abprime',
+ 'chrome': 'abprime',
+ 'targetApp': 'Pale Moon',
+ 'targetID': '{8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}',
+ 'targetMinVer': '27.0.0',
+ 'targetMaxVer': '28.*',
+ 'basilisk': '1'
+ }
+
+ return metadata[key]
+
+# These will set the config/substs
+set_config('ADDON_NAME', confvars('name'))
+set_config('ADDON_ID', confvars('id'))
+set_config('ADDON_VERSION', confvars('version'))
+set_config('ADDON_AUTHOR', confvars('creator'))
+set_config('ADDON_SHORT_DESC', confvars('description'))
+set_config('ADDON_XPI_NAME', confvars('slug'))
+set_config('ADDON_CHROME_NAME', confvars('chrome'))
+set_config('ADDON_TARGET_APP_NAME', confvars('targetApp'))
+set_config('ADDON_TARGET_APP_ID', confvars('targetID'))
+set_config('ADDON_TARGET_APP_MINVER', confvars('targetMinVer'))
+set_config('ADDON_TARGET_APP_MAXVER', confvars('targetMaxVer'))
+set_config('ADDON_TARGET_BASILISK', confvars('basilisk'))
+
+# These will set defines
+# Should weed them down to just what is required which I believe
+# is just ADDON_TARGET_BASILISK
+set_define('ADDON_NAME', confvars('name'))
+set_define('ADDON_ID', confvars('id'))
+set_define('ADDON_VERSION', confvars('version'))
+set_define('ADDON_AUTHOR', confvars('creator'))
+set_define('ADDON_SHORT_DESC', confvars('description'))
+set_define('ADDON_XPI_NAME', confvars('slug'))
+set_define('ADDON_CHROME_NAME', confvars('chrome'))
+set_define('ADDON_TARGET_APP_NAME', confvars('targetApp'))
+set_define('ADDON_TARGET_APP_ID', confvars('targetID'))
+set_define('ADDON_TARGET_APP_MINVER', confvars('targetMinVer'))
+set_define('ADDON_TARGET_APP_MAXVER', confvars('targetMaxVer'))
+set_define('ADDON_TARGET_BASILISK', confvars('basilisk'))
diff --git a/abprime/skin/about.css b/abprime/skin/about.css
new file mode 100644
index 00000000..a9657e9b
--- /dev/null
+++ b/abprime/skin/about.css
@@ -0,0 +1,40 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#mainBox
+{
+ visibility: hidden;
+}
+#mainBox[loaded]
+{
+ visibility: visible;
+}
+
+#title
+{
+ font-size: 48px;
+ font-weight: bold;
+}
+
+#mainGroup
+{
+ margin-top: 15px;
+ width: 450px;
+ height: 400px;
+ overflow: auto;
+}
+
+#homepageTitle, #authorsTitle, #contributorsTitle, #subscriptionAuthorsTitle, #translatorsTitle
+{
+ font-weight: bold;
+}
+
+#description, #homepage, #authorsBox, #contributorsBox, #subscriptionAuthorsBox
+{
+ margin-bottom: 10px;
+}
diff --git a/abprime/skin/abp-icon-big.png b/abprime/skin/abp-icon-big.png
new file mode 100644
index 00000000..fbd57c2c
Binary files /dev/null and b/abprime/skin/abp-icon-big.png differ
diff --git a/abprime/skin/abp-status-16.png b/abprime/skin/abp-status-16.png
new file mode 100644
index 00000000..202b4ad2
Binary files /dev/null and b/abprime/skin/abp-status-16.png differ
diff --git a/abprime/skin/abp-status.png b/abprime/skin/abp-status.png
new file mode 100644
index 00000000..c8f75ffc
Binary files /dev/null and b/abprime/skin/abp-status.png differ
diff --git a/abprime/skin/checkbox.png b/abprime/skin/checkbox.png
new file mode 100644
index 00000000..ba6c53ed
Binary files /dev/null and b/abprime/skin/checkbox.png differ
diff --git a/abprime/skin/close.png b/abprime/skin/close.png
new file mode 100644
index 00000000..a4aaf5c4
Binary files /dev/null and b/abprime/skin/close.png differ
diff --git a/abprime/skin/composer.css b/abprime/skin/composer.css
new file mode 100644
index 00000000..b18f9163
--- /dev/null
+++ b/abprime/skin/composer.css
@@ -0,0 +1,66 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+@namespace html url("http://www.w3.org/1999/xhtml");
+
+/*
+ * Force left-to-right everywhere where we are displaying addresses
+ */
+.suggestion > .radio-label-box:-moz-locale-dir(rtl),
+html|*.textbox-input:-moz-locale-dir(rtl)
+{
+ direction: ltr;
+ text-align: end;
+}
+
+#patternGroup {
+ overflow: auto;
+}
+
+#anchorGroup {
+ padding-left: 20px;
+}
+
+#typeGroupLabel {
+ margin-top: 10px;
+}
+
+#typeGroup {
+ overflow: auto;
+ margin-bottom: 10px
+}
+
+:root:not([advancedMode="true"]) #options {
+ display: none;
+}
+
+#disabledWarning, #groupDisabledWarning, #regexpWarning, #shortpatternWarning, #matchWarning {
+ color: #E00000;
+}
+
+#disabledWarning > *, #groupDisabledWarning > * {
+ margin: 0px;
+ font-size: inherit;
+}
+
+.text-link {
+ font-size: 80%;
+ -moz-user-focus: ignore;
+}
+
+.help {
+ color: #0000E0;
+ border-bottom: 1px dotted #0000E0;
+ cursor: help;
+ margin: 0px;
+ padding: 0px;
+}
+
+tooltip {
+ /* Gecko 1.8.1 doesn't support multiline tooltips :-( */
+ max-width: none;
+}
diff --git a/abprime/skin/filters.css b/abprime/skin/filters.css
new file mode 100644
index 00000000..1432ff5d
--- /dev/null
+++ b/abprime/skin/filters.css
@@ -0,0 +1,208 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#buttons
+{
+ margin-top: 10px;
+}
+
+#noSubscriptions
+{
+ font-style: italic;
+}
+
+.localeMatch
+{
+ font-weight: bold;
+}
+.selectSubscriptionItem
+{
+ margin: 0px;
+}
+
+.subscription
+{
+ padding: 5px;
+}
+.subscription:not(:last-child)
+{
+ border-bottom: 1px solid rgba(0, 0, 0, 0.25);
+}
+.subscription:not([selected="true"]) > .disabled
+{
+}
+
+.subscription:not([selected="true"]) > .disabled .titleBox
+{
+ color: #808080;
+}
+.subscription:not([selected="true"]) > .disabled .status
+{
+ color: #808080;
+}
+
+.titleBox .title,
+.titleBox > .titleEditor
+{
+ font-weight: bold;
+}
+
+.subscription description, .subscription textbox
+{
+ margin: 0px !important;
+ padding: 0px !important;
+ border-width: 0px !important;
+ -moz-appearance: none !important;
+}
+
+.subscription .link
+{
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.subscription .warning
+{
+ color: #FF0000;
+}
+
+.enabledCheckbox
+{
+ padding: 2px;
+ -moz-margin-end: 10px;
+}
+.enabledCheckbox:focus
+{
+ outline: 1px dotted gray;
+}
+.enabledCheckbox .checkbox-label-box
+{
+ display: none;
+}
+
+.actionButton
+{
+ font: -moz-info;
+}
+
+splitter
+{
+ border-width: 0px !important;
+}
+
+#filtersTooltip
+{
+ max-width: none;
+}
+
+.tooltipLabel
+{
+ font-weight: bold;
+ -moz-margin-end: 10px;
+}
+
+#tooltip-additional
+{
+ color: #C00000;
+ margin-top: 10px;
+}
+
+tree
+{
+ margin: 0px;
+}
+
+#col-slow {
+ text-align: center;
+}
+
+#col-hitcount, #col-lasthit {
+ text-align: right;
+}
+
+#col-hitcount
+{
+ min-width: 60px;
+}
+#col-enabled
+{
+ min-width: 48px;
+}
+#col-slow
+{
+ min-width: 30px;
+}
+
+/*
+ * Force left-to-right for filter text but not comments
+ */
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-invalid),
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-whitelist),
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-filterlist),
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter, type-elemhide)
+{
+ direction: ltr;
+ text-align: end;
+}
+
+treechildren::-moz-tree-cell-text(col-filter, dummy-true)
+{
+ font-style: italic;
+}
+
+treechildren::-moz-tree-cell-text(col-filter, type-whitelist, selected-false)
+{
+ color: #008000;
+}
+
+treechildren::-moz-tree-cell-text(col-filter, type-elemhide, selected-false)
+{
+ color: #000080;
+}
+
+treechildren::-moz-tree-cell-text(col-slow)
+{
+ font-size: 0px;
+}
+
+treechildren::-moz-tree-cell-text(col-filter, disabled-true, selected-false)
+{
+ color: #808080;
+}
+
+treechildren::-moz-tree-cell-text(col-filter, type-comment, selected-false)
+{
+ color: #808080;
+}
+
+treechildren::-moz-tree-cell-text(col-filter, type-invalid, selected-false)
+{
+ color: #C00000;
+}
+
+treechildren::-moz-tree-image(col-enabled, disabled-true)
+{
+ list-style-image: url(checkbox.png);
+ -moz-image-region: rect(13px 13px 26px 0px);
+}
+
+treechildren::-moz-tree-image(col-enabled, disabled-false)
+{
+ list-style-image: url(checkbox.png);
+ -moz-image-region: rect(0px 13px 13px 0px);
+}
+
+treechildren::-moz-tree-image(col-slow, slow-true)
+{
+ list-style-image: url(slow.png);
+}
+
+.findbar-highlight
+{
+ display: none;
+}
diff --git a/abprime/skin/firstRun.css b/abprime/skin/firstRun.css
new file mode 100644
index 00000000..366ddb28
--- /dev/null
+++ b/abprime/skin/firstRun.css
@@ -0,0 +1,52 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+:root
+{
+ max-width: 500px;
+ background-image: url(abp-icon-big.png);
+ background-repeat: no-repeat;
+ background-position: 95% 5%;
+ font-size: 130%;
+}
+
+.sectionTitle
+{
+ font-weight: bold;
+}
+
+#changeDescription > .text-link
+{
+ margin: 0px;
+}
+
+#listNameContainer,
+#listNone
+{
+ margin: 10px 40px;
+}
+
+#listNone
+{
+ font-style: italic;
+}
+
+#acceptableAds
+{
+ font-size: 90%;
+ font-weight: bold;
+}
+#acceptableAds > *
+{
+ font-weight: normal;
+}
+
+.sectionContainer
+{
+ margin: 1em 2em;
+}
diff --git a/abprime/skin/item-state.png b/abprime/skin/item-state.png
new file mode 100644
index 00000000..a4943790
Binary files /dev/null and b/abprime/skin/item-state.png differ
diff --git a/abprime/skin/jar.mn b/abprime/skin/jar.mn
new file mode 100644
index 00000000..2a44ae2d
--- /dev/null
+++ b/abprime/skin/jar.mn
@@ -0,0 +1,25 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% skin @ADDON_CHROME_NAME@ classic/1.0 chrome/skin/
+ skin/about.css
+ skin/abp-icon-big.png
+ skin/abp-status-16.png
+ skin/abp-status.png
+ skin/checkbox.png
+ skin/close.png
+ skin/composer.css
+ skin/filters.css
+ skin/firstRun.css
+ skin/item-state.png
+ skin/overlay.css
+ skin/sendReport.css
+ skin/sidebar.css
+ skin/slow.png
+ skin/subscriptionSelection.css
+
+% style chrome://global/content/customizeToolbar.xul chrome://@ADDON_CHROME_NAME@/skin/overlay.css
\ No newline at end of file
diff --git a/abprime/skin/moz.build b/abprime/skin/moz.build
new file mode 100644
index 00000000..e0eb66aa
--- /dev/null
+++ b/abprime/skin/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/abprime/skin/overlay.css b/abprime/skin/overlay.css
new file mode 100644
index 00000000..92aa982d
--- /dev/null
+++ b/abprime/skin/overlay.css
@@ -0,0 +1,165 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#abp-status
+{
+ cursor: pointer;
+}
+
+toolbar[iconsize="small"] #abp-toolbarbutton,
+#PersonalToolbar #abp-toolbarbutton,
+#header-view-toolbar > #abp-toolbarbutton,
+#abp-status {
+ list-style-image: url("abp-status-16.png");
+ -moz-image-region: rect(0px, 16px, 16px, 0px);
+}
+toolbar[iconsize="small"] #abp-toolbarbutton[abpstate="disabled"],
+#PersonalToolbar #abp-toolbarbutton[abpstate="disabled"],
+#header-view-toolbar > #abp-toolbarbutton[abpstate="disabled"],
+#abp-status[abpstate="disabled"],
+toolbar[iconsize="small"] #abp-toolbarbutton[abpstate="whitelisted"],
+#PersonalToolbar #abp-toolbarbutton[abpstate="whitelisted"],
+#header-view-toolbar > #abp-toolbarbutton[abpstate="whitelisted"],
+#abp-status[abpstate="whitelisted"] {
+ -moz-image-region: rect(16px, 16px, 32px, 0px);
+}
+
+#abp-toolbar-popup {
+ list-style-image: none;
+ -moz-image-region: rect(0px, 0px, 0px, 0px);
+}
+
+toolbox[vertical="true"] toolbar #abp-toolbarbutton dropmarker {
+ display: none !important;
+}
+
+menuitem[default="true"] {
+ font-weight: bold;
+}
+
+#abp-toolbarbutton,
+#abp-site-info {
+ list-style-image: url("abp-status.png");
+ -moz-image-region: rect(0px, 24px, 24px, 0px);
+}
+#abp-toolbarbutton[abpstate="disabled"],
+#abp-toolbarbutton[abpstate="whitelisted"],
+#abp-site-info[abpaction="enable"],
+#abp-site-info[abpaction="enable_site"] {
+ -moz-image-region: rect(24px, 24px, 48px, 0px);
+}
+
+/* Hack: force the label to be displayed below icon for type="menu" */
+#abp-toolbarbutton[type="menu"]
+{
+ -moz-box-orient: horizontal;
+}
+toolbar[mode="full"]:not([labelalign="end"]) #abp-toolbarbutton[type="menu"]
+{
+ -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#menu-vertical");
+}
+
+/* Thunderbird-specific toolbar icon styles */
+#header-view-toolbar > #abp-toolbarbutton
+{
+ -moz-appearance: dualbutton;
+ padding: 0px !important;
+}
+
+/* Hide toolbar icon text in Thunderbird to save space */
+#header-view-toolbar > #abp-toolbarbutton .toolbarbutton-text
+{
+ display: none;
+}
+
+/* SeaMonkey expects the icon to be rather large, add margin */
+#mail-toolbox #abp-toolbarbutton .toolbarbutton-icon
+{
+ margin-top: 5px;
+}
+
+#abp-status-image {
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+#abp-site-info .pageaction-image {
+ width: 32px;
+ height: 32px;
+ padding: 4px;
+}
+
+#abp-toolbarbutton > toolbarbutton {
+ /* Argh, Songbird defines image region directly on the anonymous toolbarbutton element */
+ -moz-image-region: inherit !important;
+}
+
+#abp-tooltip {
+ max-width: none;
+}
+
+#abp-tooltip label {
+ font-weight: bold;
+ margin-bottom: 0px;
+}
+
+#abp-tooltip description:not([hidden="true"])+label {
+ margin-top: 10px;
+}
+
+#abp-sidebar-title {
+ padding-left: 4px;
+}
+
+#abp-sidebar-toolbar {
+ display: -moz-box !important;
+ visibility: visible !important;
+}
+
+#abp-sidebar-close {
+ padding: 4px 2px;
+ border-style: none !important;
+ -moz-user-focus: normal;
+ list-style-image: url("close.png");
+ -moz-appearance: none;
+ -moz-image-region: rect(0px, 14px, 14px, 0px);
+}
+
+#abp-sidebar-close:hover {
+ -moz-image-region: rect(0px, 28px, 14px, 14px);
+}
+
+#abp-sidebar-close:hover:active {
+ -moz-image-region: rect(0px, 42px, 14px, 28px);
+}
+
+.abp-contributebutton
+{
+ margin-top: 20px;
+}
+
+.abp-contributebutton-btn
+{
+ font: -moz-info;
+ margin-left: 40px;
+ margin-right: 40px;
+}
+
+.abp-contributebutton-close
+{
+ border-style: none !important;
+ -moz-user-focus: normal;
+ list-style-image: url("close.png");
+ -moz-appearance: none;
+ -moz-image-region: rect(0px, 14px, 14px, 0px);
+}
+
+.abp-contributebutton-close:hover
+{
+ -moz-image-region: rect(0px, 28px, 14px, 14px);
+}
diff --git a/abprime/skin/sendReport.css b/abprime/skin/sendReport.css
new file mode 100644
index 00000000..4051104b
--- /dev/null
+++ b/abprime/skin/sendReport.css
@@ -0,0 +1,111 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+.wizard-header
+{
+ -moz-binding: url(chrome://adblockplus/content/sendReport.xul#headerBinding) !important;
+ padding: 10px 5px !important;
+}
+
+.progressLabel
+{
+ margin: 5px 0px;
+ text-align: center;
+ font-size: 110%;
+ font-weight: normal;
+}
+
+.progressLabel.active
+{
+ font-weight: bold;
+}
+
+progressmeter
+{
+ margin-top: 100px;
+}
+
+.radioDescription
+{
+ -moz-margin-start: 32px;
+}
+
+radio, checkbox, .topLabel, #dataDeck
+{
+ margin-top: 15px;
+}
+
+#recentReports
+{
+ margin-top: 15px;
+}
+
+#recentReportsList
+{
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+#outdatedSubscriptionsList
+{
+ margin: 10px 20px;
+}
+
+#issuesBox
+{
+ overflow: auto;
+}
+
+#issuesChangeMessage
+{
+ color: red;
+}
+
+#screenshotButtons
+{
+ margin-top: 10px;
+}
+
+#screenshotBox
+{
+ overflow-y: scroll;
+}
+
+#commentLengthWarning
+{
+ color: red;
+}
+
+#commentLengthWarning[visible="false"]
+{
+ visibility: hidden;
+}
+
+/*
+ * Force left-to-right everywhere where we are displaying addresses
+ */
+#data:-moz-locale-dir(rtl)
+{
+ direction: ltr;
+}
+
+#sendReportError
+{
+ color: red;
+ font-size: 150%;
+}
+
+#sendReportErrorLinks, #typeWarningTextLink
+{
+ margin: 0px;
+}
+
+#sendReportErrorBox
+{
+ margin-bottom: 10px;
+}
diff --git a/abprime/skin/sidebar.css b/abprime/skin/sidebar.css
new file mode 100644
index 00000000..eca31722
--- /dev/null
+++ b/abprime/skin/sidebar.css
@@ -0,0 +1,99 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#suggestionsList {
+ margin: 0px;
+}
+
+#detachButton, #reattachButton:not([disabled="true"]) {
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+#reattachButton[disabled="true"] {
+ color: GrayText;
+}
+
+#detachButton, #reattachButton {
+ font-size: 90%;
+}
+
+tooltip {
+ max-width: none;
+}
+
+#tooltipPreview {
+ margin:10px;
+ max-width: 300px;
+ max-height: 300px;
+}
+
+#tooltip label {
+ font-weight: bold;
+}
+
+#contextBlock,
+#contextWhitelist {
+ font-weight: bold;
+}
+
+#state {
+ min-width: 16px;
+}
+
+#size {
+ text-align: end;
+}
+
+/*
+ * Force left-to-right everywhere where we are displaying addresses
+ */
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-filter),
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-address),
+treechildren:-moz-locale-dir(rtl)::-moz-tree-cell(col-size)
+{
+ direction: ltr;
+ text-align: end;
+}
+
+.disabledTextLabel
+{
+ font-style: italic;
+}
+
+treechildren::-moz-tree-cell-text(state-filtered, selected-false),
+treechildren::-moz-tree-cell-text(state-hidden, selected-false) {
+ color: #C00000;
+}
+treechildren::-moz-tree-cell-text(state-whitelisted, selected-false) {
+ color: #008000;
+}
+
+treechildren::-moz-tree-image(col-state, dummy-false)
+{
+ list-style-image: url(item-state.png);
+ -moz-image-region: rect(0px 10px 10px 0px);
+ -moz-margin-start: 3px;
+}
+treechildren::-moz-tree-image(col-state, filter-disabled-true, dummy-false) {
+ -moz-image-region: rect(10px 10px 20px 0px);
+}
+treechildren::-moz-tree-image(col-state, state-filtered, dummy-false),
+treechildren::-moz-tree-image(col-state, state-hidden, dummy-false) {
+ -moz-image-region: rect(20px 10px 30px 0px);
+}
+treechildren::-moz-tree-image(col-state, state-whitelisted, dummy-false) {
+ -moz-image-region: rect(30px 10px 40px 0px);
+}
+
+treechildren::-moz-tree-cell-text(col-filter, state-hidden, selected-false) {
+ color: #000080;
+}
+treechildren::-moz-tree-cell-text(col-filter, filter-disabled-true, selected-false) {
+ color: #C0C0C0;
+}
diff --git a/abprime/skin/slow.png b/abprime/skin/slow.png
new file mode 100644
index 00000000..8463e9e0
Binary files /dev/null and b/abprime/skin/slow.png differ
diff --git a/abprime/skin/subscriptionSelection.css b/abprime/skin/subscriptionSelection.css
new file mode 100644
index 00000000..6716d569
--- /dev/null
+++ b/abprime/skin/subscriptionSelection.css
@@ -0,0 +1,58 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+dialog
+{
+ width: 550px;
+}
+
+*[invisible="true"]
+{
+ visibility: hidden;
+}
+
+#supplementMessage
+{
+ color: #F00000;
+}
+
+.localeMatch
+{
+ font-weight: bold;
+}
+
+#all-subscriptions-loading
+{
+ margin: 50px;
+}
+
+#all-subscriptions
+{
+ min-height: 200px;
+}
+#all-subscriptions > richlistitem > .variant
+{
+ width: 200px;
+}
+#all-subscriptions > richlistitem:not(:first-child) > .subscriptionTitle,
+#all-subscriptions > richlistitem:not(:first-child) > .subscriptionTitle + .variant
+{
+ border-top: 1px dashed black;
+ margin-top: 0px;
+ padding-top: 4px;
+}
+
+#supplementMessage
+{
+ margin-top: 5px;
+}
+#supplementMessage > label
+{
+ margin-left: 0px;
+ margin-right: 0px;
+}
diff --git a/communicator/communicator.mozbuild b/communicator/communicator.mozbuild
deleted file mode 100644
index 0978601c..00000000
--- a/communicator/communicator.mozbuild
+++ /dev/null
@@ -1,23 +0,0 @@
-# vim: set filetype=python:
-# 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/.
-
-if CONFIG['MOZ_COMPOSER']:
- DIRS += ['/editor/ui']
-
-if CONFIG['MOZ_MAILNEWS']:
- DIRS += [
- '/modules/ldap',
- '/modules/mork',
- '/mailnews',
- ]
-
-if CONFIG['MOZ_CALENDAR']:
- DIRS += [
- '/modules/libical',
- '/calendar/lightning',
- '/calendar/timezones'
- ]
-
-DIRS += ['/communicator']
\ No newline at end of file
diff --git a/inspector/addon/inspector.js b/inspector/addon/inspector.js
new file mode 100644
index 00000000..46724858
--- /dev/null
+++ b/inspector/addon/inspector.js
@@ -0,0 +1,14 @@
+/* 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/. */
+
+pref("inspector.blink.border-color", "#CC0000");
+pref("inspector.blink.border-width", 2);
+pref("inspector.blink.duration", 1200);
+pref("inspector.blink.on", true);
+pref("inspector.blink.speed", 100);
+pref("inspector.blink.invert", false);
+pref("inspector.dom.showAnon", true);
+pref("inspector.dom.showWhitespaceNodes", true);
+pref("inspector.dom.showAccessibleNodes", false);
+pref("inspector.dom.showProcessingInstructions", true);
diff --git a/inspector/addon/install.rdf b/inspector/addon/install.rdf
new file mode 100644
index 00000000..a7d6bb2d
--- /dev/null
+++ b/inspector/addon/install.rdf
@@ -0,0 +1,36 @@
+
+
+
+
+#filter substitution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/addon/moz.build b/inspector/addon/moz.build
new file mode 100644
index 00000000..f8909dd7
--- /dev/null
+++ b/inspector/addon/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+FINAL_TARGET_PP_FILES += ['install.rdf']
+
+FINAL_TARGET_FILES.chrome.icons.default += [
+ 'winInspectorMain.ico',
+ 'winInspectorMain.xpm',
+ 'winInspectorMain16.xpm',
+]
+
+FINAL_TARGET_FILES.defaults.preferences += ['inspector.js']
\ No newline at end of file
diff --git a/inspector/addon/winInspectorMain.ico b/inspector/addon/winInspectorMain.ico
new file mode 100644
index 00000000..78efb3ac
Binary files /dev/null and b/inspector/addon/winInspectorMain.ico differ
diff --git a/inspector/addon/winInspectorMain.xpm b/inspector/addon/winInspectorMain.xpm
new file mode 100644
index 00000000..d37c5f4d
--- /dev/null
+++ b/inspector/addon/winInspectorMain.xpm
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/* XPM */
+static char * winInspectorMain_xpm[] = {
+"32 32 6 1",
+" c None",
+". c #000000",
+"+ c #FFB300",
+"@ c #FF7500",
+"# c #FF8C00",
+"$ c #FF0000",
+"................................",
+"................................",
+"..++++++++++++++++++++++++++++..",
+"..++++++++++++++++++++++++++++..",
+"..++++++++++........++++++++++..",
+"..++++++++++........++++++++++..",
+"..++++++....@@@@@@@@....++++++..",
+"..++++++....@@@@@@@@....++++++..",
+"..++++++..@@@@@@@@@@@@..++++++..",
+"..++++++..@@@@@@@@@@@@..++++++..",
+"..++++..@@@@@@@@@@@@@@@@..++++..",
+"..++++..@@@@@@@@@@@@@@@@..++++..",
+"..++++..@@@@@@@@@@@@@@@@..++++..",
+"..++++..@@@@@@@@@@@@@@@@..++++..",
+"..++++..@@@@@@@@@@@@##@@..++++..",
+"..++++..@@@@@@@@@@@@##@@..++++..",
+"..++++..@@@@@@@@@@@@##@@..++++..",
+"..++++..@@@@@@@@@@@@##@@..++++..",
+"..++++++..@@@@@@####$$..++++++..",
+"..++++++..@@@@@@####$$..++++++..",
+"..++++++....@@@@@@@@......++++..",
+"..++++++....@@@@@@@@......++++..",
+"..++++++++++........++......++..",
+"..++++++++++........++......++..",
+"..++++++++++++++++++++++........",
+"..++++++++++++++++++++++........",
+"..++++++++++++++++++++++++......",
+"..++++++++++++++++++++++++......",
+"..++++++++++++++++++++++++++....",
+"..++++++++++++++++++++++++++....",
+"................................",
+"................................"};
diff --git a/inspector/addon/winInspectorMain16.xpm b/inspector/addon/winInspectorMain16.xpm
new file mode 100644
index 00000000..ed600d1f
--- /dev/null
+++ b/inspector/addon/winInspectorMain16.xpm
@@ -0,0 +1,29 @@
+/* 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/. */
+
+/* XPM */
+static char * winInspectorMain16_xpm[] = {
+"16 16 6 1",
+" c None",
+". c #000000",
+"+ c #FFAD00",
+"@ c #FF6D00",
+"# c #FF8500",
+"$ c #FF0000",
+"................",
+".++++++++++++++.",
+".+++++....+++++.",
+".+++..@@@@..+++.",
+".+++.@@@@@@.+++.",
+".++.@@@@@@@@.++.",
+".++.@@@@@@@@.++.",
+".++.@@@@@@#@.++.",
+".++.@@@@@@#@.++.",
+".+++.@@@##$.+++.",
+".+++..@@@@...++.",
+".+++++....+...+.",
+".+++++++++++....",
+".++++++++++++...",
+".+++++++++++++..",
+"................"};
diff --git a/inspector/app.mozbuild b/inspector/app.mozbuild
new file mode 100644
index 00000000..be6f2be4
--- /dev/null
+++ b/inspector/app.mozbuild
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# 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/.
+
+if not CONFIG['MOZ_DISABLE_PLATFORM']:
+ error('Please add --disable-platform to your mozconfig')
+ #include('/toolkit/toolkit.mozbuild')
+
+# Never add tier dirs after the application srcdir because they
+# apparently won't get packaged properly on Mac.
+DIRS += ['/inspector']
diff --git a/inspector/build.mk b/inspector/build.mk
new file mode 100644
index 00000000..125caf74
--- /dev/null
+++ b/inspector/build.mk
@@ -0,0 +1,7 @@
+# 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/.
+
+package:
+ cd $(DIST)/bin; \
+ zip -Dr9X ../${INSPECTOR_XPI_NAME}-${INSPECTOR_VERSION}.xpi * -x \*/.mkdir.done; \
diff --git a/inspector/components/initializer.js b/inspector/components/initializer.js
new file mode 100644
index 00000000..3552d26f
--- /dev/null
+++ b/inspector/components/initializer.js
@@ -0,0 +1,56 @@
+/*
+ * This Source Code is subject to the terms of the Mozilla Public License
+ * version 2.0 (the "License"). You can obtain a copy of the License at
+ * http://mozilla.org/MPL/2.0/.
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Application startup/shutdown observer, triggers init()/uninit() methods
+ * @constructor
+ */
+function Initializer() {}
+Initializer.prototype =
+{
+ classDescription: "DOMi initializer",
+ contractID: "@mozilla.org/domi/startup;1",
+ classID: Components.ID("{87fcf1f0-959f-4ee7-abba-889478e93775}"),
+ _xpcom_categories: [{ category: "app-startup", service: true }],
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ observe: function(subject, topic, data)
+ {
+ let observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
+ switch (topic)
+ {
+ case "app-startup":
+ observerService.addObserver(this, "profile-after-change", true);
+ break;
+ case "profile-after-change":
+ observerService.addObserver(this, "quit-application", true);
+ Cu.import("resource://inspector/InspectElement.jsm");
+ InspectElement.init();
+ break;
+ case "quit-application":
+ try {
+ // This will fail if component was added via chrome.manifest (Gecko 2.0)
+ observerService.removeObserver(this, "profile-after-change");
+ }catch(e) {}
+ observerService.removeObserver(this, "quit-application");
+ InspectElement.uninit();
+ break;
+ }
+ }
+};
+
+if (XPCOMUtils.generateNSGetFactory)
+ var NSGetFactory = XPCOMUtils.generateNSGetFactory([Initializer]);
+else
+ var NSGetModule = XPCOMUtils.generateNSGetModule([Initializer]);
diff --git a/inspector/components/inspector-cmdline.js b/inspector/components/inspector-cmdline.js
new file mode 100644
index 00000000..fa2293a8
--- /dev/null
+++ b/inspector/components/inspector-cmdline.js
@@ -0,0 +1,60 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
+const nsISupportsString = Components.interfaces.nsISupportsString;
+const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
+
+function InspectorCmdLineHandler() {}
+InspectorCmdLineHandler.prototype =
+{
+ classDescription: "DOM Inspector Command Line Handler",
+ classID: Components.ID("{38293526-6b13-4d4f-a075-71939435b408}"),
+ contractID: "@mozilla.org/commandlinehandler/general-startup;1?type=inspector",
+ /* Needed for XPCOMUtils NSGetModule */
+ _xpcom_categories: [{category: "command-line-handler",
+ entry: "m-inspector"}],
+
+ /* nsISupports */
+ QueryInterface: XPCOMUtils.generateQI([nsICommandLineHandler]),
+
+ /* nsICommandLineHandler */
+ handle : function handler_handle(cmdLine) {
+ var args = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(nsISupportsString);
+ try {
+ var uristr = cmdLine.handleFlagWithParam("inspector", false);
+ if (uristr == null)
+ return;
+ try {
+ args.data = cmdLine.resolveURI(uristr).spec;
+ }
+ catch (e) {
+ return;
+ }
+ }
+ catch (e) {
+ cmdLine.handleFlag("inspector", true);
+ }
+
+ var wwatch = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(nsIWindowWatcher);
+ wwatch.openWindow(null, "chrome://inspector/content/", "_blank",
+ "chrome,dialog=no,all", args);
+ },
+
+ helpInfo : " -inspector Open the DOM inspector.\n"
+};
+
+
+/**
+ * XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4).
+ * XPCOMUtils.generateNSGetModule is for Mozilla 1.9.0 (Firefox 3.0).
+ */
+if (XPCOMUtils.generateNSGetFactory)
+ var NSGetFactory = XPCOMUtils.generateNSGetFactory([InspectorCmdLineHandler]);
+else
+ var NSGetModule = XPCOMUtils.generateNSGetModule([InspectorCmdLineHandler]);
diff --git a/inspector/components/jar.mn b/inspector/components/jar.mn
new file mode 100644
index 00000000..6400488c
--- /dev/null
+++ b/inspector/components/jar.mn
@@ -0,0 +1,12 @@
+# 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/.
+
+[.] chrome.jar:
+% category command-line-handler m-inspector @mozilla.org/commandlinehandler/general-startup;1?type=inspector
+% component {38293526-6b13-4d4f-a075-71939435b408} components/inspector-cmdline.js
+% contract @mozilla.org/commandlinehandler/general-startup;1?type=inspector {38293526-6b13-4d4f-a075-71939435b408}
+
+% component {87fcf1f0-959f-4ee7-abba-889478e93775} components/initializer.js
+% contract @mozilla.org/domi/startup;1 {87fcf1f0-959f-4ee7-abba-889478e93775}
+% category profile-after-change @mozilla.org/domi/startup;1 @mozilla.org/domi/startup;1
\ No newline at end of file
diff --git a/inspector/components/moz.build b/inspector/components/moz.build
new file mode 100644
index 00000000..2e5dba61
--- /dev/null
+++ b/inspector/components/moz.build
@@ -0,0 +1,15 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_COMPONENTS += [
+ 'initializer.js',
+ 'inspector-cmdline.js',
+]
+
+# We don't want component sub-manifest files for extensions..
+# They don't always work correctly
+NO_JS_MANIFEST = True
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/inspector/confvars.configure b/inspector/confvars.configure
new file mode 100644
index 00000000..d67666b1
--- /dev/null
+++ b/inspector/confvars.configure
@@ -0,0 +1,30 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# Templates apperently ARE allowed to use set_config and set_define and can use loops
+# so take advantage of that to apply the old axion_cmake defines without the need of
+# confvars.sh or configure.in
+@template
+def ConfVars(mode='moz.configure'):
+ confvars = {
+ 'INSPECTOR_NAME': 'DOM Inspector',
+ 'INSPECTOR_ID': 'inspector@mozilla.org',
+ 'INSPECTOR_VERSION': '3.0.2',
+ 'INSPECTOR_AUTHOR': 'Binary Outcast',
+ 'INSPECTOR_SHORT_DESC': 'DOM Inspector is a tool that can be used to inspect and edit the live DOM of any web document or XUL application.',
+ 'INSPECTOR_XPI_NAME': 'domi',
+ 'INSPECTOR_CHROME_NAME': 'inspector',
+ }
+
+ if mode == 'moz.configure':
+ for key, value in confvars.iteritems():
+ set_config(key, value)
+ set_define(key, value)
+ elif mode == 'moz.build':
+ for key, value in confvars.iteritems():
+ DEFINES[key] = value
+ elif mode == 'id':
+ return confvars['INSPECTOR_ID']
\ No newline at end of file
diff --git a/inspector/content/Flasher.js b/inspector/content/Flasher.js
new file mode 100644
index 00000000..19eb8077
--- /dev/null
+++ b/inspector/content/Flasher.js
@@ -0,0 +1,391 @@
+/* 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/. */
+
+/***************************************************************
+* Flasher ---------------------------------------------------
+* Object for controlling a timed flashing animation which
+* paints a border around an element.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+//////////// global constants ////////////////////
+
+const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
+const INVERT = "filter: url(\"data:image/svg+xml;charset=utf8, %23invert\") !important; "
+
+////////////////////////////////////////////////////////////////////////////
+//// class Flasher
+
+function Flasher(aColor, aThickness, aDuration, aSpeed, aInvert)
+{
+ document.querySelector(HIGHLIGHTED_PSEUDO_CLASS);
+ this.mIOService = XPCU.getService("@mozilla.org/network/io-service;1", "nsIIOService");
+ this.mDOMUtils = XPCU.getService("@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
+ this.mShell = XPCU.getService("@mozilla.org/inspector/flasher;1", "inIFlasher") || this.mDOMUtils;
+ this.color = aColor;
+ this.thickness = aThickness;
+ this.invert = aInvert;
+ this.duration = aDuration;
+ this.mSpeed = aSpeed;
+}
+
+Flasher.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mFlashTimeout: null,
+ mElement: null,
+ mRegistryId: null,
+ mFlashes: 0,
+ mStartTime: 0,
+ mDOMUtils: null,
+ mWinUtils: null,
+ mStyleURI: null,
+ mColor: "#000000",
+ mInvert: false,
+ mThickness: 0,
+ mDuration: 0,
+ mSpeed: 0,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Properties
+
+ get flashing() { return this.mFlashTimeout != null; },
+
+ get element() { return this.mElement; },
+ set element(val)
+ {
+ if (val && val.nodeType == Node.ELEMENT_NODE) {
+ this.mElement = val;
+ this.mShell.scrollElementIntoView(val);
+ this.mWinUtils = val.ownerDocument.defaultView
+ .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindowUtils);
+ } else {
+ throw "Invalid node type.";
+ }
+ },
+
+ get color() { return this.mColor; },
+ set color(aVal)
+ {
+ var spacer = document.createElement("spacer");
+ spacer.style.color = aVal;
+ if (spacer.style.color) {
+ this.mStyleURI = null;
+ this.mColor = aVal;
+ }
+ return aVal;
+ },
+
+ get thickness() { return this.mThickness | 0; },
+ set thickness(aVal) { this.mStyleURI = null; return this.mThickness = aVal; },
+
+ get duration() { return this.mDuration; },
+ set duration(aVal) { this.mDuration = aVal; },
+
+ get speed() { return this.mSpeed; },
+ set speed(aVal) { this.mSpeed = aVal; },
+
+ get invert() { return !!this.mInvert; },
+ set invert(aVal) { this.mStyleURI = null; return this.mInvert = aVal; },
+
+ // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+ // :::::::::::::::::::: Methods ::::::::::::::::::::::::::::
+ // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+ start: function(aDuration, aSpeed, aHold)
+ {
+ if (!this.mStyleURI) {
+ var styleURI = "data:text/css;charset=utf-8," + HIGHLIGHTED_PSEUDO_CLASS +
+ " { outline: " + this.thickness + "px solid " +
+ encodeURIComponent(this.color) +
+ " !important; outline-offset: " + -this.thickness +
+ "px !important; " + (this.invert ? INVERT : "") + "}";
+ this.mStyleURI = this.mIOService.newURI(styleURI, null, null);
+ }
+
+ this.mWinUtils.loadSheet(this.mStyleURI, this.mWinUtils.AGENT_SHEET);
+ this.mUDuration = aDuration ? aDuration * 1000 : this.mDuration;
+ this.mUSpeed = aSpeed ? aSpeed : this.mSpeed;
+ this.mHold = aHold;
+ this.mFlashes = 0;
+ this.mStartTime = Date.now();
+ this.doFlash();
+ },
+
+ doFlash: function()
+ {
+ if (this.mHold || this.mFlashes & 1) {
+ this.paintOn();
+ } else {
+ this.paintOff();
+ }
+ this.mFlashes++;
+
+ if (this.mUDuration < 0 || Date.now() - this.mStartTime < this.mUDuration) {
+ this.mFlashTimeout = window.setTimeout(this.timeout, this.mUSpeed, this);
+ } else {
+ this.stop();
+ }
+ },
+
+ timeout: function(self)
+ {
+ self.doFlash();
+ },
+
+ stop: function()
+ {
+ if (this.flashing) {
+ this.mWinUtils.removeSheet(this.mStyleURI, this.mWinUtils.AGENT_SHEET);
+ window.clearTimeout(this.mFlashTimeout);
+ this.mFlashTimeout = null;
+ this.paintOff();
+ }
+ },
+
+ paintOn: function()
+ {
+ this.mDOMUtils.addPseudoClassLock(this.mElement, HIGHLIGHTED_PSEUDO_CLASS);
+ },
+
+ paintOff: function()
+ {
+ this.mDOMUtils.removePseudoClassLock(this.mElement, HIGHLIGHTED_PSEUDO_CLASS);
+ }
+
+};
+
+////////////////////////////////////////////////////////////////////////////
+//// class LegacyFlasher
+
+function LegacyFlasher(aColor, aThickness, aDuration, aSpeed, aInvert)
+{
+ this.mShell = XPCU.getService("@mozilla.org/inspector/flasher;1", "inIFlasher");
+ this.color = aColor;
+ this.mShell.thickness = aThickness;
+ this.mShell.invert = aInvert;
+ this.duration = aDuration;
+ this.mSpeed = aSpeed;
+}
+
+LegacyFlasher.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mFlashTimeout: null,
+ mElement:null,
+ mRegistryId: null,
+ mFlashes: 0,
+ mStartTime: 0,
+ mDuration: 0,
+ mSpeed: 0,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Properties
+
+ get flashing() { return this.mFlashTimeout != null; },
+
+ get element() { return this.mElement; },
+ set element(val)
+ {
+ if (val && val.nodeType == Node.ELEMENT_NODE) {
+ this.mElement = val;
+ this.mShell.scrollElementIntoView(val);
+ } else
+ throw "Invalid node type.";
+ },
+
+ get color() { return this.mShell.color; },
+ set color(aVal)
+ {
+ try {
+ this.mShell.color = aVal;
+ }
+ catch (e) { // Catch exception in case aVal is an invalid or empty value.
+ Components.utils.reportError(e);
+ }
+ return aVal;
+ },
+
+ get thickness() { return this.mShell.thickness; },
+ set thickness(aVal) { this.mShell.thickness = aVal; },
+
+ get duration() { return this.mDuration; },
+ set duration(aVal) { this.mDuration = aVal; },
+
+ get speed() { return this.mSpeed; },
+ set speed(aVal) { this.mSpeed = aVal; },
+
+ get invert() { return this.mShell.invert; },
+ set invert(aVal) { this.mShell.invert = aVal; },
+
+ // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+ // :::::::::::::::::::: Methods ::::::::::::::::::::::::::::
+ // :::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+ start: function(aDuration, aSpeed, aHold)
+ {
+ this.mUDuration = aDuration ? aDuration*1000 : this.mDuration;
+ this.mUSpeed = aSpeed ? aSpeed : this.mSpeed
+ this.mHold = aHold;
+ this.mFlashes = 0;
+ this.mStartTime = new Date();
+ this.doFlash();
+ },
+
+ doFlash: function()
+ {
+ if (this.mHold || this.mFlashes%2) {
+ this.paintOn();
+ } else {
+ this.paintOff();
+ }
+ this.mFlashes++;
+
+ if (this.mUDuration < 0 || new Date() - this.mStartTime < this.mUDuration) {
+ this.mFlashTimeout = window.setTimeout(this.timeout, this.mUSpeed, this);
+ } else {
+ this.stop();
+ }
+ },
+
+ timeout: function(self)
+ {
+ self.doFlash();
+ },
+
+ stop: function()
+ {
+ if (this.flashing) {
+ window.clearTimeout(this.mFlashTimeout);
+ this.mFlashTimeout = null;
+ this.paintOff();
+ }
+ },
+
+ paintOn: function()
+ {
+ this.mShell.drawElementOutline(this.mElement);
+ },
+
+ paintOff: function()
+ {
+ this.mShell.repaintElement(this.mElement);
+ }
+
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DOMIFlasher
+
+/**
+ * The special version of the flasher operating with DOM Inspector flasher
+ * preferences.
+ */
+function DOMIFlasher()
+{
+ this.init();
+}
+
+DOMIFlasher.prototype =
+{
+ //////////////////////////////////////////////////////////////////////////////
+ //// Public
+
+ get flashOnSelect() { return PrefUtils.getPref("inspector.blink.on"); },
+ set flashOnSelect(aVal) { PrefUtils.setPref("inspector.blink.on", aVal); },
+
+ get color() { return PrefUtils.getPref("inspector.blink.border-color"); },
+ set color(aVal) { PrefUtils.setPref("inspector.blink.border-color", aVal); },
+
+ get thickness() { return PrefUtils.getPref("inspector.blink.border-width"); },
+ set thickness(aVal) { PrefUtils.setPref("inspector.blink.border-width", aVal); },
+
+ get duration() { return PrefUtils.getPref("inspector.blink.duration"); },
+ set duration(aVal) { PrefUtils.setPref("inspector.blink.duration", aVal); },
+
+ get speed() { return PrefUtils.getPref("inspector.blink.speed"); },
+ set speed(aVal) { PrefUtils.setPref("inspector.blink.speed", aVal); },
+
+ get invert() { return PrefUtils.getPref("inspector.blink.invert"); },
+ set invert(aVal) { PrefUtils.setPref("inspector.blink.invert", aVal); },
+
+ flashElement: function DOMIFlasher_flashElement(aElement)
+ {
+ if (this.mFlasher.flashing)
+ this.mFlasher.stop();
+
+ this.mFlasher.element = aElement;
+ this.mFlasher.start();
+ },
+
+ flashElementOnSelect: function DOMIFlasher_flashElementOnSelect(aElement)
+ {
+ if (this.flashOnSelect) {
+ this.flashElement(aElement);
+ }
+ },
+
+ destroy: function DOMIFlasher_destroy()
+ {
+ PrefUtils.removeObserver("inspector.blink.", this);
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// Private
+
+ init: function DOMIFlasher_init()
+ {
+ try {
+ this.mFlasher = new Flasher(this.color, this.thickness, this.duration,
+ this.speed, this.invert);
+ } catch (e) {
+ this.mFlasher = new LegacyFlasher(this.color, this.thickness,
+ this.duration, this.speed, this.invert);
+ }
+
+ PrefUtils.addObserver("inspector.blink.", this);
+
+ this.updateFlashOnSelectCommand();
+ },
+
+ updateFlashOnSelectCommand: function DOMIFlasher_updateFlashOnSelectCommand()
+ {
+ var cmdEl = document.getElementById("cmdFlashOnSelect");
+ if (this.flashOnSelect) {
+ cmdEl.setAttribute("checked", "true");
+ } else {
+ cmdEl.removeAttribute("checked");
+ }
+ },
+
+ observe: function DOMIFlasher_observe(aSubject, aTopic, aData)
+ {
+ if (aData == "inspector.blink.on") {
+ this.updateFlashOnSelectCommand();
+ return;
+ }
+
+ var value = PrefUtils.getPref(aData);
+
+ if (aData == "inspector.blink.border-color") {
+ this.mFlasher.color = value;
+ } else if (aData == "inspector.blink.border-width") {
+ this.mFlasher.thickness = value;
+ } else if (aData == "inspector.blink.duration") {
+ this.mFlasher.duration = value;
+ } else if (aData == "inspector.blink.speed") {
+ this.mFlasher.speed = value;
+ } else if (aData == "inspector.blink.invert") {
+ this.mFlasher.invert = value;
+ }
+ }
+}
diff --git a/inspector/content/ViewerRegistry.js b/inspector/content/ViewerRegistry.js
new file mode 100644
index 00000000..f3b109ae
--- /dev/null
+++ b/inspector/content/ViewerRegistry.js
@@ -0,0 +1,251 @@
+/* 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/. */
+
+/*****************************************************************************
+* ViewerRegistry -------------------------------------------------------------
+* The central registry where information about all installed viewers is
+* kept.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/rdf/RDFU.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kViewerURLPrefix = "chrome://inspector/content/viewers/";
+const kViewerRegURL = "chrome://inspector/content/res/viewer-registry.rdf";
+
+//////////////////////////////////////////////////////////////////////////////
+//// Class ViewerRegistry
+
+function ViewerRegistry() // implements inIViewerRegistry
+{
+ this.mViewerHash = {};
+}
+
+ViewerRegistry.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Interface inIViewerRegistry (not yet formalized...)
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mDS: null,
+ mObserver: null,
+ mViewerDS: null,
+ mViewerHash: null,
+ mFilters: null,
+
+ get url()
+ {
+ return this.mURL;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Loading Methods
+
+ load: function VR_Load(aURL, aObserver)
+ {
+ this.mURL = aURL;
+ this.mObserver = aObserver;
+ RDFU.loadDataSource(aURL, new ViewerRegistryLoadObserver(this));
+ },
+
+ onError: function VR_OnError(aStatus, aErrorMsg)
+ {
+ this.mObserver.onViewerRegistryLoadError(aStatus, aErrorMsg);
+ },
+
+ onLoad: function VR_OnLoad(aDS)
+ {
+ this.mDS = aDS;
+ this.prepareRegistry();
+ this.mObserver.onViewerRegistryLoad();
+ },
+
+ prepareRegistry: function VR_PrepareRegistry()
+ {
+ this.mViewerDS = RDFArray.fromContainer(this.mDS, "inspector:viewers",
+ kInspectorNSURI);
+
+ // create and cache the filter functions
+ var js, fn;
+ this.mFilters = [];
+ for (var i = 0; i < this.mViewerDS.length; ++i) {
+ js = this.getEntryProperty(i, "filter");
+ try {
+ fn = new Function("object", "linkedViewer", js);
+ }
+ catch (ex) {
+ fn = new Function("return false");
+ debug("### ERROR - Syntax error in filter for viewer \"" +
+ this.getEntryProperty(i, "description") + "\"\n");
+ }
+ this.mFilters.push(fn);
+ }
+ },
+
+ /**
+ * Returns the absolute url where the xul file for a viewer can be found.
+ *
+ * @param aIndex
+ * The numerical index of the entry representing the viewer.
+ * @return A string of the fully canonized url.
+ */
+ getEntryURL: function VR_GetEntryURL(aIndex)
+ {
+ var uid = this.getEntryProperty(aIndex, "uid");
+ return kViewerURLPrefix + uid + "/" + uid + ".xul";
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Lookup Methods
+
+ /**
+ * Searches the viewer registry for all viewers that can view a particular
+ * object.
+ *
+ * @param aObject
+ * The object being searched against.
+ * @param aPanelId
+ * A string containing the id of the panel requesting viewers.
+ * @param aLinkedViewer
+ * The view object of linked panel.
+ * @return An array of nsIRDFResource entries in the viewer registry.
+ */
+ findViewersForObject:
+ function VR_FindViewersForObject(aObject, aPanelId, aLinkedViewer)
+ {
+ // check each entry in the registry
+ var len = this.mViewerDS.length;
+ var entry;
+ var urls = [];
+ for (var i = 0; i < len; ++i) {
+ if (this.getEntryProperty(i, "panels").indexOf(aPanelId) == -1) {
+ continue;
+ }
+ if (this.objectMatchesEntry(aObject, aLinkedViewer, i)) {
+ if (this.getEntryProperty(i, "important")) {
+ urls.unshift(i);
+ } else {
+ urls.push(i);
+ }
+ }
+ }
+
+ return urls;
+ },
+
+ /**
+ * Determines if an object is eligible to be viewed by a particular viewer.
+ *
+ * @param aObject
+ * The object being checked for eligibility.
+ * @param aLinkedViewer
+ * The view object of linked panel.
+ * @param aIndex
+ * The numerical index of the entry.
+ * @return true if object can be viewed.
+ */
+ objectMatchesEntry:
+ function VR_ObjectMatchesEntry(aObject, aLinkedViewer, aIndex)
+ {
+ try {
+ return this.mFilters[aIndex](aObject, aLinkedViewer);
+ }
+ catch (ex) {
+ Components.utils.reportError(ex);
+ }
+ return false;
+ },
+
+ /**
+ * Notifies the registry that a viewer has been instantiated, and that it
+ * corresponds to a particular entry in the viewer registry.
+ *
+ * @param aViewer
+ * The inIViewer object to cache.
+ * @param aIndex
+ * The numerical index of the entry.
+ */
+ cacheViewer: function VR_CacheViewer(aViewer, aIndex)
+ {
+ var uid = this.getEntryProperty(aIndex, "uid");
+ this.mViewerHash[uid] = { viewer: aViewer, entry: aIndex };
+ },
+
+ uncacheViewer: function VR_UncacheViewer(aViewer)
+ {
+ delete this.mViewerHash[aViewer.uid];
+ },
+
+ // for previously loaded viewers only
+ getViewerByUID: function VR_GetViewerByUID(aUID)
+ {
+ return this.mViewerHash[aUID].viewer;
+ },
+
+ // for previously loaded viewers only
+ getEntryForViewer: function VR_GetEntryForViewer(aViewer)
+ {
+ return this.mViewerHash[aViewer.uid].entry;
+ },
+
+ // for previously loaded viewers only
+ getEntryByUID: function VR_GetEntryByUID(aUID)
+ {
+ return this.mViewerHash[aUID].aIndex;
+ },
+
+ getEntryProperty: function VR_GetEntryProperty(aIndex, aProp)
+ {
+ return this.mViewerDS.get(aIndex, aProp);
+ },
+
+ getEntryCount: function VR_GetEntryCount()
+ {
+ return this.mViewerDS.length;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Viewer Registration
+
+ addNewEntry: function VR_AddNewEntry(aUID, aDescription, aFilter)
+ {
+ },
+
+ removeEntry: function VR_RemoveEntry(aIndex)
+ {
+ },
+
+ saveRegistry: function VR_SaveRegistry()
+ {
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+//// Listener Objects
+
+function ViewerRegistryLoadObserver(aTarget)
+{
+ this.mTarget = aTarget;
+}
+
+ViewerRegistryLoadObserver.prototype =
+{
+ mTarget: null,
+
+ onError: function VRLO_OnError(aStatus, aErrorMsg)
+ {
+ this.mTarget.onError(aStatus, aErrorMsg);
+ },
+
+ onDataSourceReady: function VRLO_OnDataSourceReady(aDS)
+ {
+ this.mTarget.onLoad(aDS);
+ }
+};
diff --git a/inspector/content/commandOverlay.xul b/inspector/content/commandOverlay.xul
new file mode 100644
index 00000000..b40799f6
--- /dev/null
+++ b/inspector/content/commandOverlay.xul
@@ -0,0 +1,58 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/editingOverlay.xul b/inspector/content/editingOverlay.xul
new file mode 100644
index 00000000..bb37cf54
--- /dev/null
+++ b/inspector/content/editingOverlay.xul
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/extensions/titledSplitter.css b/inspector/content/extensions/titledSplitter.css
new file mode 100644
index 00000000..7853b522
--- /dev/null
+++ b/inspector/content/extensions/titledSplitter.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin/titledSplitter.css");
+
+.titled-splitter {
+ -moz-binding: url("titledSplitter.xml#titledSplitter") !important;
+}
diff --git a/inspector/content/extensions/titledSplitter.xml b/inspector/content/extensions/titledSplitter.xml
new file mode 100644
index 00000000..0eec31f2
--- /dev/null
+++ b/inspector/content/extensions/titledSplitter.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/extensions/wsm-colorpicker.js b/inspector/content/extensions/wsm-colorpicker.js
new file mode 100644
index 00000000..20ae5e52
--- /dev/null
+++ b/inspector/content/extensions/wsm-colorpicker.js
@@ -0,0 +1,51 @@
+/* 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/. */
+
+/***************************************************************
+* wsm-colorpicker ----------------------------------------------
+* Quick script which adds support for the color picker widget
+* to nsWidgetStateManager in the pref winodw.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+function AddColorPicker(aCallback)
+{
+ window.addEventListener("load", AddColorPicker_delayCheck, false);
+ window.AddColorPicker_callback = aCallback;
+}
+
+function AddColorPicker_delayCheck()
+{
+ if (parent.hPrefWindow)
+ AddColorPicker_addColorHandlers()
+ else
+ setTimeout("AddColorPicker_delayCheck()", 1);
+}
+
+function AddColorPicker_addColorHandlers()
+{
+ parent.hPrefWindow.wsm.handlers.colorpicker = {
+ set: function (aElementID, aDataObject)
+ {
+ var wsm = parent.hPrefWindow.wsm;
+ var element = wsm.contentArea.document.getElementById( aElementID );
+ element.color = aDataObject.color;
+ },
+
+ get: function (aElementID)
+ {
+ var wsm = parent.hPrefWindow.wsm;
+ var element = wsm.contentArea.document.getElementById( aElementID );
+ var dataObject = wsm.generic_Get(element);
+ if(dataObject) {
+ dataObject.color = element.color;
+ return dataObject;
+ }
+ return null;
+ }
+ }
+
+ window.AddColorPicker_callback();
+}
diff --git a/inspector/content/hooks.js b/inspector/content/hooks.js
new file mode 100644
index 00000000..4f3e4bd8
--- /dev/null
+++ b/inspector/content/hooks.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+function inspectDOMDocument(aDocument, aModal)
+{
+ window.openDialog("chrome://inspector/content/", "_blank",
+ "chrome,all,dialog=no"+(aModal?",modal":""), aDocument);
+}
+
+function inspectDOMNode(aNode, aModal)
+{
+ window.openDialog("chrome://inspector/content/", "_blank",
+ "chrome,all,dialog=no"+(aModal?",modal":""), aNode);
+}
+
+function inspectObject(aObject, aModal)
+{
+ window.openDialog("chrome://inspector/content/object.xul", "_blank",
+ "chrome,all,dialog=no"+(aModal?",modal":""), aObject);
+}
diff --git a/inspector/content/inspector.css b/inspector/content/inspector.css
new file mode 100644
index 00000000..d07e15c2
--- /dev/null
+++ b/inspector/content/inspector.css
@@ -0,0 +1,38 @@
+/* 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/. */
+
+@import url("chrome://inspector/content/extensions/titledSplitter.css");
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+*[hide="true"] {
+ visibility: hidden;
+}
+
+domi-panelset {
+ -moz-binding: url("chrome://inspector/content/inspector.xml#panelset");
+ display: -moz-box;
+}
+
+domi-panel {
+ -moz-binding: url("chrome://inspector/content/inspector.xml#panel");
+ display: -moz-box;
+}
+
+#ppsViewerPopupset {
+ display: none;
+}
+
+.tree-list > treecol,
+.tree-list > .tree-columns > treecol {
+ -moz-binding: none;
+ background-color: transparent !important;
+ margin: 0px !important;
+ border: none !important;
+ padding: 0px !important;
+}
+
+.tree-nocolpicker > .tree-columns > .tree-columnpicker {
+ visibility: collapse;
+}
diff --git a/inspector/content/inspector.js b/inspector/content/inspector.js
new file mode 100644
index 00000000..b817a0c5
--- /dev/null
+++ b/inspector/content/inspector.js
@@ -0,0 +1,746 @@
+/* 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/. */
+
+/*****************************************************************************
+* InspectorApp ---------------------------------------------------------------
+* The primary object that controls the Inspector application.
+*****************************************************************************/
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var inspector;
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kIsMac = /Mac/.test(navigator.platform);
+const kInspectorTitle = kIsMac ? "" : " - " + document.title;
+
+const kAccessibleRetrievalContractID =
+ "@mozilla.org/accessibleRetrieval;1";
+const kClipboardHelperContractID =
+ "@mozilla.org/widget/clipboardhelper;1";
+const kPromptServiceContractID =
+ "@mozilla.org/embedcomp/prompt-service;1";
+const kFOStreamContractID =
+ "@mozilla.org/network/file-output-stream;1";
+const kEncoderContractIDbase =
+ "@mozilla.org/layout/documentEncoder;1?type=";
+const kSerializerContractID =
+ "@mozilla.org/xmlextras/xmlserializer;1";
+const kWindowMediatorContractID =
+ "@mozilla.org/appshell/window-mediator;1";
+const kFilePickerContractID =
+ "@mozilla.org/filepicker;1";
+
+const nsIAccessible = Components.interfaces.nsIAccessible;
+const nsIWebNavigation = Components.interfaces.nsIWebNavigation;
+const nsIDocShellTreeItem = Components.interfaces.nsIDocShellTreeItem;
+const nsIDocShell = Components.interfaces.nsIDocShell;
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", InspectorApp_initialize, false);
+window.addEventListener("unload", InspectorApp_destroy, false);
+
+function InspectorApp_initialize()
+{
+ inspector = new InspectorApp();
+
+ // window.arguments may be either a string or a node.
+ // If passed via a command line handler, it will be a uri string.
+ // If passed via navigator hooks, it will be a dom node to inspect.
+ var initNode, initURI;
+ if (window.arguments && window.arguments.length) {
+ if (typeof window.arguments[0] == "string") {
+ initURI = window.arguments[0];
+ }
+ else if (window.arguments[0] instanceof Components.interfaces.nsIDOMNode) {
+ initNode = window.arguments[0];
+ }
+ }
+ inspector.initialize(initNode, initURI);
+
+ // Fix up content primary for older versions.
+ // See bug 1324899.
+ if (Services.vc.compare(Services.appinfo.platformVersion, "53.0a1") < 0) {
+ document.getElementById("ifBrowser").setAttribute("type",
+ "content-primary");
+ }
+
+ // Enable/disable Mac outlier keys.
+ if (kIsMac) {
+ document.getElementById("keyEnterLocation2").setAttribute("disabled",
+ "true");
+ }
+ else {
+ document.getElementById("keyEditDeleteMac").setAttribute("disabled",
+ "true");
+ }
+
+ if (/Win/.test(navigator.platform)) {
+ document.getElementById("mnEditRedo").setAttribute("key", "keyEditRedo2");
+ }
+
+ // Get rid of any menus that we expose as overlay points for integration
+ // with several applications but aren't of use with the one hosting us here.
+ var menubar = document.getElementById("mbrInspectorMain");
+ var kid = menubar.firstChild;
+ while (kid) {
+ let nextSibling = kid.nextSibling;
+ if (!kid.hasChildNodes()) {
+ menubar.removeChild(kid);
+ }
+ kid = nextSibling;
+ }
+}
+
+function InspectorApp_destroy()
+{
+ inspector.destroy();
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Class InspectorApp
+
+function InspectorApp()
+{
+}
+
+InspectorApp.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mShowBrowser: false,
+ mClipboardHelper: null,
+ mPromptService: null,
+
+ mDocPanel: null,
+ mObjectPanel: null,
+ mDocViewerListPopup: null,
+ mObjectViewerListPopup: null,
+
+ mLastKnownDocPanelSubject: null,
+ mLastKnownObjectPanelSubject: null,
+
+ get document()
+ {
+ return this.mDocPanel.viewer.subject
+ },
+
+ get panelset()
+ {
+ return this.mPanelSet;
+ },
+
+ initialize: function IA_Initialize(aTarget, aURI)
+ {
+ this.mInitTarget = aTarget;
+
+ var el = document.getElementById("bxBrowser");
+ el.addEventListener("pageshow", BrowserPageShowListener, true);
+
+ this.setBrowser(false, true);
+
+ this.mClipboardHelper = XPCU.getService(kClipboardHelperContractID,
+ "nsIClipboardHelper");
+ this.mPromptService = XPCU.getService(kPromptServiceContractID,
+ "nsIPromptService");
+
+ this.mDocViewerListPopup =
+ document.getElementById("mppDocViewerList");
+ this.mObjectViewerListPopup =
+ document.getElementById("mppObjectViewerList");
+
+ this.mPanelSet = document.getElementById("bxPanelSet");
+ this.mPanelSet.addObserver("panelsetready", this);
+ this.mPanelSet.initialize();
+
+ // check if accessibility service is available
+ if (!(kAccessibleRetrievalContractID in Components.classes)) {
+ var elm = document.getElementById("cmd:toggleAccessibleNodes");
+ if (elm) {
+ elm.setAttribute("disabled", "true");
+ }
+
+ elm = document.getElementById("mnInspectApplicationAccessible");
+ if (elm) {
+ elm.setAttribute("disabled", "true");
+ }
+ }
+
+ if (aURI) {
+ this.gotoURL(aURI);
+ }
+ },
+
+ destroy: function IA_Destroy()
+ {
+ InsUtil.persistAll("bxDocPanel");
+ InsUtil.persistAll("bxObjectPanel");
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Viewer Panels
+
+ initViewerPanels: function IA_InitViewerPanels()
+ {
+ this.mDocPanel = this.mPanelSet.getPanel(0);
+ this.mDocPanel.addObserver("subjectChange", this);
+ this.mObjectPanel = this.mPanelSet.getPanel(1);
+ this.mObjectPanel.addObserver("subjectChange", this);
+
+ if (this.mInitTarget) {
+ if (this.mInitTarget.nodeType == Node.DOCUMENT_NODE) {
+ this.setTargetDocument(this.mInitTarget, false);
+ }
+ else if (this.mInitTarget.nodeType == Node.ELEMENT_NODE) {
+ this.setTargetDocument(this.mInitTarget.ownerDocument, false);
+ this.mDocPanel.params = this.mInitTarget;
+ }
+ this.mInitTarget = null;
+ }
+ },
+
+ onEvent: function IA_OnEvent(aEvent)
+ {
+ switch (aEvent.type) {
+ case "panelsetready":
+ this.initViewerPanels();
+ break;
+ case "subjectChange":
+ // A subjectChange really means the *viewer's* subject changed, and
+ // one will be dispatched everytime a new viewer is loaded. Don't
+ // update the entries if the panel's subject is the same as before and
+ // the subjectChange was only dispatched because of a new viewer.
+ if (aEvent.target == this.mDocPanel.viewer) {
+ let panel = this.mDocPanel;
+ let mpp = this.mDocViewerListPopup;
+ // Update the viewer list.
+ if (this.mLastKnownDocPanelSubject != aEvent.subject) {
+ panel.rebuildViewerList(mpp);
+ this.mLastKnownDocPanelSubject = aEvent.subject;
+ }
+ panel.updateViewerListSelection(mpp);
+
+ if (aEvent.subject) {
+ if ("location" in aEvent.subject) {
+ // display document url
+ this.locationText = aEvent.subject.location;
+
+ document.title =
+ (aEvent.subject.title || aEvent.subject.location) +
+ kInspectorTitle;
+
+ this.updateCommand("cmdSave");
+ }
+ else if (("nsIAccessibleApplication" in Components.interfaces &&
+ aEvent.subject instanceof
+ Components.interfaces.nsIAccessibleApplication) ||
+ (aEvent.subject instanceof nsIAccessible &&
+ !aEvent.subject.parent)) {
+ // Update title for application accessible in compatible way for
+ // Gecko 2.0 and 1.9.2.
+ this.locationText = "";
+
+ var title = this.mPanelSet.
+ stringBundle.getString("applicationAccesible.title");
+ document.title = title + kInspectorTitle;
+
+ this.updateCommand("cmdSave");
+ }
+ }
+ }
+ else if (aEvent.target == this.mObjectPanel.viewer) {
+ let panel = this.mObjectPanel;
+ let mpp = this.mObjectViewerListPopup;
+ // Update the viewer list.
+ if (this.mLastKnownObjectPanelSubject != aEvent.subject) {
+ panel.rebuildViewerList(mpp);
+ this.mLastKnownObjectPanelSubject = aEvent.subject;
+ }
+ panel.updateViewerListSelection(mpp);
+ }
+ break;
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// UI Commands
+
+ updateCommand: function IA_UpdateCommand(aCommand)
+ {
+ var command = document.getElementById(aCommand);
+
+ var disabled = false;
+ switch (aCommand) {
+ case "cmdSave":
+ var doc = this.mDocPanel.subject;
+ disabled =
+ !((kEncoderContractIDbase + doc.contentType) in Components.classes ||
+ (kSerializerContractID in Components.classes));
+ break;
+ }
+
+ command.setAttribute("disabled", disabled);
+ },
+
+ doViewerCommand: function IA_DoViewerCommand(aCommand)
+ {
+ this.mPanelSet.execCommand(aCommand);
+ },
+
+ enterLocation: function IA_EnterLocation()
+ {
+ this.locationBar.focus();
+ this.locationBar.select();
+ },
+
+ showPrefsDialog: function IA_ShowPrefsDialog()
+ {
+ goPreferences("inspector_pane");
+ },
+
+ toggleBrowser: function IA_ToggleBrowser(aToggleSplitter)
+ {
+ this.setBrowser(!this.mShowBrowser, aToggleSplitter)
+ },
+
+ /**
+ * Toggle 'blink on select' command.
+ */
+ toggleFlashOnSelect: function IA_ToggleFlashOnSelect()
+ {
+ this.mPanelSet.flasher.flashOnSelect =
+ !this.mPanelSet.flasher.flashOnSelect;
+ },
+
+ setBrowser: function IA_SetBrowser(aValue, aToggleSplitter)
+ {
+ this.mShowBrowser = aValue;
+ if (aToggleSplitter) {
+ this.openSplitter("Browser", aValue);
+ }
+ var cmd = document.getElementById("cmdToggleDocument");
+ cmd.setAttribute("checked", aValue);
+ },
+
+ openSplitter: function IA_OpenSplitter(aName, aTruth)
+ {
+ var splitter = document.getElementById("spl" + aName);
+ if (aTruth) {
+ splitter.open();
+ }
+ else {
+ splitter.close();
+ }
+ },
+
+ /**
+ * Saves the current document state in the inspector.
+ */
+ save: function IA_Save()
+ {
+ var picker = XPCU.createInstance(kFilePickerContractID, "nsIFilePicker");
+ var title = document.getElementById("mi-save").label;
+ picker.init(window, title, picker.modeSave)
+ picker.appendFilters(picker.filterHTML | picker.filterXML |
+ picker.filterXUL);
+ if (picker.show() == picker.returnCancel) {
+ return;
+ }
+
+ var fos = XPCU.createInstance(kFOStreamContractID, "nsIFileOutputStream");
+ const flags = 0x02 | 0x08 | 0x20; // write, create, truncate
+
+ var doc = this.mDocPanel.subject;
+ if ((kEncoderContractIDbase + doc.contentType) in Components.classes) {
+ // first we try to use the document encoder for that content type. If
+ // that fails, we move on to the xml serializer.
+ var encoder =
+ XPCU.createInstance(kEncoderContractIDbase + doc.contentType,
+ "nsIDocumentEncoder");
+ encoder.init(doc, doc.contentType, encoder.OutputRaw);
+ encoder.setCharset(doc.characterSet);
+ fos.init(picker.file, flags, -1, 0);
+ try {
+ encoder.encodeToStream(fos);
+ }
+ finally {
+ fos.close();
+ }
+ }
+ else {
+ var serializer = XPCU.createInstance(kSerializerContractID,
+ "nsIDOMSerializer");
+ fos.init(picker.file, flags, -1, 0);
+ try {
+ serializer.serializeToStream(doc, fos);
+ }
+ finally {
+ fos.close();
+ }
+ }
+ },
+
+ exit: function IA_Exit()
+ {
+ window.close();
+ // Todo: remove observer service here
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Navigation
+
+ gotoTypedURL: function IA_GotoTypedURL()
+ {
+ var url = document.getElementById("tfURLBar").value;
+ this.gotoURL(url);
+ },
+
+ gotoURL: function IA_GotoURL(aURL, aNoSaveHistory)
+ {
+ this.mPendingURL = aURL;
+ this.mPendingNoSave = aNoSaveHistory;
+ this.browseToURL(aURL);
+ this.setBrowser(true, true);
+ },
+
+ browseToURL: function IA_BrowseToURL(aURL)
+ {
+ try {
+ this.webNavigation.loadURI(aURL, nsIWebNavigation.LOAD_FLAGS_NONE, null,
+ null, null);
+ }
+ catch(ex) {
+ // nsIWebNavigation.loadURI will spit out an appropriate user prompt, so
+ // we don't need to do anything here. See nsDocShell::DisplayLoadError()
+ }
+ },
+
+ /**
+ * Creates the submenu for Inspect Content/Chrome Document
+ */
+ showInspectDocumentList:
+ function IA_ShowInspectDocumentList(aEvent, aChrome)
+ {
+ var menu = aEvent.target;
+ var ww = XPCU.getService(kWindowMediatorContractID, "nsIWindowMediator");
+ var windows = ww.getXULWindowEnumerator(null);
+ var docs = [];
+
+ while (windows.hasMoreElements()) {
+ try {
+ // Get the window's main docshell
+ var windowDocShell =
+ XPCU.QI(windows.getNext(), "nsIXULWindow").docShell;
+ this.appendContainedDocuments(docs, windowDocShell,
+ aChrome ?
+ nsIDocShellTreeItem.typeChrome :
+ nsIDocShellTreeItem.typeContent);
+ }
+ catch (ex) {
+ // We've failed with this window somehow, but we're catching the error
+ // so the others will still work
+ Components.utils.reportError(ex);
+ }
+ }
+
+ // Clear out any previous menu
+ this.emptyChildren(menu);
+
+ // Now add what we found to the menu
+ if (!docs.length) {
+ var noneMenuItem = document.createElementNS(kXULNSURI, "menuitem");
+ var label = this.mPanelSet.stringBundle.getString(
+ "inspectWindow.noDocuments.message"
+ );
+ noneMenuItem.setAttribute("label", label);
+ noneMenuItem.setAttribute("disabled", true);
+ menu.appendChild(noneMenuItem);
+ }
+ else {
+ for (var i = 0; i < docs.length; i++) {
+ this.addInspectDocumentMenuItem(menu, docs[i], i + 1);
+ }
+ }
+ },
+
+ /**
+ * Appends to the array the documents contained in docShell (including the
+ * passed docShell itself).
+ *
+ * @param array
+ * The array to append to.
+ * @param docShell
+ * The docshell to look for documents in.
+ * @param type
+ * One of the types defined in nsIDocShellTreeItem.
+ */
+ appendContainedDocuments:
+ function IA_AppendContainedDocuments(array, docShell, type)
+ {
+ // Load all the window's content docShells
+ var containedDocShells = docShell.getDocShellEnumerator(type,
+ nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ try {
+ // Get the corresponding document for this docshell
+ var childDoc = XPCU.QI(containedDocShells.getNext(), "nsIDocShell")
+ .contentViewer.DOMDocument;
+
+ // Ignore the DOM Inspector's browser docshell if it's not being used
+ if (docShell.contentViewer.DOMDocument.location.href !=
+ document.location.href ||
+ childDoc.location.href != "about:blank") {
+ array.push(childDoc);
+ }
+ }
+ catch (ex) {
+ // We've failed with this document somehow, but we're catching the
+ // error so the others will still work
+ dump(ex + "\n");
+ }
+ }
+ },
+
+ /**
+ * Creates a menu item for Inspect Document.
+ *
+ * @param doc
+ * Document related to this menu item.
+ * @param docNumber
+ * The position of the document.
+ */
+ addInspectDocumentMenuItem:
+ function IA_AddInspectDocumentMenuItem(parent, doc, docNumber)
+ {
+ var menuItem = document.createElementNS(kXULNSURI, "menuitem");
+ menuItem.doc = doc;
+ // Use the URL if there's no title
+ var title = doc.title || doc.location.href;
+ // The first ten items get numeric access keys
+ if (docNumber < 10) {
+ menuItem.setAttribute("label", docNumber + " " + title);
+ menuItem.setAttribute("accesskey", docNumber);
+ }
+ else {
+ menuItem.setAttribute("label", title);
+ }
+ parent.appendChild(menuItem);
+ },
+
+ setTargetApplicationAccessible: function setTargetApplicationAccessible()
+ {
+ var accService =
+ XPCU.getService(kAccessibleRetrievalContractID, "nsIAccessibleRetrieval");
+
+ if (accService) {
+ if ("getApplicationAccessible" in accService) {
+ this.mDocPanel.subject = accService.getApplicationAccessible();
+ }
+ else {
+ // Gecko 1.9.2 support.
+ var accessible = accService.getAccessibleFor(document);
+ while (accessible.parent) {
+ accessible = accessible.parent;
+ }
+ this.mDocPanel.subject = accessible;
+ }
+ }
+ },
+
+ setTargetWindow: function IA_SetTargetWindow(aWindow)
+ {
+ this.setTargetDocument(aWindow.document, true);
+ },
+
+ setTargetDocument: function IA_SetTargetDocument(aDoc, aIsInternal)
+ {
+ var cmd = document.getElementById("cmdToggleDocument");
+
+ if (aIsInternal == undefined) {
+ aIsInternal = false;
+ }
+
+ cmd.setAttribute("disabled", !aIsInternal);
+ this.setBrowser(aIsInternal, true);
+
+ this.mDocPanel.subject = aDoc;
+ },
+
+ get webNavigation()
+ {
+ var browser = document.getElementById("ifBrowser");
+ return browser.webNavigation;
+ },
+
+ get locationBar()
+ {
+ return document.getElementById("tfURLBar");
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// UI Labels Getters and Setters
+
+ get locationText()
+ {
+ return this.locationBar.value;
+ },
+
+ set locationText(aText)
+ {
+ this.locationBar.value = aText;
+ },
+
+ get statusText()
+ {
+ return document.getElementById("txStatus").value;
+ },
+
+ set statusText(aText)
+ {
+ document.getElementById("txStatus").value = aText;
+ },
+
+ get progress()
+ {
+ return document.getElementById("pmStatus").value;
+ },
+
+ set progress(aPct)
+ {
+ document.getElementById("pmStatus").value = aPct;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Document Loading
+
+ documentLoaded: function IA_DocumentLoaded()
+ {
+ this.setTargetWindow(content);
+
+ var url = this.webNavigation.currentURI.spec;
+
+ // put the url into the urlbar
+ this.locationText = url;
+
+ // add url to the history, unless explicity told not to
+ if (!this.mPendingNoSave) {
+ this.addToHistory(url);
+ }
+
+ this.mPendingURL = null;
+ this.mPendingNoSave = null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// History
+
+ addToHistory: function IA_AddToHistory(aURL)
+ {
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Uncategorized
+
+ get isViewingContent()
+ {
+ return this.mPanelSet.getPanel(0).subject != null;
+ },
+
+ fillInTooltip: function IA_FillInTooltip(aMenuItem)
+ {
+ var doc = aMenuItem.doc;
+ if (!doc) {
+ return false;
+ }
+
+ var titleLabel = document.getElementById("docItemsTitle");
+ var uriLabel = document.getElementById("docItemsURI");
+ titleLabel.value = doc.title;
+ uriLabel.value = doc.location.href;
+ titleLabel.hidden = !titleLabel.value;
+ return true;
+ },
+
+ initPopup: function IA_InitPopup(aPopup)
+ {
+ var items = aPopup.getElementsByTagName("menuitem");
+ var js, fn, item;
+ for (var i = 0; i < items.length; i++) {
+ item = items[i];
+ fn = "isDisabled" in item ? item.isDisabled : null;
+ if (!fn) {
+ js = item.getAttribute("isDisabled");
+ if (js) {
+ fn = new Function(js);
+ item.isDisabled = fn;
+ }
+ else {
+ // to prevent annoying "strict" warning messages
+ item.isDisabled = null;
+ }
+ }
+ if (fn) {
+ if (item.isDisabled()) {
+ item.setAttribute("disabled", "true");
+ }
+ else {
+ item.removeAttribute("disabled");
+ }
+ }
+
+ fn = null;
+ }
+ },
+
+ emptyChildren: function IA_EmptyChildren(aNode)
+ {
+ while (aNode.hasChildNodes()) {
+ aNode.removeChild(aNode.lastChild);
+ }
+ },
+
+ onSplitterOpen: function IA_OnSplitterOpen(aSplitter)
+ {
+ if (aSplitter.id == "splBrowser") {
+ this.setBrowser(aSplitter.isOpened, false);
+ }
+ },
+
+ onViewerListCommand: function IA_OnViewerListCommand(aItem)
+ {
+ var mpp = aItem.parentNode;
+ if (mpp == this.mDocViewerListPopup) {
+ this.mDocPanel.onViewerListCommand(aItem);
+ }
+ else if (mpp == this.mObjectViewerListPopup) {
+ this.mObjectPanel.onViewerListCommand(aItem);
+ }
+ },
+
+ // needed by overlayed commands from viewer to get references to a specific
+ // viewer object by name
+ getViewer: function IA_GetViewer(aUID)
+ {
+ return this.mPanelSet.registry.getViewerByUID(aUID);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+//// Event Listeners
+
+function BrowserPageShowListener(aEvent)
+{
+ // since we will also get pageshow events for frame documents,
+ // make sure we respond to the top-level document load
+ if (aEvent.target.defaultView == content) {
+ inspector.documentLoaded();
+ }
+}
diff --git a/inspector/content/inspector.xml b/inspector/content/inspector.xml
new file mode 100644
index 00000000..2fb38bbe
--- /dev/null
+++ b/inspector/content/inspector.xml
@@ -0,0 +1,926 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+ null
+ null
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ return this.getElementsByTagName("domi-panel");
+
+
+
+
+
+ return this.panels.length;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0;
+ }
+ else if (aCommand == "cmdEditRedo") {
+ enabled = this.mTransactionManager &&
+ this.mTransactionManager.numberOfRedoItems > 0;
+ }
+ else {
+ if (this.focusedPanel && this.focusedPanel.viewer) {
+ enabled = this.focusedPanel.viewer.isCommandEnabled(aCommand);
+ }
+ }
+ this.setCommandAttribute(aCommand, "disabled", !enabled);
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+ null
+ null
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ this.mFlasher.destroy();
+ this.mFlasher = null;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+ null
+ null
+ null
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+ null
+ null
+ null
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1) {
+ Components.utils.reportError("expected only zero or one checked items");
+ }
+ while (checked.length) {
+ checked[0].removeAttribute("checked");
+ }
+ var entry = this.mCurrentEntry;
+ checked = aPopup.getElementsByAttribute("viewerListEntry", entry);
+ if (checked.length == 1) {
+ checked[0].setAttribute("checked", "true");
+ }
+ else {
+ Components.utils.reportError("expected one item to match entry " +
+ entry);
+ }
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/inspector.xul b/inspector/content/inspector.xul
new file mode 100644
index 00000000..673b8763
--- /dev/null
+++ b/inspector/content/inspector.xul
@@ -0,0 +1,58 @@
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/inspectorOverlay.xul b/inspector/content/inspectorOverlay.xul
new file mode 100644
index 00000000..22da190b
--- /dev/null
+++ b/inspector/content/inspectorOverlay.xul
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/jar.mn b/inspector/content/jar.mn
new file mode 100644
index 00000000..e36c5288
--- /dev/null
+++ b/inspector/content/jar.mn
@@ -0,0 +1,135 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% content @INSPECTOR_CHROME_NAME@ chrome/content/
+ content/commandOverlay.xul
+ content/editingOverlay.xul
+ content/Flasher.js
+ content/hooks.js
+ content/inspector.css
+ content/inspector.js
+ content/inspector.xml
+ content/inspector.xul
+ content/inspectorOverlay.xul
+ content/keysetOverlay.xul
+ content/object.js
+ content/object.xul
+ content/popupOverlay.xul
+ content/sidebar.js
+ content/sidebar.xul
+ content/statusbarOverlay.xul
+ content/tasksOverlay-cz.xul
+ content/tasksOverlay-ff.xul
+ content/tasksOverlay-mobile.xul
+ content/tasksOverlay-bn.xul
+ content/tasksOverlay-sb.xul
+ content/tasksOverlay-tb.xul
+ content/tasksOverlay.xul
+ content/toolboxOverlay.xul
+ content/utils.js
+ content/ViewerRegistry.js
+ content/extensions/titledSplitter.css (extensions/titledSplitter.css)
+ content/extensions/titledSplitter.xml (extensions/titledSplitter.xml)
+ content/extensions/wsm-colorpicker.js (extensions/wsm-colorpicker.js)
+ content/jsutil/commands/baseCommands.js (jsutil/commands/baseCommands.js)
+ content/jsutil/events/ObserverManager.js (jsutil/events/ObserverManager.js)
+ content/jsutil/rdf/RDFArray.js (jsutil/rdf/RDFArray.js)
+ content/jsutil/rdf/RDFU.js (jsutil/rdf/RDFU.js)
+ content/jsutil/system/clipboardFlavors.js (jsutil/system/clipboardFlavors.js)
+ content/jsutil/system/DiskSearch.js (jsutil/system/DiskSearch.js)
+ content/jsutil/system/FilePickerUtils.js (jsutil/system/FilePickerUtils.js)
+ content/jsutil/system/PrefUtils.js (jsutil/system/PrefUtils.js)
+ content/jsutil/xpcom/XPCU.js (jsutil/xpcom/XPCU.js)
+ content/jsutil/xul/DNDUtils.js (jsutil/xul/DNDUtils.js)
+ content/jsutil/xul/FrameExchange.js (jsutil/xul/FrameExchange.js)
+ content/jsutil/xul/inBaseTreeView.js (jsutil/xul/inBaseTreeView.js)
+ content/jsutil/xul/inDataTreeView.js (jsutil/xul/inDataTreeView.js)
+ content/jsutil/xul/inFormManager.js (jsutil/xul/inFormManager.js)
+ content/jsutil/xul/inTreeBuilder.js (jsutil/xul/inTreeBuilder.js)
+ content/prefs/pref-inspector.js (prefs/pref-inspector.js)
+ content/prefs/pref-inspector.xul (prefs/pref-inspector.xul)
+ content/prefs/pref-sidebar.js (prefs/pref-sidebar.js)
+ content/prefs/prefsOverlay.xul (prefs/prefsOverlay.xul)
+ content/res/viewer-registry.rdf (res/viewer-registry.rdf)
+ content/viewers/accessibleEvent/accessibleEvent.js (viewers/accessibleEvent/accessibleEvent.js)
+ content/viewers/accessibleEvent/accessibleEvent.xul (viewers/accessibleEvent/accessibleEvent.xul)
+ content/viewers/accessibleEvents/accessibleEvents.js (viewers/accessibleEvents/accessibleEvents.js)
+ content/viewers/accessibleEvents/accessibleEvents.xul (viewers/accessibleEvents/accessibleEvents.xul)
+ content/viewers/accessibleEvents/handlerHelpDialog.xul (viewers/accessibleEvents/handlerHelpDialog.xul)
+ content/viewers/accessibleObject/accessibleObject.js (viewers/accessibleObject/accessibleObject.js)
+ content/viewers/accessibleObject/accessibleObject.xul (viewers/accessibleObject/accessibleObject.xul)
+ content/viewers/accessibleProps/accessibleProps.js (viewers/accessibleProps/accessibleProps.js)
+ content/viewers/accessibleProps/accessibleProps.xul (viewers/accessibleProps/accessibleProps.xul)
+ content/viewers/accessibleProps/accessiblePropViewerMgr.js (viewers/accessibleProps/accessiblePropViewerMgr.js)
+ content/viewers/accessibleRelations/accessibleRelations.js (viewers/accessibleRelations/accessibleRelations.js)
+ content/viewers/accessibleRelations/accessibleRelations.xul (viewers/accessibleRelations/accessibleRelations.xul)
+ content/viewers/accessibleTree/accessibleTree.js (viewers/accessibleTree/accessibleTree.js)
+ content/viewers/accessibleTree/accessibleTree.xul (viewers/accessibleTree/accessibleTree.xul)
+ content/viewers/accessibleTree/evalJSDialog.js (viewers/accessibleTree/evalJSDialog.js)
+ content/viewers/accessibleTree/evalJSDialog.xul (viewers/accessibleTree/evalJSDialog.xul)
+ content/viewers/boxModel/boxModel.js (viewers/boxModel/boxModel.js)
+ content/viewers/boxModel/boxModel.xul (viewers/boxModel/boxModel.xul)
+ content/viewers/computedStyle/computedStyle.js (viewers/computedStyle/computedStyle.js)
+ content/viewers/computedStyle/computedStyle.xul (viewers/computedStyle/computedStyle.xul)
+ content/viewers/dom/columnsDialog.js (viewers/dom/columnsDialog.js)
+ content/viewers/dom/columnsDialog.xul (viewers/dom/columnsDialog.xul)
+ content/viewers/dom/commandOverlay.xul (viewers/dom/commandOverlay.xul)
+ content/viewers/dom/dom.js (viewers/dom/dom.js)
+ content/viewers/dom/dom.xul (viewers/dom/dom.xul)
+ content/viewers/dom/FindDialog.js (viewers/dom/FindDialog.js)
+ content/viewers/dom/findDialog.xul (viewers/dom/findDialog.xul)
+ content/viewers/dom/insertDialog.js (viewers/dom/insertDialog.js)
+ content/viewers/dom/insertDialog.xul (viewers/dom/insertDialog.xul)
+ content/viewers/dom/keysetOverlay.xul (viewers/dom/keysetOverlay.xul)
+ content/viewers/dom/popupOverlay.xul (viewers/dom/popupOverlay.xul)
+ content/viewers/dom/pseudoClassDialog.js (viewers/dom/pseudoClassDialog.js)
+ content/viewers/dom/pseudoClassDialog.xul (viewers/dom/pseudoClassDialog.xul)
+ content/viewers/domNode/domNode.js (viewers/domNode/domNode.js)
+ content/viewers/domNode/domNode.xul (viewers/domNode/domNode.xul)
+ content/viewers/domNode/domNodeDialog.js (viewers/domNode/domNodeDialog.js)
+ content/viewers/domNode/domNodeDialog.xul (viewers/domNode/domNodeDialog.xul)
+ content/viewers/jsObject/evalExprDialog.js (viewers/jsObject/evalExprDialog.js)
+ content/viewers/jsObject/evalExprDialog.xul (viewers/jsObject/evalExprDialog.xul)
+ content/viewers/jsObject/jsObject.js (viewers/jsObject/jsObject.js)
+ content/viewers/jsObject/jsObject.xul (viewers/jsObject/jsObject.xul)
+ content/viewers/jsObject/jsObjectViewer.js (viewers/jsObject/jsObjectViewer.js)
+ content/viewers/jsObject/jsObjectViewer.xul (viewers/jsObject/jsObjectViewer.xul)
+ content/viewers/styleRules/commandOverlay.xul (viewers/styleRules/commandOverlay.xul)
+ content/viewers/styleRules/keysetOverlay.xul (viewers/styleRules/keysetOverlay.xul)
+ content/viewers/styleRules/popupOverlay.xul (viewers/styleRules/popupOverlay.xul)
+ content/viewers/styleRules/styleRules.js (viewers/styleRules/styleRules.js)
+ content/viewers/styleRules/styleRules.xul (viewers/styleRules/styleRules.xul)
+ content/viewers/stylesheets/stylesheets.js (viewers/stylesheets/stylesheets.js)
+ content/viewers/stylesheets/stylesheets.xul (viewers/stylesheets/stylesheets.xul)
+ content/viewers/usedFontFaces/usedFontFaces.js (viewers/usedFontFaces/usedFontFaces.js)
+ content/viewers/usedFontFaces/usedFontFaces.xul (viewers/usedFontFaces/usedFontFaces.xul)
+ content/viewers/xblBindings/xblBindings.js (viewers/xblBindings/xblBindings.js)
+ content/viewers/xblBindings/xblBindings.xul (viewers/xblBindings/xblBindings.xul)
+
+# Extension Overlays
+% overlay chrome://inspector/content/commandOverlay.xul chrome://inspector/content/viewers/dom/commandOverlay.xul
+% overlay chrome://inspector/content/commandOverlay.xul chrome://inspector/content/viewers/styleRules/commandOverlay.xul
+% overlay chrome://inspector/content/keysetOverlay.xul chrome://inspector/content/viewers/dom/keysetOverlay.xul
+% overlay chrome://inspector/content/popupOverlay.xul chrome://inspector/content/viewers/dom/popupOverlay.xul
+% overlay chrome://inspector/content/popupOverlay.xul chrome://inspector/content/viewers/styleRules/popupOverlay.xul
+
+# Phoenix Overlays
+% overlay chrome://browser/content/browser.xul chrome://inspector/content/tasksOverlay-ff.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
+% overlay chrome://browser/content/macBrowserOverlay.xul chrome://inspector/content/tasksOverlay-ff.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
+% overlay chrome://inspector/content/inspector.xul chrome://browser/content/baseMenuOverlay.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={8de7fcbb-c55c-4fbe-bfc5-fc555c87dbc4}
+
+# Mail Overlays
+% overlay chrome://messenger/content/mailWindowOverlay.xul chrome://inspector/content/tasksOverlay-tb.xul application={3550f703-e582-4d05-9a08-453d09bdfdc6}
+
+# Chat Overlays
+% overlay chrome://ambassador/content/ambassador.xul chrome://inspector/content/tasksOverlay-cz.xul application={4523665a-317f-4a66-9376-3763d1ad1978}
+
+# Navigator Overlays
+% overlay chrome://communicator/content/tasksOverlay.xul chrome://inspector/content/tasksOverlay.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={a3210b97-8e8a-4737-9aa0-aa0e607640b9}
+% overlay chrome://communicator/content/pref/preferences.xul chrome://inspector/content/prefs/prefsOverlay.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={a3210b97-8e8a-4737-9aa0-aa0e607640b9}
+% overlay chrome://inspector/content/inspector.xul chrome://communicator/content/tasksOverlay.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={a3210b97-8e8a-4737-9aa0-aa0e607640b9}
+% overlay chrome://inspector/content/inspector.xul chrome://communicator/content/utilityOverlay.xul application={92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a} application={a3210b97-8e8a-4737-9aa0-aa0e607640b9}
diff --git a/inspector/content/jsutil/commands/baseCommands.js b/inspector/content/jsutil/commands/baseCommands.js
new file mode 100644
index 00000000..0cd586f9
--- /dev/null
+++ b/inspector/content/jsutil/commands/baseCommands.js
@@ -0,0 +1,164 @@
+/* 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/. */
+
+/*****************************************************************************
+* Base Commands --------------------------------------------------------------
+* Transactions which can be used to implement common commands.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* (Other files may be necessary, depending on which base commands are used.)
+*****************************************************************************/
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kClipboardHelperClassID = "@mozilla.org/widget/clipboardhelper;1";
+
+//////////////////////////////////////////////////////////////////////////////
+//// Transactions
+
+/**
+ * Base implementation of an nsITransaction.
+ * @param aIsTransient [optional]
+ * Override the isTransient field. The default is true.
+ */
+function inBaseCommand(aIsTransient)
+{
+ if (aIsTransient !== undefined) {
+ this.isTransient = aIsTransient;
+ }
+}
+
+inBaseCommand.prototype = {
+ isTransient: true,
+
+ merge: function BaseCommand_Merge()
+ {
+ return false;
+ },
+
+ QueryInterface:
+ XPCOMUtils.generateQI([Components.interfaces.nsITransaction]),
+
+ doTransaction: function BaseCommand_DoTransaction() {},
+ undoTransaction: function BaseCommand_UndoTransaction() {},
+
+ redoTransaction: function BaseCommand_RedoTransaction()
+ {
+ this.doTransaction();
+ }
+};
+
+/**
+ * Open the object "mini" viewer (object.xul) on a given object. The mObject
+ * field should be overridden to contain the object to be inspected.
+ *
+ * Primitives are uninteresting and attempts to inspect them will fail.
+ * Consumers should take this into account when determining whether the
+ * corresponding command should be enabled. For convenience,
+ * cmdEditInspectInNewWindowBase.isInspectable is provided.
+ */
+function cmdEditInspectInNewWindowBase()
+{
+ this.mObject = null;
+}
+
+cmdEditInspectInNewWindowBase.isInspectable =
+ function InspectInNewWindowBase_IsInspectable(aValue)
+{
+ var type = typeof aValue;
+ return (type == "object" && aValue !== null) || type == "function";
+};
+
+cmdEditInspectInNewWindowBase.prototype = new inBaseCommand();
+
+cmdEditInspectInNewWindowBase.prototype.doTransaction =
+ function InspectInNewWindowBase_DoTransaction()
+{
+ if (cmdEditInspectInNewWindowBase.isInspectable(this.mObject)) {
+ inspectObject(this.mObject);
+ }
+};
+
+/**
+ * Copy a string to the clipboard. The mString field should be overridden to
+ * contain the string to be copied.
+ */
+function cmdEditCopySimpleStringBase()
+{
+ this.mString = null;
+}
+
+cmdEditCopySimpleStringBase.prototype = new inBaseCommand();
+
+cmdEditCopySimpleStringBase.prototype.doTransaction =
+ function CopySimpleStringBase_DoTransaction()
+{
+ if (this.mString) {
+ var helper = XPCU.getService(kClipboardHelperClassID,
+ "nsIClipboardHelper");
+ helper.copyString(this.mString);
+ }
+};
+
+/**
+ * An nsITransaction to copy items to the panelset clipboard.
+ * @param aObjects
+ * an array of objects that define a clipboard flavor, a delimiter, and
+ * toString().
+ */
+function cmdEditCopy(aObjects)
+{
+ this.mObjects = aObjects;
+}
+
+cmdEditCopy.prototype = new inBaseCommand();
+
+cmdEditCopy.prototype.doTransaction = function Utils_Copy_DoTransaction()
+{
+ if (this.mObjects.length == 1) {
+ viewer.pane.panelset.setClipboardData(this.mObjects[0],
+ this.mObjects[0].flavor,
+ this.mObjects.toString());
+ }
+ else {
+ var joinedObjects = this.mObjects.join(this.mObjects[0].delimiter);
+ viewer.pane.panelset.setClipboardData(this.mObjects,
+ this.mObjects[0].flavor + "s",
+ joinedObjects);
+ }
+}
+
+/**
+ * Open a source view on a file. The mURI field should be overridden to
+ * contain the URI of the file on which to open the source view. The
+ * mLineNumber field may optionally be set to contain the line number at which
+ * the source view should be opened.
+ */
+function cmdEditViewFileURIBase()
+{
+ this.mURI = null;
+ this.mLineNumber = 0;
+}
+
+cmdEditViewFileURIBase.prototype = new inBaseCommand();
+
+cmdEditViewFileURIBase.prototype.doTransaction =
+ function ViewFileURIBase_DoTransaction()
+{
+ if (this.mURI) {
+ // 1.9.0 toolkit doesn't have this method
+ if ("viewSource" in gViewSourceUtils) {
+ gViewSourceUtils.viewSource(this.mURI, null, null, this.mLineNumber);
+ }
+ else {
+ openDialog("chrome://global/content/viewSource.xul",
+ "_blank",
+ "all,dialog=no",
+ this.mURI, null, null, this.mLineNumber, null);
+ }
+ }
+};
diff --git a/inspector/content/jsutil/events/ObserverManager.js b/inspector/content/jsutil/events/ObserverManager.js
new file mode 100644
index 00000000..4e524bcc
--- /dev/null
+++ b/inspector/content/jsutil/events/ObserverManager.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+/***************************************************************
+* ObserverManager -----------------------------------------------
+* Manages observer and event dispatching for an object.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+////////////////////////////////////////////////////////////////////////////
+//// class ObserverManager
+
+function ObserverManager(aTarget)
+{
+ this.mTarget = aTarget;
+ this.mObservers = {};
+}
+
+ObserverManager.prototype =
+{
+ addObserver: function(aEventName, aObserver)
+ {
+ var list;
+ if (!(aEventName in this.mObservers)) {
+ list = [];
+ this.mObservers[aEventName] = list;
+ } else
+ list = this.mObservers[aEventName];
+
+
+ list.push(aObserver);
+ },
+
+ removeObserver: function(aEventName, aObserver)
+ {
+ if (aEventName in this.mObservers) {
+ var list = this.mObservers[aEventName];
+ for (var i = 0; i < list.length; ++i) {
+ if (list[i] == aObserver) {
+ list.splice(i, 1);
+ return;
+ }
+ }
+ }
+ },
+
+ dispatchEvent: function(aEventName, aEventData)
+ {
+ if (aEventName in this.mObservers) {
+ if (!("target" in aEventData))
+ aEventData.target = this.mTarget;
+ aEventData.type = aEventName;
+
+ var list = this.mObservers[aEventName];
+ for (var i = 0; i < list.length; ++i)
+ list[i].onEvent(aEventData);
+ }
+ }
+
+};
diff --git a/inspector/content/jsutil/rdf/RDFArray.js b/inspector/content/jsutil/rdf/RDFArray.js
new file mode 100644
index 00000000..84489d49
--- /dev/null
+++ b/inspector/content/jsutil/rdf/RDFArray.js
@@ -0,0 +1,163 @@
+/* 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/. */
+
+/***************************************************************
+* RDFArray -----------------------------------------------
+* A convenience routine for creating an RDF-based 2d array
+* indexed by number and hashkey.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/rdf/RDFU.js
+****************************************************************/
+
+//////////// global constants ////////////////////
+
+RDFArray.URI = "http://www.joehewitt.com/mozilla/util/RDFArray#";
+RDFArray.RDF_URI = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+
+///// class RDFArray /////////////////////////
+
+function RDFArray(aNSURI, aRootURN, aArcName)
+{
+ this.mNSURI = aNSURI;
+ this.mRootURN = aRootURN;
+ this.mArcName = aArcName ? aArcName : "list";
+}
+
+RDFArray.prototype = {
+ get datasource() { return this.mDS; },
+ get length() { return this.mLength } ,
+
+ initialize: function()
+ {
+ this.mLength = 0;
+
+ this.mDS = XPCU.createInstance("@mozilla.org/rdf/datasource;1?name=in-memory-datasource", "nsIRDFDataSource");
+
+ var root = gRDF.GetResource(this.mRootURN);
+ var res = gRDF.GetAnonymousResource();
+ this.mDS.Assert(root, gRDF.GetResource(this.mNSURI+this.mArcName), res, true);
+
+ this.mSeq = gRDFCU.MakeSeq(this.mDS, res);
+ },
+
+ add: function(aObject)
+ {
+ var res = this.createResourceFrom(aObject);
+ this.addResource(res);
+ },
+
+ addResource: function(aResource)
+ {
+ this.mSeq.AppendElement(aResource);
+ this.mLength++;
+ },
+
+ insertAt: function(aIndex, aObject)
+ {
+ var res = this.createResourceFrom(aObject);
+ this.insertResourceAt(aIndex, res);
+ },
+
+ insertResourceAt: function(aIndex, aResource)
+ {
+ },
+
+ removeAt: function(aIndex)
+ {
+ this.mSeq.RemoveElementAt(aIndex+1, true);
+ this.mLength--;
+ },
+
+ removeById: function(aId)
+ {
+ var proj = gRDF.GetResource(aId);
+ proj = XPCU.QI(proj, "nsIRDFNode");
+ this.mSeq.RemoveElement(proj, true);
+ this.mLength--;
+ },
+
+ get: function(aIndex, aKey)
+ {
+ var res = RDFU.getSeqElementAt(this.mSeq, aIndex);
+ return RDFU.readAttribute(this.mDS, res, this.mNSURI+aKey);
+ },
+
+ set: function(aIndex, aKey, aValue)
+ {
+ var res = RDFU.getSeqElementAt(this.mSeq, aIndex);
+ RDFU.writeAttribute(this.mDS, res, this.mNSURI+aKey, aValue);
+ },
+
+ getResource: function(aIndex)
+ {
+ return RDFU.getSeqElementAt(this.mSeq, aIndex);
+ },
+
+ getResourceId: function(aIndex)
+ {
+ var el = RDFU.getSeqElementAt(this.mSeq, aIndex);
+ return el.Value;
+ },
+
+ save: function()
+ {
+ RDFU.saveDataSource(this.mDS);
+ },
+
+
+ clear: function()
+ {
+ while (this.mLength) {
+ this.mSeq.RemoveElementAt(1, true);
+ this.mLength--;
+ }
+ },
+
+ createResourceFrom: function(aObject)
+ {
+ var el = gRDF.GetAnonymousResource();
+
+ // add a literal node for each property being added
+ for (var name in aObject) {
+ this.mDS.Assert(el, gRDF.GetResource(this.mNSURI+name), gRDF.GetLiteral(aObject[name]), true);
+ }
+
+ return el;
+ }
+
+};
+
+/////////// static factory methods /////////////////////////
+
+RDFArray.fromContainer = function(aDS, aResourceID, aNSURI)
+{
+ var list = new RDFArray(aNSURI);
+
+ list.mDS = aDS;
+ var seq = RDFU.findSeq(aDS, aResourceID);
+ list.mSeq = seq;
+ list.mLength = seq.GetCount();
+
+ return list;
+}
+
+RDFArray.fromContainerArc = function(aDS, aSourceId, aTargetId, aNSURI)
+{
+ var list = new RDFArray(aNSURI);
+ list.mDS = aDS;
+
+ var source = gRDF.GetResource(aSourceId);
+ var target = gRDF.GetResource(aTargetId);
+ var seqRes = aDS.GetTarget(source, target, true);
+
+ var seq = RDFU.makeSeq(aDS, seqRes);
+ list.mSeq = seq;
+ list.mLength = seq.GetCount();
+
+ return list;
+}
+
diff --git a/inspector/content/jsutil/rdf/RDFU.js b/inspector/content/jsutil/rdf/RDFU.js
new file mode 100644
index 00000000..f790d7a2
--- /dev/null
+++ b/inspector/content/jsutil/rdf/RDFU.js
@@ -0,0 +1,121 @@
+/* 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/. */
+
+/***************************************************************
+* RDFU -----------------------------------------------
+* Convenience routines for common RDF commands.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+****************************************************************/
+
+//////////// global constants ////////////////////
+
+try {
+var gRDF = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService();
+gRDF = gRDF.QueryInterface(Components.interfaces.nsIRDFService);
+
+var gRDFCU = Components.classes['@mozilla.org/rdf/container-utils;1'].getService();
+gRDFCU = gRDFCU.QueryInterface(Components.interfaces.nsIRDFContainerUtils);
+} catch (ex) { alert("RDFU: " + ex); }
+///////////////////////////////////////////////////
+
+var RDFU = {
+
+ getSeqElementAt: function(aSeq, aIndex)
+ {
+ var ordinal = gRDFCU.IndexToOrdinalResource(aIndex+1);
+ return aSeq.DataSource.GetTarget(aSeq.Resource, ordinal, true);
+ },
+
+ readAttribute: function(aDS, aRes, aName)
+ {
+ var attr = aDS.GetTarget(aRes, gRDF.GetResource(aName), true);
+ if (attr)
+ attr = XPCU.QI(attr, "nsIRDFLiteral");
+ return attr ? attr.Value : null;
+ },
+
+
+ writeAttribute: function(aDS, aRes, aName, aValue)
+ {
+ var attr = aDS.GetTarget(aRes, gRDF.GetResource(aName), true);
+ if (attr)
+ aDS.Change(aRes, gRDF.GetResource(aName), attr, gRDF.GetLiteral(aValue));
+ },
+
+
+ findSeq: function(aDS, aResName)
+ {
+ try {
+ var res = gRDF.GetResource(aResName);
+ var seq = this.makeSeq(aDS, res);
+ } catch (ex) {
+ alert("Unable to find sequence:" + ex);
+ }
+
+ return seq;
+ },
+
+ makeSeq: function(aDS, aRes)
+ {
+ var seq = XPCU.createInstance("@mozilla.org/rdf/container;1", "nsIRDFContainer");
+ seq.Init(aDS, aRes);
+ return seq;
+ },
+
+ createSeq: function(aDS, aBaseRes, aArcRes)
+ {
+ var res = gRDF.GetAnonymousResource();
+ aDS.Assert(aBaseRes, aArcRes, res, true);
+ var seq = gRDFCU.MakeSeq(aDS, res);
+ return seq;
+ },
+
+ loadDataSource: function(aURL, aListener)
+ {
+ var ds = gRDF.GetDataSource(aURL);
+ var rds = XPCU.QI(ds, "nsIRDFRemoteDataSource");
+
+ var observer = new DSLoadObserver(aListener);
+
+ if (rds.loaded) {
+ observer.onEndLoad(ds);
+ } else {
+ var sink = XPCU.QI(ds, "nsIRDFXMLSink");
+ sink.addXMLSinkObserver(observer);
+ }
+ },
+
+ saveDataSource: function(aDS)
+ {
+ var ds = XPCU.QI(aDS, "nsIRDFRemoteDataSource");
+ ds.Flush();
+ }
+};
+
+///////////
+
+function DSLoadObserver(aListener) { this.mListener = aListener; }
+
+DSLoadObserver.prototype =
+{
+ onBeginLoad: function(aSink) { },
+ onInterrupt: function(aSink) {},
+ onResume: function(aSink) {},
+ onError: function(aSink, aStatus, aErrorMsg)
+ {
+ this.mListener.onError(aStatus, aErrorMsg);
+ aSink.removeXMLSinkObserver(this);
+ },
+
+ onEndLoad: function(aSink)
+ {
+ var ds = XPCU.QI(aSink, "nsIRDFDataSource");
+ this.mListener.onDataSourceReady(ds);
+ aSink = XPCU.QI(aSink, "nsIRDFXMLSink");
+ aSink.removeXMLSinkObserver(this);
+ }
+
+};
diff --git a/inspector/content/jsutil/system/DiskSearch.js b/inspector/content/jsutil/system/DiskSearch.js
new file mode 100644
index 00000000..8645cdd2
--- /dev/null
+++ b/inspector/content/jsutil/system/DiskSearch.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+/***************************************************************
+* DiskSearch -------------------------------------------------
+* A utility for handily searching the disk for files matching
+* a certain criteria.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+//////////// global constants ////////////////////
+
+////////////////////////////////////////////////////////////////////////////
+//// class DiskSearch
+
+var DiskSearch =
+{
+ findFiles: function(aRootDir, aExtList, aRecurse)
+ {
+ // has the file extensions so we don't have to
+ // linear search the array every time through
+ var extHash = {};
+ for (var i = 0; i < aExtList.length; i++) {
+ extHash[aExtList[i]] = true;
+ }
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+ this.fileHandler = ios.getProtocolHandler("file").QueryInterface(Components.interfaces.nsIFileProtocolHandler);
+
+ // recursively build the list of results
+ var results = [];
+ this.recurseDir(aRootDir, extHash, aRecurse, results);
+ return results;
+ },
+
+ recurseDir: function(aDir, aExtHash, aRecurse, aResults)
+ {
+ debug("("+aResults.length+") entering " + aDir.path + "\n");
+ var entries = aDir.directoryEntries;
+ var entry, ext;
+ while (entries.hasMoreElements()) {
+ entry = XPCU.QI(entries.getNext(), "nsIFile");
+ if (aRecurse && entry.isDirectory())
+ this.recurseDir(entry, aExtHash, aRecurse, aResults);
+ ext = this.getExtension(entry.leafName);
+ if (ext) {
+ if (aExtHash[ext])
+ aResults.push(this.fileHandler.getURLSpecFromFile(entry));
+ }
+ }
+ },
+
+ getExtension: function(aFileName)
+ {
+ var dotpt = aFileName.lastIndexOf(".");
+ if (dotpt)
+ return aFileName.substr(dotpt+1).toLowerCase();
+
+ return null;
+ }
+};
+
diff --git a/inspector/content/jsutil/system/FilePickerUtils.js b/inspector/content/jsutil/system/FilePickerUtils.js
new file mode 100644
index 00000000..f4e4bc3c
--- /dev/null
+++ b/inspector/content/jsutil/system/FilePickerUtils.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/***************************************************************
+* FilePickerUtils -------------------------------------------------
+* A utility for easily dealing with the file picker dialog.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global constants ////////////////////
+
+const kFilePickerCID = "@mozilla.org/filepicker;1";
+const kLFileCID = "@mozilla.org/file/local;1";
+
+const nsIFilePicker = Components.interfaces.nsIFilePicker;
+
+////////////////////////////////////////////////////////////////////////////
+//// class FilePickerUtils
+
+var FilePickerUtils =
+{
+ pickFile: function(aTitle, aInitPath, aFilters, aMode)
+ {
+ try {
+ var modeStr = aMode ? "mode" + aMode : "modeOpen";
+ var mode;
+ try {
+ mode = nsIFilePicker[modeStr];
+ } catch (ex) {
+ dump("WARNING: Invalid FilePicker mode '"+aMode+"'. Defaulting to 'Open'.\n");
+ mode = nsIFilePicker.modeOpen;
+ }
+
+ var fp = XPCU.createInstance(kFilePickerCID, "nsIFilePicker");
+ fp.init(window, aTitle, mode);
+
+ // join array of filter names into bit string
+ var filters = this.prepareFilters(aFilters);
+
+ if (aInitPath) {
+ var dir = XPCU.createInstance(kLFileCID, "nsILocalFile");
+ dir.initWithPath(aInitPath);
+ fp.displayDirectory = dir;
+ }
+
+ if (fp.show() == nsIFilePicker.returnOK) {
+ return fp.file;
+ }
+ } catch (ex) {
+ dump("ERROR: Unable to open file picker.\n" + ex + "\n");
+ }
+ return null;
+ },
+
+ pickDir: function(aTitle, aInitPath)
+ {
+ try {
+ var fp = XPCU.createInstance(kFilePickerCID, "nsIFilePicker");
+ fp.init(window, aTitle, nsIFilePicker.modeGetFolder);
+
+ if (aInitPath) {
+ var dir = XPCU.createInstance(kLFileCID, "nsILocalFile");
+ dir.initWithPath(aInitPath);
+ fp.displayDirectory = dir;
+ }
+
+ if (fp.show() == nsIFilePicker.returnOK) {
+ return fp.file;
+ }
+ } catch (ex) {
+ dump("ERROR: Unable to open directory picker.\n" + ex + "\n");
+ }
+
+ return null;
+ },
+
+ prepareFilters: function(aFilters)
+ {
+ // join array of filter names into bit string
+ var filters = 0;
+ for (var i = 0; i < aFilters.length; ++i)
+ filters = filters | nsIFilePicker[aFilters[i]];
+
+ return filters;
+ }
+
+};
+
diff --git a/inspector/content/jsutil/system/PrefUtils.js b/inspector/content/jsutil/system/PrefUtils.js
new file mode 100644
index 00000000..f06208e4
--- /dev/null
+++ b/inspector/content/jsutil/system/PrefUtils.js
@@ -0,0 +1,90 @@
+/* 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/. */
+
+/***************************************************************
+* PrefUtils -------------------------------------------------
+* Utility for easily using the Mozilla preferences system.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+//////////// global constants ////////////////////
+
+const nsIPrefBranch = Components.interfaces.nsIPrefBranch;
+
+////////////////////////////////////////////////////////////////////////////
+//// class PrefUtils
+
+var PrefUtils =
+{
+ mPrefs: null,
+
+ init: function()
+ {
+ var prefService = XPCU.getService("@mozilla.org/preferences-service;1", "nsIPrefService");
+ this.mPrefs = prefService.getBranch(null);
+ },
+
+ addObserver: function addObserver(aDomain, aFunction)
+ {
+ if (!this.mPrefs) this.init();
+
+ var pbi = XPCU.QI(this.mPrefs, "nsIPrefBranch");
+ if (pbi)
+ pbi.addObserver(aDomain, aFunction, false);
+ },
+
+ removeObserver: function removeObserver(aDomain, aFunction)
+ {
+ if (!this.mPrefs) this.init();
+
+ var pbi = XPCU.QI(this.mPrefs, "nsIPrefBranch");
+ if (pbi)
+ pbi.removeObserver(aDomain, aFunction);
+ },
+
+ setPref: function(aName, aValue)
+ {
+ if (!this.mPrefs) this.init();
+
+ var type = this.mPrefs.getPrefType(aName);
+ try {
+ if (type == nsIPrefBranch.PREF_STRING) {
+ var str = Components.classes["@mozilla.org/supports-string;1"]
+ .createInstance(Components.interfaces.nsISupportsString);
+ str.data = aValue;
+ this.mPrefs.setComplexValue(aName, Components.interfaces.nsISupportsString, str);
+ } else if (type == nsIPrefBranch.PREF_BOOL) {
+ this.mPrefs.setBoolPref(aName, aValue);
+ } else if (type == nsIPrefBranch.PREF_INT) {
+ this.mPrefs.setIntPref(aName, aValue);
+ }
+ } catch(ex) {
+ debug("ERROR: Unable to write pref \"" + aName + "\".\n");
+ }
+ },
+
+ getPref: function(aName)
+ {
+ if (!this.mPrefs) this.init();
+
+ var type = this.mPrefs.getPrefType(aName);
+ try {
+ if (type == nsIPrefBranch.PREF_STRING) {
+ return this.mPrefs.getComplexValue(aName, Components.interfaces.nsISupportsString).data;
+ } else if (type == nsIPrefBranch.PREF_BOOL) {
+ return this.mPrefs.getBoolPref(aName);
+ } else if (type == nsIPrefBranch.PREF_INT) {
+ return this.mPrefs.getIntPref(aName);
+ }
+ } catch(ex) {
+ debug("ERROR: Unable to read pref \"" + aName + "\".\n");
+ }
+ return null;
+ }
+
+};
+
diff --git a/inspector/content/jsutil/system/clipboardFlavors.js b/inspector/content/jsutil/system/clipboardFlavors.js
new file mode 100644
index 00000000..1b9e598f
--- /dev/null
+++ b/inspector/content/jsutil/system/clipboardFlavors.js
@@ -0,0 +1,62 @@
+/* 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/. */
+
+/*****************************************************************************
+* Clipboard Flavors ----------------------------------------------------------
+* Flavors for copying inspected data to the clipboard.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/utils.js
+*****************************************************************************/
+
+/**
+ * Represents a CSS property.
+ * @param aProperty
+ * the name of the property
+ * @param aValue
+ * the value of the property
+ * @param aImportant
+ * boolean indicating whether this is !important
+ */
+function CSSProperty(aProperty, aValue, aImportant)
+{
+ this.flavor = "inspector/css-property";
+ this.delimiter = "\n";
+ this.property = aProperty;
+ this.value = aValue;
+ this.important = aImportant == true;
+}
+
+/**
+ * Returns a usable CSS string for the CSSProperty.
+ * @return a string in the form "property: value;"
+ */
+CSSProperty.prototype.toString = function CSSProperty_ToString()
+{
+ return this.property + ": " + this.value + (this.important ?
+ " !important" :
+ "") + ";";
+}
+
+/**
+ * Represents a DOM attribute.
+ * @param aNode
+ * the attribute node
+ */
+function DOMAttribute(aNode)
+{
+ this.flavor = "inspector/dom-attribute";
+ this.node = aNode.cloneNode(false);
+ this.delimiter = " ";
+}
+
+/**
+ * Returns a string representing an attribute name/value pair
+ * @return a string in the form of 'name="value"'
+ */
+DOMAttribute.prototype.toString = function DOMA_ToString()
+{
+ return this.node.nodeName + '="' +
+ InsUtil.unicodeToEntity(this.node.nodeValue) + '"';
+};
diff --git a/inspector/content/jsutil/xpcom/XPCU.js b/inspector/content/jsutil/xpcom/XPCU.js
new file mode 100644
index 00000000..9bef3eaf
--- /dev/null
+++ b/inspector/content/jsutil/xpcom/XPCU.js
@@ -0,0 +1,36 @@
+/* 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 XPCU =
+{
+ getService: function(aURL, aInterface)
+ {
+ try {
+ return Components.classes[aURL].getService(Components.interfaces[aInterface]);
+ } catch (ex) {
+ dump("Error getting service: " + aURL + ", " + aInterface + "\n" + ex);
+ return null;
+ }
+ },
+
+ createInstance: function (aURL, aInterface)
+ {
+ try {
+ return Components.classes[aURL].createInstance(Components.interfaces[aInterface]);
+ } catch (ex) {
+ dump("Error creating instance: " + aURL + ", " + aInterface + "\n" + ex);
+ return null;
+ }
+ },
+
+ QI: function(aEl, aIName)
+ {
+ try {
+ return aEl.QueryInterface(Components.interfaces[aIName]);
+ } catch (ex) {
+ throw("Unable to QI " + aEl + " to " + aIName);
+ }
+ }
+
+};
\ No newline at end of file
diff --git a/inspector/content/jsutil/xul/DNDUtils.js b/inspector/content/jsutil/xul/DNDUtils.js
new file mode 100644
index 00000000..a6ea144b
--- /dev/null
+++ b/inspector/content/jsutil/xul/DNDUtils.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+/***************************************************************
+* DNDUtils -------------------------------------------------
+* Utility functions for common drag and drop tasks.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var app;
+
+//////////// global constants ////////////////////
+
+////////////////////////////////////////////////////////////////////////////
+//// class DNDUtils
+
+var DNDUtils =
+{
+ invokeSession: function(aTarget, aTypes, aValues)
+ {
+ var transData, trans, supports;
+ let transArray = XPCU.createInstance("@mozilla.org/array;1", "nsIMutableArray");
+ for (var i = 0; i < aTypes.length; ++i) {
+ transData = this.createTransferableData(aValues[i]);
+ trans = XPCU.createInstance("@mozilla.org/widget/transferable;1", "nsITransferable");
+ trans.addDataFlavor(aTypes[i]);
+ trans.setTransferData (aTypes[i], transData.data, transData.size);
+ supports = trans.QueryInterface(Components.interfaces.nsISupports);
+ transArray.appendElement(supports, /* weak */ false);
+ }
+
+ var nsIDragService = Components.interfaces.nsIDragService;
+ var dragService = XPCU.getService("@mozilla.org/widget/dragservice;1", "nsIDragService");
+ dragService.invokeDragSession(aTarget, transArray, null, nsIDragService.DRAGDROP_ACTION_MOVE);
+ },
+
+ createTransferableData: function(aValue)
+ {
+ var obj = {};
+ if (typeof(aValue) == "string") {
+ obj.data = XPCU.createInstance("@mozilla.org/supports-string;1", "nsISupportsString");
+ obj.data.data = aValue;
+ obj.size = aValue.length*2;
+ } else if (false) {
+ // TODO: create data for other primitive types
+ }
+
+ return obj;
+ },
+
+ checkCanDrop: function(aType)
+ {
+ var dragService = XPCU.getService("@mozilla.org/widget/dragservice;1", "nsIDragService");
+ var dragSession = dragService.getCurrentSession();
+
+ var gotFlavor = false;
+ for (var i = 0; i < arguments.length; ++i)
+ gotFlavor |= dragSession.isDataFlavorSupported(arguments[i]);
+
+ dragSession.canDrop = gotFlavor;
+ return gotFlavor;
+ },
+
+ getData: function(aType, aIndex)
+ {
+ var dragService = XPCU.getService("@mozilla.org/widget/dragservice;1", "nsIDragService");
+ var dragSession = dragService.getCurrentSession();
+
+ var trans = XPCU.createInstance("@mozilla.org/widget/transferable;1", "nsITransferable");
+ trans.addDataFlavor(aType);
+
+ dragSession.getData(trans, aIndex);
+ var data = new Object();
+ trans.getAnyTransferData ({}, data, {});
+ return data.value;
+ }
+
+};
+
diff --git a/inspector/content/jsutil/xul/FrameExchange.js b/inspector/content/jsutil/xul/FrameExchange.js
new file mode 100644
index 00000000..8b559826
--- /dev/null
+++ b/inspector/content/jsutil/xul/FrameExchange.js
@@ -0,0 +1,34 @@
+/* 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/. */
+
+/***************************************************************
+* FrameExchange ----------------------------------------------
+* Utility for passing specific data to a document loaded
+* through an iframe
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// implementation ////////////////////
+
+var FrameExchange = {
+ mData: {},
+
+ loadURL: function(aFrame, aURL, aData)
+ {
+ this.mData[aURL] = aData;
+ aFrame.setAttribute("src", aURL);
+ },
+
+ receiveData: function(aWindow)
+ {
+ var id = aWindow.location.href;
+ var data = this.mData[id];
+ this.mData[id] = null;
+
+ return data;
+ }
+};
+
+
diff --git a/inspector/content/jsutil/xul/inBaseTreeView.js b/inspector/content/jsutil/xul/inBaseTreeView.js
new file mode 100644
index 00000000..b524ba5a
--- /dev/null
+++ b/inspector/content/jsutil/xul/inBaseTreeView.js
@@ -0,0 +1,96 @@
+/* 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/. */
+
+/***************************************************************
+* inBaseTreeView -------------------------------------------------
+* Simple tree view object meant to be extended.
+*
+* Usage example: MyView.prototype = new inBaseTreeView();
+****************************************************************/
+
+//XXX Don't use anonymous functions
+function inBaseTreeView() { }
+
+inBaseTreeView.prototype =
+{
+ mRowCount: 0,
+ mTree: null,
+
+ get rowCount() { return this.mRowCount; },
+ setTree: function(aTree) { this.mTree = aTree; },
+ getCellText: function(aRow, aCol) { return ""; },
+ getRowProperties: function(aIndex, aProperties) { return ""; },
+ getCellProperties: function(aIndex, aCol, aProperties) { return ""; },
+ getColumnProperties: function(aCol, aProperties) { return ""; },
+ getParentIndex: function(aRowIndex) { },
+ hasNextSibling: function(aRowIndex, aAfterIndex) { },
+ getLevel: function(aIndex) {},
+ getImageSrc: function(aRow, aCol) {},
+ getProgressMode: function(aRow, aCol) {},
+ getCellValue: function(aRow, aCol) {},
+ isContainer: function(aIndex) {},
+ isContainerOpen: function(aIndex) {},
+ isContainerEmpty: function(aIndex) {},
+ isSeparator: function(aIndex) {},
+ isSorted: function() {},
+ toggleOpenState: function(aIndex) {},
+ selectionChanged: function() {},
+ cycleHeader: function(aCol) {},
+ cycleCell: function(aRow, aCol) {},
+ isEditable: function(aRow, aCol) {},
+ isSelectable: function(aRow, aCol) {},
+ setCellValue: function(aRow, aCol, aValue) {},
+ setCellText: function(aRow, aCol, aValue) {},
+ performAction: function(aAction) {},
+ performActionOnRow: function(aAction, aRow) {},
+ performActionOnCell: function(aAction, aRow, aCol) {},
+
+
+ // extra utility stuff
+
+ createAtom: function createAtom(aVal)
+ {
+ try {
+ var i = Components.interfaces.nsIAtomService;
+ var svc = Components.classes["@mozilla.org/atom-service;1"].getService(i);
+ return svc.getAtom(aVal);
+ } catch(ex) {
+ return null;
+ }
+ },
+
+ /**
+ * Returns an array of selected indices in the tree.
+ * @return an array of indices
+ */
+ getSelectedIndices: function getSelectedIndices()
+ {
+ var indices = [];
+ var rangeCount = this.selection.getRangeCount();
+ for (var i = 0; i < rangeCount; i++) {
+ var start = {};
+ var end = {};
+ this.selection.getRangeAt(i,start,end);
+ for (var c = start.value; c <= end.value; c++) {
+ indices.push(c);
+ }
+ }
+ return indices;
+ },
+
+ /**
+ * Returns an array of row objects selected in the tree.
+ * @return an array of row objects
+ */
+ getSelectedRowObjects: function getSelectedRowObjects()
+ {
+ var declarations = [];
+ var indices = this.getSelectedIndices();
+ for (var i = 0; i < indices.length; i++) {
+ declarations.push(this.getRowObjectFromIndex(indices[i]));
+ }
+ return declarations;
+ }
+
+};
diff --git a/inspector/content/jsutil/xul/inDataTreeView.js b/inspector/content/jsutil/xul/inDataTreeView.js
new file mode 100644
index 00000000..b3662a94
--- /dev/null
+++ b/inspector/content/jsutil/xul/inDataTreeView.js
@@ -0,0 +1,243 @@
+/* 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/. */
+
+
+///////////////////////////////////////////////////////////////////////////////
+//// inDataTreeView
+
+function inDataTreeView()
+{
+ this.mRows = [];
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// inDataTreeView. nsITreeView interface implementation
+
+inDataTreeView.prototype = new inBaseTreeView();
+
+inDataTreeView.prototype.__defineGetter__("rowCount",
+ function inDataTreeView_rowCount()
+ {
+ return this.mRows.length;
+ }
+);
+
+inDataTreeView.prototype.getCellText =
+ function inDataTreeView_getCellText(aRowIdx, aCol)
+{
+ var row = this.getRowAt(aRowIdx);
+ return row ? row.node.data[aCol.id] : "";
+};
+
+inDataTreeView.prototype.isContainer =
+ function inDataTreeView_isContainer(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ return row ? row.node.children.length > 0 : false;
+};
+
+inDataTreeView.prototype.isContainerOpen =
+ function inDataTreeView_isContainerOpen(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ return row ? row.isOpen : false;
+};
+
+inDataTreeView.prototype.isContainerEmpty =
+ function inDataTreeView_isContainerEmpty(aRowIdx)
+{
+ return !this.isContainer(aRowIdx);
+};
+
+inDataTreeView.prototype.getLevel =
+ function inDataTreeView_getLevel(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ return row ? row.node.level : 0;
+};
+
+inDataTreeView.prototype.getParentIndex =
+ function inDataTreeView_getParentIndex(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ if (!row) {
+ return -1;
+ }
+
+ for (let i = aRowIdx - 1; i >= 0; --i) {
+ let checkRow = this.getRowAt(i);
+ if (checkRow.node == row.node.parent) {
+ return i;
+ }
+ }
+
+ return -1;
+};
+
+inDataTreeView.prototype.hasNextSibling =
+ function inDataTreeView_hasNextSibling(aRowIdx, aAfterRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ if (!row || !row.node.parent) {
+ return false;
+ }
+
+ var lastIdx = row.node.parent.children.length - 1;
+ return row.node.parent.children[lastIdx] != row.node;
+};
+
+inDataTreeView.prototype.toggleOpenState =
+ function inDataTreeView_toggleOpenState(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ if (!row) {
+ return;
+ }
+
+ var oldCount = this.rowCount;
+ if (row.isOpen) {
+ this.collapseRowAt(aRowIdx);
+ }
+ else {
+ this.expandRowAt(aRowIdx);
+ }
+
+ this.mTree.invalidateRow(aRowIdx);
+ this.mTree.rowCountChanged(aRowIdx + 1, this.rowCount - oldCount);
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// inDataTreeView. Public.
+
+/**
+ * Append the child row to the row at the given index.
+ */
+inDataTreeView.prototype.appendChild =
+ function inDataTreeView_appendChild(aParent, aData)
+{
+ var node = new inDataTreeViewNode(aData);
+ if (aParent) {
+ node.level = aParent.level + 1;
+ node.parent = aParent;
+ aParent.children.push(node);
+ return node;
+ }
+
+ this.mRows.push(new inDataTreeViewRow(node));
+ return node;
+};
+
+/**
+ * Expand nodes from the given list. The caller should ensure that every node
+ * to expand precedes its ancestors in the list, in other words the list should
+ * be in reverse order, and include all ancestor nodes.
+ */
+inDataTreeView.prototype.expandNodes =
+ function inDataTreeView_expandNodes(aNodes)
+{
+ var node = aNodes.pop();
+ for (let rowIdx = 0; node && rowIdx < this.mRows.length; rowIdx++) {
+ if (this.getRowAt(rowIdx).node == node) {
+ this.expandRowAt(rowIdx);
+ node = aNodes.pop();
+ }
+ }
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+//// inDataTreeView. Tree utils.
+
+/**
+ * Expands a tree node on the given row.
+ */
+inDataTreeView.prototype.expandRowAt =
+ function inDataTreeView_expandRowAt(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ if (!row || row.isOpen) {
+ return;
+ }
+
+ var kids = row.node.children;
+ var kidCount = kids.length;
+
+ for (let i = this.rowCount - 1; i > aRowIdx; --i) {
+ this.mRows[i + kidCount] = this.mRows[i];
+ }
+
+ for (let i = 0; i < kidCount; ++i) {
+ this.mRows[aRowIdx + i + 1] = new inDataTreeViewRow(kids[i]);
+ }
+
+ row.isOpen = true;
+};
+
+/**
+ * Collapse a tree node on the given row.
+ */
+inDataTreeView.prototype.collapseRowAt =
+ function inDataTreeView_collapseRowAt(aRowIdx)
+{
+ var row = this.getRowAt(aRowIdx);
+ if (!row || !row.isOpen) {
+ return;
+ }
+
+ var removeCount = 0;
+ var rowLevel = row.node.level;
+ for (let idx = aRowIdx + 1; idx < this.rowCount; idx++) {
+ if (this.getRowAt(idx).node.level <= rowLevel) {
+ removeCount = idx - aRowIdx - 1;
+ break;
+ }
+ }
+ this.mRows.splice(aRowIdx + 1, removeCount);
+
+ row.isOpen = false;
+};
+
+/**
+ * Return a tree row object by the given row index.
+ */
+inDataTreeView.prototype.getRowAt =
+ function inDataTreeView_getRowAt(aRowIdx)
+{
+ if (aRowIdx < 0 || aRowIdx >= this.rowCount) {
+ return null;
+ }
+
+ return this.mRows[aRowIdx];
+}
+
+/**
+ * Return a tree row data object by the given row index.
+ */
+inDataTreeView.prototype.getDataAt =
+ function inDataTreeView_getDataAt(aRowIdx)
+{
+ if (aRowIdx < 0 || aRowIdx >= this.rowCount) {
+ return null;
+ }
+
+ return this.mRows[aRowIdx].node.data;
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// inDataTreeViewNode
+
+function inDataTreeViewNode(aData)
+{
+ this.parent = null;
+ this.children = [];
+
+ this.level = 0;
+ this.data = aData;
+}
+
+function inDataTreeViewRow(aNode)
+{
+ this.node = aNode;
+ this.isOpen = false;
+}
diff --git a/inspector/content/jsutil/xul/inFormManager.js b/inspector/content/jsutil/xul/inFormManager.js
new file mode 100644
index 00000000..1c03d5cc
--- /dev/null
+++ b/inspector/content/jsutil/xul/inFormManager.js
@@ -0,0 +1,111 @@
+/* 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/. */
+
+/***************************************************************
+* inFormManager -------------------------------------------------
+* Manages the reading and writing of forms via simple maps of
+* attribute/value pairs. A "form" is simply a XUL window which
+* contains "form widgets" such as textboxes and menulists.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+////////////////////////////////////////////////////////////////////////////
+//// class inFormManager
+
+var inFormManager =
+{
+ // Object
+ readWindow: function(aWindow, aIds)
+ {
+ var el, fn;
+ var doc = aWindow.document;
+ var map = {};
+ for (var i = 0; i < aIds.length; i++) {
+ el = doc.getElementById(aIds[i]);
+ if (el) {
+ this.persistIf(doc, el);
+ fn = this["read_"+el.localName];
+ if (fn)
+ map[aIds[i]] = fn(el);
+ }
+ }
+
+ return map;
+ },
+
+ // void
+ writeWindow: function(aWindow, aMap)
+ {
+ var el, fn;
+ var doc = aWindow.document;
+ for (var i = 0; i < aIds.length; i++) {
+ el = doc.getElementById(aIds[i]);
+ if (el) {
+ fn = this["write_"+el.localName];
+ if (fn)
+ fn(el, aMap[aIds[i]]);
+ }
+ }
+ },
+
+ persistIf: function(aDoc, aEl)
+ {
+ if (aEl.getAttribute("persist") == "true" && aEl.hasAttribute("id")) {
+ aEl.setAttribute("value", aEl.value);
+ aDoc.persist(aEl.getAttribute("id"), "value");
+ }
+ },
+
+ read_textbox: function(aEl)
+ {
+ return aEl.value;
+ },
+
+ read_menulist: function(aEl)
+ {
+ return aEl.getAttribute("value");
+ },
+
+ read_checkbox: function(aEl)
+ {
+ return aEl.getAttribute("checked") == "true";
+ },
+
+ read_radiogroup: function(aEl)
+ {
+ return aEl.getAttribute("value");
+ },
+
+ read_colorpicker: function(aEl)
+ {
+ return aEl.getAttribute("color");
+ },
+
+ write_textbox: function(aEl, aValue)
+ {
+ aEl.setAttribute("value", aValue);
+ },
+
+ write_menulist: function(aEl, aValue)
+ {
+ aEl.setAttribute("value", aValue);
+ },
+
+ write_checkbox: function(aEl, aValue)
+ {
+ aEl.setAttribute("checked", aValue);
+ },
+
+ write_radiogroup: function(aEl, aValue)
+ {
+ aEl.setAttribute("value", aValue);
+ },
+
+ write_colorpicker: function(aEl, aValue)
+ {
+ aEl.color = aValue;
+ }
+};
+
diff --git a/inspector/content/jsutil/xul/inTreeBuilder.js b/inspector/content/jsutil/xul/inTreeBuilder.js
new file mode 100644
index 00000000..5387375f
--- /dev/null
+++ b/inspector/content/jsutil/xul/inTreeBuilder.js
@@ -0,0 +1,636 @@
+/* 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/. */
+
+/***************************************************************
+* inTreeBuilder -------------------------------------------------
+* Automatically builds up an tree so that it will display a tabular
+* set of data with titled columns and optionally an icon for each row.
+* The tree that is supplied must have an treechildren with an
+* empty template inside of it.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xul/DNDUtils.js
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var inTreeBuilderPartCount = 0;
+
+//////////// global constants ////////////////////
+
+////////////////////////////////////////////////////////////////////////////
+//// class inTreeBuilder
+
+function inTreeBuilder(aTree, aNameSpace, aArcName)
+{
+ this.tree = aTree;
+ this.nameSpace = aNameSpace;
+ this.arcName = aArcName;
+
+ this.mColumns = [];
+ this.mExtras = [];
+}
+
+inTreeBuilder.prototype =
+{
+ mTree: null,
+ mTreeBody: null,
+ // datasource stuff
+ mNameSpace: null,
+ mArcName: null,
+ mIsRefContainer: false,
+ mIsContainer: true,
+
+ // table structure stuff
+ mIsIconic: false,
+ mColumns: null,
+ mSplitters: true,
+ mRowFields: null,
+ mRowAttrs: null,
+ mAllowDND: false,
+ mLastDragCol: null,
+ mLastDragColWhere: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// properties
+
+ // the xul tree node we will construct
+ get tree() { return this.mTree },
+ set tree(aVal)
+ {
+ this.mTree = aVal;
+ if (aVal)
+ this.mTreeBody = aVal.getElementsByTagName("treechildren")[0];
+ aVal._treeBuilder = this
+ },
+
+ // the namespace to use for all fields
+ get nameSpace() { return this.mNameSpace },
+ set nameSpace(aVal) { this.mNameSpace = aVal },
+
+ // the name of the arc to the container
+ get arcName() { return this.mArcName },
+ set arcName(aVal) { this.mArcName = aVal },
+
+ // is the datasource ref an arc to a container, or a container itself
+ get isRefContainer() { return this.mIsRefContainer },
+ set isRefContainer(aVal) { this.mIsRefContainer = aVal },
+
+ // is each row a potential container?
+ get isContainer() { return this.mIsContainer },
+ set isContainer(aVal) { this.mIsContainer = aVal },
+
+ // place an icon before the first column of each row
+ get isIconic() { return this.mIsIconic },
+ set isIconic(aVal) { this.mIsIconic = aVal },
+
+ // put splitters between the columns?
+ get useSplitters() { return this.mSplitters },
+ set useSplitters(aVal) { this.mSplitters = aVal },
+
+ // extra attributes to set on the treeitem
+ get rowAttributes() { return this.mRowAttrs },
+ set rowAttributes(aVal) { this.mRowAttrs = aVal },
+
+ // extra data fields to set on the treeitem
+ get rowFields() { return this.mRowFields },
+ set rowFields(aVal) { this.mRowFields = aVal },
+
+ // extra data fields to set on the treeitem
+ get allowDragColumns() { return this.mAllowDND },
+ set allowDragColumns(aVal) { this.mAllowDND = aVal },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// event handlers
+
+ get onColumnAdd() { return this.mOnColumnAdd },
+ set onColumnAdd(aFn) { this.mOnColumnAdd = aFn },
+
+ get onColumnRemove() { return this.mOnColumnRemove },
+ set onColumnRemove(aFn) { this.mOnColumnRemove = aFn },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// initialization
+
+ initialize: function()
+ {
+ this.initColumns();
+ this.initTemplate();
+ },
+
+ initColumns: function()
+ {
+ this.initDND();
+ },
+
+ initTemplate: function()
+ {
+ var template = this.mTree.getElementsByTagNameNS(kXULNSURI, "template")[0];
+ this.mTemplate = template;
+ this.clearChildren(template);
+
+ var rule = document.createElementNS(kXULNSURI, "rule");
+ template.appendChild(rule);
+ this.mRule = rule;
+
+ this.createDefaultConditions();
+
+ var bindings = document.createElementNS(kXULNSURI, "bindings");
+ rule.appendChild(bindings);
+ this.mBindings = bindings;
+
+ this.createDefaultAction();
+ },
+
+ createDefaultConditions: function()
+ {
+ var conditions = document.createElementNS(kXULNSURI, "conditions");
+ this.mRule.appendChild(conditions);
+
+ var content = document.createElementNS(kXULNSURI, "treerow");
+ content.setAttribute("uri", "?uri");
+ conditions.appendChild(content);
+
+ var triple = this.createTriple("?uri", this.mNameSpace+this.mArcName,
+ this.mIsRefContainer ? "?table" : "?row");
+ conditions.appendChild(triple);
+
+ if (this.mIsRefContainer) {
+ var member = this.createMember("?table", "?row");
+ conditions.appendChild(member);
+ }
+ },
+
+ createDefaultAction: function()
+ {
+ var action = document.createElementNS(kXULNSURI, "action");
+ this.mRule.appendChild(action);
+
+ var orow = this.createTemplatePart("treerow");
+ orow.setAttribute("uri", "?row");
+ action.appendChild(orow);
+ this.mTreeRow = orow;
+
+ // assign the item attributes
+ if (this.mRowAttrs)
+ for (key in this.mRowAttrs)
+ orow.setAttribute(key, this.mRowAttrs[key]);
+ },
+
+ createDefaultBindings: function()
+ {
+ // assign the item fields
+ var binding;
+ if (this.mRowFields) {
+ var props = "";
+ for (key in this.mRowFields) {
+ binding = this.createBinding(this.mRowFields[key]);
+ this.mBindings.appendChild(binding);
+ props += key+"-?"+this.mRowFields[key]+" ";
+ }
+ this.mTreeRow.setAttribute("properties", props);
+ }
+ },
+
+ createTriple: function(aSubject, aPredicate, aObject)
+ {
+ var triple = document.createElementNS(kXULNSURI, "triple");
+ triple.setAttribute("subject", aSubject);
+ triple.setAttribute("predicate", aPredicate);
+ triple.setAttribute("object", aObject);
+
+ return triple;
+ },
+
+ createMember: function(aContainer, aChild)
+ {
+ var member = document.createElementNS(kXULNSURI, "member");
+ member.setAttribute("container", aContainer);
+ member.setAttribute("child", aChild);
+
+ return member;
+ },
+
+ reset: function()
+ {
+ this.mColumns = [];
+ this.mIsIconic = false;
+
+ this.resetTree();
+ },
+
+ resetTree: function()
+ {
+ var kids = this.mTree.childNodes;
+ for (var i = kids.length - 1; i >= 0; --i)
+ if (kids[i].localName != "treechildren")
+ this.mTree.removeChild(kids[i]);
+
+ this.clearChildren(this.mBindings);
+ this.clearChildren(this.mTreeRow);
+ this.createDefaultBindings();
+ },
+
+ clearChildren: function(aEl)
+ {
+ while (aEl.hasChildNodes())
+ aEl.removeChild(aEl.lastChild);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// column drag and drop
+
+ initDND: function()
+ {
+ if (this.mAllowDND) {
+ // addEventListener doesn't work for dnd events, apparently... so we use the attributes
+ this.addDNDListener(this.mTree, "ondragenter");
+ this.addDNDListener(this.mTree, "ondragover");
+ this.addDNDListener(this.mTree, "ondragexit");
+ this.addDNDListener(this.mTree, "ondragstart");
+ this.addDNDListener(this.mTree, "ondrop");
+ }
+ },
+
+ onDragEnter: function(aEvent)
+ {
+ },
+
+ onDragOver: function(aEvent)
+ {
+ if (!DNDUtils.checkCanDrop("TreeBuilder/column-add"))
+ return;
+
+ var idx = this.getColumnIndexFromX(aEvent.clientX, 0.5);
+ this.mColumnInsertIndex = idx;
+ var col = this.getColumnAt(idx);
+
+ if (idx == -1)
+ this.markColumnInsert(col, "after");
+ else
+ this.markColumnInsert(col, "before");
+ },
+
+ onDragExit: function(aEvent)
+ {
+ },
+
+ onDrop: function(aEvent)
+ {
+ this.markColumnInsert(null);
+ var dragService = XPCU.getService("@mozilla.org/widget/dragservice;1", "nsIDragService");
+ var dragSession = dragService.getCurrentSession();
+
+ if (!dragSession.isDataFlavorSupported("TreeBuilder/column-add"))
+ return false;
+
+ var trans = XPCU.createInstance("@mozilla.org/widget/transferable;1", "nsITransferable");
+ trans.addDataFlavor("TreeBuilder/column-add");
+
+ dragSession.getData(trans, 0);
+ var data = {};
+ trans.getAnyTransferData ({}, data, {});
+
+ var string = XPCU.QI(data.value, "nsISupportsString");
+
+ this.insertColumn(this.mColumnInsertIndex,
+ {
+ name: string.data,
+ title: string.data,
+ flex: 1
+ });
+
+
+ // if we rebuildContent during this thread, it will crash in the dnd service
+ setTimeout(function(me) { me.build(); me.buildContent() }, 1, this);
+
+ // bug 56270 - dragSession.sourceDocument is null --
+ // causes me to code this very temporary, very nasty hack
+ // to tell columnsDialog.js about the drop
+ if (this.mTree.onClientDrop) {
+ this.mTree.onClientDrop();
+ }
+ },
+
+ markColumnInsert: function (aColumn, aWhere)
+ {
+ var col = this.mLastDragCol;
+ var lastWhere = this.mLastDragColWhere;
+ if (col)
+ col.setAttribute("properties", "");
+
+ if (aWhere != "before" && aWhere != "after") {
+ this.mLastDragCol = null;
+ this.mLastDragColWhere = null;
+ return;
+ }
+
+ col = aColumn;
+ if (col) {
+ this.mLastDragCol = col;
+ this.mLastDragColWhere = aWhere;
+ col.setAttribute("properties", "dnd-insert-"+aWhere);
+ }
+
+ var bx = this.mTree.boxObject.QueryInterface(Components.interfaces.nsITreeBoxObject);
+ bx.invalidate();
+ },
+
+ getColumnIndexFromX: function(aX, aThresh)
+ {
+ var width = 0;
+ var col, cw;
+ for (var i = 0; i < this.columnCount; ++i) {
+ col = this.getColumnAt(i);
+ cw = col.boxObject.width;
+ width += cw;
+ if (width-(cw*aThresh) > aX)
+ return i;
+ }
+
+ return -1;
+ },
+
+ getColumnIndexFromHeader: function(aHeader)
+ {
+ var headers = this.mTree.getElementsByTagName("treecol");
+ for (var i = 0; i < headers.length; ++i) {
+ if (headers[i] == aHeader)
+ return i;
+ }
+ return -1;
+ },
+
+ //// for drag-n-drop removal/arrangement of columns
+
+ onDragStart: function(aEvent)
+ {
+ var target = aEvent.target;
+ if (target.parentNode == this.mTree) {
+ var column = target.getAttribute("label");
+
+ var idx = this.getColumnIndexFromHeader(target);
+ if (idx == -1) return;
+ this.mColumnDragging = idx;
+
+ DNDUtils.invokeSession(target, ["TreeBuilder/column-remove"], [column]);
+ }
+ },
+
+ addColumnDropTarget: function(aBox)
+ {
+ aBox._treeBuilderDropTarget = this;
+ this.addDNDListener(aBox, "ondragover", "Target");
+ this.addDNDListener(aBox, "ondrop", "Target");
+ },
+
+ removeColumnDropTarget: function(aBox)
+ {
+ aBox._treeBuilderDropTarget = this;
+ this.removeDNDListener(aBox, "ondragover", "Target");
+ this.removeDNDListener(aBox, "ondrop", "Target");
+ },
+
+ onDragOverTarget: function(aBox, aEvent)
+ {
+ DNDUtils.checkCanDrop("TreeBuilder/column-remove");
+ },
+
+ onDropTarget: function(aBox, aEvent)
+ {
+ this.removeColumn(this.mColumnDragging);
+ this.build();
+ // if we rebuildContent during this thread, it will crash in the dnd service
+ setTimeout(function(aTreeBuilder) { aTreeBuilder.buildContent() }, 1, this);
+ },
+
+ // these are horrible hacks to compensate for the lack of true multiple
+ // listener support for the DND events
+
+ addDNDListener: function(aBox, aType, aModifier)
+ {
+ var js = "inTreeBuilder_"+aType+(aModifier?"_"+aModifier:"")+"(this, event);";
+
+ var attr = aBox.getAttribute(aType);
+ attr = attr ? attr : "";
+ aBox.setAttribute(aType, attr + js);
+ },
+
+ removeDNDListener: function(aBox, aType, aModifier)
+ {
+ var js = "inTreeBuilder_"+aType+(aModifier?"_"+aModifier:"")+"(this, event);";
+
+ var attr = aBox.getAttribute(aType);
+ var idx = attr.indexOf(js);
+ if (idx != -1)
+ attr = attr.substr(0,idx) + attr.substr(idx+1);
+ aBox.setAttribute(aType, attr);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// column properties
+
+ addColumn: function(aData)
+ {
+ this.mColumns.push(aData);
+
+ if (this.mOnColumnAdd)
+ this.mOnColumnAdd(this.mColumns.length-1)
+ },
+
+ insertColumn: function(aIndex, aData)
+ {
+ var idx;
+ if (aIndex == -1) {
+ this.mColumns.push(aData);
+ idx = this.mColumns.length-1;
+ } else {
+ this.mColumns.splice(aIndex, 0, aData);
+ idx = aIndex;
+ }
+
+ if (this.mOnColumnAdd)
+ this.mOnColumnAdd(idx);
+ },
+
+ removeColumn: function(aIndex)
+ {
+ this.mColumns.splice(aIndex, 1);
+
+ if (this.mOnColumnRemove)
+ this.mOnColumnRemove(aIndex);
+ },
+
+ getColumnAt: function(aIndex)
+ {
+ var kids = this.mTree.getElementsByTagName("treecol");
+ return aIndex < 0 || aIndex >= kids.length ? kids[kids.length-1] : kids[aIndex];
+ },
+
+ get columnCount() { return this.mColumns.length },
+
+ getColumnName: function(aIndex) { return this.mColumns[aIndex].name },
+ setColumnName: function(aIndex, aValue) { this.mColumns[aIndex].name = aValue },
+
+ getColumnTitle: function(aIndex) { return this.mColumns[aIndex].title },
+ setColumnTitle: function(aIndex, aValue) { this.mColumns[aIndex].title = aValue },
+
+ getColumnClassName: function(aIndex) { return this.mColumns[aIndex].className },
+ setColumnClassName: function(aIndex, aValue) { this.mColumns[aIndex].className = aValue },
+
+ getColumnFlex: function(aIndex) { return this.mColumns[aIndex].flex },
+ setColumnFlex: function(aIndex, aValue) { this.mColumns[aIndex].flex = aValue },
+
+ getExtras: function(aIndex) { return this.mColumns[aIndex].extras },
+ setExtras: function(aIndex, aValue) { this.mColumns[aIndex].extras = aExtras },
+
+ getAttrs: function(aIndex) { return this.mColumns[aIndex].attrs },
+ setAttrs: function(aIndex, aValue) { this.mColumns[aIndex].attrs = aExtras },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// template building
+
+ build: function(aBuildContent)
+ {
+ try {
+ this.resetTree();
+ this.buildColumns();
+ this.buildTemplate();
+ if (aBuildContent)
+ this.buildContent();
+ } catch (ex) {
+ debug("### ERROR - inTreeBuilder::build failed.\n" + ex);
+ }
+
+ //dumpDOM2(this.mTree);
+
+ },
+
+ buildContent: function()
+ {
+ this.mTreeBody.builder.rebuild();
+ },
+
+ buildColumns: function()
+ {
+ var cols = document.createElementNS(kXULNSURI, "treecols");
+ var col, val, split;
+ for (var i = 0; i < this.mColumns.length; i++) {
+ col = document.createElementNS(kXULNSURI, "treecol");
+ col.setAttribute("id", "treecol-"+this.mColumns[i].name);
+ col.setAttribute("persist", "width");
+ col.setAttribute("label", this.mColumns[i].title);
+
+ // mark first node as primary, if necessary
+ if (i == 0 && this.mIsContainer)
+ col.setAttribute("primary", "true");
+
+ val = cols[i].className;
+ if (val)
+ col.setAttribute("class", val);
+ val = cols[i].flex;
+ if (val)
+ col.setAttribute("flex", val);
+
+ cols.appendChild(col);
+
+ if (this.mSplitters && i < this.mColumns.length-1) {
+ split = document.createElementNS(kXULNSURI, "splitter");
+ split.setAttribute("class", "tree-splitter");
+ cols.appendChild(split);
+ }
+ }
+ this.mTree.appendChild(cols);
+ },
+
+ buildTemplate: function()
+ {
+ var cols = this.mColumns;
+ var bindings = this.mBindings;
+ var row = this.mTreeRow;
+ var cell, binding, val, extras, attrs, key, className;
+
+ if (this.mIsIconic) {
+ binding = this.createBinding("_icon");
+ bindings.appendChild(binding);
+ }
+
+ for (var i = 0; i < cols.length; ++i) {
+ val = cols[i].name;
+ if (!val)
+ throw "Column data is incomplete - missing name at index " + i + ".";
+
+ cell = this.createTemplatePart("treecell");
+ className = "";
+
+ // build the default value data field
+ binding = this.createBinding(val);
+ bindings.appendChild(binding);
+
+ cell.setAttribute("label", "?" + val);
+ cell.setAttribute("ref", "treecol-"+cols[i].name);
+ cell.setAttribute("class", className);
+
+ var props = "";
+ for (key in this.mRowFields)
+ props += key+"-?"+this.mRowFields[key]+" ";
+ cell.setAttribute("properties", props);
+
+ row.appendChild(cell);
+ }
+ },
+
+ createBinding: function(aName)
+ {
+ var binding = document.createElementNS(kXULNSURI, "binding");
+ binding.setAttribute("subject", "?row");
+ binding.setAttribute("predicate", this.mNameSpace+aName);
+ binding.setAttribute("object", "?" + aName);
+ return binding;
+ },
+
+ createTemplatePart: function(aTagName)
+ {
+ var el = document.createElementNS(kXULNSURI, aTagName);
+ el.setAttribute("id", "templatePart"+inTreeBuilderPartCount);
+ inTreeBuilderPartCount++;
+ return el;
+ }
+
+};
+
+function inTreeBuilder_ondragstart(aTree, aEvent)
+{
+ return aTree._treeBuilder.onDragStart(aEvent);
+}
+
+function inTreeBuilder_ondragenter(aTree, aEvent)
+{
+ return aTree._treeBuilder.onDragEnter(aEvent);
+}
+
+function inTreeBuilder_ondragover(aTree, aEvent)
+{
+ return aTree._treeBuilder.onDragOver(aEvent);
+}
+
+function inTreeBuilder_ondragexit(aTree, aEvent)
+{
+ return aTree._treeBuilder.onDragExit(aEvent);
+}
+
+function inTreeBuilder_ondrop(aTree, aEvent)
+{
+ return aTree._treeBuilder.onDrop(aEvent);
+}
+
+function inTreeBuilder_ondragover_Target(aBox, aEvent)
+{
+ return aBox._treeBuilderDropTarget.onDragOverTarget(aBox, aEvent);
+}
+
+function inTreeBuilder_ondrop_Target(aBox, aEvent)
+{
+ return aBox._treeBuilderDropTarget.onDropTarget(aBox, aEvent);
+}
diff --git a/inspector/content/keysetOverlay.xul b/inspector/content/keysetOverlay.xul
new file mode 100644
index 00000000..c0a2dd53
--- /dev/null
+++ b/inspector/content/keysetOverlay.xul
@@ -0,0 +1,21 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/moz.build b/inspector/content/moz.build
new file mode 100644
index 00000000..e0eb66aa
--- /dev/null
+++ b/inspector/content/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/inspector/content/object.js b/inspector/content/object.js
new file mode 100644
index 00000000..6e622650
--- /dev/null
+++ b/inspector/content/object.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+/***************************************************************
+* ObjectApp -------------------------------------------------
+* The primary object that controls the Inspector compact app.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var inspector;
+
+//////////// global constants ////////////////////
+
+//////////////////////////////////////////////////
+
+window.addEventListener("load", ObjectApp_initialize, false);
+window.addEventListener("unload", ObjectApp_destroy, false);
+
+function ObjectApp_initialize()
+{
+ inspector = new ObjectApp();
+ inspector.initialize();
+}
+
+function ObjectApp_destroy()
+{
+ inspector.destroy();
+}
+
+////////////////////////////////////////////////////////////////////////////
+//// class ObjectApp
+
+function ObjectApp()
+{
+}
+
+ObjectApp.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ initialize: function()
+ {
+ this.mInitTarget = window.arguments && window.arguments.length > 0 ? window.arguments[0] : null;
+
+ this.mPanelSet = document.getElementById("bxPanelSet");
+ this.mPanelSet.addObserver("panelsetready", this, false);
+ this.mPanelSet.initialize();
+ },
+
+ destroy: function()
+ {
+ },
+
+ doViewerCommand: function(aCommand)
+ {
+ this.mPanelSet.execCommand(aCommand);
+ },
+
+ getViewer: function(aUID)
+ {
+ return this.mPanelSet.registry.getViewerByUID(aUID);
+ },
+
+ onEvent: function(aEvent)
+ {
+ switch (aEvent.type) {
+ case "panelsetready":
+ this.initViewerPanels();
+ }
+ },
+
+ initViewerPanels: function()
+ {
+ if (this.mInitTarget)
+ this.target = this.mInitTarget;
+ },
+
+ set target(aObj)
+ {
+ this.mPanelSet.getPanel(0).subject = aObj;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+//// event listeners
+
diff --git a/inspector/content/object.xul b/inspector/content/object.xul
new file mode 100644
index 00000000..16d03713
--- /dev/null
+++ b/inspector/content/object.xul
@@ -0,0 +1,44 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/popupOverlay.xul b/inspector/content/popupOverlay.xul
new file mode 100644
index 00000000..f7609120
--- /dev/null
+++ b/inspector/content/popupOverlay.xul
@@ -0,0 +1,102 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/prefs/pref-inspector.js b/inspector/content/prefs/pref-inspector.js
new file mode 100644
index 00000000..435eeb29
--- /dev/null
+++ b/inspector/content/prefs/pref-inspector.js
@@ -0,0 +1,49 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: ts=2 sw=2 sts=2
+ * 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/. */
+
+function Startup()
+{
+ SidebarPrefs_initialize();
+ enableBlinkPrefs(document.getElementById("inspector.blink.on").value);
+}
+
+function enableBlinkPrefs(aTruth)
+{
+ /*
+ * define the pair of label and control used in the prefpane to allow
+ * disabling of both elements, if a pref is locked.
+ */
+ let els = {
+ lbElBorderColor: "cprElBorderColor",
+ lbElBorderWidth: "txfElBorderWidth",
+ lbElDuration: "txfElDuration",
+ lbElSpeed: "txfElSpeed",
+ "": "cbElInvert"
+ };
+
+ for (let [label, control] in Iterator(els)) {
+ let controlElem = document.getElementById(control);
+
+ // only remove disabled attribute, if pref isn't locked
+ if (aTruth && !isPrefLocked(controlElem)) {
+ controlElem.removeAttribute("disabled");
+ if (label)
+ document.getElementById(label).removeAttribute("disabled");
+ } else {
+ controlElem.setAttribute("disabled", true);
+ if (label)
+ document.getElementById(label).setAttribute("disabled", true);
+ }
+ }
+}
+
+function isPrefLocked(elem)
+{
+ if (!elem.hasAttribute("preference"))
+ return false;
+
+ return document.getElementById(elem.getAttribute("preference")).locked;
+}
diff --git a/inspector/content/prefs/pref-inspector.xul b/inspector/content/prefs/pref-inspector.xul
new file mode 100644
index 00000000..7ae8c57e
--- /dev/null
+++ b/inspector/content/prefs/pref-inspector.xul
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/prefs/pref-sidebar.js b/inspector/content/prefs/pref-sidebar.js
new file mode 100644
index 00000000..9b9577e2
--- /dev/null
+++ b/inspector/content/prefs/pref-sidebar.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+/***************************************************************
+* SidebarPrefs -------------------------------------------------
+* The controller for the lovely sidebar prefs panel.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/rdf/RDFU.js
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var sidebarPref;
+
+//////////// global constants ////////////////////
+
+const kDirServiceCID = "@mozilla.org/file/directory_service;1"
+const kNCURI = "http://home.netscape.com/NC-rdf#";
+const kSidebarPanelId = "UPnls"; // directory services property to find panels.rdf
+const kSidebarURNPanelList = "urn:sidebar:current-panel-list";
+const kSidebarURN3rdParty = "urn:sidebar:3rdparty-panel";
+const kSidebarURL = "chrome://inspector/content/sidebar.xul";
+
+//////////////////////////////////////////////////
+
+function SidebarPrefs_initialize()
+{
+ sidebarPref = new SidebarPrefs();
+ sidebarPref.initSidebarData();
+}
+
+///// class SidebarPrefs /////////////////////////
+
+function SidebarPrefs()
+{
+}
+
+SidebarPrefs.prototype =
+{
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Because nsSidebar has been so mean to me, I'm going to re-write it's
+ // addPanel code right here so I don't have to fight with it. Pbbbbt!
+ ///////////////////////////////////////////////////////////////////////////
+
+ initSidebarData: function()
+ {
+ var file = this.getDirectoryFile(kSidebarPanelId);
+ if (file)
+ RDFU.loadDataSource(file, gSidebarLoadListener);
+ },
+
+ initSidebarData2: function(aDS)
+ {
+ var res = aDS.GetTarget(gRDF.GetResource(kSidebarURNPanelList), gRDF.GetResource(kNCURI + "panel-list"), true);
+ this.mDS = aDS;
+ this.mPanelSeq = RDFU.makeSeq(aDS, res);
+ this.mPanelRes = gRDF.GetResource(kSidebarURN3rdParty + ":" + kSidebarURL);
+
+ if (this.isSidebarInstalled()) {
+ document.getElementById("tbxSidebar").setAttribute("hidden", "true");
+ }
+ },
+
+ isSidebarInstalled: function()
+ {
+ return this.mPanelSeq.IndexOf(this.mPanelRes) != -1;
+ },
+
+ installSidebar: function()
+ {
+ if (this.isSidebarInstalled()) {
+ return false;
+ }
+
+ var bundle = document.getElementById("inspector-bundle");
+ var kSidebarTitle = bundle.getString("sidebar.title");
+
+ this.mDS.Assert(this.mPanelRes, gRDF.GetResource(kNCURI + "title"), gRDF.GetLiteral(kSidebarTitle), true);
+ this.mDS.Assert(this.mPanelRes, gRDF.GetResource(kNCURI + "content"), gRDF.GetLiteral(kSidebarURL), true);
+ this.mPanelSeq.AppendElement(this.mPanelRes);
+ this.forceSidebarRefresh();
+
+ var msg = document.getElementById("txSidebarMsg");
+ msg.removeChild(msg.firstChild);
+
+ msg.appendChild(document.createTextNode(bundle.getString("sidebarInstalled")));
+ var btn = document.getElementById("btnSidebarInstall");
+ btn.setAttribute("disabled", "true");
+
+ return true;
+ },
+
+ forceSidebarRefresh: function()
+ {
+ var listRes = gRDF.GetResource(kSidebarURNPanelList);
+ var refreshRes = gRDF.GetResource(kNCURI + "refresh");
+ var trueRes = gRDF.GetLiteral("true");
+ this.mDS.Assert(listRes, refreshRes, trueRes, true);
+ this.mDS.Unassert(listRes, refreshRes, trueRes);
+ },
+
+ getDirectoryFile: function(aFileId)
+ {
+ try {
+ var dirService = XPCU.getService(kDirServiceCID, "nsIProperties");
+ var file = dirService.get(aFileId, Components.interfaces.nsIFile);
+ if (!file.exists())
+ return null;
+
+ var ioService = XPCU.getService("@mozilla.org/network/io-service;1", "nsIIOService");
+ var fileHandler = XPCU.QI(ioService.getProtocolHandler("file"), "nsIFileProtocolHandler");
+
+ return fileHandler.getURLSpecFromFile(file);
+
+ } catch (ex) {
+ return null;
+ }
+ }
+
+};
+
+var gSidebarLoadListener = {
+ onDataSourceReady: function(aDS)
+ {
+ sidebarPref.initSidebarData2(aDS);
+ },
+
+ onError: function()
+ {
+ }
+};
+
diff --git a/inspector/content/prefs/prefsOverlay.xul b/inspector/content/prefs/prefsOverlay.xul
new file mode 100644
index 00000000..7c848a97
--- /dev/null
+++ b/inspector/content/prefs/prefsOverlay.xul
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/res/viewer-registry.rdf b/inspector/content/res/viewer-registry.rdf
new file mode 100644
index 00000000..b2ba7ea3
--- /dev/null
+++ b/inspector/content/res/viewer-registry.rdf
@@ -0,0 +1,336 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/sidebar.js b/inspector/content/sidebar.js
new file mode 100644
index 00000000..b04f170e
--- /dev/null
+++ b/inspector/content/sidebar.js
@@ -0,0 +1,117 @@
+/* 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/. */
+
+/*****************************************************************************
+* InspectorSidebar -----------------------------------------------------------
+* The primary object that controls the Inspector sidebar.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var inspector;
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kObserverServiceContractID = "@mozilla.org/observer-service;1";
+
+const gNavigator = window.content;
+
+//////////////////////////////////////////////////////////////////////////////
+
+function InspectorSidebar_initialize()
+{
+ inspector = new InspectorSidebar();
+ inspector.initialize();
+}
+
+window.addEventListener("load", InspectorSidebar_initialize, false);
+
+//////////////////////////////////////////////////////////////////////////////
+//// class InspectorSidebar
+
+function InspectorSidebar()
+{
+}
+
+InspectorSidebar.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ get document()
+ {
+ return this.mDocPanel.viewer.subject;
+ },
+
+ initialize: function IS_Initialize()
+ {
+ this.installNavObserver();
+
+ this.mPanelSet = document.getElementById("bxPanelSet");
+ this.mPanelSet.addObserver("panelsetready", this, false);
+ this.mPanelSet.initialize();
+ },
+
+ destroy: function IS_Destroy()
+ {
+ },
+
+ doViewerCommand: function IS_DoViewerCommand(aCommand)
+ {
+ this.mPanelSet.execCommand(aCommand);
+ },
+
+ getViewer: function IS_GetViewer(aUID)
+ {
+ return this.mPanelSet.registry.getViewerByUID(aUID);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Viewer Panels
+
+ initViewerPanels: function IS_InitViewerPanels()
+ {
+ this.mDocPanel = this.mPanelSet.getPanel(0);
+ this.mDocPanel.addObserver("subjectChange", this, false);
+ this.mObjectPanel = this.mPanelSet.getPanel(1);
+ },
+
+ onEvent: function IS_OnEvent(aEvent)
+ {
+ if (aEvent.type == "panelsetready") {
+ this.initViewerPanels();
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Navigation
+
+ setTargetWindow: function IS_SetTargetWindow(aWindow)
+ {
+ this.setTargetDocument(aWindow.document);
+ },
+
+ setTargetDocument: function IS_SetTargetDocument(aDoc)
+ {
+ this.mPanelSet.getPanel(0).subject = aDoc;
+ },
+
+ installNavObserver: function IS_InstallNavObserver()
+ {
+ var observerService = XPCU.getService(kObserverServiceContractID,
+ "nsIObserverService");
+ observerService.addObserver(NavLoadObserver, "EndDocumentLoad", false);
+ }
+};
+
+var NavLoadObserver = {
+ observe: function NLO_Observe(aWindow)
+ {
+ inspector.setTargetWindow(aWindow);
+ }
+};
diff --git a/inspector/content/sidebar.xul b/inspector/content/sidebar.xul
new file mode 100644
index 00000000..9712056a
--- /dev/null
+++ b/inspector/content/sidebar.xul
@@ -0,0 +1,61 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/statusbarOverlay.xul b/inspector/content/statusbarOverlay.xul
new file mode 100644
index 00000000..ade885c4
--- /dev/null
+++ b/inspector/content/statusbarOverlay.xul
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tasksOverlay-cz.xul b/inspector/content/tasksOverlay-cz.xul
new file mode 100644
index 00000000..27db68c9
--- /dev/null
+++ b/inspector/content/tasksOverlay-cz.xul
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tasksOverlay-ff.xul b/inspector/content/tasksOverlay-ff.xul
new file mode 100644
index 00000000..bb03f608
--- /dev/null
+++ b/inspector/content/tasksOverlay-ff.xul
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tasksOverlay-mobile.xul b/inspector/content/tasksOverlay-mobile.xul
new file mode 100644
index 00000000..b74f1361
--- /dev/null
+++ b/inspector/content/tasksOverlay-mobile.xul
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tasksOverlay-sb.xul b/inspector/content/tasksOverlay-sb.xul
new file mode 100644
index 00000000..5c83f9f8
--- /dev/null
+++ b/inspector/content/tasksOverlay-sb.xul
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tasksOverlay-tb.xul b/inspector/content/tasksOverlay-tb.xul
new file mode 100644
index 00000000..7bb7732a
--- /dev/null
+++ b/inspector/content/tasksOverlay-tb.xul
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tasksOverlay.xul b/inspector/content/tasksOverlay.xul
new file mode 100644
index 00000000..40f90b54
--- /dev/null
+++ b/inspector/content/tasksOverlay.xul
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/tests/allskin.xul b/inspector/content/tests/allskin.xul
new file mode 100644
index 00000000..51fcd799
--- /dev/null
+++ b/inspector/content/tests/allskin.xul
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/toolboxOverlay.xul b/inspector/content/toolboxOverlay.xul
new file mode 100644
index 00000000..fb74376e
--- /dev/null
+++ b/inspector/content/toolboxOverlay.xul
@@ -0,0 +1,68 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/utils.js b/inspector/content/utils.js
new file mode 100644
index 00000000..678b1f98
--- /dev/null
+++ b/inspector/content/utils.js
@@ -0,0 +1,194 @@
+/* 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/. */
+
+/*****************************************************************************
+* Inspector Utils ------------------------------------------------------------
+* Common functions and constants used across the app.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/rdf/RDFU.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kInspectorNSURI = "http://www.mozilla.org/inspector#";
+const kXULNSURI = "http://www.mozilla.org/keymaster/gatekeeper/" +
+ "there.is.only.xul";
+const kHTMLNSURI = "http://www.w3.org/1999/xhtml";
+const kCharTable = {
+ '&': "&",
+ '<': "<",
+ '>': ">",
+ '"': """
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var gEntityConverter;
+
+var InsUtil = {
+ /***************************************************************************
+ * Convenience function for calling nsIChromeRegistry::convertChromeURL
+ ***************************************************************************/
+ convertChromeURL: function IU_ConvertChromeURL(aURL)
+ {
+ var uri =
+ XPCU.getService("@mozilla.org/network/io-service;1", "nsIIOService")
+ .newURI(aURL, null, null);
+ var reg = XPCU.getService("@mozilla.org/chrome/chrome-registry;1",
+ "nsIChromeRegistry");
+
+ return reg.convertChromeURL(uri);
+ },
+
+ /***************************************************************************
+ * Convenience function for getting a literal value from
+ * nsIRDFDataSource::GetTarget
+ * @param aDS
+ * nsISupports
+ * @param aId
+ * string
+ * @param aPropName
+ * string
+ ***************************************************************************/
+ getDSProperty: function IU_GetDSProperty(aDS, aId, aPropName)
+ {
+ var ruleRes = gRDF.GetResource(aId);
+ var ds = XPCU.QI(aDS, "nsIRDFDataSource"); // just to be sure
+ var propRes = ds.GetTarget(ruleRes,
+ gRDF.GetResource(kInspectorNSURI + aPropName),
+ true);
+ propRes = XPCU.QI(propRes, "nsIRDFLiteral");
+
+ return propRes.Value;
+ },
+
+ /***************************************************************************
+ * Convenience function for persisting an element's persisted attributes.
+ ***************************************************************************/
+ persistAll: function IU_PersistAll(aId)
+ {
+ var el = document.getElementById(aId);
+ if (el) {
+ var attrs = el.getAttribute("persist").split(" ");
+ for (var i = 0; i < attrs.length; ++i) {
+ document.persist(aId, attrs[i]);
+ }
+ }
+ },
+
+ /***************************************************************************
+ * Convenience function for escaping HTML strings.
+ ***************************************************************************/
+ unicodeToEntity: function IU_UnicodeToEntity(text)
+ {
+ const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;
+
+ function charTableLookup(letter)
+ {
+ return kCharTable[letter];
+ }
+
+ function convertEntity(letter)
+ {
+ try {
+ return gEntityConverter.ConvertToEntity(letter, entityVersion);
+ }
+ catch (ex) {
+ return letter;
+ }
+ }
+
+ if (!gEntityConverter) {
+ try {
+ gEntityConverter =
+ XPCU.createInstance("@mozilla.org/intl/entityconverter;1",
+ "nsIEntityConverter");
+ }
+ catch (ex) { }
+ }
+
+ // replace chars in our charTable
+ text = text.replace(/[<>&"]/g, charTableLookup);
+
+ // replace chars > 0x7f via nsIEntityConverter
+ text = text.replace(/[^\0-\u007f]/g, convertEntity);
+
+ return text;
+ },
+
+ /**
+ * Determine which from a list of indexes is nearest to the given index.
+ * @param aIndex
+ * The index to search for.
+ * @param aIndexList
+ * A sorted list of indexes to be searched.
+ * @return The index in the list closest to aIndex. This will be aIndex
+ * itself if it appears in the list, or -1 if the list is empty.
+ * @note
+ */
+ getNearestIndex: function IU_GetNearestIndex(aIndex, aIndexList)
+ {
+ // Four easy cases:
+ // - empty list
+ // - single element list
+ // - given index comes before the first element
+ // - given index comes after the last element
+ if (aIndexList.length == 0) {
+ return -1;
+ }
+ var first = aIndexList[0];
+ if (aIndexList.length == 1 || aIndex <= first) {
+ return first;
+ }
+ var high = aIndexList.length - 1;
+ var last = aIndexList[high];
+ if (aIndex >= last) {
+ return last;
+ }
+
+ var mid, low = 0;
+ while (low <= high) {
+ mid = low + Math.floor((high - low) / 2);
+ let current = aIndexList[mid];
+ if (aIndex > current) {
+ low = mid + 1;
+ }
+ else if (aIndex < current) {
+ high = mid - 1;
+ }
+ else {
+ return aIndex;
+ }
+ }
+
+ // By handling the four easy cases above, we eliminated the possibility
+ // that low or high will be out of bounds at this point. If aIndex had
+ // been present, it would have been sandwiched between these two values:
+ var previous = aIndexList[high];
+ var next = aIndexList[low];
+
+ if ((aIndex - previous) < (next - aIndex)) {
+ return previous;
+ }
+ // Even if previous and next are equidistant to aIndex's position, we'll
+ // go with the one that's greater.
+ return next;
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// Debugging Utilities
+
+// dump text to the Error Console
+function debug(aText)
+{
+ // XX comment out to reduce noise
+ var cs =
+ XPCU.getService("@mozilla.org/consoleservice;1", "nsIConsoleService");
+ cs.logStringMessage(aText);
+}
diff --git a/inspector/content/viewers/accessibleEvent/accessibleEvent.js b/inspector/content/viewers/accessibleEvent/accessibleEvent.js
new file mode 100644
index 00000000..24429f6c
--- /dev/null
+++ b/inspector/content/viewers/accessibleEvent/accessibleEvent.js
@@ -0,0 +1,255 @@
+/* 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/. */
+
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kAccessibleRetrievalCID = "@mozilla.org/accessibleRetrieval;1";
+
+const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval;
+
+const nsIAccessibleEvent = Components.interfaces.nsIAccessibleEvent;
+const nsIAccessibleStateChangeEvent =
+ Components.interfaces.nsIAccessibleStateChangeEvent;
+const nsIAccessibleTextChangeEvent =
+ Components.interfaces.nsIAccessibleTextChangeEvent;
+const nsIAccessibleCaretMoveEvent =
+ Components.interfaces.nsIAccessibleCaretMoveEvent;
+
+const nsIDOMNode = Components.interfaces.nsIDOMNode;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization/Destruction
+
+window.addEventListener("load", AccessibleEventViewer_initialize, false);
+
+function AccessibleEventViewer_initialize()
+{
+ viewer = new AccessibleEventViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// class AccessibleEventViewer
+function AccessibleEventViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+ this.mAccService = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+}
+
+AccessibleEventViewer.prototype =
+{
+ mSubject: null,
+ mPane: null,
+ mAccEventSubject: null,
+ mAccService: null,
+
+ get uid() { return "accessibleEvent"; },
+ get pane() { return this.mPane; },
+
+ get subject() { return this.mSubject; },
+ set subject(aObject)
+ {
+ this.mSubject = aObject;
+ this.updateView();
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ },
+
+ initialize: function initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ isCommandEnabled: function isCommandEnabled(aCommand)
+ {
+ return false;
+ },
+
+ getCommand: function getCommand(aCommand)
+ {
+ return null;
+ },
+
+ destroy: function destroy() {},
+
+ // event dispatching
+
+ addObserver: function addObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+ removeObserver: function removeObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ // private
+ updateView: function updateView()
+ {
+ this.clearView();
+
+ if (!this.pane.params) {
+ return;
+ }
+
+ this.mAccEventSubject = this.pane.params.accessibleEvent;
+ if (!this.mAccEventSubject)
+ return;
+
+ XPCU.QI(this.mAccEventSubject, nsIAccessibleEvent);
+
+ // Update accessible event properties.
+ var shownPropsId = "";
+ if (this.mAccEventSubject instanceof nsIAccessibleStateChangeEvent)
+ shownPropsId = "stateChangeEvent";
+ else if (this.mAccEventSubject instanceof nsIAccessibleTextChangeEvent)
+ shownPropsId = "textChangeEvent";
+ else if (this.mAccEventSubject instanceof nsIAccessibleCaretMoveEvent)
+ shownPropsId = "caretMoveEvent";
+
+ var props = document.getElementsByAttribute("prop", "*");
+ for (var i = 0; i < props.length; i++) {
+ var propElm = props[i];
+ var isActive = !propElm.hasAttribute("class") ||
+ (propElm.getAttribute("class") == shownPropsId);
+
+ if (isActive) {
+ var prop = propElm.getAttribute("prop");
+ propElm.textContent = this[prop];
+ propElm.parentNode.removeAttribute("hidden");
+ } else {
+ propElm.parentNode.setAttribute("hidden", "true");
+ }
+ }
+
+ // Update handler output.
+ var outputElm = document.getElementById("handlerOutput");
+ var outputList = this.pane.params.accessibleEventHandlerOutput;
+ if (outputList) {
+ while (outputElm.firstChild) {
+ outputElm.removeChild(outputElm.lastChild);
+ }
+
+ for (let i = 0; i < outputList.length; i++) {
+ var output = outputList[i];
+
+ // Generate a tree.
+ if (typeof output == "object" && "cols" in output && "view" in output) {
+ var tree = document.createElement("tree");
+ tree.setAttribute("flex", "1");
+ tree.setAttribute("treelines", "true");
+
+ var treecols = document.createElement("treecols");
+ for (let col in output.cols) {
+ var treecol = document.createElement("treecol");
+ treecol.setAttribute("id", col);
+ treecol.setAttribute("label", output.cols[col].name);
+ treecol.setAttribute("flex", output.cols[col].flex);
+ if (output.cols[col].isPrimary) {
+ treecol.setAttribute("primary", "true");
+ }
+ treecol.setAttribute("persist", "width,hidden,ordinal");
+ treecols.appendChild(treecol);
+
+ var splitter = document.createElement("splitter");
+ splitter.setAttribute("class", "tree-splitter");
+ treecols.appendChild(splitter);
+ }
+ tree.appendChild(treecols);
+
+ var treechildren = document.createElement("treechildren");
+ tree.appendChild(treechildren);
+ outputElm.appendChild(tree);
+ tree.treeBoxObject.view = output.view;
+
+ }
+ else {
+ // Output text.
+ var node = document.createElement("description");
+ node.textContent = output;
+ outputElm.appendChild(node);
+ }
+ }
+
+ outputElm.parentNode.removeAttribute("hidden");
+ }
+ else {
+ outputElm.parentNode.setAttribute("hidden", "true");
+ }
+ },
+
+ clearView: function clearView()
+ {
+ var containers = document.getElementsByAttribute("prop", "*");
+ for (var i = 0; i < containers.length; ++i)
+ containers[i].textContent = "";
+ },
+
+ get isFromUserInput()
+ {
+ return this.mAccEventSubject.isFromUserInput;
+ },
+
+ get state()
+ {
+ var state = 0, extraState = 0;
+ var isExtraState = typeof this.mAccEventSubject.isExtraState == "function" ?
+ this.mAccEventSubject.isExtraState() : this.mAccEventSubject.isExtraState;
+ if (isExtraState) {
+ extraState = this.mAccEventSubject.state;
+ }
+ else {
+ state = this.mAccEventSubject.state;
+ }
+
+ var states = this.mAccService.getStringStates(state, extraState);
+
+ var list = [];
+ for (var i = 0; i < states.length; i++)
+ list.push(states.item(i));
+ return list.join();
+ },
+
+ get isEnabled()
+ {
+ return typeof this.mAccEventSubject.isEnabled == "function" ?
+ this.mAccEventSubject.isEnabled() : this.mAccEventSubject.isEnabled;
+ },
+
+ get startOffset()
+ {
+ return this.mAccEventSubject.start;
+ },
+
+ get length()
+ {
+ return this.mAccEventSubject.length;
+ },
+
+ get isInserted()
+ {
+ return typeof this.mAccEventSubject.isInserted == "function" ?
+ this.mAccEventSubject.isInserted() : this.mAccEventSubject.isInserted;
+ },
+
+ get modifiedText()
+ {
+ return this.mAccEventSubject.modifiedText;
+ },
+
+ get caretOffset()
+ {
+ return this.mAccEventSubject.caretOffset;
+ }
+};
+
diff --git a/inspector/content/viewers/accessibleEvent/accessibleEvent.xul b/inspector/content/viewers/accessibleEvent/accessibleEvent.xul
new file mode 100644
index 00000000..315860fe
--- /dev/null
+++ b/inspector/content/viewers/accessibleEvent/accessibleEvent.xul
@@ -0,0 +1,70 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &descIsFromUserInput.label;
+
+
+
+ &descState.label;
+
+
+
+ &descIsEnabled.label;
+
+
+
+ &descStartOffset.label;
+
+
+
+ &descLength.label;
+
+
+
+ &descIsInserted.label;
+
+
+
+ &descModifiedText.label;
+
+
+
+ &descCaretOffset.label;
+
+
+
+
+
+ &descrHandlerOutput.label;
+
+
+
+
diff --git a/inspector/content/viewers/accessibleEvents/accessibleEvents.js b/inspector/content/viewers/accessibleEvents/accessibleEvents.js
new file mode 100644
index 00000000..40feefa8
--- /dev/null
+++ b/inspector/content/viewers/accessibleEvents/accessibleEvents.js
@@ -0,0 +1,1200 @@
+/* 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/. */
+
+/***************************************************************
+* AccessibleEventsViewer --------------------------------------------
+* The viewer for the accessible events occured on a document accessible.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+****************************************************************/
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+var gBundle;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kObserverServiceCID = "@mozilla.org/observer-service;1";
+const kAccessibleRetrievalCID = "@mozilla.org/accessibleRetrieval;1";
+
+const Ci = Components.interfaces;
+const nsIObserverService = Ci.nsIObserverService;
+const nsIAccessibleRetrieval = Ci.nsIAccessibleRetrieval;
+const nsIAccessibleEvent = Ci.nsIAccessibleEvent;
+const nsIAccessible = Ci.nsIAccessible;
+const nsIPropertyElement = Ci.nsIPropertyElement;
+
+const gAccInterfaces =
+[
+ Ci.nsIAccessible,
+ Ci.nsIAccessibleDocument,
+ Ci.nsIAccessibleEditableText,
+ Ci.nsIAccessibleHyperLink,
+ Ci.nsIAccessibleHyperText,
+ Ci.nsIAccessibleImage,
+ Ci.nsIAccessibleSelectable,
+ Ci.nsIAccessibleTable,
+ Ci.nsIAccessibleTableCell,
+ Ci.nsIAccessibleText,
+ Ci.nsIAccessibleValue
+];
+
+if ("nsIAccessNode" in Ci)
+ gAccInterfaces.push(Ci.nsIAccessNode);
+
+/**
+ * QI nsIAccessNode interface if any, used for compatibility with Gecko versions
+ * prior to Gecko13.
+ */
+function QIAccessNode(aAccessible)
+{
+ return "nsIAccessNode" in Ci ?
+ XPCU.QI(aAccessible, Ci.nsIAccessNode) : aAccessible;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization
+
+window.addEventListener("load", AccessibleEventsViewer_initialize, false);
+
+function AccessibleEventsViewer_initialize()
+{
+ gBundle = document.getElementById("accessiblePropsBundle");
+
+ viewer = new AccessibleEventsViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// class AccessibleEventsViewer
+
+function AccessibleEventsViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+
+ this.mTree = document.getElementById("olAccessibleEvents");
+ this.mOlBox = this.mTree.treeBoxObject;
+
+ this.mWatchTree = document.getElementById("watchEventList");
+ this.mWatchBox = this.mWatchTree.treeBoxObject;
+}
+
+AccessibleEventsViewer.prototype =
+{
+ // initialization
+
+ mSubject: null,
+ mPane: null,
+ mView: null,
+
+ // interface inIViewer
+
+ get uid() { return "accessibleEvents"; },
+ get pane() { return this.mPane; },
+ get selection() { return this.mSelection; },
+
+ get subject() { return this.mSubject; },
+ set subject(aObject)
+ {
+ this.mWatchView = new WatchAccessibleEventsListView();
+
+ if (this.mView) {
+ this.mView.destroy();
+ }
+ this.mView = new AccessibleEventsView(aObject, this.mWatchView);
+
+ this.mOlBox.view = this.mView;
+ this.mWatchBox.view = this.mWatchView;
+
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ },
+
+ initialize: function initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function destroy()
+ {
+ this.mView.destroy();
+ this.mOlBox.view = null;
+ this.mWatchBox.view = null;
+ },
+
+ isCommandEnabled: function isCommandEnabled(aCommand)
+ {
+ return false;
+ },
+
+ getCommand: function getCommand(aCommand)
+ {
+ return null;
+ },
+
+ // event dispatching
+
+ addObserver: function addObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+ removeObserver: function removeObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ // utils
+
+ onItemSelected: function onItemSelected()
+ {
+ var idx = this.mTree.currentIndex;
+ var object = this.mView.getObject(idx);
+
+ // Set subject for right panel view.
+ this.mSelection = object.accessnode;
+
+ // Set parameters for right panel view.
+ if (this.pane.panelset.panelCount > 1) {
+ this.pane.panelset.getPanel(1).params = {
+ accessibleEvent: object.event,
+ accessibleEventHandlerOutput: object.eventHandlerOutput
+ };
+ }
+
+ this.mObsMan.dispatchEvent("selectionChange",
+ { selection: this.mSelection } );
+ },
+
+ onWatchViewItemSelected: function onWatchViewItemSelected()
+ {
+ this.mWatchView.updateHandlerEditor();
+ this.mWatchView.updateHandlerState();
+ },
+
+ onWatchViewHandlerStateChanged:
+ function onWatchViewHandlerStateChanged(aState)
+ {
+ this.mWatchView.updateHandlerState(aState);
+ },
+
+ onWatchViewKeyPressed: function onWatchViewKeyPressed(aEvent)
+ {
+ // SPACE key was pressed. Toggle the row's Watched column tick.
+ if (aEvent.charCode == KeyEvent.DOM_VK_SPACE)
+ this.mWatchView.toggleEventWatched();
+ },
+
+ /**
+ * Clear the list of handled events.
+ */
+ clearEventsList: function clearEventsList()
+ {
+ this.mView.clear();
+ },
+
+ /**
+ * Start or stop to watch all events.
+ *
+ * @param aDoWatch [in] indicates whether to start or stop events watching.
+ */
+ watchAllEvents: function watchAllEvents(aDoWatch)
+ {
+ this.mWatchView.watchAllEvents(aDoWatch);
+ },
+
+ /**
+ * Shows context help for event handler editor.
+ */
+ showWatchViewHandlerHelp: function showWatchViewHandlerHelp()
+ {
+ openDialog("chrome://inspector/content/viewers/accessibleEvents/handlerHelpDialog.xul",
+ "_blank", "chrome,modal,centerscreen");
+ },
+
+ /**
+ * Open/hide handler editor.
+ */
+ toggleHandlerEditor: function toggleHandlerEditor(aSplitter)
+ {
+ var state = aSplitter.getAttribute("state");
+ if (state == "collapsed") {
+ aSplitter.setAttribute("state", "open");
+ }
+ else {
+ aSplitter.setAttribute("state", "collapsed");
+ }
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// AccessibleEventsView
+
+function AccessibleEventsView(aObject, aWatchView)
+{
+ this.mWatchView = aWatchView;
+ this.mEvents = [];
+ this.mRowCount = 0;
+
+ this.mAccService = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+
+ this.mAccessible = aObject instanceof nsIAccessible ?
+ aObject : this.mAccService.getAccessibleFor(aObject);
+
+ this.canSkipTreeTraversal = false;
+ this.mDOMIRootDocumentAccessible = null;
+ var acc = QIAccessNode(this.mAccService.getAccessibleFor(document));
+ if ("rootDocument" in acc) {
+ this.mDOMIRootDocumentAccessible = acc.rootDocument;
+ this.mApplicationAccessible = this.mDOMIRootDocumentAccessible.parent;
+
+ // We can skip accessible tree traversal for perf on Gecko 2.0 if the
+ // inspected accessible is application accessible.
+ this.canSkipTreeTraversal =
+ (this.mAccessible == this.mApplicationAccessible);
+ }
+ else {
+ // Gecko 1.9.2 compatibility.
+ while (acc.parent) {
+ this.mDOMIRootDocumentAccessible = acc;
+ acc = acc.parent;
+ }
+ this.mApplicationAccessible = acc;
+ }
+
+ this.mObserverService = XPCU.getService(kObserverServiceCID,
+ nsIObserverService);
+
+ this.mObserverService.addObserver(this, "accessible-event", false);
+}
+
+AccessibleEventsView.prototype = new inBaseTreeView();
+
+/**
+ * Global variables used to store event object and user's event handler output
+ * from helper functions.
+ */
+var gEvent = null;
+var gEventHandlerOutput = [ ];
+
+AccessibleEventsView.prototype.observe =
+function observe(aSubject, aTopic, aData)
+{
+ var event = XPCU.QI(aSubject, nsIAccessibleEvent);
+ var accessible = event.accessible;
+ if (!accessible)
+ return;
+
+ var accessnode = QIAccessNode(accessible);
+
+ // Ignore events on this DOM Inspector to avoid a mess (Gecko 2.0).
+ if (accessnode.rootDocument &&
+ accessnode.rootDocument == this.mDOMIRootDocumentAccessible) {
+ return;
+ }
+
+ // Ignore events having target not in subtree of currently inspected
+ // document accessible.
+ if (!this.canSkipTreeTraversal) {
+ var parentDocAccessible = accessible.document;
+ while (true) {
+ // The target accessible is inspected document accessible or its child.
+ if (parentDocAccessible == this.mAccessible) {
+ break;
+ }
+
+ // Ignore events on this DOM inspector to avoid a mess.
+ if (parentDocAccessible == this.mDOMIRootDocumentAccessible) {
+ return;
+ }
+
+ // Ignore events that aren't in subtree of inspected accessible.
+ if (!parentDocAccessible.parent || !parentDocAccessible.parent.document) {
+ return;
+ }
+
+ parentDocAccessible = parentDocAccessible.parent.document;
+ }
+ }
+
+ // Ignore unwatched events.
+ var type = event.eventType;
+ if (!this.mWatchView.isEventWatched(type))
+ return;
+
+ // Execute user handlers.
+ gEvent = event;
+ gEventHandlerOutput = [ ];
+ var expr = this.mWatchView.getHandlerExpr(type);
+ if (expr) {
+ for (let idx = 0; idx < gAccInterfaces.length; idx++) {
+ // Accessibility interfaces implicit query.
+ accessible instanceof gAccInterfaces[idx];
+ }
+
+ try {
+ var f = Function("event", "target", expr);
+ f(event, accessible);
+ }
+ catch (ex) {
+ output(ex);
+ }
+ }
+
+ // Put event into list.
+ var date = new Date();
+ var node = accessnode.DOMNode;
+ var role = "", name = "";
+ try {
+ // may fail prior Gecko 2.0
+ role = this.mAccService.getStringRole(accessible.role);
+ name = accessible.name;
+ } catch(e) {
+ }
+
+ var eventObj = {
+ event: event,
+ eventHandlerOutput: gEventHandlerOutput,
+ accessnode: accessnode,
+ node: node,
+ id: node.id || "",
+ nodename: node ? node.nodeName : "",
+ name: name,
+ role: role,
+ type: this.mAccService.getStringEventType(type),
+ time: date.toLocaleTimeString()
+ };
+
+ this.mEvents.unshift(eventObj);
+ ++this.mRowCount;
+ this.mTree.rowCountChanged(0, 1);
+}
+
+AccessibleEventsView.prototype.destroy =
+function destroy()
+{
+ this.mObserverService.removeObserver(this, "accessible-event");
+}
+
+AccessibleEventsView.prototype.clear =
+function clear()
+{
+ var count = this.mRowCount;
+ this.mRowCount = 0;
+ this.mEvents = [];
+ this.mTree.rowCountChanged(0, -count);
+}
+
+/**
+ * Return object to be used as a subject for the right panel. It could be either
+ * DOM node or accessible object depending on whether accessible has a DOM node.
+ * Also the returned object has properties that can used by accessibleEvent
+ * view to represent information about accessible event and output from user
+ * defined accessible event handlers.
+ */
+AccessibleEventsView.prototype.getObject =
+function getObject(aRow)
+{
+ return aRow < 0 ? null : this.mEvents[aRow];
+}
+
+AccessibleEventsView.prototype.getCellText =
+function getCellText(aRow, aCol)
+{
+ if (aCol.id == "olcEventType")
+ return this.mEvents[aRow].type;
+ if (aCol.id == "olcEventTime")
+ return this.mEvents[aRow].time;
+ if (aCol.id == "olcEventTargetNodeName")
+ return this.mEvents[aRow].nodename;
+ if (aCol.id == "olcEventTargetNodeID")
+ return this.mEvents[aRow].id;
+ if (aCol.id == "olcEventTargetRole")
+ return this.mEvents[aRow].role;
+ if (aCol.id == "olcEventTargetName")
+ return this.mEvents[aRow].name;
+ return "";
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// WatchAccessibleEventsListView
+
+const kIgnoredEvents = -1;
+const kMutationEvents = 0;
+const kChangeEvents = 1;
+const kNotificationEvents = 2;
+const kSelectionEvents = 3;
+const kMenuEvents = 4;
+const kDocumentEvents = 5;
+const kTextEvents = 6;
+const kTableEvents = 7;
+const kWindowEvents = 8;
+const kHyperLinkEvents = 9;
+const kHyperTextEvents = 10;
+
+function WatchAccessibleEventsListView()
+{
+ // nsITreeView
+ this.__proto__ = new inBaseTreeView();
+
+ this.__defineGetter__(
+ "rowCount",
+ function watchview_getRowCount()
+ {
+ var rowCount = 0;
+
+ for (var idx = 0; idx < this.mChildren.length; idx++) {
+ rowCount++;
+
+ if (this.mChildren[idx].open)
+ rowCount += this.mChildren[idx].children.length;
+ }
+
+ return rowCount;
+ }
+ );
+
+ this.getCellText = function watchview_getCellText(aRowIndex, aCol)
+ {
+ if (aCol.id == "welEventType") {
+ var data = this.getData(aRowIndex);
+ return data.text;
+ }
+
+ return "??";
+ };
+
+ this.getCellValue = function watchview_getCellValue(aRowIndex, aCol)
+ {
+ if (aCol.id == "welIsWatched") {
+ var data = this.getData(aRowIndex);
+ return data.value;
+ }
+ else if (aCol.id == "welIsHandlerEnabled") {
+ var data = this.getData(aRowIndex);
+ return data.isHandlerEnabled;
+ }
+
+ return false;
+ };
+
+ this.getParentIndex = function watchview_getParentIndex(aRowIndex)
+ {
+ var info = this.getInfo(aRowIndex);
+ return info.parentIndex;
+ };
+
+ this.hasNextSibling = function(aRowIndex, aAfterIndex)
+ {
+ var info = this.getInfo(aRowIndex);
+ var siblings = info.parentData.children;
+ return siblings[siblings.length - 1] != info.data;
+ };
+
+ this.getLevel = function watchview_getLevel(aRowIndex)
+ {
+ var info = this.getInfo(aRowIndex);
+ return info.level;
+ };
+
+ this.isContainer = function watchview_isContainer(aRowIndex)
+ {
+ var info = this.getInfo(aRowIndex);
+ return info.level == 0;
+ };
+
+ this.isContainerOpen = function watchview_isContainerOpen(aRowIndex)
+ {
+ var data = this.getData(aRowIndex);
+ return data.open;
+ };
+
+ this.isContainerEmpty = function watchview_isContainerEmpty(aRowIndex)
+ {
+ return false;
+ };
+
+ this.toggleOpenState = function watchview_toogleOpenState(aRowIndex)
+ {
+ var data = this.getData(aRowIndex);
+
+ data.open = !data.open;
+ var rowCount = data.children.length;
+
+ if (data.open)
+ this.mTree.rowCountChanged(aRowIndex + 1, rowCount);
+ else
+ this.mTree.rowCountChanged(aRowIndex + 1, -rowCount);
+
+ this.mTree.invalidateRow(aRowIndex);
+ };
+
+ this.isEditable = function watchview_isEditable(aRowIndex, aCol)
+ {
+ if (aCol.id == "welIsWatched" ||
+ (aCol.id == "welIsHandlerEnabled" && !this.isContainer(aRowIndex))) {
+ return true;
+ }
+ return false;
+ };
+
+ this.setCellValue = function watchview_setCellValue(aRowIndex, aCol, aValue)
+ {
+ if (aCol.id == "welIsWatched") {
+ var newValue = aValue == "true";
+
+ var info = this.getInfo(aRowIndex);
+ var data = info.data;
+
+ data.value = newValue;
+
+ if (this.isContainer(aRowIndex)) {
+ var children = data.children;
+ for (var idx = 0; idx < children.length; idx++)
+ children[idx].value = newValue;
+
+ this.mTree.invalidateColumnRange(aRowIndex, aRowIndex + children.length,
+ aCol);
+ return;
+ }
+
+ this.mTree.invalidateCell(aRowIndex, aCol);
+
+ var parentData = info.parentData;
+ if (parentData.value && !newValue) {
+ parentData.value = false;
+ this.mTree.invalidateCell(info.parentIndex, aCol);
+ }
+ }
+ else if (aCol.id == "welIsHandlerEnabled") {
+ var newValue = aValue == "true";
+ this.updateHandlerState(newValue, aRowIndex);
+ }
+ };
+
+ //////////////////////////////////////////////////////////////////////////////
+ ///// Public
+
+ /**
+ * Return true if the given event type is watched.
+ */
+ this.isEventWatched = function watchview_isEventWatched(aType)
+ {
+ return this.mReverseData[aType].value;
+ };
+
+ /**
+ * Start or stop to watch all events.
+ */
+ this.watchAllEvents = function watchview_watchAllEvents(aAll)
+ {
+ for (var idx = 0; idx < this.mChildren.length; idx++) {
+ var data = this.mChildren[idx];
+ data.value = aAll;
+ for (var jdx = 0; jdx < data.children.length; jdx++) {
+ var subdata = data.children[jdx];
+ subdata.value = aAll;
+ }
+ }
+
+ this.mTree.invalidate();
+ };
+
+ this.getHandlerExpr = function watchview_getHandlerExpr(aType)
+ {
+ var data = this.mReverseData[aType];
+ if (data.isHandlerEnabled) {
+ return data.handlerSource;
+ }
+
+ return "";
+ }
+
+ this.toggleEventWatched = function watchview_toggleEventWatched()
+ {
+ var idx = this.selection.currentIndex;
+ var colWatched = this.mTree.columns.welIsWatched;
+ var newValue = !this.getCellValue(idx, colWatched);
+
+ // setCellValue() needs the new value to be passed as a string.
+ this.setCellValue(idx, colWatched, newValue.toString());
+ }
+
+ /**
+ * Updates state and value of handler source editor.
+ */
+ this.updateHandlerEditor = function watchview_updateHandlerEditor()
+ {
+ var idx = this.selection.currentIndex;
+
+ if (idx == -1 || this.isContainer(idx)) {
+ this.mHandlerState.hidden = true;
+ this.mHandlerEditor.disabled = true;
+ this.mHandlerEditor.value = "";
+ this.mHandlerEditorLabel.hidden = false;
+ return;
+ }
+
+ this.mHandlerState.hidden = false;
+ this.mHandlerEditorLabel.hidden = true;
+ this.mHandlerEditor.disabled = false;
+
+ var data = this.getData(idx);
+
+ var label = gBundle.getFormattedString("handlerEditorLabel", [data.text]);
+ this.mHandlerState.label = label;
+ this.mHandlerEditor.value = data.handlerSource;
+ }
+
+ /**
+ * Updates state of handler (enabled or disabled) and UI displaying the state.
+ * @param aValue [optional]
+ * New value of handler state column at the given row index, if
+ * undefined then data wasn't changed, needs to update UI
+ * @param aRowIdx [optional]
+ * The given row index, if missed then current row index is used
+ */
+ this.updateHandlerState =
+ function watchview_updateHandlerState(aValue, aRowIdx)
+ {
+ var updateData = aValue != undefined;
+ var updateCheckbox = !updateData || aRowIdx == this.selection.currentIndex;
+
+ var rowIdx = aRowIdx == undefined ? this.selection.currentIndex : aRowIdx;
+
+ var data = this.getData(rowIdx);
+
+ if (updateData) {
+ data.isHandlerEnabled = aValue;
+ var col = this.mTree.columns.getNamedColumn("welIsHandlerEnabled");
+ this.mTree.invalidateCell(rowIdx, col);
+ }
+
+ if (updateCheckbox) {
+ this.mHandlerState.checked = data.isHandlerEnabled;
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ ///// Private
+
+ /**
+ * Return the data of the tree item at the given row index.
+ */
+ this.getData = function watchview_getData(aRowIndex)
+ {
+ return this.getInfo(aRowIndex).data;
+ };
+
+ /**
+ * Return an object describing the tree item at the given row index:
+ *
+ * {
+ * data: null, // the data of tree item
+ * parentIndex: -1, // index of parent row
+ * parentData: null, // the data of parent tree item
+ * level: 0 // the level of the tree item
+ * };
+ */
+ this.getInfo = function watchview_getInfo(aRowIndex)
+ {
+ var info = {
+ data: null,
+ parentIndex: -1,
+ parentData: null,
+ level: 0
+ };
+
+ var groupIdx = 0;
+ var rowIdx = aRowIndex;
+
+ for (var idx = 0; idx < this.mChildren.length; idx++) {
+ var groupItem = this.mChildren[idx];
+
+ if (rowIdx == 0) {
+ info.data = groupItem;
+ return info;
+ }
+
+ rowIdx--;
+ if (groupItem.open) {
+ var typeItemLen = groupItem.children.length;
+ if (rowIdx < typeItemLen) {
+ info.data = groupItem.children[rowIdx];
+ info.parentIndex = idx;
+ info.parentData = groupItem;
+ info.level = 1;
+ return info;
+ }
+
+ rowIdx -= typeItemLen;
+ }
+ }
+
+ return info;
+ };
+
+ /**
+ * Initialize the tree view.
+ */
+ this.init = function watchview_init()
+ {
+ // Register event groups.
+ for (var idx = 0; idx < gEventGroupMap.length; idx++)
+ this.registerEventGroup(idx, gBundle.getString(gEventGroupMap[idx]));
+
+ // Register event types.
+ for (var idx = 1; idx < gEventTypesMap.length; idx++) {
+ var props = gEventTypesMap[idx];
+ this.registerEventType(props.group, idx, props.isIgnored);
+ }
+
+ // Prepare handler source editor.
+ this.mHandlerState = document.getElementById("welHandlerState");
+ this.mHandlerEditor = document.getElementById("welHandlerEditor");
+ this.mHandlerEditorLabel = document.getElementById("welHandlerEditorLabel");
+ this.mHandlerEditor.addEventListener("input", this, false);
+ this.mHandlerEditor.disabled = true;
+ };
+
+ /**
+ * Add tree item for the group.
+ */
+ this.registerEventGroup = function watchview_registerEventGroup(aType, aName)
+ {
+ var item = {
+ text: aName,
+ value: true,
+ handlerSource: "",
+ open: false,
+ children: []
+ };
+
+ this.mChildren[aType] = item;
+ };
+
+ /**
+ * Add tree item for the event type.
+ */
+ this.registerEventType = function watchview_registerEventType(aGroup, aType,
+ aIgnored)
+ {
+ if (aGroup == kIgnoredEvents)
+ return;
+
+ var item = {
+ text: this.mAccService.getStringEventType(aType),
+ value: !aIgnored,
+ isHandlerEnabled: false,
+ handlerSource: ""
+ };
+
+ var groupItem = this.mChildren[aGroup];
+ if (aIgnored)
+ groupItem.value = false;
+
+ var children = groupItem.children;
+ children.push(item);
+
+ this.mReverseData[aType] = item;
+ };
+
+ /**
+ * Listen for 'input' event from handler source textbox to record the handler.
+ */
+ this.handleEvent = function watchview_handleEvent(aEvent)
+ {
+ if (aEvent.target != this.mHandlerEditor) {
+ return;
+ }
+
+ switch (aEvent.type) {
+ case "input":
+ var idx = this.selection.currentIndex;
+
+ // Enable/disable event handler automatically.
+ var isEnabled = this.mHandlerEditor.value != "";
+ this.updateHandlerState(isEnabled, idx);
+
+ // Update event handler code.
+ var data = this.getData(idx);
+ data.handlerSource = this.mHandlerEditor.value;
+ break;
+ }
+ }
+
+ this.mAccService = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+
+ this.mChildren = [];
+ this.mReverseData = [];
+ this.mHandlers = {};
+
+ this.mHandlerState = null;
+ this.mHandlerEditor = null;
+ this.mHandlerEditorLabel = null;
+
+ this.init();
+}
+
+function eventProps(aGroup, aValue)
+{
+ this.group = aGroup;
+ this.isIgnored = aValue;
+}
+
+/**
+ * The map of event groups.
+ */
+var gEventGroupMap =
+[
+ "mutationEvents", // kMutationEvents
+ "changeEvents", // kChangeEvents,
+ "notificationEvents", // kNotificationEvents,
+ "selectionEvents", // kSelectionEvents
+ "menuEvents", // kMenuEvents,
+ "documentEvents", // kDocumentEvents,
+ "textEvents", // kTextEvents,
+ "tableEvents", // kTableEvents,
+ "windowEvents", // kWindowEvents,
+ "hyperLinkEvents", // kHyperLinkEvents
+ "hyperTextEvents", // kHyperTextEvents
+];
+
+/**
+ * The map of event types. Events are listed in the order of nsIAccessibleEvent.
+ */
+var gEventTypesMap =
+[
+ new eventProps(kIgnoredEvents), // No event
+
+ new eventProps(kMutationEvents), // EVENT_SHOW
+ new eventProps(kMutationEvents), // EVENT_HIDE
+ new eventProps(kMutationEvents), // EVENT_REORDER
+
+ new eventProps(kChangeEvents), // EVENT_ACTIVE_DECENDENT_CHANGED
+
+ new eventProps(kNotificationEvents), // EVENT_FOCUS
+
+ new eventProps(kChangeEvents), // EVENT_STATE_CHANGE
+ new eventProps(kChangeEvents), // EVENT_LOCATION_CHANGE
+ new eventProps(kChangeEvents), // EVENT_NAME_CHANGE
+ new eventProps(kChangeEvents), // EVENT_DESCRIPTION_CHANGE
+ new eventProps(kChangeEvents), // EVENT_VALUE_CHANGE
+ new eventProps(kChangeEvents), // EVENT_HELP_CHANGE
+ new eventProps(kChangeEvents), // EVENT_DEFACTION_CHANGE
+ new eventProps(kChangeEvents), // EVENT_ACTION_CHANGE
+ new eventProps(kChangeEvents), // EVENT_ACCELERATOR_CHANGE
+
+ new eventProps(kSelectionEvents), // EVENT_SELECTION
+ new eventProps(kSelectionEvents), // EVENT_SELECTION_ADD
+ new eventProps(kSelectionEvents), // EVENT_SELECTION_REMOVE
+ new eventProps(kSelectionEvents), // EVENT_SELECTION_WITHIN
+
+ new eventProps(kNotificationEvents), // EVENT_ALERT
+ new eventProps(kNotificationEvents), // EVENT_FOREGROUND
+
+ new eventProps(kMenuEvents), // EVENT_MENU_START
+ new eventProps(kMenuEvents), // EVENT_MENU_END
+ new eventProps(kMenuEvents), // EVENT_MENUPOPUP_START
+ new eventProps(kMenuEvents), // EVENT_MENUPOPUP_END
+
+ new eventProps(kNotificationEvents), // EVENT_CAPTURE_START
+ new eventProps(kNotificationEvents), // EVENT_CAPTURE_END
+ new eventProps(kNotificationEvents), // EVENT_MOVESIZE_START
+ new eventProps(kNotificationEvents), // EVENT_MOVESIZE_END
+ new eventProps(kNotificationEvents), // EVENT_CONTEXTHELP_START
+ new eventProps(kNotificationEvents), // EVENT_CONTEXTHELP_END
+ new eventProps(kNotificationEvents, true), // EVENT_DRAGDROP_START
+ new eventProps(kNotificationEvents, true), // EVENT_DRAGDROP_END
+ new eventProps(kNotificationEvents), // EVENT_DIALOG_START
+ new eventProps(kNotificationEvents), // EVENT_DIALOG_END
+ new eventProps(kNotificationEvents), // EVENT_SCROLLING_START
+ new eventProps(kNotificationEvents), // EVENT_SCROLLING_END
+ new eventProps(kNotificationEvents), // EVENT_MINIMIZE_START
+ new eventProps(kNotificationEvents), // EVENT_MINIMIZE_END
+
+ new eventProps(kDocumentEvents), // EVENT_DOCUMENT_LOAD_START
+ new eventProps(kDocumentEvents), // EVENT_DOCUMENT_LOAD_COMPLETE
+ new eventProps(kDocumentEvents), // EVENT_DOCUMENT_RELOAD
+ new eventProps(kDocumentEvents), // EVENT_DOCUMENT_LOAD_STOPPED
+ new eventProps(kDocumentEvents), // EVENT_DOCUMENT_ATTRIBUTES_CHANGED
+ new eventProps(kDocumentEvents), // EVENT_DOCUMENT_CONTENT_CHANGED
+
+ new eventProps(kChangeEvents), // EVENT_PROPERTY_CHANGED
+
+ new eventProps(kSelectionEvents), // EVENT_SELECTION_CHANGED
+
+ new eventProps(kChangeEvents), // EVENT_TEXT_ATTRIBUTE_CHANGED
+
+ new eventProps(kTextEvents), // EVENT_TEXT_CARET_MOVED
+ new eventProps(kTextEvents), // EVENT_TEXT_CHANGED
+ new eventProps(kTextEvents), // EVENT_TEXT_INSERTED
+ new eventProps(kTextEvents), // EVENT_TEXT_REMOVED
+ new eventProps(kTextEvents), // EVENT_TEXT_UPDATED
+ new eventProps(kTextEvents), // EVENT_TEXT_SELECTION_CHANGED
+
+ new eventProps(kNotificationEvents), // EVENT_VISIBLE_DATA_CHANGED
+ new eventProps(kNotificationEvents), // EVENT_TEXT_COLUMN_CHANGED
+ new eventProps(kNotificationEvents), // EVENT_SECTION_CHANGED
+
+ new eventProps(kTableEvents), // EVENT_TABLE_CAPTION_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_MODEL_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_SUMMARY_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_ROW_DESCRIPTION_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_ROW_HEADER_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_ROW_INSERT
+ new eventProps(kTableEvents), // EVENT_TABLE_ROW_DELETE
+ new eventProps(kTableEvents), // EVENT_TABLE_ROW_REORDER
+ new eventProps(kTableEvents), // EVENT_TABLE_COLUMN_DESCRIPTION_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_COLUMN_HEADER_CHANGED
+ new eventProps(kTableEvents), // EVENT_TABLE_COLUMN_INSERT
+ new eventProps(kTableEvents), // EVENT_TABLE_COLUMN_DELETE
+ new eventProps(kTableEvents), // EVENT_TABLE_COLUMN_REORDER
+
+ new eventProps(kWindowEvents), // EVENT_WINDOW_ACTIVATE
+ new eventProps(kWindowEvents), // EVENT_WINDOW_CREATE
+ new eventProps(kWindowEvents), // EVENT_WINDOW_DEACTIVATE
+ new eventProps(kWindowEvents), // EVENT_WINDOW_DESTROY
+ new eventProps(kWindowEvents), // EVENT_WINDOW_MAXIMIZE
+ new eventProps(kWindowEvents), // EVENT_WINDOW_MINIMIZE
+ new eventProps(kWindowEvents), // EVENT_WINDOW_RESIZE
+ new eventProps(kWindowEvents), // EVENT_WINDOW_RESTORE
+
+ new eventProps(kHyperLinkEvents), // EVENT_HYPERLINK_END_INDEX_CHANGED
+ new eventProps(kHyperLinkEvents), // EVENT_HYPERLINK_NUMBER_OF_ANCHORS_CHANGED
+ new eventProps(kHyperLinkEvents), // EVENT_HYPERLINK_SELECTED_LINK_CHANGED
+
+ new eventProps(kHyperTextEvents), // EVENT_HYPERTEXT_LINK_ACTIVATED
+ new eventProps(kHyperTextEvents), // EVENT_HYPERTEXT_LINK_SELECTED
+
+ new eventProps(kHyperLinkEvents), // EVENT_HYPERLINK_START_INDEX_CHANGED
+
+ new eventProps(kHyperTextEvents), // EVENT_HYPERTEXT_CHANGED
+ new eventProps(kHyperTextEvents), // EVENT_HYPERTEXT_NLINKS_CHANGED
+
+ new eventProps(kChangeEvents), // EVENT_OBJECT_ATTRIBUTE_CHANGED
+ new eventProps(kChangeEvents), // EVENT_PAGE_CHANGED
+
+ new eventProps(kDocumentEvents) // EVENT_INTERNAL_LOAD
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Functions and objects for usage in event handler editor.
+
+var accRetrieval = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+
+function output(aValue)
+{
+ gEventHandlerOutput.push(aValue);
+}
+
+function outputRole(aAccessible)
+{
+ output(accRetrieval.getStringRole(aAccessible.role));
+}
+
+function outputStates(aAccessible)
+{
+ var stateObj = {}, extraStateObj = {};
+ aAccessible.getState(stateObj, extraStateObj);
+ var states = accRetrieval.getStringStates(stateObj.value,
+ extraStateObj.value);
+
+ var list = [];
+ for (let i = 0; i < states.length; i++) {
+ list.push(states.item(i));
+ }
+
+ output(list.join());
+}
+
+function outputAttrs(aAccessible)
+{
+ var str = "";
+ var attrs = aAccessible.attributes;
+ if (attrs) {
+ var enumerate = attrs.enumerate();
+ while (enumerate.hasMoreElements()) {
+ var prop = XPCU.QI(enumerate.getNext(), nsIPropertyElement);
+ str += prop.key + ": " + prop.value + "; ";
+ }
+
+ if (str)
+ output(str);
+ }
+}
+
+function outputTree(aAccessible, aHighlightList)
+{
+ var treeObj = {
+ cols: {
+ outputtreeRole: {
+ name: gBundle.getString("role"),
+ flex: 2,
+ isPrimary: true
+ },
+ outputtreeName: {
+ name: gBundle.getString("name"),
+ flex: 2
+ },
+ outputtreeNodename: {
+ name: gBundle.getString("nodeName"),
+ flex: 1
+ },
+ outputtreeId:{
+ name: gBundle.getString("id"),
+ flex: 1
+ }
+ },
+ view: new inAccTreeView(aAccessible, aHighlightList)
+ };
+
+ output(treeObj);
+}
+
+function outputDOMAttrs(aAccessible)
+{
+ var DOMNode = QIAccessNode(aAccessible).DOMNode;
+ var DOMAttributes = DOMNode.attributes;
+ for (let i = 0; i < DOMAttributes.length; i++) {
+ var DOMAttribute = DOMAttributes.item(i);
+ output(DOMAttribute.name + ": " + DOMAttribute.value);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Private functions.
+
+function inAccTreeView(aAccessible, aHighlightList)
+{
+ inDataTreeView.call(this);
+
+ var list = this.generateChildren(aAccessible, aHighlightList);
+ if (list) {
+ this.expandNodes(list);
+ }
+}
+
+inAccTreeView.prototype = new inDataTreeView();
+
+// nsITreeView
+inAccTreeView.prototype.getRowProperties =
+ function inAccTreeView_getRowProperties(aRowIdx, aProperties)
+{
+ var data = this.getDataAt(aRowIdx);
+ if (data && data.properties) {
+ if (!aProperties)
+ return data.properties.join(" ");
+
+ for (let i = 0; i < data.properties.length; i++) {
+ var atom = this.createAtom(data.properties[i]);
+ aProperties.AppendElement(atom);
+ }
+ }
+
+ return "";
+};
+
+inAccTreeView.prototype.getCellProperties =
+ function inAccTreeView_getCellProperties(aRowIdx, aCol, aProperties)
+{
+ return this.getRowProperties(aRowIdx, aProperties);
+};
+
+// Initialization
+inAccTreeView.prototype.generateChildren =
+ function inAccTreeView_generateChildren(aAccessible, aHighlightList, aParent,
+ aIsUnattached)
+{
+ var data = {
+ properties: []
+ };
+
+ var accessible = QIAccessNode(aAccessible);
+
+ // Highlight the row for accessible from the list.
+ var isHighlighted =
+ aHighlightList && aHighlightList.indexOf(aAccessible) != -1;
+ if (isHighlighted) {
+ data.properties.push("highlight");
+ }
+
+ // Gray out the row for accessible unattached from the tree.
+ if (aIsUnattached) {
+ data.properties.push("grayout");
+ }
+
+ // Add cells data.
+ data["outputtreeRole"] = accRetrieval.getStringRole(aAccessible.role);
+ data["outputtreeName"] = aAccessible.name;
+ data["outputtreeNodename"] = accessible.DOMNode.nodeName;
+ data["outputtreeId"] =
+ ("id" in accessible.DOMNode ? accessible.DOMNode.id : "");
+
+ var parent = this.appendChild(aParent, data);
+ var nodesToExpand = null;
+
+ // Insert highlighted row for target of handled hide event if it's specified
+ // in the list (works for Gecko versions higher 13).
+ var containsUnattached = false;
+ var accBeforeUnattached = null;
+ if ("nsIAccessibleHideEvent" in Ci &&
+ gEvent instanceof Ci.nsIAccessibleHideEvent &&
+ gEvent.targetParent == aAccessible) {
+ containsUnattached = true;
+ try {
+ if (gEvent.targetNextSibling.parent == aAccessible) {
+ accBeforeUnattached = gEvent.targetNextSibling;
+ }
+ } catch (e) {
+ }
+ try {
+ if (!accBeforeUnattached &&
+ gEvent.targetPrevSibling.parent == aAccessible) {
+ accBeforeUnattached = gEvent.targetPrevSibling.nextSibling;
+ }
+ } catch (e) {
+ }
+ }
+
+ // Add children.
+ var childCount = aAccessible.childCount;
+ for (let i = 0; i < childCount; i++) {
+ var child = aAccessible.getChildAt(i);
+
+ // Add unattached child before current child.
+ if (accBeforeUnattached == child) {
+ var list =
+ this.generateChildren(gEvent.accessible, aHighlightList, parent, true);
+ if (list) {
+ nodesToExpand = list.concat(nodesToExpand || []);
+ }
+ }
+
+ var list =
+ this.generateChildren(child, aHighlightList, parent);
+ if (list) {
+ nodesToExpand = list.concat(nodesToExpand || []);
+ }
+ }
+
+ // Put unattached child as last child of the parent, we don't have good guess
+ // about its hierarchy position.
+ if (containsUnattached && !accBeforeUnattached) {
+ var list =
+ this.generateChildren(gEvent.accessible, aHighlightList, parent, true);
+ if (list) {
+ nodesToExpand = list.concat(nodesToExpand || []);
+ }
+ }
+
+ return nodesToExpand ? nodesToExpand.concat(parent) : isHighlighted && [];
+};
diff --git a/inspector/content/viewers/accessibleEvents/accessibleEvents.xul b/inspector/content/viewers/accessibleEvents/accessibleEvents.xul
new file mode 100644
index 00000000..fa1b6226
--- /dev/null
+++ b/inspector/content/viewers/accessibleEvents/accessibleEvents.xul
@@ -0,0 +1,150 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &handlerEditorNoEvent.label;
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/accessibleEvents/handlerHelpDialog.xul b/inspector/content/viewers/accessibleEvents/handlerHelpDialog.xul
new file mode 100644
index 00000000..37b03181
--- /dev/null
+++ b/inspector/content/viewers/accessibleEvents/handlerHelpDialog.xul
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+ &general.descr;
+ &helpers.descr;
+
+
+
+
+
+
+
+
+ &event.descr;
+
+
+
+ &target.descr;
+
+
+
+ &accRetrieval.descr;
+
+
+
+ &output.descr;
+
+
+
+ &outputAttrs.descr;
+
+
+
+ &outputRole.descr;
+
+
+
+ &outputStates.descr;
+
+
+
+ &outputTree.descr;
+
+
+
+ &outputDOMAttrs.descr;
+
+
+
+
+
diff --git a/inspector/content/viewers/accessibleObject/accessibleObject.js b/inspector/content/viewers/accessibleObject/accessibleObject.js
new file mode 100644
index 00000000..8932ee56
--- /dev/null
+++ b/inspector/content/viewers/accessibleObject/accessibleObject.js
@@ -0,0 +1,59 @@
+/* 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/. */
+
+/***************************************************************
+* AccessibleObjectViewer --------------------------------------------
+* The viewer for the accessible object.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/events/ObserverManager.js
+****************************************************************/
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+var bundle;
+var accService;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kAccessibleRetrievalCID = "@mozilla.org/accessibleRetrieval;1";
+
+const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval;
+const nsIAccessible = Components.interfaces.nsIAccessible;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization/Destruction
+
+window.addEventListener("load", AccessibleObjectViewer_initialize, false);
+
+function AccessibleObjectViewer_initialize()
+{
+ bundle = document.getElementById("inspector-bundle");
+ accService = XPCU.getService(kAccessibleRetrievalCID, nsIAccessibleRetrieval);
+
+ viewer = new JSObjectViewer();
+
+ viewer.__defineGetter__(
+ "uid",
+ function uidGetter()
+ {
+ return "accessibleObject";
+ }
+ );
+
+ viewer.__defineSetter__(
+ "subject",
+ function subjectSetter(aObject)
+ {
+ var accObject = aObject instanceof nsIAccessible ?
+ aObject : accService.getAccessibleFor(aObject);
+ this.setSubject(accObject);
+ }
+ );
+
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
diff --git a/inspector/content/viewers/accessibleObject/accessibleObject.xul b/inspector/content/viewers/accessibleObject/accessibleObject.xul
new file mode 100644
index 00000000..2f720f4e
--- /dev/null
+++ b/inspector/content/viewers/accessibleObject/accessibleObject.xul
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/accessibleProps/accessiblePropViewerMgr.js b/inspector/content/viewers/accessibleProps/accessiblePropViewerMgr.js
new file mode 100644
index 00000000..da02ee4a
--- /dev/null
+++ b/inspector/content/viewers/accessibleProps/accessiblePropViewerMgr.js
@@ -0,0 +1,619 @@
+/* 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/. */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Global
+
+const nsIAccessibleText = Components.interfaces.nsIAccessibleText;
+const nsIAccessibleTableCell = Components.interfaces.nsIAccessibleTableCell;
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Accessible property viewer manager
+
+/**
+ * Used to show additional properties of the accessible in tabbox.
+ *
+ * @param aPaneElm
+ * A pane element where the view is hosted.
+ */
+function accessiblePropViewerMgr(aPaneElm)
+{
+ /**
+ * Updates all property views for the given accessible.
+ *
+ * @param aAccessible
+ * The given accessible
+ */
+ this.updateViews = function accessiblePropViewerMgr_updateViews(aAccessible)
+ {
+ for (var id in this.viewers)
+ {
+ var tab = document.getElementById("tab_" + id);
+ tab.hidden = !this.viewers[id].update(aAccessible);
+ }
+
+ this.tabboxElm.selectedIndex = this.getCurrentViewerIdx();
+ }
+
+ /**
+ * Clear the data of property views.
+ */
+ this.clearViews = function accessiblePropViewerMgr_clearViews()
+ {
+ for (var id in this.viewers)
+ {
+ this.viewers[id].clear();
+
+ var tab = document.getElementById("tab_" + id);
+ tab.hidden = true;
+ }
+ }
+
+ this.isCommandEnabled =
+ function accessiblePropViewerMgr_isCommandEnabled(aCommand)
+ {
+ var tab = this.tabboxElm.selectedTab;
+ var viewerid = tab.id.replace("tab_", "");
+ var viewer = this.viewers[viewerid];
+ if ("isCommandEnabled" in viewer) {
+ return viewer.isCommandEnabled(aCommand);
+ }
+ return false;
+ },
+
+ /**
+ * Process 'inspectInNewView' command for selected property view.
+ */
+ this.inspectInNewView = function accessiblePropViewerMgr_inspectInNewView()
+ {
+ var tab = this.tabboxElm.selectedTab;
+ var viewerid = tab.id.replace("tab_", "");
+ var viewer = this.viewers[viewerid];
+ if ("inspectInNewView" in viewer)
+ viewer.inspectInNewView();
+ }
+
+ this.doCommand = function accessiblePropViewerMgr_doCommand(aCommandId)
+ {
+ var tab = this.tabboxElm.selectedTab;
+ var viewerid = tab.id.replace("tab_", "");
+ var viewer = this.viewers[viewerid];
+ if ("doCommand" in viewer)
+ viewer.doCommand(aCommandId);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// private
+
+ this.handleEvent = function accessiblePropViewerMgr_handleEvent(aEvent)
+ {
+ this.setCurrentViewerIdx(this.tabboxElm.selectedIndex);
+ viewer.pane.panelset.updateAllCommands();
+ }
+
+ this.setCurrentViewerIdx = function accessiblePropViewerMgr_setCurrentViewerIdx(aIdx)
+ {
+ this.paneElm.accessiblePropsCurrentViewerIdx = aIdx;
+ }
+
+ this.getCurrentViewerIdx = function accessiblePropViewerMgr_getCurrentViewerIdx()
+ {
+ var idx = this.paneElm.accessiblePropsCurrentViewerIdx;
+
+ idx = idx ? idx : 0;
+ var tab = this.tabsElm.children[idx];
+ if (tab.hidden)
+ return 0;
+
+ return idx;
+ }
+
+ this.viewers = {
+ "attributes": new attributesViewer(),
+ "actions": new actionViewer(),
+ "textattrs": new textAttrsViewer(),
+ "tablecell": new tableCellViewer()
+ };
+
+ this.tabboxElm = document.getElementById("tabviewers");
+ this.tabsElm = this.tabboxElm.tabs;
+ this.tabsElm.addEventListener("select", this, false);
+ this.paneElm = aPaneElm;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+//// Accessible property viewers
+
+/**
+ * Object attribute property view. Used to display accessible attributes.
+ */
+function attributesViewer()
+{
+ /**
+ * Updates the view for the given accessible.
+ *
+ * @param aAccessible
+ * The given accessible
+ */
+ this.update = function attributesViewer_update(aAccessible)
+ {
+ var attrs = aAccessible.attributes;
+ if (attrs) {
+ var enumerate = attrs.enumerate();
+ while (enumerate.hasMoreElements())
+ this.addAttribute(enumerate.getNext());
+ }
+
+ return true;
+ }
+
+ /**
+ * Clear the view's data.
+ */
+ this.clear = function attributesViewer_clear()
+ {
+ var trAttrBody = document.getElementById("trAttrBody");
+ while (trAttrBody.hasChildNodes())
+ trAttrBody.removeChild(trAttrBody.lastChild)
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// private
+
+ this.addAttribute = function attrbiutesViewer_addAttribute(aElement)
+ {
+ var prop = XPCU.QI(aElement, nsIPropertyElement);
+
+ var trAttrBody = document.getElementById("trAttrBody");
+
+ var ti = document.createElement("treeitem");
+ var tr = document.createElement("treerow");
+
+ var tc = document.createElement("treecell");
+ tc.setAttribute("label", prop.key);
+ tr.appendChild(tc);
+
+ tc = document.createElement("treecell");
+ tc.setAttribute("label", prop.value);
+ tr.appendChild(tc);
+
+ ti.appendChild(tr);
+
+ trAttrBody.appendChild(ti);
+ }
+}
+
+
+/**
+ * Action property view.
+ */
+function actionViewer()
+{
+ /**
+ * Updates the view for the given accessible.
+ *
+ * @param aAccessible
+ * The given accessible
+ */
+ this.update = function actionViewer_update(aAccessible)
+ {
+ this.mAccessible = aAccessible;
+
+ // nsIAccessible::numActions was renamed to actionCount in Mozilla 15.
+ let count = ("actionCount" in aAccessible) ?
+ aAccessible.actionCount : aAccessible.numActions;
+ if (!count)
+ return false;
+
+ this.updateActionItem(this.mDefaultActionItem, 0);
+
+ for (let idx = 1; idx < count; idx++) {
+ let actionItem = this.mDefaultActionItem.cloneNode(true);
+ this.updateActionItem(actionItem, idx);
+ this.mActionItemContainer.appendChild(actionItem);
+ }
+
+ return true;
+ }
+
+ /**
+ * Clear the view's data.
+ */
+ this.clear = function actionViewer_clear()
+ {
+ this.mAccessible = null;
+
+ let cntr = this.mActionItemContainer;
+ while (cntr.firstChild != cntr.lastChild)
+ cntr.removeChild(cntr.lastChild);
+
+ this.setValues(this.mDefaultActionItem, "", "", "", "", "");
+ }
+
+ /**
+ * Performes a command.
+ */
+ this.doCommand = function actionViewer_doCommand(aCommandId)
+ {
+ this.mAccessible.doAction(aCommandId);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// private
+
+ this.updateActionItem = function actionViewer_updateActionItem(aActionItem,
+ aActionIndex)
+ {
+ var index = (aActionIndex + 1) + ".";
+ var name = this.mAccessible.getActionName(aActionIndex);
+ var description = this.mAccessible.getActionDescription(aActionIndex);
+
+ var keysStr = "";
+ try {
+ let keys = this.mAccessible.getKeyBindings(aActionIndex);
+ for (let idx = 0; idx < keys.length; idx++)
+ keysStr += keys.item(idx);
+
+ } catch (e) { }
+
+ let jsCommand = "viewer.doCommand(" + aActionIndex + ");";
+
+ this.setValues(aActionItem, index, name, description, keysStr, jsCommand);
+ }
+
+ this.setValues = function actionViewer_setValues(aActionItem,
+ aIndex, aName, aDescription,
+ aKeyBindings, aJSCommand)
+ {
+ let elm = aActionItem.getElementsByAttribute("prop", "actionIndex")[0];
+ elm.textContent = aIndex;
+
+ elm = aActionItem.getElementsByAttribute("prop", "actionName")[0];
+ elm.textContent = aName;
+
+ elm = aActionItem.getElementsByAttribute("prop", "actionDescription")[0];
+ elm.textContent = aDescription;
+
+ elm = aActionItem.getElementsByAttribute("prop", "actionKeyBindings")[0];
+ elm.textContent = aKeyBindings;
+
+ elm = aActionItem.getElementsByAttribute("prop", "invokeAction")[0];
+ if (aJSCommand)
+ elm.setAttribute("oncommand", aJSCommand)
+ else
+ elm.removeAttribute("oncommand");
+ }
+
+ this.mAccessible = null;
+
+ this.mActionItemContainer = document.getElementById("actionItemContainer");
+ this.mDefaultActionItem = document.getElementById("actionItem");
+}
+
+
+/**
+ * Text attributes property view.
+ */
+function textAttrsViewer()
+{
+ /**
+ * Updates the view for the given accessible.
+ */
+ this.update = function textAttrsViewer_update(aAccessible)
+ {
+ if (!(aAccessible instanceof nsIAccessibleText))
+ return false;
+
+ // Default text attributes.
+ this.addAttributes(aAccessible.defaultTextAttributes,
+ "textAttrs:default:treeBody");
+
+ // Generate text ranges.
+ var length = aAccessible.characterCount;
+ var offset = 0;
+ while (offset < length) {
+ const kHTMLNS = "http://www.w3.org/1999/xhtml";
+ var textRangeElm = document.createElementNS(kHTMLNS, "span");
+ textRangeElm.setAttribute("class", "textAttrsTextRange");
+
+ var endOffset = { };
+
+ textRangeElm.textAttrs =
+ aAccessible.getTextAttributes(false, offset, { }, endOffset);
+ textRangeElm.startOffset = offset;
+ textRangeElm.endOffset = endOffset.value;
+
+ var text = aAccessible.getText(offset, endOffset.value);
+ textRangeElm.textContent = text;
+
+ textRangeElm.addEventListener("focus", this, false);
+ textRangeElm.setAttribute("tabindex", 0);
+
+ document.getElementById("textAttrs:content").appendChild(textRangeElm);
+
+ offset = endOffset.value;
+ }
+
+ return true;
+ }
+
+ /**
+ * Clear the view's data.
+ */
+ this.clear = function textAttrsViewer_clear()
+ {
+ var content = document.getElementById("textAttrs:content");
+ while (content.hasChildNodes()) {
+ content.removeChild(content.lastChild);
+ }
+
+ var treeBody = document.getElementById("textAttrs:default:treeBody");
+ while (treeBody.hasChildNodes()) {
+ treeBody.removeChild(treeBody.lastChild);
+ }
+
+ document.getElementById("textAttrs:startOffset").textContent = "";
+ document.getElementById("textAttrs:endOffset").textContent = "";
+
+ treeBody = document.getElementById("textAttrs:treeBody");
+ while (treeBody.hasChildNodes()) {
+ treeBody.removeChild(treeBody.lastChild);
+ }
+ }
+
+ this.handleEvent = function textAttrsViewer_handleEvent(aEvent)
+ {
+ var treeBody = document.getElementById("textAttrs:treeBody");
+ while (treeBody.hasChildNodes()) {
+ treeBody.removeChild(treeBody.lastChild);
+ }
+
+ if (this.mLastElm) {
+ this.mLastElm.removeAttribute("selected");
+ }
+
+ this.mLastElm = aEvent.target;
+ this.mLastElm.setAttribute("selected", "true");
+
+ document.getElementById("textAttrs:startOffset").textContent =
+ this.mLastElm.startOffset;
+ document.getElementById("textAttrs:endOffset").textContent =
+ this.mLastElm.endOffset;
+
+ this.addAttributes(this.mLastElm.textAttrs, "textAttrs:treeBody");
+ }
+
+ this.addAttributes = function textAttrsViewer_addAttributes(aTextAttrs,
+ aTreeID)
+ {
+ var enumerate = aTextAttrs.enumerate();
+ while (enumerate.hasMoreElements()) {
+ var prop = XPCU.QI(enumerate.getNext(), nsIPropertyElement);
+
+ var treeBody = document.getElementById(aTreeID);
+
+ var ti = document.createElement("treeitem");
+ var tr = document.createElement("treerow");
+
+ var tc = document.createElement("treecell");
+ tc.setAttribute("label", prop.key);
+ tr.appendChild(tc);
+
+ tc = document.createElement("treecell");
+ tc.setAttribute("label", prop.value);
+ tr.appendChild(tc);
+
+ ti.appendChild(tr);
+
+ treeBody.appendChild(ti);
+ }
+ }
+
+ this.mLastElm = null;
+}
+
+
+/**
+ * Table cell property view. Used to display table cell properties of the
+ * accessible implementing nsIAccessibleTableCell.
+ */
+function tableCellViewer()
+{
+ /**
+ * Updates the view for the given accessible.
+ *
+ * @param aAccessible
+ * The given accessible
+ */
+ this.update = function tableCellViewer_update(aAccessible)
+ {
+ if (!(aAccessible instanceof nsIAccessibleTableCell))
+ return false;
+
+ // columnIndex
+ var columnIndex = aAccessible.columnIndex;
+ this.columnIndexElm.textContent = columnIndex;
+
+ // rowIndex
+ var rowIndex = aAccessible.rowIndex;
+ this.rowIndexElm.textContent = rowIndex;
+
+ // columnExtent
+ var columnExtent = aAccessible.columnExtent;
+ this.columnExtentElm.textContent = columnExtent;
+
+ // rowIndex
+ var rowExtent = aAccessible.rowExtent;
+ this.rowExtentElm.textContent = rowExtent;
+
+ // isSelected
+ var isSelected = aAccessible.isSelected();
+ this.isSelectedElm.textContent = isSelected;
+
+ // table, columnHeaderCells, rowHeaderCells
+ this.addRelated(aAccessible);
+
+ return true;
+ }
+
+ /**
+ * Clear the view's data.
+ */
+ this.clear = function tableCellViewer_clear()
+ {
+ this.mTreeBox.view = null;
+
+ this.columnIndexElm.textContent = "";
+ this.rowIndexElm.textContent = "";
+ this.columnExtentElm.textContent = "";
+ this.rowExtentElm.textContent = "";
+ this.isSelectedElm.textContent = "";
+ }
+
+ this.isCommandEnabled = function tableCellViewer_isCommandEnable(aCommand)
+ {
+ if (aCommand == "cmdEditInspectInNewWindow") {
+ return this.mTreeView.selection.count == 1;
+ }
+ return false;
+ },
+
+ /**
+ * Prepares 'inspectInNewView' command.
+ */
+ this.inspectInNewView = function tableCellViewer_inspectInNewView()
+ {
+ if (this.mTreeView.selection.count == 1) {
+ var minAndMax = {};
+ this.mTreeView.selection.getRangeAt(0, minAndMax, minAndMax);
+ var node = this.mTreeView.getDOMNode(minAndMax.value);
+ if (node) {
+ inspectObject(node);
+ }
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// private
+
+ this.addRelated = function tableCellViewer_addRelated(aAccessible)
+ {
+ this.mTreeView = new TableCellTreeView(aAccessible);
+ this.mTreeBox.view = this.mTreeView;
+ }
+
+ this.mTree = document.getElementById("tableCell:accObjects");
+ this.mTreeBox = this.mTree.treeBoxObject;
+
+ this.columnIndexElm = document.getElementById("tableCell:columnIndex");
+ this.rowIndexElm = document.getElementById("tableCell:rowIndex");
+ this.columnExtentElm = document.getElementById("tableCell:columnExtent");
+ this.rowExtentElm = document.getElementById("tableCell:rowExtent");
+ this.isSelectedElm = document.getElementById("tableCell:isSelected");
+}
+
+
+///////////////////////////////////////////////////////////////////////////////
+//// TableCellTreeView. nsITreeView
+
+function TableCellTreeView(aTableCell)
+{
+ this.tableCell = aTableCell;
+ this.mRowCount = this.getRowCount();
+}
+
+TableCellTreeView.prototype = new inBaseTreeView();
+
+TableCellTreeView.prototype.getRowCount =
+ function TableCellTreeView_rowCount()
+{
+ this.columnHeaderCells = this.tableCell.columnHeaderCells;
+ this.columnHeaderCellsLen = (this.columnHeaderCells ?
+ this.columnHeaderCells.length : 0);
+
+ this.rowHeaderCells = this.tableCell.rowHeaderCells;
+ this.rowHeaderCellsLen = (this.rowHeaderCells ?
+ this.rowHeaderCells.length : 0);
+
+ return 1 + this.columnHeaderCellsLen + this.rowHeaderCellsLen;
+}
+
+TableCellTreeView.prototype.getCellText =
+ function TableCellTreeView_getCellText(aRow, aCol)
+{
+ var accessible = this.getAccessible(aRow);
+ if (!accessible)
+ return "";
+
+ if (aCol.id == "tableCell:property") {
+ return this.getPropertyName(aRow);
+
+ } else if (aCol.id == "tableCell:role") {
+ return gAccService.getStringRole(accessible.role);
+
+ } else if (aCol.id == "tableCell:name") {
+ return accessible.name;
+
+ } else if (aCol.id == "tableCell:nodeName") {
+ var node = this.getDOMNode(aRow);
+ if (node)
+ return node.nodeName;
+ }
+
+ return "";
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// TableCellTreeView. Utils
+
+/**
+ * Return an accessible for the given row index.
+ *
+ * @param aRow
+ * Row index
+ */
+TableCellTreeView.prototype.getAccessible =
+ function TableCellTreeView_getAccessible(aRow)
+{
+ if (aRow == 0)
+ return this.tableCell.table;
+
+ if (aRow <= this.columnHeaderCellsLen)
+ return this.columnHeaderCells.queryElementAt(aRow - 1, nsIAccessible);
+
+ return this.rowHeaderCells.queryElementAt(aRow - 1 - this.columnHeaderCellsLen, nsIAccessible);
+}
+
+/**
+ * Retrun interface attribute name (property) used at the given row index.
+ *
+ * @param aRow
+ * Row index
+ */
+TableCellTreeView.prototype.getPropertyName =
+ function TableCellTreeView_getPropertyName(aRow)
+{
+ if (aRow == 0)
+ return "table";
+
+ if (aRow <= this.columnHeaderCellsLen)
+ return "column header cell";
+
+ return "row header cell";
+}
+
+/**
+ * Return DOM node at the given row index.
+ *
+ * @param aRow
+ * Row index
+ */
+TableCellTreeView.prototype.getDOMNode =
+ function TableCellTreeView_getDOMNode(aRow)
+{
+ var accessNode = QIAccessNode(this.getAccessible(aRow));
+ return accessNode && accessNode.DOMNode;
+}
diff --git a/inspector/content/viewers/accessibleProps/accessibleProps.js b/inspector/content/viewers/accessibleProps/accessibleProps.js
new file mode 100644
index 00000000..975b2dc6
--- /dev/null
+++ b/inspector/content/viewers/accessibleProps/accessibleProps.js
@@ -0,0 +1,241 @@
+/* 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/. */
+
+/*****************************************************************************
+* AccessiblePropsViewer ------------------------------------------------------
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/jsutil/events/ObserverManager.js
+* chrome://inspector/content/jsutil/system/PrefUtils.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/xul/FrameExchange.js
+*****************************************************************************/
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+var gBundle;
+var gAccService = null;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kAccessibleRetrievalCID = "@mozilla.org/accessibleRetrieval;1";
+
+const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval;
+const nsIAccessible = Components.interfaces.nsIAccessible;
+
+const nsIPropertyElement = Components.interfaces.nsIPropertyElement;
+
+/**
+ * QI nsIAccessNode interface if any, used for compatibility with Gecko versions
+ * prior to Gecko13.
+ */
+function QIAccessNode(aAccessible)
+{
+ return "nsIAccessNode" in Components.interfaces ?
+ XPCU.QI(aAccessible, Components.interfaces.nsIAccessNode) : aAccessible;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization/Destruction
+
+window.addEventListener("load", AccessiblePropsViewer_initialize, false);
+
+function AccessiblePropsViewer_initialize()
+{
+ gBundle = document.getElementById("accessiblePropsBundle");
+
+ viewer = new AccessiblePropsViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// class AccessiblePropsViewer
+function AccessiblePropsViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+
+ gAccService = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+}
+
+AccessiblePropsViewer.prototype =
+{
+ mSubject: null,
+ mPane: null,
+ mAccSubject: null,
+ mAccService: null,
+ mPropViewerMgr: null,
+
+ get uid() { return "accessibleProps" },
+ get pane() { return this.mPane },
+
+ get subject() { return this.mSubject },
+ set subject(aObject)
+ {
+ this.mSubject = aObject;
+ this.updateView();
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ },
+
+ initialize: function initialize(aPane)
+ {
+ this.mPropViewerMgr = new accessiblePropViewerMgr(aPane);
+
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ isCommandEnabled: function isCommandEnabled(aCommand)
+ {
+ return this.mPropViewerMgr.isCommandEnabled(aCommand);
+ },
+
+ getCommand: function getCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditInspectInNewWindow":
+ return new cmdEditInspectInNewWindow(this.mPropViewerMgr);
+ }
+ return null;
+ },
+
+ destroy: function destroy() {},
+
+ /////////////////////////
+ //// event dispatching
+
+ addObserver: function addObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+ removeObserver: function removeObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ /////////////////////////
+ //// utils
+
+ doCommand: function doCommand(aCommandId)
+ {
+ this.mPropViewerMgr.doCommand(aCommandId);
+ },
+
+ // private
+ updateView: function updateView()
+ {
+ this.clearView();
+
+ this.mAccSubject = this.mSubject instanceof nsIAccessible ?
+ this.mSubject : gAccService.getAccessibleFor(this.mSubject);
+
+ // accessible properties.
+ var propContainer = document.getElementById("mainPropContainer");
+ var containers = propContainer.getElementsByAttribute("prop", "*");
+
+ for (var i = 0; i < containers.length; ++i) {
+ var value = "";
+ try {
+ var prop = containers[i].getAttribute("prop");
+ value = this[prop];
+ } catch (e) {
+ dump("Accessibility " + prop + " property is not available.\n");
+ }
+
+ if (value instanceof Array)
+ containers[i].value = value.join(", ");
+ else
+ containers[i].value = value;
+ }
+
+ this.mPropViewerMgr.updateViews(this.mAccSubject);
+ },
+
+ clearView: function clearView()
+ {
+ var containers = document.getElementsByAttribute("prop", "*");
+ for (var i = 0; i < containers.length; ++i)
+ containers[i].textContent = "";
+
+ this.mPropViewerMgr.clearViews();
+ },
+
+ get role()
+ {
+ // 'finalRole' is replaced by 'role' property in Gecko 1.9.2.
+ var role = "finalRole" in this.mAccSubject ?
+ this.mAccSubject.finalRole : this.mAccSubject.role;
+ return gAccService.getStringRole(role);
+ },
+
+ get name()
+ {
+ return this.mAccSubject.name;
+ },
+
+ get description()
+ {
+ return this.mAccSubject.description;
+ },
+
+ get value()
+ {
+ return this.mAccSubject.value;
+ },
+
+ get state()
+ {
+ var stateObj = {value: null};
+ var extStateObj = {value: null};
+
+ // Since Firefox 3.1 nsIAccessible::getFinalState has been renamed to
+ // nsIAccessible::getState.
+ if ("getState" in this.mAccSubject)
+ this.mAccSubject.getState(stateObj, extStateObj);
+ else
+ this.mAccSubject.getFinalState(stateObj, extStateObj);
+
+ var list = [];
+
+ var states = gAccService.getStringStates(stateObj.value,
+ extStateObj.value);
+
+ for (var i = 0; i < states.length; i++)
+ list.push(states.item(i));
+ return list;
+ },
+
+ get bounds()
+ {
+ var x = { value: 0 };
+ var y = { value: 0 };
+ var width = { value: 0 };
+ var height = { value: 0 };
+ this.mAccSubject.getBounds(x, y, width, height);
+
+ return gBundle.getFormattedString("accBounds",
+ [x.value, y.value,
+ width.value, height.value]);
+ }
+};
+
+function cmdEditInspectInNewWindow(aMgr)
+{
+ this.mPropViewerMgr = aMgr;
+}
+
+cmdEditInspectInNewWindow.prototype = new inBaseCommand();
+
+cmdEditInspectInNewWindow.prototype.doTransaction =
+ function InspectInNewWindow_DoTransaction()
+{
+ if (this.mPropViewerMgr) {
+ this.mPropViewerMgr.inspectInNewView();
+ }
+};
diff --git a/inspector/content/viewers/accessibleProps/accessibleProps.xul b/inspector/content/viewers/accessibleProps/accessibleProps.xul
new file mode 100644
index 00000000..1015646a
--- /dev/null
+++ b/inspector/content/viewers/accessibleProps/accessibleProps.xul
@@ -0,0 +1,226 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &descRole.label;
+
+
+
+ &descName.label;
+
+
+
+ &descDescription.label;
+
+
+
+ &descValue.label;
+
+
+
+ &descState.label;
+
+
+
+ &descBounds.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &descStartOffset.label;
+
+ &descEndOffset.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &descColumnIdx.label;
+
+
+ &descRowIdx.label;
+
+
+
+ &descColumnExtent.label;
+
+
+ &descRowExtent.label;
+
+
+
+ &descIsSelected.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/accessibleRelations/accessibleRelations.js b/inspector/content/viewers/accessibleRelations/accessibleRelations.js
new file mode 100644
index 00000000..cabde916
--- /dev/null
+++ b/inspector/content/viewers/accessibleRelations/accessibleRelations.js
@@ -0,0 +1,256 @@
+/* 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/. */
+
+/***************************************************************
+* AccessibleRelationsViewer --------------------------------------------
+* The viewer for the accessible relations for the inspected accessible.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+****************************************************************/
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+var gAccService = null;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kAccessibleRetrievalCID = "@mozilla.org/accessibleRetrieval;1";
+
+const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval;
+const nsIAccessibleRelation = Components.interfaces.nsIAccessibleRelation;
+const nsIAccessible = Components.interfaces.nsIAccessible;
+
+/**
+ * Used for compatibility with Gecko versions prior to Gecko13.
+ */
+const nsIAccessNode = Components.interfaces.nsIAccessNode || nsIAccessible;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization
+
+window.addEventListener("load", AccessibleRelationsViewer_initialize, false);
+
+function AccessibleRelationsViewer_initialize()
+{
+ gAccService = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+
+ viewer = new AccessibleRelationsViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// class AccessibleRelationsViewer
+
+function AccessibleRelationsViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+
+ this.mTree = document.getElementById("olAccessibleRelations");
+ this.mTreeBox = this.mTree.treeBoxObject;
+
+ this.mTargetsTree = document.getElementById("olAccessibleTargets");
+ this.mTargetsTreeBox = this.mTargetsTree.treeBoxObject;
+}
+
+AccessibleRelationsViewer.prototype =
+{
+ /////////////////////////
+ //// initialization
+
+ mSubject: null,
+ mPane: null,
+ mView: null,
+ mTargetsView: null,
+
+ /////////////////////////
+ //// interface inIViewer
+
+ get uid() { return "accessibleRelations"; },
+ get pane() { return this.mPane; },
+ get selection() { return this.mSelection; },
+
+ get subject() { return this.mSubject; },
+ set subject(aObject)
+ {
+ this.mView = new AccessibleRelationsView(aObject);
+ this.mTreeBox.view = this.mView;
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ },
+
+ initialize: function initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function destroy()
+ {
+ this.mTreeBox.view = null;
+ this.mTargetsTreeBox.view = null;
+ },
+
+ isCommandEnabled: function isCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditInspectInNewWindow":
+ return !!this.getSelectedTargetDOMNode();
+ }
+ return false;
+ },
+
+ getCommand: function getCommand(aCommand)
+ {
+ if (aCommand in window) {
+ return new window[aCommand]();
+ }
+ return null;
+ },
+
+ /////////////////////////
+ //// event dispatching
+
+ addObserver: function addObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+ removeObserver: function removeObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ /////////////////////////
+ //// utils
+
+ onItemSelected: function onItemSelected()
+ {
+ var idx = this.mTree.currentIndex;
+ var relation = this.mView.getRelationObject(idx);
+ this.mTargetsView = new AccessibleTargetsView(relation);
+ this.mTargetsTreeBox.view = this.mTargetsView;
+ },
+
+ getSelectedTargetDOMNode: function getSelectedTargetDOMNode()
+ {
+ return this.mTargetsView.getSelectedDOMNode();
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// AccessibleRelationsView
+
+function AccessibleRelationsView(aObject)
+{
+ this.mAccessible = aObject instanceof nsIAccessible ?
+ aObject : gAccService.getAccessibleFor(aObject);
+
+ this.mRelations = this.mAccessible.getRelations();
+}
+
+AccessibleRelationsView.prototype = new inBaseTreeView();
+
+AccessibleRelationsView.prototype.__defineGetter__("rowCount",
+function rowCount()
+{
+ return this.mRelations.length;
+});
+
+AccessibleRelationsView.prototype.getRelationObject =
+function getRelationObject(aRow)
+{
+ return this.mRelations.queryElementAt(aRow, nsIAccessibleRelation);
+}
+
+AccessibleRelationsView.prototype.getCellText =
+function getCellText(aRow, aCol)
+{
+ if (aCol.id == "olcRelationType") {
+ var relation = this.getRelationObject(aRow);
+ if (relation)
+ return gAccService.getStringRelationType(relation.relationType);
+ }
+
+ return "";
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// AccessibleTargetsView
+
+function AccessibleTargetsView(aRelation)
+{
+ this.mRelation = aRelation;
+ this.mTargets = this.mRelation.getTargets();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// AccessibleTargetsView. nsITreeView
+
+AccessibleTargetsView.prototype = new inBaseTreeView();
+
+AccessibleTargetsView.prototype.__defineGetter__("rowCount",
+function rowCount()
+{
+ return this.mTargets.length;
+});
+
+AccessibleTargetsView.prototype.getCellText =
+function getCellText(aRow, aCol)
+{
+ if (aCol.id == "olcRole") {
+ var accessible = this.getAccessible(aRow);
+ if (accessible) {
+ // 'finalRole' is replaced by 'role' property in Gecko 1.9.2.
+ var role = "finalRole" in accessible ?
+ accessible.finalRole : accessible.role;
+ return gAccService.getStringRole(role);
+ }
+ } else if (aCol.id == "olcNodeName") {
+ var node = this.getDOMNode(aRow);
+ if (node)
+ return node.nodeName;
+ }
+
+ return "";
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// AccessibleTargetsView. Utils
+
+AccessibleTargetsView.prototype.getAccessible =
+function getAccessible(aRow)
+{
+ return this.mTargets.queryElementAt(aRow, nsIAccessible);
+}
+
+AccessibleTargetsView.prototype.getDOMNode =
+function getDOMNode(aRow)
+{
+ var accessNode = this.mTargets.queryElementAt(aRow, nsIAccessNode);
+ return accessNode && accessNode.DOMNode;
+}
+
+AccessibleTargetsView.prototype.getSelectedDOMNode =
+ function getSelectedDOMNode()
+{
+ if (this.selection.count == 1) {
+ var rangeMinAndMax = {};
+ this.selection.getRangeAt(0, rangeMinAndMax, rangeMinAndMax);
+ return this.getDOMNode(rangeMinAndMax.value);
+ }
+ return null;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Transactions
+
+function cmdEditInspectInNewWindow() {
+ this.mObject = viewer.getSelectedTargetDOMNode();
+}
+
+cmdEditInspectInNewWindow.prototype = new cmdEditInspectInNewWindowBase();
diff --git a/inspector/content/viewers/accessibleRelations/accessibleRelations.xul b/inspector/content/viewers/accessibleRelations/accessibleRelations.xul
new file mode 100644
index 00000000..87d4e471
--- /dev/null
+++ b/inspector/content/viewers/accessibleRelations/accessibleRelations.xul
@@ -0,0 +1,86 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/accessibleTree/accessibleTree.js b/inspector/content/viewers/accessibleTree/accessibleTree.js
new file mode 100644
index 00000000..48095ec6
--- /dev/null
+++ b/inspector/content/viewers/accessibleTree/accessibleTree.js
@@ -0,0 +1,599 @@
+/* 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/. */
+
+/***************************************************************
+* AccessibleEventsViewer --------------------------------------------
+* The viewer for the accessible tree of a document.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/hooks.js
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/jsutil/events/ObserverManager.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/xul/FrameExchange.js
+****************************************************************/
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kObserverServiceCID = "@mozilla.org/observer-service;1";
+const kAccessibleRetrievalCID = "@mozilla.org/accessibleRetrieval;1";
+
+const nsIObserverService = Components.interfaces.nsIObserverService;
+
+const nsIAccessibleRetrieval = Components.interfaces.nsIAccessibleRetrieval;
+const nsIAccessibleEvent = Components.interfaces.nsIAccessibleEvent;
+const nsIAccessible = Components.interfaces.nsIAccessible;
+
+const nsIDOMNode = Components.interfaces.nsIDOMNode;
+
+/**
+ * QI nsIAccessNode interface if any, used for compatibility with Gecko versions
+ * prior to Gecko13.
+ */
+function QIAccessNode(aAccessible)
+{
+ return "nsIAccessNode" in Components.interfaces ?
+ XPCU.QI(aAccessible, Components.interfaces.nsIAccessNode) : aAccessible;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization
+
+window.addEventListener("load", AccessibleTreeViewer_initialize, false);
+
+function AccessibleTreeViewer_initialize()
+{
+ viewer = new AccessibleTreeViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// AccessibleEventsViewer
+
+function AccessibleTreeViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+
+ this.mTree = document.getElementById("olAccessibleTree");
+ this.mOlBox = this.mTree.treeBoxObject;
+}
+
+AccessibleTreeViewer.prototype =
+{
+ //Initialization
+
+ mSubject: null,
+ mPane: null,
+ mView: null,
+
+ // interface inIViewer
+
+ get uid() { return "accessibleTree"; },
+ get pane() { return this.mPane; },
+ get selection() { return this.mSelection; },
+
+ get subject() { return this.mSubject; },
+ set subject(aObject)
+ {
+ this.mView = new inAccTreeView(aObject);
+ this.mOlBox.view = this.mView;
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ this.mView.selection.select(0);
+ },
+
+ initialize: function initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function destroy()
+ {
+ this.mView.destroy();
+ this.mOlBox.view = null;
+ },
+
+ isCommandEnabled: function isCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditInspectInNewWindow":
+ return this.mTree.view.selection.count == 1;
+ }
+ return false;
+ },
+
+ getCommand: function getCommand(aCommand)
+ {
+ if (aCommand in window) {
+ return new window[aCommand]();
+ }
+ return null;
+ },
+
+ // event dispatching
+
+ addObserver: function addObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+ removeObserver: function removeObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ // UI commands
+ cmdEvalJS: function cmdEvalJS()
+ {
+ var sel = this.getSelectedAccessible();
+ if (sel) {
+ var win = openDialog("chrome://inspector/content/viewers/accessibleTree/evalJSDialog.xul",
+ "_blank", "chrome,resizable=yes", sel, this.mView);
+ }
+ },
+
+ // stuff
+
+ onItemSelected: function onItemSelected()
+ {
+ var idx = this.mTree.currentIndex;
+ this.mSelection = this.mView.getObject(idx);
+ this.mObsMan.dispatchEvent("selectionChange",
+ { selection: this.mSelection } );
+
+ if (this.mSelection) {
+ var node = this.mSelection.DOMNode;
+ if (node.nodeType == nsIDOMNode.ELEMENT_NODE) {
+ var flasher = this.mPane.panelset.flasher;
+ flasher.flashElementOnSelect(node);
+ }
+ }
+
+ viewer.pane.panelset.updateAllCommands();
+ },
+
+ getSelectedAccessible: function getSelectedAccessible()
+ {
+ if (this.mTree.view.selection.count == 1) {
+ var rangeMinAndMax = {};
+ this.mTree.view.selection.getRangeAt(0, rangeMinAndMax, rangeMinAndMax);
+ return this.mView.getAccessible(rangeMinAndMax.value);
+ }
+ return null;
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// inAccTreeView
+
+function inAccTreeView(aObject)
+{
+ this.mNodes = [];
+
+ this.mAccService = XPCU.getService(kAccessibleRetrievalCID,
+ nsIAccessibleRetrieval);
+
+ this.mAccessible = aObject instanceof nsIAccessible ?
+ aObject : this.mAccService.getAccessibleFor(aObject);
+
+ this.mObserverService = XPCU.getService(kObserverServiceCID,
+ nsIObserverService);
+
+ this.mObserverService.addObserver(this, "accessible-event", false);
+
+ var node = this.createNode(this.mAccessible);
+ this.mNodes.push(node);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// inAccTreeView. nsITreeView interface
+
+inAccTreeView.prototype = new inBaseTreeView();
+
+inAccTreeView.prototype.__defineGetter__("rowCount",
+function rowCount()
+{
+ return this.mNodes.length;
+});
+
+inAccTreeView.prototype.getCellText =
+function getCellText(aRow, aCol)
+{
+ var node = this.rowToNode(aRow);
+ if (!node)
+ return "";
+
+ var accessible = node.accessible;
+
+ if (aCol.id == "olcRole") {
+ // 'finalRole' is replaced by 'role' property in Gecko 1.9.2.
+ var role = "finalRole" in accessible ?
+ accessible.finalRole : accessible.role;
+ return this.mAccService.getStringRole(role);
+ }
+
+ if (aCol.id == "olcName")
+ return accessible.name;
+
+ if (aCol.id == "olcNodeName") {
+ var node = QIAccessNode(accessible).DOMNode;
+ return node ? node.nodeName : "";
+ }
+
+ return "";
+}
+
+inAccTreeView.prototype.isContainer =
+function isContainer(aRow)
+{
+ var node = this.rowToNode(aRow);
+ return node ? node.isContainer : false;
+}
+
+inAccTreeView.prototype.isContainerOpen =
+function isContainerOpen(aRow)
+{
+ var node = this.rowToNode(aRow);
+ return node ? node.isOpen : false;
+}
+
+inAccTreeView.prototype.isContainerEmpty =
+function isContainerEmpty(aRow)
+{
+ return !this.isContainer(aRow);
+}
+
+inAccTreeView.prototype.getLevel =
+function getLevel(aRow)
+{
+ var node = this.rowToNode(aRow);
+ return node ? node.level : 0;
+}
+
+inAccTreeView.prototype.getParentIndex =
+function getParentIndex(aRow)
+{
+ var node = this.rowToNode(aRow);
+ if (!node)
+ return -1;
+
+ var checkNode = null;
+ var i = aRow - 1;
+ do {
+ checkNode = this.rowToNode(i);
+ if (!checkNode)
+ return -1;
+
+ if (checkNode == node.parent)
+ return i;
+ --i;
+ } while (checkNode);
+
+ return -1;
+}
+
+inAccTreeView.prototype.hasNextSibling =
+function hasNextSibling(aRow, aAfterRow)
+{
+ var node = this.rowToNode(aRow);
+ return node && (node.next != null);
+}
+
+inAccTreeView.prototype.toggleOpenState =
+function toggleOpenState(aRow)
+{
+ var node = this.rowToNode(aRow);
+ if (!node)
+ return;
+
+ var oldCount = this.rowCount;
+ if (node.isOpen)
+ this.collapseNode(aRow);
+ else
+ this.expandNode(aRow);
+
+ this.mTree.invalidateRow(aRow);
+ this.mTree.rowCountChanged(aRow + 1, this.rowCount - oldCount);
+}
+
+inAccTreeView.prototype.getRowProperties =
+function getRowProperties(aRowIdx, aProperties)
+{
+ var node = this.rowToNode(aRowIdx);
+ if (node && node.highlighted) {
+ if (!aProperties)
+ return "highlight";
+
+ let atom = this.createAtom("highlight");
+ aProperties.AppendElement(atom);
+ }
+
+ return "";
+}
+
+inAccTreeView.prototype.getCellProperties =
+function getCellProperties(aRowIdx, aCol, aProperties)
+{
+ return this.getRowProperties(aRowIdx, aProperties);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// inAccTreeView. Public.
+
+/**
+ * Destroy the view.
+ */
+inAccTreeView.prototype.destroy =
+function inAccTreeView_destroy()
+{
+ this.mObserverService.removeObserver(this, "accessible-event");
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// inAccTreeView. Tree utils.
+
+/**
+ * Expands a tree node on the given row.
+ *
+ * @param aRow - row index.
+ */
+inAccTreeView.prototype.expandNode =
+function expandNode(aRow)
+{
+ var node = this.rowToNode(aRow);
+ if (!node)
+ return;
+
+ var kids = node.accessible.children;
+ var kidCount = kids.length;
+
+ var newNode = null;
+ var prevNode = null;
+
+ for (var i = 0; i < kidCount; ++i) {
+ var accessible = kids.queryElementAt(i, nsIAccessible);
+ newNode = this.createNode(accessible, node);
+ this.mNodes.splice(aRow + i + 1, 0, newNode);
+
+ if (prevNode)
+ prevNode.next = newNode;
+ newNode.previous = prevNode;
+ prevNode = newNode;
+ }
+
+ node.isOpen = true;
+}
+
+/**
+ * Collapse a tree node on the given row.
+ *
+ * @param aRow - row index.
+ */
+inAccTreeView.prototype.collapseNode =
+function collapseNode(aRow)
+{
+ var node = this.rowToNode(aRow);
+ if (!node)
+ return;
+
+ var row = this.getLastDescendantOf(node, aRow);
+ this.mNodes.splice(aRow + 1, row - aRow);
+
+ node.isOpen = false;
+}
+
+/**
+ * Expand the tree and highlight accessibles in the given subtree that comply
+ * to filter function.
+ *
+ * @param aRoot - the root accessible of subtree to search in
+ * @param aFilterFunc - a function that returns true if the passed accessible
+ complies to search criteria.
+ */
+inAccTreeView.prototype.search =
+function search(aRoot, aFilterFunc)
+{
+ QIAccessNode(aRoot);
+ if (aFilterFunc(aRoot)) {
+ let chain = [];
+ let parent = aRoot;
+ do {
+ chain.push(parent);
+ if (parent == aRoot.document)
+ break;
+
+ parent = parent.parent;
+ } while (parent);
+
+ let current = chain.pop();
+ for (let idx = 0; idx < this.mNodes.length; idx++) {
+ let node = this.mNodes[idx];
+ if (node.accessible == current) {
+ if (chain.length == 0) {
+ node.highlighted = true;
+ this.mTree.invalidateRow(idx);
+ } else {
+ if (!node.isOpen)
+ this.toggleOpenState(idx);
+
+ current = chain.pop();
+ }
+ }
+ }
+ }
+
+ var count = aRoot.childCount;
+ for (let idx = 0; idx < count; idx++) {
+ let child = aRoot.getChildAt(idx);
+ this.search(child, aFilterFunc);
+ }
+}
+
+/**
+ * Clear search results.
+ */
+inAccTreeView.prototype.clearSearch =
+function clearSearch()
+{
+ for (let idx = 0; idx < this.mNodes.length; idx++) {
+ if (this.mNodes[idx].highlighted) {
+ this.mNodes[idx].highlighted = false;
+ this.mTree.invalidateRow(idx);
+ }
+ }
+}
+
+
+/**
+ * Create a tree node.
+ *
+ * @param aAccessible - an accessible object associated with created tree node.
+ * @param aParent - parent tree node for the created tree node.
+ * @retrurn - tree node object for the given accesible.
+ */
+inAccTreeView.prototype.createNode =
+function createNode(aAccessible, aParent)
+{
+ var node = new inAccTreeViewNode(aAccessible);
+ node.level = aParent ? aParent.level + 1 : 0;
+ node.parent = aParent;
+ node.isContainer = aAccessible.children.length > 0;
+
+ return node;
+}
+
+/**
+ * Return row index of the last node that is a descendant of the given node.
+ * If there is no required node then return the given row.
+ *
+ * @param aNode - tree node for that last descedant is searched.
+ * @param aRow - row index of the given tree node.
+ */
+inAccTreeView.prototype.getLastDescendantOf =
+function getLastDescendantOf(aNode, aRow)
+{
+ var rowCount = this.rowCount;
+
+ var row = aRow + 1;
+ for (; row < rowCount; ++row) {
+ if (this.mNodes[row].level <= aNode.level)
+ return row - 1;
+ }
+
+ return rowCount - 1;
+}
+
+/**
+ * Return a tree node by the given row.
+ *
+ * @param aRow - row index.
+ */
+inAccTreeView.prototype.rowToNode =
+function rowToNode(aRow)
+{
+ if (aRow < 0 || aRow >= this.rowCount)
+ return null;
+
+ return this.mNodes[aRow];
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// inAccTreeView. Accessibility utils.
+
+/**
+ * Return DOM node for an accessible or accessible if there is no associated
+ * DOM node by the tree node pointed by the given row index.
+ *
+ * @param aRow - row index.
+ */
+inAccTreeView.prototype.getObject =
+function getObject(aRow)
+{
+ var node = this.mNodes[aRow];
+ return node && QIAccessNode(node.accessible);
+}
+
+/**
+ * Return accessible of the tree node pointed by the given
+ * row index.
+ *
+ * @param aRow - the row index to get the accessible from.
+ * @returns the accessible for the given index.
+ */
+inAccTreeView.prototype.getAccessible =
+function getAccessible(aRow)
+{
+ var node = this.mNodes[aRow];
+ if (!node)
+ return null;
+
+ return node.accessible;
+}
+
+inAccTreeView.prototype.observe =
+function inAccTreeView_observe(aSubject, aTopic, aData)
+{
+ let event = XPCU.QI(aSubject, nsIAccessibleEvent);
+
+ // Update the children if they were changed.
+ if (event.eventType != nsIAccessibleEvent.EVENT_REORDER)
+ return;
+
+ var accessible = event.accessible;
+ if (!accessible)
+ return;
+
+ // Ignore the event if its target is from anther document.
+ var parentAccessible = accessible;
+ while (parentAccessible != this.mAccessible) {
+ parentAccessible = parentAccessible.parent;
+ if (!parentAccessible) {
+ return;
+ }
+ }
+
+ for (let idx = 0; idx < this.mNodes.length; idx++) {
+ let node = this.mNodes[idx];
+ if (node.accessible == accessible) {
+ if (node.isOpen) {
+ // Toggle open state twice to update the children.
+ this.toggleOpenState(idx);
+ this.toggleOpenState(idx);
+ }
+ break;
+ }
+ }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// inAccTreeViewNode
+
+function inAccTreeViewNode(aAccessible)
+{
+ this.accessible = aAccessible;
+
+ this.parent = null;
+ this.next = null;
+ this.previous = null;
+
+ this.level = 0;
+ this.isOpen = false;
+ this.isContainer = false;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Transactions
+
+function cmdEditInspectInNewWindow()
+{
+ this.mObject = viewer.getSelectedAccessible();
+}
+
+cmdEditInspectInNewWindow.prototype = new cmdEditInspectInNewWindowBase();
diff --git a/inspector/content/viewers/accessibleTree/accessibleTree.xul b/inspector/content/viewers/accessibleTree/accessibleTree.xul
new file mode 100644
index 00000000..c79948ae
--- /dev/null
+++ b/inspector/content/viewers/accessibleTree/accessibleTree.xul
@@ -0,0 +1,72 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/accessibleTree/evalJSDialog.js b/inspector/content/viewers/accessibleTree/evalJSDialog.js
new file mode 100644
index 00000000..d8b03947
--- /dev/null
+++ b/inspector/content/viewers/accessibleTree/evalJSDialog.js
@@ -0,0 +1,89 @@
+/* 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 gAcc = window.arguments[0];
+var gTreeView = window.arguments[1];
+
+var gInputArea = null;
+var gOutputArea = null;
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+function load()
+{
+ gInputArea = document.getElementById("JSInputArea");
+ gOutputArea = document.getElementById("JSOutputArea");
+}
+
+function closeDialog()
+{
+ gTreeView.clearSearch();
+}
+
+function execute()
+{
+ if (!gAcc)
+ return;
+
+ for (var idx = 0; idx < gAccInterfaces.length; idx++) {
+ if (gAcc instanceof gAccInterfaces[idx])
+ gAcc.QueryInterface(gAccInterfaces[idx]);
+ }
+
+ gOutputArea.value = "";
+
+ var expr = gInputArea.value;
+ try {
+ var f = Function("accessible", "tree", expr);
+ var result = f(gAcc, gTreeView);
+ } catch (ex) {
+ output(ex);
+ }
+}
+
+var gAccInterfaces =
+[
+ Ci.nsIAccessible,
+ Ci.nsIAccessibleDocument,
+ Ci.nsIAccessibleEditableText,
+ Ci.nsIAccessibleHyperLink,
+ Ci.nsIAccessibleHyperText,
+ Ci.nsIAccessibleImage,
+ Ci.nsIAccessibleSelectable,
+ Ci.nsIAccessibleTable,
+ Ci.nsIAccessibleTableCell,
+ Ci.nsIAccessibleText,
+ Ci.nsIAccessibleValue
+];
+
+// Used for compatibility with Gecko versions prior to Gecko13.
+if ("nsIAccessNode" in Ci)
+ gAccInterfaces.push(Ci.nsIAccessNode);
+
+function output(aValue)
+{
+ gOutputArea.value += aValue;
+}
+
+function outputTextAttrs(aAccessible, aOffset)
+{
+ if (aAccessible instanceof Ci.nsIAccessibleText) {
+ var startOffsetObj = {}, endOffsetObj = {};
+ var attrs = aAccessible.getTextAttributes(false, aOffset,
+ startOffsetObj, endOffsetObj);
+ if (attrs) {
+ var str = "Start offset: " + startOffsetObj.value;
+ str += ", end offset: " + endOffsetObj.value + "\nText attributes:\n";
+
+ var enumerator = attrs.enumerate();
+ while (enumerator.hasMoreElements()) {
+ var prop = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ str += "\t" + prop.key + ": " + prop.value + ";\n";
+ }
+
+ output(str);
+ }
+ }
+}
diff --git a/inspector/content/viewers/accessibleTree/evalJSDialog.xul b/inspector/content/viewers/accessibleTree/evalJSDialog.xul
new file mode 100644
index 00000000..7657b885
--- /dev/null
+++ b/inspector/content/viewers/accessibleTree/evalJSDialog.xul
@@ -0,0 +1,43 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+ &txtInputArea.label;
+
+
+
+
+
+
diff --git a/inspector/content/viewers/boxModel/boxModel.js b/inspector/content/viewers/boxModel/boxModel.js
new file mode 100644
index 00000000..3052cc02
--- /dev/null
+++ b/inspector/content/viewers/boxModel/boxModel.js
@@ -0,0 +1,244 @@
+/* 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/. */
+
+/***************************************************************
+* BoxModelViewer --------------------------------------------
+* The viewer for the boxModel and visual appearance of an element.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var viewer;
+
+//////////// global constants ////////////////////
+
+const kIMPORT_RULE = Components.interfaces.nsIDOMCSSRule.IMPORT_RULE;
+
+//////////////////////////////////////////////////
+
+window.addEventListener("load", BoxModelViewer_initialize, false);
+
+function BoxModelViewer_initialize()
+{
+ viewer = new BoxModelViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+////////////////////////////////////////////////////////////////////////////
+//// class BoxModelViewer
+
+function BoxModelViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+}
+
+BoxModelViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPane: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// interface inIViewer
+
+ get uid() { return "boxModel" },
+ get pane() { return this.mPane },
+
+ get subject() { return this.mSubject },
+ set subject(aObject)
+ {
+ this.mSubject = aObject instanceof Components.interfaces.nsIDOMNode ?
+ aObject : aObject.DOMNode;
+ this.showStats();
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+ },
+
+ initialize: function(aPane)
+ {
+ this.initGroups();
+
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ groupPosition: {},
+ groupDimension: {},
+ groupMargin: {},
+ groupBorder: {},
+ groupPadding: {},
+
+ initGroups: function()
+ {
+ this.groupPosition.x = document.getElementById("positionXValue");
+ this.groupPosition.y = document.getElementById("positionYValue");
+ this.groupPosition.screenX =
+ document.getElementById("positionScreenXValue");
+ this.groupPosition.screenY =
+ document.getElementById("positionScreenYValue");
+
+ this.groupDimension.width =
+ document.getElementById("dimensionWidthValue");
+ this.groupDimension.height =
+ document.getElementById("dimensionHeightValue");
+
+ this.groupMargin.top = document.getElementById("marginTopValue");
+ this.groupMargin.right = document.getElementById("marginRightValue");
+ this.groupMargin.bottom = document.getElementById("marginBottomValue");
+ this.groupMargin.left = document.getElementById("marginLeftValue");
+
+ this.groupBorder.top = document.getElementById("borderTopValue");
+ this.groupBorder.right = document.getElementById("borderRightValue");
+ this.groupBorder.bottom = document.getElementById("borderBottomValue");
+ this.groupBorder.left = document.getElementById("borderLeftValue");
+
+ this.groupPadding.top = document.getElementById("paddingTopValue");
+ this.groupPadding.right = document.getElementById("paddingRightValue");
+ this.groupPadding.bottom = document.getElementById("paddingBottomValue");
+ this.groupPadding.left = document.getElementById("paddingLeftValue");
+ },
+
+ destroy: function()
+ {
+ },
+
+ isCommandEnabled: function(aCommand)
+ {
+ return false;
+ },
+
+ getCommand: function(aCommand)
+ {
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// event dispatching
+
+ addObserver: function(aEvent, aObserver) { this.mObsMan.addObserver(aEvent, aObserver); },
+ removeObserver: function(aEvent, aObserver) { this.mObsMan.removeObserver(aEvent, aObserver); },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// statistical updates
+
+ showStats: function()
+ {
+ this.showPositionStats();
+ this.showDimensionStats();
+ this.showMarginStats();
+ this.showBorderStats();
+ this.showPaddingStats();
+ },
+
+ showStatistic: function(aElement, aSize)
+ {
+ if (aSize == null) {
+ aSize = "";
+ }
+ var str = aSize.toString();
+ aElement.setAttribute("value", str);
+
+ var nonzero = aSize != 0 && str.indexOf("0px");
+ aElement.setAttribute("class", nonzero ? "plain nonzero" : "plain");
+
+ aElement.setAttribute("size", str.length + 1);
+ },
+
+ showPositionStats: function()
+ {
+ var group = this.groupPosition;
+ if ("boxObject" in this.mSubject) { // xul elements
+ var bx = this.mSubject.boxObject;
+ this.showStatistic(group.x, bx.x);
+ this.showStatistic(group.y, bx.y);
+ this.showStatistic(group.screenX, bx.screenX);
+ this.showStatistic(group.screenY, bx.screenY);
+ group.screenX.parentNode.hidden = false;
+ group.screenY.parentNode.hidden = false;
+ } else { // html elements
+ this.showStatistic(group.x, this.mSubject.offsetLeft);
+ this.showStatistic(group.y, this.mSubject.offsetTop);
+ group.screenX.parentNode.hidden = true;
+ group.screenY.parentNode.hidden = true;
+ }
+ },
+
+ showDimensionStats: function()
+ {
+ var group = this.groupDimension;
+ if ("boxObject" in this.mSubject) { // xul elements
+ var bx = this.mSubject.boxObject;
+ this.showStatistic(group.width, bx.width);
+ this.showStatistic(group.height, bx.height);
+ } else { // html elements
+ this.showStatistic(group.width, this.mSubject.offsetWidth);
+ this.showStatistic(group.height, this.mSubject.offsetHeight);
+ }
+ },
+
+ getSubjectComputedStyle: function()
+ {
+ var view = this.mSubject.ownerDocument.defaultView;
+ return view.getComputedStyle(this.mSubject, "");
+ },
+
+ showMarginStats: function()
+ {
+ var style = this.getSubjectComputedStyle();
+ var data = [this.readMarginStyle(style, "top"), this.readMarginStyle(style, "right"),
+ this.readMarginStyle(style, "bottom"), this.readMarginStyle(style, "left")];
+ this.showSideStats(this.groupMargin, data);
+ },
+
+ showBorderStats: function()
+ {
+ var style = this.getSubjectComputedStyle();
+ var data = [this.readBorderStyle(style, "top"), this.readBorderStyle(style, "right"),
+ this.readBorderStyle(style, "bottom"), this.readBorderStyle(style, "left")];
+ this.showSideStats(this.groupBorder, data);
+ },
+
+ showPaddingStats: function()
+ {
+ var style = this.getSubjectComputedStyle();
+ var data = [this.readPaddingStyle(style, "top"), this.readPaddingStyle(style, "right"),
+ this.readPaddingStyle(style, "bottom"), this.readPaddingStyle(style, "left")];
+ this.showSideStats(this.groupPadding, data);
+ },
+
+ showSideStats: function(aGroup, aData)
+ {
+ this.showStatistic(aGroup.top, aData[0]);
+ this.showStatistic(aGroup.right, aData[1]);
+ this.showStatistic(aGroup.bottom, aData[2]);
+ this.showStatistic(aGroup.left, aData[3]);
+ },
+
+ readMarginStyle: function(aStyle, aSide)
+ {
+ return aStyle.getPropertyCSSValue("margin-"+aSide).cssText;
+ },
+
+ readPaddingStyle: function(aStyle, aSide)
+ {
+ return aStyle.getPropertyCSSValue("padding-"+aSide).cssText;
+ },
+
+ readBorderStyle: function(aStyle, aSide)
+ {
+ var style = aStyle.getPropertyCSSValue("border-"+aSide+"-style").cssText;
+ if (!style || !style.length) {
+ return "none";
+ } else {
+ return aStyle.getPropertyCSSValue("border-"+aSide+"-width").cssText + " " +
+ style + " " +
+ aStyle.getPropertyCSSValue("border-"+aSide+"-color").cssText;
+ }
+ }
+};
diff --git a/inspector/content/viewers/boxModel/boxModel.xul b/inspector/content/viewers/boxModel/boxModel.xul
new file mode 100644
index 00000000..72d6e81e
--- /dev/null
+++ b/inspector/content/viewers/boxModel/boxModel.xul
@@ -0,0 +1,176 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &boxPosition.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &boxDimension.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &boxMargin.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &boxBorder.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &boxPadding.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/computedStyle/computedStyle.js b/inspector/content/viewers/computedStyle/computedStyle.js
new file mode 100644
index 00000000..72c2377e
--- /dev/null
+++ b/inspector/content/viewers/computedStyle/computedStyle.js
@@ -0,0 +1,197 @@
+/* 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/. */
+
+/*****************************************************************************
+* ComputedStyleViewer --------------------------------------------------------
+* The viewer for the computed CSS styles on a DOM element.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/events/ObserverManager.js
+* chrome://inspector/content/commands/baseCommands.js
+* chrome://inspector/content/system/clipboardFlavors.js
+* chrome://inspector/content/xul/inBaseTreeView.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", ComputedStyleViewer_initialize, false);
+
+function ComputedStyleViewer_initialize()
+{
+ viewer = new ComputedStyleViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// class ComputedStyleViewer
+
+function ComputedStyleViewer()
+{
+ this.mObsMan = new ObserverManager(this);
+ this.mURL = window.location;
+
+ this.mTree = document.getElementById("olStyles");
+}
+
+//XXX Don't use anonymous functions
+ComputedStyleViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPane: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// interface inIViewer
+
+ get uid()
+ {
+ return "computedStyle";
+ },
+
+ get pane()
+ {
+ return this.mPane;
+ },
+
+ get subject()
+ {
+ return this.mSubject;
+ },
+
+ set subject(aObject)
+ {
+ this.mSubject = aObject instanceof Components.interfaces.nsIDOMNode ?
+ aObject : aObject.DOMNode;
+
+ var bo = this.mTree.treeBoxObject;
+ var firstVisibleRow = -1;
+ var selectedIndices;
+ var currentIndex;
+ if (this.mTreeView) {
+ firstVisibleRow = bo.getFirstVisibleRow();
+ selectedIndices = this.mTreeView.getSelectedIndices();
+ currentIndex = this.mTreeView.selection.currentIndex;
+ }
+
+ this.mTreeView = new ComputedStyleView(this.mSubject);
+ this.mTree.view = this.mTreeView;
+
+ if (firstVisibleRow >= 0) {
+ bo.beginUpdateBatch();
+ try {
+ bo.scrollToRow(firstVisibleRow);
+ let selection = this.mTreeView.selection;
+ for (let i = 0, n = selectedIndices.length; i < n; ++i) {
+ selection.toggleSelect(selectedIndices[i]);
+ }
+ selection.currentIndex = currentIndex;
+ }
+ catch (ex) {
+ Components.utils.reportError(ex);
+ }
+ bo.endUpdateBatch();
+ }
+
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+ },
+
+ initialize: function CSVr_Initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function CSVr_Destroy()
+ {
+ // We need to remove the view at this time or else it will attempt to
+ // re-paint while the document is being deconstructed, resulting in some
+ // nasty XPConnect assertions
+ this.mTree.view = null;
+ },
+
+ isCommandEnabled: function CSVr_IsCommandEnabled(aCommand)
+ {
+ if (aCommand == "cmdEditCopy") {
+ return this.mTree.view.selection.count > 0;
+ }
+ return false;
+ },
+
+ getCommand: function CSVr_GetCommand(aCommand)
+ {
+ if (aCommand == "cmdEditCopy") {
+ return new cmdEditCopy(this.mTreeView.getSelectedRowObjects());
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// event dispatching
+
+ addObserver: function CSVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function CSVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Miscellaneous
+
+ onItemSelected: function CSVr_OnItemSelected()
+ {
+ // This will (eventually) call isCommandEnabled on Copy
+ viewer.pane.panelset.updateAllCommands();
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+//// ComputedStyleView
+
+function ComputedStyleView(aObject)
+{
+ var view = aObject.ownerDocument.defaultView;
+ this.mStyleList = view.getComputedStyle(aObject, "");
+ this.mRowCount = this.mStyleList.length;
+}
+
+ComputedStyleView.prototype = new inBaseTreeView();
+
+ComputedStyleView.prototype.getCellText = function CSV_GetCellText(aRow, aCol)
+{
+ var prop = this.mStyleList.item(aRow);
+ if (aCol.id == "olcStyleName") {
+ return prop;
+ }
+ else if (aCol.id == "olcStyleValue") {
+ return this.mStyleList.getPropertyValue(prop);
+ }
+
+ return null;
+}
+
+/**
+ * Returns a CSSProperty for the row in the tree corresponding to the passed
+ * index.
+ * @param aIndex
+ * index of the row in the tree
+ * @return a CSSProperty
+ */
+ComputedStyleView.prototype.getRowObjectFromIndex =
+ function CSV_GetRowObjectFromIndex(aIndex)
+{
+ var prop = this.mStyleList.item(aIndex);
+ return new CSSProperty(prop, this.mStyleList.getPropertyValue(prop));
+}
diff --git a/inspector/content/viewers/computedStyle/computedStyle.xul b/inspector/content/viewers/computedStyle/computedStyle.xul
new file mode 100644
index 00000000..e9e0034e
--- /dev/null
+++ b/inspector/content/viewers/computedStyle/computedStyle.xul
@@ -0,0 +1,51 @@
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/FindDialog.js b/inspector/content/viewers/dom/FindDialog.js
new file mode 100644
index 00000000..1396e4ec
--- /dev/null
+++ b/inspector/content/viewers/dom/FindDialog.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/***************************************************************
+* FindDialog ---------------------------------------------------
+* Controls the dialog box used for searching the DOM.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/rdf/RDFU.js
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var dialog;
+
+//////////// global constants ////////////////////
+
+//////////////////////////////////////////////////
+
+window.addEventListener("load", FindDialog_initialize, false);
+
+function FindDialog_initialize()
+{
+ dialog = new FindDialog();
+}
+
+////////////////////////////////////////////////////////////////////////////
+//// class FindDialog
+
+function FindDialog()
+{
+ this.setDirection(window.arguments[1] ? window.arguments[1] : "down");
+ if (window.arguments[2]) {
+ this.setValue(window.arguments[2][0], window.arguments[2][1])
+ }
+ this.toggleType(window.arguments[0] ? window.arguments[0] : "id");
+ this.mOpener = window.opener.viewer;
+
+ var txf = document.getElementById("tfText1");
+ txf.select();
+ txf.focus();
+}
+
+FindDialog.prototype =
+{
+ mType: null,
+
+ ////////// Properties
+
+
+ ////////// Methods
+
+ doFind: function()
+ {
+ var el = document.getElementById("tfText1");
+ var dir = document.getElementById("rgDirection").value;
+ if (this.mType == "id") {
+ this.mOpener.startFind("id", dir, el.value);
+ } else if (this.mType == "tag") {
+ this.mOpener.startFind("tag", dir, el.value);
+ } else if (this.mType == "attr") {
+ var el2 = document.getElementById("tfText2");
+ this.mOpener.startFind("attr", dir, el.value, el2.value);
+ }
+ },
+
+ toggleType: function(aType)
+ {
+ this.mType = aType;
+
+ if (aType == "id") {
+ this.showDirection(false);
+ this.setLabel1(0);
+ this.showRow2(false);
+ } else if (aType == "tag") {
+ this.showDirection(true);
+ this.setLabel1(1);
+ this.showRow2(false);
+ } else if (aType == "attr") {
+ this.showDirection(true);
+ this.setLabel1(2);
+ this.showRow2(true);
+ }
+
+ var rd = document.getElementById("rdType_"+aType.toLowerCase());
+ if (rd) {
+ var rg = document.getElementById("rgType");
+ rg.selectedItem = rd;
+ }
+
+ },
+
+ setLabel1: function(aIndex)
+ {
+ var deck = document.getElementById("rwRow1Text");
+
+ // We want to add a control attribute to the selected label so that
+ // accessibility aids can get the textbox's label.
+ // Remove the control attribute from the old panel.
+ deck.selectedPanel.removeAttribute("control");
+
+ deck.selectedIndex = aIndex;
+ // Add the control attribute to the new panel.
+ deck.selectedPanel.control = "tfText1";
+ },
+
+ showRow2: function(aTruth)
+ {
+ var row = document.getElementById("rwRow2");
+ row.setAttribute("hide", !aTruth);
+ },
+
+ setDirection: function(aMode)
+ {
+ var rd = document.getElementById("rdDir_"+aMode.toLowerCase());
+ if (rd) {
+ var rg = document.getElementById("rgDirection");
+ rg.selectedItem = rd;
+ }
+ },
+
+ setValue: function(aValue1, aValue2)
+ {
+ var txf;
+ if (aValue1) {
+ txf = document.getElementById("tfText1");
+ txf.value = aValue1;
+ }
+ if (aValue2) {
+ txf = document.getElementById("tfText2");
+ txf.value = aValue2;
+ }
+ },
+
+ showDirection: function(aTruth)
+ {
+ document.getElementById("rdDir_up").disabled = !aTruth;
+ document.getElementById("rdDir_down").disabled = !aTruth;
+ }
+
+};
+
diff --git a/inspector/content/viewers/dom/columnsDialog.js b/inspector/content/viewers/dom/columnsDialog.js
new file mode 100644
index 00000000..651ca130
--- /dev/null
+++ b/inspector/content/viewers/dom/columnsDialog.js
@@ -0,0 +1,219 @@
+/* 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/. */
+
+/***************************************************************
+* ColumnsDialog --------------------------------------------
+* Dialog box for editing the columns in the DOM Viewer tree.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var dialog;
+
+var gColumnTitles = [
+ "nodeName",
+ "nodeValue",
+ "nodeType",
+ "prefix",
+ "localName",
+ "namespaceURI"
+];
+
+//////////// global constants ////////////////////
+
+//////////////////////////////////////////////////
+
+window.addEventListener("load", ColumnsDialog_initialize, false);
+window.addEventListener("unload", ColumnsDialog_destroy, false);
+
+function ColumnsDialog_initialize()
+{
+ dialog = new ColumnsDialog();
+ dialog.initialize();
+}
+
+function ColumnsDialog_destroy()
+{
+ dialog.destroy();
+}
+
+////////////////////////////////////////////////////////////////////////////
+//// class ColumnsDialog
+
+function ColumnsDialog()
+{
+ this.mViewer = window.arguments[0];
+ this.mColBoxHash = {};
+}
+
+ColumnsDialog.prototype =
+{
+ mBox: null,
+ mDraggingBox: null,
+
+ get box() { return this.mBox },
+
+ initialize: function()
+ {
+ // bug 56270 - dragSession.sourceDocument is null --
+ // causes me to code this very temporary, very nasty hack
+ // to make sure I get notified when a column is dropped
+ opener.viewer.mDOMTree.onClientDrop = ColumnsDialogDropOut;
+
+ this.buildContents();
+
+ // notify the dom viewer that we've opened
+ this.mViewer.onColumnsDialogReady(this);
+ },
+
+ destroy: function()
+ {
+ // notify the dom viewer that we're going away
+ this.mViewer.onColumnsDialogClose(this);
+ },
+
+ buildContents: function()
+ {
+ var box = document.getElementById("bxColumns");
+ this.mBox = box;
+
+ // create the special attribute box
+ var item = this.createAttrItem();
+ box.appendChild(item);
+
+ // add all boxes except those that are already
+ // in the viewer
+ for (var i = 0; i < gColumnTitles.length; ++i)
+ if (!this.mViewer.hasColumn(gColumnTitles[i])) {
+ this.addItem(gColumnTitles[i]);
+ }
+ },
+
+ addItem: function(aValue)
+ {
+ if (!aValue || aValue.charAt(0) == "@")
+ return null;
+
+ item = this.createItem(aValue);
+ this.mColBoxHash[aValue] = item;
+ if (item) {
+ this.mBox.appendChild(item);
+ window.sizeToContent();
+ }
+ },
+
+ removeItem: function(aValue)
+ {
+ if (!aValue || aValue.charAt(0) == "@")
+ return null;
+
+ var colBox = this.mColBoxHash[aValue];
+ if (!colBox._isAttrCol) {
+ this.mBox.removeChild(colBox);
+ window.sizeToContent();
+ }
+ },
+
+ createItem: function(aValue)
+ {
+ var text = document.createElementNS(kXULNSURI, "text");
+ text._ColValue = aValue;
+ text._isColBox = true;
+ text._isAttrCol = false;
+ text.setAttribute("class", "column-selector");
+ text.setAttribute("value", aValue);
+
+ return text;
+ },
+
+ createAttrItem: function(aValue)
+ {
+ var box = document.createElementNS(kXULNSURI, "box");
+ box._isColBox = true;
+ box._isAttrCol = true;
+ box.setAttribute("class", "column-selector");
+ box.setAttribute("align", "center");
+
+ var text = document.createElementNS(kXULNSURI, "text");
+ text.setAttribute("value", "Attr");
+ box.appendChild(text);
+
+ var txf = document.createElementNS(kXULNSURI, "textbox");
+ txf.setAttribute("class", "attr-column-selector");
+ txf.setAttribute("flex", 1);
+ box.appendChild(txf);
+
+ return box;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Drag and Drop
+
+ onDragStart: function(aEvent)
+ {
+ var box = this.getBoxTarget(aEvent.target);
+ this.setDraggingBox(box);
+ if (!box) return false;
+
+ var column = this.getColumnValue(box);
+ if (!column) return false;
+
+ DNDUtils.invokeSession(aEvent.target, ["TreeBuilder/column-add"], [column]);
+
+ return false;
+ },
+
+ onDropOut: function()
+ {
+ var box = document.getElementById("bxColumns");
+ var value = this.mDraggingBox._ColValue;
+ this.setDraggingBox(null);
+ this.removeItem(value);
+ },
+
+ onDropIn: function(aEvent)
+ {
+ var data = DNDUtils.getData("TreeBuilder/column-remove", 0);
+ var string = XPCU.QI(data, "nsISupportsString");
+ this.addItem(string.data);
+ },
+
+ setDraggingBox: function(aBox)
+ {
+ if (this.mDraggingBox) {
+ this.mDraggingBox.removeAttribute("col-dragging");
+ }
+
+ this.mDraggingBox = aBox;
+
+ if (aBox)
+ aBox.setAttribute("col-dragging", "true");
+ },
+
+ getBoxTarget: function(aNode)
+ {
+ var node = aNode;
+ while (node && !node._isColBox)
+ node = node.parentNode;
+ return node;
+ },
+
+ getColumnValue: function(aColBox)
+ {
+ if (aColBox._isAttrCol) {
+ var txf = aColBox.getElementsByTagName("textbox")[0];
+ return "@" + txf.value;
+ } else {
+ return aColBox._ColValue;
+ }
+ }
+
+};
+
+function ColumnsDialogDropOut()
+{
+ dialog.onDropOut();
+}
diff --git a/inspector/content/viewers/dom/columnsDialog.xul b/inspector/content/viewers/dom/columnsDialog.xul
new file mode 100644
index 00000000..bed49127
--- /dev/null
+++ b/inspector/content/viewers/dom/columnsDialog.xul
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/commandOverlay.xul b/inspector/content/viewers/dom/commandOverlay.xul
new file mode 100644
index 00000000..eadfae7b
--- /dev/null
+++ b/inspector/content/viewers/dom/commandOverlay.xul
@@ -0,0 +1,44 @@
+
+
+
+
+ %dtd1;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/dom.js b/inspector/content/viewers/dom/dom.js
new file mode 100644
index 00000000..d31f8876
--- /dev/null
+++ b/inspector/content/viewers/dom/dom.js
@@ -0,0 +1,2272 @@
+/* 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/. */
+
+/*****************************************************************************
+* DOMViewer ------------------------------------------------------------------
+* Views all nodes within a document.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://global/content/XPCNativeWrapper.js
+* chrome://inspector/content/hooks.js
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/jsutil/events/ObserverManager.js
+* chrome://inspector/content/jsutil/system/PrefUtils.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/xul/FrameExchange.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kDOMViewClassID = "@mozilla.org/inspector/dom-view;1";
+const kPromptServiceClassID = "@mozilla.org/embedcomp/prompt-service;1";
+const kAccessibleRetrievalClassID = "@mozilla.org/accessibleRetrieval;1";
+const kDOMUtilsClassID = "@mozilla.org/inspector/dom-utils;1";
+const kDeepTreeWalkerClassID = "@mozilla.org/inspector/deep-tree-walker;1";
+const nsIDOMNode = Components.interfaces.nsIDOMNode;
+const nsIDOMElement = Components.interfaces.nsIDOMElement;
+const nsIDOMDocument = Components.interfaces.nsIDOMDocument;
+const nsIDOMCharacterData = Components.interfaces.nsIDOMCharacterData;
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", DOMViewer_initialize, false);
+window.addEventListener("unload", DOMViewer_destroy, false);
+
+function DOMViewer_initialize()
+{
+ viewer = new DOMViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+function DOMViewer_destroy()
+{
+ PrefUtils.removeObserver("inspector", PrefChangeObserver);
+ viewer.removeClickListeners();
+ viewer = null;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// class DOMViewer
+
+function DOMViewer() // implements inIViewer
+{
+ this.mObsMan = new ObserverManager(this);
+
+ this.mDOMUtils = XPCU.getService(kDOMUtilsClassID, "inIDOMUtils");
+
+ this.mDOMTree = document.getElementById("trDOMTree");
+ this.mDOMTreeBody = document.getElementById("trDOMTreeBody");
+
+ // prepare and attach the DOM DataSource
+ this.mDOMView = XPCU.createInstance(kDOMViewClassID, "inIDOMView");
+ this.mDOMView.showSubDocuments = true;
+ // hide attribute nodes
+ this.mDOMView.whatToShow &= ~(NodeFilter.SHOW_ATTRIBUTE);
+ this.mDOMTree.treeBoxObject.view = this.mDOMView;
+
+ PrefUtils.addObserver("inspector", PrefChangeObserver);
+}
+
+DOMViewer.prototype =
+{
+ mSubject: null,
+ mDOMView: null,
+ // searching stuff
+ mFindResult: null,
+ mColumns: null,
+ mFindDir: null,
+ mFindParams: null,
+ mFindType: null,
+ mFindWalker: null,
+ mSelecting: false,
+
+ mSelectionBatchNest: 0,
+ mPendingSelection: null,
+
+ /**
+ * Prevent the viewer from dispatching selectionChange events while batches
+ * are underway. The last change in selection while disabled is remembered,
+ * however, and when all batches have ended, an event is dispatched for it.
+ * To prevent this pending selectionChange from being dispatched, set
+ * mPendingSelection to null before calling endSelectionBatch.
+ *
+ * Nested batches are permitted.
+ */
+ beginSelectionBatch: function DVr_BeginSelectionBatch()
+ {
+ ++this.mSelectionBatchNest;
+ },
+
+ endSelectionBatch: function DVr_EndSelectionBatch()
+ {
+ --this.mSelectionBatchNest;
+ if (this.mSelectionBatchNest < 0) {
+ Components.utils.reportError("Attempted to end a selection batch " +
+ "that doesn't exist");
+ }
+ else if (!this.mSelectionBatchNest && this.mPendingSelection) {
+ this.changeSelection(this.mPendingSelection);
+ this.mPendingSelection = null;
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// interface inIViewer
+
+ //// attributes
+
+ get uid() {
+ return "dom"
+ },
+
+ get pane() {
+ return this.mPanel
+ },
+
+ get editable() {
+ return true;
+ },
+
+ get selection() {
+ return this.mSelection
+ },
+
+ get subject() {
+ return this.mSubject
+ },
+
+ set subject(aObject) {
+ this.mSubject = aObject;
+ this.mDOMView.rootNode = aObject;
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ this.setInitialSelection(aObject);
+ },
+
+ //// methods
+
+ /**
+ * Properly sets up the DOM Viewer
+ *
+ * @param aPane
+ * The panel this references.
+ */
+ initialize: function DVr_initialize(aPane)
+ {
+ this.mPanel = aPane;
+
+ this.setAnonContent(PrefUtils.getPref("inspector.dom.showAnon"));
+ this.setProcessingInstructions(
+ PrefUtils.getPref("inspector.dom.showProcessingInstructions")
+ );
+ this.setAccessibleNodes(
+ PrefUtils.getPref("inspector.dom.showAccessibleNodes")
+ );
+ this.setWhitespaceNodes(
+ PrefUtils.getPref("inspector.dom.showWhitespaceNodes")
+ );
+
+ this.pane.panelset.addTransactionListener(this);
+
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function DVr_Destroy()
+ {
+ this.mDOMTree.treeBoxObject.view = null;
+ this.pane.panelset.removeTransactionListener(this);
+ },
+
+ isCommandEnabled: function DVr_IsCommandEnabled(aCommand)
+ {
+ // NB: Don't confuse selected nodes and currentNode. currentNode derives
+ // from currentIndex. Think of currentIndex like the position of the
+ // cursor in a textbox. Commands like Copy need text to be selected,
+ // while Paste and Insert need no selection, only that the cursor be in
+ // the area we're interested in.
+ // XXX Bring all commands around to handling multiple selection.
+ var clipboardNode = null;
+ var currentNode = null;
+ var parentNode = null;
+ var selectedNode = this.selectedNode;
+ if (/^cmdEditPaste/.test(aCommand)) {
+ if (this.mPanel.panelset.clipboardFlavor != "inspector/dom-node") {
+ return false;
+ }
+ clipboardNode = this.mPanel.panelset.getClipboardData();
+ }
+ if (/^cmdEdit(Paste|Insert)/.test(aCommand)) {
+ currentNode =
+ viewer.currentNode && new XPCNativeWrapper(viewer.currentNode);
+ parentNode = currentNode && currentNode.parentNode;
+ }
+ switch (aCommand) {
+ case "cmdEditPaste":
+ case "cmdEditPasteBefore":
+ // Paste before and after, like Insert, don't operate on a selection,
+ // but the other paste commands do.
+ return this.isValidChild(parentNode, clipboardNode);
+ case "cmdEditPasteReplace":
+ return !!selectedNode &&
+ this.isValidChild(parentNode, clipboardNode, selectedNode);
+ case "cmdEditPasteFirstChild":
+ case "cmdEditPasteLastChild":
+ return this.isValidChild(selectedNode, clipboardNode);
+ case "cmdEditPasteAsParent":
+ return !!selectedNode &&
+ this.isValidChild(clipboardNode, selectedNode) &&
+ this.isValidChild(parentNode, clipboardNode, selectedNode);
+ case "cmdEditInsertAfter":
+ case "cmdEditInsertBefore":
+ return parentNode instanceof nsIDOMElement;
+ case "cmdEditInsertFirstChild":
+ case "cmdEditInsertLastChild":
+ return selectedNode instanceof nsIDOMElement;
+ case "cmdEditCut":
+ case "cmdEditCopy":
+ return !!selectedNode;
+ case "cmdEditDelete":
+ // If at least one of the selected nodes can be deleted, allow it.
+ let selectedNodes = this.getSelectedNodes();
+ for (let i = 0, n = selectedNodes.length; i < n; ++i) {
+ if (cmdEditDelete.isDeletable(selectedNodes[i])) {
+ return true;
+ }
+ }
+ return false;
+ case "cmdInspectBrowser":
+ if (!(selectedNode instanceof nsIDOMElement)) {
+ return false;
+ }
+ let n = selectedNode.localName.toLowerCase();
+ return n == "tabbrowser" || n == "browser" || n == "iframe" ||
+ n == "frame" || n == "editor";
+ case "cmdBlink":
+ return selectedNode instanceof nsIDOMElement;
+ case "cmdCopyXML":
+ case "cmdShowPseudoClasses":
+ return true;
+ case "cmdEditInspectInNewWindow":
+ return this.mDOMTree.view.selection.count == 1;
+ }
+ return false;
+ },
+
+ /**
+ * Determines whether the passed parent/child combination is valid.
+ * @param parent
+ * @param child
+ * @param replaced
+ * the node the child is replacing (optional)
+ * @return whether the passed parent can have the passed child as a child,
+ */
+ isValidChild: function DVr_IsValidChild(parent, child, replaced)
+ {
+ // the document (fragment) node must be an only child and can't be
+ // replaced
+ if (parent == null) {
+ return false;
+ }
+ // the only types that can ever have children
+ if (parent.nodeType != nsIDOMNode.ELEMENT_NODE &&
+ parent.nodeType != nsIDOMNode.DOCUMENT_NODE &&
+ parent.nodeType != nsIDOMNode.DOCUMENT_FRAGMENT_NODE) {
+
+ return false;
+ }
+ // the only types that can't ever be children
+ if (child.nodeType == nsIDOMNode.DOCUMENT_NODE ||
+ child.nodeType == nsIDOMNode.DOCUMENT_FRAGMENT_NODE ||
+ child.nodeType == nsIDOMNode.ATTRIBUTE_NODE) {
+
+ return false;
+ }
+ // doctypes can only be the children of documents
+ if (child.nodeType == nsIDOMNode.DOCUMENT_TYPE_NODE &&
+ parent.nodeType != nsIDOMNode.DOCUMENT_NODE) {
+
+ return false;
+ }
+ // only elements and fragments can have text, cdata, and entities as
+ // children
+ if (parent.nodeType != nsIDOMNode.ELEMENT_NODE &&
+ parent.nodeType != nsIDOMNode.DOCUMENT_FRAGMENT_NODE &&
+ (child.nodeType == nsIDOMNode.TEXT_NODE ||
+ child.nodeType == nsIDOMNode.CDATA_NODE ||
+ child.nodeType == nsIDOMNode.ENTITY_NODE)) {
+
+ return false;
+ }
+ // documents can only have one document element or doctype
+ if (parent.nodeType == nsIDOMNode.DOCUMENT_NODE &&
+ (child.nodeType == nsIDOMNode.ELEMENT_NODE ||
+ child.nodeType == nsIDOMNode.DOCUMENT_TYPE_NODE) &&
+ (!replaced || child.nodeType != replaced.nodeType)) {
+
+ for (var i = 0; i < parent.childNodes.length; i++) {
+ if (parent.childNodes[i].nodeType == child.nodeType) {
+ return false;
+ }
+ }
+ }
+ return true;
+ },
+
+ getCommand: function DVr_GetCommand(aCommand)
+ {
+ if (aCommand in window) {
+ try {
+ return new window[aCommand]();
+ }
+ catch (ex) {
+ // User canceled the transaction.
+ }
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsITransactionListener implementation
+
+ willDo: function DVr_WillDo(aManager, aTransaction)
+ {
+ var command = aTransaction.wrappedJSObject;
+ if (command instanceof cmdEditDelete) {
+ let nodes = command.nodes;
+ let deletables = [];
+ for (let i = 0, n = nodes.length; i < n; ++i) {
+ let node = nodes[i];
+ if (cmdEditDelete.isDeletable(node)) {
+ deletables.push(node);
+ }
+ }
+
+ // Save the currentNode and the linked pane's current subject, but
+ // don't overwrite them on redo.
+ if (!("oldCurrentNode" in command)) {
+ command.oldCurrentNode = this.currentNode;
+ command.oldLinkedSubject = this.mSelection;
+ }
+
+ if (cmdEditDelete.isDeletable(this.mSelection)) {
+ command.newLinkedSubject =
+ this.getNextInNextBestChain(this.mSelection, undefined,
+ function DVr_WillDo_Filter(aNode)
+ {
+ return deletables.indexOf(aNode) >= 0;
+ });
+ }
+ }
+ },
+
+ didDo: function DVr_DidDo(aManager, aTransaction, aResult)
+ {
+ var command = aTransaction.wrappedJSObject;
+ if (command instanceof cmdEditDelete) {
+ if (!("newLinkedSubject" in command)) {
+ // The linked panel's old subject wasn't deletable, but the
+ // transaction went through because other selected nodes were. Leave
+ // things alone and let the linked subject and any other non-deletable
+ // nodes remain selected.
+ return;
+ }
+ let newLinkedSubject = command.newLinkedSubject;
+ let selection = this.mDOMTree.view.selection;
+ if (selection.count > 0) {
+ // There are still some nodes selected (because they weren't
+ // deletable). newLinkedSubject should be one of them.
+ let idx = this.getRowIndexFromNode(newLinkedSubject);
+ if (!selection.isSelected(idx)) {
+ debug("node chosen for new linked subject was apparently deletable");
+ return;
+ }
+ this.changeSelection(newLinkedSubject);
+ }
+ else {
+ this.showNodeInTree(newLinkedSubject);
+ }
+ }
+ },
+
+ willUndo: stubImpl,
+
+ didUndo: function DVr_DidUndo(aManager, aTransaction, aResult)
+ {
+ var command = aTransaction.wrappedJSObject;
+ if (command instanceof cmdEditDelete) {
+ // Find all "deleted" rows and select them, even any that weren't
+ // deletable. We also want currentNode and mSelection to reflect the
+ // values they had before this transaction.
+ let nodes = command.nodes;
+ if (!nodes.length) {
+ return;
+ }
+
+ // Disable selectionChange events, because otherwise the linked pane's
+ // viewers will be flipping around, which is also computational overhead
+ // that we just don't need.
+ this.mDOMTree.treeBoxObject.beginUpdateBatch();
+ this.beginSelectionBatch();
+
+ this.mDOMTree.view.selection.clearSelection();
+ for (let i = nodes.length - 1; i >= 0; --i) {
+ this.showNodeInTree(nodes[i], true);
+ }
+ this.changeSelection(command.oldLinkedSubject);
+ this.mDOMTree.currentIndex =
+ this.getRowIndexFromNode(command.oldCurrentNode);
+
+ this.endSelectionBatch();
+ this.mDOMTree.treeBoxObject.endUpdateBatch();
+ }
+ },
+
+ willRedo: function DVr_WillRedo(aManager, aTransaction)
+ {
+ var command = aTransaction.wrappedJSObject;
+ if (command instanceof cmdEditDelete) {
+ this.willDo(aManager, aTransaction);
+ }
+ },
+
+ didRedo: function DVr_DidRedo(aManager, aTransaction, aResult)
+ {
+ var command = aTransaction.wrappedJSObject;
+ if (command instanceof cmdEditDelete) {
+ this.didDo(aManager, aTransaction, aResult);
+ }
+ },
+
+ willBeginBatch: stubImpl,
+
+ didBeginBatch: stubImpl,
+
+ willEndBatch: stubImpl,
+
+ didEndBatch: stubImpl,
+
+ willMerge: stubImpl,
+
+ didMerge: stubImpl,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Event Dispatching
+
+ addObserver: function DVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function DVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// UI Commands
+
+ showFindDialog: function DVr_ShowFindDialog()
+ {
+ var win =
+ openDialog("chrome://inspector/content/viewers/dom/findDialog.xul",
+ "_blank", "chrome,dependent", this.mFindType, this.mFindDir,
+ this.mFindParams);
+ },
+
+ /**
+ * Toggles the setting for displaying anonymous content.
+ */
+ toggleAnonContent: function DVr_ToggleAnonContent()
+ {
+ var value = PrefUtils.getPref("inspector.dom.showAnon");
+ PrefUtils.setPref("inspector.dom.showAnon", !value);
+ },
+
+ /**
+ * Sets the UI and filters for anonymous content.
+ *
+ * @param aValue
+ * The value that we should be setting things to.
+ */
+ setAnonContent: function DVr_SetAnonContent(aValue)
+ {
+ this.mDOMView.showAnonymousContent = aValue;
+ this.mPanel.panelset.setCommandAttribute("cmd:toggleAnon", "checked",
+ aValue);
+ },
+
+ toggleSubDocs: function DVr_ToggleSubDocs()
+ {
+ var val = !this.mDOMView.showSubDocuments;
+ this.mDOMView.showSubDocuments = val;
+ this.mPanel.panelset.setCommandAttribute("cmd:toggleSubDocs", "checked",
+ val);
+ },
+
+ /**
+ * Toggles the visibility of Processing Instructions.
+ */
+ toggleProcessingInstructions: function DVr_ToggleProcessingInstructions()
+ {
+ var value = PrefUtils.getPref("inspector.dom.showProcessingInstructions");
+ PrefUtils.setPref("inspector.dom.showProcessingInstructions", !value);
+ },
+
+ /**
+ * Sets the visibility of Processing Instructions.
+ *
+ * @param aValue
+ * The visibility of the instructions.
+ */
+ setProcessingInstructions: function DVr_SetProcessingInstructions(aValue)
+ {
+ this.mPanel.panelset
+ .setCommandAttribute("cmd:toggleProcessingInstructions", "checked",
+ aValue);
+ if (aValue) {
+ this.mDOMView.whatToShow |= NodeFilter.SHOW_PROCESSING_INSTRUCTION;
+ }
+ else {
+ this.mDOMView.whatToShow &= ~NodeFilter.SHOW_PROCESSING_INSTRUCTION;
+ }
+ },
+
+ /**
+ * Toggle state of 'Show Accessible Nodes' option.
+ */
+ toggleAccessibleNodes: function DVr_ToggleAccessibleNodes()
+ {
+ var value = PrefUtils.getPref("inspector.dom.showAccessibleNodes");
+ PrefUtils.setPref("inspector.dom.showAccessibleNodes", !value);
+ },
+
+ /**
+ * Set state of 'Show Accessible Nodes' option.
+ *
+ * @param aValue
+ * if true then accessible nodes will be shown
+ */
+ setAccessibleNodes: function DVr_SetAccessibleNodes(aValue)
+ {
+ if (!(kAccessibleRetrievalClassID in Components.classes)) {
+ aValue = false;
+ }
+
+ this.mDOMView.showAccessibleNodes = aValue;
+ this.mPanel.panelset.setCommandAttribute("cmd:toggleAccessibleNodes",
+ "checked", aValue);
+ },
+
+ /**
+ * Return state of 'Show Accessible Nodes' option.
+ */
+ getAccessibleNodes: function DVr_GetAccessibleNodes()
+ {
+ return this.mDOMView.showAccessibleNodes;
+ },
+
+ /**
+ * Toggles the value for whitespace nodes.
+ */
+ toggleWhitespaceNodes: function DVr_ToggleWhitespaceNodes()
+ {
+ var value = PrefUtils.getPref("inspector.dom.showWhitespaceNodes");
+ PrefUtils.setPref("inspector.dom.showWhitespaceNodes", !value);
+ },
+
+ /**
+ * Sets the UI for displaying whitespace nodes.
+ *
+ * @param aValue
+ * true if whitespace nodes should be shown
+ */
+ setWhitespaceNodes: function DVr_SetWhitespaceNodes(aValue)
+ {
+ this.mPanel.panelset.setCommandAttribute("cmd:toggleWhitespaceNodes",
+ "checked", aValue);
+ this.mDOMView.showWhitespaceNodes = aValue;
+ },
+
+ showColumnsDialog: function DVr_ShowColumnsDialog()
+ {
+ var win =
+ openDialog("chrome://inspector/content/viewers/dom/columnsDialog.xul",
+ "_blank", "chrome,dependent", this);
+ },
+
+ cmdShowPseudoClasses: function DVr_CmdShowPseudoClasses()
+ {
+ var node = this.selectedNode;
+
+ if (node) {
+ openDialog("chrome://inspector/content/viewers/dom/" +
+ "pseudoClassDialog.xul",
+ "_blank", "chrome", node);
+ }
+ },
+
+ cmdBlink: function DVr_CmdBlink()
+ {
+ this.flashElement(this.selectedNode);
+ },
+
+ onTreeSelectionChange: function DVr_OnTreeSelectionChange()
+ {
+ // NB: We're called on deselection, too.
+ var currentIndex = this.mDOMTree.currentIndex;
+ var currentNode = this.currentNode;
+
+ if (this.mDOMTree.view.selection.isSelected(currentIndex)) {
+ this.changeSelection(currentNode);
+ }
+ // Otherwise, we were deselected. We only care, though, if we're the
+ // object viewer's subject, and there are other nodes selected. (If no
+ // nodes are selected, we'll leave mSelection alone and won't fire any
+ // events, because nobody wants to inspect null.)
+ else if (this.mSelection == currentNode &&
+ this.mDOMTree.view.selection.count) {
+ // Find closest nearby selected node and use that.
+ var nearestSelectedIndex =
+ InsUtil.getNearestIndex(currentIndex, this.getSelectedIndexes());
+ this.changeSelection(this.getNodeFromRowIndex(nearestSelectedIndex));
+ }
+
+ viewer.pane.panelset.updateAllCommands();
+ },
+
+ /**
+ * Change the viewer selection. Note that "selection" here refers to the
+ * (not formalized) inIViewer::selection, *not* the tree selection. See
+ * bug 570879.
+ * @param aNode
+ * The new viewer selection. If there is an object panel linked to
+ * ours, this will be used for its subject.
+ */
+ changeSelection: function DVr_ChangeSelection(aNode)
+ {
+ if (this.mSelectionBatchNest) {
+ this.mPendingSelection = aNode;
+ }
+ else {
+ this.mSelection = aNode;
+ this.mObsMan.dispatchEvent("selectionChange", { selection: aNode } );
+ if (this.mSelection) {
+ this.flashElement(this.mSelection, true);
+ }
+ }
+ },
+
+ setInitialSelection: function DVr_SetInitialSelection(aObject)
+ {
+ var fireSelected = this.mDOMTree.currentIndex == 0;
+
+ if (this.mPanel.params) {
+ this.showNodeInTree(this.mPanel.params);
+ }
+ else {
+ this.showNodeInTree(aObject, false, true);
+ }
+
+ if (fireSelected) {
+ this.onTreeSelectionChange();
+ }
+ },
+
+ onPopupShowing: function DVr_OnPopupShowing(aPopup)
+ {
+ if (aPopup.id != "ppDOMContext") {
+ // This is a nested menupopup, and it should have been already taken
+ // care of.
+ return;
+ }
+ this.checkMenu(aPopup);
+ },
+
+ /**
+ * Recursively enable/disable descendants of a given popup, based on whether
+ * their commands are enabled or disabled. If the descendant has a
+ * checkvalid attribute, the descendant will be hidden as well as disabled.
+ * Recursively checked menus will be enabled or disabled depending on
+ * whether there are one or more enabled children in their respective
+ * menupopups.
+ * @param aPopup
+ * The menupopup at which to start checking.
+ * @return true if the popup contains any enabled items
+ */
+ checkMenu: function DVr_CheckMenu(aPopup) {
+ // We can't use XBL getters/setters here, because we're using recursion,
+ // and items in nested menus won't have CSS frames since they're not
+ // visible.
+ var hasEnabledItems = false;
+ for (let i = 0; i < aPopup.childNodes.length; ++i) {
+ let el = aPopup.childNodes[i];
+ if (el.localName == "menuseparator") {
+ continue;
+ }
+ let subject = el;
+ let isEnabled = false;
+ let checkValid = false;
+ if (el.hasAttribute("command")) {
+ let cmd = document.getElementById(el.getAttribute("command"));
+ if (cmd) {
+ checkValid = el.hasAttribute("checkvalid");
+ isEnabled = this.isCommandEnabled(cmd.id);
+ subject = cmd;
+ }
+ }
+ else if (el.localName == "menu") {
+ let kid = el.firstChild;
+ while (kid) {
+ if (kid.localName == "menupopup") {
+ // Disable this menu if none of the descendants are enabled.
+ isEnabled = this.checkMenu(kid);
+ break;
+ }
+ kid = kid.nextSibling;
+ }
+ }
+ else if (el.getAttribute("disabled") != "true") {
+ // There is no command here. (Maybe we're being extended?) We'll
+ // leave it up to third parties to manage enabling/disabling their own
+ // menuitems, and assume that they already reflect the correct state
+ // by the time we're called.
+ isEnabled = true;
+ }
+
+ if (isEnabled) {
+ hasEnabledItems = true;
+ subject.removeAttribute("disabled");
+ }
+ else {
+ subject.setAttribute("disabled", "true");
+ }
+
+ if (checkValid) {
+ el.hidden = !isEnabled;
+ }
+ }
+
+ return hasEnabledItems;
+ },
+
+ cmdInspectBrowser: function DVr_CmdInspectBrowser()
+ {
+ var node = this.selectedNode;
+ var n = node && node.localName.toLowerCase();
+ if (n == "iframe" || n == "frame" ||
+ (node.namespaceURI == kXULNSURI && (n == "browser" ||
+ n == "tabbrowser" ||
+ n == "editor"))) {
+ this.pane.subject = node.contentDocument;
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// XML Serialization
+
+ cmdCopySelectedXML: function DVr_CmdCopySelectedXML()
+ {
+ var node = this.selectedNode;
+ if (node) {
+ var xml = this.toXML(node);
+
+ var helper = XPCU.getService(kClipboardHelperClassID,
+ "nsIClipboardHelper");
+ helper.copyString(xml);
+ }
+ },
+
+ toXML: function DVr_ToXML(aNode)
+ {
+ return (new XMLSerializer()).serializeToString(aNode);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Click Selection
+
+ selectByClick: function DVr_SelectByClick()
+ {
+ if (this.mSelecting) {
+ this.stopSelectByClick();
+ this.removeClickListeners();
+ }
+ else {
+ // wait until after user releases the mouse after selecting this command
+ // from a UI element
+ window.setTimeout("viewer.startSelectByClick()", 10);
+ }
+ },
+
+ startSelectByClick: function DVr_StartSelectByClick()
+ {
+ this.mSelecting = true;
+ this.mSelectDocs = this.getAllDocuments();
+
+ for (var i = 0; i < this.mSelectDocs.length; ++i) {
+ var doc = this.mSelectDocs[i];
+ doc.addEventListener("mousedown", MouseDownListener, true);
+ doc.addEventListener("mouseup", EventCanceller, true);
+ doc.addEventListener("click", ListenerRemover, true);
+ // If user moves the mouse out of the original target area, there
+ // will be no onclick event fired.... so we have to deal with
+ // that.
+ doc.addEventListener("mouseout", ListenerRemover, true);
+ }
+ this.mPanel.panelset.setCommandAttribute("cmd:selectByClick", "checked",
+ "true");
+ },
+
+ doSelectByClick: function DVr_DoSelectByClick(aTarget)
+ {
+ if (aTarget.nodeType == nsIDOMNode.TEXT_NODE) {
+ aTarget = aTarget.parentNode;
+ }
+
+ this.stopSelectByClick();
+ this.showNodeInTree(aTarget);
+ },
+
+ stopSelectByClick: function DVr_StopSelectByClick()
+ {
+ this.mSelecting = false;
+
+ this.mPanel.panelset.setCommandAttribute("cmd:selectByClick", "checked",
+ null);
+ },
+
+ removeClickListeners: function DVr_RemoveClickListeners()
+ {
+ if (!this.mSelectDocs) { // we didn't select an element by click
+ return;
+ }
+
+ for (var i = 0; i < this.mSelectDocs.length; ++i) {
+ var doc = this.mSelectDocs[i];
+ doc.removeEventListener("mousedown", MouseDownListener, true);
+ doc.removeEventListener("mouseup", EventCanceller, true);
+ doc.removeEventListener("click", ListenerRemover, true);
+ doc.removeEventListener("mouseout", ListenerRemover, true);
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Find Methods
+
+ startFind: function DVr_StartFind(aType, aDir)
+ {
+ this.mFindResult = null;
+ this.mFindType = aType;
+ this.mFindDir = aDir;
+ this.mFindParams = [];
+ for (var i = 2; i < arguments.length; ++i) {
+ this.mFindParams[i-2] = arguments[i];
+ }
+
+ var fn = null;
+ switch (aType) {
+ case "id":
+ fn = "doFindElementById";
+ break;
+ case "tag":
+ fn = "doFindElementsByTagName";
+ break;
+ case "attr":
+ fn = "doFindElementsByAttr";
+ break;
+ };
+
+ this.mFindFn = fn;
+ this.mFindWalker = this.createDOMWalker(this.mDOMView.rootNode);
+ this.findNext();
+ },
+
+ findNext: function DVr_FindNext()
+ {
+ var walker = this.mFindWalker;
+ if (!walker) {
+ Components.utils.reportError("deep tree walker unavailable");
+ return;
+ }
+ var result = null;
+ var currentNode = walker.currentNode;
+ while (currentNode) {
+ if (this[this.mFindFn](walker)) {
+ result = walker.currentNode;
+ walker.nextNode();
+ break;
+ }
+ currentNode = walker.nextNode();
+ }
+
+ if (result && result != this.mFindResult) {
+ this.showNodeInTree(result);
+ this.mFindResult = result;
+ this.mDOMTree.focus();
+ }
+ else {
+ var bundle = this.mPanel.panelset.stringBundle;
+ var msg = bundle.getString("findNodesDocumentEnd.message");
+ var title = bundle.getString("findNodesDocumentEnd.title");
+
+ var promptService = XPCU.getService(kPromptServiceClassID,
+ "nsIPromptService");
+ promptService.alert(window, title, msg);
+ }
+ },
+
+ doFindElementById: function DVr_DoFindElementById(aWalker)
+ {
+ var re = new RegExp(this.mFindParams[0], "i");
+
+ // NB: In HTML getAttribute can return null, so we have to check that it's
+ // actually set; if we don't and the search string is "null", implicit
+ // toString conversion means that we'll match on every element without an
+ // ID. Additionally, for elements without an ID, getAttribute returns an
+ // empty string in XUL (bug 232598), so our check must handle that...
+ return aWalker.currentNode &&
+ aWalker.currentNode.nodeType == nsIDOMNode.ELEMENT_NODE &&
+ aWalker.currentNode.hasAttribute("id") &&
+ re.test(aWalker.currentNode.getAttribute("id"));
+ },
+
+ doFindElementsByTagName: function DVr_DoFindElementsByTagName(aWalker)
+ {
+ var re = new RegExp(this.mFindParams[0], "i");
+
+ return aWalker.currentNode &&
+ aWalker.currentNode.nodeType == nsIDOMNode.ELEMENT_NODE &&
+ re.test(aWalker.currentNode.localName);
+ },
+
+ doFindElementsByAttr: function DVr_DoFindElementsByAttr(aWalker)
+ {
+ var re = new RegExp(this.mFindParams[1], "i");
+
+ return aWalker.currentNode &&
+ aWalker.currentNode.nodeType == nsIDOMNode.ELEMENT_NODE &&
+ aWalker.currentNode.hasAttribute(this.mFindParams[0]) &&
+ re.test(aWalker.currentNode.getAttribute(this.mFindParams[0]));
+ },
+
+ /**
+ * Makes sure each ancestor in the given node's ancestor chain is open, so
+ * so that there exists a row in the tree that represents the node. That
+ * row will be selected and scrolled into view. If that node doesn't have a
+ * row in the tree, the nearest ancestor is used as a fallback. Further
+ * parameters allow finer control over what it means to "show" a node,
+ * including overriding the default behavior.
+ *
+ * @param aNode
+ * Node to show.
+ * @param aAugment [optional]
+ * true if selection should add to the current selection, false if it
+ * should clear it and be the only row selected. This has no effect
+ * if aNoSelect is true.
+ * @param aExpand [optional]
+ * true if the node's open state should be changed to open.
+ * @param aNoVisible [optional]
+ * true if you don't care whether the given node is scrolled into
+ * sight.
+ * @param aNoSelect [optional]
+ * true if you don't want the given element to be selected.
+ * @return The current index of the given node in the tree.
+ */
+ showNodeInTree:
+ function DVr_ShowNodeInTree(aNode, aAugment, aExpand, aNoVisible,
+ aNoSelect)
+ {
+ var bx = this.mDOMTree.treeBoxObject;
+
+ if (!aNode) {
+ if (!aAugment && !aNoSelect) {
+ bx.view.selection.select(-1);
+ }
+ return -1;
+ }
+
+ // Keep searching until a pre-created ancestor is found, and then open
+ // each ancestor until the row for the given node is created.
+ var line = [];
+ var parent = aNode;
+ var index = null;
+ while (parent) {
+ index = this.getRowIndexFromNode(parent);
+ line.push(parent);
+ if (index < 0) {
+ // The row for this node hasn't been created yet.
+ parent =
+ this.mDOMUtils.getParentForNode(parent,
+ this.mDOMView.showAnonymousContent);
+ }
+ else {
+ // The ancestor chain is already open above this point.
+ break;
+ }
+ }
+
+ // We've got all the ancestors, now open them one-by-one from the top
+ // down.
+ var view = bx.view;
+ var lastIndex;
+ for (let i = line.length - 1; i >= 0; --i) {
+ index = this.getRowIndexFromNode(line[i]);
+ if (index < 0) {
+ index = -1;
+ break; // We can't find the row, so stop trying to descend.
+ }
+ if ((aExpand || i > 0) && !view.isContainerOpen(index)) {
+ view.toggleOpenState(index);
+ }
+ lastIndex = index;
+ }
+
+ if (lastIndex >= 0) {
+ if (!aNoVisible) {
+ bx.ensureRowIsVisible(lastIndex);
+ }
+ if (!aNoSelect) {
+ view.selection.rangedSelect(lastIndex, lastIndex, aAugment);
+ }
+ }
+
+ return index;
+ },
+
+ /**
+ * Rebuild the tree by re-opening previously opened rows, re-selecting
+ * previously selected rows, and restoring the viewer selection and the
+ * node previously at currentNode, if possible.
+ * @param aIncludeAnons [optional]
+ * If a rebuild was triggered in response to a pref change for
+ * showing anonymous content, this must be former pref value.
+ */
+ rebuild: function DVr_Rebuild(aIncludeAnons)
+ {
+ var selection = this.mDOMTree.view.selection;
+
+ // Remember all non-ignorable nodes of open rows. Re-opening them will be
+ // the first step to recreating the tree's state.
+ var opened = [];
+ for (let i = 0, n = this.mDOMTree.view.rowCount; i < n; ++i) {
+ if (this.mDOMTree.view.isContainerOpen(i)) {
+ let node = this.getNodeFromRowIndex(i);
+ if (!this.isIgnorableNode(node)) {
+ opened.push(node);
+ }
+ }
+ }
+
+ // Remember all nodes of selected rows. Also save the indexes of rows of
+ // non-ignorable nodes so we can determine the best viewer selection
+ // candidate below.
+ var ignorableSelectedNodes = [];
+ var nonIgnorableSelectedNodes = [];
+ var nonIgnorableSelectedIndexes = [];
+ let selectedIndexes = this.getSelectedIndexes();
+ for (let i = 0, n = selectedIndexes.length; i < n; ++i) {
+ let idx = selectedIndexes[i];
+ let node = this.getNodeFromRowIndex(idx);
+ if (this.isIgnorableNode(node)) {
+ ignorableSelectedNodes.push(node);
+ }
+ else {
+ nonIgnorableSelectedNodes.push(node);
+ nonIgnorableSelectedIndexes.push(idx);
+ }
+ }
+
+ // Remember the node showing in the object pane. If the current node
+ // there is going to be filtered out, pick another one by using the same
+ // algorithm used when a row is deselected.
+ var viewerSelection = this.selection;
+ if (this.isIgnorableNode(viewerSelection)) {
+ let idx = this.getRowIndexFromNode(viewerSelection);
+ let nearestNonIgnorableSelectedIndex =
+ InsUtil.getNearestIndex(idx, nonIgnorableSelectedIndexes);
+ viewerSelection =
+ this.getNodeFromRowIndex(nearestNonIgnorableSelectedIndex);
+ }
+ if (!viewerSelection) {
+ // There was no nearest non-ignorable selected node (ostensibly, because
+ // there are no non-ignorable selected nodes), so find a fallback from
+ // the next-best chain.
+ if (nonIgnorableSelectedNodes.length) {
+ debug("some nodes are non-ignorable, but no suitable viewer " +
+ "selection was found");
+ }
+ viewerSelection =
+ this.getNextInNextBestChain(this.selection, aIncludeAnons);
+ }
+
+ // Remember currentNode.
+ var currentNode = this.currentNode;
+ if (this.isIgnorableNode(currentNode)) {
+ currentNode = this.getNextInNextBestChain(currentNode, aIncludeAnons);
+ }
+
+ selection.clearSelection();
+ this.mDOMView.rebuild();
+
+ // We're now operating under the new inIDOMView parameters. Restore the
+ // previously opened rows. This won't, however, ensure that all non-
+ // ignorable nodes which were previously exposed will be re-exposed.
+ // Consider the case of going from not showing anons to showing them, and
+ // where the ancestor of a previously exposed, non-ignorable node (or the
+ // node itself) is inserted as a "child" of an anonymous node. See the
+ // comments below about re-selecting nodes for how we deal with this.
+ this.mDOMTree.treeBoxObject.beginUpdateBatch();
+ for (let i = 0, n = opened.length; i < n; ++i) {
+ let idx =
+ this.showNodeInTree(opened[i], false, // Don't augment the selection.
+ true, // Expand to show children.
+ true, // The node need not be visible.
+ true); // Don't select it.
+ if (idx < 0) {
+ debug("previously opened node expected to be in tree but isn't");
+ }
+ }
+
+ // Re-select all rows for non-ignorable nodes which had been previously
+ // selected.
+ this.beginSelectionBatch();
+ for (let i = 0, n = nonIgnorableSelectedNodes.length; i < n; ++i) {
+ let idx = this.showNodeInTree(nonIgnorableSelectedNodes[i], true);
+ if (idx < 0) {
+ debug("previously selected node expected to have row in tree but " +
+ "doesn't");
+ }
+ }
+
+ // For rows of previously selected nodes which are ignorable, we call
+ // showNodeInTree on them without selecting them, in order to make sure as
+ // much of their ancestor chain is exposed as possible. This two-phase
+ // system is necessary because it's not guaranteed that all non-ignorable
+ // rows which were exposed before are now exposed again. See the above
+ // comments about re-opening nodes for why this is true.
+ for (let i = 0, n = ignorableSelectedNodes.length; i < n; ++i) {
+ let idx = this.showNodeInTree(ignorableSelectedNodes[i], false, false,
+ true, true);
+ if (idx >= 0) {
+ debug("previously selected node expected to be ignorable but still " +
+ "has a row in the tree");
+ // Well, I guess we'll go ahead and select it then...
+ selection.toggleSelect(idx);
+ }
+ }
+
+ // Restore the viewer selection.
+ if (!selection.count) {
+ // All previously selected nodes have been filtered out. Select the
+ // fallback we found from the next-best chain.
+ this.showNodeInTree(viewerSelection);
+ }
+ else if (this.mPendingSelection != viewerSelection) {
+ // The previous viewer selection should now be one of the selected
+ // nodes, but it's not the one that was selected last.
+ if (this.getRowIndexFromNode(viewerSelection) >= 0) {
+ this.changeSelection(viewerSelection);
+ }
+ else {
+ debug("new viewer selection expected to have row in tree but " +
+ "doesn't");
+ }
+ }
+
+ // Attempt to restore currentIndex to the previous currentNode.
+ var currentIndex = this.showNodeInTree(currentNode, false, false, true,
+ true);
+ if (currentIndex >= 0) {
+ this.mDOMTree.currentIndex = currentIndex;
+ }
+ else if (currentNode) {
+ debug("currentNode expected to have row in tree but doesn't");
+ }
+
+ this.mDOMTree.treeBoxObject.endUpdateBatch();
+ this.endSelectionBatch();
+ },
+
+ /**
+ * Determine whether the given node will be displayed in the tree, based on
+ * the tree's current show parameters and node filters.
+ * @param aNode
+ * A node contained within the subtree of the tree's root (including
+ * any subdocuments and their children).
+ * @return A boolean indicating whether the node is ignorable
+ */
+ isIgnorableNode: function DVr_IsIgnorableNode(aNode)
+ {
+ if (!(aNode instanceof nsIDOMNode)) {
+ return true;
+ }
+
+ // The node filter doesn't actually get checked for documents in
+ // inDOMView.
+ if (!(aNode instanceof nsIDOMDocument)) {
+ let nodeTypeFilter = 1 << (aNode.nodeType - 1);
+ if (!(this.mDOMView.whatToShow & nodeTypeFilter)) {
+ return true;
+ }
+ }
+
+ if (aNode instanceof nsIDOMCharacterData &&
+ this.mDOMUtils.isIgnorableWhitespace(aNode) &&
+ !this.mDOMView.showWhitespaceNodes) {
+ return true;
+ }
+
+ if (aNode != this.mDOMView.rootNode) {
+ if (!this.mDOMView.showSubDocuments &&
+ aNode.ownerDocument != this.mDOMView.rootNode) {
+ return true;
+ }
+
+ if (!this.mDOMView.showAnonymousContent) {
+ // Anonymous nodes are obviously ignorable, but so are non-anonymous
+ // nodes in the contentDocument of an anonymous element (as is the
+ // contentDocument itself).
+ let current = aNode;
+ if (current instanceof nsIDOMDocument) {
+ current = this.mDOMUtils.getParentForNode(current, true);
+ }
+ while (current && current.ownerDocument) {
+ if (current.ownerDocument.getBindingParent(current)) {
+ return true;
+ }
+ if (current.ownerDocument == this.mDOMView.rootNode) {
+ break;
+ }
+ current = this.mDOMUtils.getParentForNode(current.ownerDocument,
+ true);
+ }
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * The next-best chain extending from a given node is defined as the next
+ * non-ignorable siblings in document order, followed the preceding non-
+ * ignorable siblings in reverse document order, followed by the parent node
+ * (if it's non-ignorable) and its non-ignorable siblings ordered similarly,
+ * continuing all the way up the ancestor chain.
+ * @param aNode
+ * The node for which we'll traverse its next-best chain.
+ * @param aIncludeAnons [optional]
+ * Whether to consider the effects of anonymous content on the chain
+ * structure. Note that this is not necessarily the same as the DOM
+ * view's showAnonymousContent attribute, and even if true, won't
+ * affect whether anonymous nodes can be returned. However, the
+ * default is whatever the showAnonymousContent attribute's value is.
+ * @param aFilterFn [optional]
+ * If specified, when a node is considered, aFilterFn will be called
+ * with that node as its only parameter. A truthy return value will
+ * disqualify the node's eligibility to be returned as the next in
+ * the next-best chain. NB: A falsy return value won't guarantee
+ * that the node will positively appear in the chain; all nodes still
+ * need to pass the isIgnorableNode check.
+ * @return The next node in the next-best chain.
+ */
+ getNextInNextBestChain:
+ function DVr_GetNextInNextBestChain(aNode, aIncludeAnons, aFilterFn)
+ {
+ if (!aNode) {
+ return null;
+ }
+
+ var withoutFilter = function DVr_GetNextInNextBestChain_IsIgnorable(aNode)
+ {
+ return viewer.isIgnorableNode(aNode);
+ };
+ var withFilter =
+ function DVr_GetNextInNextBestChain_FilteredIsIgnorable(aNode)
+ {
+ return viewer.isIgnorableNode(aNode) || aFilterFn(aNode);
+ };
+
+ var isIgnorable = aFilterFn ? withFilter : withoutFilter;
+
+
+ // The approach is broken down as follows:
+ // 1. Locating the topmost ignorable node in the ancestor chain (including
+ // the given node).
+ // 2. Locating the next non-ignorable sibling of that ancestor, beginning
+ // by searching forward in the sibling group starting at that node then
+ // backwards if we exhaust the list of all siblings that follow.
+ var showAnons = this.mDOMView.showAnonymousContent;
+ var ancestorChain = [];
+ var ancestor = aNode;
+ while (ancestor != this.mDOMView.rootNode) {
+ ancestor = this.mDOMUtils.getParentForNode(ancestor, showAnons);
+ if (!ancestor) {
+ break;
+ }
+ ancestorChain.push(ancestor);
+ }
+ var topmost = aNode;
+ for (let i = ancestorChain.length - 1; i >= 0; --i) {
+ // XXX This is O(h*d), where h is the node depth from the root, and d is
+ // the document nesting level of aNode's ownerDocument. If the document
+ // consists entirely of nested iframes/browsers/what-have-you, that
+ // means it will technically be O(h^2). There's room for optimization
+ // here, but d is expected to be much smaller than h in practice, i.e.,
+ // anywhere between 1 and something like 3, and I can't think of a good
+ // way to get around this right now without creating API awkwardness.
+ if (isIgnorable(ancestorChain[i])) {
+ topmost = ancestorChain[i];
+ break;
+ }
+ }
+ // Short circuit for rootNode; it won't have any siblings.
+ if (topmost == this.mDOMView.rootNode) {
+ // Just in case for some crazy reason the root node is ignorable...
+ return isIgnorable(topmost) ? null : topmost;
+ }
+
+ // Follow through on step 2.
+
+ // The nature of anonymous content means that the nodes around aNode can
+ // get completely rearranged, depending on whether we're showing anonymous
+ // content or not. If we're getting called from a rebuild due to a pref
+ // change for showing anonymous content, showAnons is going to reflect the
+ // new value. If that's why we are getting called, we want to select from
+ // the next-best chain based on the order nodes were visually presented
+ // when the user toggled the pref, which is is why we need the
+ // aIncludeAnons override, since showAnons is untrustworthy here.
+ var includeAnons =
+ aIncludeAnons === undefined ? showAnons : aIncludeAnons;
+ // NB: By allowing includeAnons to deviate from showAnons above, we can't
+ // guarantee that ancestor will be non-ignorable (it might be anonymous,
+ // and we're not showing anons), so we'll need to check for it later.
+ ancestor = this.mDOMUtils.getParentForNode(topmost, includeAnons);
+ // As a side-effect of special-casing the rootNode-as-topmost check
+ // earlier, we're guaranteed ancestor is non-null here.
+ var walker = this.createDOMWalker(ancestor, includeAnons);
+ var current = walker.firstChild();
+
+ // We have to relocate the walker over topmost by iterating over nodes
+ // until we get there. We'll save the ones we pass up along the way so we
+ // can immediately begin looking at those preceding nodes in reverse order
+ // if we find that we've exhausted all of the nodes that follow.
+ var preceding = [];
+ while (current != topmost) {
+ preceding.push(current);
+ current = walker.nextSibling();
+ if (!current) {
+ debug("unexpected end of nodes");
+ return null;
+ }
+ }
+
+ // Look for the next non-ignorable in the nodes that follow topmost.
+ do {
+ current = walker.nextSibling();
+ if (!isIgnorable(current)) {
+ return current;
+ }
+ } while (current);
+
+ // Look for the next non-ignorable in the nodes that precede topmost.
+ for (let i = preceding.length - 1; i >= 0; --i) {
+ current = preceding[i];
+ if (!isIgnorable(current)) {
+ return current;
+ }
+ }
+
+ // All of the adjacent nodes are ignorable, too. Use topmost's parent,
+ // which we know to be non-ignorable (by virtue of it being an ancestor of
+ // topmost). Recall from before that we can't be sure that the ancestor
+ // we've got in |ancestor| is non-ignorable, though...
+ if (includeAnons != showAnons) {
+ ancestor = this.mDOMUtils.getParentForNode(topmost, showAnons);
+ }
+
+ return ancestor;
+ },
+
+ /**
+ * Convenience method for instantiating a deep tree walker, using much of
+ * the same preferences as the DOM view.
+ * @param aRoot
+ * The root node to begin traversal. See the W3 Traversal spec for
+ * more information.
+ * @param aShowAnons [optional]
+ * Whether to include anonymous content in the walk. This is the
+ * same as setting showAnonymousContent on an instantiated deep tree
+ * walker. The default is the value of the showAnonymousContent
+ * attribute on the DOM view.
+ */
+ createDOMWalker:
+ function DVr_CreateDOMWalker(aRoot, aShowAnons)
+ {
+ var walker = XPCU.createInstance(kDeepTreeWalkerClassID,
+ "inIDeepTreeWalker");
+ walker.showAnonymousContent = aShowAnons === undefined ?
+ this.mDOMView.showAnonymousContent :
+ aShowAnons;
+ walker.showSubDocuments = this.mDOMView.showSubDocuments;
+ walker.init(aRoot, this.mDOMView.whatToShow);
+ return walker;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Columns
+
+ // XXX re-implement custom columns code someday
+
+ initColumns: function DVr_InitColumns()
+ {
+ var colPref = PrefUtils.getPref("inspector.dom.columns");
+ var cols = colPref.split(",")
+ this.mColumns = cols;
+ this.mColumnHash = {};
+ },
+
+ saveColumns: function DVr_SaveColumns()
+ {
+ var cols = this.mColumns.join(",");
+ PrefUtils.setPref("inspector.dom.columns", cols);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Flashing
+
+ flashElement: function DVr_FlashElement(aElement, aIsOnSelect)
+ {
+ // make sure we only try to flash element nodes, and don't
+ // flash the documentElement (it's too darn big!)
+ if (aElement.nodeType == nsIDOMNode.ELEMENT_NODE &&
+ aElement != aElement.ownerDocument.documentElement) {
+
+ var flasher = this.mPanel.panelset.flasher;
+ if (aIsOnSelect) {
+ flasher.flashElementOnSelect(aElement);
+ }
+ else {
+ flasher.flashElement(aElement);
+ }
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Prefs
+
+ /**
+ * Called by PrefChangeObserver.
+ *
+ * @param aName
+ * The name of the preference that has been changed.
+ */
+ onPrefChanged: function DVr_OnPrefChanged(aName)
+ {
+ var value = PrefUtils.getPref(aName);
+ var includeAnons = undefined;
+
+ switch (aName) {
+ case "inspector.dom.showAnon":
+ includeAnons = this.mDOMView.showAnonymousContent;
+ this.setAnonContent(value);
+ break;
+
+ case "inspector.dom.showProcessingInstructions":
+ this.setProcessingInstructions(value);
+ break;
+
+ case "inspector.dom.showAccessibleNodes":
+ this.setAccessibleNodes(value);
+ break;
+
+ case "inspector.dom.showWhitespaceNodes":
+ this.setWhitespaceNodes(value);
+ break;
+
+ default:
+ return;
+ }
+
+ this.rebuild(includeAnons);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Uncategorized
+
+ getAllDocuments: function DVr_GetAllDocuments()
+ {
+ var doc = this.mDOMView.rootNode;
+ var results = [doc];
+ this.findDocuments(doc, results);
+ return results;
+ },
+
+ findDocuments: function DVr_FindDocuments(aDoc, aArray)
+ {
+ this.addKidsToArray(aDoc.getElementsByTagName("frame"), aArray);
+ this.addKidsToArray(aDoc.getElementsByTagName("iframe"), aArray);
+ this.addKidsToArray(aDoc.getElementsByTagNameNS(kXULNSURI, "browser"),
+ aArray);
+ this.addKidsToArray(aDoc.getElementsByTagNameNS(kXULNSURI, "tabbrowser"),
+ aArray);
+ this.addKidsToArray(aDoc.getElementsByTagNameNS(kXULNSURI, "editor"),
+ aArray);
+ },
+
+ addKidsToArray: function DVr_AddKidsToArray(aKids, aArray)
+ {
+ for (var i = 0; i < aKids.length; ++i) {
+ try {
+ if (!aKids[i].contentDocument)
+ continue;
+ aArray.push(aKids[i].contentDocument);
+ // Now recurse down into the kid and look for documents there
+ this.findDocuments(aKids[i].contentDocument, aArray);
+ }
+ catch (ex) {
+ // if we can't access the content document, skip it
+ }
+ }
+ },
+
+ /**
+ * Get the node corresponding to the tree's currentIndex (the row with the
+ * focus rect). NB: This is *not* a method to get the tree's selection.
+ * Use selectedNode or getSelectedNodes for that.
+ * @return the node corresponding to the tree's currentIndex, which may or
+ * may not be a part of the tree's selection
+ */
+ get currentNode()
+ {
+ var index = this.mDOMTree.currentIndex;
+ return this.getNodeFromRowIndex(index);
+ },
+
+ /**
+ * Get the node represented by the tree's selection.
+ * @return The currently selected node, or null if zero or two or more nodes
+ * are selected.
+ */
+ get selectedNode()
+ {
+ if (this.mDOMTree.view.selection.count == 1) {
+ var minAndMax = {};
+ this.mDOMTree.view.selection.getRangeAt(0, minAndMax, minAndMax);
+ return this.getNodeFromRowIndex(minAndMax.value);
+ }
+ return null;
+ },
+
+ /**
+ * Get the nodes corresponding to the tree's selected rows.
+ * @return An array of nodes.
+ */
+ getSelectedNodes: function DVr_GetSelectedNodes()
+ {
+ var nodes = [];
+ var indexes = this.getSelectedIndexes();
+ for (let i = 0, n = indexes.length; i < n; ++i) {
+ nodes.push(this.getNodeFromRowIndex(indexes[i]));
+ }
+ return nodes;
+ },
+
+ /**
+ * Determine the tree's selected rows.
+ * @return An array of row indexes.
+ */
+ getSelectedIndexes: function DVr_GetSelectedIndexes()
+ {
+ var indexes = [];
+ var selection = this.mDOMTree.view.selection;
+ for (let i = 0, n = selection.getRangeCount(); i < n; ++i) {
+ var min = {};
+ var max = {};
+ selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ indexes.push(j);
+ }
+ }
+ return indexes;
+ },
+
+ getNodeFromRowIndex: function DVr_GetNodeFromRowIndex(aIndex)
+ {
+ try {
+ return this.mDOMView.getNodeFromRowIndex(aIndex);
+ }
+ catch (ex) {
+ return null;
+ }
+ },
+
+ getRowIndexFromNode: function DVr_GetRowIndexFromNode(aNode)
+ {
+ try {
+ return this.mDOMView.getRowIndexFromNode(aNode);
+ }
+ catch (ex) {
+ return -1;
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// Command Objects
+
+/**
+ * Deletes one or more nodes from the tree.
+ */
+function cmdEditDelete()
+{
+ // Approach:
+ // 1. Order the nodes with the primary criterion being the node depth, with
+ // the nodes of greatest depth appearing near the beginning of the list.
+ // The secondary (tiebreaker) criterion is applied only to nodes which
+ // are siblings, and is the node order, corresponding to the nodes'
+ // indexing in their parents' childNodes NodeLists.
+ // 2. Iterate over the list, storing the current node's parent and its next
+ // sibling (or null if it's the last of its parent's children).
+ //
+ // Observe that after the nodes are deleted, this allows us to cleanly undo
+ // this transaction by working backwards and reinserting nodes.
+ var nodes = viewer.getSelectedNodes().sort(cmdEditDelete.sortComparator);
+
+ this.nodes = [];
+ this.mParents = [];
+ this.mSiblings = [];
+
+ var didPrompt = false;
+ for (let i = 0, n = nodes.length; i < n; ++i) {
+ let node = nodes[i];
+
+ // If we delete a descendant of an anonymous node and that anonymous
+ // node's binding parent, the nodes array and the mParents array (and
+ // potentially the mSiblings array) would continue to needlessly reference
+ // nodes from that anonymous subtree. Even if the binding parent gets
+ // restored via undo, a new anonymous content tree will be created for it,
+ // so we can eliminate the references to the nodes in the old subtree.
+ let bindingParent = node.ownerDocument.getBindingParent(node);
+ if (bindingParent && cmdEditDelete.isDeletable(bindingParent) &&
+ nodes.indexOf(bindingParent, i) >= 0) { // XXX O(1/2 * n^2)
+ // Notify about the issue described above, and ask if it's okay to
+ // continue.
+ if (!didPrompt) {
+ let bundle = viewer.pane.panelset.stringBundle;
+ let msg = bundle.getString("irrecoverableSubtree.message");
+ let title = bundle.getString("irrecoverableSubtree.title");
+
+ let promptService = XPCU.getService(kPromptServiceClassID,
+ "nsIPromptService");
+ let confirmation = promptService.confirm(window, title, msg);
+
+ if (!confirmation) {
+ throw new Error("User canceled transaction");
+ }
+
+ didPrompt = true;
+ }
+ }
+ else {
+ this.nodes.push(node);
+ this.mParents.push(node.parentNode);
+ this.mSiblings.push(node.nextSibling);
+ }
+ }
+
+ this.wrappedJSObject = this;
+}
+
+cmdEditDelete.sortComparator = function Delete_SortComparator(a, b)
+{
+ // Sibling nodes get arranged by natural order.
+ if (a.parentNode && a.parentNode == b.parentNode) {
+ let kids = a.parentNode.childNodes;
+ for (let i = 0, n = kids.length; i < n; ++i) {
+ if (kids[i] == a) {
+ return -1;
+ }
+ if (kids[i] == b) {
+ break;
+ }
+ }
+ return 1;
+ }
+
+ // Otherwise, nodes at greatest depth appear first.
+ var rootNode = viewer.mDOMView.rootNode;
+ var showAnons = viewer.mDOMView.showAnonymousContent;
+ var aAncestor = viewer.mDOMUtils.getParentForNode(a, showAnons);
+ var bAncestor = viewer.mDOMUtils.getParentForNode(b, showAnons);
+
+ // Check for equivalence to the root node, too, because getParentForNode
+ // will walk all the way up the tree (e.g., out of a content document to a
+ // browser containing it).
+ while (aAncestor != bAncestor && aAncestor && bAncestor &&
+ aAncestor != rootNode && bAncestor != rootNode) {
+ aAncestor = viewer.mDOMUtils.getParentForNode(aAncestor, showAnons);
+ bAncestor = viewer.mDOMUtils.getParentForNode(bAncestor, showAnons);
+ }
+ if (!aAncestor || aAncestor == rootNode) {
+ return 1;
+ }
+ return -1;
+};
+
+/**
+ * Determine if a node is deletable by our deletion methods.
+ * @param aNode
+ * The node to check.
+ * @param aFailure [optional]
+ * Outparam whose value will correspond to a cmdEditDelete error
+ * constant. If the node is found to be deletable, aFailure will be
+ * not be altered.
+ * @return Boolean indicating deletability.
+ */
+cmdEditDelete.isDeletable = function Delete_IsDeletable(aNode, aFailure)
+{
+ var failure = aFailure || { };
+ if (!aNode) {
+ failure.value = this.NODE_NULL;
+ return false;
+ }
+ var parent = aNode.parentNode;
+ if (!parent) {
+ failure.value = this.NO_PARENT;
+ return false;
+ }
+ if (Array.indexOf(parent.childNodes, aNode) < 0) {
+ failure.value = this.NOT_EXPLICIT_CHILD;
+ return false;
+ }
+ return true;
+};
+
+cmdEditDelete.NODE_NULL = 1;
+cmdEditDelete.NO_PARENT = 2;
+cmdEditDelete.NOT_EXPLICIT_CHILD = 3;
+
+cmdEditDelete.prototype = new inBaseCommand(false);
+cmdEditDelete.prototype.constructor = cmdEditDelete;
+
+cmdEditDelete.prototype.nodes = null;
+
+cmdEditDelete.prototype.doTransaction = function Delete_DoTransaction()
+{
+ // Note that the "indexes" here refer to the given nodes' indexes in this
+ // instance's |nodes| array, not the row indexes of the view.
+ this.mDeletedIndexes = [];
+ for (let i = 0, n = this.nodes.length; i < n; ++i) {
+ let node = this.nodes[i];
+ let failure = {};
+ if (cmdEditDelete.isDeletable(node, failure)) {
+ try {
+ this.mParents[i].removeChild(node);
+ this.mDeletedIndexes.push(i);
+ }
+ catch (ex) {
+ Components.utils.reportError(node + " was expected to be deletable but isn't");
+ }
+ }
+ else {
+ let consoleMsg = node.toString();
+ switch (failure.value) {
+ case cmdEditDelete.NO_PARENT:
+ consoleMsg += " has no parent node and cannot be deleted.";
+ break;
+ case cmdEditDelete.NOT_EXPLICIT_CHILD:
+ consoleMsg += " is anonymous to its parent node and cannot be deleted.";
+ break;
+ }
+ this.logString(consoleMsg);
+ }
+ }
+};
+
+cmdEditDelete.prototype.logString = function Delete_LogString(aMessage)
+{
+ if (("mConsoleService" in this)) {
+ // This is not the first call.
+ if (this.mConsoleService) {
+ this.mConsoleService.logStringMessage(aMessage);
+ }
+ else {
+ dump(aMessage);
+ }
+ }
+ else {
+ try {
+ this.mConsoleService = XPCU.getService("@mozilla.org/consoleservice;1",
+ "nsIConsoleService");
+ }
+ catch (ex) {
+ // Null it out for the next call, so we can use our fallback.
+ this.mConsoleService = null;
+ }
+ this.logString(aMessage);
+ }
+};
+
+cmdEditDelete.prototype.undoTransaction = function Delete_UndoTransaction()
+{
+ // Recall that since not all nodes in this.nodes are necessarily deletable,
+ // this.mDeletedIndexes is a list of indexes into this.nodes where the node
+ // at each index is one which was found to be deletable and was successfully
+ // removed.
+ for (let i = this.mDeletedIndexes.length - 1; i >= 0; --i) {
+ let idx = this.mDeletedIndexes[i];
+ try {
+ this.mParents[idx].insertBefore(this.nodes[idx], this.mSiblings[idx]);
+ }
+ catch (ex) {
+ // XXX allow recovery from external manipulation
+ Components.utils.reportError("Couldn't undo deletion for node " +
+ this.nodes[idx]);
+ }
+ }
+};
+
+function cmdEditCut() {}
+
+cmdEditCut.prototype = new inBaseCommand(false);
+
+cmdEditCut.prototype.cmdCopy = null;
+cmdEditCut.prototype.cmdDelete = null;
+
+cmdEditCut.prototype.doTransaction = function Cut_DoTransaction()
+{
+ if (!this.cmdCopy) {
+ this.cmdDelete = new cmdEditDelete();
+ this.cmdCopy = new cmdEditCopy();
+ }
+ this.cmdCopy.doTransaction();
+ this.cmdDelete.doTransaction();
+};
+
+cmdEditCut.prototype.undoTransaction = function Cut_UndoTransaction()
+{
+ this.cmdDelete.undoTransaction();
+};
+
+function cmdEditCopy() {
+ this.mNode = viewer.selectedNode;
+}
+
+cmdEditCopy.prototype = new inBaseCommand();
+
+cmdEditCopy.prototype.mNode = null;
+
+cmdEditCopy.prototype.doTransaction = function Copy_DoTransaction()
+{
+ if (this.mNode) {
+ viewer.pane.panelset.setClipboardData(this.mNode.cloneNode(true),
+ "inspector/dom-node", null);
+ }
+};
+
+/**
+ * Pastes the node on the clipboard as the next sibling of the selected node.
+ */
+function cmdEditPaste() {}
+
+cmdEditPaste.prototype = new inBaseCommand(false);
+
+cmdEditPaste.prototype.pastedNode = null;
+cmdEditPaste.prototype.pastedBefore = null;
+
+cmdEditPaste.prototype.doTransaction = function Paste_DoTransaction()
+{
+ var node = this.pastedNode || viewer.pane.panelset.getClipboardData();
+ var ref = this.pastedBefore || viewer.currentNode;
+ if (ref) {
+ this.pastedNode = node.cloneNode(true);
+ this.pastedBefore = ref;
+ ref.parentNode.insertBefore(this.pastedNode, ref.nextSibling);
+ return false;
+ }
+ return true;
+};
+
+cmdEditPaste.prototype.undoTransaction = function Paste_UndoTransaction()
+{
+ if (this.pastedNode) {
+ this.pastedNode.parentNode.removeChild(this.pastedNode);
+ }
+};
+
+/**
+ * Pastes the node on the clipboard as the previous sibling of the selected
+ * node.
+ */
+function cmdEditPasteBefore() {}
+
+cmdEditPasteBefore.prototype = new inBaseCommand(false);
+
+cmdEditPasteBefore.prototype.pastedNode = null;
+cmdEditPasteBefore.prototype.pastedBefore = null;
+
+cmdEditPasteBefore.prototype.doTransaction =
+ function PasteBefore_DoTransaction()
+{
+ var node = this.pastedNode || viewer.pane.panelset.getClipboardData();
+ var ref = this.pastedBefore || viewer.currentNode;
+ if (ref) {
+ this.pastedNode = node.cloneNode(true);
+ this.pastedBefore = ref;
+ ref.parentNode.insertBefore(this.pastedNode, ref);
+ return false;
+ }
+ return true;
+};
+
+cmdEditPasteBefore.prototype.undoTransaction =
+ function PasteBefore_UndoTransaction()
+{
+ if (this.pastedNode) {
+ this.pastedNode.parentNode.removeChild(this.pastedNode);
+ }
+};
+
+/**
+ * Pastes the node on the clipboard in the place of the selected node,
+ * overwriting it.
+ */
+function cmdEditPasteReplace() {}
+
+cmdEditPasteReplace.prototype = new inBaseCommand(false);
+
+cmdEditPasteReplace.prototype.pastedNode = null;
+cmdEditPasteReplace.prototype.originalNode = null;
+
+cmdEditPasteReplace.prototype.doTransaction =
+ function PasteReplace_DoTransaction()
+{
+ var node = this.pastedNode || viewer.pane.panelset.getClipboardData();
+ var selected = this.originalNode || viewer.selectedNode;
+ if (selected) {
+ this.pastedNode = node.cloneNode(true);
+ this.originalNode = selected;
+ selected.parentNode.replaceChild(this.pastedNode, selected);
+ return false;
+ }
+ return true;
+};
+
+cmdEditPasteReplace.prototype.undoTransaction =
+ function PasteReplace_UndoTransaction()
+{
+ if (this.pastedNode) {
+ this.pastedNode.parentNode.replaceChild(this.originalNode,
+ this.pastedNode);
+ }
+};
+
+/**
+ * Pastes the node on the clipboard as the first child of the selected node.
+ */
+function cmdEditPasteFirstChild() {}
+
+cmdEditPasteFirstChild.prototype = new inBaseCommand(false);
+
+cmdEditPasteFirstChild.prototype.pastedNode = null;
+cmdEditPasteFirstChild.prototype.pastedBefore = null;
+
+cmdEditPasteFirstChild.prototype.doTransaction =
+ function PasteFirstChild_DoTransaction()
+{
+ var node = this.pastedNode || viewer.pane.panelset.getClipboardData();
+ var selected = this.pastedBefore || viewer.selectedNode;
+ if (selected) {
+ this.pastedNode = node.cloneNode(true);
+ this.pastedBefore = selected.firstChild;
+ selected.insertBefore(this.pastedNode, this.pastedBefore);
+ return false;
+ }
+ return true;
+};
+
+cmdEditPasteFirstChild.prototype.undoTransaction =
+ function PasteFirstChild_UndoTransaction()
+{
+ if (this.pastedNode) {
+ this.pastedNode.parentNode.removeChild(this.pastedNode);
+ }
+};
+
+/**
+ * Pastes the node on the clipboard as the last child of the selected node.
+ */
+function cmdEditPasteLastChild() {}
+
+cmdEditPasteLastChild.prototype = new inBaseCommand(false);
+
+cmdEditPasteLastChild.prototype.pastedNode = null;
+cmdEditPasteLastChild.prototype.selectedNode = null;
+
+cmdEditPasteLastChild.prototype.doTransaction =
+ function PasteLastChild_DoTransaction()
+{
+ var node = this.pastedNode || viewer.pane.panelset.getClipboardData();
+ var selected = this.selectedNode || viewer.selectedNode;
+ if (selected) {
+ this.pastedNode = node.cloneNode(true);
+ this.selectedNode = selected;
+ selected.appendChild(this.pastedNode);
+ return false;
+ }
+ return true;
+};
+
+cmdEditPasteLastChild.prototype.undoTransaction =
+ function PasteLastChild_UndoTransaction()
+{
+ if (this.selectedNode) {
+ this.selectedNode.removeChild(this.pastedNode);
+ }
+};
+
+/**
+ * Pastes the node on the clipboard in the place of the selected node, making
+ * the selected node its child.
+ */
+function cmdEditPasteAsParent() {}
+
+cmdEditPasteAsParent.prototype = new inBaseCommand(false);
+
+cmdEditPasteAsParent.prototype.pastedNode = null;
+cmdEditPasteAsParent.prototype.originalNode = null;
+cmdEditPasteAsParent.prototype.originalParentNode = null;
+
+cmdEditPasteAsParent.prototype.doTransaction =
+ function PasteAsParent_DoTransaction()
+{
+ var node = this.pastedNode || viewer.pane.panelset.getClipboardData();
+ var selected = this.originalNode || viewer.selectedNode;
+ var parent = this.originalParentNode || selected.parentNode;
+ if (selected) {
+ this.pastedNode = node.cloneNode(true);
+ this.originalNode = selected;
+ this.originalParentNode = parent;
+ parent.replaceChild(this.pastedNode, selected);
+ this.pastedNode.appendChild(selected);
+ return false;
+ }
+ return true;
+};
+
+cmdEditPasteAsParent.prototype.undoTransaction =
+ function PasteAsParent_UndoTransaction()
+{
+ if (this.pastedNode) {
+ this.originalParentNode.replaceChild(this.originalNode,
+ this.pastedNode);
+ }
+};
+
+/**
+ * Generic prototype for inserting a new node somewhere
+ */
+function InsertNode() {}
+
+InsertNode.prototype = new inBaseCommand(false);
+
+InsertNode.prototype.insertedNode = null;
+InsertNode.prototype.originalNode = null;
+InsertNode.prototype.attr = null;
+
+InsertNode.prototype.insertNode = function Insert_InsertNode()
+{
+};
+
+InsertNode.prototype.createNode = function Insert_CreateNode()
+{
+ var doc = this.originalNode.ownerDocument;
+ if (!this.attr) {
+ this.attr = { type: null, value: null,
+ namespaceURI: this.originalNode.namespaceURI,
+ accepted: false,
+ enableNamespaces: doc.contentType != "text/html" };
+
+ window.openDialog("chrome://inspector/content/viewers/dom/" +
+ "insertDialog.xul",
+ "insert", "chrome,modal,centerscreen", doc,
+ this.attr);
+ }
+
+ if (this.attr.accepted) {
+ switch (this.attr.type) {
+ case nsIDOMNode.ELEMENT_NODE:
+ if (this.attr.enableNamespaces) {
+ this.insertedNode = doc.createElementNS(this.attr.namespaceURI,
+ this.attr.value);
+ }
+ else {
+ this.insertedNode = doc.createElement(this.attr.value);
+ }
+ break;
+ case nsIDOMNode.TEXT_NODE:
+ this.insertedNode = doc.createTextNode(this.attr.value);
+ break;
+ }
+ return true;
+ }
+ return false;
+};
+
+InsertNode.prototype.doTransaction = function Insert_DoTransaction()
+{
+ if (this.originalNode) {
+ if (this.createNode()) {
+ this.insertNode();
+ return false;
+ }
+ }
+ return true;
+};
+
+InsertNode.prototype.undoTransaction = function Insert_UndoTransaction()
+{
+ if (this.insertedNode) {
+ this.insertedNode.parentNode.removeChild(this.insertedNode);
+ }
+};
+
+/**
+ * Inserts a node after the selected node.
+ */
+function cmdEditInsertAfter()
+{
+ this.originalNode = viewer.currentNode;
+}
+
+cmdEditInsertAfter.prototype = new InsertNode();
+
+cmdEditInsertAfter.prototype.insertNode = function InsertAfter_InsertNode()
+{
+ this.originalNode.parentNode.insertBefore(this.insertedNode,
+ this.originalNode.nextSibling);
+};
+
+/**
+ * Inserts a node before the selected node.
+ */
+function cmdEditInsertBefore()
+{
+ this.originalNode = viewer.currentNode;
+}
+
+cmdEditInsertBefore.prototype = new InsertNode();
+
+cmdEditInsertBefore.prototype.insertNode = function InsertBefore_InsertNode()
+{
+ this.originalNode.parentNode.insertBefore(this.insertedNode,
+ this.originalNode);
+};
+
+/**
+ * Inserts a node as the first child of the selected node.
+ */
+function cmdEditInsertFirstChild()
+{
+ this.originalNode = viewer.selectedNode;
+}
+
+cmdEditInsertFirstChild.prototype = new InsertNode();
+
+cmdEditInsertFirstChild.prototype.insertNode =
+ function InsertFirstChild_InsertNode()
+{
+ this.originalNode.insertBefore(this.insertedNode,
+ this.originalNode.firstChild);
+};
+
+/**
+ * Inserts a node as the last child of the selected node.
+ */
+function cmdEditInsertLastChild()
+{
+ this.originalNode = viewer.selectedNode;
+}
+
+cmdEditInsertLastChild.prototype = new InsertNode();
+
+cmdEditInsertLastChild.prototype.insertNode =
+ function InsertLastChild_InsertNode()
+{
+ this.originalNode.appendChild(this.insertedNode);
+};
+
+function cmdEditInspectInNewWindow()
+{
+ this.mObject = viewer.selectedNode;
+}
+
+cmdEditInspectInNewWindow.prototype = new cmdEditInspectInNewWindowBase();
+
+//////////////////////////////////////////////////////////////////////////////
+//// Listener Objects
+
+var MouseDownListener = {
+ handleEvent: function MDL_HandleEvent(aEvent)
+ {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+
+ var target = viewer.mDOMView.showAnonymousContent ?
+ aEvent.originalTarget :
+ aEvent.target;
+ viewer.doSelectByClick(target);
+ }
+};
+
+var EventCanceller = {
+ handleEvent: function EC_HandleEvent(aEvent)
+ {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+};
+
+var ListenerRemover = {
+ handleEvent: function LR_HandleEvent(aEvent)
+ {
+ if (!viewer.mSelecting) {
+ if (aEvent.type == "click") {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ }
+ viewer.removeClickListeners();
+ }
+ }
+};
+
+var PrefChangeObserver = {
+ observe: function PCO_Observe(aSubject, aTopic, aData)
+ {
+ viewer.onPrefChanged(aData);
+ }
+};
+
+function gColumnAddListener(aIndex)
+{
+ viewer.onColumnAdd(aIndex);
+}
+
+function gColumnRemoveListener(aIndex)
+{
+ viewer.onColumnRemove(aIndex);
+}
+
+function dumpDOM2(aNode)
+{
+ dump(DOMViewer.prototype.toXML(aNode));
+}
+
+function stubImpl(aNode)
+{
+}
diff --git a/inspector/content/viewers/dom/dom.xul b/inspector/content/viewers/dom/dom.xul
new file mode 100644
index 00000000..84902272
--- /dev/null
+++ b/inspector/content/viewers/dom/dom.xul
@@ -0,0 +1,198 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/findDialog.xul b/inspector/content/viewers/dom/findDialog.xul
new file mode 100644
index 00000000..0fd83ad9
--- /dev/null
+++ b/inspector/content/viewers/dom/findDialog.xul
@@ -0,0 +1,96 @@
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/insertDialog.js b/inspector/content/viewers/dom/insertDialog.js
new file mode 100644
index 00000000..34beaca2
--- /dev/null
+++ b/inspector/content/viewers/dom/insertDialog.js
@@ -0,0 +1,136 @@
+/* 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/. */
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var dialog;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization/Destruction
+
+window.addEventListener("load", InsertDialog_initialize, false);
+
+function InsertDialog_initialize()
+{
+ dialog = new InsertDialog();
+ dialog.initialize();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// class InsertDialog
+
+function InsertDialog()
+{
+ this.mDoc = window.arguments[0];
+ this.mData = window.arguments[1];
+
+ this.nodeType = document.getElementById("ml_nodeType");
+ this.tagName = document.getElementById("tx_tagName");
+ this.nodeValue = document.getElementById("tx_nodeValue");
+ this.namespace = document.getElementById("tx_namespace");
+ this.menulist = document.getElementById("ml_namespace");
+ this.customNS = document.getElementById("mi_custom");
+}
+
+InsertDialog.prototype =
+{
+ /**
+ * This function initializes the content of the dialog.
+ */
+ initialize: function initialize()
+ {
+ var menulist = this.menulist;
+ var menuitems = menulist.firstChild.childNodes;
+ var defaultNS = document.getElementById("mi_namespace");
+ var accept = document.documentElement.getButton("accept");
+
+ menulist.disabled = !this.mData.enableNamespaces;
+ defaultNS.value = this.mDoc.documentElement.namespaceURI;
+
+ if (this.mData.enableNamespaces) {
+ let uri = this.mData.namespaceURI;
+ menulist.value = uri;
+ if (!menulist.selectedItem) {
+ // The original node's namespace isn't one listed in the menulist.
+ this.customNS.value = uri;
+ menulist.selectedItem = this.customNS;
+ }
+ }
+ this.updateNamespace();
+ this.updateType();
+ this.tagName.focus();
+ },
+
+ /**
+ * The function that is called on accept. Sets data.
+ */
+ accept: function accept()
+ {
+ switch (this.nodeType.value)
+ {
+ case "element":
+ this.mData.type = Components.interfaces.nsIDOMNode.ELEMENT_NODE;
+ this.mData.value = this.tagName.value;
+ break;
+ case "text":
+ this.mData.type = Components.interfaces.nsIDOMNode.TEXT_NODE;
+ this.mData.value = this.nodeValue.value;
+ break;
+ }
+ this.mData.namespaceURI = this.namespace.value;
+ this.mData.accepted = true;
+ return true;
+ },
+
+ /**
+ * updateType changes the visibility of rows based on the node type.
+ */
+ updateType: function updateType()
+ {
+ switch (dialog.nodeType.value)
+ {
+ case "text":
+ document.getElementById("row_text").hidden = false;
+ document.getElementById("row_element").hidden = true;
+ break;
+ case "element":
+ document.getElementById("row_text").hidden = true;
+ document.getElementById("row_element").hidden = false;
+ break;
+ }
+ dialog.toggleAccept();
+ },
+
+ /**
+ * updateNamespace updates the namespace textbox based on the namespace menu.
+ */
+ updateNamespace: function updateNamespace()
+ {
+ this.namespace.disabled = dialog.menulist.selectedItem != this.customNS;
+ this.namespace.value = dialog.menulist.value;
+ },
+
+ /**
+ * Change the "Custom" menuitem's value to reflect the namespace textbox's
+ * value.
+ *
+ * This fires on input events, so if the user switches away from the
+ * "Custom" menuitem and then back, the previously-entered value remains.
+ */
+ updateCustom: function updateCustom()
+ {
+ this.customNS.value = this.namespace.value;
+ },
+
+ /**
+ * toggleAccept enables/disables the Accept button when there is/isn't an
+ * attribute name.
+ */
+ toggleAccept: function toggleAccept()
+ {
+ document.documentElement.getButton("accept").disabled =
+ (dialog.tagName.value == "") && (dialog.nodeType.selectedItem.value == "element");
+ }
+};
diff --git a/inspector/content/viewers/dom/insertDialog.xul b/inspector/content/viewers/dom/insertDialog.xul
new file mode 100644
index 00000000..d610f9f9
--- /dev/null
+++ b/inspector/content/viewers/dom/insertDialog.xul
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/keysetOverlay.xul b/inspector/content/viewers/dom/keysetOverlay.xul
new file mode 100644
index 00000000..e5a7b147
--- /dev/null
+++ b/inspector/content/viewers/dom/keysetOverlay.xul
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/popupOverlay.xul b/inspector/content/viewers/dom/popupOverlay.xul
new file mode 100644
index 00000000..a105ee73
--- /dev/null
+++ b/inspector/content/viewers/dom/popupOverlay.xul
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/dom/pseudoClassDialog.js b/inspector/content/viewers/dom/pseudoClassDialog.js
new file mode 100644
index 00000000..2863f37b
--- /dev/null
+++ b/inspector/content/viewers/dom/pseudoClassDialog.js
@@ -0,0 +1,97 @@
+/* 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/. */
+
+/***************************************************************
+* PseudoClassDialog --------------------------------------------
+* A dialog for choosing the pseudo-classes that should be
+* imitated on the selected element.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var dialog;
+
+//////////// global constants ////////////////////
+
+const gCheckBoxIds = {
+ cbxStateHover: 4,
+ cbxStateActive: 1,
+ cbxStateFocus: 2
+};
+/////////////////////////////////////////////////
+
+window.addEventListener("load", PseudoClassDialog_initialize, false);
+
+function PseudoClassDialog_initialize()
+{
+ dialog = new PseudoClassDialog();
+ dialog.initialize();
+}
+
+////////////////////////////////////////////////////////////////////////////
+//// class PseudoClassDialog
+
+function PseudoClassDialog()
+{
+ this.mOpener = window.opener.viewer;
+ this.mSubject = window.arguments[0];
+
+ this.mDOMUtils = XPCU.getService("@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
+}
+
+PseudoClassDialog.prototype =
+{
+
+ initialize: function()
+ {
+ if ("hasPseudoClassLock" in this.mDOMUtils) {
+ for (var key in gCheckBoxIds) {
+ var cbx = document.getElementById(key);
+ if (this.mDOMUtils.hasPseudoClassLock(this.mSubject, cbx.label)) {
+ cbx.setAttribute("checked", "true");
+ }
+ }
+ }
+ else {
+ var state = this.mDOMUtils.getContentState(this.mSubject);
+
+ for (var key in gCheckBoxIds) {
+ if (gCheckBoxIds[key] & state) {
+ var cbx = document.getElementById(key);
+ cbx.setAttribute("checked", "true");
+ }
+ }
+ }
+ },
+
+ onOk: function()
+ {
+ var el = this.mSubject;
+ var root = el.ownerDocument.documentElement;
+
+ for (var key in gCheckBoxIds) {
+ var cbx = document.getElementById(key);
+ if (cbx.checked) {
+ if ("addPseudoClassLock" in this.mDOMUtils) {
+ this.mDOMUtils.addPseudoClassLock(el, cbx.label);
+ }
+ else {
+ this.mDOMUtils.setContentState(el, gCheckBoxIds[key]);
+ }
+ }
+ else {
+ if ("removePseudoClassLock" in this.mDOMUtils) {
+ this.mDOMUtils.removePseudoClassLock(el, cbx.label);
+ }
+ else {
+ this.mDOMUtils.setContentState(root, gCheckBoxIds[key]);
+ }
+ }
+ }
+ }
+
+};
+
diff --git a/inspector/content/viewers/dom/pseudoClassDialog.xul b/inspector/content/viewers/dom/pseudoClassDialog.xul
new file mode 100644
index 00000000..672f0df7
--- /dev/null
+++ b/inspector/content/viewers/dom/pseudoClassDialog.xul
@@ -0,0 +1,42 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/domNode/domNode.js b/inspector/content/viewers/domNode/domNode.js
new file mode 100644
index 00000000..9a78668d
--- /dev/null
+++ b/inspector/content/viewers/domNode/domNode.js
@@ -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/. */
+
+/*****************************************************************************
+* DOMNodeViewer --------------------------------------------------------------
+* The default viewer for DOM Nodes
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kDOMViewCID = "@mozilla.org/inspector/dom-view;1";
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+var gPromptService;
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", DOMNodeViewer_initialize, false);
+
+function DOMNodeViewer_initialize()
+{
+ viewer = new DOMNodeViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// DOMNodeViewer Class
+
+function DOMNodeViewer() // implements inIViewer
+{
+ this.mObsMan = new ObserverManager(this);
+
+ this.mURL = window.location;
+ this.mAttrTree = document.getElementById("olAttr");
+ this.mAttrGroupBox = document.getElementById("grpAttr");
+
+ // prepare and attach the DOM DataSource
+ this.mDOMView = XPCU.createInstance(kDOMViewCID, "inIDOMView");
+ this.mDOMView.whatToShow = NodeFilter.SHOW_ATTRIBUTE;
+ this.mAttrTree.treeBoxObject.view = this.mDOMView;
+}
+
+DOMNodeViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mDOMView: null,
+ mSubject: null,
+ mPanel: null,
+
+ get selectedIndex()
+ {
+ return this.mAttrTree.currentIndex;
+ },
+
+ /**
+ * Returns an array of the selected indices
+ */
+ get selectedIndices()
+ {
+ var indices = [];
+ var rangeCount = this.mAttrTree.view.selection.getRangeCount();
+ for (var i = 0; i < rangeCount; ++i) {
+ var start = {};
+ var end = {};
+ this.mAttrTree.view.selection.getRangeAt(i, start, end);
+ for (var c = start.value; c <= end.value; ++c) {
+ indices.push(c);
+ }
+ }
+ return indices;
+ },
+
+ /**
+ * Returns a DOMAttribute from the selected index
+ */
+ get selectedAttribute()
+ {
+ var index = this.selectedIndex;
+ return index >= 0 ?
+ new DOMAttribute(this.mDOMView.getNodeFromRowIndex(index)) : null;
+ },
+
+ /**
+ * Returns an array of DOMAttributes from the selected indices
+ */
+ get selectedAttributes()
+ {
+ var indices = this.selectedIndices;
+ var attrs = [];
+ for (var i = 0; i < indices.length; ++i) {
+ var idx = this.mDOMView.getNodeFromRowIndex(indices[i]);
+ attrs.push(new DOMAttribute(idx));
+ }
+ return attrs;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// interface inIViewer
+
+ //// attributes
+
+ get uid()
+ {
+ return "domNode"
+ },
+
+ get pane()
+ {
+ return this.mPanel
+ },
+
+
+ get selection()
+ {
+ return null
+ },
+
+
+ get subject()
+ {
+ return this.mSubject
+ },
+
+ set subject(aObject)
+ {
+ // the node value's textbox won't fire onchange when we change subjects, so
+ // let's fire it. this won't do anything if it wasn't actually changed
+ viewer.pane.panelset.execCommand('cmdEditTextValue');
+
+ this.mSubject = aObject instanceof Components.interfaces.nsIDOMNode ?
+ aObject : aObject.DOMNode;
+ var deck = document.getElementById("dkContent");
+
+ switch (this.mSubject.nodeType) {
+ // things with useful nodeValues
+ case Node.TEXT_NODE:
+ case Node.CDATA_SECTION_NODE:
+ case Node.COMMENT_NODE:
+ case Node.PROCESSING_INSTRUCTION_NODE:
+ deck.selectedIndex = 1;
+ var txb = document.getElementById("txbTextNodeValue").value =
+ this.mSubject.nodeValue;
+ break;
+ //XXX this view is designed for elements, write a more useful one for
+ // document nodes, etc.
+ default:
+ var bundle = this.pane.panelset.stringBundle;
+ deck.selectedIndex = 0;
+
+ this.setTextValue("localName", this.mSubject.localName);
+ this.setTextValue("nodeType", bundle.getString(this.mSubject.nodeType));
+ this.setTextValue("namespace", this.mSubject.namespaceURI);
+ }
+
+ var hideAttributes = this.mSubject.nodeType != Node.ELEMENT_NODE;
+ this.mAttrGroupBox.hidden = hideAttributes;
+ if (!hideAttributes && this.mSubject != this.mDOMView.rootNode) {
+ this.mDOMView.rootNode = this.mSubject;
+ this.mAttrTree.view.selection.select(-1);
+ }
+
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+ },
+
+ // methods
+
+ initialize: function DNVr_Initialize(aPane)
+ {
+ this.mPanel = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function DNVr_Destroy()
+ {
+ // the node value's textbox won't fire onchange when we change views, so
+ // let's fire it. this won't do anything if it wasn't actually changed
+ viewer.pane.panelset.execCommand('cmdEditTextValue');
+ },
+
+ isCommandEnabled: function DNVr_IsCommandEnabled(aCommand)
+ {
+ // NB: This function can be fired before the subject is set.
+ switch (aCommand) {
+ case "cmdEditPaste":
+ var flavor = this.mPanel.panelset.clipboardFlavor;
+ return (flavor == "inspector/dom-attribute" ||
+ flavor == "inspector/dom-attributes");
+ case "cmdEditInsert":
+ return this.subject && this.subject.nodeType == Node.ELEMENT_NODE;
+ case "cmdEditCut":
+ case "cmdEditCopy":
+ case "cmdEditDelete":
+ return this.selectedAttribute != null;
+ case "cmdEditEdit":
+ return this.mAttrTree.currentIndex >= 0 &&
+ this.mAttrTree.view.selection.count == 1;
+ case "cmdEditTextValue":
+ if (this.subject) {
+ // something with a useful nodeValue
+ if (this.subject.nodeType == Node.TEXT_NODE ||
+ this.subject.nodeType == Node.CDATA_SECTION_NODE ||
+ this.subject.nodeType == Node.COMMENT_NODE ||
+ this.subject.nodeType == Node.PROCESSING_INSTRUCTION_NODE) {
+ // did something change?
+ return this.subject.nodeValue !=
+ document.getElementById("txbTextNodeValue").value;
+ }
+ }
+ return false;
+ }
+ return false;
+ },
+
+ getCommand: function DNVr_GetCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditCut":
+ return new cmdEditCut();
+ case "cmdEditCopy":
+ return new cmdEditCopy(this.selectedAttributes);
+ case "cmdEditPaste":
+ return new cmdEditPaste();
+ case "cmdEditInsert":
+ var command = new cmdEditInsert();
+ return command.promptFor();
+ case "cmdEditEdit":
+ var command = new cmdEditEdit();
+ return command.promptFor();
+ case "cmdEditDelete":
+ return new cmdEditDelete();
+ case "cmdEditTextValue":
+ return new cmdEditTextValue();
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Event Dispatching
+
+ addObserver: function DNVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function DNVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Uncategorized
+
+ setTextValue: function DNVr_SetTextValue(aName, aText)
+ {
+ var field = document.getElementById("tx_" + aName);
+ if (field) {
+ field.value = aText;
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// Command Objects
+
+function cmdEditCut() {}
+cmdEditCut.prototype = new inBaseCommand(false);
+
+cmdEditCut.prototype.cmdCopy = null,
+cmdEditCut.prototype.cmdDelete = null,
+
+cmdEditCut.prototype.doTransaction = function DNVr_Cut_DoTransaction()
+{
+ if (!this.cmdCopy) {
+ this.cmdDelete = new cmdEditDelete();
+ this.cmdCopy = new cmdEditCopy(viewer.selectedAttributes);
+ this.cmdCopy.doTransaction();
+ }
+ this.cmdDelete.doTransaction();
+};
+
+cmdEditCut.prototype.undoTransaction = function DVVr_Cut_UndoTransaction()
+{
+ this.cmdDelete.undoTransaction();
+};
+
+function cmdEditPaste() {}
+cmdEditPaste.prototype = new inBaseCommand(false);
+
+cmdEditPaste.prototype.pastedAttr = null;
+cmdEditPaste.prototype.previousAttrValue = null;
+cmdEditPaste.prototype.subject = null;
+cmdEditPaste.prototype.flavor = null;
+
+cmdEditPaste.prototype.doTransaction = function DNVr_Paste_DoTransaction()
+{
+ var subject, pastedAttr, flavor;
+ if (this.subject) {
+ subject = this.subject;
+ pastedAttr = this.pastedAttr;
+ flavor = this.flavor;
+ }
+ else {
+ subject = viewer.subject;
+ pastedAttr = viewer.pane.panelset.getClipboardData();
+ flavor = viewer.pane.panelset.clipboardFlavor;
+ this.pastedAttr = pastedAttr;
+ this.subject = subject;
+ this.flavor = flavor;
+ if (flavor == "inspector/dom-attributes") {
+ this.previousAttrValue = [];
+ for (var i = 0; i < pastedAttr.length; ++i) {
+ this.previousAttrValue[pastedAttr[i].node.nodeName] =
+ viewer.subject.getAttribute(pastedAttr[i].node.nodeName);
+ }
+ }
+ else if (flavor == "inspector/dom-attribute") {
+ this.previousAttrValue =
+ viewer.subject.getAttribute(pastedAttr.node.nodeName);
+ }
+ }
+
+ if (subject && pastedAttr) {
+ if (flavor == "inspector/dom-attributes") {
+ for (var i = 0; i < pastedAttr.length; ++i) {
+ subject.setAttribute(pastedAttr[i].node.nodeName,
+ pastedAttr[i].node.nodeValue);
+ }
+ }
+ else if (flavor == "inspector/dom-attribute") {
+ subject.setAttribute(pastedAttr.node.nodeName,
+ pastedAttr.node.nodeValue);
+ }
+ }
+};
+
+cmdEditPaste.prototype.undoTransaction = function DNVr_Paste_UndoTransaction()
+{
+ if (this.pastedAttr) {
+ if (this.flavor == "inspector/dom-attributes") {
+ for (var i = 0; i < this.pastedAttr.length; ++i) {
+ var attrNodeName = this.pastedAttr[i].node.nodeName;
+ if (this.previousAttrValue[attrNodeName]) {
+ this.subject.setAttribute(attrNodeName,
+ this.previousAttrValue[attrNodeName]);
+ }
+ else {
+ this.subject.removeAttribute(attrNodeName);
+ }
+ }
+ }
+ else if (this.flavor == "inspector/dom-attribute") {
+ if (this.previousAttrValue) {
+ this.subject.setAttribute(this.pastedAttr.node.nodeName,
+ this.previousAttrValue);
+ }
+ else {
+ this.subject.removeAttribute(this.pastedAttr.node.nodeName);
+ }
+ }
+ }
+};
+
+function cmdEditInsert() {}
+cmdEditInsert.prototype = new inBaseCommand(false);
+
+cmdEditInsert.prototype.attr = null;
+cmdEditInsert.prototype.subject = null;
+cmdEditInsert.prototype.name = null;
+cmdEditInsert.prototype.value = null;
+cmdEditInsert.prototype.namespaceURI = null;
+cmdEditInsert.prototype.accepted = false;
+
+cmdEditInsert.prototype.promptFor = function DNVr_Insert_PromptFor()
+{
+ var bundle = viewer.pane.panelset.stringBundle;
+ var title = bundle.getString("newAttribute.title");
+ var doc = viewer.subject.ownerDocument;
+
+ window.openDialog("chrome://inspector/content/viewers/domNode/" +
+ "domNodeDialog.xul", "insert",
+ "dialog,modal,centerscreen,resizable", this, title, doc);
+
+ this.subject = viewer.subject;
+
+ return this.accepted ? this : null;
+};
+
+cmdEditInsert.prototype.doTransaction = function DNVr_Insert_DoTransaction()
+{
+ this.subject.setAttributeNS(this.namespaceURI,
+ this.name,
+ this.value);
+};
+
+cmdEditInsert.prototype.undoTransaction =
+ function DNVr_Insert_UndoTransaction()
+{
+ if (this.subject == viewer.subject) {
+ this.subject.removeAttributeNS(this.namespaceURI,
+ this.name);
+ }
+};
+
+function cmdEditDelete() {}
+cmdEditDelete.prototype = new inBaseCommand(false);
+
+cmdEditDelete.prototype.attrs = null;
+cmdEditDelete.prototype.subject = null;
+
+cmdEditDelete.prototype.doTransaction = function DNVr_Delete_DoTransaction()
+{
+ var attrs = this.attrs ? this.attrs : viewer.selectedAttributes;
+ if (attrs) {
+ this.attrs = attrs;
+ this.subject = viewer.subject;
+ for (var i = 0; i < this.attrs.length; ++i) {
+ this.subject.removeAttribute(this.attrs[i].node.nodeName);
+ }
+ }
+};
+
+cmdEditDelete.prototype.undoTransaction =
+ function DNVr_Delete_UndoTransaction()
+{
+ if (this.attrs) {
+ for (var i = 0; i < this.attrs.length; ++i) {
+ this.subject.setAttribute(this.attrs[i].node.nodeName,
+ this.attrs[i].node.nodeValue);
+ }
+ }
+};
+
+// XXX when editing the a attribute in this document:
+// data:text/xml,
+// You only get "hi" and not the mutltiline text (windows)
+// This seems to work on Linux, but not very usable
+function cmdEditEdit() {}
+cmdEditEdit.prototype = new inBaseCommand(false);
+
+cmdEditEdit.prototype.subject = null;
+cmdEditEdit.prototype.name = null;
+cmdEditEdit.prototype.value = null;
+cmdEditEdit.prototype.namespaceURI = null;
+cmdEditEdit.prototype.previousValue = null;
+cmdEditEdit.prototype.previousNamespaceURI = null;
+cmdEditEdit.prototype.accepted = false;
+
+cmdEditEdit.prototype.promptFor = function DNVr_Edit_PromptFor()
+{
+ var attr = viewer.selectedAttribute.node;
+ if (!attr) {
+ return null;
+ }
+ var bundle = viewer.pane.panelset.stringBundle;
+ var title = bundle.getString("editAttribute.title");
+ var doc = attr.ownerDocument;
+
+ this.subject = viewer.subject;
+ this.name = attr.nodeName;
+ this.previousValue = attr.nodeValue;
+ this.previousNamespaceURI = attr.namespaceURI;
+ this.value = this.previousValue;
+ this.namespaceURI = this.previousNamespaceURI;
+
+ window.openDialog("chrome://inspector/content/viewers/domNode/" +
+ "domNodeDialog.xul", "edit",
+ "dialog,modal,centerscreen,resizable", this, title, doc);
+
+ return this.accepted ? this : null;
+};
+
+cmdEditEdit.prototype.doTransaction = function DNVr_Edit_DoTransaction()
+{
+ if (this.previousNamespaceURI == this.namespaceURI) {
+ this.subject.setAttributeNS(this.previousNamespaceURI,
+ this.name,
+ this.value);
+ }
+ else {
+ this.subject.removeAttributeNS(this.previousNamespaceURI,
+ this.name);
+ this.subject.setAttributeNS(this.namespaceURI,
+ this.name,
+ this.value);
+ }
+};
+
+cmdEditEdit.prototype.undoTransaction = function DNVr_Edit_UndoTransaction()
+{
+ if (this.previousNamespaceURI == this.namespaceURI) {
+ this.subject.setAttributeNS(this.previousNamespaceURI,
+ this.name,
+ this.previousValue);
+ }
+ else {
+ this.subject.removeAttributeNS(this.namespaceURI,
+ this.name);
+ this.subject.setAttributeNS(this.previousNamespaceURI,
+ this.name,
+ this.previousValue);
+ }
+};
+
+/**
+ * Handles editing of text nodes.
+ */
+function cmdEditTextValue() {
+ this.newValue = document.getElementById("txbTextNodeValue").value;
+ this.subject = viewer.subject;
+ this.previousValue = this.subject.nodeValue;
+}
+
+cmdEditTextValue.prototype = new inBaseCommand(false);
+
+cmdEditTextValue.prototype.doTransaction =
+ function DNVr_EditText_DoTransaction()
+{
+ this.subject.nodeValue = this.newValue;
+};
+
+cmdEditTextValue.prototype.undoTransaction =
+ function DNVr_EditText_UndoTransaction()
+{
+ this.subject.nodeValue = this.previousValue;
+ this.refreshView();
+};
+
+cmdEditTextValue.prototype.redoTransaction =
+ function DNVr_EditText_RedoTransaction()
+{
+ this.doTransaction();
+ this.refreshView();
+};
+
+cmdEditTextValue.prototype.refreshView = function DNVr_EditText_RefreshView()
+{
+ // if we're still on the same subject, update the textbox
+ if (viewer.subject == this.subject) {
+ document.getElementById("txbTextNodeValue").value =
+ this.subject.nodeValue;
+ }
+};
diff --git a/inspector/content/viewers/domNode/domNode.xul b/inspector/content/viewers/domNode/domNode.xul
new file mode 100644
index 00000000..646138cb
--- /dev/null
+++ b/inspector/content/viewers/domNode/domNode.xul
@@ -0,0 +1,135 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/domNode/domNodeDialog.js b/inspector/content/viewers/domNode/domNodeDialog.js
new file mode 100644
index 00000000..39831c29
--- /dev/null
+++ b/inspector/content/viewers/domNode/domNodeDialog.js
@@ -0,0 +1,101 @@
+/* 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/. */
+
+///////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var dialog;
+
+///////////////////////////////////////////////////////////////////////////////
+//// Initialization/Destruction
+
+window.addEventListener("load", DomNodeDialog_initialize, false);
+
+function DomNodeDialog_initialize()
+{
+ dialog = new DomNodeDialog();
+ dialog.initialize();
+}
+
+///////////////////////////////////////////////////////////////////////////////
+//// class DomNodeDialog
+
+function DomNodeDialog()
+{
+ this.mData = window.arguments[0];
+ this.mTitle = window.arguments[1];
+ this.mDoc = window.arguments[2];
+
+ this.nodeName = document.getElementById("tx_nodeName");
+ this.nodeValue = document.getElementById("tx_nodeValue");
+ this.namespace = document.getElementById("tx_namespace");
+ this.menulist = document.getElementById("ml_namespace");
+}
+
+DomNodeDialog.prototype =
+{
+ /**
+ * This function initializes the content of the dialog.
+ */
+ initialize: function initialize()
+ {
+ document.title = this.mTitle;
+ var defaultNS = document.getElementById("mi_namespace");
+ var customNS = document.getElementById("mi_custom");
+ var accept = document.documentElement.getButton("accept");
+
+ accept.disabled = this.mData.name == null;
+ this.nodeName.value = this.mData.name || "";
+ this.nodeName.disabled = this.mData.name != null;
+ this.nodeValue.value = this.mData.value || "";
+ this.menulist.disabled = !this.enableNamespaces();
+ defaultNS.value = this.mDoc.documentElement.namespaceURI;
+ customNS.value = this.mData.namespaceURI;
+ this.menulist.value = this.mData.namespaceURI || "";
+
+ this.toggleNamespace();
+ },
+
+ /**
+ * The function that is called on accept. Sets data.
+ */
+ accept: function accept()
+ {
+ this.mData.name = this.nodeName.value;
+ this.mData.value = this.nodeValue.value;
+ this.mData.namespaceURI = this.namespace.value;
+ this.mData.accepted = true;
+ return true;
+ },
+
+ /**
+ * toggleNamespace toggles the namespace textbox based on the namespace menu.
+ */
+ toggleNamespace: function toggleNamespace()
+ {
+ dialog.namespace.disabled = dialog.menulist.selectedItem.id != "mi_custom";
+ dialog.namespace.value = dialog.menulist.value;
+ },
+
+ /**
+ * enableNamespaces determines if the document accepts namespaces or not
+ *
+ * @return True if the document can have namespaced attributes, false
+ * otherwise.
+ */
+ enableNamespaces: function enableNamespaces()
+ {
+ return this.mDoc.contentType != "text/html";
+ },
+
+ /**
+ * toggleAccept enables/disables the Accept button when there is/isn't an
+ * attribute name.
+ */
+ toggleAccept: function toggleAccept()
+ {
+ document.documentElement.getButton("accept").disabled =
+ dialog.nodeName.value == "";
+ }
+};
diff --git a/inspector/content/viewers/domNode/domNodeDialog.xul b/inspector/content/viewers/domNode/domNodeDialog.xul
new file mode 100644
index 00000000..26fb042e
--- /dev/null
+++ b/inspector/content/viewers/domNode/domNodeDialog.xul
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/jsObject/evalExprDialog.js b/inspector/content/viewers/jsObject/evalExprDialog.js
new file mode 100644
index 00000000..ee9b4405
--- /dev/null
+++ b/inspector/content/viewers/jsObject/evalExprDialog.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+/*****************************************************************************
+* EvalExprDialog -------------------------------------------------------------
+* A dialog for entering javascript expression to evaluate and view in the JS
+* Object Viewer.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+*****************************************************************************/
+
+var gViewer = window.arguments[0];
+var gTarget = window.arguments[1];
+
+/**
+ * Executes the JavaScript expression entered by the user.
+ */
+function execute()
+{
+ var txf = document.getElementById("txfExprInput");
+ var rad = document.getElementById("inspect-new-window");
+ try {
+ gViewer.doEvalExpr(txf.value, gTarget, rad.selected);
+ }
+ catch (ex) {
+ // alert the user of an error in their expression, and don't close
+ let svc = XPCU.getService("@mozilla.org/embedcomp/prompt-service;1",
+ "nsIPromptService");
+ svc.alert(window, ex.name, ex.message);
+
+ return false;
+ }
+ return true;
+}
diff --git a/inspector/content/viewers/jsObject/evalExprDialog.xul b/inspector/content/viewers/jsObject/evalExprDialog.xul
new file mode 100644
index 00000000..0e29e753
--- /dev/null
+++ b/inspector/content/viewers/jsObject/evalExprDialog.xul
@@ -0,0 +1,42 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+ &jsEval.desc;
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/jsObject/jsObject.js b/inspector/content/viewers/jsObject/jsObject.js
new file mode 100644
index 00000000..e8fe99df
--- /dev/null
+++ b/inspector/content/viewers/jsObject/jsObject.js
@@ -0,0 +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/. */
+
+/***************************************************************
+* JSObjectViewer --------------------------------------------
+* The viewer for all facets of a javascript object.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+****************************************************************/
+
+//////////// global variables /////////////////////
+
+var viewer;
+var bundle;
+
+//////////////////////////////////////////////////
+
+window.addEventListener("load", JSObjectViewer_initialize, false);
+
+function JSObjectViewer_initialize()
+{
+ bundle = document.getElementById("inspector-bundle");
+ viewer = new JSObjectViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
diff --git a/inspector/content/viewers/jsObject/jsObject.xul b/inspector/content/viewers/jsObject/jsObject.xul
new file mode 100644
index 00000000..5b2111e4
--- /dev/null
+++ b/inspector/content/viewers/jsObject/jsObject.xul
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/jsObject/jsObjectViewer.js b/inspector/content/viewers/jsObject/jsObjectViewer.js
new file mode 100644
index 00000000..149eea24
--- /dev/null
+++ b/inspector/content/viewers/jsObject/jsObjectViewer.js
@@ -0,0 +1,617 @@
+/* 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/. */
+
+/*****************************************************************************
+* JSObjectViewer -------------------------------------------------------------
+* The viewer for all facets of a javascript object.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/hooks.js
+* chrome://inspector/content/jsutil/events/ObserverManager.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kClipboardHelperCID = "@mozilla.org/widget/clipboardhelper;1";
+
+//////////////////////////////////////////////////////////////////////////////
+//// Class JSObjectViewer
+
+function JSObjectViewer()
+{
+ this.mObsMan = new ObserverManager(this);
+}
+
+JSObjectViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPane: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// interface inIViewer
+
+ get uid()
+ {
+ return "jsObject";
+ },
+
+ get pane()
+ {
+ return this.mPane;
+ },
+
+ get selection()
+ {
+ return this.mSelection;
+ },
+
+ get subject()
+ {
+ return this.mSubject;
+ },
+
+ set subject(aObject)
+ {
+ var object =
+ "@mozilla.org/accessibleRetrieval;1" in Components.classes &&
+ aObject instanceof Components.interfaces.nsIAccessible ?
+ aObject.DOMNode : aObject;
+
+ this.setSubject(object);
+ },
+
+ // The accessibleObject viewer extends JSObjectViewer. This method is here
+ // (and not just inlined above) so that the accessibleObject viewer can get
+ // access to this function without having to use __lookupSetter__ before
+ // overriding with its own subject setter.
+ setSubject: function JSOVr_SetSubject(aObject)
+ {
+ this.mSubject = this.unwrapObject(aObject);
+ this.mView = new JSObjectView(this.mSubject);
+ this.mTree.view = this.mView;
+
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+
+ // If the user has just switched to us from another viewer in the document
+ // pane and we don't set the selection below, the object pane will
+ // continue to show whatever now-irrelevant thing it was showing before.
+ this.mView.selection.select(0);
+ this.mView.toggleOpenState(0);
+ },
+
+ initialize: function JSOVr_Initialize(aPane)
+ {
+ this.mPane = aPane;
+ this.mTree = document.getElementById("treeJSObject");
+
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function JSOVr_Destroy()
+ {
+ },
+
+ isCommandEnabled: function JSOVr_IsCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdCopyValue":
+ case "cmdEvalExpr":
+ return this.getSelectedCount() == 1;
+ case "cmdEditInspectInNewWindow":
+ if (this.getSelectedCount() != 1) {
+ return false;
+ }
+ let obj = this.getSelectedObject();
+ return cmdEditInspectInNewWindowBase.isInspectable(obj);
+ }
+ return false;
+ },
+
+ getCommand: function JSOVr_GetCommand(aCommand)
+ {
+ if (aCommand in window) {
+ return new window[aCommand]();
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Event Dispatching
+
+ addObserver: function JSOVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function JSOVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// UI Commands
+
+ cmdCopyValue: function JSOVr_CmdCopyValue()
+ {
+ if (this.getSelectedCount() != 1) {
+ return;
+ }
+
+ var obj = this.getSelectedObject();
+ var helper = XPCU.getService(kClipboardHelperCID, "nsIClipboardHelper");
+ helper.copyString(obj);
+ },
+
+ cmdEvalExpr: function JSOVr_CmdEvalExpr()
+ {
+ if (this.getSelectedCount() != 1) {
+ return;
+ }
+
+ var obj = this.getSelectedObject();
+ openDialog("chrome://inspector/content/viewers/jsObject/evalExprDialog.xul",
+ "_blank", "chrome", this, obj);
+ },
+
+ doEvalExpr: function JSOVr_DoEvalExpr(aExpr, aTarget, aNewView)
+ {
+ // TODO: I should really write some C++ code to execute the js code in the
+ // js context of the inspected window
+
+ try {
+ var f = Function("target", aExpr);
+ var result = f(aTarget);
+
+ if (result) {
+ if (aNewView) {
+ inspectObject(result);
+ }
+ else {
+ this.subject = result;
+ }
+ }
+ }
+ catch (ex) {
+ dump("Error in expression.\n");
+ throw (ex);
+ }
+ },
+
+ getSelectedCount: function JSOVr_GetSelectedCount()
+ {
+ return this.mView.selection.count;
+ },
+
+ getSelectedObject: function JSOVr_GetSelectedObject()
+ {
+ if (this.getSelectedCount() != 1) {
+ throw new Error("Selection count not 1");
+ }
+ return this.mView.getSelectedRowObjects()[0];
+ },
+
+ onTreeSelectionChange: function JSOVr_OnTreeSelectionChange()
+ {
+ // NB: This function gets called on selection *and* deselection.
+ var view = this.mView;
+ var currentIndex = view.selection.currentIndex;
+ var currentValue = view.getRowObjectFromIndex(currentIndex);
+
+ if (view.selection.isSelected(currentIndex)) {
+ this.changeSelection(currentValue);
+ }
+ // Otherwise, the row at currentIndex was deselected. If there are other
+ // rows selected, use the nearest one for mSelection. If not, we'll leave
+ // mSelection alone and won't dispatch any event; if there's an object
+ // panel linked to ours, just let it keep inspecting the value from the
+ // deselected row.
+ else if (this.mSelection == currentValue && view.selection.count) {
+ var nearestSelectedIndex =
+ InsUtil.getNearestIndex(currentIndex, view.getSelectedIndices());
+ this.changeSelection(view.getRowObjectFromIndex(nearestSelectedIndex));
+ }
+
+ this.updateAllCommands();
+ },
+
+ changeSelection: function JSOVr_ChangeSelection(aVal)
+ {
+ this.mSelection = aVal;
+ this.mObsMan.dispatchEvent("selectionChange", { selection: aVal });
+ },
+
+ updateAllCommands: function JSOVr_UpdateAllCommands()
+ {
+ this.pane.panelset.updateAllCommands();
+
+ // There's no need to worry about any other commands outside this
+ // commandset; cmdInspectInNewWindow is global, so it just got updated.
+ var commands = document.getElementById("cmdsJSObjectViewer").childNodes;
+ for (let i = 0, n = commands.length; i < n; ++i) {
+ let command = commands[i];
+ if (this.isCommandEnabled(command.id)) {
+ command.removeAttribute("disabled");
+ }
+ else {
+ command.setAttribute("disabled", true);
+ }
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Miscellaneous Utility Methods
+
+ unwrapObject: function JSOVr_UnwrapObject(aObject)
+ {
+ /* unwrap() throws for primitive values, so don't call it for those */
+ if (typeof(aObject) === "object" && aObject) {
+ aObject = XPCNativeWrapper.unwrap(aObject);
+ }
+ return aObject;
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// JSObjectView
+
+function JSObjectView(aObject)
+{
+ this.mKeys = [bundle.getString("root.title")];
+ this.mValues = [aObject];
+ this.mValueStrings = [this.jsValueToString(aObject)];
+
+ this.mLevels = [0];
+ this.mOpenStates = [false];
+
+ this.mRowCount = 1;
+}
+
+JSObjectView.prototype = new inBaseTreeView();
+
+JSObjectView.prototype.mLevels = null;
+JSObjectView.prototype.mValues = null;
+JSObjectView.prototype.mValueStrings = null;
+JSObjectView.prototype.mLevels = null;
+JSObjectView.prototype.mOpenStates = null;
+
+JSObjectView.prototype.jsValueToString = function JSOV_JSValueToString(aVal)
+{
+ var str;
+ try {
+ str = String(aVal);
+ }
+ catch (ex) {
+ str = Object.prototype.toString.call(aVal);
+ }
+
+ if (typeof(aVal) == "string") {
+ str = "\"" + str + "\"";
+ }
+
+ return str;
+};
+
+/**
+ * Sort the keys for an object into the following order:
+ * - constants with numeric values sorted numerically by value
+ * - sorted alphanumerically by key in the event of a tie
+ * - constants with non-numeric values sorted alphanumerically by key
+ * - other numeric key names (e.g., array indices) sorted numerically
+ * - other key names sorted alphanumerically
+ * @param aObject
+ * The object whose keys we're sorting.
+ * @param aKeys
+ * The list of property names of aObject being sorted.
+ */
+JSObjectView.prototype.sortKeys = function JSOV_SortKeys(aObject, aKeys)
+{
+ /**
+ * A sort comparator for numeric values. Numerics come before non-numerics.
+ * If both parameters are non-numeric, returns 0.
+ */
+ var numericSortComparator =
+ function JSOV_SortKeys_NumericSortComparator(a, b)
+ {
+ if (isNaN(a)) {
+ return isNaN(b) ? 0 : 1;
+ }
+ if (isNaN(b)) {
+ return -1;
+ }
+ return a - b;
+ };
+
+ var keySortComparator = function JSOV_SortKeys_KeySortComparator(a, b)
+ {
+ var aIsConstant = a == a.toUpperCase() && isNaN(a);
+ var bIsConstant = b == b.toUpperCase() && isNaN(b);
+ // constants come first
+ if (aIsConstant) {
+ if (bIsConstant) {
+ // both are constants. sort by numeric value, then non-numeric name
+ return numericSortComparator(aObject[a], aObject[b]) ||
+ a.localeCompare(b);
+ }
+ // a is constant, b is not
+ return -1;
+ }
+ if (bIsConstant) {
+ // b is constant, a is not
+ return 1;
+ }
+ // neither are constants. go by numeric property name, then non-numeric
+ // property name
+ return numericSortComparator(a, b) || a.localeCompare(b);
+ };
+
+ aKeys.sort(keySortComparator);
+};
+
+/**
+ * Get the number of rows that are descendants of the given row.
+ * @param aIndex
+ * The index of the row.
+ * @return The number of descendants, as above.
+ */
+JSObjectView.prototype.getDescendantCount =
+ function JSOV_GetDescendantCount(aIndex)
+{
+ if (this.checkForBadIndex(aIndex)) {
+ return 0;
+ }
+
+ var level = this.mLevels[aIndex];
+ var currentIndex = aIndex + 1;
+ var rowCount = this.mRowCount;
+ while (this.mLevels[currentIndex] > level && currentIndex < rowCount) {
+ ++currentIndex;
+ }
+
+ return currentIndex - aIndex - 1;
+};
+
+JSObjectView.prototype.getRowObjectFromIndex =
+ function JSOV_GetRowObjectFromIndex(aIndex)
+{
+ if (this.checkForBadIndex(aIndex)) {
+ throw new RangeError("Invalid index " + aIndex);
+ }
+
+ return this.mValues[aIndex];
+}
+
+JSObjectView.prototype.collapseRow = function JSOV_CollapseRow(aIndex)
+{
+ var rowsDeleted = this.getDescendantCount(aIndex);
+ if (rowsDeleted) {
+ let after = aIndex + 1;
+ this.mKeys.splice(after, rowsDeleted);
+ this.mValues.splice(after, rowsDeleted);
+ this.mValueStrings.splice(after, rowsDeleted);
+ this.mOpenStates.splice(after, rowsDeleted);
+ this.mLevels.splice(after, rowsDeleted);
+ }
+ return rowsDeleted;
+};
+
+JSObjectView.prototype.expandRow = function JSOV_ExpandRow(aIndex)
+{
+ var insertedKeys = [];
+ var insertedValues = [];
+ var insertedValueStrings = [];
+ var insertedOpenStates = [];
+ var insertedLevels = [];
+
+ // Get the new keys.
+ var obj = this.mValues[aIndex];
+ for (let key in obj) {
+ // Not pretty, but we need some way to weed out properties that throw.
+ // It's not as simple as just going ahead and caching the values now,
+ // because when we sort the keys, we'd lose the correspondence between
+ // array indices.
+ try {
+ let val = obj[key];
+ insertedKeys.push(key);
+ }
+ catch (ex) {
+ // Faked properties throw NOT_YET_IMPLEMENTED. Discard them.
+ }
+ }
+ this.sortKeys(obj, insertedKeys);
+
+ // Get the new data.
+ var rowsInserted = insertedKeys.length;
+ var level = this.mLevels[aIndex] + 1;
+ for (let i = 0; i < rowsInserted; ++i) {
+ let val = viewer.unwrapObject(obj[insertedKeys[i]]);
+ insertedValues.push(val);
+ insertedValueStrings.push(this.jsValueToString(val));
+ insertedOpenStates.push(false);
+ insertedLevels.push(level);
+ }
+
+ // Splice in everything.
+ var after = aIndex + 1;
+ this.spliceFrom(this.mKeys, after, insertedKeys);
+ this.spliceFrom(this.mValues, after, insertedValues);
+ this.spliceFrom(this.mValueStrings, after, insertedValueStrings);
+ this.spliceFrom(this.mOpenStates, after, insertedOpenStates);
+ this.spliceFrom(this.mLevels, after, insertedLevels);
+
+ return rowsInserted;
+};
+
+/**
+ * Splice elements copied from one array into another at the given index.
+ * There is no way to specify that any elements should be removed.
+ * @param aDestination
+ * The array the data should be spliced into.
+ * @param aIndex
+ * The index into aDestination that the data should be copied to.
+ * @param aSource
+ * The array that should be copied into aDestination at aIndex.
+ */
+JSObjectView.prototype.spliceFrom =
+ function JSOV_SpliceFrom(aDestination, aIndex, aSource)
+{
+ Array.prototype.splice.apply(aDestination, ([aIndex, 0]).concat(aSource));
+};
+
+/**
+ * Check if the purported row is outside the range of valid row indexes.
+ * @param aIndex
+ * The index of the given row.
+ * @return true iff aIndex is outside the range
+ */
+JSObjectView.prototype.checkForBadIndex =
+ function JSOV_CheckForBadIndex(aIndex)
+{
+ if (aIndex < 0 || aIndex >= this.mRowCount) {
+ Components.utils.reportError("Bad index");
+ return true;
+ }
+
+ return false;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// JSObjectView nsITreeView Implementation
+
+JSObjectView.prototype.toggleOpenState = function JSOV_ToggleOpenState(aIndex)
+{
+ if (this.isContainerEmpty(aIndex)) {
+ return;
+ }
+
+ var rowCountChange = 0;
+ var isOpen = this.mOpenStates[aIndex];
+ if (isOpen) {
+ rowCountChange = -this.collapseRow(aIndex);
+ }
+ else {
+ rowCountChange = this.expandRow(aIndex);
+ }
+ this.mOpenStates[aIndex] = !isOpen;
+
+ this.mRowCount += rowCountChange;
+
+ // Notify the box object.
+ var bo = this.mTree;
+ if (bo) {
+ bo.rowCountChanged(aIndex + 1, rowCountChange);
+ bo.invalidateRow(aIndex);
+ }
+};
+
+JSObjectView.prototype.getCellText = function JSOV_GetCellText(aIndex, aCol)
+{
+ if (this.checkForBadIndex(aIndex)) {
+ return "";
+ }
+
+ switch (aCol.id) {
+ case "colProp":
+ return this.mKeys[aIndex];
+ case "colVal":
+ return this.mValueStrings[aIndex];
+ }
+ return "";
+};
+
+JSObjectView.prototype.getParentIndex = function JSOV_GetParentIndex(aIndex)
+{
+ if (this.checkForBadIndex(aIndex) || aIndex == 0) {
+ return -1;
+ }
+
+ var parentLevel = this.mLevels[aIndex] - 1;
+ for (let i = aIndex - 1; i >= 0; --i) {
+ if (this.mLevels[i] == parentLevel) {
+ return i;
+ }
+ }
+
+ Components.utils.reportError("Unrooted rows present");
+ return -1;
+};
+
+JSObjectView.prototype.hasNextSibling =
+ function JSOV_HasNextSibling(aIndex, aAfterIndex)
+{
+ if (this.checkForBadIndex(aIndex)) {
+ return false;
+ }
+
+ var level = this.mLevels[aIndex];
+ for (let i = aAfterIndex + 1, n = this.mRowCount; i < n; ++i) {
+ if (this.mLevels[i] == level) {
+ return true;
+ }
+ if (this.mLevels[i] < level) {
+ break;
+ }
+ }
+
+ return false;
+};
+
+JSObjectView.prototype.getLevel = function JSOV_GetLevel(aIndex)
+{
+ if (this.checkForBadIndex(aIndex)) {
+ return -1;
+ }
+
+ return this.mLevels[aIndex];
+};
+
+JSObjectView.prototype.isContainer = function JSOV_IsContainer(aIndex)
+{
+ if (this.checkForBadIndex(aIndex)) {
+ return false;
+ }
+
+ return cmdEditInspectInNewWindowBase.isInspectable(this.mValues[aIndex]);
+};
+
+JSObjectView.prototype.isContainerEmpty =
+ function JSOV_IsContainerEmpty(aIndex)
+{
+ if (!this.isContainer(aIndex)) {
+ return true;
+ }
+
+ var val = this.mValues[aIndex];
+ for (let key in val) {
+ return false;
+ }
+
+ return true;
+};
+
+JSObjectView.prototype.isContainerOpen = function JSOV_IsContainerOpen(aIndex)
+{
+ if (!this.isContainer(aIndex)) {
+ return false;
+ }
+
+ return this.mOpenStates[aIndex];
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Transactions
+
+function cmdEditInspectInNewWindow()
+{
+ if (viewer.getSelectedCount() == 1) {
+ this.mObject = viewer.getSelectedObject();
+ }
+}
+
+cmdEditInspectInNewWindow.prototype = new cmdEditInspectInNewWindowBase();
diff --git a/inspector/content/viewers/jsObject/jsObjectViewer.xul b/inspector/content/viewers/jsObject/jsObjectViewer.xul
new file mode 100644
index 00000000..edf6ca59
--- /dev/null
+++ b/inspector/content/viewers/jsObject/jsObjectViewer.xul
@@ -0,0 +1,60 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/styleRules/commandOverlay.xul b/inspector/content/viewers/styleRules/commandOverlay.xul
new file mode 100644
index 00000000..42f8d580
--- /dev/null
+++ b/inspector/content/viewers/styleRules/commandOverlay.xul
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/styleRules/keysetOverlay.xul b/inspector/content/viewers/styleRules/keysetOverlay.xul
new file mode 100644
index 00000000..d1d681ef
--- /dev/null
+++ b/inspector/content/viewers/styleRules/keysetOverlay.xul
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/styleRules/popupOverlay.xul b/inspector/content/viewers/styleRules/popupOverlay.xul
new file mode 100644
index 00000000..ceccb3ff
--- /dev/null
+++ b/inspector/content/viewers/styleRules/popupOverlay.xul
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/styleRules/styleRules.js b/inspector/content/viewers/styleRules/styleRules.js
new file mode 100644
index 00000000..1b482fef
--- /dev/null
+++ b/inspector/content/viewers/styleRules/styleRules.js
@@ -0,0 +1,809 @@
+/* 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/. */
+
+/*****************************************************************************
+* StyleRulesViewer -----------------------------------------------------------
+* The viewer for CSS style rules that apply to a DOM element.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/jsutil/rdf/RDFU.js
+* chrome://global/content/viewSourceUtils.js
+* chrome://inspector/content/jsutil/commands/baseCommands.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+var gPromptService;
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kDOMUtilsCID = "@mozilla.org/inspector/dom-utils;1";
+const kPromptServiceCID = "@mozilla.org/embedcomp/prompt-service;1";
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", StyleRulesViewer_initialize, false);
+
+function StyleRulesViewer_initialize()
+{
+ viewer = new StyleRulesViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+
+ gPromptService = XPCU.getService(kPromptServiceCID, "nsIPromptService");
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Class StyleRulesViewer
+
+function StyleRulesViewer() // implements inIViewer
+{
+ this.mObsMan = new ObserverManager(this);
+
+ this.mURL = window.location;
+ this.mRuleTree = document.getElementById("olStyleRules");
+ this.mRuleBoxObject = this.mRuleTree.treeBoxObject;
+ this.mPropsTree = document.getElementById("olStyleProps");
+ this.mPropsBoxObject = this.mPropsTree.treeBoxObject;
+ this.mFocusedTree = null;
+ this.mDOMUtils = XPCU.getService(kDOMUtilsCID, "inIDOMUtils");
+}
+
+StyleRulesViewer.prototype =
+{
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPanel: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Interface inIViewer
+
+ get uid()
+ {
+ return "styleRules"
+ },
+
+ get pane()
+ {
+ return this.mPanel
+ },
+
+ get selection()
+ {
+ return null
+ },
+
+ get subject()
+ {
+ return this.mSubject
+ },
+
+ set subject(aObject)
+ {
+ this.mSubject =
+ ("@mozilla.org/accessibleRetrieval;1" in Components.classes &&
+ aObject instanceof Components.interfaces.nsIAccessible) ?
+ aObject.DOMNode : aObject;
+
+ // update the rule tree
+ this.mRuleView = new StyleRuleView(this.mSubject);
+ this.mRuleBoxObject.view = this.mRuleView;
+ // clear the props tree
+ this.mPropsTree.disabled = true;
+ this.mPropsTree.contextMenu = null;
+ this.mPropsView = null;
+ this.mPropsBoxObject.view = null;
+
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+ },
+
+ initialize: function SRVr_Initialize(aPane)
+ {
+ this.mPanel = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function SRVr_Destroy()
+ {
+ // We need to remove the views at this time or else they will attempt to
+ // re-paint while the document is being deconstructed, resulting in
+ // some nasty XPConnect assertions
+ this.mRuleBoxObject.view = null;
+ this.mPropsBoxObject.view = null;
+ },
+
+ isCommandEnabled: function SRVr_IsCommandEnabled(aCommand)
+ {
+ var rule = this.getSelectedRule();
+ var fileURI = rule && rule.parentStyleSheet && rule.parentStyleSheet.href;
+ // XXX can't edit resource: stylesheets because of bug 343508, and
+ // CSSFontFaceRules because of bug 443978
+ var isEditable = !(/^resource:/.test(fileURI) ||
+ rule instanceof CSSFontFaceRule);
+
+ var propFocus = this.mFocusedTree == this.mPropsTree;
+ var propCount = this.mPropsTree.view.selection.count;
+
+ switch (aCommand) {
+ // ppStylePropsContext
+ // The first three of these are context-sensitive; until they are
+ // supported for the rule pane, they are meaningless when it has focus.
+ case "cmdEditCopy":
+ return propFocus && propCount > 0;
+ case "cmdEditDelete":
+ return isEditable && propFocus && propCount > 0;
+ case "cmdEditInsert":
+ return isEditable && propFocus;
+ case "cmdTogglePriority":
+ return isEditable && propCount > 0;
+ case "cmdEditEdit":
+ return isEditable && propCount == 1;
+ // ppStyleRulesContext
+ case "cmdEditCopyFileURI":
+ case "cmdEditViewFileURI":
+ return !!fileURI;
+ }
+ return false;
+ },
+
+ getCommand: function SRVr_GetCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditCopy":
+ return new cmdEditCopy(this.mPropsView.getSelectedRowObjects());
+ case "cmdEditDelete":
+ return new cmdEditDelete(this.getSelectedDec(),
+ this.mPropsView.getSelectedRowObjects());
+ case "cmdEditInsert":
+ var bundle = this.mPanel.panelset.stringBundle;
+ var msg = bundle.getString("styleRulePropertyName.message");
+ var title = bundle.getString("styleRuleNewProperty.title");
+
+ var property = { value: "" };
+ var value = { value: "" };
+ var dummy = { value: false };
+
+ if (!gPromptService.prompt(window, title, msg, property, null,
+ dummy)) {
+ return null;
+ }
+
+ msg = bundle.getString("styleRulePropertyValue.message");
+ if (!gPromptService.prompt(window, title, msg, value, null, dummy)) {
+ return null;
+ }
+
+ return new cmdEditInsert(this.getSelectedDec(), property.value,
+ value.value, "");
+ case "cmdEditEdit":
+ var rule = this.getSelectedDec();
+ var property = this.getSelectedProp();
+ var priority = rule.getPropertyPriority(property);
+
+ var bundle = this.mPanel.panelset.stringBundle;
+ var msg = bundle.getString("styleRulePropertyValue.message");
+ var title = bundle.getString("styleRuleEditProperty.title");
+
+ var value = { value: rule.getPropertyValue(property) };
+ var dummy = { value: false };
+
+ if (!gPromptService.prompt(window, title, msg, value, null, dummy)) {
+ return null;
+ }
+
+ return new cmdEditEdit(rule, property, value.value, priority);
+ case "cmdTogglePriority":
+ return new cmdTogglePriority(this.getSelectedDec(),
+ this.mPropsView.getSelectedRowObjects());
+ case "cmdEditCopyFileURI":
+ return new cmdEditCopyFileURI(this.getSelectedRule());
+ case "cmdEditViewFileURI":
+ return new cmdEditViewFileURI(this.getSelectedRule());
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Event Dispatching
+
+ addObserver: function SRVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function SRVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Uncategorized
+
+ get DOMUtils() {
+ return this.mDOMUtils;
+ },
+
+ getSelectedDec: function SRVr_GetSelectedDec()
+ {
+ var idx = this.mRuleTree.currentIndex;
+ return this.mRuleView.selection.count == 1 ?
+ this.mRuleView.getDecAt(idx) :
+ null;
+ },
+
+ getSelectedProp: function SRVr_GetSelectedProp()
+ {
+ if (this.mPropsView.selection.count != 1) {
+ return null;
+ }
+ var dec = this.getSelectedDec();
+ // API awkwardness
+ var min = {}, max = {};
+ this.mPropsView.selection.getRangeAt(0, min, max);
+ return dec.item(min.value);
+ },
+
+ getSelectedRule: function SRVr_GetSelectedRule()
+ {
+ var idx = this.mRuleTree.currentIndex;
+ return this.mRuleView.selection.count == 1 ?
+ this.mRuleView.getRuleAt(idx) :
+ null;
+ },
+
+ onRuleSelect: function SRVr_OnRuleSelect()
+ {
+ var dec = this.getSelectedDec();
+ this.mPropsView = new StylePropsView(dec);
+ this.mPropsBoxObject.view = this.mPropsView;
+ viewer.pane.panelset.updateAllCommands();
+ // for non-style rules, change props tree depending on its relevance
+ this.mPropsTree.disabled = !dec;
+ this.mPropsTree.contextMenu = dec ? "ppStylePropsContext" : null;
+ },
+
+ onPropSelect: function SRVr_OnPropSelect()
+ {
+ viewer.pane.panelset.updateAllCommands();
+ },
+
+ onTreeFocus: function SRVr_OnTreeFocus(aTree)
+ {
+ this.mFocusedTree = aTree;
+ viewer.pane.panelset.updateAllCommands();
+ },
+
+ onPopupShowing: function SRVr_OnPopupShowing(aCommandSetId)
+ {
+ var commandset = document.getElementById(aCommandSetId);
+ for (let i = 0; i < commandset.childNodes.length; i++) {
+ var command = commandset.childNodes[i];
+ command.setAttribute("disabled", !viewer.isCommandEnabled(command.id));
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// StyleRuleView
+
+function StyleRuleView(aObject)
+{
+ this.mLevel = [];
+ this.mOpen = [];
+ if (aObject instanceof Components.interfaces.nsIDOMCSSStyleSheet) {
+ document.getElementById("olcRule").setAttribute("primary", "true");
+ this.mSheetRules = [];
+ for (let i = 0; i < aObject.cssRules.length; i++) {
+ this.mSheetRules[i] = aObject.cssRules[i];
+ this.mLevel[i] = 0;
+ this.mOpen[i] = false;
+ }
+ }
+ else {
+ document.getElementById("olcRule").removeAttribute("primary");
+ this.mRules = viewer.DOMUtils.getCSSStyleRules(aObject);
+ if (aObject.hasAttribute("style")) {
+ try {
+ this.mStyleAttribute =
+ new XPCNativeWrapper(aObject, "style").style;
+ }
+ catch (ex) {
+ }
+ }
+ }
+}
+
+StyleRuleView.prototype = new inBaseTreeView();
+
+StyleRuleView.prototype.mSheetRules = null;
+StyleRuleView.prototype.mLevel = null;
+StyleRuleView.prototype.mOpen = null;
+StyleRuleView.prototype.mRules = null;
+StyleRuleView.prototype.mStyleAttribute = null;
+
+StyleRuleView.prototype.getRuleAt = function SRV_GetRuleAt(aRow)
+{
+ if (aRow >= 0) {
+ if (this.mRules) {
+ var rule = this.mRules.GetElementAt(aRow);
+ try {
+ return XPCU.QI(rule, "nsIDOMCSSStyleRule");
+ }
+ catch (ex) {
+ }
+ }
+ else {
+ return this.mSheetRules[aRow];
+ }
+ }
+ return null;
+}
+
+StyleRuleView.prototype.getDecAt = function SRV_GetDecAt(aRow)
+{
+ if (aRow >= 0) {
+ if (this.mRules) {
+ if (this.mStyleAttribute && aRow == this.mRules.Count()) {
+ return this.mStyleAttribute;
+ }
+ var rule = this.mRules.GetElementAt(aRow);
+ try {
+ return XPCU.QI(rule, "nsIDOMCSSStyleRule").style;
+ }
+ catch (ex) {
+ }
+ }
+ // for CSSStyleRule, CSSFontFaceRule, CSSPageRule, and
+ // ElementCSSInlineStyle
+ else if ("style" in this.mSheetRules[aRow]) {
+ return this.mSheetRules[aRow].style;
+ }
+ }
+ return null;
+}
+
+StyleRuleView.prototype.getChildCount = function SRV_GetChildCount(aRow)
+{
+ if (aRow >= 0) {
+ var rule = this.mSheetRules[aRow];
+ if (rule instanceof CSSImportRule) {
+ return rule.styleSheet ? rule.styleSheet.cssRules.length : 0;
+ }
+ if (rule instanceof CSSMediaRule ||
+ rule instanceof CSSMozDocumentRule) {
+ return rule.cssRules.length;
+ }
+ }
+ return 0;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Interface nsITreeView (Override inBaseTreeView)
+
+StyleRuleView.prototype.__defineGetter__("rowCount", function()
+{
+ if (this.mRules) {
+ return this.mRules.Count() + (this.mStyleAttribute ? 1 : 0);
+ }
+ if (this.mSheetRules) {
+ return this.mSheetRules.length;
+ }
+ return 0;
+});
+
+StyleRuleView.prototype.getCellText = function SRV_GetCellText(aRow, aCol)
+{
+ if (aRow > this.rowCount) {
+ return "";
+ }
+
+ // special case for the style attribute
+ if (this.mStyleAttribute && aRow == this.mRules.Count()) {
+ if (aCol.id == "olcRule") {
+ return 'style=""';
+ }
+
+ if (aCol.id == "olcFileURL") {
+ // we ought to be able to get to the URL...
+ return "";
+ }
+
+ if (aCol.id == "olcLine") {
+ return "";
+ }
+ return "";
+ }
+
+ var rule = this.getRuleAt(aRow);
+ if (!rule) {
+ return "";
+ }
+
+ if (aCol.id == "olcRule") {
+ if (rule instanceof CSSStyleRule) {
+ return rule.selectorText;
+ }
+ if (rule instanceof CSSFontFaceRule) {
+ return "@font-face";
+ }
+ if (rule instanceof CSSMediaRule ||
+ rule instanceof CSSMozDocumentRule) {
+ // get rule text up until the block begins, and trim off whitespace
+ return rule.cssText.replace(/\s*{[\s\S]*/, "");
+ }
+ return rule.cssText;
+ }
+
+ if (aCol.id == "olcFileURL") {
+ return rule.parentStyleSheet ? rule.parentStyleSheet.href : "";
+ }
+
+ if (aCol.id == "olcLine") {
+ return rule.type == CSSRule.STYLE_RULE ?
+ viewer.DOMUtils.getRuleLine(rule) :
+ "";
+ }
+
+ return "";
+}
+
+StyleRuleView.prototype.getLevel = function SRV_GetLevel(aRow)
+{
+ if (aRow in this.mLevel) {
+ return this.mLevel[aRow];
+ }
+ return 0;
+}
+
+StyleRuleView.prototype.getParentIndex = function SRV_GetParentIndex(aRow)
+{
+ var level = this.getLevel(aRow);
+ for (let i = aRow - 1; i >= 0; --i) {
+ if (this.getLevel(i) < level) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+StyleRuleView.prototype.hasNextSibling =
+ function SRV_HasNextSibling(aRow, aAfter)
+{
+ var baseLevel = this.getLevel(aRow);
+ var rowCount = this.rowCount; // quick access since this property is dynamic
+ for (let i = aAfter + 1; i < rowCount; ++i) {
+ if (this.getLevel(i) < baseLevel) {
+ break;
+ }
+ if (this.getLevel(i) == baseLevel) {
+ return true;
+ }
+ }
+ return false;
+}
+
+StyleRuleView.prototype.isContainer = function SRV_IsContainer(aRow)
+{
+ if (this.mSheetRules) {
+ if (this.mSheetRules[aRow] instanceof CSSImportRule ||
+ this.mSheetRules[aRow] instanceof CSSMediaRule ||
+ this.mSheetRules[aRow] instanceof CSSMozDocumentRule) {
+ return true;
+ }
+ }
+ return false;
+}
+
+StyleRuleView.prototype.isContainerEmpty = function SRV_IsContainerEmpty(aRow)
+{
+ return !this.getChildCount(aRow);
+}
+
+StyleRuleView.prototype.isContainerOpen = function SRV_IsContainerOpen(aRow)
+{
+ return this.mOpen[aRow];
+}
+
+StyleRuleView.prototype.toggleOpenState = function SRV_ToggleOpenState(aRow)
+{
+ var oldLength = this.mSheetRules.length;
+ var childLevel = this.mLevel[aRow] + 1;
+ if (this.mOpen[aRow]) {
+ // find the number of children and other descendants
+ let count = this.mSheetRules.length - aRow - 1;
+ for (let i = aRow + 1, n = this.mSheetRules.length; i < n; ++i) {
+ if (this.mLevel[i] < childLevel) {
+ count = i - aRow - 1;
+ break;
+ }
+ }
+ this.mSheetRules.splice(aRow + 1, count);
+ this.mLevel.splice(aRow + 1, count);
+ this.mOpen.splice(aRow + 1, count);
+ }
+ else {
+ var inserts = [];
+ var rule = this.mSheetRules[aRow];
+ if (rule instanceof CSSImportRule) {
+ // @import is tricky, because its styleSheet property is allowed to be
+ // null if its media-type qualifier isn't supported, among other
+ // reasons.
+ inserts = rule.styleSheet ? rule.styleSheet.cssRules : [];
+ }
+ else if (rule instanceof CSSMediaRule ||
+ rule instanceof CSSMozDocumentRule) {
+ inserts = rule.cssRules;
+ }
+ // make space for children
+ var count = this.getChildCount(aRow);
+ for (let i = this.rowCount - 1; i > aRow; --i) {
+ this.mSheetRules[i + count] = this.mSheetRules[i];
+ this.mLevel[i + count] = this.mLevel[i];
+ this.mOpen[i + count] = this.mOpen[i];
+ }
+ // fill in children
+ for (let i = 0; i < inserts.length; ++i) {
+ this.mSheetRules[aRow + 1 + i] = inserts[i];
+ this.mLevel[aRow + 1 + i] = childLevel;
+ this.mOpen[aRow + 1 + i] = false;
+ }
+ }
+ this.mOpen[aRow] = !this.mOpen[aRow];
+ viewer.mRuleTree.treeBoxObject.rowCountChanged(aRow + 1,
+ this.mSheetRules.length - oldLength);
+ viewer.mRuleTree.treeBoxObject.invalidateRow(aRow);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// StylePropsView
+
+function StylePropsView(aDec)
+{
+ this.mDec = aDec;
+}
+
+StylePropsView.prototype = new inBaseTreeView();
+
+StylePropsView.prototype.__defineGetter__("rowCount", function()
+{
+ return this.mDec ? this.mDec.length : 0;
+});
+
+StylePropsView.prototype.getCellProperties =
+ function SPV_GetCellProperties(aRow, aCol, aProperties)
+{
+ if (aCol.id == "olcPropPriority") {
+ var prop = this.mDec.item(aRow);
+ if (this.mDec.getPropertyPriority(prop) == "important") {
+ if (!aProperties)
+ return "important";
+
+ aProperties.AppendElement(this.createAtom("important"));
+ }
+ }
+
+ return "";
+}
+
+StylePropsView.prototype.getCellText = function SPV_GetCellText(aRow, aCol)
+{
+ var prop = this.mDec.item(aRow);
+
+ if (aCol.id == "olcPropName") {
+ return prop;
+ }
+ else if (aCol.id == "olcPropValue") {
+ return this.mDec.getPropertyValue(prop)
+ }
+
+ return null;
+}
+
+/**
+ * Returns a CSSProperty for the row in the tree corresponding to the
+ * passed index.
+ * @param aIndex
+ * index of the row in the tree
+ * @return a CSSProperty
+ */
+StylePropsView.prototype.getRowObjectFromIndex =
+ function SPV_GetRowObjectFromIndex(aIndex)
+{
+ var prop = this.mDec.item(aIndex);
+ return new CSSProperty(prop, this.mDec.getPropertyValue(prop),
+ this.mDec.getPropertyPriority(prop));
+}
+
+/**
+ * Handles inserting a CSS property
+ * @param aRule
+ * the rule that will contain the new property
+ * @param aProperty
+ * the name of the new property
+ * @param aValue
+ * the value of the new property
+ * @param aPriority
+ * the priority of the new property ("important" or "")
+ */
+function cmdEditInsert(aRule, aProperty, aValue, aPriority)
+{
+ this.rule = aRule;
+ this.property = aProperty;
+ this.value = aValue;
+ this.priority = aPriority;
+}
+
+cmdEditInsert.prototype = new inBaseCommand(false);
+
+cmdEditInsert.prototype.doTransaction = function Insert_DoTransaction()
+{
+ viewer.mPropsBoxObject.beginUpdateBatch();
+ try {
+ this.rule.setProperty(this.property, this.value, this.priority);
+ }
+ finally {
+ viewer.mPropsBoxObject.endUpdateBatch();
+ }
+};
+
+cmdEditInsert.prototype.undoTransaction = function Insert_UndoTransaction()
+{
+ this.rule.removeProperty(this.property);
+ viewer.mPropsBoxObject.invalidate();
+};
+
+/**
+ * Handles deleting CSS properties
+ * @param aRule
+ * the rule containing the properties
+ * @param aProperties
+ * an array of CSSPropertys to delete
+ */
+function cmdEditDelete(aRule, aProperties)
+{
+ this.rule = aRule;
+ this.properties = aProperties;
+}
+
+cmdEditDelete.prototype = new inBaseCommand(false);
+
+cmdEditDelete.prototype.doTransaction = function Delete_DoTransaction()
+{
+ viewer.mPropsBoxObject.beginUpdateBatch();
+ for (let i = 0; i < this.properties.length; i++) {
+ this.rule.removeProperty(this.properties[i].property);
+ }
+ viewer.mPropsBoxObject.endUpdateBatch();
+};
+
+cmdEditDelete.prototype.undoTransaction = function Delete_UndoTransaction()
+{
+ viewer.mPropsBoxObject.beginUpdateBatch();
+ try {
+ for (let i = 0; i < this.properties.length; i++) {
+ this.rule.setProperty(this.properties[i].property,
+ this.properties[i].value,
+ this.properties[i].important ?
+ "important" :
+ "");
+ }
+ }
+ finally {
+ viewer.mPropsBoxObject.endUpdateBatch();
+ }
+};
+
+/**
+ * Handles editing CSS properties
+ * @param aRule
+ * the rule containing the property
+ * @param aProperty
+ * the property to change
+ * @param aNewValue
+ * the new value for the property
+ * @param aNewPriority
+ * the new priority for the property ("important" or "")
+ */
+function cmdEditEdit(aRule, aProperty, aNewValue, aNewPriority)
+{
+ this.rule = aRule;
+ this.property = aProperty;
+ this.oldValue = aRule.getPropertyValue(aProperty);
+ this.newValue = aNewValue;
+ this.oldPriority = aRule.getPropertyPriority(aProperty);
+ this.newPriority = aNewPriority;
+}
+
+cmdEditEdit.prototype = new inBaseCommand(false);
+
+cmdEditEdit.prototype.doTransaction = function Edit_DoTransaction()
+{
+ this.rule.setProperty(this.property, this.newValue,
+ this.newPriority);
+ viewer.mPropsBoxObject.invalidate();
+};
+
+cmdEditEdit.prototype.undoTransaction = function Edit_UndoTransaction()
+{
+ this.rule.setProperty(this.property, this.oldValue,
+ this.oldPriority);
+ viewer.mPropsBoxObject.invalidate();
+};
+
+/**
+ * Handles toggling CSS !important.
+ * @param aRule
+ * the rule containing the properties
+ * @param aProperties
+ * an array of CSSPropertys to toggle
+ */
+function cmdTogglePriority(aRule, aProperties)
+{
+ this.rule = aRule;
+ this.properties = aProperties;
+}
+
+cmdTogglePriority.prototype = new inBaseCommand(false);
+
+cmdTogglePriority.prototype.doTransaction =
+ function TogglePriority_DoTransaction()
+{
+ for (let i = 0; i < this.properties.length; i++) {
+ // XXX bug 305761 means we can't make something not important, so
+ // instead we'll delete this property and make a new one at the proper
+ // priority. This method also sucks because the property gets moved to
+ // the bottom.
+ var property = this.properties[i].property;
+ var value = this.properties[i].value;
+ var newPriority = this.rule.getPropertyPriority(property) == "" ?
+ "important" : "";
+ this.rule.removeProperty(property);
+ this.rule.setProperty(property, value, newPriority);
+ }
+ viewer.mPropsBoxObject.invalidate();
+};
+
+cmdTogglePriority.prototype.undoTransaction =
+ function TogglePriority_UndoTransaction()
+{
+ this.doTransaction();
+};
+
+/**
+ * Copy the URI for a CSS rule's parent style sheet onto the clipboard.
+ * @param aRule
+ * The nsIDOMCSSRule whose parent style sheet's URI should be copied.
+ */
+function cmdEditCopyFileURI(aRule)
+{
+ this.mString = aRule && aRule.parentStyleSheet &&
+ aRule.parentStyleSheet.href;
+}
+
+cmdEditCopyFileURI.prototype = new cmdEditCopySimpleStringBase();
+
+/**
+ * Open a source view on a CSS rule's parent style sheet. This will attempt
+ * open the file at the line that the rule appears on.
+ * @param aRule
+ * The source view will open on nsIDOMCSSRule aRule's parent style
+ * sheet. If aRule is an nsIDOMCSSStyleRule, the source view will open
+ * to the line in the style sheet that aRule appears on.
+ */
+function cmdEditViewFileURI(aRule)
+{
+ this.mURI = aRule && aRule.parentStyleSheet && aRule.parentStyleSheet.href;
+ if (aRule.type == CSSRule.STYLE_RULE) {
+ this.mLineNumber = viewer.DOMUtils.getRuleLine(aRule);
+ }
+}
+
+cmdEditViewFileURI.prototype = new cmdEditViewFileURIBase();
diff --git a/inspector/content/viewers/styleRules/styleRules.xul b/inspector/content/viewers/styleRules/styleRules.xul
new file mode 100644
index 00000000..f177b9f7
--- /dev/null
+++ b/inspector/content/viewers/styleRules/styleRules.xul
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/stylesheets/stylesheets.js b/inspector/content/viewers/stylesheets/stylesheets.js
new file mode 100644
index 00000000..a121b5db
--- /dev/null
+++ b/inspector/content/viewers/stylesheets/stylesheets.js
@@ -0,0 +1,359 @@
+/* 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/. */
+
+/*****************************************************************************
+* StyleSheetsViewer ----------------------------------------------------------
+* The viewer for the style sheets loaded by a document.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/utils.js
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://global/content/viewSourceUtils.js
+* chrome://inspector/content/jsutil/commands/baseCommands.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", StyleSheetsViewer_initialize, false);
+
+function StyleSheetsViewer_initialize()
+{
+ viewer = new StyleSheetsViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Class StyleSheetsViewer
+
+function StyleSheetsViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+
+ this.mTree = document.getElementById("olStyleSheets");
+ this.mOlBox = this.mTree.treeBoxObject;
+}
+
+StyleSheetsViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPane: null,
+ mView: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Interface inIViewer
+
+ get uid()
+ {
+ return "stylesheets";
+ },
+
+ get pane()
+ {
+ return this.mPane;
+ },
+
+ get selection()
+ {
+ return this.mSelection;
+ },
+
+ get subject()
+ {
+ return this.mSubject;
+ },
+
+ set subject(aObject)
+ {
+ this.mView = new StyleSheetsView(aObject);
+ this.mOlBox.view = this.mView;
+ this.mObsMan.dispatchEvent("subjectChange", { subject: aObject });
+ this.mView.selection.select(0);
+ },
+
+ initialize: function SSVr_Initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function SSVr_Destroy()
+ {
+ this.mOlBox.view = null;
+ },
+
+ isCommandEnabled: function SSVr_IsCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditCopyFileURI":
+ case "cmdEditViewFileURI":
+ case "cmdEditInspectInNewWindow":
+ return !!this.getSelectedSheet();
+ }
+ return false;
+ },
+
+ getCommand: function SSVr_GetCommand(aCommand)
+ {
+ if (aCommand in window) {
+ return new window[aCommand]();
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Event Dispatching
+
+ addObserver: function SSVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function SSVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Stuff
+
+ onItemSelected: function SSVr_OnItemSelected()
+ {
+ this.pane.panelset.updateAllCommands();
+
+ var idx = this.mTree.currentIndex;
+ this.mSelection = this.mView.getSheet(idx);
+ this.mObsMan.dispatchEvent("selectionChange",
+ { selection: this.mSelection });
+ },
+
+ getSelectedSheet: function SSVr_GetSelectedSheet()
+ {
+ if (this.mView.selection.count == 1) {
+ let minAndMax = {};
+ this.mView.selection.getRangeAt(0, minAndMax, minAndMax);
+ return this.mView.getSheet(minAndMax.value);
+ }
+ return null;
+ },
+
+ onPopupShowing: function SRVr_OnPopupShowing(aCommandSetId)
+ {
+ // cmdEditInspectInNewWindow should already be up to date, but we need to
+ // make sure the others are as well.
+ var commandset = document.getElementById(aCommandSetId);
+ for (let i = 0; i < commandset.childNodes.length; i++) {
+ var command = commandset.childNodes[i];
+ if (viewer.isCommandEnabled(command.id)) {
+ command.removeAttribute("disabled");
+ }
+ else {
+ command.setAttribute("disabled", "true");
+ }
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// StyleSheetsView
+
+function StyleSheetsView(aDocument)
+{
+ this.mDocument = aDocument;
+ this.mSheets = [];
+ this.mLevels = [];
+ this.mOpen = [];
+ this.mChildCount = [];
+ this.mRowCount = 0;
+
+ var ss = aDocument.styleSheets;
+ for (let i = 0; i < ss.length; ++i) {
+ this.insertSheet(ss[i], 0, -1);
+ }
+}
+
+StyleSheetsView.prototype = new inBaseTreeView();
+
+StyleSheetsView.prototype.getSheet =
+function SSV_GetSheet(aRow)
+{
+ return this.mSheets[aRow];
+}
+
+StyleSheetsView.prototype.insertSheet =
+function SSV_InsertSheet(aSheet, aLevel, aRow)
+{
+ var row = aRow < 0 ? this.mSheets.length : aRow;
+
+ this.mSheets[row] = aSheet;
+ this.mLevels[row] = aLevel;
+ this.mOpen[row] = false;
+
+ var count = 0;
+ var rules = aSheet.cssRules;
+ for (let i = 0; i < rules.length; ++i) {
+ if (rules[i].type == CSSRule.IMPORT_RULE) {
+ ++count;
+ }
+ }
+ this.mChildCount[row] = count;
+ ++this.mRowCount;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Interface nsITreeView
+
+StyleSheetsView.prototype.getCellText =
+function SSV_GetCellText(aRow, aCol)
+{
+ var rule = this.mSheets[aRow];
+ if (aCol.id == "olcHref") {
+ if (rule.href) {
+ return rule.href;
+ }
+ // fall back for style elements
+ if (rule.ownerNode && rule.ownerNode.ownerDocument) {
+ return rule.ownerNode.ownerDocument.documentURI;
+ }
+ }
+ else if (aCol.id == "olcRules") {
+ return this.mSheets[aRow].cssRules.length;
+ }
+ return "";
+}
+
+StyleSheetsView.prototype.getLevel =
+function SSV_GetLevel(aRow)
+{
+ return this.mLevels[aRow];
+}
+
+StyleSheetsView.prototype.isContainer =
+function SSV_IsContainer(aRow)
+{
+ return this.mChildCount[aRow] > 0;
+}
+
+StyleSheetsView.prototype.isContainerEmpty =
+function SSV_IsContainerEmpty(aRow)
+{
+ return !this.isContainer(aRow);
+}
+
+StyleSheetsView.prototype.getParentIndex =
+function SSV_GetParentIndex(aRow)
+{
+ var baseLevel = this.mLevels[aRow];
+ for (let i = aRow - 1; i >= 0; --i) {
+ if (this.mLevels[i] < baseLevel) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+StyleSheetsView.prototype.hasNextSibling =
+function SSV_HasNextSibling(aRow, aAfter)
+{
+ var baseLevel = this.mLevels[aRow];
+ for (let i = aAfter + 1; i < this.mRowCount; ++i) {
+ if (this.mLevels[i] < baseLevel) {
+ break;
+ }
+ if (this.mLevels[i] == baseLevel) {
+ return true;
+ }
+ }
+ return false;
+}
+
+StyleSheetsView.prototype.isContainerOpen =
+function SSV_IsContainerOpen(aRow)
+{
+ return this.mOpen[aRow];
+}
+
+StyleSheetsView.prototype.toggleOpenState =
+function SSV_ToggleOpenState(aRow)
+{
+ var changeCount = 0;
+ if (this.mOpen[aRow]) {
+ var baseLevel = this.mLevels[aRow];
+ for (let i = aRow + 1; i < this.mRowCount; ++i) {
+ if (this.mLevels[i] <= baseLevel) {
+ break;
+ }
+ ++changeCount;
+ }
+ // shift data up
+ this.mSheets.splice(aRow + 1, changeCount);
+ this.mLevels.splice(aRow + 1, changeCount);
+ this.mOpen.splice(aRow + 1, changeCount);
+ this.mChildCount.splice(aRow + 1, changeCount);
+ changeCount = -changeCount;
+ this.mRowCount += changeCount;
+ }
+ else {
+ // for quick access
+ var rules = this.mSheets[aRow].cssRules;
+ var level = this.mLevels[aRow] + 1;
+ var childCount = this.mChildCount[aRow];
+ // shift data down
+ for (let i = this.mRowCount - 1; i > aRow; --i) {
+ this.mSheets[i + childCount] = this.mSheets[i];
+ this.mLevels[i + childCount] = this.mLevels[i];
+ this.mOpen[i + childCount] = this.mOpen[i];
+ this.mChildCount[i + childCount] = this.mChildCount[i];
+ }
+ // fill in new rows
+ for (let i = 0; i < rules.length; ++i) {
+ if (rules[i].type == CSSRule.IMPORT_RULE) {
+ ++changeCount;
+ this.insertSheet(rules[i].styleSheet, level, aRow + changeCount);
+ }
+ else if (rules[i].type != CSSRule.CHARSET_RULE) {
+ // only @charset and other @imports may precede @import, so exit now
+ break;
+ }
+ }
+ }
+
+ this.mOpen[aRow] = !this.mOpen[aRow];
+ this.mTree.rowCountChanged(aRow + 1, changeCount);
+ this.mTree.invalidateRow(aRow);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Transactions
+
+function cmdEditInspectInNewWindow()
+{
+ this.mObject = viewer.getSelectedSheet();
+}
+
+cmdEditInspectInNewWindow.prototype = new cmdEditInspectInNewWindowBase();
+
+function cmdEditCopyFileURI()
+{
+ var sheet = viewer.getSelectedSheet();
+ this.mString = sheet && sheet.href;
+}
+
+cmdEditCopyFileURI.prototype = new cmdEditCopySimpleStringBase();
+
+function cmdEditViewFileURI()
+{
+ var sheet = viewer.getSelectedSheet();
+ this.mURI = sheet && sheet.href;
+}
+
+cmdEditViewFileURI.prototype = new cmdEditViewFileURIBase();
diff --git a/inspector/content/viewers/stylesheets/stylesheets.xul b/inspector/content/viewers/stylesheets/stylesheets.xul
new file mode 100644
index 00000000..e7ed931e
--- /dev/null
+++ b/inspector/content/viewers/stylesheets/stylesheets.xul
@@ -0,0 +1,61 @@
+
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/usedFontFaces/usedFontFaces.js b/inspector/content/viewers/usedFontFaces/usedFontFaces.js
new file mode 100644
index 00000000..a8e89f07
--- /dev/null
+++ b/inspector/content/viewers/usedFontFaces/usedFontFaces.js
@@ -0,0 +1,229 @@
+/* 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/. */
+
+/*****************************************************************************
+* UsedFontFacesViewer --------------------------------------------------------
+* The viewer for the font faces used for a DOM node.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+* chrome://inspector/content/events/ObserverManager.js
+* chrome://inspector/content/commands/baseCommands.js
+* chrome://inspector/content/system/clipboardFlavors.js
+* chrome://inspector/content/xul/inBaseTreeView.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", UsedFontFacesViewer_initialize, false);
+
+function UsedFontFacesViewer_initialize()
+{
+ viewer = new UsedFontFacesViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// class UsedFontFacesViewer
+
+function UsedFontFacesViewer()
+{
+ this.mObsMan = new ObserverManager(this);
+
+ this.mDOMUtils = XPCU.getService("@mozilla.org/inspector/dom-utils;1",
+ "inIDOMUtils");
+
+ this.mTree = document.getElementById("olFonts");
+}
+
+UsedFontFacesViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPane: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// interface inIViewer
+
+ get uid()
+ {
+ return "usedFontFaces";
+ },
+
+ get pane()
+ {
+ return this.mPane;
+ },
+
+ get subject()
+ {
+ return this.mSubject;
+ },
+
+ set subject(aObject)
+ {
+ this.mSubject = aObject instanceof Components.interfaces.nsIDOMNode ?
+ aObject : aObject.DOMNode;
+ this.mTreeView = new UsedFontFacesView(this.mSubject);
+ this.mTree.view = this.mTreeView;
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+ },
+
+ initialize: function UFFVr_Initialize(aPane)
+ {
+ this.mPane = aPane;
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function UFFVr_Destroy()
+ {
+ // We need to remove the view at this time or else it will attempt to
+ // re-paint while the document is being deconstructed, resulting in some
+ // nasty XPConnect assertions
+ this.mTree.view = null;
+ },
+
+ isCommandEnabled: function UFFVr_IsCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ // ppUsedFontFacesContext
+ case "cmdEditCopy":
+ return this.mTree.view.selection.count > 0;
+ case "cmdEditCopyFileURI":
+ return this.mTreeView.getSelectedRowObjects()
+ .some(function(aFont) { return !!aFont.URI; });
+ }
+ return false;
+ },
+
+ getCommand: function UFFVr_GetCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditCopy":
+ return new cmdEditCopy(this.mTreeView.getSelectedRowObjects());
+ case "cmdEditCopyFileURI":
+ return new cmdEditCopyFileURI(this.mTreeView.getSelectedRowObjects());
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// event dispatching
+
+ addObserver: function UFFVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function UFFVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Miscellaneous
+
+ onItemSelected: function UFFVr_OnItemSelected()
+ {
+ // This will (eventually) call isCommandEnabled on Copy
+ viewer.pane.panelset.updateAllCommands();
+ },
+
+ onPopupShowing: function UFFVr_OnPopupShowing(aCommandSetId)
+ {
+ var commandset = document.getElementById(aCommandSetId);
+ for (let i = 0; i < commandset.childNodes.length; i++) {
+ var command = commandset.childNodes[i];
+ command.setAttribute("disabled", !viewer.isCommandEnabled(command.id));
+ }
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////
+//// UsedFontFacesView
+
+function UsedFontFacesView(aObject)
+{
+ // XXX Can't create a range for a DocumentType object
+ if (aObject instanceof Components.interfaces.nsIDOMDocumentType) {
+ this.mFontList = [];
+ this.mRowCount = 0;
+ }
+ else {
+ var range = (aObject.ownerDocument || aObject).createRange();
+ range.selectNodeContents(aObject);
+ this.mFontList = viewer.mDOMUtils.getUsedFontFaces(range);
+ this.mRowCount = this.mFontList.length;
+ }
+}
+
+UsedFontFacesView.prototype = new inBaseTreeView();
+
+UsedFontFacesView.prototype.getCellText = function UFFV_GetCellText(aRow, aCol)
+{
+ var font = this.mFontList.item(aRow);
+ if (aCol.id == "olcFontName") {
+ return font.name;
+ }
+ else if (aCol.id == "olcCSSFamilyName") {
+ return font.CSSFamilyName;
+ }
+ else if (aCol.id == "olcURI") {
+ return font.URI;
+ }
+ else if (aCol.id == "olcLocalName") {
+ return font.localName;
+ }
+ else if (aCol.id == "olcFormat") {
+ return font.format;
+ }
+
+ return null;
+};
+
+/**
+ * Returns a FontFace for the row in the tree corresponding to the passed
+ * index.
+ * @param aIndex
+ * index of the row in the tree
+ * @return a FontFace
+ */
+UsedFontFacesView.prototype.getRowObjectFromIndex =
+ function UFFV_GetRowObjectFromIndex(aIndex)
+{
+ return this.mFontList.item(aIndex);
+};
+
+/**
+ * Copy the names of fonts onto the clipboard.
+ * @param aFonts
+ * The font faces whose names should be copied.
+ */
+function cmdEditCopy(aFonts)
+{
+ this.mString = aFonts.map(function(aFont) { return aFont.name; }).join("\n");
+}
+
+cmdEditCopy.prototype = new cmdEditCopySimpleStringBase();
+
+/**
+ * Copy the URIs for downloaded fonts onto the clipboard.
+ * @param aFonts
+ * The font faces whose URIs should be copied.
+ */
+function cmdEditCopyFileURI(aFonts)
+{
+ this.mString = aFonts.map(function(aFont) { return aFont.URI; })
+ .filter(function(aURI) { return !!aURI; })
+ .join("\n");
+}
+
+cmdEditCopyFileURI.prototype = new cmdEditCopySimpleStringBase();
diff --git a/inspector/content/viewers/usedFontFaces/usedFontFaces.xul b/inspector/content/viewers/usedFontFaces/usedFontFaces.xul
new file mode 100644
index 00000000..8ef6356c
--- /dev/null
+++ b/inspector/content/viewers/usedFontFaces/usedFontFaces.xul
@@ -0,0 +1,68 @@
+
+
+
+ %dtd1;
+ %dtd2;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/content/viewers/xblBindings/xblBindings.js b/inspector/content/viewers/xblBindings/xblBindings.js
new file mode 100644
index 00000000..0f2c98d5
--- /dev/null
+++ b/inspector/content/viewers/xblBindings/xblBindings.js
@@ -0,0 +1,730 @@
+/* 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/. */
+
+/*****************************************************************************
+* XBLBindingsViewer ----------------------------------------------------------
+* Inspects the XBL bindings for a given element, including anonymous content,
+* methods, properties, event handlers, and resources.
+* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+* REQUIRED IMPORTS:
+* chrome://inspector/content/jsutil/xpcom/XPCU.js
+*****************************************************************************/
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Variables
+
+var viewer;
+
+//////////////////////////////////////////////////////////////////////////////
+//// Global Constants
+
+const kDOMViewContractID = "@mozilla.org/inspector/dom-view;1";
+const kDOMUtilsContractID = "@mozilla.org/inspector/dom-utils;1";
+const kXBLNSURI = "http://www.mozilla.org/xbl";
+
+//////////////////////////////////////////////////////////////////////////////
+
+window.addEventListener("load", XBLBindingsViewer_initialize, false);
+
+function XBLBindingsViewer_initialize()
+{
+ viewer = new XBLBindingsViewer();
+ viewer.initialize(parent.FrameExchange.receiveData(window));
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// Class XBLBindingsViewer
+
+function XBLBindingsViewer()
+{
+ this.mURL = window.location;
+ this.mObsMan = new ObserverManager(this);
+ this.mDOMUtils = XPCU.getService(kDOMUtilsContractID, "inIDOMUtils");
+
+ this.mBindingsList = document.getElementById("mlBindings");
+
+ this.mContentTree = document.getElementById("olContent");
+ this.mMethodTree = document.getElementById("olMethods");
+ this.mPropTree = document.getElementById("olProps");
+ this.mHandlerTree = document.getElementById("olHandlers");
+ this.mResourceTree = document.getElementById("olResources");
+
+ this.mTreeViews = {};
+
+ this.generateViewGetterAndSetter("contentView", this.mContentTree);
+ this.generateViewGetterAndSetter("methodView", this.mMethodTree);
+ this.generateViewGetterAndSetter("propView", this.mPropTree);
+ this.generateViewGetterAndSetter("handlerView", this.mHandlerTree);
+ this.generateViewGetterAndSetter("resourceView", this.mResourceTree);
+
+ this.mControllers = {};
+
+ this.addController(this.mBindingsList, BindingsListController);
+ this.addController(this.mResourceTree, ResourceTreeController);
+
+ // prepare and attach the content DOM datasource
+ var contentView = XPCU.createInstance(kDOMViewContractID, "inIDOMView");
+ contentView.whatToShow &= ~(NodeFilter.SHOW_TEXT);
+ XPCU.QI(contentView, "nsITreeView");
+ this.contentView = contentView;
+}
+
+XBLBindingsViewer.prototype =
+{
+ ////////////////////////////////////////////////////////////////////////////
+ //// Initialization
+
+ mSubject: null,
+ mPane: null,
+ mControllers: null,
+ mTreeViews: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Interface inIViewer
+
+ get uid()
+ {
+ return "xblBindings";
+ },
+
+ get pane()
+ {
+ return this.mPane;
+ },
+
+ get subject()
+ {
+ return this.mSubject;
+ },
+
+ set subject(aObject)
+ {
+ this.mSubject = aObject instanceof Components.interfaces.nsIDOMNode ?
+ aObject : aObject.DOMNode;
+
+ this.populateBindings();
+
+ this.displayBinding(this.mBindingsList.value);
+
+ this.mObsMan.dispatchEvent("subjectChange", { subject: this.mSubject });
+ },
+
+ initialize: function XBLBVr_Initialize(aPane)
+ {
+ this.mPane = aPane;
+
+ aPane.notifyViewerReady(this);
+ },
+
+ destroy: function XBLBVr_Destroy()
+ {
+ this.contentView = null;
+ this.methodView = null;
+ this.propView = null;
+ this.handlerView = null;
+ this.resourceView = null;
+ },
+
+ isCommandEnabled: function XBLBVr_IsCommandEnabled(aCommand)
+ {
+ var controller =
+ document.commandDispatcher.getControllerForCommand(aCommand);
+ return !!controller && controller.isCommandEnabled(aCommand);
+ },
+
+ getCommand: function XBLBVr_GetCommand(aCommand)
+ {
+ var controller =
+ this.mControllers[document.commandDispatcher.focusedElement.id];
+ if (controller) {
+ return controller.getCommand(aCommand);
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Event Dispatching
+
+ addObserver: function XBLBVr_AddObserver(aEvent, aObserver)
+ {
+ this.mObsMan.addObserver(aEvent, aObserver);
+ },
+
+ removeObserver: function XBLBVr_RemoveObserver(aEvent, aObserver)
+ {
+ this.mObsMan.removeObserver(aEvent, aObserver);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Displaying Binding Info
+
+ populateBindings: function XBLBVr_PopulateBindings()
+ {
+ var urls = this.mDOMUtils.getBindingURLs(this.mSubject);
+
+ this.mBindingsList.removeAllItems();
+
+ for (let i = 0, n = urls.length; i < n; ++i) {
+ var url = urls.queryElementAt(i, Components.interfaces.nsIURI).spec;
+ var currentItem = this.mBindingsList.appendItem(url, url);
+ currentItem.crop = "center";
+ currentItem.tooltipText = url;
+ }
+
+ this.mBindingsList.selectedIndex = 0;
+ },
+
+ displayBinding: function XBLBVr_DisplayBinding(aURL)
+ {
+ this.mBindingsList.tooltipText = aURL;
+ this.mBindingURL = aURL;
+ if (aURL) {
+ var req = new XMLHttpRequest();
+ req.addEventListener("load", gDocLoadListener, true);
+ req.open("GET", aURL);
+ req.overrideMimeType("application/xml");
+ req.send(null);
+ }
+ else {
+ this.doDisplayBinding(null);
+ }
+ },
+
+ doDisplayBinding: function XBLBVr_DoDisplayBinding(doc)
+ {
+ if (doc) {
+ var url = this.mBindingURL;
+ var poundPt = url.indexOf("#");
+ var id = url.substr(poundPt + 1);
+ var bindings = doc.getElementsByTagNameNS(kXBLNSURI, "binding");
+ var binding = null;
+ for (var i = 0; i < bindings.length; ++i) {
+ if (bindings[i].getAttribute("id") == id) {
+ binding = bindings[i];
+ break;
+ }
+ }
+ this.mBinding = binding;
+ }
+ else {
+ this.mBinding = null;
+ }
+
+ this.displayContent();
+ this.displayMethods();
+ this.displayProperties();
+ this.displayHandlers();
+ this.displayResources();
+
+ // switch to the first non-disabled tab if the one that's showing is
+ // disabled, otherwise, you can't use the keyboard to switch tabs
+ var tabbox = document.getElementById("bxBindingAspects");
+ if (tabbox.selectedTab.disabled) {
+ for (let i = 0; i < tabbox.tabs.childNodes.length; ++i) {
+ if (!tabbox.tabs.childNodes[i].disabled) {
+ tabbox.selectedTab = tabbox.tabs.childNodes[i];
+ break;
+ }
+ }
+ }
+
+ this.mBindingsList.disabled = !this.mBinding;
+ },
+
+ displayContent: function XBLBVr_DisplayContent()
+ {
+ this.contentView.rootNode = this.mBinding &&
+ this.mBinding.getElementsByTagNameNS(kXBLNSURI, "content").item(0);
+ this.mContentTree.disabled = !this.contentView.rootNode;
+ document.getElementById("tbContent").disabled =
+ !this.contentView.rootNode;
+ if (this.contentView.rootNode) {
+ this.contentView.selection.select(0);
+ }
+ },
+
+ displayMethods: function XBLBVr_DisplayMethods()
+ {
+ this.methodView =
+ this.mBinding ? new MethodTreeView(this.mBinding) : null;
+
+ var active = this.mBinding &&
+ this.mBinding.getElementsByTagNameNS(kXBLNSURI, "method").length > 0;
+ this.mMethodTree.disabled = !active;
+ document.getElementById("tbMethods").disabled = !active;
+ if (active && this.methodView.rowCount) {
+ this.methodView.selection.select(0);
+ }
+ },
+
+ displayProperties: function XBLBVr_DisplayProperties()
+ {
+ this.propView =
+ this.mBinding ? new PropTreeView(this.mBinding) : null;
+
+ var active = this.mBinding &&
+ this.mBinding.getElementsByTagNameNS(kXBLNSURI, "property").length > 0;
+ this.mPropTree.disabled = !active;
+ document.getElementById("tbProps").disabled = !active;
+ if (active && this.propView.rowCount) {
+ this.propView.selection.select(0);
+ }
+ },
+
+ displayHandlers: function XBLBVr_DisplayHandlers()
+ {
+ this.handlerView =
+ this.mBinding ? new HandlerTreeView(this.mBinding) : null;
+
+ var active = this.mBinding &&
+ this.mBinding.getElementsByTagNameNS(kXBLNSURI, "handler").length > 0;
+ this.mHandlerTree.disabled = !active;
+ document.getElementById("tbHandlers").disabled = !active;
+ if (active && this.handlerView.rowCount) {
+ this.handlerView.selection.select(0);
+ }
+ },
+
+ displayResources: function XBLBVr_DisplayResources()
+ {
+ this.resourceView =
+ this.mBinding ? new ResourceTreeView(this.mBinding) : null;
+
+ var active = this.mBinding &&
+ this.mBinding.getElementsByTagNameNS(kXBLNSURI, "resources").length > 0;
+ document.getElementById("tbResources").disabled = !active;
+ this.mResourceTree.disabled = !active;
+ },
+
+ displayMethod: function XBLBVr_DisplayMethod(aMethod)
+ {
+ var body = aMethod.getElementsByTagNameNS(kXBLNSURI, "body").item(0);
+ document.getElementById("txbMethodCode").value =
+ this.justifySource(this.readDOMText(body));
+ },
+
+ displayProperty: function XBLBVr_DisplayProperty(aProp)
+ {
+ var rgroup = document.getElementById("rgPropGetterSetter");
+ var getradio = document.getElementById("raPropGetter");
+ var setradio = document.getElementById("raPropSetter");
+
+ // disable/enable radio buttons
+ getradio.disabled =
+ !aProp || !(aProp.hasAttribute("onget") ||
+ aProp.getElementsByTagName("getter").length);
+ setradio.disabled =
+ !aProp || !(aProp.hasAttribute("onset") ||
+ aProp.getElementsByTagName("setter").length);
+
+ // make sure a valid radio button is selected
+ if (rgroup.selectedIndex < 0) {
+ rgroup.selectedIndex = 0;
+ }
+ if (rgroup.selectedItem.disabled) {
+ var other = rgroup.getItemAtIndex((rgroup.selectedIndex + 1) % 2);
+ if (!other.disabled) {
+ rgroup.selectedItem = other;
+ }
+ }
+
+ // display text
+ var et = rgroup.value;
+ var text = "";
+ if (et && aProp) {
+ text = aProp.getAttribute("on" + et);
+ if (!text) {
+ let kids = aProp.getElementsByTagNameNS(kXBLNSURI, et + "ter");
+ text = this.readDOMText(kids.item(0));
+ }
+ }
+ document.getElementById("txbPropCode").value = this.justifySource(text);
+ },
+
+ displayHandler: function XBLBVr_DisplayHandler(aHandler)
+ {
+ var text = "";
+ if (aHandler) {
+ text = aHandler.getAttribute("action") || this.readDOMText(aHandler);
+ }
+ document.getElementById("txbHandlerCode").value =
+ this.justifySource(text);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Selection
+
+ onMethodSelected: function XBLBVr_OnMethodSelected()
+ {
+ var idx = this.mMethodTree.currentIndex;
+ var methods = this.mBinding.getElementsByTagNameNS(kXBLNSURI, "method");
+ var method = methods[idx];
+ this.displayMethod(method);
+ },
+
+ onPropSelected: function XBLBVr_OnPropSelected()
+ {
+ var idx = this.mPropTree.currentIndex;
+ var props = this.mBinding.getElementsByTagNameNS(kXBLNSURI, "property");
+ var prop = props[idx];
+ this.displayProperty(prop);
+ },
+
+ onHandlerSelected: function XBLBVr_OnHandlerSelected()
+ {
+ var idx = this.mHandlerTree.currentIndex;
+ var handlers = this.mBinding.getElementsByTagNameNS(kXBLNSURI, "handler");
+ var handler = handlers[idx];
+ this.displayHandler(handler);
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// Misc
+
+ /**
+ * Generates getter and setter methods for property named by the given
+ * identifier. The setter will set the tree's view and cache it. The
+ * getter will recall the cached view.
+ * @param aIdentifier
+ * The name of the property where the getter and setter should live.
+ * @param aTree
+ * The tree whose view should be set in the setter.
+ */
+ generateViewGetterAndSetter:
+ function XBLBVr_GenerateViewGetterAndSetter(aIdentifier, aTree)
+ {
+ this.__defineSetter__(aIdentifier, function(aVal)
+ {
+ aTree.view = aVal;
+ return this.mTreeViews[aIdentifier] = aVal;
+ });
+ this.__defineGetter__(aIdentifier, function()
+ {
+ return this.mTreeViews[aIdentifier];
+ });
+ },
+
+ /**
+ * Creates a controller, registers it with this viewer, and appends it to
+ * the controllers of the given element.
+ * @param aEl
+ * The element to which we should add the controller. aEl.id must be
+ * non-empty.
+ * @param aControllerConstructor
+ * A constructor function whose instances will implement
+ * nsIController. aEl will be passed as the first parameter during
+ * construction.
+ */
+ addController: function XBLBVr_AddController(aEl, aControllerConstructor) {
+ var controller = new aControllerConstructor(aEl);
+ this.mControllers[aEl.id] = controller;
+ aEl.controllers.appendController(controller);
+ },
+
+
+ onPopupShowing: function XBLBVr_OnPopupShowing(aPopup)
+ {
+ var kids = aPopup.childNodes;
+ for (let i = 0, n = kids.length; i < n; ++i) {
+ let command = document.getElementById(kids[i].command);
+ if (this.isCommandEnabled(command.id)) {
+ command.removeAttribute("disabled");
+ }
+ else {
+ command.setAttribute("disabled", "true");
+ }
+ }
+ },
+
+ readDOMText: function XBLBVr_ReadDOMText(aEl)
+ {
+ if (!aEl) {
+ return "";
+ }
+
+ var text = aEl.nodeValue || "";
+ for (var i = 0; i < aEl.childNodes.length; ++i) {
+ text += this.readDOMText(aEl.childNodes[i]);
+ }
+ return text;
+ },
+
+ // Remove newlines at the beginning of the string and the lowest level of
+ // indentation from the beginning of each line, since most XBL getters,
+ // setters, methods, and handlers are handwritten CDATA.
+ justifySource: function XBLBVr_JustifySource(aStr)
+ {
+ // convert indentation to use spaces
+ while (/^ *\t/m.test(aStr)) {
+ aStr = aStr.replace(/^(( )*) {0,7}\t/gm, "$1 ");
+ }
+ // remove trailing spaces from all lines
+ aStr = aStr.replace(/ +$/gm, "");
+ // lose the trailing blank lines
+ aStr = aStr.replace(/\n*$/, "");
+ // lose the initial blank lines
+ aStr = aStr.replace(/^\n*/, "");
+ // now check if, for some crazy reason, there are lines in the rest of the
+ // source at a lower indentation level than the first line
+ var indentations = aStr.match(/^ *(?=[^\n])/gm);
+ if (indentations) {
+ indentations.sort();
+ if (indentations[0]) {
+ aStr = aStr.replace(RegExp("^" + indentations[0], "gm"), "");
+ }
+ }
+ return aStr;
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// Controllers
+
+function BindingsListController(aBindingsList) {}
+
+BindingsListController.prototype = {
+
+ commands: {
+ cmdEditCopyFileURI: function BLC_CopyFileURI()
+ {
+ this.mString = document.popupNode.value;
+ },
+
+ cmdEditViewFileURI: function BLC_ViewFileURI()
+ {
+ this.mURI = document.popupNode.value;
+ }
+ },
+
+ getCommand: function BLC_GetCommand(aCommand)
+ {
+ if (this.supportsCommand(aCommand)) {
+ return new this.commands[aCommand]();
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsIController Implementation
+
+ isCommandEnabled: function BLC_IsCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditCopyFileURI":
+ case "cmdEditViewFileURI":
+ return !!document.popupNode.value;
+ }
+ return false;
+ },
+
+ supportsCommand: function BLC_SupportsCommand(aCommand)
+ {
+ return aCommand in this.commands;
+ },
+
+ doCommand: function BLC_DoCommand(aCommand) {},
+
+ onEvent: function BLC_OnEvent(aEvent) {}
+}
+
+let commands = BindingsListController.prototype.commands;
+commands.cmdEditCopyFileURI.prototype = new cmdEditCopySimpleStringBase();
+commands.cmdEditViewFileURI.prototype = new cmdEditViewFileURIBase();
+
+function ResourceTreeController(aTree) {}
+
+ResourceTreeController.prototype = {
+
+ commands: {
+ cmdEditCopyFileURI: function RTC_CopyFileURI()
+ {
+ this.mString = viewer.resourceView.getSelectedResourceURI();
+ },
+
+ cmdEditViewFileURI: function RTC_ViewFileURI()
+ {
+ this.mURI = viewer.resourceView.getSelectedResourceURI();
+ }
+ },
+
+ getCommand: function RTC_GetCommand(aCommand)
+ {
+ if (this.supportsCommand(aCommand)) {
+ return new this.commands[aCommand]();
+ }
+ return null;
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsIController Implementation
+
+ isCommandEnabled: function RTC_IsCommandEnabled(aCommand)
+ {
+ switch (aCommand) {
+ case "cmdEditCopyFileURI":
+ return !!viewer.resourceView.getSelectedResourceURI();
+ case "cmdEditViewFileURI":
+ return !!viewer.resourceView.getSelectedResourceURI() &&
+ viewer.resourceView.getSelectedResourceType() != "image";
+ }
+ return false;
+ },
+
+ supportsCommand: function RTC_SupportsCommand(aCommand)
+ {
+ return aCommand in this.commands;
+ },
+
+ doCommand: function RTC_DoCommand(aCommand) {},
+
+ onEvent: function RTC_OnEvent(aEvent) {}
+}
+
+commands = ResourceTreeController.prototype.commands;
+commands.cmdEditCopyFileURI.prototype = new cmdEditCopySimpleStringBase();
+commands.cmdEditViewFileURI.prototype = new cmdEditViewFileURIBase();
+
+//////////////////////////////////////////////////////////////////////////////
+//// MethodTreeView
+
+function MethodTreeView(aBinding)
+{
+ this.mMethods = aBinding.getElementsByTagNameNS(kXBLNSURI, "method");
+ this.mRowCount = this.mMethods ? this.mMethods.length : 0;
+}
+
+MethodTreeView.prototype = new inBaseTreeView();
+
+MethodTreeView.prototype.getCellText =
+function MTV_GetCellText(aRow, aCol)
+{
+ if (aCol.id == "olcMethodName") {
+ var method = this.mMethods[aRow];
+ var name = method.getAttribute("name");
+ var params = method.getElementsByTagNameNS(kXBLNSURI, "parameter");
+ var pstr = "";
+ if (params.length) {
+ pstr += params[0].getAttribute("name");
+ }
+ for (var i = 1; i < params.length; ++i) {
+ pstr += ", " + params[i].getAttribute("name");
+ }
+ return name + "(" + pstr + ")";
+ }
+
+ return "";
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// PropTreeView
+
+function PropTreeView(aBinding)
+{
+ this.mProps = aBinding.getElementsByTagNameNS(kXBLNSURI, "property");
+ this.mRowCount = this.mProps ? this.mProps.length : 0;
+}
+
+PropTreeView.prototype = new inBaseTreeView();
+
+PropTreeView.prototype.getCellText =
+function PTV_GetCellText(aRow, aCol)
+{
+ if (aCol.id == "olcPropName") {
+ return this.mProps[aRow].getAttribute("name");
+ }
+
+ return "";
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// HandlerTreeView
+
+function HandlerTreeView(aBinding)
+{
+ this.mHandlers = aBinding.getElementsByTagNameNS(kXBLNSURI, "handler");
+ this.mRowCount = this.mHandlers ? this.mHandlers.length : 0;
+}
+
+HandlerTreeView.prototype = new inBaseTreeView();
+
+HandlerTreeView.prototype.getCellText =
+function HTV_GetCellText(aRow, aCol)
+{
+ var handler = this.mHandlers[aRow];
+ if (aCol.id == "olcHandlerEvent") {
+ return handler.getAttribute("event");
+ }
+ else if (aCol.id == "olcHandlerPhase") {
+ return handler.getAttribute("phase");
+ }
+
+ return "";
+}
+
+//////////////////////////////////////////////////////////////////////////////
+//// ResourceTreeView
+
+function ResourceTreeView(aBinding)
+{
+ this.mResources = [];
+ var res = aBinding.getElementsByTagNameNS(kXBLNSURI, "resources").item(0);
+ if (res) {
+ var kids = res.childNodes;
+ for (var i = 0; i < kids.length; ++i) {
+ if (kids[i].nodeType == Node.ELEMENT_NODE) {
+ this.mResources.push(kids[i]);
+ }
+ }
+ }
+
+ this.mRowCount = this.mResources.length;
+
+ this.wrappedJSObject = this;
+}
+
+ResourceTreeView.prototype = new inBaseTreeView();
+
+ResourceTreeView.prototype.getCellText =
+function RTV_GetCellText(aRow, aCol)
+{
+ var resource = this.mResources[aRow];
+ if (aCol.id == "olcResourceType") {
+ return resource.localName;
+ }
+ else if (aCol.id == "olcResourceSrc") {
+ return resource.getAttribute("src");
+ }
+
+ return "";
+}
+
+ResourceTreeView.prototype.getSelectedResourceURI =
+ function RTV_GetSelectedResourceURI()
+{
+ if (this.selection.count == 1) {
+ let minAndMax = {};
+ this.selection.getRangeAt(0, minAndMax, minAndMax);
+ return this.mResources[minAndMax.value].getAttribute("src");
+ }
+ return null;
+};
+
+ResourceTreeView.prototype.getSelectedResourceType =
+ function RTV_GetSelectedResourceType()
+{
+ if (this.selection.count == 1) {
+ let minAndMax = {};
+ this.selection.getRangeAt(0, minAndMax, minAndMax);
+ return this.mResources[minAndMax.value].localName;
+ }
+ return null;
+};
+
+//////////////////////////////////////////////////////////////////////////////
+//// Event Listeners
+
+function gDocLoadListener(event)
+{
+ viewer.doDisplayBinding(event.target.responseXML);
+}
diff --git a/inspector/content/viewers/xblBindings/xblBindings.xul b/inspector/content/viewers/xblBindings/xblBindings.xul
new file mode 100644
index 00000000..ec16661c
--- /dev/null
+++ b/inspector/content/viewers/xblBindings/xblBindings.xul
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/editing.dtd b/inspector/locale/editing.dtd
new file mode 100644
index 00000000..838cf0b0
--- /dev/null
+++ b/inspector/locale/editing.dtd
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/inspector.dtd b/inspector/locale/inspector.dtd
new file mode 100644
index 00000000..78732ae9
--- /dev/null
+++ b/inspector/locale/inspector.dtd
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/inspector.properties b/inspector/locale/inspector.properties
new file mode 100644
index 00000000..32df408a
--- /dev/null
+++ b/inspector/locale/inspector.properties
@@ -0,0 +1,37 @@
+# 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/.
+
+applicationAccesible.title = Application Accessible
+inspectWindow.noDocuments.message = (None)
+styleRuleNewProperty.title = New Style Rule
+styleRuleEditProperty.title = Edit Style Rule
+styleRulePropertyValue.message = Enter the property value:
+styleRulePropertyName.message = Enter the property name:
+sidebar.title = DOM Inspector
+sidebarInstalled = The sidebar is installed.
+newAttribute.title = New Attribute
+editAttribute.title = Edit Attribute
+findNodesDocumentEnd.message = End of document reached.
+findNodesDocumentEnd.title = Find Nodes
+# LOCALIZATION NOTE (root.title) label displayed for the tree head of the
+# JavaScript Object tree
+root.title = Subject
+
+irrecoverableSubtree.message = Some of the selected nodes are part of an anonymous subtree that will be destroyed. Deleting these nodes cannot be undone. Do you want to delete the selected nodes?
+irrecoverableSubtree.title = Delete Anonymous Subtree
+
+# The following items correspond to the node types defined by the W3C DOM Core
+# specification.
+1 = Element
+2 = Attribute
+3 = Text
+4 = CDATA Section
+5 = Entity Reference
+6 = Entity
+7 = Processing Instruction
+8 = Comment
+9 = Document
+10 = Document Type
+11 = Document Fragment
+12 = Notation
diff --git a/inspector/locale/jar.mn b/inspector/locale/jar.mn
new file mode 100644
index 00000000..f07747c8
--- /dev/null
+++ b/inspector/locale/jar.mn
@@ -0,0 +1,33 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% locale @INSPECTOR_CHROME_NAME@ en-US chrome/locale/
+ locale/editing.dtd
+ locale/inspector.dtd
+ locale/inspector.properties
+ locale/prefs.dtd
+ locale/tasksOverlay.dtd
+ locale/viewer-registry.dtd
+ locale/viewers/accessibleEvent.dtd (viewers/accessibleEvent.dtd)
+ locale/viewers/accessibleEvents.dtd (viewers/accessibleEvents.dtd)
+ locale/viewers/accessibleEvents.properties (viewers/accessibleEvents.properties)
+ locale/viewers/accessibleEventsHandlerHelpDialog.dtd (viewers/accessibleEventsHandlerHelpDialog.dtd)
+ locale/viewers/accessibleProps.dtd (viewers/accessibleProps.dtd)
+ locale/viewers/accessibleProps.properties (viewers/accessibleProps.properties)
+ locale/viewers/accessibleRelations.dtd (viewers/accessibleRelations.dtd)
+ locale/viewers/accessibleTree.dtd (viewers/accessibleTree.dtd)
+ locale/viewers/accessibleTreeEvalJSDialog.dtd (viewers/accessibleTreeEvalJSDialog.dtd)
+ locale/viewers/boxModel.dtd (viewers/boxModel.dtd)
+ locale/viewers/computedStyle.dtd (viewers/computedStyle.dtd)
+ locale/viewers/dom.dtd (viewers/dom.dtd)
+ locale/viewers/domNode.dtd (viewers/domNode.dtd)
+ locale/viewers/jsObject.dtd (viewers/jsObject.dtd)
+ locale/viewers/styleRules.dtd (viewers/styleRules.dtd)
+ locale/viewers/stylesheets.dtd (viewers/stylesheets.dtd)
+ locale/viewers/usedFontFaces.dtd (viewers/usedFontFaces.dtd)
+ locale/viewers/xblBindings.dtd (viewers/xblBindings.dtd)
+
diff --git a/inspector/locale/moz.build b/inspector/locale/moz.build
new file mode 100644
index 00000000..e0eb66aa
--- /dev/null
+++ b/inspector/locale/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/inspector/locale/prefs.dtd b/inspector/locale/prefs.dtd
new file mode 100644
index 00000000..3a6eab3f
--- /dev/null
+++ b/inspector/locale/prefs.dtd
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/tasksOverlay.dtd b/inspector/locale/tasksOverlay.dtd
new file mode 100644
index 00000000..8e1cddf2
--- /dev/null
+++ b/inspector/locale/tasksOverlay.dtd
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewer-registry.dtd b/inspector/locale/viewer-registry.dtd
new file mode 100644
index 00000000..39cb5cfc
--- /dev/null
+++ b/inspector/locale/viewer-registry.dtd
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleEvent.dtd b/inspector/locale/viewers/accessibleEvent.dtd
new file mode 100644
index 00000000..548dd355
--- /dev/null
+++ b/inspector/locale/viewers/accessibleEvent.dtd
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleEvents.dtd b/inspector/locale/viewers/accessibleEvents.dtd
new file mode 100644
index 00000000..d38007fb
--- /dev/null
+++ b/inspector/locale/viewers/accessibleEvents.dtd
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleEvents.properties b/inspector/locale/viewers/accessibleEvents.properties
new file mode 100644
index 00000000..5e3510d4
--- /dev/null
+++ b/inspector/locale/viewers/accessibleEvents.properties
@@ -0,0 +1,22 @@
+# 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/.
+
+mutationEvents = mutation events
+changeEvents = change events
+notificationEvents = notification events
+selectionEvents = selection events
+menuEvents = menu events
+documentEvents = document events
+textEvents = text events
+tableEvents = table events
+windowEvents = window events
+hyperLinkEvents = hyper link events
+hyperTextEvents = hyper text events
+
+handlerEditorLabel = Enable "%S" event handler.
+
+role = Role
+name = Name
+nodeName = Node Name
+id = ID
diff --git a/inspector/locale/viewers/accessibleEventsHandlerHelpDialog.dtd b/inspector/locale/viewers/accessibleEventsHandlerHelpDialog.dtd
new file mode 100644
index 00000000..3b67ad7c
--- /dev/null
+++ b/inspector/locale/viewers/accessibleEventsHandlerHelpDialog.dtd
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleProps.dtd b/inspector/locale/viewers/accessibleProps.dtd
new file mode 100644
index 00000000..32db3bb4
--- /dev/null
+++ b/inspector/locale/viewers/accessibleProps.dtd
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleProps.properties b/inspector/locale/viewers/accessibleProps.properties
new file mode 100644
index 00000000..df6fd914
--- /dev/null
+++ b/inspector/locale/viewers/accessibleProps.properties
@@ -0,0 +1,5 @@
+# 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/.
+
+accBounds = x: %S; y: %S; width: %S; height: %S;
diff --git a/inspector/locale/viewers/accessibleRelations.dtd b/inspector/locale/viewers/accessibleRelations.dtd
new file mode 100644
index 00000000..4c8c2216
--- /dev/null
+++ b/inspector/locale/viewers/accessibleRelations.dtd
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleTree.dtd b/inspector/locale/viewers/accessibleTree.dtd
new file mode 100644
index 00000000..b081c009
--- /dev/null
+++ b/inspector/locale/viewers/accessibleTree.dtd
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/accessibleTreeEvalJSDialog.dtd b/inspector/locale/viewers/accessibleTreeEvalJSDialog.dtd
new file mode 100644
index 00000000..99ac8314
--- /dev/null
+++ b/inspector/locale/viewers/accessibleTreeEvalJSDialog.dtd
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/boxModel.dtd b/inspector/locale/viewers/boxModel.dtd
new file mode 100644
index 00000000..95d8057f
--- /dev/null
+++ b/inspector/locale/viewers/boxModel.dtd
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/computedStyle.dtd b/inspector/locale/viewers/computedStyle.dtd
new file mode 100644
index 00000000..8b7f36ef
--- /dev/null
+++ b/inspector/locale/viewers/computedStyle.dtd
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/inspector/locale/viewers/dom.dtd b/inspector/locale/viewers/dom.dtd
new file mode 100644
index 00000000..e230f545
--- /dev/null
+++ b/inspector/locale/viewers/dom.dtd
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/domNode.dtd b/inspector/locale/viewers/domNode.dtd
new file mode 100644
index 00000000..c9c02781
--- /dev/null
+++ b/inspector/locale/viewers/domNode.dtd
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/jsObject.dtd b/inspector/locale/viewers/jsObject.dtd
new file mode 100644
index 00000000..f2cf65c1
--- /dev/null
+++ b/inspector/locale/viewers/jsObject.dtd
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/styleRules.dtd b/inspector/locale/viewers/styleRules.dtd
new file mode 100644
index 00000000..c286aa69
--- /dev/null
+++ b/inspector/locale/viewers/styleRules.dtd
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/stylesheets.dtd b/inspector/locale/viewers/stylesheets.dtd
new file mode 100644
index 00000000..d7a3e591
--- /dev/null
+++ b/inspector/locale/viewers/stylesheets.dtd
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/inspector/locale/viewers/usedFontFaces.dtd b/inspector/locale/viewers/usedFontFaces.dtd
new file mode 100644
index 00000000..de5424ed
--- /dev/null
+++ b/inspector/locale/viewers/usedFontFaces.dtd
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/inspector/locale/viewers/xblBindings.dtd b/inspector/locale/viewers/xblBindings.dtd
new file mode 100644
index 00000000..35098797
--- /dev/null
+++ b/inspector/locale/viewers/xblBindings.dtd
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/inspector/modules/InspectElement.jsm b/inspector/modules/InspectElement.jsm
new file mode 100644
index 00000000..4f9db4c7
--- /dev/null
+++ b/inspector/modules/InspectElement.jsm
@@ -0,0 +1,89 @@
+var EXPORTED_SYMBOLS = ["InspectElement"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var InspectElement = {
+ ww: Services.ww, // nsIWindowWatcher
+ wm: Services.wm, // nsIWindowMediator
+
+ get isWinNT() {
+ var os = Services.appinfo.OS;
+ return os == "WINNT" ? true : false;
+ },
+
+ handleEvent: function(e) {
+ // Shift + 右键 响应
+ if (!e.shiftKey || e.button != 2) return;
+ try {
+ e.stopPropagation();
+ e.preventDefault();
+ } catch (ex) {}
+ if (e.type != "click") return;
+ let elem = e.originalTarget;
+ let shadowElem = e.target;
+ let win = e.currentTarget;
+ this.inspect(win, elem, shadowElem);
+ },
+ inspect: function(win, elem, shadowElem) {
+ win.openDialog("chrome://inspector/content/", "_blank",
+ "chrome, all, dialog=no", elem);
+ this.closePopup(elem, win);
+ },
+ closePopup: function (elem, win) {
+ var parent = elem.parentNode;
+ var list = [];
+ while (parent != win && parent != null) {
+ if (parent.localName == "menupopup" || parent.localName == "popup") {
+ list.push(parent);
+ }
+ parent = parent.parentNode;
+ }
+ var len = list.length;
+ if (!len) return;
+ list[len - 1].hidePopup();
+ },
+
+ aListener: {
+ onOpenWindow: function (aWindow) {
+ var win = aWindow.docShell.QueryInterface(
+ Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("load", function _() {
+ this.removeEventListener("load", _, false);
+ win.addEventListener("click", InspectElement, true);
+ // fix context menu bug in linux
+ if (InspectElement.isWinNT) return;
+ //win.addEventListener("mousedown", InspectElement, true);
+ win.addEventListener("mouseup", InspectElement, false);
+ win.addEventListener("contextmenu", InspectElement, true);
+ }, false);
+ },
+ onCloseWindow: function (aWindow) {},
+ onWindowTitleChange: function (aWindow, aTitle) {},
+ },
+
+ init: function () {
+ this.wm.addListener(this.aListener);
+ var cw = this.ww.getWindowEnumerator();
+ while (cw.hasMoreElements()) {
+ var win = cw.getNext().QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("click", InspectElement, true);
+ // fix context menu bug in linux
+ if (this.isWinNT) continue;
+ //win.addEventListener("mousedown", InspectElement, true);
+ win.addEventListener("mouseup", InspectElement, false);
+ win.addEventListener("contextmenu", InspectElement, true);
+ }
+ },
+ uninit: function () {
+ this.wm.removeListener(this.aListener);
+ var cw = this.ww.getWindowEnumerator();
+ while (cw.hasMoreElements()) {
+ var win = cw.getNext().QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.removeEventListener("click", InspectElement, true);
+ if (this.isWinNT) continue;
+ //win.removeEventListener("mousedown", InspectElement, true);
+ win.removeEventListener("mouseup", InspectElement, false);
+ win.removeEventListener("contextmenu", InspectElement, true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/inspector/modules/jar.mn b/inspector/modules/jar.mn
new file mode 100644
index 00000000..71b73bbb
--- /dev/null
+++ b/inspector/modules/jar.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% resource @INSPECTOR_CHROME_NAME@ modules/
\ No newline at end of file
diff --git a/inspector/modules/moz.build b/inspector/modules/moz.build
new file mode 100644
index 00000000..a96afb5a
--- /dev/null
+++ b/inspector/modules/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += ['InspectElement.jsm']
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/inspector/moz.build b/inspector/moz.build
new file mode 100644
index 00000000..fc271f72
--- /dev/null
+++ b/inspector/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'addon',
+ 'components',
+ 'content',
+ 'locale',
+ 'modules',
+ 'skin',
+]
+
diff --git a/inspector/moz.configure b/inspector/moz.configure
new file mode 100644
index 00000000..fe82fcc4
--- /dev/null
+++ b/inspector/moz.configure
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+include('../build/moz.configure/platform.configure')
+include('confvars.configure')
+ConfVars('moz.configure')
diff --git a/inspector/skin/classic/ImageSearchItem.gif b/inspector/skin/classic/ImageSearchItem.gif
new file mode 100644
index 00000000..f078b634
Binary files /dev/null and b/inspector/skin/classic/ImageSearchItem.gif differ
diff --git a/inspector/skin/classic/btnFind-dis.gif b/inspector/skin/classic/btnFind-dis.gif
new file mode 100644
index 00000000..bc5e6979
Binary files /dev/null and b/inspector/skin/classic/btnFind-dis.gif differ
diff --git a/inspector/skin/classic/btnFind.gif b/inspector/skin/classic/btnFind.gif
new file mode 100644
index 00000000..3433ac04
Binary files /dev/null and b/inspector/skin/classic/btnFind.gif differ
diff --git a/inspector/skin/classic/btnSelecting-act.gif b/inspector/skin/classic/btnSelecting-act.gif
new file mode 100644
index 00000000..90b80c3f
Binary files /dev/null and b/inspector/skin/classic/btnSelecting-act.gif differ
diff --git a/inspector/skin/classic/btnSelecting-dis.gif b/inspector/skin/classic/btnSelecting-dis.gif
new file mode 100644
index 00000000..c5f5dea9
Binary files /dev/null and b/inspector/skin/classic/btnSelecting-dis.gif differ
diff --git a/inspector/skin/classic/btnSelecting.gif b/inspector/skin/classic/btnSelecting.gif
new file mode 100644
index 00000000..d970f50d
Binary files /dev/null and b/inspector/skin/classic/btnSelecting.gif differ
diff --git a/inspector/skin/classic/iconImportant.gif b/inspector/skin/classic/iconImportant.gif
new file mode 100644
index 00000000..b5d34635
Binary files /dev/null and b/inspector/skin/classic/iconImportant.gif differ
diff --git a/inspector/skin/classic/iconViewerList-dis.gif b/inspector/skin/classic/iconViewerList-dis.gif
new file mode 100644
index 00000000..6e265777
Binary files /dev/null and b/inspector/skin/classic/iconViewerList-dis.gif differ
diff --git a/inspector/skin/classic/iconViewerList.gif b/inspector/skin/classic/iconViewerList.gif
new file mode 100644
index 00000000..82f7dff9
Binary files /dev/null and b/inspector/skin/classic/iconViewerList.gif differ
diff --git a/inspector/skin/classic/inspector.css b/inspector/skin/classic/inspector.css
new file mode 100644
index 00000000..c8f70a02
--- /dev/null
+++ b/inspector/skin/classic/inspector.css
@@ -0,0 +1,6 @@
+/* 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/. */
+
+@import url("chrome://global/skin");
+@import url("chrome://inspector/content/inspector.css");
diff --git a/inspector/skin/classic/inspectorWindow.css b/inspector/skin/classic/inspectorWindow.css
new file mode 100644
index 00000000..37a054f8
--- /dev/null
+++ b/inspector/skin/classic/inspectorWindow.css
@@ -0,0 +1,45 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://navigator/skin");
+@import url("chrome://browser/skin");
+
+#bxURLBar {
+ padding: 2px;
+}
+
+#bxSearch {
+ height: 150px;
+}
+
+#splPanels {
+ width: 5px;
+}
+
+/* ::::: toolbar buttons ::::: */
+
+#btnSelecting {
+ list-style-image: url("chrome://inspector/skin/btnSelecting.gif");
+}
+
+#btnSelecting[checked="true"] {
+ list-style-image: url("chrome://inspector/skin/btnSelecting-act.gif");
+}
+
+#btnSelecting[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/btnSelecting-dis.gif");
+}
+
+#btnFind {
+ list-style-image: url("chrome://inspector/skin/btnFind.gif");
+}
+
+#btnFind[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/btnFind-dis.gif");
+}
+
+.viewer-list, .viewer-menu {
+ -moz-user-focus: ignore;
+}
diff --git a/inspector/skin/classic/jar.mn b/inspector/skin/classic/jar.mn
new file mode 100644
index 00000000..efd0d12f
--- /dev/null
+++ b/inspector/skin/classic/jar.mn
@@ -0,0 +1,35 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% skin @INSPECTOR_CHROME_NAME@ classic/1.0 chrome/skin/classic/
+ skin/classic/btnFind-dis.gif
+ skin/classic/btnFind.gif
+ skin/classic/btnSelecting-act.gif
+ skin/classic/btnSelecting-dis.gif
+ skin/classic/btnSelecting.gif
+ skin/classic/iconImportant.gif
+ skin/classic/iconViewerList-dis.gif
+ skin/classic/iconViewerList.gif
+ skin/classic/ImageSearchItem.gif
+ skin/classic/inspector.css
+ skin/classic/inspectorWindow.css
+ skin/classic/panelset.css
+ skin/classic/sidebar.css
+ skin/classic/titledsplitter-close.gif
+ skin/classic/titledSplitter.css
+ skin/classic/viewers/accessibleEvent/accessibleEvent.css (viewers/accessibleEvent/accessibleEvent.css)
+ skin/classic/viewers/accessibleEvents/accessibleEvents.css (viewers/accessibleEvents/accessibleEvents.css)
+ skin/classic/viewers/accessibleProps/accessibleProps.css (viewers/accessibleProps/accessibleProps.css)
+ skin/classic/viewers/accessibleTree/accessibleTree.css (viewers/accessibleTree/accessibleTree.css)
+ skin/classic/viewers/boxModel/boxModel.css (viewers/boxModel/boxModel.css)
+ skin/classic/viewers/dom/columnsDialog.css (viewers/dom/columnsDialog.css)
+ skin/classic/viewers/dom/dom.css (viewers/dom/dom.css)
+ skin/classic/viewers/dom/findDialog.css (viewers/dom/findDialog.css)
+ skin/classic/viewers/domNode/domNode.css (viewers/domNode/domNode.css)
+ skin/classic/viewers/styleRules/styleRules.css (viewers/styleRules/styleRules.css)
+ skin/classic/viewers/xblBindings/xblBindings.css (viewers/xblBindings/xblBindings.css)
+
diff --git a/inspector/skin/classic/moz.build b/inspector/skin/classic/moz.build
new file mode 100644
index 00000000..e0eb66aa
--- /dev/null
+++ b/inspector/skin/classic/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/inspector/skin/classic/panelset.css b/inspector/skin/classic/panelset.css
new file mode 100644
index 00000000..cc64d064
--- /dev/null
+++ b/inspector/skin/classic/panelset.css
@@ -0,0 +1,34 @@
+/* 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/. */
+
+panel[disabled="true"] > .viewer-pane-box-1 {
+ visibility: collapse;
+}
+
+.viewer-menu, .viewer-list {
+ -moz-user-focus: normal;
+}
+
+.viewer-list {
+ list-style-image: url("chrome://inspector/skin/iconViewerList.gif");
+}
+
+.viewer-list[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/iconViewerList-dis.gif");
+}
+
+.viewer-pane-toolbox {
+ border-right: none;
+}
+
+.viewer-pane-box-2 {
+ border-left: 1px solid ThreeDShadow;
+ border-right: 1px solid ThreeDShadow;
+}
+
+.viewer-pane-toolbox {
+ border-top: none;
+ border-bottom: none;
+}
+
diff --git a/inspector/skin/classic/sidebar.css b/inspector/skin/classic/sidebar.css
new file mode 100644
index 00000000..1f186ef5
--- /dev/null
+++ b/inspector/skin/classic/sidebar.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+tabpanels {
+ padding: 0px;
+}
\ No newline at end of file
diff --git a/inspector/skin/classic/titledSplitter.css b/inspector/skin/classic/titledSplitter.css
new file mode 100644
index 00000000..b9f834aa
--- /dev/null
+++ b/inspector/skin/classic/titledSplitter.css
@@ -0,0 +1,53 @@
+/* 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/. */
+
+.titled-splitter {
+ border-top: 1px outset InactiveCaption;
+ border-bottom: 1px outset InactiveCaption;
+ background-color: InactiveCaption;
+ color: InactiveCaptionText;
+ cursor: default;
+}
+
+box[orient="vertical"] > .titled-splitter {
+ cursor: default;
+}
+
+.titledsplitter-container {
+ padding-bottom: 2px;
+}
+
+.titledsplitter-dragbar {
+ min-height: 2px;
+ cursor: n-resize !important;
+}
+
+.titledsplitter-titlebox {
+ padding: 0px 5px 0px 5px;
+}
+
+.titledsplitter-text {
+ margin: 0px !important;
+ font-weight: bold;
+}
+
+.titledsplitter-closebutton {
+ -moz-binding: none;
+ margin: 1px;
+ border: 1px outset ThreeDFace;
+ padding: 2px;
+ min-width: 0px;
+ background-color: ThreeDFace;
+}
+
+.titledsplitter-closeimage {
+ list-style-image: url("chrome://inspector/skin/titledsplitter-close.gif");
+}
+
+.titledsplitter-closebutton:hover:active {
+ border-style: inset;
+ padding: 3px 1px 1px 3px;
+}
+
+
diff --git a/inspector/skin/classic/titledsplitter-close.gif b/inspector/skin/classic/titledsplitter-close.gif
new file mode 100644
index 00000000..a53eddd0
Binary files /dev/null and b/inspector/skin/classic/titledsplitter-close.gif differ
diff --git a/inspector/skin/classic/viewers/accessibleEvent/accessibleEvent.css b/inspector/skin/classic/viewers/accessibleEvent/accessibleEvent.css
new file mode 100644
index 00000000..86e7d290
--- /dev/null
+++ b/inspector/skin/classic/viewers/accessibleEvent/accessibleEvent.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin/viewers/accessibleTree/accessibleTree.css");
+
+#handlerOutput {
+ overflow: auto;
+}
diff --git a/inspector/skin/classic/viewers/accessibleEvents/accessibleEvents.css b/inspector/skin/classic/viewers/accessibleEvents/accessibleEvents.css
new file mode 100644
index 00000000..ecf0a7f8
--- /dev/null
+++ b/inspector/skin/classic/viewers/accessibleEvents/accessibleEvents.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+treechildren::-moz-tree-checkbox(checked) {
+ list-style-image: url("chrome://global/skin/checkbox/cbox-check.gif");
+}
+
+#welHandlerEditor {
+ font-family: monospace;
+}
+
+#welGrippyButton {
+ cursor: default;
+}
+
+#welSplitter {
+ -moz-appearance: none;
+}
diff --git a/inspector/skin/classic/viewers/accessibleProps/accessibleProps.css b/inspector/skin/classic/viewers/accessibleProps/accessibleProps.css
new file mode 100644
index 00000000..227827b4
--- /dev/null
+++ b/inspector/skin/classic/viewers/accessibleProps/accessibleProps.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+.textAttrsTextRange {
+ border-bottom: medium dotted blue;
+ padding: 7px;
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+ line-height: 250%;
+ margin: 2px;
+}
+
+.textAttrsTextRange:hover {
+ border-bottom: medium dotted red;
+}
+
+.textAttrsTextRange[selected] {
+ border-bottom: medium groove purple;
+}
diff --git a/inspector/skin/classic/viewers/accessibleTree/accessibleTree.css b/inspector/skin/classic/viewers/accessibleTree/accessibleTree.css
new file mode 100644
index 00000000..d2fcea73
--- /dev/null
+++ b/inspector/skin/classic/viewers/accessibleTree/accessibleTree.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+treechildren::-moz-tree-row(highlight) {
+ background-color: #feeecd;
+}
+
+treechildren::-moz-tree-row(selected, highlight) {
+ background-color: #c0ffff;
+}
+
+treechildren::-moz-tree-cell-text(highlight) {
+ color: black;
+}
+
+treechildren::-moz-tree-cell-text(grayout) {
+ color: gray;
+ font-weight: bold;
+}
diff --git a/inspector/skin/classic/viewers/boxModel/boxModel.css b/inspector/skin/classic/viewers/boxModel/boxModel.css
new file mode 100644
index 00000000..5be126a4
--- /dev/null
+++ b/inspector/skin/classic/viewers/boxModel/boxModel.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+/* ::::: statistics ::::: */
+
+.nonzero {
+ font-weight: bold;
+}
+
+groupbox:not(#boxBorder) textbox {
+ text-align: right;
+}
+
+row {
+ -moz-box-align: baseline;
+}
+
+#enclosingBox {
+ overflow: auto;
+}
diff --git a/inspector/skin/classic/viewers/dom/columnsDialog.css b/inspector/skin/classic/viewers/dom/columnsDialog.css
new file mode 100644
index 00000000..ac084de7
--- /dev/null
+++ b/inspector/skin/classic/viewers/dom/columnsDialog.css
@@ -0,0 +1,26 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+#trColumns {
+ background-color: ThreeDFace;
+}
+
+.column-selector {
+ border: 1px outset ThreeDFace;
+ padding: 2px;
+ background-color: ThreeDFace;
+ vertical-align: middle;
+}
+
+.attr-column-selector,
+.attr-column-selector[focused="true"] {
+ margin: 0;
+ border: none;
+}
+
+[col-dragging="true"] {
+ border: 1px solid ButtonText;
+}
diff --git a/inspector/skin/classic/viewers/dom/dom.css b/inspector/skin/classic/viewers/dom/dom.css
new file mode 100644
index 00000000..303fa512
--- /dev/null
+++ b/inspector/skin/classic/viewers/dom/dom.css
@@ -0,0 +1,58 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+/* :::::::: node type color coding :::::::: */
+
+treechildren::-moz-tree-cell-text(ACCESSIBLE_NODE) {
+ font-weight: bold;
+}
+
+treechildren::-moz-tree-cell-text(ELEMENT_NODE) {
+ color: #000000;
+}
+
+treechildren::-moz-tree-cell-text(ATTRIBUTE_NODE) {
+ color: #556b2f;
+}
+
+treechildren::-moz-tree-cell-text(CDATA_SECTION_NODE),
+treechildren::-moz-tree-cell-text(TEXT_NODE) {
+ color: #0000AA;
+}
+
+treechildren::-moz-tree-cell-text(COMMENT_NODE) {
+ color: #228b22;
+}
+
+treechildren::-moz-tree-cell-text(DOCUMENT_NODE) {
+ color: #800080;
+}
+
+treechildren::-moz-tree-cell-text(DOCUMENT_TYPE_NODE) {
+ color: #606000;
+}
+
+treechildren::-moz-tree-cell-text(PROCESSING_INSTRUCTION_NODE) {
+ color: #808080;
+}
+
+treechildren::-moz-tree-cell-text(anonymous) {
+ color: #ff0000;
+}
+
+treechildren::-moz-tree-cell-text(selected, focus) {
+ color: #FFFFFF;
+}
+
+/* :::::::: drag and drop insertion indicators :::::::: */
+
+treechildren::-moz-tree-column(dnd-insert-before) {
+ border-left: 2px solid #000000;
+}
+
+treechildren::-moz-tree-column(dnd-insert-after) {
+ border-right: 2px solid #000000;
+}
diff --git a/inspector/skin/classic/viewers/dom/findDialog.css b/inspector/skin/classic/viewers/dom/findDialog.css
new file mode 100644
index 00000000..c201f993
--- /dev/null
+++ b/inspector/skin/classic/viewers/dom/findDialog.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
diff --git a/inspector/skin/classic/viewers/domNode/domNode.css b/inspector/skin/classic/viewers/domNode/domNode.css
new file mode 100644
index 00000000..882f9a7c
--- /dev/null
+++ b/inspector/skin/classic/viewers/domNode/domNode.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://inspector/content/extensions/treeEditable.css");
+
+.olNodeInfoLabel {
+ padding-right: 0.5em;
+}
+#olNodeInfo {
+ padding-bottom: 0.5em;
+}
diff --git a/inspector/skin/classic/viewers/styleRules/styleRules.css b/inspector/skin/classic/viewers/styleRules/styleRules.css
new file mode 100644
index 00000000..90b457e6
--- /dev/null
+++ b/inspector/skin/classic/viewers/styleRules/styleRules.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://inspector/content/extensions/treeEditable.css");
+
+#olcPropPriority {
+ list-style-image: url("chrome://inspector/skin/iconImportant.gif");
+}
+
+#olbStyleProps::-moz-tree-image(olcPropPriority, important) {
+ list-style-image: url("chrome://inspector/skin/iconImportant.gif");
+}
+
+/* ::::: default column widths ::::: */
+
+#olcLine {
+ width: 5em;
+}
+
diff --git a/inspector/skin/classic/viewers/xblBindings/xblBindings.css b/inspector/skin/classic/viewers/xblBindings/xblBindings.css
new file mode 100644
index 00000000..7a3f07d6
--- /dev/null
+++ b/inspector/skin/classic/viewers/xblBindings/xblBindings.css
@@ -0,0 +1,6 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin/");
+@import url("chrome://inspector/skin/viewers/dom/dom.css");
diff --git a/inspector/skin/modern/ImageSearchItem.gif b/inspector/skin/modern/ImageSearchItem.gif
new file mode 100644
index 00000000..f078b634
Binary files /dev/null and b/inspector/skin/modern/ImageSearchItem.gif differ
diff --git a/inspector/skin/modern/btnFind-dis.gif b/inspector/skin/modern/btnFind-dis.gif
new file mode 100644
index 00000000..bc5e6979
Binary files /dev/null and b/inspector/skin/modern/btnFind-dis.gif differ
diff --git a/inspector/skin/modern/btnFind.gif b/inspector/skin/modern/btnFind.gif
new file mode 100644
index 00000000..3433ac04
Binary files /dev/null and b/inspector/skin/modern/btnFind.gif differ
diff --git a/inspector/skin/modern/btnSelecting-act.gif b/inspector/skin/modern/btnSelecting-act.gif
new file mode 100644
index 00000000..90b80c3f
Binary files /dev/null and b/inspector/skin/modern/btnSelecting-act.gif differ
diff --git a/inspector/skin/modern/btnSelecting-dis.gif b/inspector/skin/modern/btnSelecting-dis.gif
new file mode 100644
index 00000000..c5f5dea9
Binary files /dev/null and b/inspector/skin/modern/btnSelecting-dis.gif differ
diff --git a/inspector/skin/modern/btnSelecting.gif b/inspector/skin/modern/btnSelecting.gif
new file mode 100644
index 00000000..d970f50d
Binary files /dev/null and b/inspector/skin/modern/btnSelecting.gif differ
diff --git a/inspector/skin/modern/iconImportant.gif b/inspector/skin/modern/iconImportant.gif
new file mode 100644
index 00000000..b5d34635
Binary files /dev/null and b/inspector/skin/modern/iconImportant.gif differ
diff --git a/inspector/skin/modern/iconViewerList-dis.gif b/inspector/skin/modern/iconViewerList-dis.gif
new file mode 100644
index 00000000..55d087b4
Binary files /dev/null and b/inspector/skin/modern/iconViewerList-dis.gif differ
diff --git a/inspector/skin/modern/iconViewerList.gif b/inspector/skin/modern/iconViewerList.gif
new file mode 100644
index 00000000..d95292f1
Binary files /dev/null and b/inspector/skin/modern/iconViewerList.gif differ
diff --git a/inspector/skin/modern/iconViewerMenu-dis.gif b/inspector/skin/modern/iconViewerMenu-dis.gif
new file mode 100644
index 00000000..cc019420
Binary files /dev/null and b/inspector/skin/modern/iconViewerMenu-dis.gif differ
diff --git a/inspector/skin/modern/iconViewerMenu.gif b/inspector/skin/modern/iconViewerMenu.gif
new file mode 100644
index 00000000..ed314dad
Binary files /dev/null and b/inspector/skin/modern/iconViewerMenu.gif differ
diff --git a/inspector/skin/modern/inspector.css b/inspector/skin/modern/inspector.css
new file mode 100644
index 00000000..c8f70a02
--- /dev/null
+++ b/inspector/skin/modern/inspector.css
@@ -0,0 +1,6 @@
+/* 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/. */
+
+@import url("chrome://global/skin");
+@import url("chrome://inspector/content/inspector.css");
diff --git a/inspector/skin/modern/inspectorWindow.css b/inspector/skin/modern/inspectorWindow.css
new file mode 100644
index 00000000..e03ae1d5
--- /dev/null
+++ b/inspector/skin/modern/inspectorWindow.css
@@ -0,0 +1,49 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://navigator/skin");
+@import url("chrome://browser/skin");
+
+#tbxInsToolbox {
+ border-bottom: none;
+}
+
+#tbInspectorPrimary {
+ background-image: url("chrome://communicator/skin/toolbar/prtb-bg-noline.gif");
+}
+
+#bxURLBar {
+ padding: 2px;
+}
+
+#bxSearch {
+ height: 150px;
+}
+
+/* ::::: toolbar buttons ::::: */
+
+#btnSelecting {
+ list-style-image: url("chrome://inspector/skin/btnSelecting.gif");
+}
+
+#btnSelecting[checked="true"] {
+ list-style-image: url("chrome://inspector/skin/btnSelecting-act.gif");
+}
+
+#btnSelecting[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/btnSelecting-dis.gif");
+}
+
+#btnFind {
+ list-style-image: url("chrome://inspector/skin/btnFind.gif");
+}
+
+#btnFind[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/btnFind-dis.gif");
+}
+
+.viewer-list, .viewer-menu {
+ -moz-user-focus: ignore;
+}
diff --git a/inspector/skin/modern/jar.mn b/inspector/skin/modern/jar.mn
new file mode 100644
index 00000000..5ca9b943
--- /dev/null
+++ b/inspector/skin/modern/jar.mn
@@ -0,0 +1,37 @@
+# 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/.
+
+#filter substitution
+
+[.] chrome.jar:
+% skin @INSPECTOR_CHROME_NAME@ modern/1.0 chrome/skin/modern/
+ skin/modern/btnFind-dis.gif
+ skin/modern/btnFind.gif
+ skin/modern/btnSelecting-act.gif
+ skin/modern/btnSelecting-dis.gif
+ skin/modern/btnSelecting.gif
+ skin/modern/iconImportant.gif
+ skin/modern/iconViewerList-dis.gif
+ skin/modern/iconViewerList.gif
+ skin/modern/iconViewerMenu.gif
+ skin/modern/iconViewerMenu-dis.gif
+ skin/modern/ImageSearchItem.gif
+ skin/modern/inspector.css
+ skin/modern/inspectorWindow.css
+ skin/modern/panelset.css
+ skin/modern/sidebar.css
+ skin/modern/titledsplitter-close.gif
+ skin/modern/titledSplitter.css
+ skin/modern/viewers/accessibleEvent/accessibleEvent.css (viewers/accessibleEvent/accessibleEvent.css)
+ skin/modern/viewers/accessibleEvents/accessibleEvents.css (viewers/accessibleEvents/accessibleEvents.css)
+ skin/modern/viewers/accessibleProps/accessibleProps.css (viewers/accessibleProps/accessibleProps.css)
+ skin/modern/viewers/accessibleTree/accessibleTree.css (viewers/accessibleTree/accessibleTree.css)
+ skin/modern/viewers/boxModel/boxModel.css (viewers/boxModel/boxModel.css)
+ skin/modern/viewers/dom/columnsDialog.css (viewers/dom/columnsDialog.css)
+ skin/modern/viewers/dom/dom.css (viewers/dom/dom.css)
+ skin/modern/viewers/dom/findDialog.css (viewers/dom/findDialog.css)
+ skin/modern/viewers/domNode/domNode.css (viewers/domNode/domNode.css)
+ skin/modern/viewers/styleRules/styleRules.css (viewers/styleRules/styleRules.css)
+ skin/modern/viewers/xblBindings/xblBindings.css (viewers/xblBindings/xblBindings.css)
+
diff --git a/inspector/skin/modern/moz.build b/inspector/skin/modern/moz.build
new file mode 100644
index 00000000..e0eb66aa
--- /dev/null
+++ b/inspector/skin/modern/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
diff --git a/inspector/skin/modern/panelset.css b/inspector/skin/modern/panelset.css
new file mode 100644
index 00000000..f90e8836
--- /dev/null
+++ b/inspector/skin/modern/panelset.css
@@ -0,0 +1,37 @@
+/* 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/. */
+
+domi-panel[disabled="true"] > .viewer-pane-box-1 {
+ visibility: collapse;
+}
+
+.viewer-menu, .viewer-list {
+ -moz-user-focus: normal;
+}
+
+.viewer-list {
+ list-style-image: url("chrome://inspector/skin/iconViewerList.gif");
+}
+
+.viewer-list[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/iconViewerList-dis.gif");
+}
+
+.viewer-menu {
+ list-style-image: url("chrome://inspector/skin/iconViewerMenu.gif");
+}
+
+.viewer-menu[disabled="true"] {
+ list-style-image: url("chrome://inspector/skin/iconViewerMenu-dis.gif");
+}
+
+.viewer-menu > .toolbarbutton-menu-dropmarker {
+ display: none;
+}
+
+.viewer-pane-toolbox {
+ border-top: none;
+ border-bottom: none;
+}
+
diff --git a/inspector/skin/modern/sidebar.css b/inspector/skin/modern/sidebar.css
new file mode 100644
index 00000000..1f186ef5
--- /dev/null
+++ b/inspector/skin/modern/sidebar.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+tabpanels {
+ padding: 0px;
+}
\ No newline at end of file
diff --git a/inspector/skin/modern/titledSplitter.css b/inspector/skin/modern/titledSplitter.css
new file mode 100644
index 00000000..9101232d
--- /dev/null
+++ b/inspector/skin/modern/titledSplitter.css
@@ -0,0 +1,54 @@
+/* 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/. */
+
+.titled-splitter {
+ border: 1px solid #000000;
+ background-color: #5B7693;
+ cursor: default;
+}
+
+box[orient="vertical"] > .titled-splitter {
+ cursor: default;
+}
+
+.titledsplitter-container {
+ padding-bottom: 2px;
+}
+
+.titledsplitter-dragbar {
+ border-top: 1px solid #92ABC9;
+ min-height: 2px;
+ cursor: n-resize;
+}
+
+.titledsplitter-titlebox {
+ padding: 0px 5px 0px 5px;
+}
+
+.titledsplitter-text {
+ margin: 0px !important;
+ font-weight: bold;
+ color: #ffffff;
+}
+
+.titledsplitter-closebutton {
+ -moz-binding: none;
+ margin: 1px;
+ border: 1px outset #C7D0D9;
+ -moz-border-radius: 0px !important;
+ padding: 2px;
+ min-width: 0px;
+ background-color: #C7D0D9;
+}
+
+.titledsplitter-closeimage {
+ list-style-image: url("chrome://inspector/skin/titledsplitter-close.gif");
+}
+
+.titledsplitter-closebutton:hover:active {
+ border-style: inset;
+ padding: 3px 1px 1px 3px;
+}
+
+
diff --git a/inspector/skin/modern/titledsplitter-close.gif b/inspector/skin/modern/titledsplitter-close.gif
new file mode 100644
index 00000000..a53eddd0
Binary files /dev/null and b/inspector/skin/modern/titledsplitter-close.gif differ
diff --git a/inspector/skin/modern/viewers/accessibleEvent/accessibleEvent.css b/inspector/skin/modern/viewers/accessibleEvent/accessibleEvent.css
new file mode 100644
index 00000000..8cb49195
--- /dev/null
+++ b/inspector/skin/modern/viewers/accessibleEvent/accessibleEvent.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin/viewers/accessibleTree/accessibleTree.css");
diff --git a/inspector/skin/modern/viewers/accessibleEvents/accessibleEvents.css b/inspector/skin/modern/viewers/accessibleEvents/accessibleEvents.css
new file mode 100644
index 00000000..6a67d368
--- /dev/null
+++ b/inspector/skin/modern/viewers/accessibleEvents/accessibleEvents.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+treechildren::-moz-tree-checkbox {
+ list-style-image: url("chrome://global/skin/checkbox/cbox.gif");
+}
+
+treechildren::-moz-tree-checkbox(checked) {
+ list-style-image: url("chrome://global/skin/checkbox/cbox-check.gif");
+}
+
+#welHandlerEditor {
+ font-family: monospace;
+}
+
+#welGrippyButton {
+ cursor: default;
+}
diff --git a/inspector/skin/modern/viewers/accessibleProps/accessibleProps.css b/inspector/skin/modern/viewers/accessibleProps/accessibleProps.css
new file mode 100644
index 00000000..227827b4
--- /dev/null
+++ b/inspector/skin/modern/viewers/accessibleProps/accessibleProps.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+.textAttrsTextRange {
+ border-bottom: medium dotted blue;
+ padding: 7px;
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+ line-height: 250%;
+ margin: 2px;
+}
+
+.textAttrsTextRange:hover {
+ border-bottom: medium dotted red;
+}
+
+.textAttrsTextRange[selected] {
+ border-bottom: medium groove purple;
+}
diff --git a/inspector/skin/modern/viewers/accessibleTree/accessibleTree.css b/inspector/skin/modern/viewers/accessibleTree/accessibleTree.css
new file mode 100644
index 00000000..d2fcea73
--- /dev/null
+++ b/inspector/skin/modern/viewers/accessibleTree/accessibleTree.css
@@ -0,0 +1,22 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+treechildren::-moz-tree-row(highlight) {
+ background-color: #feeecd;
+}
+
+treechildren::-moz-tree-row(selected, highlight) {
+ background-color: #c0ffff;
+}
+
+treechildren::-moz-tree-cell-text(highlight) {
+ color: black;
+}
+
+treechildren::-moz-tree-cell-text(grayout) {
+ color: gray;
+ font-weight: bold;
+}
diff --git a/inspector/skin/modern/viewers/boxModel/boxModel.css b/inspector/skin/modern/viewers/boxModel/boxModel.css
new file mode 100644
index 00000000..5be126a4
--- /dev/null
+++ b/inspector/skin/modern/viewers/boxModel/boxModel.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+/* ::::: statistics ::::: */
+
+.nonzero {
+ font-weight: bold;
+}
+
+groupbox:not(#boxBorder) textbox {
+ text-align: right;
+}
+
+row {
+ -moz-box-align: baseline;
+}
+
+#enclosingBox {
+ overflow: auto;
+}
diff --git a/inspector/skin/modern/viewers/dom/columnsDialog.css b/inspector/skin/modern/viewers/dom/columnsDialog.css
new file mode 100644
index 00000000..e82e3dc9
--- /dev/null
+++ b/inspector/skin/modern/viewers/dom/columnsDialog.css
@@ -0,0 +1,26 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+#trColumns {
+ background-color: #C7D0D9;
+}
+
+.column-selector {
+ border: 1px outset #C7D0D9;
+ padding: 2px;
+ background-color: #C7D0D9;
+ vertical-align: middle;
+}
+
+.attr-column-selector,
+.attr-column-selector[focused="true"] {
+ margin: 0;
+ border: none;
+}
+
+[col-dragging="true"] {
+ border: 1px solid black;
+}
diff --git a/inspector/skin/modern/viewers/dom/dom.css b/inspector/skin/modern/viewers/dom/dom.css
new file mode 100644
index 00000000..0b35a901
--- /dev/null
+++ b/inspector/skin/modern/viewers/dom/dom.css
@@ -0,0 +1,59 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+
+/* :::::::: node type color coding :::::::: */
+
+treechildren::-moz-tree-cell-text(ACCESSIBLE_NODE) {
+ font-weight: bold;
+}
+
+treechildren::-moz-tree-cell-text(ELEMENT_NODE) {
+ color: #000000;
+}
+
+treechildren::-moz-tree-cell-text(ATTRIBUTE_NODE) {
+ color: #556b2f;
+}
+
+treechildren::-moz-tree-cell-text(CDATA_SECTION_NODE),
+treechildren::-moz-tree-cell-text(TEXT_NODE) {
+ color: #0000AA;
+}
+
+treechildren::-moz-tree-cell-text(COMMENT_NODE) {
+ color: #228b22;
+}
+
+treechildren::-moz-tree-cell-text(DOCUMENT_NODE) {
+ color: #800080;
+}
+
+treechildren::-moz-tree-cell-text(DOCUMENT_TYPE_NODE) {
+ color: #606000;
+}
+
+treechildren::-moz-tree-cell-text(PROCESSING_INSTRUCTION_NODE) {
+ color: #808080;
+}
+
+treechildren::-moz-tree-cell-text(anonymous) {
+ color: #ff0000;
+}
+
+treechildren::-moz-tree-cell-text(selected, focus) {
+ color: #FFFFFF;
+}
+
+/* :::::::: drag and drop insertion indicators :::::::: */
+
+treechildren::-moz-tree-column(dnd-insert-before) {
+ border-left: 2px solid #000000;
+}
+
+treechildren::-moz-tree-column(dnd-insert-after) {
+ border-right: 2px solid #000000;
+}
+
diff --git a/inspector/skin/modern/viewers/dom/findDialog.css b/inspector/skin/modern/viewers/dom/findDialog.css
new file mode 100644
index 00000000..c201f993
--- /dev/null
+++ b/inspector/skin/modern/viewers/dom/findDialog.css
@@ -0,0 +1,5 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
diff --git a/inspector/skin/modern/viewers/domNode/domNode.css b/inspector/skin/modern/viewers/domNode/domNode.css
new file mode 100644
index 00000000..882f9a7c
--- /dev/null
+++ b/inspector/skin/modern/viewers/domNode/domNode.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://inspector/content/extensions/treeEditable.css");
+
+.olNodeInfoLabel {
+ padding-right: 0.5em;
+}
+#olNodeInfo {
+ padding-bottom: 0.5em;
+}
diff --git a/inspector/skin/modern/viewers/styleRules/styleRules.css b/inspector/skin/modern/viewers/styleRules/styleRules.css
new file mode 100644
index 00000000..90b457e6
--- /dev/null
+++ b/inspector/skin/modern/viewers/styleRules/styleRules.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://inspector/content/extensions/treeEditable.css");
+
+#olcPropPriority {
+ list-style-image: url("chrome://inspector/skin/iconImportant.gif");
+}
+
+#olbStyleProps::-moz-tree-image(olcPropPriority, important) {
+ list-style-image: url("chrome://inspector/skin/iconImportant.gif");
+}
+
+/* ::::: default column widths ::::: */
+
+#olcLine {
+ width: 5em;
+}
+
diff --git a/inspector/skin/modern/viewers/xblBindings/xblBindings.css b/inspector/skin/modern/viewers/xblBindings/xblBindings.css
new file mode 100644
index 00000000..e943e896
--- /dev/null
+++ b/inspector/skin/modern/viewers/xblBindings/xblBindings.css
@@ -0,0 +1,6 @@
+/* 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/. */
+
+@import url("chrome://inspector/skin");
+@import url("chrome://inspector/skin/viewers/dom/dom.css");
diff --git a/inspector/skin/moz.build b/inspector/skin/moz.build
new file mode 100644
index 00000000..a36fb58c
--- /dev/null
+++ b/inspector/skin/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'classic',
+ 'modern',
+]
+
diff --git a/mail/app.mozbuild b/mail/app.mozbuild
index 39f0ed49..0634e822 100644
--- a/mail/app.mozbuild
+++ b/mail/app.mozbuild
@@ -3,7 +3,11 @@
# 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/.
-include('/communicator/communicator.mozbuild')
+DIRS += [
+ '/modules/ldap',
+ '/modules/mork',
+ '/mailnews',
+]
if not CONFIG['MOZ_INCOMPLETE_EXTERNAL_LINKAGE']:
DIRS += ['/mail/components']
@@ -15,6 +19,16 @@ if CONFIG['MOZ_EXTENSIONS']:
DIRS += ['/%s' % CONFIG['MOZ_BRANDING_DIRECTORY']]
+if CONFIG['MOZ_COMPOSER']:
+ DIRS += ['/editor/ui']
+
+if CONFIG['MOZ_CALENDAR']:
+ DIRS += [
+ '/modules/libical',
+ '/calendar/lightning',
+ '/calendar/timezones'
+ ]
+
# Never add tier dirs after mail because they apparently won't get
# packaged properly on Mac.
DIRS += ['/mail']
\ No newline at end of file
diff --git a/mail/configure.in b/mail/configure.in
index c5845b37..953ec3b4 100644
--- a/mail/configure.in
+++ b/mail/configure.in
@@ -18,7 +18,6 @@ AC_DEFINE(BINOC_INTERLINK)
AC_SUBST(MOZ_BUNDLED_FONTS)
AC_DEFINE(MOZ_SEPARATE_MANIFEST_FOR_THEME_OVERRIDES)
-AC_SUBST(MOZ_MAILNEWS)
AC_SUBST(MOZ_COMPOSER)
AC_SUBST(MOZ_LDAP_XPCOM)
diff --git a/mail/confvars.sh b/mail/confvars.sh
index 1b74cbf5..48687f32 100755
--- a/mail/confvars.sh
+++ b/mail/confvars.sh
@@ -22,7 +22,6 @@ BINOC_INTERLINK=1
# Comm build options
MOZ_MORK=1
MOZ_LDAP_XPCOM=1
-MOZ_MAILNEWS=1
MOZ_COMPOSER=1
MOZ_CALENDAR=
MOZ_WEBGL_CONFORMANT=1
diff --git a/profile-switcher/app.mozbuild b/profile-switcher/app.mozbuild
new file mode 100644
index 00000000..a9e3d482
--- /dev/null
+++ b/profile-switcher/app.mozbuild
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+if not CONFIG['MOZ_DISABLE_PLATFORM']:
+ error('Please add --disable-platform to your mozconfig')
+
+# Never add tier dirs after the application srcdir because they
+# apparently won't get packaged properly on Mac.
+DIRS += ['/profile-switcher']
diff --git a/profile-switcher/build.mk b/profile-switcher/build.mk
new file mode 100644
index 00000000..a76b49aa
--- /dev/null
+++ b/profile-switcher/build.mk
@@ -0,0 +1,7 @@
+# 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/.
+
+package:
+ cd $(DIST)/bin; \
+ zip -Dr9X ../${PROFILESWITCHER_XPI_NAME}-${PROFILESWITCHER_VERSION}.xpi * -x \*/.mkdir.done; \
diff --git a/profile-switcher/confvars.configure b/profile-switcher/confvars.configure
new file mode 100644
index 00000000..7fa7fc87
--- /dev/null
+++ b/profile-switcher/confvars.configure
@@ -0,0 +1,29 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# Templates apperently ARE allowed to use set_config and set_define and can use loops
+# so take advantage of that to apply the old axion_cmake defines without the need of
+# confvars.sh or configure.in
+
+@template
+def ConfVars(mode='moz.configure'):
+ confvars = {
+ 'ADDON_NAME': 'Profile Switcher',
+ 'ADDON_ID': 'profileswitcher@projects.binaryoutcast.com',
+ 'ADDON_VERSION': '1.0.1',
+ 'ADDON_AUTHOR': 'Binary Outcast',
+ 'ADDON_SHORT_DESC': 'Easily switch profiles',
+ 'ADDON_XPI_NAME': 'profile-switcher',
+ 'ADDON_CHROME_NAME': 'profile-switcher'
+ }
+
+ if mode == 'moz.configure':
+ for key, value in confvars.iteritems():
+ set_config(key, value)
+ set_define(key, value)
+ elif mode == 'moz.build':
+ for key, value in confvars.iteritems():
+ DEFINES[key] = value
diff --git a/profile-switcher/content/overlay.js b/profile-switcher/content/overlay.js
new file mode 100644
index 00000000..c77cc1f5
--- /dev/null
+++ b/profile-switcher/content/overlay.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+function toProfileManager()
+{
+ var promgrWin = Services.wm.getMostRecentWindow("mozilla:profileSelection");
+ if (promgrWin) {
+ promgrWin.focus();
+ } else {
+ var params = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Components.interfaces.nsIDialogParamBlock);
+
+ params.SetNumberStrings(1);
+ params.SetString(0, "menu");
+ window.openDialog("chrome://profile-switcher/content/profileSelection.xul",
+ "",
+ "centerscreen,chrome,titlebar,centerscreen,modal",
+ params);
+ }
+ // Here, we don't care about the result code
+ // that was returned in the param block.
+}
+
+
diff --git a/profile-switcher/content/overlay.xul b/profile-switcher/content/overlay.xul
new file mode 100644
index 00000000..506217a5
--- /dev/null
+++ b/profile-switcher/content/overlay.xul
@@ -0,0 +1,31 @@
+
+
+
+
+
+%overlayDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/profile-switcher/content/profileSelection.js b/profile-switcher/content/profileSelection.js
new file mode 100644
index 00000000..21a782b8
--- /dev/null
+++ b/profile-switcher/content/profileSelection.js
@@ -0,0 +1,177 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var gProfileBundle;
+var gBrandBundle;
+var gProfileService;
+var gPromptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+var gProfileManagerMode = "selection";
+var gDialogParams = window.arguments[0]
+ .QueryInterface(Components.interfaces.nsIDialogParamBlock);
+
+function StartUp()
+{
+ gProfileBundle = document.getElementById("bundle_profile");
+ gBrandBundle = document.getElementById("bundle_brand");
+ if (gDialogParams.objects) {
+ document.documentElement.getButton("accept").setAttribute("label",
+ document.documentElement.getAttribute("buttonlabelstart"));
+ document.documentElement.getButton("cancel").setAttribute("label",
+ document.documentElement.getAttribute("buttonlabelexit"));
+ document.getElementById('offlineState').hidden = false;
+ gDialogParams.SetInt(0, 0);
+ }
+
+ gProfileService = Components.classes["@mozilla.org/toolkit/profile-service;1"]
+ .getService(Components.interfaces.nsIToolkitProfileService);
+ var profileEnum = gProfileService.profiles;
+ var selectedProfile = null;
+ try {
+ selectedProfile = gProfileService.selectedProfile;
+ }
+ catch (ex) {
+ }
+ while (profileEnum.hasMoreElements()) {
+ AddItem(profileEnum.getNext().QueryInterface(Components.interfaces.nsIToolkitProfile),
+ selectedProfile);
+ }
+
+ var autoSelect = document.getElementById("autoSelect");
+ if (Services.prefs.getBoolPref("profile.manage_only_at_launch"))
+ autoSelect.hidden = true;
+ else
+ autoSelect.checked = gProfileService.startWithLastProfile;
+}
+
+// function : ::AddItem();
+// purpose : utility function for adding items to a tree.
+function AddItem(aProfile, aProfileToSelect)
+{
+ var tree = document.getElementById("profiles");
+ var treeitem = document.createElement("treeitem");
+ var treerow = document.createElement("treerow");
+ var treecell = document.createElement("treecell");
+ var treetip = document.getElementById("treetip");
+ var profileDir = gProfileService.getProfileByName(aProfile.name).rootDir;
+
+ treecell.setAttribute("label", aProfile.name);
+ treerow.appendChild(treecell);
+ treeitem.appendChild(treerow);
+ treeitem.setAttribute("tooltip", profileDir.path);
+ treetip.setAttribute("value", profileDir.path);
+ tree.lastChild.appendChild(treeitem);
+ treeitem.profile = aProfile;
+ if (aProfile == aProfileToSelect) {
+ var profileIndex = tree.view.getIndexOfItem(treeitem);
+ tree.view.selection.select(profileIndex);
+ tree.treeBoxObject.ensureRowIsVisible(profileIndex);
+ }
+}
+
+// function : ::AcceptDialog();
+// purpose : sets the current profile to the selected profile (user choice: "Start Mozilla")
+function AcceptDialog()
+{
+ var autoSelect = document.getElementById("autoSelect");
+ gProfileService.startWithLastProfile = autoSelect.checked;
+ gProfileService.flush();
+
+ var profileTree = document.getElementById("profiles");
+ var selected = profileTree.view.getItemAtIndex(profileTree.currentIndex);
+
+ if (!gDialogParams.objects) {
+ var dirServ = Components.classes['@mozilla.org/file/directory_service;1']
+ .getService(Components.interfaces.nsIProperties);
+ var profD = dirServ.get("ProfD", Components.interfaces.nsIFile);
+ var profLD = dirServ.get("ProfLD", Components.interfaces.nsIFile);
+
+ if (selected.profile.rootDir.equals(profD) &&
+ selected.profile.localDir.equals(profLD))
+ return true;
+ }
+
+ try {
+ var profileLock = selected.profile.lock({});
+ gProfileService.selectedProfile = selected.profile;
+ gProfileService.defaultProfile = selected.profile;
+ gProfileService.flush();
+ if (gDialogParams.objects) {
+ gDialogParams.objects.insertElementAt(profileLock, 0);
+ gProfileService.startOffline = document.getElementById("offlineState").checked;
+ gDialogParams.SetInt(0, 1);
+ gDialogParams.SetString(0, selected.profile.name);
+ return true;
+ }
+ profileLock.unlock();
+ } catch (e) {
+ var brandName = gBrandBundle.getString("brandShortName");
+ var message = gProfileBundle.getFormattedString("dirLocked",
+ [brandName, selected.profile.name]);
+ gPromptService.alert(window, null, message);
+ return false;
+ }
+
+ // Although switching profile works by performing a restart internally,
+ // the user is quitting the old profile, so make it look like a quit.
+ var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
+ Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .notifyObservers(cancelQuit, "quit-application-requested", null);
+ if (cancelQuit.data)
+ return false;
+
+ try {
+ var env = Components.classes["@mozilla.org/process/environment;1"]
+ .getService(Components.interfaces.nsIEnvironment);
+ env.set("XRE_PROFILE_NAME", selected.profile.name);
+ env.set("XRE_PROFILE_PATH", selected.profile.rootDir.path);
+ env.set("XRE_PROFILE_LOCAL_PATH", selected.profile.localDir.path);
+ var app = Components.classes["@mozilla.org/toolkit/app-startup;1"]
+ .getService(Components.interfaces.nsIAppStartup);
+ app.quit(app.eAttemptQuit | app.eRestart);
+ return true;
+ }
+ catch (e) {
+ env.set("XRE_PROFILE_NAME", "");
+ env.set("XRE_PROFILE_PATH", "");
+ env.set("XRE_PROFILE_LOCAL_PATH", "");
+ return false;
+ }
+}
+
+// handle key event on tree
+function HandleKeyEvent(aEvent)
+{
+ if (gProfileManagerMode != "manager")
+ return;
+
+}
+
+function HandleClickEvent(aEvent)
+{
+ if (aEvent.button == 0 && aEvent.target.parentNode.view.selection.count != 0 && AcceptDialog()) {
+ window.close();
+ return true;
+ }
+
+ return false;
+}
+
+function HandleToolTipEvent(aEvent)
+{
+ var treeTip = document.getElementById("treetip");
+ var tree = document.getElementById("profiles");
+
+ var cell = tree.treeBoxObject.getCellAt(aEvent.clientX, aEvent.clientY);
+ if (cell.row < 0)
+ aEvent.preventDefault();
+ else
+ treeTip.label = tree.view.getItemAtIndex(cell.row).tooltip;
+}
diff --git a/profile-switcher/content/profileSelection.xul b/profile-switcher/content/profileSelection.xul
new file mode 100644
index 00000000..8a2c43df
--- /dev/null
+++ b/profile-switcher/content/profileSelection.xul
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+%brandDTD;
+
+%profileDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/profile-switcher/install.rdf b/profile-switcher/install.rdf
new file mode 100644
index 00000000..1ed8626f
--- /dev/null
+++ b/profile-switcher/install.rdf
@@ -0,0 +1,40 @@
+
+
+
+
+#filter substitution
+
+
+
+
+#ifdef MOZ_DISABLE_PLATFORM
+
+
+
+
+
+
+#else
+
+
+
+ true
+#endif
+
+
+
diff --git a/profile-switcher/jar.mn b/profile-switcher/jar.mn
new file mode 100644
index 00000000..9c1e2952
--- /dev/null
+++ b/profile-switcher/jar.mn
@@ -0,0 +1,23 @@
+# 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/.
+
+[.] chrome.jar:
+% content profile-switcher %content/
+ content/profileSelection.js (content/profileSelection.js)
+ content/profileSelection.xul (content/profileSelection.xul)
+ content/overlay.js (content/overlay.js)
+ content/overlay.xul (content/overlay.xul)
+
+% locale profile-switcher en-US %locale/
+ locale/profileSelection.dtd (locale/profileSelection.dtd)
+ locale/profileSelection.properties (locale/profileSelection.properties)
+ locale/overlay.dtd (locale/overlay.dtd)
+
+% skin profile-switcher classic/1.0 %skin/
+ skin/migrate.gif (skin/migrate.gif)
+ skin/profile.css (skin/profile.css)
+ skin/profileManager.css (skin/profileManager.css)
+
+% overlay chrome://browser/content/browser.xul chrome://profile-switcher/content/overlay.xul
+% overlay chrome://messenger/content/messenger.xul chrome://profile-switcher/content/overlay.xul
diff --git a/profile-switcher/locale/overlay.dtd b/profile-switcher/locale/overlay.dtd
new file mode 100644
index 00000000..47df2c4c
--- /dev/null
+++ b/profile-switcher/locale/overlay.dtd
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/profile-switcher/locale/profileSelection.dtd b/profile-switcher/locale/profileSelection.dtd
new file mode 100644
index 00000000..1f89de66
--- /dev/null
+++ b/profile-switcher/locale/profileSelection.dtd
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/profile-switcher/locale/profileSelection.properties b/profile-switcher/locale/profileSelection.properties
new file mode 100644
index 00000000..5bf2a0ca
--- /dev/null
+++ b/profile-switcher/locale/profileSelection.properties
@@ -0,0 +1,6 @@
+# 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/.
+
+dirLocked=%S cannot use the profile "%S". It may be in use, unavailable or damaged.\n\nPlease choose another profile or create a new one.
+
diff --git a/profile-switcher/moz.build b/profile-switcher/moz.build
new file mode 100644
index 00000000..2e6e7a74
--- /dev/null
+++ b/profile-switcher/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+if not CONFIG['MOZ_DISABLE_PLATFORM']:
+ include('confvars.configure')
+ ConfVars('moz.build')
+ DIST_SUBDIR = 'extensions/%s' % DEFINES['PROFILESWITCHER_ID']
+ USE_EXTENSION_MANIFEST = True
+ DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+ DEFINES['MOZ_APP_ID'] = CONFIG['MOZ_APP_ID']
+
+if CONFIG['MOZ_DISABLE_PLATFORM']:
+ DEFINES['MOZ_DISABLE_PLATFORM'] = 1
+
+FINAL_TARGET_PP_FILES += ['install.rdf']
+
+JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file
diff --git a/profile-switcher/moz.configure b/profile-switcher/moz.configure
new file mode 100644
index 00000000..fe82fcc4
--- /dev/null
+++ b/profile-switcher/moz.configure
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+include('../build/moz.configure/platform.configure')
+include('confvars.configure')
+ConfVars('moz.configure')
diff --git a/profile-switcher/skin/migrate.gif b/profile-switcher/skin/migrate.gif
new file mode 100644
index 00000000..5b438099
Binary files /dev/null and b/profile-switcher/skin/migrate.gif differ
diff --git a/profile-switcher/skin/profile.css b/profile-switcher/skin/profile.css
new file mode 100644
index 00000000..9fd5ab35
--- /dev/null
+++ b/profile-switcher/skin/profile.css
@@ -0,0 +1,61 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * 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/. */
+
+@import url("chrome://global/skin/global.css");
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+treechildren::-moz-tree-image {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://mozapps/skin/profile/profileicon.png");
+}
+
+treechildren::-moz-tree-image(rowMigrate-no) {
+ list-style-image: url("chrome://profile-switcher/skin/migrate.gif");
+}
+
+/* profile selection dialog */
+
+/* Override global.css */
+hbox.wizard-box {
+ padding: 10px 10px 10px 10px;
+}
+
+#header {
+ -moz-box-orient: vertical;
+ margin-top: -8px;
+ margin-bottom: 0;
+ margin-inline-start: -8px;
+ margin-inline-end: -10px;
+ border-left: none;
+ border-right: none;
+ border-top: none;
+ -moz-border-bottom-colors: ThreeDHighlight ThreeDShadow;
+ padding-top: 12px;
+ padding-bottom: 12px;
+ padding-inline-start: 25px;
+ padding-inline-end: 5px;
+ background-color: Window;
+ color: WindowText;
+}
+
+#header > .dialogheader-title {
+ font: inherit;
+ font-weight: bold;
+}
+
+#header > .dialogheader-description {
+ margin-inline-start: 12px !important;
+}
+
+#intro,
+#label {
+ width: 17em;
+}
+
+#managebuttons > button {
+ min-width: 8em;
+}
diff --git a/profile-switcher/skin/profileManager.css b/profile-switcher/skin/profileManager.css
new file mode 100644
index 00000000..5fc14dc6
--- /dev/null
+++ b/profile-switcher/skin/profileManager.css
@@ -0,0 +1,19 @@
+/* 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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#dialoginfo {
+ width: 576px;
+ height: 576px;
+}
+
+#table-housing {
+ background-color: white;
+ height: 100%;
+}
+
+#buttons-box {
+ margin-inline-start: 1em;
+}
diff --git a/scratchpad/app.mozbuild b/scratchpad/app.mozbuild
new file mode 100644
index 00000000..a2a77f31
--- /dev/null
+++ b/scratchpad/app.mozbuild
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+if not CONFIG['MOZ_DISABLE_PLATFORM']:
+ error('Please add --disable-platform to your mozconfig')
+
+# Never add tier dirs after the application srcdir because they
+# apparently won't get packaged properly on Mac.
+DIRS += ['/scratchpad']
diff --git a/scratchpad/build.mk b/scratchpad/build.mk
new file mode 100644
index 00000000..92fe1b14
--- /dev/null
+++ b/scratchpad/build.mk
@@ -0,0 +1,7 @@
+# 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/.
+
+package:
+ cd $(DIST)/bin; \
+ zip -Dr9X ../${ADDON_XPI_NAME}-${ADDON_VERSION}.xpi * -x \*/.mkdir.done; \
diff --git a/scratchpad/confvars.configure b/scratchpad/confvars.configure
new file mode 100644
index 00000000..ef9550d1
--- /dev/null
+++ b/scratchpad/confvars.configure
@@ -0,0 +1,29 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# Templates apperently ARE allowed to use set_config and set_define and can use loops
+# so take advantage of that to apply the old axion_cmake defines without the need of
+# confvars.sh or configure.in
+
+@template
+def ConfVars(mode='moz.configure'):
+ confvars = {
+ 'ADDON_NAME': 'Scratchpad',
+ 'ADDON_ID': 'scratchpad@projects.binaryoutcast.com',
+ 'ADDON_VERSION': '1.0.1',
+ 'ADDON_CREATOR': 'Binary Outcast',
+ 'ADDON_SHORT_DESC': 'Javascript Scratchpad',
+ 'ADDON_XPI_NAME': 'scratchpad',
+ 'ADDON_CHROME_NAME': 'scratchpad'
+ }
+
+ if mode == 'moz.configure':
+ for key, value in confvars.iteritems():
+ set_config(key, value)
+ set_define(key, value)
+ elif mode == 'moz.build':
+ for key, value in confvars.iteritems():
+ DEFINES[key] = value
diff --git a/scratchpad/content/orion/LICENSE b/scratchpad/content/orion/LICENSE
new file mode 100644
index 00000000..2d907d73
--- /dev/null
+++ b/scratchpad/content/orion/LICENSE
@@ -0,0 +1,29 @@
+Eclipse Distribution License - v 1.0
+
+Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+* Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/scratchpad/content/orion/Makefile.dryice.js b/scratchpad/content/orion/Makefile.dryice.js
new file mode 100644
index 00000000..9e0fdb50
--- /dev/null
+++ b/scratchpad/content/orion/Makefile.dryice.js
@@ -0,0 +1,89 @@
+#!/usr/bin/env node
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Source Editor component.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Mihai Sucan (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+var copy = require('dryice').copy;
+
+const ORION_EDITOR = "org.eclipse.orion.client.editor/web";
+
+var js_src = copy.createDataObject();
+
+copy({
+ source: [
+ ORION_EDITOR + "/orion/textview/global.js",
+ ORION_EDITOR + "/orion/textview/eventTarget.js",
+ ORION_EDITOR + "/orion/editor/regex.js",
+ ORION_EDITOR + "/orion/textview/keyBinding.js",
+ ORION_EDITOR + "/orion/textview/annotations.js",
+ ORION_EDITOR + "/orion/textview/rulers.js",
+ ORION_EDITOR + "/orion/textview/undoStack.js",
+ ORION_EDITOR + "/orion/textview/textModel.js",
+ ORION_EDITOR + "/orion/textview/projectionTextModel.js",
+ ORION_EDITOR + "/orion/textview/tooltip.js",
+ ORION_EDITOR + "/orion/textview/textView.js",
+ ORION_EDITOR + "/orion/textview/textDND.js",
+ ORION_EDITOR + "/orion/editor/htmlGrammar.js",
+ ORION_EDITOR + "/orion/editor/textMateStyler.js",
+ ORION_EDITOR + "/examples/textview/textStyler.js",
+ ],
+ dest: js_src,
+});
+
+copy({
+ source: js_src,
+ dest: "orion.js",
+});
+
+var css_src = copy.createDataObject();
+
+copy({
+ source: [
+ ORION_EDITOR + "/orion/textview/textview.css",
+ ORION_EDITOR + "/orion/textview/rulers.css",
+ ORION_EDITOR + "/orion/textview/annotations.css",
+ ORION_EDITOR + "/examples/textview/textstyler.css",
+ ORION_EDITOR + "/examples/editor/htmlStyles.css",
+ ],
+ dest: css_src,
+});
+
+copy({
+ source: css_src,
+ dest: "orion.css",
+});
+
diff --git a/scratchpad/content/orion/README b/scratchpad/content/orion/README
new file mode 100644
index 00000000..c7669099
--- /dev/null
+++ b/scratchpad/content/orion/README
@@ -0,0 +1,43 @@
+# Introduction
+
+This is the Orion editor packaged for Mozilla.
+
+The Orion editor web site: http://www.eclipse.org/orion
+
+# Upgrade
+
+To upgrade Orion to a newer version see the UPGRADE file.
+
+Orion version: git clone from 2012-01-26
+ commit hash 1d1150131dacecc9f4d9eb3cdda9103ea1819045
+
+ + patch for Eclipse Bug 370584 - [Firefox] Edit menu items in context menus
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=137d5a8e9bbc0fa204caae74ebd25a7d9d4729bd
+ see https://bugs.eclipse.org/bugs/show_bug.cgi?id=370584
+
+ + patches for Eclipse Bug 370606 - Problems with UndoStack and deletions at
+ the beginning of the document
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=cec71bddaf32251c34d3728df5da13c130d14f33
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=3ce24b94f1d8103b16b9cf16f2f50a6302d43b18
+ http://git.eclipse.org/c/orion/org.eclipse.orion.client.git/commit/?id=27177e9a3dc70c20b4877e3eab3adfff1d56e342
+ see https://bugs.eclipse.org/bugs/show_bug.cgi?id=370606
+
+ + patch for Mozilla Bug 730532 - remove CSS2Properties aliases for MozOpacity
+ and MozOutline*
+ see https://bugzilla.mozilla.org/show_bug.cgi?id=730532#c3
+
+# License
+
+The following files are licensed according to the contents in the LICENSE
+file:
+ orion.js
+ orion.css
+
+# Theming
+
+The syntax highlighting and the editor UI are themed using a style sheet. The
+default theme file is browser/themes/*/devtools/orion.css - this is based on the
+orion.css found in this folder.
+
+Please note that the orion.css file from this folder is not used. It is kept
+here only as reference.
diff --git a/scratchpad/content/orion/UPGRADE b/scratchpad/content/orion/UPGRADE
new file mode 100644
index 00000000..a2c006ef
--- /dev/null
+++ b/scratchpad/content/orion/UPGRADE
@@ -0,0 +1,20 @@
+Upgrade notes:
+
+1. Get the Orion client source code from:
+http://www.eclipse.org/orion
+
+2. Install Dryice from:
+https://github.com/mozilla/dryice
+
+You also need nodejs for Dryice to run:
+http://nodejs.org
+
+3. Copy Makefile.dryice.js to:
+org.eclipse.orion.client/bundles/
+
+4. Execute Makefile.dryice.js. You should get orion.js and orion.css.
+
+5. Copy the two files back here.
+
+6. Make a new build of Firefox.
+
diff --git a/scratchpad/content/orion/orion.css b/scratchpad/content/orion/orion.css
new file mode 100644
index 00000000..85515975
--- /dev/null
+++ b/scratchpad/content/orion/orion.css
@@ -0,0 +1,273 @@
+.view {
+ background-color: white;
+}
+
+.viewContainer {
+ background-color: #eeeeee;
+ font-family: monospace;
+ font-size: 10pt;
+}
+::-webkit-scrollbar-corner {
+ background-color: #eeeeee;
+}
+
+.viewContent {
+}/* Styles for rulers */
+.ruler {
+ background-color: white;
+}
+.ruler.annotations {
+ border-right: 1px solid lightgray;
+ width: 16px;
+}
+.ruler.folding {
+ border-right: 1px solid lightgray;
+ width: 14px;
+}
+.ruler.lines {
+ border-right: 1px solid lightgray;
+ text-align: right;
+}
+.ruler.overview {
+ border-left: 1px solid lightgray;
+ width: 14px;
+}
+
+/* Styles for the line number ruler */
+.rulerLines {
+}
+.rulerLines.even
+.rulerLines.odd {
+}/* Styles for the annotation ruler (all lines) */
+.annotation {
+}
+.annotation.error,
+.annotation.warning
+.annotation.task,
+.annotation.bookmark,
+.annotation.breakpoint,
+.annotation.collapsed
+.annotation.expanded {
+}
+
+/* Styles for the annotation ruler (first line) */
+.annotationHTML {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.annotationHTML.error {
+ /* images/error.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAPVvcvWHiPVucvRuc+ttcfV6f91KVN5LU99PV/FZY/JhaM4oN84pONE4Rd1ATfJLWutVYPRgbdxpcsgWKMgZKs4lNfE/UvE/U+artcpdSc5uXveimslHPuBhW/eJhfV5efaCgO2CgP+/v+PExP///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACUALAAAAAAQABAAAAZ+wJJwSCwaScgkySgkjTQZTkYzWhadnE5oE+pwqkSshwQqkzxfa4kkQXxEpA9J9EFI1KQGQQBAigYCBA14ExEWF0gXihETeA0QD3AkD5QQg0NsDnAJmwkOd5gYFSQKpXAFDBhqaxgLBwQBBAapq00YEg0UDRKqTGtKSL7Cw8JBADs=");
+}
+.annotationHTML.warning {
+ /* images/warning.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAP7bc//egf/ij/7ijv/jl/7kl//mnv7lnv/uwf7CTP7DTf7DT/7IW//Na/7Na//NbP7QdP/dmbltAIJNAF03AMSAJMSCLKqASa2DS6uBSquCSrGHTq6ETbCHT7WKUrKIUcCVXL+UXMOYX8GWXsSZYMiib6+ETbOIUcOXX86uhd3Muf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACsALAAAAAAQABAAAAZowJVwSCwaj0ihikRSJYcoBEL0XKlGkcjImQQhJBREKFnyICoThKeE/AAW6AXgdPyUAgrLJBEo0YsbAQyDhAEdRRwDDw8OaA4NDQImRBgFEJdglxAEGEQZKQcHBqOkKRpFF6mqq1WtrUEAOw==");
+}
+.annotationHTML.task {
+ /* images/task.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQAMQAAN7s4uTy6ICvY423c2WdP2ugR3mqWYeza2ejOl6VNVqPM1aJMURsJ2GaOnKlT8PbsbPDqGmmO1OCLk98LEhxKGWfOWKaN0t2KkJoJf///////wAAAAAAAAAAAAAAAAAAACH5BAEAABoALAAAAAAQABAAAAVmoCaOZDk+UaquDxkNcCxHJHLceI6QleD/vkCmQrIYjkiDMGAhJRzQ6NKRICkKgYJ2qVWQFktCmEBYkCSNZSbQaDckpAl5TCZMSBdtAaDXX0gUUYJRFCQMSYgGDCQQGI6PkBAmkyUhADs=");
+}
+.annotationHTML.bookmark {
+ /* images/bookmark.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQALMAAP7//+/VNPzZS/vifeumAPrBOOSlHOSuRP///wAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAgALAAAAAAQABAAAARLEMlJq5Xn3EvIrkenfRIhCB5pmKhRdbAJAGhssuc8n6eJoAKdkOaTAIdEQeWoA1oGsiZhYAnIcqiApVPjElyUbkFSgCkn5XElLYkAADs=");
+}
+.annotationHTML.breakpoint {
+ /* images/breakpoint.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAFheoFxkoFxnpmt0pmZxpnF7rYyWwmJwpnaFs3aDrWt8rXGBrYycwmZ3mXuNs42cu77F03GIs3aJrYGVu2J5oKCuxeDj6LK/03GLrYieu3aIoIygu6m4zcLN3MTM1m6Rs2aLriRgkSZilXGXtoGcs7LD0QBLhSZikihol3ScubrO2Yaqu5q4xpO0wpm7yabF0ZO9yaXI0r3X3tHj6P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADQALAAAAAAQABAAAAafQJpwSCwWLYZBIDAwWIw0A+FFpW6aRUPCxe1yE4ahhdCCxWSzmSwGgxGeUceKpUqhUCkVa7UK0wgkJCUjJoUmIyWBBEIEGhoeJ4YmJx6OAUIADQ0QIZIhEJoAQgEUFBUgkiAVpZdRCxIPFx8iIh8XDw4FfhYHDhgZHB0dHBkYEwdwUQoTEc3OEwp+QwYHCBMMDBMIB9JESAJLAk5Q5EVBADs=");
+}
+.annotationHTML.collapsed {
+ /* images/collapsed.png */
+ width: 14px;
+ height: 14px;
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNpi/P//PwMlgImBQkCxASzoAp++fo+6de+Z+fXbD/Jev/nAICoiwKCpqrBBTUlqNR835zJ09YzIYfDxy7eo/cevLmXlYGNQUJAEahZieP3mHcODB08Zfv/4w+BoqR3Nz8O1DKcXzt94HPqXmZlBU1+LgZNfkMHazIOBA0hr6uswgMTP33gYijcMLlx/EMAnLs7w7sc/hg9AG0HgPZB+B8S84hJA+UcBeMPg+at3DJIMnAxZzt5wsUhnXzDdsmIVWB6vAcLCfAys3z4wzN64huEfkJ/uH8IwexOQDQymD2/fgeXxekFLRWHD51evGDhZGRi4WSFSnCwgNjB2Xr1m0AbK4zXAQkdhNdPf3wx3r91g+PruLcOqnasYvn54x3Dv2k0G5r+/GMyB8nijEQTefvoadeH6w9Cbtx8GvH//kUFQkJ9BQ1V+g76m/GphPu5lBA0YenmBYgMAAgwA34GIKjmLxOUAAAAASUVORK5CYII=");
+}
+.annotationHTML.expanded {
+ /* images/expanded.png */
+ width: 14px;
+ height: 14px;
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAT5JREFUeNrUksFKw0AURW+mTWw67SSEiG209U90r4jddFO34l+5U0HdZCHiFwiCOz9AlMSmGEpMOqk1TWJSFGyFbATR2dyZd+Dw3mOENE3xkyP8PYHrBT3OX7uW43ZefA6FUaw1dJPSyrmu1k8KBYOh37Od4XFZLEPXFdRrFMGIw3U9TKMYqw1tb0VjcxLy9eEF425CCIxWE5JcxSQGxCyNloG87gXhwWIHc4J767lTZQw8ShFGSZbxRyaQmZJxd3NRUJ6ffwQNEi6PzG/L2tjdmvFCgcKqKL2F2Olu43MzggDka+IjPuOFI7Sbujn2fUglYKkkzFIi+R0I/QDrGS8UqDX5QkhiOHYfE84hkhSTkGNgOyDJFCzjhYLTq+vDtrG8r1LZtB6fcHtzB+uhD5VWzLx+lvF/8JV/XfAuwADsrJbMGG4l4AAAAABJRU5ErkJggg==");
+}
+.annotationHTML.multiple {
+ /* images/multiple.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQANUAAOdpa+yJiuFYXOFYXeBYXONwded8f+NwdmhwkHB4iPr7/ezx+fP2+2h4kOzy+Wh4iPr8/gCBwTaczjaXyjaYyjaXyTaYyfr8/QCMzQCMzACHxzao2jal2Dak1zag03iAgI/Ckn64fZrHmX+4fZLCianPopPCiarOoqbLlafLlbnXq7nWq6fLlMTcsoCIeJCQcIiIeKCYaJiQcO16ee16evGVlfGWlfahn/ahoPWhn/WhoPe1tP///////wAAAAAAACH5BAEAAD0ALAAAAAAQABAAAAaRwJ5wSCwaj8WYcslcDmObaDTGq1Zjzw4mk+FQIRcFTzaUeTRoj4zHaI+HL0lkLnnxFgsH7zWEWSoTFBMwVlUwQy6JMDCJjYwuQx8tk5MfOzk4OjcfkSssKCkqHzY0MzQ1nEIJJSYkJCcJAQCzAQlDDyIjISMiCQYEAgMGD0MNIMfHDQUHBc3EQgjR0tPSSNY9QQA7");
+}
+.annotationHTML.overlay {
+ /* images/plus.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAAXNSR0IArs4c6QAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJEAQvB2JVdrAAAAAdaVRYdENvbW1lbnQAAAAAAENyZWF0ZWQgd2l0aCBHSU1QZC5lBwAAAD1JREFUCNdtjkESADAEAzemf69f66HMqGlOIhYiFRFRtSQBWAY7mzx+EDTL6sSgb1jTk7Q87rxyqe37fXsAa78gLyZnRgEAAAAASUVORK5CYII=");
+ background-position: right bottom;
+ position: relative;
+ top: -16px;
+}
+.annotationHTML.currentBracket {
+ /* images/currentBracket.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLEBULCGQmEKAAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAnklEQVQ4y7VTsRHDIBATJg1HCUzAHEzFBExAzwZsRMkE9gifKhc72ODYibr/+xcnoQdugq0LAujEwmbn0UxQh4OxpjX1XgshwFqLnPM5PQTQGlprWpbl3RhJ/CSQUm7qPYLp7i8cEpRSoJT6ju0lIaVEQgiKMQ4lHHpQayVjzHWCn5jIOcc8z9dMBADvPZxz3SC1tzCI8vgWdvL+VzwB8JSj2GFTyxIAAAAASUVORK5CYII=");
+}
+.annotationHTML.matchingBracket {
+ /* images/matchingBracket.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLEBUMAsuyb3kAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAoklEQVQ4y61TsQ3EIAw80DcI0USKGIApWIsB2IGGKbJPugxBR3VfvfRRCOSTvw7LPuPzGXgI8f0gwAsFu5rXIYMdDiEOIdnKW5YFzjnEGH+bhwA/KKVwmibu0BhRnpEZY1BrHTaVT7fQJZjnGeu63tOAJFNKVEox53yqQZfAWstt27oidgm01ve3UEqBaBjnspG89wgh3LiFgZXHt3Dh23/FGxKViehm0X85AAAAAElFTkSuQmCC");
+}
+.annotationHTML.currentLine {
+ /* images/currentLine.gif */
+ background-image: url("data:image/gif;base64,R0lGODlhEAAQAMQAALxe0bNWzbdZzrlb0KpPx61RybBTy6VLxadNxZGctIeUroyYsG92hHyMqIKRq2l9nmyAoHGDonaIpStXj6q80k1aXf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABYALAAAAAAQABAAAAVCoCWOZGmeKDql5ppOMGXBk/zOoltSNO6XrlXwxIPNYiMGq8SoLC2MaNPygEQkDYdikUg6LQcEoWAICAaA5HPNLoUAADs=");
+}
+
+/* Styles for the overview ruler */
+.annotationOverview {
+ cursor: pointer;
+ border-radius: 2px;
+ left: 2px;
+ width: 8px;
+}
+.annotationOverview.task {
+ background-color: lightgreen;
+ border: 1px solid green;
+}
+.annotationOverview.breakpoint {
+ background-color: lightblue;
+ border: 1px solid blue;
+}
+.annotationOverview.bookmark {
+ background-color: yellow;
+ border: 1px solid orange;
+}
+.annotationOverview.error {
+ background-color: lightcoral;
+ border: 1px solid darkred;
+}
+.annotationOverview.warning {
+ background-color: Gold;
+ border: 1px solid black;
+}
+.annotationOverview.currentBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+.annotationOverview.matchingBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+.annotationOverview.currentLine {
+ background-color: #EAF2FE;
+ border: 1px solid black;
+}
+
+/* Styles for text range */
+.annotationRange {
+ background-repeat: repeat-x;
+ background-position: left bottom;
+}
+.annotationRange.task {
+ /* images/squiggly_task.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLDhEoIrb7JmcAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAGUlEQVQI12NggIH/DGdhDCM45z/DfyiBAADgdQjGhI/4DAAAAABJRU5ErkJggg==");
+}
+.annotationRange.breakpoint {
+ /* images/squiggly_breakpoint.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sLDhEqHTKradgAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAIklEQVQI11XJMQ0AMAzAMGMafwrFlD19+sUKIJTFo9k+B/kQ+Qr2bIVKOgAAAABJRU5ErkJggg==");
+}
+.annotationRange.bookmark {
+ /* images/squiggly_bookmark.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
+}
+.annotationRange.error {
+ /* images/squiggly_error.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
+}
+.annotationRange.warning {
+ /* images/squiggly_warning.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
+}
+.annotationRange.currentBracket {
+}
+.annotationRange.matchingBracket {
+ outline: 1px solid red;
+}
+
+/* Styles for lines of text */
+.annotationLine {
+}
+.annotationLine.currentLine {
+ background-color: #EAF2FE;
+}
+
+.token_singleline_comment {
+ color: green;
+}
+
+.token_multiline_comment {
+ color: green;
+}
+
+.token_doc_comment {
+ color: #00008F;
+}
+
+.token_doc_html_markup {
+ color: #7F7F9F;
+}
+
+.token_doc_tag {
+ color: #7F9FBF;
+}
+
+.token_task_tag {
+ color: #7F9FBF;
+}
+
+.token_string {
+ color: blue;
+}
+
+.token_keyword {
+ color: darkred;
+ font-weight: bold;
+}
+
+.token_space {
+ /* images/white_space.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAAABnRSTlMA/wAAAACkwsAdAAAAIUlEQVR4nGP4z8CAC+GUIEXuABhgkTuABEiRw2cmae4EAH05X7xDolNRAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+.token_tab {
+ /* images/white_tab.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAIAAACJ2loDAAAABnRSTlMA/wD/AP83WBt9AAAAMklEQVR4nGP4TwRgoK6i52c3bz5w6zMSA6tJn28d2Lx589nnCAYu63AaSLxJRLoJPwAAeNk0aG4opfMAAAAASUVORK5CYII=");
+ background-repeat: no-repeat;
+ background-position: left center;
+}
+
+.line_caret {
+ background-color: #EAF2FE;
+}
+
+/* Styling for html syntax highlighting */
+.entity-name-tag {
+ color: #3f7f7f;
+}
+
+.entity-other-attribute-name {
+ color: #7f007f;
+}
+
+.punctuation-definition-comment {
+ color: #3f5fbf;
+}
+
+.comment {
+ color: #3f5fbf
+}
+
+.string-quoted {
+ color: #2a00ff;
+ font-style: italic;
+}
+
+.invalid {
+ color: red;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/scratchpad/content/orion/orion.js b/scratchpad/content/orion/orion.js
new file mode 100644
index 00000000..8b90d9f2
--- /dev/null
+++ b/scratchpad/content/orion/orion.js
@@ -0,0 +1,12303 @@
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ * Mihai Sucan (Mozilla Foundation) - fix for Bug#364214
+ */
+
+/*global window */
+
+/**
+ * Evaluates the definition function and mixes in the returned module with
+ * the module specified by moduleName.
+ *
+ * This function is intented to by used when RequireJS is not available.
+ *
+ *
+ * @param {String} name The mixin module name.
+ * @param {String[]} deps The array of dependency names.
+ * @param {Function} callback The definition function.
+ */
+if (!window.define) {
+ window.define = function(name, deps, callback) {
+ var module = this;
+ var split = (name || "").split("/"), i, j;
+ for (i = 0; i < split.length - 1; i++) {
+ module = module[split[i]] = (module[split[i]] || {});
+ }
+ var depModules = [], depModule;
+ for (j = 0; j < deps.length; j++) {
+ depModule = this;
+ split = deps[j].split("/");
+ for (i = 0; i < split.length - 1; i++) {
+ depModule = depModule[split[i]] = (depModule[split[i]] || {});
+ }
+ depModules.push(depModule);
+ }
+ var newModule = callback.apply(this, depModules);
+ for (var p in newModule) {
+ if (newModule.hasOwnProperty(p)) {
+ module[p] = newModule[p];
+ }
+ }
+ };
+}
+
+/**
+ * Require/get the defined modules.
+ *
+ * This function is intented to by used when RequireJS is not available.
+ *
+ *
+ * @param {String[]|String} deps The array of dependency names. This can also be
+ * a string, a single dependency name.
+ * @param {Function} [callback] Optional, the callback function to execute when
+ * multiple dependencies are required. The callback arguments will have
+ * references to each module in the same order as the deps array.
+ * @returns {Object|undefined} If the deps parameter is a string, then this
+ * function returns the required module definition, otherwise undefined is
+ * returned.
+ */
+if (!window.require) {
+ window.require = function(deps, callback) {
+ var depsArr = typeof deps === "string" ? [deps] : deps;
+ var depModules = [], depModule, split, i, j;
+ for (j = 0; j < depsArr.length; j++) {
+ depModule = this;
+ split = depsArr[j].split("/");
+ for (i = 0; i < split.length - 1; i++) {
+ depModule = depModule[split[i]] = (depModule[split[i]] || {});
+ }
+ depModules.push(depModule);
+ }
+ if (callback) {
+ callback.apply(this, depModules);
+ }
+ return typeof deps === "string" ? depModules[0] : undefined;
+ };
+}/*******************************************************************************
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+define("orion/textview/eventTarget", [], function() {
+ /**
+ * Constructs a new EventTarget object.
+ *
+ * @class
+ * @name orion.textview.EventTarget
+ */
+ function EventTarget() {
+ }
+ /**
+ * Adds in the event target interface into the specified object.
+ *
+ * @param {Object} object The object to add in the event target interface.
+ */
+ EventTarget.addMixin = function(object) {
+ var proto = EventTarget.prototype;
+ for (var p in proto) {
+ if (proto.hasOwnProperty(p)) {
+ object[p] = proto[p];
+ }
+ }
+ };
+ EventTarget.prototype = /** @lends orion.textview.EventTarget.prototype */ {
+ /**
+ * Adds an event listener to this event target.
+ *
+ * @param {String} type The event type.
+ * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens.
+ * @param {Boolean} [useCapture=false] true if the listener should be trigged in the capture phase.
+ *
+ * @see #removeEventListener
+ */
+ addEventListener: function(type, listener, useCapture) {
+ if (!this._eventTypes) { this._eventTypes = {}; }
+ var state = this._eventTypes[type];
+ if (!state) {
+ state = this._eventTypes[type] = {level: 0, listeners: []};
+ }
+ var listeners = state.listeners;
+ listeners.push({listener: listener, useCapture: useCapture});
+ },
+ /**
+ * Dispatches the given event to the listeners added to this event target.
+ * @param {Event} evt The event to dispatch.
+ */
+ dispatchEvent: function(evt) {
+ if (!this._eventTypes) { return; }
+ var type = evt.type;
+ var state = this._eventTypes[type];
+ if (state) {
+ var listeners = state.listeners;
+ try {
+ state.level++;
+ if (listeners) {
+ for (var i=0, len=listeners.length; i < len; i++) {
+ if (listeners[i]) {
+ var l = listeners[i].listener;
+ if (typeof l === "function") {
+ l.call(this, evt);
+ } else if (l.handleEvent && typeof l.handleEvent === "function") {
+ l.handleEvent(evt);
+ }
+ }
+ }
+ }
+ } finally {
+ state.level--;
+ if (state.compact && state.level === 0) {
+ for (var j=listeners.length - 1; j >= 0; j--) {
+ if (!listeners[j]) {
+ listeners.splice(j, 1);
+ }
+ }
+ if (listeners.length === 0) {
+ delete this._eventTypes[type];
+ }
+ state.compact = false;
+ }
+ }
+ }
+ },
+ /**
+ * Returns whether there is a listener for the specified event type.
+ *
+ * @param {String} type The event type
+ *
+ * @see #addEventListener
+ * @see #removeEventListener
+ */
+ isListening: function(type) {
+ if (!this._eventTypes) { return false; }
+ return this._eventTypes[type] !== undefined;
+ },
+ /**
+ * Removes an event listener from the event target.
+ *
+ * All the parameters must be the same ones used to add the listener.
+ *
+ *
+ * @param {String} type The event type
+ * @param {Function|EventListener} listener The function or the EventListener that will be executed when the event happens.
+ * @param {Boolean} [useCapture=false] true if the listener should be trigged in the capture phase.
+ *
+ * @see #addEventListener
+ */
+ removeEventListener: function(type, listener, useCapture){
+ if (!this._eventTypes) { return; }
+ var state = this._eventTypes[type];
+ if (state) {
+ var listeners = state.listeners;
+ for (var i=0, len=listeners.length; i < len; i++) {
+ var l = listeners[i];
+ if (l && l.listener === listener && l.useCapture === useCapture) {
+ if (state.level !== 0) {
+ listeners[i] = null;
+ state.compact = true;
+ } else {
+ listeners.splice(i, 1);
+ }
+ break;
+ }
+ }
+ if (listeners.length === 0) {
+ delete this._eventTypes[type];
+ }
+ }
+ }
+ };
+ return {EventTarget: EventTarget};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+/*global define */
+/*jslint browser:true regexp:false*/
+/**
+ * @name orion.editor.regex
+ * @class Utilities for dealing with regular expressions.
+ * @description Utilities for dealing with regular expressions.
+ */
+define("orion/editor/regex", [], function() {
+ /**
+ * @methodOf orion.editor.regex
+ * @static
+ * @description Escapes regex special characters in the input string.
+ * @param {String} str The string to escape.
+ * @returns {String} A copy of str with regex special characters escaped.
+ */
+ function escape(str) {
+ return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&");
+ }
+
+ /**
+ * @methodOf orion.editor.regex
+ * @static
+ * @description Parses a pattern and flags out of a regex literal string.
+ * @param {String} str The string to parse. Should look something like "/ab+c/" or "/ab+c/i".
+ * @returns {Object} If str looks like a regex literal, returns an object with properties
+ *
+ * pattern {String}
+ * flags {String}
+ * otherwise returns null.
+ */
+ function parse(str) {
+ var regexp = /^\s*\/(.+)\/([gim]{0,3})\s*$/.exec(str);
+ if (regexp) {
+ return {
+ pattern : regexp[1],
+ flags : regexp[2]
+ };
+ }
+ return null;
+ }
+
+ return {
+ escape: escape,
+ parse: parse
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global window define */
+
+define("orion/textview/keyBinding", [], function() {
+ var isMac = window.navigator.platform.indexOf("Mac") !== -1;
+
+ /**
+ * Constructs a new key binding with the given key code and modifiers.
+ *
+ * @param {String|Number} keyCode the key code.
+ * @param {Boolean} mod1 the primary modifier (usually Command on Mac and Control on other platforms).
+ * @param {Boolean} mod2 the secondary modifier (usually Shift).
+ * @param {Boolean} mod3 the third modifier (usually Alt).
+ * @param {Boolean} mod4 the fourth modifier (usually Control on the Mac).
+ *
+ * @class A KeyBinding represents of a key code and a modifier state that can be triggered by the user using the keyboard.
+ * @name orion.textview.KeyBinding
+ *
+ * @property {String|Number} keyCode The key code.
+ * @property {Boolean} mod1 The primary modifier (usually Command on Mac and Control on other platforms).
+ * @property {Boolean} mod2 The secondary modifier (usually Shift).
+ * @property {Boolean} mod3 The third modifier (usually Alt).
+ * @property {Boolean} mod4 The fourth modifier (usually Control on the Mac).
+ *
+ * @see orion.textview.TextView#setKeyBinding
+ */
+ function KeyBinding (keyCode, mod1, mod2, mod3, mod4) {
+ if (typeof(keyCode) === "string") {
+ this.keyCode = keyCode.toUpperCase().charCodeAt(0);
+ } else {
+ this.keyCode = keyCode;
+ }
+ this.mod1 = mod1 !== undefined && mod1 !== null ? mod1 : false;
+ this.mod2 = mod2 !== undefined && mod2 !== null ? mod2 : false;
+ this.mod3 = mod3 !== undefined && mod3 !== null ? mod3 : false;
+ this.mod4 = mod4 !== undefined && mod4 !== null ? mod4 : false;
+ }
+ KeyBinding.prototype = /** @lends orion.textview.KeyBinding.prototype */ {
+ /**
+ * Returns whether this key binding matches the given key event.
+ *
+ * @param e the key event.
+ * @returns {Boolean} true whether the key binding matches the key event.
+ */
+ match: function (e) {
+ if (this.keyCode === e.keyCode) {
+ var mod1 = isMac ? e.metaKey : e.ctrlKey;
+ if (this.mod1 !== mod1) { return false; }
+ if (this.mod2 !== e.shiftKey) { return false; }
+ if (this.mod3 !== e.altKey) { return false; }
+ if (isMac && this.mod4 !== e.ctrlKey) { return false; }
+ return true;
+ }
+ return false;
+ },
+ /**
+ * Returns whether this key binding is the same as the given parameter.
+ *
+ * @param {orion.textview.KeyBinding} kb the key binding to compare with.
+ * @returns {Boolean} whether or not the parameter and the receiver describe the same key binding.
+ */
+ equals: function(kb) {
+ if (!kb) { return false; }
+ if (this.keyCode !== kb.keyCode) { return false; }
+ if (this.mod1 !== kb.mod1) { return false; }
+ if (this.mod2 !== kb.mod2) { return false; }
+ if (this.mod3 !== kb.mod3) { return false; }
+ if (this.mod4 !== kb.mod4) { return false; }
+ return true;
+ }
+ };
+ return {KeyBinding: KeyBinding};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/annotations", ['orion/textview/eventTarget'], function(mEventTarget) {
+ /**
+ * @class This object represents a decoration attached to a range of text. Annotations are added to a
+ * AnnotationModel which is attached to a TextModel.
+ *
+ * See:
+ * {@link orion.textview.AnnotationModel}
+ * {@link orion.textview.Ruler}
+ *
+ * @name orion.textview.Annotation
+ *
+ * @property {String} type The annotation type (for example, orion.annotation.error).
+ * @property {Number} start The start offset of the annotation in the text model.
+ * @property {Number} end The end offset of the annotation in the text model.
+ * @property {String} html The HTML displayed for the annotation.
+ * @property {String} title The text description for the annotation.
+ * @property {orion.textview.Style} style The style information for the annotation used in the annotations ruler and tooltips.
+ * @property {orion.textview.Style} overviewStyle The style information for the annotation used in the overview ruler.
+ * @property {orion.textview.Style} rangeStyle The style information for the annotation used in the text view to decorate a range of text.
+ * @property {orion.textview.Style} lineStyle The style information for the annotation used in the text view to decorate a line of text.
+ */
+ /**
+ * Constructs a new folding annotation.
+ *
+ * @param {orion.textview.ProjectionTextModel} projectionModel The projection text model.
+ * @param {String} type The annotation type.
+ * @param {Number} start The start offset of the annotation in the text model.
+ * @param {Number} end The end offset of the annotation in the text model.
+ * @param {String} expandedHTML The HTML displayed for this annotation when it is expanded.
+ * @param {orion.textview.Style} expandedStyle The style information for the annotation when it is expanded.
+ * @param {String} collapsedHTML The HTML displayed for this annotation when it is collapsed.
+ * @param {orion.textview.Style} collapsedStyle The style information for the annotation when it is collapsed.
+ *
+ * @class This object represents a folding annotation.
+ * @name orion.textview.FoldingAnnotation
+ */
+ function FoldingAnnotation (projectionModel, type, start, end, expandedHTML, expandedStyle, collapsedHTML, collapsedStyle) {
+ this.type = type;
+ this.start = start;
+ this.end = end;
+ this._projectionModel = projectionModel;
+ this._expandedHTML = this.html = expandedHTML;
+ this._expandedStyle = this.style = expandedStyle;
+ this._collapsedHTML = collapsedHTML;
+ this._collapsedStyle = collapsedStyle;
+ this.expanded = true;
+ }
+
+ FoldingAnnotation.prototype = /** @lends orion.textview.FoldingAnnotation.prototype */ {
+ /**
+ * Collapses the annotation.
+ */
+ collapse: function () {
+ if (!this.expanded) { return; }
+ this.expanded = false;
+ this.html = this._collapsedHTML;
+ this.style = this._collapsedStyle;
+ var projectionModel = this._projectionModel;
+ var baseModel = projectionModel.getBaseModel();
+ this._projection = {
+ start: baseModel.getLineStart(baseModel.getLineAtOffset(this.start) + 1),
+ end: baseModel.getLineEnd(baseModel.getLineAtOffset(this.end), true)
+ };
+ projectionModel.addProjection(this._projection);
+ },
+ /**
+ * Expands the annotation.
+ */
+ expand: function () {
+ if (this.expanded) { return; }
+ this.expanded = true;
+ this.html = this._expandedHTML;
+ this.style = this._expandedStyle;
+ this._projectionModel.removeProjection(this._projection);
+ }
+ };
+
+ /**
+ * Constructs a new AnnotationTypeList object.
+ *
+ * @class
+ * @name orion.textview.AnnotationTypeList
+ */
+ function AnnotationTypeList () {
+ }
+ /**
+ * Adds in the annotation type interface into the specified object.
+ *
+ * @param {Object} object The object to add in the annotation type interface.
+ */
+ AnnotationTypeList.addMixin = function(object) {
+ var proto = AnnotationTypeList.prototype;
+ for (var p in proto) {
+ if (proto.hasOwnProperty(p)) {
+ object[p] = proto[p];
+ }
+ }
+ };
+ AnnotationTypeList.prototype = /** @lends orion.textview.AnnotationTypeList.prototype */ {
+ /**
+ * Adds an annotation type to the receiver.
+ *
+ * Only annotations of the specified types will be shown by
+ * the receiver.
+ *
+ *
+ * @param {Object} type the annotation type to be shown
+ *
+ * @see #removeAnnotationType
+ * @see #isAnnotationTypeVisible
+ */
+ addAnnotationType: function(type) {
+ if (!this._annotationTypes) { this._annotationTypes = []; }
+ this._annotationTypes.push(type);
+ },
+ /**
+ * Gets the annotation type priority. The priority is determined by the
+ * order the annotation type is added to the receiver. Annotation types
+ * added first have higher priority.
+ *
+ * Returns 0 if the annotation type is not added.
+ *
+ *
+ * @param {Object} type the annotation type
+ *
+ * @see #addAnnotationType
+ * @see #removeAnnotationType
+ * @see #isAnnotationTypeVisible
+ */
+ getAnnotationTypePriority: function(type) {
+ if (this._annotationTypes) {
+ for (var i = 0; i < this._annotationTypes.length; i++) {
+ if (this._annotationTypes[i] === type) {
+ return i + 1;
+ }
+ }
+ }
+ return 0;
+ },
+ /**
+ * Returns an array of annotations in the specified annotation model for the given range of text sorted by type.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model.
+ * @param {Number} start the start offset of the range.
+ * @param {Number} end the end offset of the range.
+ * @return {orion.textview.Annotation[]} an annotation array.
+ */
+ getAnnotationsByType: function(annotationModel, start, end) {
+ var iter = annotationModel.getAnnotations(start, end);
+ var annotation, annotations = [];
+ while (iter.hasNext()) {
+ annotation = iter.next();
+ var priority = this.getAnnotationTypePriority(annotation.type);
+ if (priority === 0) { continue; }
+ annotations.push(annotation);
+ }
+ var self = this;
+ annotations.sort(function(a, b) {
+ return self.getAnnotationTypePriority(a.type) - self.getAnnotationTypePriority(b.type);
+ });
+ return annotations;
+ },
+ /**
+ * Returns whether the receiver shows annotations of the specified type.
+ *
+ * @param {Object} type the annotation type
+ * @returns {Boolean} whether the specified annotation type is shown
+ *
+ * @see #addAnnotationType
+ * @see #removeAnnotationType
+ */
+ isAnnotationTypeVisible: function(type) {
+ return this.getAnnotationTypePriority(type) !== 0;
+ },
+ /**
+ * Removes an annotation type from the receiver.
+ *
+ * @param {Object} type the annotation type to be removed
+ *
+ * @see #addAnnotationType
+ * @see #isAnnotationTypeVisible
+ */
+ removeAnnotationType: function(type) {
+ if (!this._annotationTypes) { return; }
+ for (var i = 0; i < this._annotationTypes.length; i++) {
+ if (this._annotationTypes[i] === type) {
+ this._annotationTypes.splice(i, 1);
+ break;
+ }
+ }
+ }
+ };
+
+ /**
+ * Constructs an annotation model.
+ *
+ * @param {textModel} textModel The text model.
+ *
+ * @class This object manages annotations for a TextModel.
+ *
+ * See:
+ * {@link orion.textview.Annotation}
+ * {@link orion.textview.TextModel}
+ *
+ * @name orion.textview.AnnotationModel
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function AnnotationModel(textModel) {
+ this._annotations = [];
+ var self = this;
+ this._listener = {
+ onChanged: function(modelChangedEvent) {
+ self._onChanged(modelChangedEvent);
+ }
+ };
+ this.setTextModel(textModel);
+ }
+
+ AnnotationModel.prototype = /** @lends orion.textview.AnnotationModel.prototype */ {
+ /**
+ * Adds an annotation to the annotation model.
+ * The annotation model listeners are notified of this change.
+ *
+ * @param {orion.textview.Annotation} annotation the annotation to be added.
+ *
+ * @see #removeAnnotation
+ */
+ addAnnotation: function(annotation) {
+ if (!annotation) { return; }
+ var annotations = this._annotations;
+ var index = this._binarySearch(annotations, annotation.start);
+ annotations.splice(index, 0, annotation);
+ var e = {
+ type: "Changed",
+ added: [annotation],
+ removed: [],
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Returns the text model.
+ *
+ * @return {orion.textview.TextModel} The text model.
+ *
+ * @see #setTextModel
+ */
+ getTextModel: function() {
+ return this._model;
+ },
+ /**
+ * @class This object represents an annotation iterator.
+ *
+ * See:
+ * {@link orion.textview.AnnotationModel#getAnnotations}
+ *
+ * @name orion.textview.AnnotationIterator
+ *
+ * @property {Function} hasNext Determines whether there are more annotations in the iterator.
+ * @property {Function} next Returns the next annotation in the iterator.
+ */
+ /**
+ * Returns an iterator of annotations for the given range of text.
+ *
+ * @param {Number} start the start offset of the range.
+ * @param {Number} end the end offset of the range.
+ * @return {orion.textview.AnnotationIterator} an annotation iterartor.
+ */
+ getAnnotations: function(start, end) {
+ var annotations = this._annotations, current;
+ //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this
+ var i = 0;
+ var skip = function() {
+ while (i < annotations.length) {
+ var a = annotations[i++];
+ if ((start === a.start) || (start > a.start ? start < a.end : a.start < end)) {
+ return a;
+ }
+ if (a.start >= end) {
+ break;
+ }
+ }
+ return null;
+ };
+ current = skip();
+ return {
+ next: function() {
+ var result = current;
+ if (result) { current = skip(); }
+ return result;
+ },
+ hasNext: function() {
+ return current !== null;
+ }
+ };
+ },
+ /**
+ * Notifies the annotation model that the given annotation has been modified.
+ * The annotation model listeners are notified of this change.
+ *
+ * @param {orion.textview.Annotation} annotation the modified annotation.
+ *
+ * @see #addAnnotation
+ */
+ modifyAnnotation: function(annotation) {
+ if (!annotation) { return; }
+ var index = this._getAnnotationIndex(annotation);
+ if (index < 0) { return; }
+ var e = {
+ type: "Changed",
+ added: [],
+ removed: [],
+ changed: [annotation]
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Notifies all listeners that the annotation model has changed.
+ *
+ * @param {orion.textview.Annotation[]} added The list of annotation being added to the model.
+ * @param {orion.textview.Annotation[]} changed The list of annotation modified in the model.
+ * @param {orion.textview.Annotation[]} removed The list of annotation being removed from the model.
+ * @param {ModelChangedEvent} textModelChangedEvent the text model changed event that trigger this change, can be null if the change was trigger by a method call (for example, {@link #addAnnotation}).
+ */
+ onChanged: function(e) {
+ return this.dispatchEvent(e);
+ },
+ /**
+ * Removes all annotations of the given type. All annotations
+ * are removed if the type is not specified.
+ * The annotation model listeners are notified of this change. Only one changed event is generated.
+ *
+ * @param {Object} type the type of annotations to be removed.
+ *
+ * @see #removeAnnotation
+ */
+ removeAnnotations: function(type) {
+ var annotations = this._annotations;
+ var removed, i;
+ if (type) {
+ removed = [];
+ for (i = annotations.length - 1; i >= 0; i--) {
+ var annotation = annotations[i];
+ if (annotation.type === type) {
+ annotations.splice(i, 1);
+ }
+ removed.splice(0, 0, annotation);
+ }
+ } else {
+ removed = annotations;
+ annotations = [];
+ }
+ var e = {
+ type: "Changed",
+ removed: removed,
+ added: [],
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Removes an annotation from the annotation model.
+ * The annotation model listeners are notified of this change.
+ *
+ * @param {orion.textview.Annotation} annotation the annotation to be removed.
+ *
+ * @see #addAnnotation
+ */
+ removeAnnotation: function(annotation) {
+ if (!annotation) { return; }
+ var index = this._getAnnotationIndex(annotation);
+ if (index < 0) { return; }
+ var e = {
+ type: "Changed",
+ removed: this._annotations.splice(index, 1),
+ added: [],
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Removes and adds the specifed annotations to the annotation model.
+ * The annotation model listeners are notified of this change. Only one changed event is generated.
+ *
+ * @param {orion.textview.Annotation} remove the annotations to be removed.
+ * @param {orion.textview.Annotation} add the annotations to be added.
+ *
+ * @see #addAnnotation
+ * @see #removeAnnotation
+ */
+ replaceAnnotations: function(remove, add) {
+ var annotations = this._annotations, i, index, annotation, removed = [];
+ if (remove) {
+ for (i = remove.length - 1; i >= 0; i--) {
+ annotation = remove[i];
+ index = this._getAnnotationIndex(annotation);
+ if (index < 0) { continue; }
+ annotations.splice(index, 1);
+ removed.splice(0, 0, annotation);
+ }
+ }
+ if (!add) { add = []; }
+ for (i = 0; i < add.length; i++) {
+ annotation = add[i];
+ index = this._binarySearch(annotations, annotation.start);
+ annotations.splice(index, 0, annotation);
+ }
+ var e = {
+ type: "Changed",
+ removed: removed,
+ added: add,
+ changed: []
+ };
+ this.onChanged(e);
+ },
+ /**
+ * Sets the text model of the annotation model. The annotation
+ * model listens for changes in the text model to update and remove
+ * annotations that are affected by the change.
+ *
+ * @param {orion.textview.TextModel} textModel the text model.
+ *
+ * @see #getTextModel
+ */
+ setTextModel: function(textModel) {
+ if (this._model) {
+ this._model.removeEventListener("Changed", this._listener.onChanged);
+ }
+ this._model = textModel;
+ if (this._model) {
+ this._model.addEventListener("Changed", this._listener.onChanged);
+ }
+ },
+ /** @ignore */
+ _binarySearch: function (array, offset) {
+ var high = array.length, low = -1, index;
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ if (offset <= array[index].start) {
+ high = index;
+ } else {
+ low = index;
+ }
+ }
+ return high;
+ },
+ /** @ignore */
+ _getAnnotationIndex: function(annotation) {
+ var annotations = this._annotations;
+ var index = this._binarySearch(annotations, annotation.start);
+ while (index < annotations.length && annotations[index].start === annotation.start) {
+ if (annotations[index] === annotation) {
+ return index;
+ }
+ index++;
+ }
+ return -1;
+ },
+ /** @ignore */
+ _onChanged: function(modelChangedEvent) {
+ var start = modelChangedEvent.start;
+ var addedCharCount = modelChangedEvent.addedCharCount;
+ var removedCharCount = modelChangedEvent.removedCharCount;
+ var annotations = this._annotations, end = start + removedCharCount;
+ //TODO binary search does not work for range intersection when there are overlaping ranges, need interval search tree for this
+ var startIndex = 0;
+ if (!(0 <= startIndex && startIndex < annotations.length)) { return; }
+ var e = {
+ type: "Changed",
+ added: [],
+ removed: [],
+ changed: [],
+ textModelChangedEvent: modelChangedEvent
+ };
+ var changeCount = addedCharCount - removedCharCount, i;
+ for (i = startIndex; i < annotations.length; i++) {
+ var annotation = annotations[i];
+ if (annotation.start >= end) {
+ annotation.start += changeCount;
+ annotation.end += changeCount;
+ e.changed.push(annotation);
+ } else if (annotation.end <= start) {
+ //nothing
+ } else if (annotation.start < start && end < annotation.end) {
+ annotation.end += changeCount;
+ e.changed.push(annotation);
+ } else {
+ annotations.splice(i, 1);
+ e.removed.push(annotation);
+ i--;
+ }
+ }
+ if (e.added.length > 0 || e.removed.length > 0 || e.changed.length > 0) {
+ this.onChanged(e);
+ }
+ }
+ };
+ mEventTarget.EventTarget.addMixin(AnnotationModel.prototype);
+
+ /**
+ * Constructs a new styler for annotations.
+ *
+ * @param {orion.textview.TextView} view The styler view.
+ * @param {orion.textview.AnnotationModel} view The styler annotation model.
+ *
+ * @class This object represents a styler for annotation attached to a text view.
+ * @name orion.textview.AnnotationStyler
+ * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType
+ * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible
+ * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType
+ */
+ function AnnotationStyler (view, annotationModel) {
+ this._view = view;
+ this._annotationModel = annotationModel;
+ var self = this;
+ this._listener = {
+ onDestroy: function(e) {
+ self._onDestroy(e);
+ },
+ onLineStyle: function(e) {
+ self._onLineStyle(e);
+ },
+ onChanged: function(e) {
+ self._onAnnotationModelChanged(e);
+ }
+ };
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ view.addEventListener("LineStyle", this._listener.onLineStyle);
+ annotationModel.addEventListener("Changed", this._listener.onChanged);
+ }
+ AnnotationStyler.prototype = /** @lends orion.textview.AnnotationStyler.prototype */ {
+ /**
+ * Destroys the styler.
+ *
+ * Removes all listeners added by this styler.
+ *
+ */
+ destroy: function() {
+ var view = this._view;
+ if (view) {
+ view.removeEventListener("Destroy", this._listener.onDestroy);
+ view.removeEventListener("LineStyle", this._listener.onLineStyle);
+ this.view = null;
+ }
+ var annotationModel = this._annotationModel;
+ if (annotationModel) {
+ annotationModel.removeEventListener("Changed", this._listener.onChanged);
+ annotationModel = null;
+ }
+ },
+ _mergeStyle: function(result, style) {
+ if (style) {
+ if (!result) { result = {}; }
+ if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) {
+ result.styleClass += " " + style.styleClass;
+ } else {
+ result.styleClass = style.styleClass;
+ }
+ var prop;
+ if (style.style) {
+ if (!result.style) { result.style = {}; }
+ for (prop in style.style) {
+ if (!result.style[prop]) {
+ result.style[prop] = style.style[prop];
+ }
+ }
+ }
+ if (style.attributes) {
+ if (!result.attributes) { result.attributes = {}; }
+ for (prop in style.attributes) {
+ if (!result.attributes[prop]) {
+ result.attributes[prop] = style.attributes[prop];
+ }
+ }
+ }
+ }
+ return result;
+ },
+ _mergeStyleRanges: function(ranges, styleRange) {
+ if (!ranges) { return; }
+ for (var i=0; i= range.end) { continue; }
+ var mergedStyle = this._mergeStyle({}, range.style);
+ mergedStyle = this._mergeStyle(mergedStyle, styleRange.style);
+ if (styleRange.start <= range.start && styleRange.end >= range.end) {
+ ranges[i] = {start: range.start, end: range.end, style: mergedStyle};
+ } else if (styleRange.start > range.start && styleRange.end < range.end) {
+ ranges.splice(i, 1,
+ {start: range.start, end: styleRange.start, style: range.style},
+ {start: styleRange.start, end: styleRange.end, style: mergedStyle},
+ {start: styleRange.end, end: range.end, style: range.style});
+ i += 2;
+ } else if (styleRange.start > range.start) {
+ ranges.splice(i, 1,
+ {start: range.start, end: styleRange.start, style: range.style},
+ {start: styleRange.start, end: range.end, style: mergedStyle});
+ i += 1;
+ } else if (styleRange.end < range.end) {
+ ranges.splice(i, 1,
+ {start: range.start, end: styleRange.end, style: mergedStyle},
+ {start: styleRange.end, end: range.end, style: range.style});
+ i += 1;
+ }
+ }
+ },
+ _onAnnotationModelChanged: function(e) {
+ if (e.textModelChangedEvent) {
+ return;
+ }
+ var view = this._view;
+ if (!view) { return; }
+ var self = this;
+ var model = view.getModel();
+ function redraw(changes) {
+ for (var i = 0; i < changes.length; i++) {
+ if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
+ var start = changes[i].start;
+ var end = changes[i].end;
+ if (model.getBaseModel) {
+ start = model.mapOffset(start, true);
+ end = model.mapOffset(end, true);
+ }
+ if (start !== -1 && end !== -1) {
+ view.redrawRange(start, end);
+ }
+ }
+ }
+ redraw(e.added);
+ redraw(e.removed);
+ redraw(e.changed);
+ },
+ _onDestroy: function(e) {
+ this.destroy();
+ },
+ _onLineStyle: function (e) {
+ var annotationModel = this._annotationModel;
+ var viewModel = this._view.getModel();
+ var baseModel = annotationModel.getTextModel();
+ var start = e.lineStart;
+ var end = e.lineStart + e.lineText.length;
+ if (baseModel !== viewModel) {
+ start = viewModel.mapOffset(start);
+ end = viewModel.mapOffset(end);
+ }
+ var annotations = annotationModel.getAnnotations(start, end);
+ while (annotations.hasNext()) {
+ var annotation = annotations.next();
+ if (!this.isAnnotationTypeVisible(annotation.type)) { continue; }
+ if (annotation.rangeStyle) {
+ var annotationStart = annotation.start;
+ var annotationEnd = annotation.end;
+ if (baseModel !== viewModel) {
+ annotationStart = viewModel.mapOffset(annotationStart, true);
+ annotationEnd = viewModel.mapOffset(annotationEnd, true);
+ }
+ this._mergeStyleRanges(e.ranges, {start: annotationStart, end: annotationEnd, style: annotation.rangeStyle});
+ }
+ if (annotation.lineStyle) {
+ e.style = this._mergeStyle({}, e.style);
+ e.style = this._mergeStyle(e.style, annotation.lineStyle);
+ }
+ }
+ }
+ };
+ AnnotationTypeList.addMixin(AnnotationStyler.prototype);
+
+ return {
+ FoldingAnnotation: FoldingAnnotation,
+ AnnotationTypeList: AnnotationTypeList,
+ AnnotationModel: AnnotationModel,
+ AnnotationStyler: AnnotationStyler
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define setTimeout clearTimeout setInterval clearInterval Node */
+
+define("orion/textview/rulers", ['orion/textview/annotations', 'orion/textview/tooltip'], function(mAnnotations, mTooltip) {
+
+ /**
+ * Constructs a new ruler.
+ *
+ * The default implementation does not implement all the methods in the interface
+ * and is useful only for objects implementing rulers.
+ *
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {String} [rulerOverview="page"] the overview for the ruler.
+ * @param {orion.textview.Style} [rulerStyle] the style for the ruler.
+ *
+ * @class This interface represents a ruler for the text view.
+ *
+ * A Ruler is a graphical element that is placed either on the left or on the right side of
+ * the view. It can be used to provide the view with per line decoration such as line numbering,
+ * bookmarks, breakpoints, folding disclosures, etc.
+ *
+ * There are two types of rulers: page and document. A page ruler only shows the content for the lines that are
+ * visible, while a document ruler always shows the whole content.
+ *
+ * See:
+ * {@link orion.textview.LineNumberRuler}
+ * {@link orion.textview.AnnotationRuler}
+ * {@link orion.textview.OverviewRuler}
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#addRuler}
+ *
+ * @name orion.textview.Ruler
+ * @borrows orion.textview.AnnotationTypeList#addAnnotationType as #addAnnotationType
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationTypePriority as #getAnnotationTypePriority
+ * @borrows orion.textview.AnnotationTypeList#getAnnotationsByType as #getAnnotationsByType
+ * @borrows orion.textview.AnnotationTypeList#isAnnotationTypeVisible as #isAnnotationTypeVisible
+ * @borrows orion.textview.AnnotationTypeList#removeAnnotationType as #removeAnnotationType
+ */
+ function Ruler (annotationModel, rulerLocation, rulerOverview, rulerStyle) {
+ this._location = rulerLocation || "left";
+ this._overview = rulerOverview || "page";
+ this._rulerStyle = rulerStyle;
+ this._view = null;
+ var self = this;
+ this._listener = {
+ onTextModelChanged: function(e) {
+ self._onTextModelChanged(e);
+ },
+ onAnnotationModelChanged: function(e) {
+ self._onAnnotationModelChanged(e);
+ }
+ };
+ this.setAnnotationModel(annotationModel);
+ }
+ Ruler.prototype = /** @lends orion.textview.Ruler.prototype */ {
+ /**
+ * Returns the annotations for a given line range merging multiple
+ * annotations when necessary.
+ *
+ * This method is called by the text view when the ruler is redrawn.
+ *
+ *
+ * @param {Number} startLine the start line index
+ * @param {Number} endLine the end line index
+ * @return {orion.textview.Annotation[]} the annotations for the line range. The array might be sparse.
+ */
+ getAnnotations: function(startLine, endLine) {
+ var annotationModel = this._annotationModel;
+ if (!annotationModel) { return []; }
+ var model = this._view.getModel();
+ var start = model.getLineStart(startLine);
+ var end = model.getLineEnd(endLine - 1);
+ var baseModel = model;
+ if (model.getBaseModel) {
+ baseModel = model.getBaseModel();
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ }
+ var result = [];
+ var annotations = this.getAnnotationsByType(annotationModel, start, end);
+ for (var i = 0; i < annotations.length; i++) {
+ var annotation = annotations[i];
+ var annotationLineStart = baseModel.getLineAtOffset(annotation.start);
+ var annotationLineEnd = baseModel.getLineAtOffset(Math.max(annotation.start, annotation.end - 1));
+ for (var lineIndex = annotationLineStart; lineIndex<=annotationLineEnd; lineIndex++) {
+ var visualLineIndex = lineIndex;
+ if (model !== baseModel) {
+ var ls = baseModel.getLineStart(lineIndex);
+ ls = model.mapOffset(ls, true);
+ if (ls === -1) { continue; }
+ visualLineIndex = model.getLineAtOffset(ls);
+ }
+ if (!(startLine <= visualLineIndex && visualLineIndex < endLine)) { continue; }
+ var rulerAnnotation = this._mergeAnnotation(result[visualLineIndex], annotation, lineIndex - annotationLineStart, annotationLineEnd - annotationLineStart + 1);
+ if (rulerAnnotation) {
+ result[visualLineIndex] = rulerAnnotation;
+ }
+ }
+ }
+ if (!this._multiAnnotation && this._multiAnnotationOverlay) {
+ for (var k in result) {
+ if (result[k]._multiple) {
+ result[k].html = result[k].html + this._multiAnnotationOverlay.html;
+ }
+ }
+ }
+ return result;
+ },
+ /**
+ * Returns the annotation model.
+ *
+ * @returns {orion.textview.AnnotationModel} the ruler annotation model.
+ *
+ * @see #setAnnotationModel
+ */
+ getAnnotationModel: function() {
+ return this._annotationModel;
+ },
+ /**
+ * Returns the ruler location.
+ *
+ * @returns {String} the ruler location, which is either "left" or "right".
+ *
+ * @see #getOverview
+ */
+ getLocation: function() {
+ return this._location;
+ },
+ /**
+ * Returns the ruler overview type.
+ *
+ * @returns {String} the overview type, which is either "page" or "document".
+ *
+ * @see #getLocation
+ */
+ getOverview: function() {
+ return this._overview;
+ },
+ /**
+ * Returns the style information for the ruler.
+ *
+ * @returns {orion.textview.Style} the style information.
+ */
+ getRulerStyle: function() {
+ return this._rulerStyle;
+ },
+ /**
+ * Returns the widest annotation which determines the width of the ruler.
+ *
+ * If the ruler does not have a fixed width it should provide the widest
+ * annotation to avoid the ruler from changing size as the view scrolls.
+ *
+ *
+ * This method is called by the text view when the ruler is redrawn.
+ *
+ *
+ * @returns {orion.textview.Annotation} the widest annotation.
+ *
+ * @see #getAnnotations
+ */
+ getWidestAnnotation: function() {
+ return null;
+ },
+ /**
+ * Sets the annotation model for the ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model.
+ *
+ * @see #getAnnotationModel
+ */
+ setAnnotationModel: function (annotationModel) {
+ if (this._annotationModel) {
+ this._annotationModel.removEventListener("Changed", this._listener.onAnnotationModelChanged);
+ }
+ this._annotationModel = annotationModel;
+ if (this._annotationModel) {
+ this._annotationModel.addEventListener("Changed", this._listener.onAnnotationModelChanged);
+ }
+ },
+ /**
+ * Sets the annotation that is displayed when a given line contains multiple
+ * annotations. This annotation is used when there are different types of
+ * annotations in a given line.
+ *
+ * @param {orion.textview.Annotation} annotation the annotation for lines with multiple annotations.
+ *
+ * @see #setMultiAnnotationOverlay
+ */
+ setMultiAnnotation: function(annotation) {
+ this._multiAnnotation = annotation;
+ },
+ /**
+ * Sets the annotation that overlays a line with multiple annotations. This annotation is displayed on
+ * top of the computed annotation for a given line when there are multiple annotations of the same type
+ * in the line. It is also used when the multiple annotation is not set.
+ *
+ * @param {orion.textview.Annotation} annotation the annotation overlay for lines with multiple annotations.
+ *
+ * @see #setMultiAnnotation
+ */
+ setMultiAnnotationOverlay: function(annotation) {
+ this._multiAnnotationOverlay = annotation;
+ },
+ /**
+ * Sets the view for the ruler.
+ *
+ * This method is called by the text view when the ruler
+ * is added to the view.
+ *
+ *
+ * @param {orion.textview.TextView} view the text view.
+ */
+ setView: function (view) {
+ if (this._onTextModelChanged && this._view) {
+ this._view.removeEventListener("ModelChanged", this._listener.onTextModelChanged);
+ }
+ this._view = view;
+ if (this._onTextModelChanged && this._view) {
+ this._view.addEventListener("ModelChanged", this._listener.onTextModelChanged);
+ }
+ },
+ /**
+ * This event is sent when the user clicks a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the click event.
+ */
+ onClick: function(lineIndex, e) {
+ },
+ /**
+ * This event is sent when the user double clicks a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the double click event.
+ */
+ onDblClick: function(lineIndex, e) {
+ },
+ /**
+ * This event is sent when the user moves the mouse over a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the mouse move event.
+ */
+ onMouseMove: function(lineIndex, e) {
+ var tooltip = mTooltip.Tooltip.getTooltip(this._view);
+ if (!tooltip) { return; }
+ if (tooltip.isVisible() && this._tooltipLineIndex === lineIndex) { return; }
+ this._tooltipLineIndex = lineIndex;
+ var self = this;
+ tooltip.setTarget({
+ y: e.clientY,
+ getTooltipInfo: function() {
+ return self._getTooltipInfo(self._tooltipLineIndex, this.y);
+ }
+ });
+ },
+ /**
+ * This event is sent when the mouse pointer enters a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the mouse over event.
+ */
+ onMouseOver: function(lineIndex, e) {
+ this.onMouseMove(lineIndex, e);
+ },
+ /**
+ * This event is sent when the mouse pointer exits a line annotation.
+ *
+ * @event
+ * @param {Number} lineIndex the line index of the annotation under the pointer.
+ * @param {DOMEvent} e the mouse out event.
+ */
+ onMouseOut: function(lineIndex, e) {
+ var tooltip = mTooltip.Tooltip.getTooltip(this._view);
+ if (!tooltip) { return; }
+ tooltip.setTarget(null);
+ },
+ /** @ignore */
+ _getTooltipInfo: function(lineIndex, y) {
+ if (lineIndex === undefined) { return; }
+ var view = this._view;
+ var model = view.getModel();
+ var annotationModel = this._annotationModel;
+ var annotations = [];
+ if (annotationModel) {
+ var start = model.getLineStart(lineIndex);
+ var end = model.getLineEnd(lineIndex);
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ }
+ annotations = this.getAnnotationsByType(annotationModel, start, end);
+ }
+ var contents = this._getTooltipContents(lineIndex, annotations);
+ if (!contents) { return null; }
+ var info = {
+ contents: contents,
+ anchor: this.getLocation()
+ };
+ var rect = view.getClientArea();
+ if (this.getOverview() === "document") {
+ rect.y = view.convert({y: y}, "view", "document").y;
+ } else {
+ rect.y = view.getLocationAtOffset(model.getLineStart(lineIndex)).y;
+ }
+ view.convert(rect, "document", "page");
+ info.x = rect.x;
+ info.y = rect.y;
+ if (info.anchor === "right") {
+ info.x += rect.width;
+ }
+ info.maxWidth = rect.width;
+ info.maxHeight = rect.height - (rect.y - view._parent.getBoundingClientRect().top);
+ return info;
+ },
+ /** @ignore */
+ _getTooltipContents: function(lineIndex, annotations) {
+ return annotations;
+ },
+ /** @ignore */
+ _onAnnotationModelChanged: function(e) {
+ var view = this._view;
+ if (!view) { return; }
+ var model = view.getModel(), self = this;
+ var lineCount = model.getLineCount();
+ if (e.textModelChangedEvent) {
+ var start = e.textModelChangedEvent.start;
+ if (model.getBaseModel) { start = model.mapOffset(start, true); }
+ var startLine = model.getLineAtOffset(start);
+ view.redrawLines(startLine, lineCount, self);
+ return;
+ }
+ function redraw(changes) {
+ for (var i = 0; i < changes.length; i++) {
+ if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
+ var start = changes[i].start;
+ var end = changes[i].end;
+ if (model.getBaseModel) {
+ start = model.mapOffset(start, true);
+ end = model.mapOffset(end, true);
+ }
+ if (start !== -1 && end !== -1) {
+ view.redrawLines(model.getLineAtOffset(start), model.getLineAtOffset(Math.max(start, end - 1)) + 1, self);
+ }
+ }
+ }
+ redraw(e.added);
+ redraw(e.removed);
+ redraw(e.changed);
+ },
+ /** @ignore */
+ _mergeAnnotation: function(result, annotation, annotationLineIndex, annotationLineCount) {
+ if (!result) { result = {}; }
+ if (annotationLineIndex === 0) {
+ if (result.html && annotation.html) {
+ if (annotation.html !== result.html) {
+ if (!result._multiple && this._multiAnnotation) {
+ result.html = this._multiAnnotation.html;
+ }
+ }
+ result._multiple = true;
+ } else {
+ result.html = annotation.html;
+ }
+ }
+ result.style = this._mergeStyle(result.style, annotation.style);
+ return result;
+ },
+ /** @ignore */
+ _mergeStyle: function(result, style) {
+ if (style) {
+ if (!result) { result = {}; }
+ if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) {
+ result.styleClass += " " + style.styleClass;
+ } else {
+ result.styleClass = style.styleClass;
+ }
+ var prop;
+ if (style.style) {
+ if (!result.style) { result.style = {}; }
+ for (prop in style.style) {
+ if (!result.style[prop]) {
+ result.style[prop] = style.style[prop];
+ }
+ }
+ }
+ if (style.attributes) {
+ if (!result.attributes) { result.attributes = {}; }
+ for (prop in style.attributes) {
+ if (!result.attributes[prop]) {
+ result.attributes[prop] = style.attributes[prop];
+ }
+ }
+ }
+ }
+ return result;
+ }
+ };
+ mAnnotations.AnnotationTypeList.addMixin(Ruler.prototype);
+
+ /**
+ * Constructs a new line numbering ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ * @param {orion.textview.Style} [oddStyle={style: {backgroundColor: "white"}] the style for lines with odd line index.
+ * @param {orion.textview.Style} [evenStyle={backgroundColor: "white"}] the style for lines with even line index.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements a line numbering ruler.
+ *
+ * See:
+ * {@link orion.textview.Ruler}
+ *
+ * @name orion.textview.LineNumberRuler
+ */
+ function LineNumberRuler (annotationModel, rulerLocation, rulerStyle, oddStyle, evenStyle) {
+ Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle);
+ this._oddStyle = oddStyle || {style: {backgroundColor: "white"}};
+ this._evenStyle = evenStyle || {style: {backgroundColor: "white"}};
+ this._numOfDigits = 0;
+ }
+ LineNumberRuler.prototype = new Ruler();
+ /** @ignore */
+ LineNumberRuler.prototype.getAnnotations = function(startLine, endLine) {
+ var result = Ruler.prototype.getAnnotations.call(this, startLine, endLine);
+ var model = this._view.getModel();
+ for (var lineIndex = startLine; lineIndex < endLine; lineIndex++) {
+ var style = lineIndex & 1 ? this._oddStyle : this._evenStyle;
+ var mapLine = lineIndex;
+ if (model.getBaseModel) {
+ var lineStart = model.getLineStart(mapLine);
+ mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart));
+ }
+ if (!result[lineIndex]) { result[lineIndex] = {}; }
+ result[lineIndex].html = (mapLine + 1) + "";
+ if (!result[lineIndex].style) { result[lineIndex].style = style; }
+ }
+ return result;
+ };
+ /** @ignore */
+ LineNumberRuler.prototype.getWidestAnnotation = function() {
+ var lineCount = this._view.getModel().getLineCount();
+ return this.getAnnotations(lineCount - 1, lineCount)[lineCount - 1];
+ };
+ /** @ignore */
+ LineNumberRuler.prototype._onTextModelChanged = function(e) {
+ var start = e.start;
+ var model = this._view.getModel();
+ var lineCount = model.getBaseModel ? model.getBaseModel().getLineCount() : model.getLineCount();
+ var numOfDigits = (lineCount+"").length;
+ if (this._numOfDigits !== numOfDigits) {
+ this._numOfDigits = numOfDigits;
+ var startLine = model.getLineAtOffset(start);
+ this._view.redrawLines(startLine, model.getLineCount(), this);
+ }
+ };
+
+ /**
+ * @class This is class represents an annotation for the AnnotationRuler.
+ *
+ * See:
+ * {@link orion.textview.AnnotationRuler}
+ *
+ *
+ * @name orion.textview.Annotation
+ *
+ * @property {String} [html=""] The html content for the annotation, typically contains an image.
+ * @property {orion.textview.Style} [style] the style for the annotation.
+ * @property {orion.textview.Style} [overviewStyle] the style for the annotation in the overview ruler.
+ */
+ /**
+ * Constructs a new annotation ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ * @param {orion.textview.Annotation} [defaultAnnotation] the default annotation.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements an annotation ruler.
+ *
+ * See:
+ * {@link orion.textview.Ruler}
+ * {@link orion.textview.Annotation}
+ *
+ * @name orion.textview.AnnotationRuler
+ */
+ function AnnotationRuler (annotationModel, rulerLocation, rulerStyle) {
+ Ruler.call(this, annotationModel, rulerLocation, "page", rulerStyle);
+ }
+ AnnotationRuler.prototype = new Ruler();
+
+ /**
+ * Constructs a new overview ruler.
+ *
+ * The overview ruler is used in conjunction with a AnnotationRuler, for each annotation in the
+ * AnnotationRuler this ruler displays a mark in the overview. Clicking on the mark causes the
+ * view to scroll to the annotated line.
+ *
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements an overview ruler.
+ *
+ * See:
+ * {@link orion.textview.AnnotationRuler}
+ * {@link orion.textview.Ruler}
+ *
+ * @name orion.textview.OverviewRuler
+ */
+ function OverviewRuler (annotationModel, rulerLocation, rulerStyle) {
+ Ruler.call(this, annotationModel, rulerLocation, "document", rulerStyle);
+ }
+ OverviewRuler.prototype = new Ruler();
+
+ /** @ignore */
+ OverviewRuler.prototype.getRulerStyle = function() {
+ var result = {style: {lineHeight: "1px", fontSize: "1px"}};
+ result = this._mergeStyle(result, this._rulerStyle);
+ return result;
+ };
+ /** @ignore */
+ OverviewRuler.prototype.onClick = function(lineIndex, e) {
+ if (lineIndex === undefined) { return; }
+ this._view.setTopIndex(lineIndex);
+ };
+ /** @ignore */
+ OverviewRuler.prototype._getTooltipContents = function(lineIndex, annotations) {
+ if (annotations.length === 0) {
+ var model = this._view.getModel();
+ var mapLine = lineIndex;
+ if (model.getBaseModel) {
+ var lineStart = model.getLineStart(mapLine);
+ mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart));
+ }
+ return "Line: " + (mapLine + 1);
+ }
+ return Ruler.prototype._getTooltipContents.call(this, lineIndex, annotations);
+ };
+ /** @ignore */
+ OverviewRuler.prototype._mergeAnnotation = function(previousAnnotation, annotation, annotationLineIndex, annotationLineCount) {
+ if (annotationLineIndex !== 0) { return undefined; }
+ var result = previousAnnotation;
+ if (!result) {
+ //TODO annotationLineCount does not work when there are folded lines
+ var height = 3 * annotationLineCount;
+ result = {html: " ", style: { style: {height: height + "px"}}};
+ result.style = this._mergeStyle(result.style, annotation.overviewStyle);
+ }
+ return result;
+ };
+
+ /**
+ * Constructs a new folding ruler.
+ *
+ * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler.
+ * @param {String} [rulerLocation="left"] the location for the ruler.
+ * @param {orion.textview.Style} [rulerStyle=undefined] the style for the ruler.
+ *
+ * @augments orion.textview.Ruler
+ * @class This objects implements an overview ruler.
+ *
+ * See:
+ * {@link orion.textview.AnnotationRuler}
+ * {@link orion.textview.Ruler}
+ *
+ * @name orion.textview.OverviewRuler
+ */
+ function FoldingRuler (annotationModel, rulerLocation, rulerStyle) {
+ AnnotationRuler.call(this, annotationModel, rulerLocation, rulerStyle);
+ }
+ FoldingRuler.prototype = new AnnotationRuler();
+
+ /** @ignore */
+ FoldingRuler.prototype.onClick = function(lineIndex, e) {
+ if (lineIndex === undefined) { return; }
+ var annotationModel = this._annotationModel;
+ if (!annotationModel) { return; }
+ var view = this._view;
+ var model = view.getModel();
+ var start = model.getLineStart(lineIndex);
+ var end = model.getLineEnd(lineIndex, true);
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ }
+ var annotation, iter = annotationModel.getAnnotations(start, end);
+ while (!annotation && iter.hasNext()) {
+ var a = iter.next();
+ if (!this.isAnnotationTypeVisible(a.type)) { continue; }
+ annotation = a;
+ }
+ if (annotation) {
+ var tooltip = mTooltip.Tooltip.getTooltip(this._view);
+ if (tooltip) {
+ tooltip.setTarget(null);
+ }
+ if (annotation.expanded) {
+ annotation.collapse();
+ } else {
+ annotation.expand();
+ }
+ this._annotationModel.modifyAnnotation(annotation);
+ }
+ };
+ /** @ignore */
+ FoldingRuler.prototype._getTooltipContents = function(lineIndex, annotations) {
+ if (annotations.length === 1) {
+ if (annotations[0].expanded) {
+ return null;
+ }
+ }
+ return AnnotationRuler.prototype._getTooltipContents.call(this, lineIndex, annotations);
+ };
+ /** @ignore */
+ FoldingRuler.prototype._onAnnotationModelChanged = function(e) {
+ if (e.textModelChangedEvent) {
+ AnnotationRuler.prototype._onAnnotationModelChanged.call(this, e);
+ return;
+ }
+ var view = this._view;
+ if (!view) { return; }
+ var model = view.getModel(), self = this, i;
+ var lineCount = model.getLineCount(), lineIndex = lineCount;
+ function redraw(changes) {
+ for (i = 0; i < changes.length; i++) {
+ if (!self.isAnnotationTypeVisible(changes[i].type)) { continue; }
+ var start = changes[i].start;
+ if (model.getBaseModel) {
+ start = model.mapOffset(start, true);
+ }
+ if (start !== -1) {
+ lineIndex = Math.min(lineIndex, model.getLineAtOffset(start));
+ }
+ }
+ }
+ redraw(e.added);
+ redraw(e.removed);
+ redraw(e.changed);
+ var rulers = view.getRulers();
+ for (i = 0; i < rulers.length; i++) {
+ view.redrawLines(lineIndex, lineCount, rulers[i]);
+ }
+ };
+
+ return {
+ Ruler: Ruler,
+ AnnotationRuler: AnnotationRuler,
+ LineNumberRuler: LineNumberRuler,
+ OverviewRuler: OverviewRuler,
+ FoldingRuler: FoldingRuler
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/undoStack", [], function() {
+
+ /**
+ * Constructs a new Change object.
+ *
+ * @class
+ * @name orion.textview.Change
+ * @private
+ */
+ function Change(offset, text, previousText) {
+ this.offset = offset;
+ this.text = text;
+ this.previousText = previousText;
+ }
+ Change.prototype = {
+ /** @ignore */
+ undo: function (view, select) {
+ this._doUndoRedo(this.offset, this.previousText, this.text, view, select);
+ },
+ /** @ignore */
+ redo: function (view, select) {
+ this._doUndoRedo(this.offset, this.text, this.previousText, view, select);
+ },
+ _doUndoRedo: function(offset, text, previousText, view, select) {
+ var model = view.getModel();
+ /*
+ * TODO UndoStack should be changing the text in the base model.
+ * This is code needs to change when modifications in the base
+ * model are supported properly by the projection model.
+ */
+ if (model.mapOffset && view.annotationModel) {
+ var mapOffset = model.mapOffset(offset, true);
+ if (mapOffset < 0) {
+ var annotationModel = view.annotationModel;
+ var iter = annotationModel.getAnnotations(offset, offset + 1);
+ while (iter.hasNext()) {
+ var annotation = iter.next();
+ if (annotation.type === "orion.annotation.folding") {
+ annotation.expand();
+ mapOffset = model.mapOffset(offset, true);
+ break;
+ }
+ }
+ }
+ if (mapOffset < 0) { return; }
+ offset = mapOffset;
+ }
+ view.setText(text, offset, offset + previousText.length);
+ if (select) {
+ view.setSelection(offset, offset + text.length);
+ }
+ }
+ };
+
+ /**
+ * Constructs a new CompoundChange object.
+ *
+ * @class
+ * @name orion.textview.CompoundChange
+ * @private
+ */
+ function CompoundChange () {
+ this.changes = [];
+ }
+ CompoundChange.prototype = {
+ /** @ignore */
+ add: function (change) {
+ this.changes.push(change);
+ },
+ /** @ignore */
+ end: function (view) {
+ this.endSelection = view.getSelection();
+ this.endCaret = view.getCaretOffset();
+ },
+ /** @ignore */
+ undo: function (view, select) {
+ for (var i=this.changes.length - 1; i >= 0; i--) {
+ this.changes[i].undo(view, false);
+ }
+ if (select) {
+ var start = this.startSelection.start;
+ var end = this.startSelection.end;
+ view.setSelection(this.startCaret ? start : end, this.startCaret ? end : start);
+ }
+ },
+ /** @ignore */
+ redo: function (view, select) {
+ for (var i = 0; i < this.changes.length; i++) {
+ this.changes[i].redo(view, false);
+ }
+ if (select) {
+ var start = this.endSelection.start;
+ var end = this.endSelection.end;
+ view.setSelection(this.endCaret ? start : end, this.endCaret ? end : start);
+ }
+ },
+ /** @ignore */
+ start: function (view) {
+ this.startSelection = view.getSelection();
+ this.startCaret = view.getCaretOffset();
+ }
+ };
+
+ /**
+ * Constructs a new UndoStack on a text view.
+ *
+ * @param {orion.textview.TextView} view the text view for the undo stack.
+ * @param {Number} [size=100] the size for the undo stack.
+ *
+ * @name orion.textview.UndoStack
+ * @class The UndoStack is used to record the history of a text model associated to an view. Every
+ * change to the model is added to stack, allowing the application to undo and redo these changes.
+ *
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ *
+ */
+ function UndoStack (view, size) {
+ this.view = view;
+ this.size = size !== undefined ? size : 100;
+ this.reset();
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ model = model.getBaseModel();
+ }
+ this.model = model;
+ var self = this;
+ this._listener = {
+ onChanging: function(e) {
+ self._onChanging(e);
+ },
+ onDestroy: function(e) {
+ self._onDestroy(e);
+ }
+ };
+ model.addEventListener("Changing", this._listener.onChanging);
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ }
+ UndoStack.prototype = /** @lends orion.textview.UndoStack.prototype */ {
+ /**
+ * Adds a change to the stack.
+ *
+ * @param change the change to add.
+ * @param {Number} change.offset the offset of the change
+ * @param {String} change.text the new text of the change
+ * @param {String} change.previousText the previous text of the change
+ */
+ add: function (change) {
+ if (this.compoundChange) {
+ this.compoundChange.add(change);
+ } else {
+ var length = this.stack.length;
+ this.stack.splice(this.index, length-this.index, change);
+ this.index++;
+ if (this.stack.length > this.size) {
+ this.stack.shift();
+ this.index--;
+ this.cleanIndex--;
+ }
+ }
+ },
+ /**
+ * Marks the current state of the stack as clean.
+ *
+ *
+ * This function is typically called when the content of view associated with the stack is saved.
+ *
+ *
+ * @see #isClean
+ */
+ markClean: function() {
+ this.endCompoundChange();
+ this._commitUndo();
+ this.cleanIndex = this.index;
+ },
+ /**
+ * Returns true if current state of stack is the same
+ * as the state when markClean() was called.
+ *
+ *
+ * For example, the application calls markClean(), then calls undo() four times and redo() four times.
+ * At this point isClean() returns true.
+ *
+ *
+ * This function is typically called to determine if the content of the view associated with the stack
+ * has changed since the last time it was saved.
+ *
+ *
+ * @return {Boolean} returns if the state is the same as the state when markClean() was called.
+ *
+ * @see #markClean
+ */
+ isClean: function() {
+ return this.cleanIndex === this.getSize().undo;
+ },
+ /**
+ * Returns true if there is at least one change to undo.
+ *
+ * @return {Boolean} returns true if there is at least one change to undo.
+ *
+ * @see #canRedo
+ * @see #undo
+ */
+ canUndo: function() {
+ return this.getSize().undo > 0;
+ },
+ /**
+ * Returns true if there is at least one change to redo.
+ *
+ * @return {Boolean} returns true if there is at least one change to redo.
+ *
+ * @see #canUndo
+ * @see #redo
+ */
+ canRedo: function() {
+ return this.getSize().redo > 0;
+ },
+ /**
+ * Finishes a compound change.
+ *
+ * @see #startCompoundChange
+ */
+ endCompoundChange: function() {
+ if (this.compoundChange) {
+ this.compoundChange.end(this.view);
+ }
+ this.compoundChange = undefined;
+ },
+ /**
+ * Returns the sizes of the stack.
+ *
+ * @return {object} a object where object.undo is the number of changes that can be un-done,
+ * and object.redo is the number of changes that can be re-done.
+ *
+ * @see #canUndo
+ * @see #canRedo
+ */
+ getSize: function() {
+ var index = this.index;
+ var length = this.stack.length;
+ if (this._undoStart !== undefined) {
+ index++;
+ }
+ return {undo: index, redo: (length - index)};
+ },
+ /**
+ * Undo the last change in the stack.
+ *
+ * @return {Boolean} returns true if a change was un-done.
+ *
+ * @see #redo
+ * @see #canUndo
+ */
+ undo: function() {
+ this._commitUndo();
+ if (this.index <= 0) {
+ return false;
+ }
+ var change = this.stack[--this.index];
+ this._ignoreUndo = true;
+ change.undo(this.view, true);
+ this._ignoreUndo = false;
+ return true;
+ },
+ /**
+ * Redo the last change in the stack.
+ *
+ * @return {Boolean} returns true if a change was re-done.
+ *
+ * @see #undo
+ * @see #canRedo
+ */
+ redo: function() {
+ this._commitUndo();
+ if (this.index >= this.stack.length) {
+ return false;
+ }
+ var change = this.stack[this.index++];
+ this._ignoreUndo = true;
+ change.redo(this.view, true);
+ this._ignoreUndo = false;
+ return true;
+ },
+ /**
+ * Reset the stack to its original state. All changes in the stack are thrown away.
+ */
+ reset: function() {
+ this.index = this.cleanIndex = 0;
+ this.stack = [];
+ this._undoStart = undefined;
+ this._undoText = "";
+ this._undoType = 0;
+ this._ignoreUndo = false;
+ this._compoundChange = undefined;
+ },
+ /**
+ * Starts a compound change.
+ *
+ * All changes added to stack from the time startCompoundChange() is called
+ * to the time that endCompoundChange() is called are compound on one change that can be un-done or re-done
+ * with one single call to undo() or redo().
+ *
+ *
+ * @see #endCompoundChange
+ */
+ startCompoundChange: function() {
+ this._commitUndo();
+ var change = new CompoundChange();
+ this.add(change);
+ this.compoundChange = change;
+ this.compoundChange.start(this.view);
+ },
+ _commitUndo: function () {
+ if (this._undoStart !== undefined) {
+ if (this._undoType === -1) {
+ this.add(new Change(this._undoStart, "", this._undoText, ""));
+ } else {
+ this.add(new Change(this._undoStart, this._undoText, ""));
+ }
+ this._undoStart = undefined;
+ this._undoText = "";
+ this._undoType = 0;
+ }
+ },
+ _onDestroy: function(evt) {
+ this.model.removeEventListener("Changing", this._listener.onChanging);
+ this.view.removeEventListener("Destroy", this._listener.onDestroy);
+ },
+ _onChanging: function(e) {
+ var newText = e.text;
+ var start = e.start;
+ var removedCharCount = e.removedCharCount;
+ var addedCharCount = e.addedCharCount;
+ if (this._ignoreUndo) {
+ return;
+ }
+ if (this._undoStart !== undefined &&
+ !((addedCharCount === 1 && removedCharCount === 0 && this._undoType === 1 && start === this._undoStart + this._undoText.length) ||
+ (addedCharCount === 0 && removedCharCount === 1 && this._undoType === -1 && (((start + 1) === this._undoStart) || (start === this._undoStart)))))
+ {
+ this._commitUndo();
+ }
+ if (!this.compoundChange) {
+ if (addedCharCount === 1 && removedCharCount === 0) {
+ if (this._undoStart === undefined) {
+ this._undoStart = start;
+ }
+ this._undoText = this._undoText + newText;
+ this._undoType = 1;
+ return;
+ } else if (addedCharCount === 0 && removedCharCount === 1) {
+ var deleting = this._undoText.length > 0 && this._undoStart === start;
+ this._undoStart = start;
+ this._undoType = -1;
+ if (deleting) {
+ this._undoText = this._undoText + this.model.getText(start, start + removedCharCount);
+ } else {
+ this._undoText = this.model.getText(start, start + removedCharCount) + this._undoText;
+ }
+ return;
+ }
+ }
+ this.add(new Change(start, newText, this.model.getText(start, start + removedCharCount)));
+ }
+ };
+
+ return {
+ UndoStack: UndoStack
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define window*/
+
+define("orion/textview/textModel", ['orion/textview/eventTarget'], function(mEventTarget) {
+ var isWindows = window.navigator.platform.indexOf("Win") !== -1;
+
+ /**
+ * Constructs a new TextModel with the given text and default line delimiter.
+ *
+ * @param {String} [text=""] the text that the model will store
+ * @param {String} [lineDelimiter=platform delimiter] the line delimiter used when inserting new lines to the model.
+ *
+ * @name orion.textview.TextModel
+ * @class The TextModel is an interface that provides text for the view. Applications may
+ * implement the TextModel interface to provide a custom store for the view content. The
+ * view interacts with its text model in order to access and update the text that is being
+ * displayed and edited in the view. This is the default implementation.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#setModel}
+ *
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function TextModel(text, lineDelimiter) {
+ this._lastLineIndex = -1;
+ this._text = [""];
+ this._lineOffsets = [0];
+ this.setText(text);
+ this.setLineDelimiter(lineDelimiter);
+ }
+
+ TextModel.prototype = /** @lends orion.textview.TextModel.prototype */ {
+ /**
+ * Returns the number of characters in the model.
+ *
+ * @returns {Number} the number of characters in the model.
+ */
+ getCharCount: function() {
+ var count = 0;
+ for (var i = 0; i
+ * The valid indices are 0 to line count exclusive. Returns null
+ * if the index is out of range.
+ *
+ *
+ * @param {Number} lineIndex the zero based index of the line.
+ * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter.
+ * @returns {String} the line text or null if out of range.
+ *
+ * @see #getLineAtOffset
+ */
+ getLine: function(lineIndex, includeDelimiter) {
+ var lineCount = this.getLineCount();
+ if (!(0 <= lineIndex && lineIndex < lineCount)) {
+ return null;
+ }
+ var start = this._lineOffsets[lineIndex];
+ if (lineIndex + 1 < lineCount) {
+ var text = this.getText(start, this._lineOffsets[lineIndex + 1]);
+ if (includeDelimiter) {
+ return text;
+ }
+ var end = text.length, c;
+ while (((c = text.charCodeAt(end - 1)) === 10) || (c === 13)) {
+ end--;
+ }
+ return text.substring(0, end);
+ } else {
+ return this.getText(start);
+ }
+ },
+ /**
+ * Returns the line index at the given character offset.
+ *
+ * The valid offsets are 0 to char count inclusive. The line index for
+ * char count is line count - 1. Returns -1 if
+ * the offset is out of range.
+ *
+ *
+ * @param {Number} offset a character offset.
+ * @returns {Number} the zero based line index or -1 if out of range.
+ */
+ getLineAtOffset: function(offset) {
+ var charCount = this.getCharCount();
+ if (!(0 <= offset && offset <= charCount)) {
+ return -1;
+ }
+ var lineCount = this.getLineCount();
+ if (offset === charCount) {
+ return lineCount - 1;
+ }
+ var lineStart, lineEnd;
+ var index = this._lastLineIndex;
+ if (0 <= index && index < lineCount) {
+ lineStart = this._lineOffsets[index];
+ lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount;
+ if (lineStart <= offset && offset < lineEnd) {
+ return index;
+ }
+ }
+ var high = lineCount;
+ var low = -1;
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ lineStart = this._lineOffsets[index];
+ lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount;
+ if (offset <= lineStart) {
+ high = index;
+ } else if (offset < lineEnd) {
+ high = index;
+ break;
+ } else {
+ low = index;
+ }
+ }
+ this._lastLineIndex = high;
+ return high;
+ },
+ /**
+ * Returns the number of lines in the model.
+ *
+ * The model always has at least one line.
+ *
+ *
+ * @returns {Number} the number of lines.
+ */
+ getLineCount: function() {
+ return this._lineOffsets.length;
+ },
+ /**
+ * Returns the line delimiter that is used by the view
+ * when inserting new lines. New lines entered using key strokes
+ * and paste operations use this line delimiter.
+ *
+ * @return {String} the line delimiter that is used by the view when inserting new lines.
+ */
+ getLineDelimiter: function() {
+ return this._lineDelimiter;
+ },
+ /**
+ * Returns the end character offset for the given line.
+ *
+ * The end offset is not inclusive. This means that when the line delimiter is included, the
+ * offset is either the start offset of the next line or char count. When the line delimiter is
+ * not included, the offset is the offset of the line delimiter.
+ *
+ *
+ * The valid indices are 0 to line count exclusive. Returns -1
+ * if the index is out of range.
+ *
+ *
+ * @param {Number} lineIndex the zero based index of the line.
+ * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter.
+ * @return {Number} the line end offset or -1 if out of range.
+ *
+ * @see #getLineStart
+ */
+ getLineEnd: function(lineIndex, includeDelimiter) {
+ var lineCount = this.getLineCount();
+ if (!(0 <= lineIndex && lineIndex < lineCount)) {
+ return -1;
+ }
+ if (lineIndex + 1 < lineCount) {
+ var end = this._lineOffsets[lineIndex + 1];
+ if (includeDelimiter) {
+ return end;
+ }
+ var text = this.getText(Math.max(this._lineOffsets[lineIndex], end - 2), end);
+ var i = text.length, c;
+ while (((c = text.charCodeAt(i - 1)) === 10) || (c === 13)) {
+ i--;
+ }
+ return end - (text.length - i);
+ } else {
+ return this.getCharCount();
+ }
+ },
+ /**
+ * Returns the start character offset for the given line.
+ *
+ * The valid indices are 0 to line count exclusive. Returns -1
+ * if the index is out of range.
+ *
+ *
+ * @param {Number} lineIndex the zero based index of the line.
+ * @return {Number} the line start offset or -1 if out of range.
+ *
+ * @see #getLineEnd
+ */
+ getLineStart: function(lineIndex) {
+ if (!(0 <= lineIndex && lineIndex < this.getLineCount())) {
+ return -1;
+ }
+ return this._lineOffsets[lineIndex];
+ },
+ /**
+ * Returns the text for the given range.
+ *
+ * The end offset is not inclusive. This means that character at the end offset
+ * is not included in the returned text.
+ *
+ *
+ * @param {Number} [start=0] the zero based start offset of text range.
+ * @param {Number} [end=char count] the zero based end offset of text range.
+ *
+ * @see #setText
+ */
+ getText: function(start, end) {
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = this.getCharCount(); }
+ if (start === end) { return ""; }
+ var offset = 0, chunk = 0, length;
+ while (chunk
+ * This notification is intended to be used only by the view. Application clients should
+ * use {@link orion.textview.TextView#event:onModelChanging}.
+ *
+ *
+ * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel
+ * as part of the implementation of {@link #setText}. This method is included in the public API for documentation
+ * purposes and to allow integration with other toolkit frameworks.
+ *
+ *
+ * @param {orion.textview.ModelChangingEvent} modelChangingEvent the changing event
+ */
+ onChanging: function(modelChangingEvent) {
+ return this.dispatchEvent(modelChangingEvent);
+ },
+ /**
+ * Notifies all listeners that the text has changed.
+ *
+ * This notification is intended to be used only by the view. Application clients should
+ * use {@link orion.textview.TextView#event:onModelChanged}.
+ *
+ *
+ * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel
+ * as part of the implementation of {@link #setText}. This method is included in the public API for documentation
+ * purposes and to allow integration with other toolkit frameworks.
+ *
+ *
+ * @param {orion.textview.ModelChangedEvent} modelChangedEvent the changed event
+ */
+ onChanged: function(modelChangedEvent) {
+ return this.dispatchEvent(modelChangedEvent);
+ },
+ /**
+ * Sets the line delimiter that is used by the view
+ * when new lines are inserted in the model due to key
+ * strokes and paste operations.
+ *
+ * If lineDelimiter is "auto", the delimiter is computed to be
+ * the first delimiter found the in the current text. If lineDelimiter
+ * is undefined or if there are no delimiters in the current text, the
+ * platform delimiter is used.
+ *
+ *
+ * @param {String} lineDelimiter the line delimiter that is used by the view when inserting new lines.
+ */
+ setLineDelimiter: function(lineDelimiter) {
+ if (lineDelimiter === "auto") {
+ lineDelimiter = undefined;
+ if (this.getLineCount() > 1) {
+ lineDelimiter = this.getText(this.getLineEnd(0), this.getLineEnd(0, true));
+ }
+ }
+ this._lineDelimiter = lineDelimiter ? lineDelimiter : (isWindows ? "\r\n" : "\n");
+ },
+ /**
+ * Replaces the text in the given range with the given text.
+ *
+ * The end offset is not inclusive. This means that the character at the
+ * end offset is not replaced.
+ *
+ *
+ * The text model must notify the listeners before and after the
+ * the text is changed by calling {@link #onChanging} and {@link #onChanged}
+ * respectively.
+ *
+ *
+ * @param {String} [text=""] the new text.
+ * @param {Number} [start=0] the zero based start offset of text range.
+ * @param {Number} [end=char count] the zero based end offset of text range.
+ *
+ * @see #getText
+ */
+ setText: function(text, start, end) {
+ if (text === undefined) { text = ""; }
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = this.getCharCount(); }
+ if (start === end && text === "") { return; }
+ var startLine = this.getLineAtOffset(start);
+ var endLine = this.getLineAtOffset(end);
+ var eventStart = start;
+ var removedCharCount = end - start;
+ var removedLineCount = endLine - startLine;
+ var addedCharCount = text.length;
+ var addedLineCount = 0;
+ var lineCount = this.getLineCount();
+
+ var cr = 0, lf = 0, index = 0;
+ var newLineOffsets = [];
+ while (true) {
+ if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
+ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
+ if (lf === -1 && cr === -1) { break; }
+ if (cr !== -1 && lf !== -1) {
+ if (cr + 1 === lf) {
+ index = lf + 1;
+ } else {
+ index = (cr < lf ? cr : lf) + 1;
+ }
+ } else if (cr !== -1) {
+ index = cr + 1;
+ } else {
+ index = lf + 1;
+ }
+ newLineOffsets.push(start + index);
+ addedLineCount++;
+ }
+
+ var modelChangingEvent = {
+ type: "Changing",
+ text: text,
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+
+ //TODO this should be done the loops below to avoid getText()
+ if (newLineOffsets.length === 0) {
+ var startLineOffset = this.getLineStart(startLine), endLineOffset;
+ if (endLine + 1 < lineCount) {
+ endLineOffset = this.getLineStart(endLine + 1);
+ } else {
+ endLineOffset = this.getCharCount();
+ }
+ if (start !== startLineOffset) {
+ text = this.getText(startLineOffset, start) + text;
+ start = startLineOffset;
+ }
+ if (end !== endLineOffset) {
+ text = text + this.getText(end, endLineOffset);
+ end = endLineOffset;
+ }
+ }
+
+ var changeCount = addedCharCount - removedCharCount;
+ for (var j = startLine + removedLineCount + 1; j < lineCount; j++) {
+ this._lineOffsets[j] += changeCount;
+ }
+ var args = [startLine + 1, removedLineCount].concat(newLineOffsets);
+ Array.prototype.splice.apply(this._lineOffsets, args);
+
+ var offset = 0, chunk = 0, length;
+ while (chunk
+ * See:
+ * {@link orion.textview.ProjectionTextModel}
+ * {@link orion.textview.ProjectionTextModel#addProjection}
+ *
+ * @name orion.textview.Projection
+ *
+ * @property {Number} start The start offset of the projection range.
+ * @property {Number} end The end offset of the projection range. This offset is exclusive.
+ * @property {String|orion.textview.TextModel} [text=""] The projection text to be inserted
+ */
+ /**
+ * Constructs a new ProjectionTextModel based on the specified TextModel.
+ *
+ * @param {orion.textview.TextModel} baseModel The base text model.
+ *
+ * @name orion.textview.ProjectionTextModel
+ * @class The ProjectionTextModel represents a projection of its base text
+ * model. Projection ranges can be added to the projection text model to hide and/or insert
+ * ranges to the base text model.
+ *
+ * The contents of the projection text model is modified when changes occur in the base model,
+ * projection model or by calls to {@link #addProjection} and {@link #removeProjection}.
+ *
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextModel}
+ * {@link orion.textview.TextView#setModel}
+ *
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function ProjectionTextModel(baseModel) {
+ this._model = baseModel; /* Base Model */
+ this._projections = [];
+ }
+
+ ProjectionTextModel.prototype = /** @lends orion.textview.ProjectionTextModel.prototype */ {
+ /**
+ * Adds a projection range to the model.
+ *
+ * The model must notify the listeners before and after the the text is
+ * changed by calling {@link #onChanging} and {@link #onChanged} respectively.
+ *
+ * @param {orion.textview.Projection} projection The projection range to be added.
+ *
+ * @see #removeProjection
+ */
+ addProjection: function(projection) {
+ if (!projection) {return;}
+ //start and end can't overlap any exist projection
+ var model = this._model, projections = this._projections;
+ projection._lineIndex = model.getLineAtOffset(projection.start);
+ projection._lineCount = model.getLineAtOffset(projection.end) - projection._lineIndex;
+ var text = projection.text;
+ if (!text) { text = ""; }
+ if (typeof text === "string") {
+ projection._model = new mTextModel.TextModel(text, model.getLineDelimiter());
+ } else {
+ projection._model = text;
+ }
+ var eventStart = this.mapOffset(projection.start, true);
+ var removedCharCount = projection.end - projection.start;
+ var removedLineCount = projection._lineCount;
+ var addedCharCount = projection._model.getCharCount();
+ var addedLineCount = projection._model.getLineCount() - 1;
+ var modelChangingEvent = {
+ type: "Changing",
+ text: projection._model.getText(),
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+ var index = this._binarySearch(projections, projection.start);
+ projections.splice(index, 0, projection);
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ },
+ /**
+ * Returns all projection ranges of this model.
+ *
+ * @return {orion.textview.Projection[]} The projection ranges.
+ *
+ * @see #addProjection
+ */
+ getProjections: function() {
+ return this._projections.slice(0);
+ },
+ /**
+ * Gets the base text model.
+ *
+ * @return {orion.textview.TextModel} The base text model.
+ */
+ getBaseModel: function() {
+ return this._model;
+ },
+ /**
+ * Maps offsets between the projection model and its base model.
+ *
+ * @param {Number} offset The offset to be mapped.
+ * @param {Boolean} [baseOffset=false] true if offset is in base model and
+ * should be mapped to the projection model.
+ * @return {Number} The mapped offset
+ */
+ mapOffset: function(offset, baseOffset) {
+ var projections = this._projections, delta = 0, i, projection;
+ if (baseOffset) {
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > offset) { break; }
+ if (projection.end > offset) { return -1; }
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ return offset + delta;
+ }
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > offset - delta) { break; }
+ var charCount = projection._model.getCharCount();
+ if (projection.start + charCount > offset - delta) {
+ return -1;
+ }
+ delta += charCount - (projection.end - projection.start);
+ }
+ return offset - delta;
+ },
+ /**
+ * Removes a projection range from the model.
+ *
+ * The model must notify the listeners before and after the the text is
+ * changed by calling {@link #onChanging} and {@link #onChanged} respectively.
+ *
+ *
+ * @param {orion.textview.Projection} projection The projection range to be removed.
+ *
+ * @see #addProjection
+ */
+ removeProjection: function(projection) {
+ //TODO remove listeners from model
+ var i, delta = 0;
+ for (i = 0; i < this._projections.length; i++) {
+ var p = this._projections[i];
+ if (p === projection) {
+ projection = p;
+ break;
+ }
+ delta += p._model.getCharCount() - (p.end - p.start);
+ }
+ if (i < this._projections.length) {
+ var model = this._model;
+ var eventStart = projection.start + delta;
+ var addedCharCount = projection.end - projection.start;
+ var addedLineCount = projection._lineCount;
+ var removedCharCount = projection._model.getCharCount();
+ var removedLineCount = projection._model.getLineCount() - 1;
+ var modelChangingEvent = {
+ type: "Changing",
+ text: model.getText(projection.start, projection.end),
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+ this._projections.splice(i, 1);
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ }
+ },
+ /** @ignore */
+ _binarySearch: function (array, offset) {
+ var high = array.length, low = -1, index;
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ if (offset <= array[index].start) {
+ high = index;
+ } else {
+ low = index;
+ }
+ }
+ return high;
+ },
+ /**
+ * @see orion.textview.TextModel#getCharCount
+ */
+ getCharCount: function() {
+ var count = this._model.getCharCount(), projections = this._projections;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ count += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ return count;
+ },
+ /**
+ * @see orion.textview.TextModel#getLine
+ */
+ getLine: function(lineIndex, includeDelimiter) {
+ if (lineIndex < 0) { return null; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, result = [], offset = 0, i, lineCount, projection;
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection._lineIndex >= lineIndex - delta) { break; }
+ lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount >= lineIndex - delta) {
+ var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
+ if (projectionLineIndex < lineCount) {
+ return projection._model.getLine(projectionLineIndex, includeDelimiter);
+ } else {
+ result.push(projection._model.getLine(lineCount));
+ }
+ }
+ offset = projection.end;
+ delta += lineCount - projection._lineCount;
+ }
+ offset = Math.max(offset, model.getLineStart(lineIndex - delta));
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection._lineIndex > lineIndex - delta) { break; }
+ result.push(model.getText(offset, projection.start));
+ lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount > lineIndex - delta) {
+ result.push(projection._model.getLine(0, includeDelimiter));
+ return result.join("");
+ }
+ result.push(projection._model.getText());
+ offset = projection.end;
+ delta += lineCount - projection._lineCount;
+ }
+ var end = model.getLineEnd(lineIndex - delta, includeDelimiter);
+ if (offset < end) {
+ result.push(model.getText(offset, end));
+ }
+ return result.join("");
+ },
+ /**
+ * @see orion.textview.TextModel#getLineAtOffset
+ */
+ getLineAtOffset: function(offset) {
+ var model = this._model, projections = this._projections;
+ var delta = 0, lineDelta = 0;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ if (projection.start > offset - delta) { break; }
+ var charCount = projection._model.getCharCount();
+ if (projection.start + charCount > offset - delta) {
+ var projectionOffset = offset - (projection.start + delta);
+ lineDelta += projection._model.getLineAtOffset(projectionOffset);
+ delta += projectionOffset;
+ break;
+ }
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += charCount - (projection.end - projection.start);
+ }
+ return model.getLineAtOffset(offset - delta) + lineDelta;
+ },
+ /**
+ * @see orion.textview.TextModel#getLineCount
+ */
+ getLineCount: function() {
+ var model = this._model, projections = this._projections;
+ var count = model.getLineCount();
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ count += projection._model.getLineCount() - 1 - projection._lineCount;
+ }
+ return count;
+ },
+ /**
+ * @see orion.textview.TextModel#getLineDelimiter
+ */
+ getLineDelimiter: function() {
+ return this._model.getLineDelimiter();
+ },
+ /**
+ * @see orion.textview.TextModel#getLineEnd
+ */
+ getLineEnd: function(lineIndex, includeDelimiter) {
+ if (lineIndex < 0) { return -1; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, offsetDelta = 0;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ if (projection._lineIndex > lineIndex - delta) { break; }
+ var lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount > lineIndex - delta) {
+ var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
+ return projection._model.getLineEnd (projectionLineIndex, includeDelimiter) + projection.start + offsetDelta;
+ }
+ offsetDelta += projection._model.getCharCount() - (projection.end - projection.start);
+ delta += lineCount - projection._lineCount;
+ }
+ return model.getLineEnd(lineIndex - delta, includeDelimiter) + offsetDelta;
+ },
+ /**
+ * @see orion.textview.TextModel#getLineStart
+ */
+ getLineStart: function(lineIndex) {
+ if (lineIndex < 0) { return -1; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, offsetDelta = 0;
+ for (var i = 0; i < projections.length; i++) {
+ var projection = projections[i];
+ if (projection._lineIndex >= lineIndex - delta) { break; }
+ var lineCount = projection._model.getLineCount() - 1;
+ if (projection._lineIndex + lineCount >= lineIndex - delta) {
+ var projectionLineIndex = lineIndex - (projection._lineIndex + delta);
+ return projection._model.getLineStart (projectionLineIndex) + projection.start + offsetDelta;
+ }
+ offsetDelta += projection._model.getCharCount() - (projection.end - projection.start);
+ delta += lineCount - projection._lineCount;
+ }
+ return model.getLineStart(lineIndex - delta) + offsetDelta;
+ },
+ /**
+ * @see orion.textview.TextModel#getText
+ */
+ getText: function(start, end) {
+ if (start === undefined) { start = 0; }
+ var model = this._model, projections = this._projections;
+ var delta = 0, result = [], i, projection, charCount;
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > start - delta) { break; }
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > start - delta) {
+ if (end !== undefined && projection.start + charCount > end - delta) {
+ return projection._model.getText(start - (projection.start + delta), end - (projection.start + delta));
+ } else {
+ result.push(projection._model.getText(start - (projection.start + delta)));
+ start = projection.end + delta + charCount - (projection.end - projection.start);
+ }
+ }
+ delta += charCount - (projection.end - projection.start);
+ }
+ var offset = start - delta;
+ if (end !== undefined) {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > end - delta) { break; }
+ result.push(model.getText(offset, projection.start));
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > end - delta) {
+ result.push(projection._model.getText(0, end - (projection.start + delta)));
+ return result.join("");
+ }
+ result.push(projection._model.getText());
+ offset = projection.end;
+ delta += charCount - (projection.end - projection.start);
+ }
+ result.push(model.getText(offset, end - delta));
+ } else {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ result.push(model.getText(offset, projection.start));
+ result.push(projection._model.getText());
+ offset = projection.end;
+ }
+ result.push(model.getText(offset));
+ }
+ return result.join("");
+ },
+ /** @ignore */
+ _onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) {
+ var model = this._model, projections = this._projections, i, projection, delta = 0, lineDelta;
+ var end = start + removedCharCount;
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > start) { break; }
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ /*TODO add stuff saved by setText*/
+ var mapStart = start + delta, rangeStart = i;
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > end) { break; }
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ }
+ /*TODO add stuff saved by setText*/
+ var mapEnd = end + delta, rangeEnd = i;
+ this.onChanging(mapStart, mapEnd - mapStart, addedCharCount/*TODO add stuff saved by setText*/, removedLineCount + lineDelta/*TODO add stuff saved by setText*/, addedLineCount/*TODO add stuff saved by setText*/);
+ projections.splice(projections, rangeEnd - rangeStart);
+ var count = text.length - (mapEnd - mapStart);
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ projection.start += count;
+ projection.end += count;
+ projection._lineIndex = model.getLineAtOffset(projection.start);
+ }
+ },
+ /**
+ * @see orion.textview.TextModel#onChanging
+ */
+ onChanging: function(modelChangingEvent) {
+ return this.dispatchEvent(modelChangingEvent);
+ },
+ /**
+ * @see orion.textview.TextModel#onChanged
+ */
+ onChanged: function(modelChangedEvent) {
+ return this.dispatchEvent(modelChangedEvent);
+ },
+ /**
+ * @see orion.textview.TextModel#setLineDelimiter
+ */
+ setLineDelimiter: function(lineDelimiter) {
+ this._model.setLineDelimiter(lineDelimiter);
+ },
+ /**
+ * @see orion.textview.TextModel#setText
+ */
+ setText: function(text, start, end) {
+ if (text === undefined) { text = ""; }
+ if (start === undefined) { start = 0; }
+ var eventStart = start, eventEnd = end;
+ var model = this._model, projections = this._projections;
+ var delta = 0, lineDelta = 0, i, projection, charCount, startProjection, endProjection, startLineDelta = 0;
+ for (i = 0; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > start - delta) { break; }
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > start - delta) {
+ if (end !== undefined && projection.start + charCount > end - delta) {
+ projection._model.setText(text, start - (projection.start + delta), end - (projection.start + delta));
+ //TODO events - special case
+ return;
+ } else {
+ startLineDelta = projection._model.getLineCount() - 1 - projection._model.getLineAtOffset(start - (projection.start + delta));
+ startProjection = {
+ projection: projection,
+ start: start - (projection.start + delta)
+ };
+ start = projection.end + delta + charCount - (projection.end - projection.start);
+ }
+ }
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += charCount - (projection.end - projection.start);
+ }
+ var mapStart = start - delta, rangeStart = i, startLine = model.getLineAtOffset(mapStart) + lineDelta - startLineDelta;
+ if (end !== undefined) {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ if (projection.start > end - delta) { break; }
+ charCount = projection._model.getCharCount();
+ if (projection.start + charCount > end - delta) {
+ lineDelta += projection._model.getLineAtOffset(end - (projection.start + delta));
+ charCount = end - (projection.start + delta);
+ end = projection.end + delta;
+ endProjection = {
+ projection: projection,
+ end: charCount
+ };
+ break;
+ }
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += charCount - (projection.end - projection.start);
+ }
+ } else {
+ for (; i < projections.length; i++) {
+ projection = projections[i];
+ lineDelta += projection._model.getLineCount() - 1 - projection._lineCount;
+ delta += projection._model.getCharCount() - (projection.end - projection.start);
+ }
+ end = eventEnd = model.getCharCount() + delta;
+ }
+ var mapEnd = end - delta, rangeEnd = i, endLine = model.getLineAtOffset(mapEnd) + lineDelta;
+
+ //events
+ var removedCharCount = eventEnd - eventStart;
+ var removedLineCount = endLine - startLine;
+ var addedCharCount = text.length;
+ var addedLineCount = 0;
+ var cr = 0, lf = 0, index = 0;
+ while (true) {
+ if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
+ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
+ if (lf === -1 && cr === -1) { break; }
+ if (cr !== -1 && lf !== -1) {
+ if (cr + 1 === lf) {
+ index = lf + 1;
+ } else {
+ index = (cr < lf ? cr : lf) + 1;
+ }
+ } else if (cr !== -1) {
+ index = cr + 1;
+ } else {
+ index = lf + 1;
+ }
+ addedLineCount++;
+ }
+
+ var modelChangingEvent = {
+ type: "Changing",
+ text: text,
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanging(modelChangingEvent);
+
+// var changeLineCount = model.getLineAtOffset(mapEnd) - model.getLineAtOffset(mapStart) + addedLineCount;
+ model.setText(text, mapStart, mapEnd);
+ if (startProjection) {
+ projection = startProjection.projection;
+ projection._model.setText("", startProjection.start);
+ }
+ if (endProjection) {
+ projection = endProjection.projection;
+ projection._model.setText("", 0, endProjection.end);
+ projection.start = projection.end;
+ projection._lineCount = 0;
+ }
+ projections.splice(rangeStart, rangeEnd - rangeStart);
+ var changeCount = text.length - (mapEnd - mapStart);
+ for (i = rangeEnd; i < projections.length; i++) {
+ projection = projections[i];
+ projection.start += changeCount;
+ projection.end += changeCount;
+// if (projection._lineIndex + changeLineCount !== model.getLineAtOffset(projection.start)) {
+// log("here");
+// }
+ projection._lineIndex = model.getLineAtOffset(projection.start);
+// projection._lineIndex += changeLineCount;
+ }
+
+ var modelChangedEvent = {
+ type: "Changed",
+ start: eventStart,
+ removedCharCount: removedCharCount,
+ addedCharCount: addedCharCount,
+ removedLineCount: removedLineCount,
+ addedLineCount: addedLineCount
+ };
+ this.onChanged(modelChangedEvent);
+ }
+ };
+ mEventTarget.EventTarget.addMixin(ProjectionTextModel.prototype);
+
+ return {ProjectionTextModel: ProjectionTextModel};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*global define setTimeout clearTimeout setInterval clearInterval Node */
+
+define("orion/textview/tooltip", ['orion/textview/textView', 'orion/textview/textModel', 'orion/textview/projectionTextModel'], function(mTextView, mTextModel, mProjectionTextModel) {
+
+ /** @private */
+ function Tooltip (view) {
+ this._view = view;
+ //TODO add API to get the parent of the view
+ this._create(view._parent.ownerDocument);
+ view.addEventListener("Destroy", this, this.destroy);
+ }
+ Tooltip.getTooltip = function(view) {
+ if (!view._tooltip) {
+ view._tooltip = new Tooltip(view);
+ }
+ return view._tooltip;
+ };
+ Tooltip.prototype = /** @lends orion.textview.Tooltip.prototype */ {
+ _create: function(document) {
+ if (this._domNode) { return; }
+ this._document = document;
+ var domNode = this._domNode = document.createElement("DIV");
+ domNode.className = "viewTooltip";
+ var viewParent = this._viewParent = document.createElement("DIV");
+ domNode.appendChild(viewParent);
+ var htmlParent = this._htmlParent = document.createElement("DIV");
+ domNode.appendChild(htmlParent);
+ document.body.appendChild(domNode);
+ this.hide();
+ },
+ destroy: function() {
+ if (!this._domNode) { return; }
+ if (this._contentsView) {
+ this._contentsView.destroy();
+ this._contentsView = null;
+ this._emptyModel = null;
+ }
+ var parent = this._domNode.parentNode;
+ if (parent) { parent.removeChild(this._domNode); }
+ this._domNode = null;
+ },
+ hide: function() {
+ if (this._contentsView) {
+ this._contentsView.setModel(this._emptyModel);
+ }
+ if (this._viewParent) {
+ this._viewParent.style.left = "-10000px";
+ this._viewParent.style.position = "fixed";
+ this._viewParent.style.visibility = "hidden";
+ }
+ if (this._htmlParent) {
+ this._htmlParent.style.left = "-10000px";
+ this._htmlParent.style.position = "fixed";
+ this._htmlParent.style.visibility = "hidden";
+ this._htmlParent.innerHTML = "";
+ }
+ if (this._domNode) {
+ this._domNode.style.visibility = "hidden";
+ }
+ if (this._showTimeout) {
+ clearTimeout(this._showTimeout);
+ this._showTimeout = null;
+ }
+ if (this._hideTimeout) {
+ clearTimeout(this._hideTimeout);
+ this._hideTimeout = null;
+ }
+ if (this._fadeTimeout) {
+ clearInterval(this._fadeTimeout);
+ this._fadeTimeout = null;
+ }
+ },
+ isVisible: function() {
+ return this._domNode && this._domNode.style.visibility === "visible";
+ },
+ setTarget: function(target) {
+ if (this.target === target) { return; }
+ this._target = target;
+ this.hide();
+ if (target) {
+ var self = this;
+ self._showTimeout = setTimeout(function() {
+ self.show(true);
+ }, 1000);
+ }
+ },
+ show: function(autoHide) {
+ if (!this._target) { return; }
+ var info = this._target.getTooltipInfo();
+ if (!info) { return; }
+ var domNode = this._domNode;
+ domNode.style.left = domNode.style.right = domNode.style.width = domNode.style.height = "auto";
+ var contents = info.contents, contentsDiv;
+ if (contents instanceof Array) {
+ contents = this._getAnnotationContents(contents);
+ }
+ if (typeof contents === "string") {
+ (contentsDiv = this._htmlParent).innerHTML = contents;
+ } else if (contents instanceof Node) {
+ (contentsDiv = this._htmlParent).appendChild(contents);
+ } else if (contents instanceof mProjectionTextModel.ProjectionTextModel) {
+ if (!this._contentsView) {
+ this._emptyModel = new mTextModel.TextModel("");
+ //TODO need hook into setup.js (or editor.js) to create a text view (and styler)
+ var newView = this._contentsView = new mTextView.TextView({
+ model: this._emptyModel,
+ parent: this._viewParent,
+ tabSize: 4,
+ sync: true,
+ stylesheet: ["/orion/textview/tooltip.css", "/orion/textview/rulers.css",
+ "/examples/textview/textstyler.css", "/css/default-theme.css"]
+ });
+ //TODO this is need to avoid IE from getting focus
+ newView._clientDiv.contentEditable = false;
+ //TODO need to find a better way of sharing the styler for multiple views
+ var view = this._view;
+ var listener = {
+ onLineStyle: function(e) {
+ view.onLineStyle(e);
+ }
+ };
+ newView.addEventListener("LineStyle", listener.onLineStyle);
+ }
+ var contentsView = this._contentsView;
+ contentsView.setModel(contents);
+ var size = contentsView.computeSize();
+ contentsDiv = this._viewParent;
+ //TODO always make the width larger than the size of the scrollbar to avoid bug in updatePage
+ contentsDiv.style.width = (size.width + 20) + "px";
+ contentsDiv.style.height = size.height + "px";
+ } else {
+ return;
+ }
+ contentsDiv.style.left = "auto";
+ contentsDiv.style.position = "static";
+ contentsDiv.style.visibility = "visible";
+ var left = parseInt(this._getNodeStyle(domNode, "padding-left", "0"), 10);
+ left += parseInt(this._getNodeStyle(domNode, "border-left-width", "0"), 10);
+ if (info.anchor === "right") {
+ var right = parseInt(this._getNodeStyle(domNode, "padding-right", "0"), 10);
+ right += parseInt(this._getNodeStyle(domNode, "border-right-width", "0"), 10);
+ domNode.style.right = (domNode.ownerDocument.body.getBoundingClientRect().right - info.x + left + right) + "px";
+ } else {
+ domNode.style.left = (info.x - left) + "px";
+ }
+ var top = parseInt(this._getNodeStyle(domNode, "padding-top", "0"), 10);
+ top += parseInt(this._getNodeStyle(domNode, "border-top-width", "0"), 10);
+ domNode.style.top = (info.y - top) + "px";
+ domNode.style.maxWidth = info.maxWidth + "px";
+ domNode.style.maxHeight = info.maxHeight + "px";
+ domNode.style.opacity = "1";
+ domNode.style.visibility = "visible";
+ if (autoHide) {
+ var self = this;
+ self._hideTimeout = setTimeout(function() {
+ var opacity = parseFloat(self._getNodeStyle(domNode, "opacity", "1"));
+ self._fadeTimeout = setInterval(function() {
+ if (domNode.style.visibility === "visible" && opacity > 0) {
+ opacity -= 0.1;
+ domNode.style.opacity = opacity;
+ return;
+ }
+ self.hide();
+ }, 50);
+ }, 5000);
+ }
+ },
+ _getAnnotationContents: function(annotations) {
+ if (annotations.length === 0) {
+ return null;
+ }
+ var model = this._view.getModel(), annotation;
+ var baseModel = model.getBaseModel ? model.getBaseModel() : model;
+ function getText(start, end) {
+ var textStart = baseModel.getLineStart(baseModel.getLineAtOffset(start));
+ var textEnd = baseModel.getLineEnd(baseModel.getLineAtOffset(end), true);
+ return baseModel.getText(textStart, textEnd);
+ }
+ var title;
+ if (annotations.length === 1) {
+ annotation = annotations[0];
+ if (annotation.title) {
+ title = annotation.title.replace(//g, ">");
+ return "" + annotation.html + "
" + title + " ";
+ } else {
+ var newModel = new mProjectionTextModel.ProjectionTextModel(baseModel);
+ var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(annotation.start));
+ newModel.addProjection({start: annotation.end, end: newModel.getCharCount()});
+ newModel.addProjection({start: 0, end: lineStart});
+ return newModel;
+ }
+ } else {
+ var tooltipHTML = "
Multiple annotations:
";
+ for (var i = 0; i < annotations.length; i++) {
+ annotation = annotations[i];
+ title = annotation.title;
+ if (!title) {
+ title = getText(annotation.start, annotation.end);
+ }
+ title = title.replace(//g, ">");
+ tooltipHTML += "
" + annotation.html + "
" + title + " ";
+ }
+ return tooltipHTML;
+ }
+ },
+ _getNodeStyle: function(node, prop, defaultValue) {
+ var value;
+ if (node) {
+ value = node.style[prop];
+ if (!value) {
+ if (node.currentStyle) {
+ var index = 0, p = prop;
+ while ((index = p.indexOf("-", index)) !== -1) {
+ p = p.substring(0, index) + p.substring(index + 1, index + 2).toUpperCase() + p.substring(index + 2);
+ }
+ value = node.currentStyle[p];
+ } else {
+ var css = node.ownerDocument.defaultView.getComputedStyle(node, null);
+ value = css ? css.getPropertyValue(prop) : null;
+ }
+ }
+ }
+ return value || defaultValue;
+ }
+ };
+ return {Tooltip: Tooltip};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ * Mihai Sucan (Mozilla Foundation) - fix for Bug#334583 Bug#348471 Bug#349485 Bug#350595 Bug#360726 Bug#361180 Bug#362835 Bug#362428 Bug#362286 Bug#354270 Bug#361474 Bug#363945 Bug#366312 Bug#370584
+ ******************************************************************************/
+
+/*global window document navigator setTimeout clearTimeout XMLHttpRequest define DOMException */
+
+define("orion/textview/textView", ['orion/textview/textModel', 'orion/textview/keyBinding', 'orion/textview/eventTarget'], function(mTextModel, mKeyBinding, mEventTarget) {
+
+ /** @private */
+ function addHandler(node, type, handler, capture) {
+ if (typeof node.addEventListener === "function") {
+ node.addEventListener(type, handler, capture === true);
+ } else {
+ node.attachEvent("on" + type, handler);
+ }
+ }
+ /** @private */
+ function removeHandler(node, type, handler, capture) {
+ if (typeof node.removeEventListener === "function") {
+ node.removeEventListener(type, handler, capture === true);
+ } else {
+ node.detachEvent("on" + type, handler);
+ }
+ }
+ var userAgent = navigator.userAgent;
+ var isIE;
+ if (document.selection && window.ActiveXObject && /MSIE/.test(userAgent)) {
+ isIE = document.documentMode ? document.documentMode : 7;
+ }
+ var isFirefox = 52;
+ var isOpera = userAgent.indexOf("Opera") !== -1;
+ var isChrome = userAgent.indexOf("Chrome") !== -1;
+ var isSafari = userAgent.indexOf("Safari") !== -1 && !isChrome;
+ var isWebkit = userAgent.indexOf("WebKit") !== -1;
+ var isPad = userAgent.indexOf("iPad") !== -1;
+ var isMac = navigator.platform.indexOf("Mac") !== -1;
+ var isWindows = navigator.platform.indexOf("Win") !== -1;
+ var isLinux = navigator.platform.indexOf("Linux") !== -1;
+ var isW3CEvents = typeof window.document.documentElement.addEventListener === "function";
+ var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function";
+ var platformDelimiter = isWindows ? "\r\n" : "\n";
+
+ /**
+ * Constructs a new Selection object.
+ *
+ * @class A Selection represents a range of selected text in the view.
+ * @name orion.textview.Selection
+ */
+ function Selection (start, end, caret) {
+ /**
+ * The selection start offset.
+ *
+ * @name orion.textview.Selection#start
+ */
+ this.start = start;
+ /**
+ * The selection end offset.
+ *
+ * @name orion.textview.Selection#end
+ */
+ this.end = end;
+ /** @private */
+ this.caret = caret; //true if the start, false if the caret is at end
+ }
+ Selection.prototype = /** @lends orion.textview.Selection.prototype */ {
+ /** @private */
+ clone: function() {
+ return new Selection(this.start, this.end, this.caret);
+ },
+ /** @private */
+ collapse: function() {
+ if (this.caret) {
+ this.end = this.start;
+ } else {
+ this.start = this.end;
+ }
+ },
+ /** @private */
+ extend: function (offset) {
+ if (this.caret) {
+ this.start = offset;
+ } else {
+ this.end = offset;
+ }
+ if (this.start > this.end) {
+ var tmp = this.start;
+ this.start = this.end;
+ this.end = tmp;
+ this.caret = !this.caret;
+ }
+ },
+ /** @private */
+ setCaret: function(offset) {
+ this.start = offset;
+ this.end = offset;
+ this.caret = false;
+ },
+ /** @private */
+ getCaret: function() {
+ return this.caret ? this.start : this.end;
+ },
+ /** @private */
+ toString: function() {
+ return "start=" + this.start + " end=" + this.end + (this.caret ? " caret is at start" : " caret is at end");
+ },
+ /** @private */
+ isEmpty: function() {
+ return this.start === this.end;
+ },
+ /** @private */
+ equals: function(object) {
+ return this.caret === object.caret && this.start === object.start && this.end === object.end;
+ }
+ };
+ /**
+ * @class This object describes the options for the text view.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#setOptions}
+ * {@link orion.textview.TextView#getOptions}
+ *
+ * @name orion.textview.TextViewOptions
+ *
+ * @property {String|DOMElement} parent the parent element for the view, it can be either a DOM element or an ID for a DOM element.
+ * @property {orion.textview.TextModel} [model] the text model for the view. If it is not set the view creates an empty {@link orion.textview.TextModel}.
+ * @property {Boolean} [readonly=false] whether or not the view is read-only.
+ * @property {Boolean} [fullSelection=true] whether or not the view is in full selection mode.
+ * @property {Boolean} [sync=false] whether or not the view creation should be synchronous (if possible).
+ * @property {Boolean} [expandTab=false] whether or not the tab key inserts white spaces.
+ * @property {String|String[]} [stylesheet] one or more stylesheet for the view. Each stylesheet can be either a URI or a string containing the CSS rules.
+ * @property {String} [themeClass] the CSS class for the view theming.
+ * @property {Number} [tabSize] The number of spaces in a tab.
+ */
+ /**
+ * Constructs a new text view.
+ *
+ * @param {orion.textview.TextViewOptions} options the view options.
+ *
+ * @class A TextView is a user interface for editing text.
+ * @name orion.textview.TextView
+ * @borrows orion.textview.EventTarget#addEventListener as #addEventListener
+ * @borrows orion.textview.EventTarget#removeEventListener as #removeEventListener
+ * @borrows orion.textview.EventTarget#dispatchEvent as #dispatchEvent
+ */
+ function TextView (options) {
+ this._init(options);
+ }
+
+ TextView.prototype = /** @lends orion.textview.TextView.prototype */ {
+ /**
+ * Adds a ruler to the text view.
+ *
+ * @param {orion.textview.Ruler} ruler the ruler.
+ */
+ addRuler: function (ruler) {
+ this._rulers.push(ruler);
+ ruler.setView(this);
+ this._createRuler(ruler);
+ this._updatePage();
+ },
+ computeSize: function() {
+ var w = 0, h = 0;
+ var model = this._model, clientDiv = this._clientDiv;
+ if (!clientDiv) { return {width: w, height: h}; }
+ var clientWidth = clientDiv.style.width;
+ /*
+ * Feature in WekKit. Webkit limits the width of the lines
+ * computed below to the width of the client div. This causes
+ * the lines to be wrapped even though "pre" is set. The fix
+ * is to set the width of the client div to a larger number
+ * before computing the lines width. Note that this value is
+ * reset to the appropriate value further down.
+ */
+ if (isWebkit) {
+ clientDiv.style.width = (0x7FFFF).toString() + "px";
+ }
+ var lineCount = model.getLineCount();
+ var document = this._frameDocument;
+ for (var lineIndex=0; lineIndex
The supported coordinate spaces are:
+ *
+ * "document" - relative to document, the origin is the top-left corner of first line
+ * "page" - relative to html page that contains the text view
+ * "view" - relative to text view, the origin is the top-left corner of the view container
+ *
+ *
+ * All methods in the view that take or return a position are in the document coordinate space.
+ *
+ * @param rect the rectangle to convert.
+ * @param rect.x the x of the rectangle.
+ * @param rect.y the y of the rectangle.
+ * @param rect.width the width of the rectangle.
+ * @param rect.height the height of the rectangle.
+ * @param {String} from the source coordinate space.
+ * @param {String} to the destination coordinate space.
+ *
+ * @see #getLocationAtOffset
+ * @see #getOffsetAtLocation
+ * @see #getTopPixel
+ * @see #setTopPixel
+ */
+ convert: function(rect, from, to) {
+ if (!this._clientDiv) { return; }
+ var scroll = this._getScroll();
+ var viewPad = this._getViewPadding();
+ var frame = this._frame.getBoundingClientRect();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ switch(from) {
+ case "document":
+ if (rect.x !== undefined) {
+ rect.x += - scroll.x + viewRect.left + viewPad.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += - scroll.y + viewRect.top + viewPad.top;
+ }
+ break;
+ case "page":
+ if (rect.x !== undefined) {
+ rect.x += - frame.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += - frame.top;
+ }
+ break;
+ }
+ //At this point rect is in the widget coordinate space
+ switch (to) {
+ case "document":
+ if (rect.x !== undefined) {
+ rect.x += scroll.x - viewRect.left - viewPad.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += scroll.y - viewRect.top - viewPad.top;
+ }
+ break;
+ case "page":
+ if (rect.x !== undefined) {
+ rect.x += frame.left;
+ }
+ if (rect.y !== undefined) {
+ rect.y += frame.top;
+ }
+ break;
+ }
+ return rect;
+ },
+ /**
+ * Destroys the text view.
+ *
+ * Removes the view from the page and frees all resources created by the view.
+ * Calling this function causes the "Destroy" event to be fire so that all components
+ * attached to view can release their references.
+ *
+ *
+ * @see #onDestroy
+ */
+ destroy: function() {
+ /* Destroy rulers*/
+ for (var i=0; i< this._rulers.length; i++) {
+ this._rulers[i].setView(null);
+ }
+ this.rulers = null;
+
+ /*
+ * Note that when the frame is removed, the unload event is trigged
+ * and the view contents and handlers is released properly by
+ * destroyView().
+ */
+ this._destroyFrame();
+
+ var e = {type: "Destroy"};
+ this.onDestroy(e);
+
+ this._parent = null;
+ this._parentDocument = null;
+ this._model = null;
+ this._selection = null;
+ this._doubleClickSelection = null;
+ this._keyBindings = null;
+ this._actions = null;
+ },
+ /**
+ * Gives focus to the text view.
+ */
+ focus: function() {
+ if (!this._clientDiv) { return; }
+ /*
+ * Feature in Chrome. When focus is called in the clientDiv without
+ * setting selection the browser will set the selection to the first dom
+ * element, which can be above the client area. When this happen the
+ * browser also scrolls the window to show that element.
+ * The fix is to call _updateDOMSelection() before calling focus().
+ */
+ this._updateDOMSelection();
+ if (isPad) {
+ this._textArea.focus();
+ } else {
+ if (isOpera) { this._clientDiv.blur(); }
+ this._clientDiv.focus();
+ }
+ /*
+ * Feature in Safari. When focus is called the browser selects the clientDiv
+ * itself. The fix is to call _updateDOMSelection() after calling focus().
+ */
+ this._updateDOMSelection();
+ },
+ /**
+ * Check if the text view has focus.
+ *
+ * @returns {Boolean} true if the text view has focus, otherwise false.
+ */
+ hasFocus: function() {
+ return this._hasFocus;
+ },
+ /**
+ * Returns all action names defined in the text view.
+ *
+ * There are two types of actions, the predefined actions of the view
+ * and the actions added by application code.
+ *
+ *
+ * The predefined actions are:
+ *
+ * Navigation actions. These actions move the caret collapsing the selection.
+ *
+ * "lineUp" - moves the caret up by one line
+ * "lineDown" - moves the caret down by one line
+ * "lineStart" - moves the caret to beginning of the current line
+ * "lineEnd" - moves the caret to end of the current line
+ * "charPrevious" - moves the caret to the previous character
+ * "charNext" - moves the caret to the next character
+ * "pageUp" - moves the caret up by one page
+ * "pageDown" - moves the caret down by one page
+ * "wordPrevious" - moves the caret to the previous word
+ * "wordNext" - moves the caret to the next word
+ * "textStart" - moves the caret to the beginning of the document
+ * "textEnd" - moves the caret to the end of the document
+ *
+ * Selection actions. These actions move the caret extending the selection.
+ *
+ * "selectLineUp" - moves the caret up by one line
+ * "selectLineDown" - moves the caret down by one line
+ * "selectLineStart" - moves the caret to beginning of the current line
+ * "selectLineEnd" - moves the caret to end of the current line
+ * "selectCharPrevious" - moves the caret to the previous character
+ * "selectCharNext" - moves the caret to the next character
+ * "selectPageUp" - moves the caret up by one page
+ * "selectPageDown" - moves the caret down by one page
+ * "selectWordPrevious" - moves the caret to the previous word
+ * "selectWordNext" - moves the caret to the next word
+ * "selectTextStart" - moves the caret to the beginning of the document
+ * "selectTextEnd" - moves the caret to the end of the document
+ * "selectAll" - selects the entire document
+ *
+ * Edit actions. These actions modify the text view text
+ *
+ * "deletePrevious" - deletes the character preceding the caret
+ * "deleteNext" - deletes the charecter following the caret
+ * "deleteWordPrevious" - deletes the word preceding the caret
+ * "deleteWordNext" - deletes the word following the caret
+ * "tab" - inserts a tab character at the caret
+ * "enter" - inserts a line delimiter at the caret
+ *
+ * Clipboard actions.
+ *
+ * "copy" - copies the selected text to the clipboard
+ * "cut" - copies the selected text to the clipboard and deletes the selection
+ * "paste" - replaces the selected text with the clipboard contents
+ *
+ *
+ *
+ *
+ * @param {Boolean} [defaultAction=false] whether or not the predefined actions are included.
+ * @returns {String[]} an array of action names defined in the text view.
+ *
+ * @see #invokeAction
+ * @see #setAction
+ * @see #setKeyBinding
+ * @see #getKeyBindings
+ */
+ getActions: function (defaultAction) {
+ var result = [];
+ var actions = this._actions;
+ for (var i = 0; i < actions.length; i++) {
+ if (!defaultAction && actions[i].defaultHandler) { continue; }
+ result.push(actions[i].name);
+ }
+ return result;
+ },
+ /**
+ * Returns the bottom index.
+ *
+ * The bottom index is the line that is currently at the bottom of the view. This
+ * line may be partially visible depending on the vertical scroll of the view. The parameter
+ * fullyVisible determines whether to return only fully visible lines.
+ *
+ *
+ * @param {Boolean} [fullyVisible=false] if true, returns the index of the last fully visible line. This
+ * parameter is ignored if the view is not big enough to show one line.
+ * @returns {Number} the index of the bottom line.
+ *
+ * @see #getTopIndex
+ * @see #setTopIndex
+ */
+ getBottomIndex: function(fullyVisible) {
+ if (!this._clientDiv) { return 0; }
+ return this._getBottomIndex(fullyVisible);
+ },
+ /**
+ * Returns the bottom pixel.
+ *
+ * The bottom pixel is the pixel position that is currently at
+ * the bottom edge of the view. This position is relative to the
+ * beginning of the document.
+ *
+ *
+ * @returns {Number} the bottom pixel.
+ *
+ * @see #getTopPixel
+ * @see #setTopPixel
+ * @see #convert
+ */
+ getBottomPixel: function() {
+ if (!this._clientDiv) { return 0; }
+ return this._getScroll().y + this._getClientHeight();
+ },
+ /**
+ * Returns the caret offset relative to the start of the document.
+ *
+ * @returns the caret offset relative to the start of the document.
+ *
+ * @see #setCaretOffset
+ * @see #setSelection
+ * @see #getSelection
+ */
+ getCaretOffset: function () {
+ var s = this._getSelection();
+ return s.getCaret();
+ },
+ /**
+ * Returns the client area.
+ *
+ * The client area is the portion in pixels of the document that is visible. The
+ * client area position is relative to the beginning of the document.
+ *
+ *
+ * @returns the client area rectangle {x, y, width, height}.
+ *
+ * @see #getTopPixel
+ * @see #getBottomPixel
+ * @see #getHorizontalPixel
+ * @see #convert
+ */
+ getClientArea: function() {
+ if (!this._clientDiv) { return {x: 0, y: 0, width: 0, height: 0}; }
+ var scroll = this._getScroll();
+ return {x: scroll.x, y: scroll.y, width: this._getClientWidth(), height: this._getClientHeight()};
+ },
+ /**
+ * Returns the horizontal pixel.
+ *
+ * The horizontal pixel is the pixel position that is currently at
+ * the left edge of the view. This position is relative to the
+ * beginning of the document.
+ *
+ *
+ * @returns {Number} the horizontal pixel.
+ *
+ * @see #setHorizontalPixel
+ * @see #convert
+ */
+ getHorizontalPixel: function() {
+ if (!this._clientDiv) { return 0; }
+ return this._getScroll().x;
+ },
+ /**
+ * Returns all the key bindings associated to the given action name.
+ *
+ * @param {String} name the action name.
+ * @returns {orion.textview.KeyBinding[]} the array of key bindings associated to the given action name.
+ *
+ * @see #setKeyBinding
+ * @see #setAction
+ */
+ getKeyBindings: function (name) {
+ var result = [];
+ var keyBindings = this._keyBindings;
+ for (var i = 0; i < keyBindings.length; i++) {
+ if (keyBindings[i].name === name) {
+ result.push(keyBindings[i].keyBinding);
+ }
+ }
+ return result;
+ },
+ /**
+ * Returns the line height for a given line index. Returns the default line
+ * height if the line index is not specified.
+ *
+ * @param {Number} [lineIndex] the line index.
+ * @returns {Number} the height of the line in pixels.
+ *
+ * @see #getLinePixel
+ */
+ getLineHeight: function(lineIndex) {
+ if (!this._clientDiv) { return 0; }
+ return this._getLineHeight();
+ },
+ /**
+ * Returns the top pixel position of a given line index relative to the beginning
+ * of the document.
+ *
+ * Clamps out of range indices.
+ *
+ *
+ * @param {Number} lineIndex the line index.
+ * @returns {Number} the pixel position of the line.
+ *
+ * @see #setTopPixel
+ * @see #convert
+ */
+ getLinePixel: function(lineIndex) {
+ if (!this._clientDiv) { return 0; }
+ lineIndex = Math.min(Math.max(0, lineIndex), this._model.getLineCount());
+ var lineHeight = this._getLineHeight();
+ return lineHeight * lineIndex;
+ },
+ /**
+ * Returns the {x, y} pixel location of the top-left corner of the character
+ * bounding box at the specified offset in the document. The pixel location
+ * is relative to the document.
+ *
+ * Clamps out of range offsets.
+ *
+ *
+ * @param {Number} offset the character offset
+ * @returns the {x, y} pixel location of the given offset.
+ *
+ * @see #getOffsetAtLocation
+ * @see #convert
+ */
+ getLocationAtOffset: function(offset) {
+ if (!this._clientDiv) { return {x: 0, y: 0}; }
+ var model = this._model;
+ offset = Math.min(Math.max(0, offset), model.getCharCount());
+ var lineIndex = model.getLineAtOffset(offset);
+ var scroll = this._getScroll();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var x = this._getOffsetToX(offset) + scroll.x - viewRect.left - viewPad.left;
+ var y = this.getLinePixel(lineIndex);
+ return {x: x, y: y};
+ },
+ /**
+ * Returns the specified view options.
+ *
+ * The returned value is either a orion.textview.TextViewOptions or an option value. An option value is returned when only one string paremeter
+ * is specified. A orion.textview.TextViewOptions is returned when there are no paremeters, or the parameters are a list of options names or a
+ * orion.textview.TextViewOptions. All view options are returned when there no paremeters.
+ *
+ *
+ * @param {String|orion.textview.TextViewOptions} [options] The options to return.
+ * @return {Object|orion.textview.TextViewOptions} The requested options or an option value.
+ *
+ * @see #setOptions
+ */
+ getOptions: function() {
+ var options;
+ if (arguments.length === 0) {
+ options = this._defaultOptions();
+ } else if (arguments.length === 1) {
+ var arg = arguments[0];
+ if (typeof arg === "string") {
+ return this._clone(this["_" + arg]);
+ }
+ options = arg;
+ } else {
+ options = {};
+ for (var index in arguments) {
+ if (arguments.hasOwnProperty(index)) {
+ options[arguments[index]] = undefined;
+ }
+ }
+ }
+ for (var option in options) {
+ if (options.hasOwnProperty(option)) {
+ options[option] = this._clone(this["_" + option]);
+ }
+ }
+ return options;
+ },
+ /**
+ * Returns the text model of the text view.
+ *
+ * @returns {orion.textview.TextModel} the text model of the view.
+ */
+ getModel: function() {
+ return this._model;
+ },
+ /**
+ * Returns the character offset nearest to the given pixel location. The
+ * pixel location is relative to the document.
+ *
+ * @param x the x of the location
+ * @param y the y of the location
+ * @returns the character offset at the given location.
+ *
+ * @see #getLocationAtOffset
+ */
+ getOffsetAtLocation: function(x, y) {
+ if (!this._clientDiv) { return 0; }
+ var scroll = this._getScroll();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var lineIndex = this._getYToLine(y - scroll.y);
+ x += -scroll.x + viewRect.left + viewPad.left;
+ var offset = this._getXToOffset(lineIndex, x);
+ return offset;
+ },
+ /**
+ * Get the view rulers.
+ *
+ * @returns the view rulers
+ *
+ * @see #addRuler
+ */
+ getRulers: function() {
+ return this._rulers.slice(0);
+ },
+ /**
+ * Returns the text view selection.
+ *
+ * The selection is defined by a start and end character offset relative to the
+ * document. The character at end offset is not included in the selection.
+ *
+ *
+ * @returns {orion.textview.Selection} the view selection
+ *
+ * @see #setSelection
+ */
+ getSelection: function () {
+ var s = this._getSelection();
+ return {start: s.start, end: s.end};
+ },
+ /**
+ * Returns the text for the given range.
+ *
+ * The text does not include the character at the end offset.
+ *
+ *
+ * @param {Number} [start=0] the start offset of text range.
+ * @param {Number} [end=char count] the end offset of text range.
+ *
+ * @see #setText
+ */
+ getText: function(start, end) {
+ var model = this._model;
+ return model.getText(start, end);
+ },
+ /**
+ * Returns the top index.
+ *
+ * The top index is the line that is currently at the top of the view. This
+ * line may be partially visible depending on the vertical scroll of the view. The parameter
+ * fullyVisible determines whether to return only fully visible lines.
+ *
+ *
+ * @param {Boolean} [fullyVisible=false] if true, returns the index of the first fully visible line. This
+ * parameter is ignored if the view is not big enough to show one line.
+ * @returns {Number} the index of the top line.
+ *
+ * @see #getBottomIndex
+ * @see #setTopIndex
+ */
+ getTopIndex: function(fullyVisible) {
+ if (!this._clientDiv) { return 0; }
+ return this._getTopIndex(fullyVisible);
+ },
+ /**
+ * Returns the top pixel.
+ *
+ * The top pixel is the pixel position that is currently at
+ * the top edge of the view. This position is relative to the
+ * beginning of the document.
+ *
+ *
+ * @returns {Number} the top pixel.
+ *
+ * @see #getBottomPixel
+ * @see #setTopPixel
+ * @see #convert
+ */
+ getTopPixel: function() {
+ if (!this._clientDiv) { return 0; }
+ return this._getScroll().y;
+ },
+ /**
+ * Executes the action handler associated with the given name.
+ *
+ * The application defined action takes precedence over predefined actions unless
+ * the defaultAction paramater is true.
+ *
+ *
+ * If the application defined action returns false, the text view predefined
+ * action is executed if present.
+ *
+ *
+ * @param {String} name the action name.
+ * @param {Boolean} [defaultAction] whether to always execute the predefined action.
+ * @returns {Boolean} true if the action was executed.
+ *
+ * @see #setAction
+ * @see #getActions
+ */
+ invokeAction: function (name, defaultAction) {
+ if (!this._clientDiv) { return; }
+ var actions = this._actions;
+ for (var i = 0; i < actions.length; i++) {
+ var a = actions[i];
+ if (a.name && a.name === name) {
+ if (!defaultAction && a.userHandler) {
+ if (a.userHandler()) { return; }
+ }
+ if (a.defaultHandler) { return a.defaultHandler(); }
+ return false;
+ }
+ }
+ return false;
+ },
+ /**
+ * Returns if the view is loaded.
+ *
+ * @returns {Boolean} true if the view is loaded.
+ *
+ * @see #onLoad
+ */
+ isLoaded: function () {
+ return !!this._clientDiv;
+ },
+ /**
+ * @class This is the event sent when the user right clicks or otherwise invokes the context menu of the view.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onContextMenu}
+ *
+ *
+ * @name orion.textview.ContextMenuEvent
+ *
+ * @property {Number} x The pointer location on the x axis, relative to the document the user is editing.
+ * @property {Number} y The pointer location on the y axis, relative to the document the user is editing.
+ * @property {Number} screenX The pointer location on the x axis, relative to the screen. This is copied from the DOM contextmenu event.screenX property.
+ * @property {Number} screenY The pointer location on the y axis, relative to the screen. This is copied from the DOM contextmenu event.screenY property.
+ */
+ /**
+ * This event is sent when the user invokes the view context menu.
+ *
+ * @event
+ * @param {orion.textview.ContextMenuEvent} contextMenuEvent the event
+ */
+ onContextMenu: function(contextMenuEvent) {
+ return this.dispatchEvent(contextMenuEvent);
+ },
+ onDragStart: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDrag: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragEnd: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragEnter: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragOver: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDragLeave: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ onDrop: function(dragEvent) {
+ return this.dispatchEvent(dragEvent);
+ },
+ /**
+ * @class This is the event sent when the text view is destroyed.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onDestroy}
+ *
+ * @name orion.textview.DestroyEvent
+ */
+ /**
+ * This event is sent when the text view has been destroyed.
+ *
+ * @event
+ * @param {orion.textview.DestroyEvent} destroyEvent the event
+ *
+ * @see #destroy
+ */
+ onDestroy: function(destroyEvent) {
+ return this.dispatchEvent(destroyEvent);
+ },
+ /**
+ * @class This object is used to define style information for the text view.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLineStyle}
+ *
+ * @name orion.textview.Style
+ *
+ * @property {String} styleClass A CSS class name.
+ * @property {Object} style An object with CSS properties.
+ * @property {String} tagName A DOM tag name.
+ * @property {Object} attributes An object with DOM attributes.
+ */
+ /**
+ * @class This object is used to style range.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLineStyle}
+ *
+ * @name orion.textview.StyleRange
+ *
+ * @property {Number} start The start character offset, relative to the document, where the style should be applied.
+ * @property {Number} end The end character offset (exclusive), relative to the document, where the style should be applied.
+ * @property {orion.textview.Style} style The style for the range.
+ */
+ /**
+ * @class This is the event sent when the text view needs the style information for a line.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLineStyle}
+ *
+ * @name orion.textview.LineStyleEvent
+ *
+ * @property {orion.textview.TextView} textView The text view.
+ * @property {Number} lineIndex The line index.
+ * @property {String} lineText The line text.
+ * @property {Number} lineStart The character offset, relative to document, of the first character in the line.
+ * @property {orion.textview.Style} style The style for the entire line (output argument).
+ * @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument).
+ */
+ /**
+ * This event is sent when the text view needs the style information for a line.
+ *
+ * @event
+ * @param {orion.textview.LineStyleEvent} lineStyleEvent the event
+ */
+ onLineStyle: function(lineStyleEvent) {
+ return this.dispatchEvent(lineStyleEvent);
+ },
+ /**
+ * @class This is the event sent when the text view has loaded its contents.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLoad}
+ *
+ * @name orion.textview.LoadEvent
+ */
+ /**
+ * This event is sent when the text view has loaded its contents.
+ *
+ * @event
+ * @param {orion.textview.LoadEvent} loadEvent the event
+ */
+ onLoad: function(loadEvent) {
+ return this.dispatchEvent(loadEvent);
+ },
+ /**
+ * @class This is the event sent when the text in the model has changed.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onModelChanged}
+ * {@link orion.textview.TextModel#onChanged}
+ *
+ * @name orion.textview.ModelChangedEvent
+ *
+ * @property {Number} start The character offset in the model where the change has occurred.
+ * @property {Number} removedCharCount The number of characters removed from the model.
+ * @property {Number} addedCharCount The number of characters added to the model.
+ * @property {Number} removedLineCount The number of lines removed from the model.
+ * @property {Number} addedLineCount The number of lines added to the model.
+ */
+ /**
+ * This event is sent when the text in the model has changed.
+ *
+ * @event
+ * @param {orion.textview.ModelChangedEvent} modelChangedEvent the event
+ */
+ onModelChanged: function(modelChangedEvent) {
+ return this.dispatchEvent(modelChangedEvent);
+ },
+ /**
+ * @class This is the event sent when the text in the model is about to change.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onModelChanging}
+ * {@link orion.textview.TextModel#onChanging}
+ *
+ * @name orion.textview.ModelChangingEvent
+ *
+ * @property {String} text The text that is about to be inserted in the model.
+ * @property {Number} start The character offset in the model where the change will occur.
+ * @property {Number} removedCharCount The number of characters being removed from the model.
+ * @property {Number} addedCharCount The number of characters being added to the model.
+ * @property {Number} removedLineCount The number of lines being removed from the model.
+ * @property {Number} addedLineCount The number of lines being added to the model.
+ */
+ /**
+ * This event is sent when the text in the model is about to change.
+ *
+ * @event
+ * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event
+ */
+ onModelChanging: function(modelChangingEvent) {
+ return this.dispatchEvent(modelChangingEvent);
+ },
+ /**
+ * @class This is the event sent when the text is modified by the text view.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onModify}
+ *
+ * @name orion.textview.ModifyEvent
+ */
+ /**
+ * This event is sent when the text view has changed text in the model.
+ *
+ * If the text is changed directly through the model API, this event
+ * is not sent.
+ *
+ *
+ * @event
+ * @param {orion.textview.ModifyEvent} modifyEvent the event
+ */
+ onModify: function(modifyEvent) {
+ return this.dispatchEvent(modifyEvent);
+ },
+ onMouseDown: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseUp: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseMove: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseOver: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ onMouseOut: function(mouseEvent) {
+ return this.dispatchEvent(mouseEvent);
+ },
+ /**
+ * @class This is the event sent when the selection changes in the text view.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onSelection}
+ *
+ * @name orion.textview.SelectionEvent
+ *
+ * @property {orion.textview.Selection} oldValue The old selection.
+ * @property {orion.textview.Selection} newValue The new selection.
+ */
+ /**
+ * This event is sent when the text view selection has changed.
+ *
+ * @event
+ * @param {orion.textview.SelectionEvent} selectionEvent the event
+ */
+ onSelection: function(selectionEvent) {
+ return this.dispatchEvent(selectionEvent);
+ },
+ /**
+ * @class This is the event sent when the text view scrolls.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onScroll}
+ *
+ * @name orion.textview.ScrollEvent
+ *
+ * @property oldValue The old scroll {x,y}.
+ * @property newValue The new scroll {x,y}.
+ */
+ /**
+ * This event is sent when the text view scrolls vertically or horizontally.
+ *
+ * @event
+ * @param {orion.textview.ScrollEvent} scrollEvent the event
+ */
+ onScroll: function(scrollEvent) {
+ return this.dispatchEvent(scrollEvent);
+ },
+ /**
+ * @class This is the event sent when the text is about to be modified by the text view.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onVerify}
+ *
+ * @name orion.textview.VerifyEvent
+ *
+ * @property {String} text The text being inserted.
+ * @property {Number} start The start offset of the text range to be replaced.
+ * @property {Number} end The end offset (exclusive) of the text range to be replaced.
+ */
+ /**
+ * This event is sent when the text view is about to change text in the model.
+ *
+ * If the text is changed directly through the model API, this event
+ * is not sent.
+ *
+ *
+ * Listeners are allowed to change these parameters. Setting text to null
+ * or undefined stops the change.
+ *
+ *
+ * @event
+ * @param {orion.textview.VerifyEvent} verifyEvent the event
+ */
+ onVerify: function(verifyEvent) {
+ return this.dispatchEvent(verifyEvent);
+ },
+ /**
+ * @class This is the event sent when the text view has unloaded its contents.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onLoad}
+ *
+ * @name orion.textview.UnloadEvent
+ */
+ /**
+ * This event is sent when the text view has unloaded its contents.
+ *
+ * @event
+ * @param {orion.textview.UnloadEvent} unloadEvent the event
+ */
+ onUnload: function(unloadEvent) {
+ return this.dispatchEvent(unloadEvent);
+ },
+ /**
+ * @class This is the event sent when the text view is focused.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onFocus}
+ *
+ * @name orion.textview.FocusEvent
+ */
+ /**
+ * This event is sent when the text view is focused.
+ *
+ * @event
+ * @param {orion.textview.FocusEvent} focusEvent the event
+ */
+ onFocus: function(focusEvent) {
+ return this.dispatchEvent(focusEvent);
+ },
+ /**
+ * @class This is the event sent when the text view goes out of focus.
+ *
+ * See:
+ * {@link orion.textview.TextView}
+ * {@link orion.textview.TextView#event:onBlur}
+ *
+ * @name orion.textview.BlurEvent
+ */
+ /**
+ * This event is sent when the text view goes out of focus.
+ *
+ * @event
+ * @param {orion.textview.BlurEvent} blurEvent the event
+ */
+ onBlur: function(blurEvent) {
+ return this.dispatchEvent(blurEvent);
+ },
+ /**
+ * Redraws the entire view, including rulers.
+ *
+ * @see #redrawLines
+ * @see #redrawRange
+ * @see #setRedraw
+ */
+ redraw: function() {
+ if (this._redrawCount > 0) { return; }
+ var lineCount = this._model.getLineCount();
+ var rulers = this.getRulers();
+ for (var i = 0; i < rulers.length; i++) {
+ this.redrawLines(0, lineCount, rulers[i]);
+ }
+ this.redrawLines(0, lineCount);
+ },
+ /**
+ * Redraws the text in the given line range.
+ *
+ * The line at the end index is not redrawn.
+ *
+ *
+ * @param {Number} [startLine=0] the start line
+ * @param {Number} [endLine=line count] the end line
+ *
+ * @see #redraw
+ * @see #redrawRange
+ * @see #setRedraw
+ */
+ redrawLines: function(startLine, endLine, ruler) {
+ if (this._redrawCount > 0) { return; }
+ if (startLine === undefined) { startLine = 0; }
+ if (endLine === undefined) { endLine = this._model.getLineCount(); }
+ if (startLine === endLine) { return; }
+ var div = this._clientDiv;
+ if (!div) { return; }
+ if (ruler) {
+ var location = ruler.getLocation();//"left" or "right"
+ var divRuler = location === "left" ? this._leftDiv : this._rightDiv;
+ var cells = divRuler.firstChild.rows[0].cells;
+ for (var i = 0; i < cells.length; i++) {
+ if (cells[i].firstChild._ruler === ruler) {
+ div = cells[i].firstChild;
+ break;
+ }
+ }
+ }
+ if (ruler) {
+ div.rulerChanged = true;
+ }
+ if (!ruler || ruler.getOverview() === "page") {
+ var child = div.firstChild;
+ while (child) {
+ var lineIndex = child.lineIndex;
+ if (startLine <= lineIndex && lineIndex < endLine) {
+ child.lineChanged = true;
+ }
+ child = child.nextSibling;
+ }
+ }
+ if (!ruler) {
+ if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) {
+ this._checkMaxLineIndex = this._maxLineIndex;
+ this._maxLineIndex = -1;
+ this._maxLineWidth = 0;
+ }
+ }
+ this._queueUpdatePage();
+ },
+ /**
+ * Redraws the text in the given range.
+ *
+ * The character at the end offset is not redrawn.
+ *
+ *
+ * @param {Number} [start=0] the start offset of text range
+ * @param {Number} [end=char count] the end offset of text range
+ *
+ * @see #redraw
+ * @see #redrawLines
+ * @see #setRedraw
+ */
+ redrawRange: function(start, end) {
+ if (this._redrawCount > 0) { return; }
+ var model = this._model;
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = model.getCharCount(); }
+ var startLine = model.getLineAtOffset(start);
+ var endLine = model.getLineAtOffset(Math.max(start, end - 1)) + 1;
+ this.redrawLines(startLine, endLine);
+ },
+ /**
+ * Removes a ruler from the text view.
+ *
+ * @param {orion.textview.Ruler} ruler the ruler.
+ */
+ removeRuler: function (ruler) {
+ var rulers = this._rulers;
+ for (var i=0; i
+ * If the action name is a predefined action, the given handler executes before
+ * the default action handler. If the given handler returns true, the
+ * default action handler is not called.
+ *
+ *
+ * @param {String} name the action name.
+ * @param {Function} handler the action handler.
+ *
+ * @see #getActions
+ * @see #invokeAction
+ */
+ setAction: function(name, handler) {
+ if (!name) { return; }
+ var actions = this._actions;
+ for (var i = 0; i < actions.length; i++) {
+ var a = actions[i];
+ if (a.name === name) {
+ a.userHandler = handler;
+ return;
+ }
+ }
+ actions.push({name: name, userHandler: handler});
+ },
+ /**
+ * Associates a key binding with the given action name. Any previous
+ * association with the specified key binding is overwriten. If the
+ * action name is null, the association is removed.
+ *
+ * @param {orion.textview.KeyBinding} keyBinding the key binding
+ * @param {String} name the action
+ */
+ setKeyBinding: function(keyBinding, name) {
+ var keyBindings = this._keyBindings;
+ for (var i = 0; i < keyBindings.length; i++) {
+ var kb = keyBindings[i];
+ if (kb.keyBinding.equals(keyBinding)) {
+ if (name) {
+ kb.name = name;
+ } else {
+ if (kb.predefined) {
+ kb.name = null;
+ } else {
+ var oldName = kb.name;
+ keyBindings.splice(i, 1);
+ var index = 0;
+ while (index < keyBindings.length && oldName !== keyBindings[index].name) {
+ index++;
+ }
+ if (index === keyBindings.length) {
+ /*
+ * Removing all the key bindings associated to an user action will cause
+ * the user action to be removed. TextView predefined actions are never
+ * removed (so they can be reinstalled in the future).
+ *
+ */
+ var actions = this._actions;
+ for (var j = 0; j < actions.length; j++) {
+ if (actions[j].name === oldName) {
+ if (!actions[j].defaultHandler) {
+ actions.splice(j, 1);
+ }
+ }
+ }
+ }
+ }
+ }
+ return;
+ }
+ }
+ if (name) {
+ keyBindings.push({keyBinding: keyBinding, name: name});
+ }
+ },
+ /**
+ * Sets the caret offset relative to the start of the document.
+ *
+ * @param {Number} caret the caret offset relative to the start of the document.
+ * @param {Boolean} [show=true] if true, the view will scroll if needed to show the caret location.
+ *
+ * @see #getCaretOffset
+ * @see #setSelection
+ * @see #getSelection
+ */
+ setCaretOffset: function(offset, show) {
+ var charCount = this._model.getCharCount();
+ offset = Math.max(0, Math.min (offset, charCount));
+ var selection = new Selection(offset, offset, false);
+ this._setSelection (selection, show === undefined || show);
+ },
+ /**
+ * Sets the horizontal pixel.
+ *
+ * The horizontal pixel is the pixel position that is currently at
+ * the left edge of the view. This position is relative to the
+ * beginning of the document.
+ *
+ *
+ * @param {Number} pixel the horizontal pixel.
+ *
+ * @see #getHorizontalPixel
+ * @see #convert
+ */
+ setHorizontalPixel: function(pixel) {
+ if (!this._clientDiv) { return; }
+ pixel = Math.max(0, pixel);
+ this._scrollView(pixel - this._getScroll().x, 0);
+ },
+ /**
+ * Sets whether the view should update the DOM.
+ *
+ * This can be used to improve the performance.
+ *
+ * When the flag is set to true,
+ * the entire view is marked as needing to be redrawn.
+ * Nested calls to this method are stacked.
+ *
+ *
+ * @param {Boolean} redraw the new redraw state
+ *
+ * @see #redraw
+ */
+ setRedraw: function(redraw) {
+ if (redraw) {
+ if (--this._redrawCount === 0) {
+ this.redraw();
+ }
+ } else {
+ this._redrawCount++;
+ }
+ },
+ /**
+ * Sets the text model of the text view.
+ *
+ * @param {orion.textview.TextModel} model the text model of the view.
+ */
+ setModel: function(model) {
+ if (!model) { return; }
+ if (model === this._model) { return; }
+ this._model.removeEventListener("Changing", this._modelListener.onChanging);
+ this._model.removeEventListener("Changed", this._modelListener.onChanged);
+ var oldLineCount = this._model.getLineCount();
+ var oldCharCount = this._model.getCharCount();
+ var newLineCount = model.getLineCount();
+ var newCharCount = model.getCharCount();
+ var newText = model.getText();
+ var e = {
+ type: "ModelChanging",
+ text: newText,
+ start: 0,
+ removedCharCount: oldCharCount,
+ addedCharCount: newCharCount,
+ removedLineCount: oldLineCount,
+ addedLineCount: newLineCount
+ };
+ this.onModelChanging(e);
+ this._model = model;
+ e = {
+ type: "ModelChanged",
+ start: 0,
+ removedCharCount: oldCharCount,
+ addedCharCount: newCharCount,
+ removedLineCount: oldLineCount,
+ addedLineCount: newLineCount
+ };
+ this.onModelChanged(e);
+ this._model.addEventListener("Changing", this._modelListener.onChanging);
+ this._model.addEventListener("Changed", this._modelListener.onChanged);
+ this._reset();
+ this._updatePage();
+ },
+ /**
+ * Sets the view options for the view.
+ *
+ * @param {orion.textview.TextViewOptions} options the view options.
+ *
+ * @see #getOptions
+ */
+ setOptions: function (options) {
+ var defaultOptions = this._defaultOptions();
+ var recreate = false, option, created = this._clientDiv;
+ if (created) {
+ for (option in options) {
+ if (options.hasOwnProperty(option)) {
+ if (defaultOptions[option].recreate) {
+ recreate = true;
+ break;
+ }
+ }
+ }
+ }
+ var changed = false;
+ for (option in options) {
+ if (options.hasOwnProperty(option)) {
+ var newValue = options[option], oldValue = this["_" + option];
+ if (this._compare(oldValue, newValue)) { continue; }
+ changed = true;
+ if (!recreate) {
+ var update = defaultOptions[option].update;
+ if (created && update) {
+ if (update.call(this, newValue)) {
+ recreate = true;
+ }
+ continue;
+ }
+ }
+ this["_" + option] = this._clone(newValue);
+ }
+ }
+ if (changed) {
+ if (recreate) {
+ var oldParent = this._frame.parentNode;
+ oldParent.removeChild(this._frame);
+ this._parent.appendChild(this._frame);
+ }
+ }
+ },
+ /**
+ * Sets the text view selection.
+ *
+ * The selection is defined by a start and end character offset relative to the
+ * document. The character at end offset is not included in the selection.
+ *
+ *
+ * The caret is always placed at the end offset. The start offset can be
+ * greater than the end offset to place the caret at the beginning of the
+ * selection.
+ *
+ *
+ * Clamps out of range offsets.
+ *
+ *
+ * @param {Number} start the start offset of the selection
+ * @param {Number} end the end offset of the selection
+ * @param {Boolean} [show=true] if true, the view will scroll if needed to show the caret location.
+ *
+ * @see #getSelection
+ */
+ setSelection: function (start, end, show) {
+ var caret = start > end;
+ if (caret) {
+ var tmp = start;
+ start = end;
+ end = tmp;
+ }
+ var charCount = this._model.getCharCount();
+ start = Math.max(0, Math.min (start, charCount));
+ end = Math.max(0, Math.min (end, charCount));
+ var selection = new Selection(start, end, caret);
+ this._setSelection(selection, show === undefined || show);
+ },
+ /**
+ * Replaces the text in the given range with the given text.
+ *
+ * The character at the end offset is not replaced.
+ *
+ *
+ * When both start and end parameters
+ * are not specified, the text view places the caret at the beginning
+ * of the document and scrolls to make it visible.
+ *
+ *
+ * @param {String} text the new text.
+ * @param {Number} [start=0] the start offset of text range.
+ * @param {Number} [end=char count] the end offset of text range.
+ *
+ * @see #getText
+ */
+ setText: function (text, start, end) {
+ var reset = start === undefined && end === undefined;
+ if (start === undefined) { start = 0; }
+ if (end === undefined) { end = this._model.getCharCount(); }
+ this._modifyContent({text: text, start: start, end: end, _code: true}, !reset);
+ if (reset) {
+ this._columnX = -1;
+ this._setSelection(new Selection (0, 0, false), true);
+
+ /*
+ * Bug in Firefox. For some reason, the caret does not show after the
+ * view is refreshed. The fix is to toggle the contentEditable state and
+ * force the clientDiv to loose and receive focus if it is focused.
+ */
+ if (isFirefox) {
+ this._fixCaret();
+ }
+ }
+ },
+ /**
+ * Sets the top index.
+ *
+ * The top index is the line that is currently at the top of the text view. This
+ * line may be partially visible depending on the vertical scroll of the view.
+ *
+ *
+ * @param {Number} topIndex the index of the top line.
+ *
+ * @see #getBottomIndex
+ * @see #getTopIndex
+ */
+ setTopIndex: function(topIndex) {
+ if (!this._clientDiv) { return; }
+ var model = this._model;
+ if (model.getCharCount() === 0) {
+ return;
+ }
+ var lineCount = model.getLineCount();
+ var lineHeight = this._getLineHeight();
+ var pageSize = Math.max(1, Math.min(lineCount, Math.floor(this._getClientHeight () / lineHeight)));
+ if (topIndex < 0) {
+ topIndex = 0;
+ } else if (topIndex > lineCount - pageSize) {
+ topIndex = lineCount - pageSize;
+ }
+ var pixel = topIndex * lineHeight - this._getScroll().y;
+ this._scrollView(0, pixel);
+ },
+ /**
+ * Sets the top pixel.
+ *
+ * The top pixel is the pixel position that is currently at
+ * the top edge of the view. This position is relative to the
+ * beginning of the document.
+ *
+ *
+ * @param {Number} pixel the top pixel.
+ *
+ * @see #getBottomPixel
+ * @see #getTopPixel
+ * @see #convert
+ */
+ setTopPixel: function(pixel) {
+ if (!this._clientDiv) { return; }
+ var lineHeight = this._getLineHeight();
+ var clientHeight = this._getClientHeight();
+ var lineCount = this._model.getLineCount();
+ pixel = Math.min(Math.max(0, pixel), lineHeight * lineCount - clientHeight);
+ this._scrollView(0, pixel - this._getScroll().y);
+ },
+ /**
+ * Scrolls the selection into view if needed.
+ *
+ * @returns true if the view was scrolled.
+ *
+ * @see #getSelection
+ * @see #setSelection
+ */
+ showSelection: function() {
+ return this._showCaret(true);
+ },
+
+ /**************************************** Event handlers *********************************/
+ _handleBodyMouseDown: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox && e.which === 1) {
+ this._clientDiv.contentEditable = false;
+ (this._overlayDiv || this._clientDiv).draggable = true;
+ this._ignoreBlur = true;
+ }
+
+ /*
+ * Prevent clicks outside of the view from taking focus
+ * away the view. Note that in Firefox and Opera clicking on the
+ * scrollbar also take focus from the view. Other browsers
+ * do not have this problem and stopping the click over the
+ * scrollbar for them causes mouse capture problems.
+ */
+ var topNode = isOpera || (isFirefox && !this._overlayDiv) ? this._clientDiv : this._overlayDiv || this._viewDiv;
+
+ var temp = e.target ? e.target : e.srcElement;
+ while (temp) {
+ if (topNode === temp) {
+ return;
+ }
+ temp = temp.parentNode;
+ }
+ if (e.preventDefault) { e.preventDefault(); }
+ if (e.stopPropagation){ e.stopPropagation(); }
+ if (!isW3CEvents) {
+ /* In IE 8 is not possible to prevent the default handler from running
+ * during mouse down event using usual API. The workaround is to use
+ * setCapture/releaseCapture.
+ */
+ topNode.setCapture();
+ setTimeout(function() { topNode.releaseCapture(); }, 0);
+ }
+ },
+ _handleBodyMouseUp: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox && e.which === 1) {
+ this._clientDiv.contentEditable = true;
+ (this._overlayDiv || this._clientDiv).draggable = false;
+
+ /*
+ * Bug in Firefox. For some reason, Firefox stops showing the caret
+ * in some cases. For example when the user cancels a drag operation
+ * by pressing ESC. The fix is to detect that the drag operation was
+ * cancelled, toggle the contentEditable state and force the clientDiv
+ * to loose and receive focus if it is focused.
+ */
+ this._fixCaret();
+ this._ignoreBlur = false;
+ }
+ },
+ _handleBlur: function (e) {
+ if (!e) { e = window.event; }
+ if (this._ignoreBlur) { return; }
+ this._hasFocus = false;
+ /*
+ * Bug in IE 8 and earlier. For some reason when text is deselected
+ * the overflow selection at the end of some lines does not get redrawn.
+ * The fix is to create a DOM element in the body to force a redraw.
+ */
+ if (isIE < 9) {
+ if (!this._getSelection().isEmpty()) {
+ var document = this._frameDocument;
+ var child = document.createElement("DIV");
+ var body = document.body;
+ body.appendChild(child);
+ body.removeChild(child);
+ }
+ }
+ if (isFirefox || isIE) {
+ if (this._selDiv1) {
+ var color = isIE ? "transparent" : "#AFAFAF";
+ this._selDiv1.style.background = color;
+ this._selDiv2.style.background = color;
+ this._selDiv3.style.background = color;
+ }
+ }
+ if (!this._ignoreFocus) {
+ this.onBlur({type: "Blur"});
+ }
+ },
+ _handleContextMenu: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox && this._lastMouseButton === 3) {
+ // We need to update the DOM selection, because on
+ // right-click the caret moves to the mouse location.
+ // See bug 366312.
+ var timeDiff = e.timeStamp - this._lastMouseTime;
+ if (timeDiff <= this._clickTime) {
+ this._updateDOMSelection();
+ }
+ }
+ if (this.isListening("ContextMenu")) {
+ var evt = this._createMouseEvent("ContextMenu", e);
+ evt.screenX = e.screenX;
+ evt.screenY = e.screenY;
+ this.onContextMenu(evt);
+ }
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ },
+ _handleCopy: function (e) {
+ if (this._ignoreCopy) { return; }
+ if (!e) { e = window.event; }
+ if (this._doCopy(e)) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleCut: function (e) {
+ if (!e) { e = window.event; }
+ if (this._doCut(e)) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDOMAttrModified: function (e) {
+ if (!e) { e = window.event; }
+ var ancestor = false;
+ var parent = this._parent;
+ while (parent) {
+ if (parent === e.target) {
+ ancestor = true;
+ break;
+ }
+ parent = parent.parentNode;
+ }
+ if (!ancestor) { return; }
+ var state = this._getVisible();
+ if (state === "visible") {
+ this._createView();
+ } else if (state === "hidden") {
+ this._destroyView();
+ }
+ },
+ _handleDataModified: function(e) {
+ this._startIME();
+ },
+ _handleDblclick: function (e) {
+ if (!e) { e = window.event; }
+ var time = e.timeStamp ? e.timeStamp : new Date().getTime();
+ this._lastMouseTime = time;
+ if (this._clickCount !== 2) {
+ this._clickCount = 2;
+ this._handleMouse(e);
+ }
+ },
+ _handleDragStart: function (e) {
+ if (!e) { e = window.event; }
+ if (isFirefox) {
+ var self = this;
+ setTimeout(function() {
+ self._clientDiv.contentEditable = true;
+ self._clientDiv.draggable = false;
+ self._ignoreBlur = false;
+ }, 0);
+ }
+ if (this.isListening("DragStart") && this._dragOffset !== -1) {
+ this._isMouseDown = false;
+ this.onDragStart(this._createMouseEvent("DragStart", e));
+ this._dragOffset = -1;
+ } else {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDrag: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("Drag")) {
+ this.onDrag(this._createMouseEvent("Drag", e));
+ }
+ },
+ _handleDragEnd: function (e) {
+ if (!e) { e = window.event; }
+ this._dropTarget = false;
+ this._dragOffset = -1;
+ if (this.isListening("DragEnd")) {
+ this.onDragEnd(this._createMouseEvent("DragEnd", e));
+ }
+ if (isFirefox) {
+ this._fixCaret();
+ /*
+ * Bug in Firefox. For some reason, Firefox stops showing the caret when the
+ * selection is dropped onto itself. The fix is to detected the case and
+ * call fixCaret() a second time.
+ */
+ if (e.dataTransfer.dropEffect === "none" && !e.dataTransfer.mozUserCancelled) {
+ this._fixCaret();
+ }
+ }
+ },
+ _handleDragEnter: function (e) {
+ if (!e) { e = window.event; }
+ var prevent = true;
+ this._dropTarget = true;
+ if (this.isListening("DragEnter")) {
+ prevent = false;
+ this.onDragEnter(this._createMouseEvent("DragEnter", e));
+ }
+ /*
+ * Webkit will not send drop events if this event is not prevented, as spec in HTML5.
+ * Firefox and IE do not follow this spec for contentEditable. Note that preventing this
+ * event will result is loss of functionality (insertion mark, etc).
+ */
+ if (isWebkit || prevent) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDragOver: function (e) {
+ if (!e) { e = window.event; }
+ var prevent = true;
+ if (this.isListening("DragOver")) {
+ prevent = false;
+ this.onDragOver(this._createMouseEvent("DragOver", e));
+ }
+ /*
+ * Webkit will not send drop events if this event is not prevented, as spec in HTML5.
+ * Firefox and IE do not follow this spec for contentEditable. Note that preventing this
+ * event will result is loss of functionality (insertion mark, etc).
+ */
+ if (isWebkit || prevent) {
+ if (prevent) { e.dataTransfer.dropEffect = "none"; }
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleDragLeave: function (e) {
+ if (!e) { e = window.event; }
+ this._dropTarget = false;
+ if (this.isListening("DragLeave")) {
+ this.onDragLeave(this._createMouseEvent("DragLeave", e));
+ }
+ },
+ _handleDrop: function (e) {
+ if (!e) { e = window.event; }
+ this._dropTarget = false;
+ if (this.isListening("Drop")) {
+ this.onDrop(this._createMouseEvent("Drop", e));
+ }
+ /*
+ * This event must be prevented otherwise the user agent will modify
+ * the DOM. Note that preventing the event on some user agents (i.e. IE)
+ * indicates that the operation is cancelled. This causes the dropEffect to
+ * be set to none in the dragend event causing the implementor to not execute
+ * the code responsible by the move effect.
+ */
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ },
+ _handleDocFocus: function (e) {
+ if (!e) { e = window.event; }
+ this._clientDiv.focus();
+ },
+ _handleFocus: function (e) {
+ if (!e) { e = window.event; }
+ this._hasFocus = true;
+ /*
+ * Feature in IE. The selection is not restored when the
+ * view gets focus and the caret is always placed at the
+ * beginning of the document. The fix is to update the DOM
+ * selection during the focus event.
+ */
+ if (isIE) {
+ this._updateDOMSelection();
+ }
+ if (isFirefox || isIE) {
+ if (this._selDiv1) {
+ var color = this._hightlightRGB;
+ this._selDiv1.style.background = color;
+ this._selDiv2.style.background = color;
+ this._selDiv3.style.background = color;
+ }
+ }
+ if (!this._ignoreFocus) {
+ this.onFocus({type: "Focus"});
+ }
+ },
+ _handleKeyDown: function (e) {
+ if (!e) { e = window.event; }
+ if (isPad) {
+ if (e.keyCode === 8) {
+ this._doBackspace({});
+ e.preventDefault();
+ }
+ return;
+ }
+ switch (e.keyCode) {
+ case 16: /* Shift */
+ case 17: /* Control */
+ case 18: /* Alt */
+ case 91: /* Command */
+ break;
+ default:
+ this._setLinksVisible(false);
+ }
+ if (e.keyCode === 229) {
+ if (this._readonly) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ var startIME = true;
+
+ /*
+ * Bug in Safari. Some Control+key combinations send key events
+ * with keyCode equals to 229. This is unexpected and causes the
+ * view to start an IME composition. The fix is to ignore these
+ * events.
+ */
+ if (isSafari && isMac) {
+ if (e.ctrlKey) {
+ startIME = false;
+ }
+ }
+ if (startIME) {
+ this._startIME();
+ }
+ } else {
+ this._commitIME();
+ }
+ /*
+ * Feature in Firefox. When a key is held down the browser sends
+ * right number of keypress events but only one keydown. This is
+ * unexpected and causes the view to only execute an action
+ * just one time. The fix is to ignore the keydown event and
+ * execute the actions from the keypress handler.
+ * Note: This only happens on the Mac and Linux (Firefox 3.6).
+ *
+ * Feature in Opera. Opera sends keypress events even for non-printable
+ * keys. The fix is to handle actions in keypress instead of keydown.
+ */
+ if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
+ this._keyDownEvent = e;
+ return true;
+ }
+
+ if (this._doAction(e)) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ } else {
+ e.cancelBubble = true;
+ e.returnValue = false;
+ e.keyCode = 0;
+ }
+ return false;
+ }
+ },
+ _handleKeyPress: function (e) {
+ if (!e) { e = window.event; }
+ /*
+ * Feature in Embedded WebKit. Embedded WekKit on Mac runs in compatibility mode and
+ * generates key press events for these Unicode values (Function keys). This does not
+ * happen in Safari or Chrome. The fix is to ignore these key events.
+ */
+ if (isMac && isWebkit) {
+ if ((0xF700 <= e.keyCode && e.keyCode <= 0xF7FF) || e.keyCode === 13 || e.keyCode === 8) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ }
+ if (((isMac || isLinux) && isFirefox < 4) || isOpera) {
+ if (this._doAction(this._keyDownEvent)) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ }
+ var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
+ if (e.charCode !== undefined) {
+ if (ctrlKey) {
+ switch (e.charCode) {
+ /*
+ * In Firefox and Safari if ctrl+v, ctrl+c ctrl+x is canceled
+ * the clipboard events are not sent. The fix to allow
+ * the browser to handles these key events.
+ */
+ case 99://c
+ case 118://v
+ case 120://x
+ return true;
+ }
+ }
+ }
+ var ignore = false;
+ if (isMac) {
+ if (e.ctrlKey || e.metaKey) { ignore = true; }
+ } else {
+ if (isFirefox) {
+ //Firefox clears the state mask when ALT GR generates input
+ if (e.ctrlKey || e.altKey) { ignore = true; }
+ } else {
+ //IE and Chrome only send ALT GR when input is generated
+ if (e.ctrlKey ^ e.altKey) { ignore = true; }
+ }
+ }
+ if (!ignore) {
+ var key = isOpera ? e.which : (e.charCode !== undefined ? e.charCode : e.keyCode);
+ if (key > 31) {
+ this._doContent(String.fromCharCode (key));
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ }
+ },
+ _handleKeyUp: function (e) {
+ if (!e) { e = window.event; }
+ var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
+ if (!ctrlKey) {
+ this._setLinksVisible(false);
+ }
+ // don't commit for space (it happens during JP composition)
+ if (e.keyCode === 13) {
+ this._commitIME();
+ }
+ },
+ _handleLinkClick: function (e) {
+ if (!e) { e = window.event; }
+ var ctrlKey = isMac ? e.metaKey : e.ctrlKey;
+ if (!ctrlKey) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleLoad: function (e) {
+ var state = this._getVisible();
+ if (state === "visible" || (state === "hidden" && isWebkit)) {
+ this._createView();
+ }
+ },
+ _handleMouse: function (e) {
+ var result = true;
+ var target = this._frameWindow;
+ if (isIE || (isFirefox && !this._overlayDiv)) { target = this._clientDiv; }
+ if (this._overlayDiv) {
+ if (this._hasFocus) {
+ this._ignoreFocus = true;
+ }
+ var self = this;
+ setTimeout(function () {
+ self.focus();
+ self._ignoreFocus = false;
+ }, 0);
+ }
+ if (this._clickCount === 1) {
+ result = this._setSelectionTo(e.clientX, e.clientY, e.shiftKey, !isOpera && this.isListening("DragStart"));
+ if (result) { this._setGrab(target); }
+ } else {
+ /*
+ * Feature in IE8 and older, the sequence of events in the IE8 event model
+ * for a doule-click is:
+ *
+ * down
+ * up
+ * up
+ * dblclick
+ *
+ * Given that the mouse down/up events are not balanced, it is not possible to
+ * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
+ * mouse down and ungrab on mouse move when the button 1 is not set.
+ */
+ if (isW3CEvents) { this._setGrab(target); }
+
+ this._doubleClickSelection = null;
+ this._setSelectionTo(e.clientX, e.clientY, e.shiftKey);
+ this._doubleClickSelection = this._getSelection();
+ }
+ return result;
+ },
+ _handleMouseDown: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseDown")) {
+ this.onMouseDown(this._createMouseEvent("MouseDown", e));
+ }
+ if (this._linksVisible) {
+ var target = e.target || e.srcElement;
+ if (target.tagName !== "A") {
+ this._setLinksVisible(false);
+ } else {
+ return;
+ }
+ }
+ this._commitIME();
+
+ var button = e.which; // 1 - left, 2 - middle, 3 - right
+ if (!button) {
+ // if IE 8 or older
+ if (e.button === 4) { button = 2; }
+ if (e.button === 2) { button = 3; }
+ if (e.button === 1) { button = 1; }
+ }
+
+ // For middle click we always need getTime(). See _getClipboardText().
+ var time = button !== 2 && e.timeStamp ? e.timeStamp : new Date().getTime();
+ var timeDiff = time - this._lastMouseTime;
+ var deltaX = Math.abs(this._lastMouseX - e.clientX);
+ var deltaY = Math.abs(this._lastMouseY - e.clientY);
+ var sameButton = this._lastMouseButton === button;
+ this._lastMouseX = e.clientX;
+ this._lastMouseY = e.clientY;
+ this._lastMouseTime = time;
+ this._lastMouseButton = button;
+
+ if (button === 1) {
+ this._isMouseDown = true;
+ if (sameButton && timeDiff <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) {
+ this._clickCount++;
+ } else {
+ this._clickCount = 1;
+ }
+ if (this._handleMouse(e) && (isOpera || isChrome || (isFirefox && !this._overlayDiv))) {
+ if (!this._hasFocus) {
+ this.focus();
+ }
+ e.preventDefault();
+ }
+ }
+ },
+ _handleMouseOver: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseOver")) {
+ this.onMouseOver(this._createMouseEvent("MouseOver", e));
+ }
+ },
+ _handleMouseOut: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseOut")) {
+ this.onMouseOut(this._createMouseEvent("MouseOut", e));
+ }
+ },
+ _handleMouseMove: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseMove")) {
+ var topNode = this._overlayDiv || this._clientDiv;
+ var temp = e.target ? e.target : e.srcElement;
+ while (temp) {
+ if (topNode === temp) {
+ this.onMouseMove(this._createMouseEvent("MouseMove", e));
+ break;
+ }
+ temp = temp.parentNode;
+ }
+ }
+ if (this._dropTarget) {
+ return;
+ }
+ /*
+ * Bug in IE9. IE sends one mouse event when the user changes the text by
+ * pasting or undo. These operations usually happen with the Ctrl key
+ * down which causes the view to enter link mode. Link mode does not end
+ * because there are no further events. The fix is to only enter link
+ * mode when the coordinates of the mouse move event have changed.
+ */
+ var changed = this._linksVisible || this._lastMouseMoveX !== e.clientX || this._lastMouseMoveY !== e.clientY;
+ this._lastMouseMoveX = e.clientX;
+ this._lastMouseMoveY = e.clientY;
+ this._setLinksVisible(changed && !this._isMouseDown && (isMac ? e.metaKey : e.ctrlKey));
+
+ /*
+ * Feature in IE8 and older, the sequence of events in the IE8 event model
+ * for a doule-click is:
+ *
+ * down
+ * up
+ * up
+ * dblclick
+ *
+ * Given that the mouse down/up events are not balanced, it is not possible to
+ * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
+ * mouse down and ungrab on mouse move when the button 1 is not set.
+ *
+ * In order to detect double-click and drag gestures, it is necessary to send
+ * a mouse down event from mouse move when the button is still down and isMouseDown
+ * flag is not set.
+ */
+ if (!isW3CEvents) {
+ if (e.button === 0) {
+ this._setGrab(null);
+ return true;
+ }
+ if (!this._isMouseDown && e.button === 1 && (this._clickCount & 1) !== 0) {
+ this._clickCount = 2;
+ return this._handleMouse(e, this._clickCount);
+ }
+ }
+ if (!this._isMouseDown || this._dragOffset !== -1) {
+ return;
+ }
+
+ var x = e.clientX;
+ var y = e.clientY;
+ if (isChrome) {
+ if (e.currentTarget !== this._frameWindow) {
+ var rect = this._frame.getBoundingClientRect();
+ x -= rect.left;
+ y -= rect.top;
+ }
+ }
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var width = this._getClientWidth (), height = this._getClientHeight();
+ var leftEdge = viewRect.left + viewPad.left;
+ var topEdge = viewRect.top + viewPad.top;
+ var rightEdge = viewRect.left + viewPad.left + width;
+ var bottomEdge = viewRect.top + viewPad.top + height;
+ var model = this._model;
+ var caretLine = model.getLineAtOffset(this._getSelection().getCaret());
+ if (y < topEdge && caretLine !== 0) {
+ this._doAutoScroll("up", x, y - topEdge);
+ } else if (y > bottomEdge && caretLine !== model.getLineCount() - 1) {
+ this._doAutoScroll("down", x, y - bottomEdge);
+ } else if (x < leftEdge) {
+ this._doAutoScroll("left", x - leftEdge, y);
+ } else if (x > rightEdge) {
+ this._doAutoScroll("right", x - rightEdge, y);
+ } else {
+ this._endAutoScroll();
+ this._setSelectionTo(x, y, true);
+ /*
+ * Feature in IE. IE does redraw the selection background right
+ * away after the selection changes because of mouse move events.
+ * The fix is to call getBoundingClientRect() on the
+ * body element to force the selection to be redraw. Some how
+ * calling this method forces a redraw.
+ */
+ if (isIE) {
+ var body = this._frameDocument.body;
+ body.getBoundingClientRect();
+ }
+ }
+ },
+ _createMouseEvent: function(type, e) {
+ var scroll = this._getScroll();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var x = e.clientX + scroll.x - viewRect.left - viewPad.left;
+ var y = e.clientY + scroll.y - viewRect.top;
+ return {
+ type: type,
+ event: e,
+ x: x,
+ y: y
+ };
+ },
+ _handleMouseUp: function (e) {
+ if (!e) { e = window.event; }
+ if (this.isListening("MouseUp")) {
+ this.onMouseUp(this._createMouseEvent("MouseUp", e));
+ }
+ if (this._linksVisible) {
+ return;
+ }
+ var left = e.which ? e.button === 0 : e.button === 1;
+ if (left) {
+ if (this._dragOffset !== -1) {
+ var selection = this._getSelection();
+ selection.extend(this._dragOffset);
+ selection.collapse();
+ this._setSelection(selection, true, true);
+ this._dragOffset = -1;
+ }
+ this._isMouseDown = false;
+ this._endAutoScroll();
+
+ /*
+ * Feature in IE8 and older, the sequence of events in the IE8 event model
+ * for a doule-click is:
+ *
+ * down
+ * up
+ * up
+ * dblclick
+ *
+ * Given that the mouse down/up events are not balanced, it is not possible to
+ * grab on mouse down and ungrab on mouse up. The fix is to grab on the first
+ * mouse down and ungrab on mouse move when the button 1 is not set.
+ */
+ if (isW3CEvents) { this._setGrab(null); }
+
+ /*
+ * Note that there cases when Firefox sets the DOM selection in mouse up.
+ * This happens for example after a cancelled drag operation.
+ *
+ * Note that on Chrome and IE, the caret stops blicking if mouse up is
+ * prevented.
+ */
+ if (isFirefox) {
+ e.preventDefault();
+ }
+ }
+ },
+ _handleMouseWheel: function (e) {
+ if (!e) { e = window.event; }
+ var lineHeight = this._getLineHeight();
+ var pixelX = 0, pixelY = 0;
+ // Note: On the Mac the correct behaviour is to scroll by pixel.
+ if (isFirefox) {
+ var pixel;
+ if (isMac) {
+ pixel = e.detail * 3;
+ } else {
+ var limit = 256;
+ pixel = Math.max(-limit, Math.min(limit, e.detail)) * lineHeight;
+ }
+ if (e.axis === e.HORIZONTAL_AXIS) {
+ pixelX = pixel;
+ } else {
+ pixelY = pixel;
+ }
+ } else {
+ //Webkit
+ if (isMac) {
+ /*
+ * In Safari, the wheel delta is a multiple of 120. In order to
+ * convert delta to pixel values, it is necessary to divide delta
+ * by 40.
+ *
+ * In Chrome and Safari 5, the wheel delta depends on the type of the
+ * mouse. In general, it is the pixel value for Mac mice and track pads,
+ * but it is a multiple of 120 for other mice. There is no presise
+ * way to determine if it is pixel value or a multiple of 120.
+ *
+ * Note that the current approach does not calculate the correct
+ * pixel value for Mac mice when the delta is a multiple of 120.
+ */
+ var denominatorX = 40, denominatorY = 40;
+ if (e.wheelDeltaX % 120 !== 0) { denominatorX = 1; }
+ if (e.wheelDeltaY % 120 !== 0) { denominatorY = 1; }
+ pixelX = -e.wheelDeltaX / denominatorX;
+ if (-1 < pixelX && pixelX < 0) { pixelX = -1; }
+ if (0 < pixelX && pixelX < 1) { pixelX = 1; }
+ pixelY = -e.wheelDeltaY / denominatorY;
+ if (-1 < pixelY && pixelY < 0) { pixelY = -1; }
+ if (0 < pixelY && pixelY < 1) { pixelY = 1; }
+ } else {
+ pixelX = -e.wheelDeltaX;
+ var linesToScroll = 8;
+ pixelY = (-e.wheelDeltaY / 120 * linesToScroll) * lineHeight;
+ }
+ }
+ /*
+ * Feature in Safari. If the event target is removed from the DOM
+ * safari stops smooth scrolling. The fix is keep the element target
+ * in the DOM and remove it on a later time.
+ *
+ * Note: Using a timer is not a solution, because the timeout needs to
+ * be at least as long as the gesture (which is too long).
+ */
+ if (isSafari) {
+ var lineDiv = e.target;
+ while (lineDiv && lineDiv.lineIndex === undefined) {
+ lineDiv = lineDiv.parentNode;
+ }
+ this._mouseWheelLine = lineDiv;
+ }
+ var oldScroll = this._getScroll();
+ this._scrollView(pixelX, pixelY);
+ var newScroll = this._getScroll();
+ if (isSafari) { this._mouseWheelLine = null; }
+ if (oldScroll.x !== newScroll.x || oldScroll.y !== newScroll.y) {
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handlePaste: function (e) {
+ if (this._ignorePaste) { return; }
+ if (!e) { e = window.event; }
+ if (this._doPaste(e)) {
+ if (isIE) {
+ /*
+ * Bug in IE,
+ */
+ var self = this;
+ this._ignoreFocus = true;
+ setTimeout(function() {
+ self._updateDOMSelection();
+ this._ignoreFocus = false;
+ }, 0);
+ }
+ if (e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleResize: function (e) {
+ if (!e) { e = window.event; }
+ var element = this._frameDocument.documentElement;
+ var newWidth = element.clientWidth;
+ var newHeight = element.clientHeight;
+ if (this._frameWidth !== newWidth || this._frameHeight !== newHeight) {
+ this._frameWidth = newWidth;
+ this._frameHeight = newHeight;
+ /*
+ * Feature in IE7. For some reason, sometimes Internet Explorer 7
+ * returns incorrect values for element.getBoundingClientRect() when
+ * inside a resize handler. The fix is to queue the work.
+ */
+ if (isIE < 9) {
+ this._queueUpdatePage();
+ } else {
+ this._updatePage();
+ }
+ }
+ },
+ _handleRulerEvent: function (e) {
+ if (!e) { e = window.event; }
+ var target = e.target ? e.target : e.srcElement;
+ var lineIndex = target.lineIndex;
+ var element = target;
+ while (element && !element._ruler) {
+ if (lineIndex === undefined && element.lineIndex !== undefined) {
+ lineIndex = element.lineIndex;
+ }
+ element = element.parentNode;
+ }
+ var ruler = element ? element._ruler : null;
+ if (lineIndex === undefined && ruler && ruler.getOverview() === "document") {
+ var buttonHeight = isPad ? 0 : 17;
+ var clientHeight = this._getClientHeight ();
+ var lineCount = this._model.getLineCount ();
+ var viewPad = this._getViewPadding();
+ var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
+ lineIndex = Math.floor((e.clientY - buttonHeight) * lineCount / trackHeight);
+ if (!(0 <= lineIndex && lineIndex < lineCount)) {
+ lineIndex = undefined;
+ }
+ }
+ if (ruler) {
+ switch (e.type) {
+ case "click":
+ if (ruler.onClick) { ruler.onClick(lineIndex, e); }
+ break;
+ case "dblclick":
+ if (ruler.onDblClick) { ruler.onDblClick(lineIndex, e); }
+ break;
+ case "mousemove":
+ if (ruler.onMouseMove) { ruler.onMouseMove(lineIndex, e); }
+ break;
+ case "mouseover":
+ if (ruler.onMouseOver) { ruler.onMouseOver(lineIndex, e); }
+ break;
+ case "mouseout":
+ if (ruler.onMouseOut) { ruler.onMouseOut(lineIndex, e); }
+ break;
+ }
+ }
+ },
+ _handleScroll: function () {
+ var scroll = this._getScroll();
+ var oldX = this._hScroll;
+ var oldY = this._vScroll;
+ if (oldX !== scroll.x || oldY !== scroll.y) {
+ this._hScroll = scroll.x;
+ this._vScroll = scroll.y;
+ this._commitIME();
+ this._updatePage(oldY === scroll.y);
+ var e = {
+ type: "Scroll",
+ oldValue: {x: oldX, y: oldY},
+ newValue: scroll
+ };
+ this.onScroll(e);
+ }
+ },
+ _handleSelectStart: function (e) {
+ if (!e) { e = window.event; }
+ if (this._ignoreSelect) {
+ if (e && e.preventDefault) { e.preventDefault(); }
+ return false;
+ }
+ },
+ _handleUnload: function (e) {
+ if (!e) { e = window.event; }
+ this._destroyView();
+ },
+ _handleInput: function (e) {
+ var textArea = this._textArea;
+ this._doContent(textArea.value);
+ textArea.selectionStart = textArea.selectionEnd = 0;
+ textArea.value = "";
+ e.preventDefault();
+ },
+ _handleTextInput: function (e) {
+ this._doContent(e.data);
+ e.preventDefault();
+ },
+ _touchConvert: function (touch) {
+ var rect = this._frame.getBoundingClientRect();
+ var body = this._parentDocument.body;
+ return {left: touch.clientX - rect.left - body.scrollLeft, top: touch.clientY - rect.top - body.scrollTop};
+ },
+ _handleTextAreaClick: function (e) {
+ var pt = this._touchConvert(e);
+ this._clickCount = 1;
+ this._ignoreDOMSelection = false;
+ this._setSelectionTo(pt.left, pt.top, false);
+ var textArea = this._textArea;
+ textArea.focus();
+ },
+ _handleTouchStart: function (e) {
+ var touches = e.touches, touch, pt, sel;
+ this._touchMoved = false;
+ this._touchStartScroll = undefined;
+ if (touches.length === 1) {
+ touch = touches[0];
+ var pageX = touch.pageX;
+ var pageY = touch.pageY;
+ this._touchStartX = pageX;
+ this._touchStartY = pageY;
+ this._touchStartTime = e.timeStamp;
+ this._touchStartScroll = this._getScroll();
+ sel = this._getSelection();
+ pt = this._touchConvert(touches[0]);
+ this._touchGesture = "none";
+ if (!sel.isEmpty()) {
+ if (this._hitOffset(sel.end, pt.left, pt.top)) {
+ this._touchGesture = "extendEnd";
+ } else if (this._hitOffset(sel.start, pt.left, pt.top)) {
+ this._touchGesture = "extendStart";
+ }
+ }
+ if (this._touchGesture === "none") {
+ var textArea = this._textArea;
+ textArea.value = "";
+ textArea.style.left = "-1000px";
+ textArea.style.top = "-1000px";
+ textArea.style.width = "3000px";
+ textArea.style.height = "3000px";
+ }
+ } else if (touches.length === 2) {
+ this._touchGesture = "select";
+ if (this._touchTimeout) {
+ clearTimeout(this._touchTimeout);
+ this._touchTimeout = null;
+ }
+ pt = this._touchConvert(touches[0]);
+ var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ pt = this._touchConvert(touches[1]);
+ var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ sel = this._getSelection();
+ sel.setCaret(offset1);
+ sel.extend(offset2);
+ this._setSelection(sel, true, true);
+ }
+ //Cannot prevent to show magnifier
+// e.preventDefault();
+ },
+ _handleTouchMove: function (e) {
+ this._touchMoved = true;
+ var touches = e.touches, pt, sel;
+ if (touches.length === 1) {
+ var touch = touches[0];
+ var pageX = touch.pageX;
+ var pageY = touch.pageY;
+ var deltaX = this._touchStartX - pageX;
+ var deltaY = this._touchStartY - pageY;
+ pt = this._touchConvert(touch);
+ sel = this._getSelection();
+ if (this._touchGesture === "none") {
+ if ((e.timeStamp - this._touchStartTime) < 200 && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) {
+ this._touchGesture = "scroll";
+ } else {
+ this._touchGesture = "caret";
+ }
+ }
+ if (this._touchGesture === "select") {
+ if (this._hitOffset(sel.end, pt.left, pt.top)) {
+ this._touchGesture = "extendEnd";
+ } else if (this._hitOffset(sel.start, pt.left, pt.top)) {
+ this._touchGesture = "extendStart";
+ } else {
+ this._touchGesture = "caret";
+ }
+ }
+ switch (this._touchGesture) {
+ case "scroll":
+ this._touchStartX = pageX;
+ this._touchStartY = pageY;
+ this._scrollView(deltaX, deltaY);
+ break;
+ case "extendStart":
+ case "extendEnd":
+ this._clickCount = 1;
+ var lineIndex = this._getYToLine(pt.top);
+ var offset = this._getXToOffset(lineIndex, pt.left);
+ sel.setCaret(this._touchGesture === "extendStart" ? sel.end : sel.start);
+ sel.extend(offset);
+ if (offset >= sel.end && this._touchGesture === "extendStart") {
+ this._touchGesture = "extendEnd";
+ }
+ if (offset <= sel.start && this._touchGesture === "extendEnd") {
+ this._touchGesture = "extendStart";
+ }
+ this._setSelection(sel, true, true);
+ break;
+ case "caret":
+ this._setSelectionTo(pt.left, pt.top, false);
+ break;
+ }
+ } else if (touches.length === 2) {
+ pt = this._touchConvert(touches[0]);
+ var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ pt = this._touchConvert(touches[1]);
+ var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left);
+ sel = this._getSelection();
+ sel.setCaret(offset1);
+ sel.extend(offset2);
+ this._setSelection(sel, true, true);
+ }
+ e.preventDefault();
+ },
+ _handleTouchEnd: function (e) {
+ var self = this;
+ if (!this._touchMoved) {
+ if (e.touches.length === 0 && e.changedTouches.length === 1) {
+ var touch = e.changedTouches[0];
+ var pt = this._touchConvert(touch);
+ var textArea = this._textArea;
+ textArea.value = "";
+ textArea.style.left = "-1000px";
+ textArea.style.top = "-1000px";
+ textArea.style.width = "3000px";
+ textArea.style.height = "3000px";
+ setTimeout(function() {
+ self._clickCount = 1;
+ self._ignoreDOMSelection = false;
+ self._setSelectionTo(pt.left, pt.top, false);
+ }, 300);
+ }
+ }
+ if (e.touches.length === 0) {
+ setTimeout(function() {
+ var selection = self._getSelection();
+ var text = self._model.getText(selection.start, selection.end);
+ var textArea = self._textArea;
+ textArea.value = text;
+ textArea.selectionStart = 0;
+ textArea.selectionEnd = text.length;
+ if (!selection.isEmpty()) {
+ var touchRect = self._touchDiv.getBoundingClientRect();
+ var bounds = self._getOffsetBounds(selection.start);
+ textArea.style.left = (touchRect.width / 2) + "px";
+ textArea.style.top = ((bounds.top > 40 ? bounds.top - 30 : bounds.top + 30)) + "px";
+ }
+ }, 0);
+ }
+// e.preventDefault();
+ },
+
+ /************************************ Actions ******************************************/
+ _doAction: function (e) {
+ var keyBindings = this._keyBindings;
+ for (var i = 0; i < keyBindings.length; i++) {
+ var kb = keyBindings[i];
+ if (kb.keyBinding.match(e)) {
+ if (kb.name) {
+ var actions = this._actions;
+ for (var j = 0; j < actions.length; j++) {
+ var a = actions[j];
+ if (a.name === kb.name) {
+ if (a.userHandler) {
+ if (!a.userHandler()) {
+ if (a.defaultHandler) {
+ a.defaultHandler();
+ } else {
+ return false;
+ }
+ }
+ } else if (a.defaultHandler) {
+ a.defaultHandler();
+ }
+ break;
+ }
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ },
+ _doBackspace: function (args) {
+ var selection = this._getSelection();
+ if (selection.isEmpty()) {
+ var model = this._model;
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ var lineStart = model.getLineStart(lineIndex);
+ if (caret === lineStart) {
+ if (lineIndex > 0) {
+ selection.extend(model.getLineEnd(lineIndex - 1));
+ }
+ } else {
+ var removeTab = false;
+ if (this._expandTab && args.unit === "character" && (caret - lineStart) % this._tabSize === 0) {
+ var lineText = model.getText(lineStart, caret);
+ removeTab = !/[^ ]/.test(lineText); // Only spaces between line start and caret.
+ }
+ if (removeTab) {
+ selection.extend(caret - this._tabSize);
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, -1));
+ }
+ }
+ }
+ this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+ return true;
+ },
+ _doContent: function (text) {
+ var selection = this._getSelection();
+ this._modifyContent({text: text, start: selection.start, end: selection.end, _ignoreDOMSelection: true}, true);
+ },
+ _doCopy: function (e) {
+ var selection = this._getSelection();
+ if (!selection.isEmpty()) {
+ var text = this._getBaseText(selection.start, selection.end);
+ return this._setClipboardText(text, e);
+ }
+ return true;
+ },
+ _doCursorNext: function (args) {
+ if (!args.select) {
+ if (this._clearSelection("next")) { return true; }
+ }
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (caret === model.getLineEnd(lineIndex)) {
+ if (lineIndex + 1 < model.getLineCount()) {
+ selection.extend(model.getLineStart(lineIndex + 1));
+ }
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, 1));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doCursorPrevious: function (args) {
+ if (!args.select) {
+ if (this._clearSelection("previous")) { return true; }
+ }
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (caret === model.getLineStart(lineIndex)) {
+ if (lineIndex > 0) {
+ selection.extend(model.getLineEnd(lineIndex - 1));
+ }
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, -1));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doCut: function (e) {
+ var selection = this._getSelection();
+ if (!selection.isEmpty()) {
+ var text = this._getBaseText(selection.start, selection.end);
+ this._doContent("");
+ return this._setClipboardText(text, e);
+ }
+ return true;
+ },
+ _doDelete: function (args) {
+ var selection = this._getSelection();
+ if (selection.isEmpty()) {
+ var model = this._model;
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (caret === model.getLineEnd (lineIndex)) {
+ if (lineIndex + 1 < model.getLineCount()) {
+ selection.extend(model.getLineStart(lineIndex + 1));
+ }
+ } else {
+ selection.extend(this._getOffset(caret, args.unit, 1));
+ }
+ }
+ this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+ return true;
+ },
+ _doEnd: function (args) {
+ var selection = this._getSelection();
+ var model = this._model;
+ if (args.ctrl) {
+ selection.extend(model.getCharCount());
+ } else {
+ var lineIndex = model.getLineAtOffset(selection.getCaret());
+ selection.extend(model.getLineEnd(lineIndex));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doEnter: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ this._doContent(model.getLineDelimiter());
+ if (args && args.noCursor) {
+ selection.end = selection.start;
+ this._setSelection(selection);
+ }
+ return true;
+ },
+ _doHome: function (args) {
+ var selection = this._getSelection();
+ var model = this._model;
+ if (args.ctrl) {
+ selection.extend(0);
+ } else {
+ var lineIndex = model.getLineAtOffset(selection.getCaret());
+ selection.extend(model.getLineStart(lineIndex));
+ }
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _doLineDown: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (lineIndex + 1 < model.getLineCount()) {
+ var scrollX = this._getScroll().x;
+ var x = this._columnX;
+ if (x === -1 || args.wholeLine || (args.select && isIE)) {
+ var offset = args.wholeLine ? model.getLineEnd(lineIndex + 1) : caret;
+ x = this._getOffsetToX(offset) + scrollX;
+ }
+ selection.extend(this._getXToOffset(lineIndex + 1, x - scrollX));
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true, true);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doLineUp: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ if (lineIndex > 0) {
+ var scrollX = this._getScroll().x;
+ var x = this._columnX;
+ if (x === -1 || args.wholeLine || (args.select && isIE)) {
+ var offset = args.wholeLine ? model.getLineStart(lineIndex - 1) : caret;
+ x = this._getOffsetToX(offset) + scrollX;
+ }
+ selection.extend(this._getXToOffset(lineIndex - 1, x - scrollX));
+ if (!args.select) { selection.collapse(); }
+ this._setSelection(selection, true, true);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doPageDown: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var caretLine = model.getLineAtOffset(caret);
+ var lineCount = model.getLineCount();
+ if (caretLine < lineCount - 1) {
+ var scroll = this._getScroll();
+ var clientHeight = this._getClientHeight();
+ var lineHeight = this._getLineHeight();
+ var lines = Math.floor(clientHeight / lineHeight);
+ var scrollLines = Math.min(lineCount - caretLine - 1, lines);
+ scrollLines = Math.max(1, scrollLines);
+ var x = this._columnX;
+ if (x === -1 || (args.select && isIE)) {
+ x = this._getOffsetToX(caret) + scroll.x;
+ }
+ selection.extend(this._getXToOffset(caretLine + scrollLines, x - scroll.x));
+ if (!args.select) { selection.collapse(); }
+ var verticalMaximum = lineCount * lineHeight;
+ var scrollOffset = scroll.y + scrollLines * lineHeight;
+ if (scrollOffset + clientHeight > verticalMaximum) {
+ scrollOffset = verticalMaximum - clientHeight;
+ }
+ this._setSelection(selection, true, true, scrollOffset - scroll.y);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doPageUp: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ var caret = selection.getCaret();
+ var caretLine = model.getLineAtOffset(caret);
+ if (caretLine > 0) {
+ var scroll = this._getScroll();
+ var clientHeight = this._getClientHeight();
+ var lineHeight = this._getLineHeight();
+ var lines = Math.floor(clientHeight / lineHeight);
+ var scrollLines = Math.max(1, Math.min(caretLine, lines));
+ var x = this._columnX;
+ if (x === -1 || (args.select && isIE)) {
+ x = this._getOffsetToX(caret) + scroll.x;
+ }
+ selection.extend(this._getXToOffset(caretLine - scrollLines, x - scroll.x));
+ if (!args.select) { selection.collapse(); }
+ var scrollOffset = Math.max(0, scroll.y - scrollLines * lineHeight);
+ this._setSelection(selection, true, true, scrollOffset - scroll.y);
+ this._columnX = x;
+ }
+ return true;
+ },
+ _doPaste: function(e) {
+ var self = this;
+ var result = this._getClipboardText(e, function(text) {
+ if (text) {
+ if (isLinux && self._lastMouseButton === 2) {
+ var timeDiff = new Date().getTime() - self._lastMouseTime;
+ if (timeDiff <= self._clickTime) {
+ self._setSelectionTo(self._lastMouseX, self._lastMouseY);
+ }
+ }
+ self._doContent(text);
+ }
+ });
+ return result !== null;
+ },
+ _doScroll: function (args) {
+ var type = args.type;
+ var model = this._model;
+ var lineCount = model.getLineCount();
+ var clientHeight = this._getClientHeight();
+ var lineHeight = this._getLineHeight();
+ var verticalMaximum = lineCount * lineHeight;
+ var verticalScrollOffset = this._getScroll().y;
+ var pixel;
+ switch (type) {
+ case "textStart": pixel = 0; break;
+ case "textEnd": pixel = verticalMaximum - clientHeight; break;
+ case "pageDown": pixel = verticalScrollOffset + clientHeight; break;
+ case "pageUp": pixel = verticalScrollOffset - clientHeight; break;
+ case "centerLine":
+ var selection = this._getSelection();
+ var lineStart = model.getLineAtOffset(selection.start);
+ var lineEnd = model.getLineAtOffset(selection.end);
+ var selectionHeight = (lineEnd - lineStart + 1) * lineHeight;
+ pixel = (lineStart * lineHeight) - (clientHeight / 2) + (selectionHeight / 2);
+ break;
+ }
+ if (pixel !== undefined) {
+ pixel = Math.min(Math.max(0, pixel), verticalMaximum - clientHeight);
+ this._scrollView(0, pixel - verticalScrollOffset);
+ }
+ },
+ _doSelectAll: function (args) {
+ var model = this._model;
+ var selection = this._getSelection();
+ selection.setCaret(0);
+ selection.extend(model.getCharCount());
+ this._setSelection(selection, false);
+ return true;
+ },
+ _doTab: function (args) {
+ var text = "\t";
+ if (this._expandTab) {
+ var model = this._model;
+ var caret = this._getSelection().getCaret();
+ var lineIndex = model.getLineAtOffset(caret);
+ var lineStart = model.getLineStart(lineIndex);
+ var spaces = this._tabSize - ((caret - lineStart) % this._tabSize);
+ text = (new Array(spaces + 1)).join(" ");
+ }
+ this._doContent(text);
+ return true;
+ },
+
+ /************************************ Internals ******************************************/
+ _applyStyle: function(style, node, reset) {
+ if (reset) {
+ var attrs = node.attributes;
+ for (var i= attrs.length; i-->0;) {
+ if (attrs[i].specified) {
+ node.removeAttributeNode(attrs[i]);
+ }
+ }
+ }
+ if (!style) {
+ return;
+ }
+ if (style.styleClass) {
+ node.className = style.styleClass;
+ }
+ var properties = style.style;
+ if (properties) {
+ for (var s in properties) {
+ if (properties.hasOwnProperty(s)) {
+ node.style[s] = properties[s];
+ }
+ }
+ }
+ var attributes = style.attributes;
+ if (attributes) {
+ for (var a in attributes) {
+ if (attributes.hasOwnProperty(a)) {
+ node.setAttribute(a, attributes[a]);
+ }
+ }
+ }
+ },
+ _autoScroll: function () {
+ var selection = this._getSelection();
+ var line;
+ var x = this._autoScrollX;
+ if (this._autoScrollDir === "up" || this._autoScrollDir === "down") {
+ var scroll = this._autoScrollY / this._getLineHeight();
+ scroll = scroll < 0 ? Math.floor(scroll) : Math.ceil(scroll);
+ line = this._model.getLineAtOffset(selection.getCaret());
+ line = Math.max(0, Math.min(this._model.getLineCount() - 1, line + scroll));
+ } else if (this._autoScrollDir === "left" || this._autoScrollDir === "right") {
+ line = this._getYToLine(this._autoScrollY);
+ x += this._getOffsetToX(selection.getCaret());
+ }
+ selection.extend(this._getXToOffset(line, x));
+ this._setSelection(selection, true);
+ },
+ _autoScrollTimer: function () {
+ this._autoScroll();
+ var self = this;
+ this._autoScrollTimerID = setTimeout(function () {self._autoScrollTimer();}, this._AUTO_SCROLL_RATE);
+ },
+ _calculateLineHeight: function() {
+ var parent = this._clientDiv;
+ var document = this._frameDocument;
+ var c = " ";
+ var line = document.createElement("DIV");
+ line.style.position = "fixed";
+ line.style.left = "-1000px";
+ var span1 = document.createElement("SPAN");
+ span1.appendChild(document.createTextNode(c));
+ line.appendChild(span1);
+ var span2 = document.createElement("SPAN");
+ span2.style.fontStyle = "italic";
+ span2.appendChild(document.createTextNode(c));
+ line.appendChild(span2);
+ var span3 = document.createElement("SPAN");
+ span3.style.fontWeight = "bold";
+ span3.appendChild(document.createTextNode(c));
+ line.appendChild(span3);
+ var span4 = document.createElement("SPAN");
+ span4.style.fontWeight = "bold";
+ span4.style.fontStyle = "italic";
+ span4.appendChild(document.createTextNode(c));
+ line.appendChild(span4);
+ parent.appendChild(line);
+ var lineRect = line.getBoundingClientRect();
+ var spanRect1 = span1.getBoundingClientRect();
+ var spanRect2 = span2.getBoundingClientRect();
+ var spanRect3 = span3.getBoundingClientRect();
+ var spanRect4 = span4.getBoundingClientRect();
+ var h1 = spanRect1.bottom - spanRect1.top;
+ var h2 = spanRect2.bottom - spanRect2.top;
+ var h3 = spanRect3.bottom - spanRect3.top;
+ var h4 = spanRect4.bottom - spanRect4.top;
+ var fontStyle = 0;
+ var lineHeight = lineRect.bottom - lineRect.top;
+ if (h2 > h1) {
+ fontStyle = 1;
+ }
+ if (h3 > h2) {
+ fontStyle = 2;
+ }
+ if (h4 > h3) {
+ fontStyle = 3;
+ }
+ var style;
+ if (fontStyle !== 0) {
+ style = {style: {}};
+ if ((fontStyle & 1) !== 0) {
+ style.style.fontStyle = "italic";
+ }
+ if ((fontStyle & 2) !== 0) {
+ style.style.fontWeight = "bold";
+ }
+ }
+ this._largestFontStyle = style;
+ parent.removeChild(line);
+ return lineHeight;
+ },
+ _calculatePadding: function() {
+ var document = this._frameDocument;
+ var parent = this._clientDiv;
+ var pad = this._getPadding(this._viewDiv);
+ var div1 = document.createElement("DIV");
+ div1.style.position = "fixed";
+ div1.style.left = "-1000px";
+ div1.style.paddingLeft = pad.left + "px";
+ div1.style.paddingTop = pad.top + "px";
+ div1.style.paddingRight = pad.right + "px";
+ div1.style.paddingBottom = pad.bottom + "px";
+ div1.style.width = "100px";
+ div1.style.height = "100px";
+ var div2 = document.createElement("DIV");
+ div2.style.width = "100%";
+ div2.style.height = "100%";
+ div1.appendChild(div2);
+ parent.appendChild(div1);
+ var rect1 = div1.getBoundingClientRect();
+ var rect2 = div2.getBoundingClientRect();
+ parent.removeChild(div1);
+ pad = {
+ left: rect2.left - rect1.left,
+ top: rect2.top - rect1.top,
+ right: rect1.right - rect2.right,
+ bottom: rect1.bottom - rect2.bottom
+ };
+ return pad;
+ },
+ _clearSelection: function (direction) {
+ var selection = this._getSelection();
+ if (selection.isEmpty()) { return false; }
+ if (direction === "next") {
+ selection.start = selection.end;
+ } else {
+ selection.end = selection.start;
+ }
+ this._setSelection(selection, true);
+ return true;
+ },
+ _clone: function (obj) {
+ /*Note that this code only works because of the limited types used in TextViewOptions */
+ if (obj instanceof Array) {
+ return obj.slice(0);
+ }
+ return obj;
+ },
+ _compare: function (s1, s2) {
+ if (s1 === s2) { return true; }
+ if (s1 && !s2 || !s1 && s2) { return false; }
+ if ((s1 && s1.constructor === String) || (s2 && s2.constructor === String)) { return false; }
+ if (s1 instanceof Array || s2 instanceof Array) {
+ if (!(s1 instanceof Array && s2 instanceof Array)) { return false; }
+ if (s1.length !== s2.length) { return false; }
+ for (var i = 0; i < s1.length; i++) {
+ if (!this._compare(s1[i], s2[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (!(s1 instanceof Object) || !(s2 instanceof Object)) { return false; }
+ var p;
+ for (p in s1) {
+ if (s1.hasOwnProperty(p)) {
+ if (!s2.hasOwnProperty(p)) { return false; }
+ if (!this._compare(s1[p], s2[p])) {return false; }
+ }
+ }
+ for (p in s2) {
+ if (!s1.hasOwnProperty(p)) { return false; }
+ }
+ return true;
+ },
+ _commitIME: function () {
+ if (this._imeOffset === -1) { return; }
+ // make the state of the IME match the state the view expects it be in
+ // when the view commits the text and IME also need to be committed
+ // this can be accomplished by changing the focus around
+ this._scrollDiv.focus();
+ this._clientDiv.focus();
+
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(this._imeOffset);
+ var lineStart = model.getLineStart(lineIndex);
+ var newText = this._getDOMText(lineIndex);
+ var oldText = model.getLine(lineIndex);
+ var start = this._imeOffset - lineStart;
+ var end = start + newText.length - oldText.length;
+ if (start !== end) {
+ var insertText = newText.substring(start, end);
+ this._doContent(insertText);
+ }
+ this._imeOffset = -1;
+ },
+ _convertDelimiter: function (text, addTextFunc, addDelimiterFunc) {
+ var cr = 0, lf = 0, index = 0, length = text.length;
+ while (index < length) {
+ if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); }
+ if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); }
+ var start = index, end;
+ if (lf === -1 && cr === -1) {
+ addTextFunc(text.substring(index));
+ break;
+ }
+ if (cr !== -1 && lf !== -1) {
+ if (cr + 1 === lf) {
+ end = cr;
+ index = lf + 1;
+ } else {
+ end = cr < lf ? cr : lf;
+ index = (cr < lf ? cr : lf) + 1;
+ }
+ } else if (cr !== -1) {
+ end = cr;
+ index = cr + 1;
+ } else {
+ end = lf;
+ index = lf + 1;
+ }
+ addTextFunc(text.substring(start, end));
+ addDelimiterFunc();
+ }
+ },
+ _createActions: function () {
+ var KeyBinding = mKeyBinding.KeyBinding;
+ //no duplicate keybindings
+ var bindings = this._keyBindings = [];
+
+ // Cursor Navigation
+ bindings.push({name: "lineUp", keyBinding: new KeyBinding(38), predefined: true});
+ bindings.push({name: "lineDown", keyBinding: new KeyBinding(40), predefined: true});
+ bindings.push({name: "charPrevious", keyBinding: new KeyBinding(37), predefined: true});
+ bindings.push({name: "charNext", keyBinding: new KeyBinding(39), predefined: true});
+ if (isMac) {
+ bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(33), predefined: true});
+ bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(34), predefined: true});
+ bindings.push({name: "pageUp", keyBinding: new KeyBinding(33, null, null, true), predefined: true});
+ bindings.push({name: "pageDown", keyBinding: new KeyBinding(34, null, null, true), predefined: true});
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, true), predefined: true});
+ bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, null, null, true), predefined: true});
+ bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, null, null, true), predefined: true});
+ bindings.push({name: "scrollTextStart", keyBinding: new KeyBinding(36), predefined: true});
+ bindings.push({name: "scrollTextEnd", keyBinding: new KeyBinding(35), predefined: true});
+ bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true});
+ bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true});
+ bindings.push({name: "scrollPageUp", keyBinding: new KeyBinding(38, null, null, null, true), predefined: true});
+ bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding(40, null, null, null, true), predefined: true});
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, null, null, null, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, null, null, null, true), predefined: true});
+ //TODO These two actions should be changed to paragraph start and paragraph end when word wrap is implemented
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(38, null, null, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(40, null, null, true), predefined: true});
+ } else {
+ bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true});
+ bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true});
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true});
+ bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true});
+ bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true});
+ bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true});
+ bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true});
+ }
+ if (isFirefox && isLinux) {
+ bindings.push({name: "lineUp", keyBinding: new KeyBinding(38, true), predefined: true});
+ bindings.push({name: "lineDown", keyBinding: new KeyBinding(40, true), predefined: true});
+ }
+
+ // Select Cursor Navigation
+ bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true});
+ bindings.push({name: "selectLineDown", keyBinding: new KeyBinding(40, null, true), predefined: true});
+ bindings.push({name: "selectCharPrevious", keyBinding: new KeyBinding(37, null, true), predefined: true});
+ bindings.push({name: "selectCharNext", keyBinding: new KeyBinding(39, null, true), predefined: true});
+ bindings.push({name: "selectPageUp", keyBinding: new KeyBinding(33, null, true), predefined: true});
+ bindings.push({name: "selectPageDown", keyBinding: new KeyBinding(34, null, true), predefined: true});
+ if (isMac) {
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, true, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true});
+ bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true});
+ bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true});
+ bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
+ bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
+ bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true});
+ bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true});
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, null, true, null, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, null, true, null, true), predefined: true});
+ //TODO These two actions should be changed to select paragraph start and select paragraph end when word wrap is implemented
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(38, null, true, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(40, null, true, true), predefined: true});
+ } else {
+ if (isLinux) {
+ bindings.push({name: "selectWholeLineUp", keyBinding: new KeyBinding(38, true, true), predefined: true});
+ bindings.push({name: "selectWholeLineDown", keyBinding: new KeyBinding(40, true, true), predefined: true});
+ }
+ bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true});
+ bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true});
+ bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true});
+ bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true});
+ bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true});
+ bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true});
+ }
+
+ //Misc
+ bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8), predefined: true});
+ bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8, null, true), predefined: true});
+ bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true, true), predefined: true});
+ bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, true), predefined: true});
+ bindings.push({name: "tab", keyBinding: new KeyBinding(9), predefined: true});
+ bindings.push({name: "enter", keyBinding: new KeyBinding(13), predefined: true});
+ bindings.push({name: "enter", keyBinding: new KeyBinding(13, null, true), predefined: true});
+ bindings.push({name: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true});
+ if (isMac) {
+ bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true});
+ bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, null, null, true), predefined: true});
+ }
+
+ /*
+ * Feature in IE/Chrome: prevent ctrl+'u', ctrl+'i', and ctrl+'b' from applying styles to the text.
+ *
+ * Note that Chrome applies the styles on the Mac with Ctrl instead of Cmd.
+ */
+ if (!isFirefox) {
+ var isMacChrome = isMac && isChrome;
+ bindings.push({name: null, keyBinding: new KeyBinding('u', !isMacChrome, false, false, isMacChrome), predefined: true});
+ bindings.push({name: null, keyBinding: new KeyBinding('i', !isMacChrome, false, false, isMacChrome), predefined: true});
+ bindings.push({name: null, keyBinding: new KeyBinding('b', !isMacChrome, false, false, isMacChrome), predefined: true});
+ }
+
+ if (isFirefox) {
+ bindings.push({name: "copy", keyBinding: new KeyBinding(45, true), predefined: true});
+ bindings.push({name: "paste", keyBinding: new KeyBinding(45, null, true), predefined: true});
+ bindings.push({name: "cut", keyBinding: new KeyBinding(46, null, true), predefined: true});
+ }
+
+ // Add the emacs Control+ ... key bindings.
+ if (isMac) {
+ bindings.push({name: "lineStart", keyBinding: new KeyBinding("a", false, false, false, true), predefined: true});
+ bindings.push({name: "lineEnd", keyBinding: new KeyBinding("e", false, false, false, true), predefined: true});
+ bindings.push({name: "lineUp", keyBinding: new KeyBinding("p", false, false, false, true), predefined: true});
+ bindings.push({name: "lineDown", keyBinding: new KeyBinding("n", false, false, false, true), predefined: true});
+ bindings.push({name: "charPrevious", keyBinding: new KeyBinding("b", false, false, false, true), predefined: true});
+ bindings.push({name: "charNext", keyBinding: new KeyBinding("f", false, false, false, true), predefined: true});
+ bindings.push({name: "deletePrevious", keyBinding: new KeyBinding("h", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteNext", keyBinding: new KeyBinding("d", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteLineEnd", keyBinding: new KeyBinding("k", false, false, false, true), predefined: true});
+ if (isFirefox) {
+ bindings.push({name: "scrollPageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteLineStart", keyBinding: new KeyBinding("u", false, false, false, true), predefined: true});
+ bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding("w", false, false, false, true), predefined: true});
+ } else {
+ bindings.push({name: "pageDown", keyBinding: new KeyBinding("v", false, false, false, true), predefined: true});
+ bindings.push({name: "centerLine", keyBinding: new KeyBinding("l", false, false, false, true), predefined: true});
+ bindings.push({name: "enterNoCursor", keyBinding: new KeyBinding("o", false, false, false, true), predefined: true});
+ //TODO implement: y (yank), t (transpose)
+ }
+ }
+
+ //1 to 1, no duplicates
+ var self = this;
+ this._actions = [
+ {name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}},
+ {name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}},
+ {name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}},
+ {name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}},
+ {name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}},
+ {name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}},
+ {name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}},
+ {name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}},
+ {name: "scrollPageUp", defaultHandler: function() {return self._doScroll({type: "pageUp"});}},
+ {name: "scrollPageDown", defaultHandler: function() {return self._doScroll({type: "pageDown"});}},
+ {name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}},
+ {name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}},
+ {name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}},
+ {name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}},
+ {name: "scrollTextStart", defaultHandler: function() {return self._doScroll({type: "textStart"});}},
+ {name: "scrollTextEnd", defaultHandler: function() {return self._doScroll({type: "textEnd"});}},
+ {name: "centerLine", defaultHandler: function() {return self._doScroll({type: "centerLine"});}},
+
+ {name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}},
+ {name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}},
+ {name: "selectWholeLineUp", defaultHandler: function() {return self._doLineUp({select: true, wholeLine: true});}},
+ {name: "selectWholeLineDown", defaultHandler: function() {return self._doLineDown({select: true, wholeLine: true});}},
+ {name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}},
+ {name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}},
+ {name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}},
+ {name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}},
+ {name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}},
+ {name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}},
+ {name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}},
+ {name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}},
+ {name: "selectTextStart", defaultHandler: function() {return self._doHome({select: true, ctrl:true});}},
+ {name: "selectTextEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:true});}},
+
+ {name: "deletePrevious", defaultHandler: function() {return self._doBackspace({unit:"character"});}},
+ {name: "deleteNext", defaultHandler: function() {return self._doDelete({unit:"character"});}},
+ {name: "deleteWordPrevious", defaultHandler: function() {return self._doBackspace({unit:"word"});}},
+ {name: "deleteWordNext", defaultHandler: function() {return self._doDelete({unit:"word"});}},
+ {name: "deleteLineStart", defaultHandler: function() {return self._doBackspace({unit: "line"});}},
+ {name: "deleteLineEnd", defaultHandler: function() {return self._doDelete({unit: "line"});}},
+ {name: "tab", defaultHandler: function() {return self._doTab();}},
+ {name: "enter", defaultHandler: function() {return self._doEnter();}},
+ {name: "enterNoCursor", defaultHandler: function() {return self._doEnter({noCursor:true});}},
+ {name: "selectAll", defaultHandler: function() {return self._doSelectAll();}},
+ {name: "copy", defaultHandler: function() {return self._doCopy();}},
+ {name: "cut", defaultHandler: function() {return self._doCut();}},
+ {name: "paste", defaultHandler: function() {return self._doPaste();}}
+ ];
+ },
+ _createLine: function(parent, div, document, lineIndex, model) {
+ var lineText = model.getLine(lineIndex);
+ var lineStart = model.getLineStart(lineIndex);
+ var e = {type:"LineStyle", textView: this, lineIndex: lineIndex, lineText: lineText, lineStart: lineStart};
+ this.onLineStyle(e);
+ var lineDiv = div || document.createElement("DIV");
+ if (!div || !this._compare(div.viewStyle, e.style)) {
+ this._applyStyle(e.style, lineDiv, div);
+ lineDiv.viewStyle = e.style;
+ }
+ lineDiv.lineIndex = lineIndex;
+ var ranges = [];
+ var data = {tabOffset: 0, ranges: ranges};
+ this._createRanges(e.ranges, lineText, 0, lineText.length, lineStart, data);
+
+ /*
+ * A trailing span with a whitespace is added for three different reasons:
+ * 1. Make sure the height of each line is the largest of the default font
+ * in normal, italic, bold, and italic-bold.
+ * 2. When full selection is off, Firefox, Opera and IE9 do not extend the
+ * selection at the end of the line when the line is fully selected.
+ * 3. The height of a div with only an empty span is zero.
+ */
+ var c = " ";
+ if (!this._fullSelection && isIE < 9) {
+ /*
+ * IE8 already selects extra space at end of a line fully selected,
+ * adding another space at the end of the line causes the selection
+ * to look too big. The fix is to use a zero-width space (\uFEFF) instead.
+ */
+ c = "\uFEFF";
+ }
+ if (isWebkit) {
+ /*
+ * Feature in WekKit. Adding a regular white space to the line will
+ * cause the longest line in the view to wrap even though "pre" is set.
+ * The fix is to use the zero-width non-joiner character (\u200C) instead.
+ * Note: To not use \uFEFF because in old version of Chrome this character
+ * shows a glyph;
+ */
+ c = "\u200C";
+ }
+ ranges.push({text: c, style: this._largestFontStyle, ignoreChars: 1});
+
+ var range, span, style, oldSpan, oldStyle, text, oldText, end = 0, oldEnd = 0, next;
+ var changeCount, changeStart;
+ if (div) {
+ var modelChangedEvent = div.modelChangedEvent;
+ if (modelChangedEvent) {
+ if (modelChangedEvent.removedLineCount === 0 && modelChangedEvent.addedLineCount === 0) {
+ changeStart = modelChangedEvent.start - lineStart;
+ changeCount = modelChangedEvent.addedCharCount - modelChangedEvent.removedCharCount;
+ } else {
+ changeStart = -1;
+ }
+ div.modelChangedEvent = undefined;
+ }
+ oldSpan = div.firstChild;
+ }
+ for (var i = 0; i < ranges.length; i++) {
+ range = ranges[i];
+ text = range.text;
+ end += text.length;
+ style = range.style;
+ if (oldSpan) {
+ oldText = oldSpan.firstChild.data;
+ oldStyle = oldSpan.viewStyle;
+ if (oldText === text && this._compare(style, oldStyle)) {
+ oldEnd += oldText.length;
+ oldSpan._rectsCache = undefined;
+ span = oldSpan = oldSpan.nextSibling;
+ continue;
+ } else {
+ while (oldSpan) {
+ if (changeStart !== -1) {
+ var spanEnd = end;
+ if (spanEnd >= changeStart) {
+ spanEnd -= changeCount;
+ }
+ var length = oldSpan.firstChild.data.length;
+ if (oldEnd + length > spanEnd) { break; }
+ oldEnd += length;
+ }
+ next = oldSpan.nextSibling;
+ lineDiv.removeChild(oldSpan);
+ oldSpan = next;
+ }
+ }
+ }
+ span = this._createSpan(lineDiv, document, text, style, range.ignoreChars);
+ if (oldSpan) {
+ lineDiv.insertBefore(span, oldSpan);
+ } else {
+ lineDiv.appendChild(span);
+ }
+ if (div) {
+ div.lineWidth = undefined;
+ }
+ }
+ if (div) {
+ var tmp = span ? span.nextSibling : null;
+ while (tmp) {
+ next = tmp.nextSibling;
+ div.removeChild(tmp);
+ tmp = next;
+ }
+ } else {
+ parent.appendChild(lineDiv);
+ }
+ return lineDiv;
+ },
+ _createRanges: function(ranges, text, start, end, lineStart, data) {
+ if (start >= end) { return; }
+ if (ranges) {
+ for (var i = 0; i < ranges.length; i++) {
+ var range = ranges[i];
+ if (range.end <= lineStart + start) { continue; }
+ var styleStart = Math.max(lineStart + start, range.start) - lineStart;
+ if (styleStart >= end) { break; }
+ var styleEnd = Math.min(lineStart + end, range.end) - lineStart;
+ if (styleStart < styleEnd) {
+ styleStart = Math.max(start, styleStart);
+ styleEnd = Math.min(end, styleEnd);
+ if (start < styleStart) {
+ this._createRange(text, start, styleStart, null, data);
+ }
+ while (i + 1 < ranges.length && ranges[i + 1].start - lineStart === styleEnd && this._compare(range.style, ranges[i + 1].style)) {
+ range = ranges[i + 1];
+ styleEnd = Math.min(lineStart + end, range.end) - lineStart;
+ i++;
+ }
+ this._createRange(text, styleStart, styleEnd, range.style, data);
+ start = styleEnd;
+ }
+ }
+ }
+ if (start < end) {
+ this._createRange(text, start, end, null, data);
+ }
+ },
+ _createRange: function(text, start, end, style, data) {
+ if (start >= end) { return; }
+ var tabSize = this._customTabSize, range;
+ if (tabSize && tabSize !== 8) {
+ var tabIndex = text.indexOf("\t", start);
+ while (tabIndex !== -1 && tabIndex < end) {
+ if (start < tabIndex) {
+ range = {text: text.substring(start, tabIndex), style: style};
+ data.ranges.push(range);
+ data.tabOffset += range.text.length;
+ }
+ var spacesCount = tabSize - (data.tabOffset % tabSize);
+ if (spacesCount > 0) {
+ //TODO hack to preserve text length in getDOMText()
+ var spaces = "\u00A0";
+ for (var i = 1; i < spacesCount; i++) {
+ spaces += " ";
+ }
+ range = {text: spaces, style: style, ignoreChars: spacesCount - 1};
+ data.ranges.push(range);
+ data.tabOffset += range.text.length;
+ }
+ start = tabIndex + 1;
+ tabIndex = text.indexOf("\t", start);
+ }
+ }
+ if (start < end) {
+ range = {text: text.substring(start, end), style: style};
+ data.ranges.push(range);
+ data.tabOffset += range.text.length;
+ }
+ },
+ _createSpan: function(parent, document, text, style, ignoreChars) {
+ var isLink = style && style.tagName === "A";
+ if (isLink) { parent.hasLink = true; }
+ var tagName = isLink && this._linksVisible ? "A" : "SPAN";
+ var child = document.createElement(tagName);
+ child.appendChild(document.createTextNode(text));
+ this._applyStyle(style, child);
+ if (tagName === "A") {
+ var self = this;
+ addHandler(child, "click", function(e) { return self._handleLinkClick(e); }, false);
+ }
+ child.viewStyle = style;
+ if (ignoreChars) {
+ child.ignoreChars = ignoreChars;
+ }
+ return child;
+ },
+ _createRuler: function(ruler) {
+ if (!this._clientDiv) { return; }
+ var document = this._frameDocument;
+ var body = document.body;
+ var side = ruler.getLocation();
+ var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
+ if (!rulerParent) {
+ rulerParent = document.createElement("DIV");
+ rulerParent.style.overflow = "hidden";
+ rulerParent.style.MozUserSelect = "none";
+ rulerParent.style.WebkitUserSelect = "none";
+ if (isIE) {
+ rulerParent.attachEvent("onselectstart", function() {return false;});
+ }
+ rulerParent.style.position = "absolute";
+ rulerParent.style.top = "0px";
+ rulerParent.style.cursor = "default";
+ body.appendChild(rulerParent);
+ if (side === "left") {
+ this._leftDiv = rulerParent;
+ rulerParent.className = "viewLeftRuler";
+ } else {
+ this._rightDiv = rulerParent;
+ rulerParent.className = "viewRightRuler";
+ }
+ var table = document.createElement("TABLE");
+ rulerParent.appendChild(table);
+ table.cellPadding = "0px";
+ table.cellSpacing = "0px";
+ table.border = "0px";
+ table.insertRow(0);
+ var self = this;
+ addHandler(rulerParent, "click", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "dblclick", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "mousemove", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "mouseover", function(e) { self._handleRulerEvent(e); });
+ addHandler(rulerParent, "mouseout", function(e) { self._handleRulerEvent(e); });
+ }
+ var div = document.createElement("DIV");
+ div._ruler = ruler;
+ div.rulerChanged = true;
+ div.style.position = "relative";
+ var row = rulerParent.firstChild.rows[0];
+ var index = row.cells.length;
+ var cell = row.insertCell(index);
+ cell.vAlign = "top";
+ cell.appendChild(div);
+ },
+ _createFrame: function() {
+ if (this.frame) { return; }
+ var parent = this._parent;
+ while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
+ var parentDocument = parent.ownerDocument;
+ this._parentDocument = parentDocument;
+ var frame = parentDocument.createElement("IFRAME");
+ this._frame = frame;
+ frame.frameBorder = "0px";//for IE, needs to be set before the frame is added to the parent
+ frame.style.border = "0px";
+ frame.style.width = "100%";
+ frame.style.height = "100%";
+ frame.scrolling = "no";
+ var self = this;
+ /*
+ * Note that it is not possible to create the contents of the frame if the
+ * parent is not connected to the document. Only create it when the load
+ * event is trigged.
+ */
+ this._loadHandler = function(e) {
+ self._handleLoad(e);
+ };
+ addHandler(frame, "load", this._loadHandler, !!isFirefox);
+ if (!isWebkit) {
+ /*
+ * Feature in IE and Firefox. It is not possible to get the style of an
+ * element if it is not layed out because one of the ancestor has
+ * style.display = none. This means that the view cannot be created in this
+ * situations, since no measuring can be performed. The fix is to listen
+ * for DOMAttrModified and create or destroy the view when the style.display
+ * attribute changes.
+ */
+ addHandler(parentDocument, "DOMAttrModified", this._attrModifiedHandler = function(e) {
+ self._handleDOMAttrModified(e);
+ });
+ }
+ parent.appendChild(frame);
+ /* create synchronously if possible */
+ if (this._sync) {
+ this._handleLoad();
+ }
+ },
+ _getFrameHTML: function() {
+ var html = [];
+ html.push("");
+ html.push("");
+ html.push("");
+ if (isIE < 9) {
+ html.push(" ");
+ }
+ html.push("");
+ if (this._stylesheet) {
+ var stylesheet = typeof(this._stylesheet) === "string" ? [this._stylesheet] : this._stylesheet;
+ for (var i = 0; i < stylesheet.length; i++) {
+ var sheet = stylesheet[i];
+ var isLink = this._isLinkURL(sheet);
+ if (isLink && this._sync) {
+ try {
+ var objXml = new XMLHttpRequest();
+ if (objXml.overrideMimeType) {
+ objXml.overrideMimeType("text/css");
+ }
+ objXml.open("GET", sheet, false);
+ objXml.send(null);
+ sheet = objXml.responseText;
+ isLink = false;
+ } catch (e) {}
+ }
+ if (isLink) {
+ html.push(" ");
+ } else {
+ html.push("");
+ }
+ }
+ }
+ /*
+ * Feature in WebKit. In WebKit, window load will not wait for the style sheets
+ * to be loaded unless there is script element after the style sheet link elements.
+ */
+ html.push("");
+ html.push("");
+ html.push("");
+ html.push("");
+ return html.join("");
+ },
+ _createView: function() {
+ if (this._frameDocument) { return; }
+ var frameWindow = this._frameWindow = this._frame.contentWindow;
+ var frameDocument = this._frameDocument = frameWindow.document;
+ var self = this;
+ function write() {
+ frameDocument.open("text/html", "replace");
+ frameDocument.write(self._getFrameHTML());
+ frameDocument.close();
+ self._windowLoadHandler = function(e) {
+ /*
+ * Bug in Safari. Safari sends the window load event before the
+ * style sheets are loaded. The fix is to defer creation of the
+ * contents until the document readyState changes to complete.
+ */
+ if (self._isDocumentReady()) {
+ self._createContent();
+ }
+ };
+ addHandler(frameWindow, "load", self._windowLoadHandler);
+ }
+ write();
+ if (this._sync) {
+ this._createContent();
+ } else {
+ /*
+ * Bug in Webkit. Webkit does not send the load event for the iframe window when the main page
+ * loads as a result of backward or forward navigation.
+ * The fix is to use a timer to create the content only when the document is ready.
+ */
+ this._createViewTimer = function() {
+ if (self._clientDiv) { return; }
+ if (self._isDocumentReady()) {
+ self._createContent();
+ } else {
+ setTimeout(self._createViewTimer, 10);
+ }
+ };
+ setTimeout(this._createViewTimer, 10);
+ }
+ },
+ _isDocumentReady: function() {
+ var frameDocument = this._frameDocument;
+ if (!frameDocument) { return false; }
+ if (frameDocument.readyState === "complete") {
+ return true;
+ } else if (frameDocument.readyState === "interactive" && isFirefox) {
+ /*
+ * Bug in Firefox. Firefox does not change the document ready state to complete
+ * all the time. The fix is to wait for the ready state to be "interactive" and check that
+ * all css rules are initialized.
+ */
+ var styleSheets = frameDocument.styleSheets;
+ var styleSheetCount = 1;
+ if (this._stylesheet) {
+ styleSheetCount += typeof(this._stylesheet) === "string" ? 1 : this._stylesheet.length;
+ }
+ if (styleSheetCount === styleSheets.length) {
+ var index = 0;
+ while (index < styleSheets.length) {
+ var count = 0;
+ try {
+ count = styleSheets.item(index).cssRules.length;
+ } catch (ex) {
+ /*
+ * Feature in Firefox. To determine if a stylesheet is loaded the number of css rules is used, if the
+ * stylesheet is not loaded this operation will throw an invalid access error. When a stylesheet from
+ * a different domain is loaded, accessing the css rules will result in a security exception. In this
+ * case count is set to 1 to indicate the stylesheet is loaded.
+ */
+ if (ex.code !== DOMException.INVALID_ACCESS_ERR) {
+ count = 1;
+ }
+ }
+ if (count === 0) { break; }
+ index++;
+ }
+ return index === styleSheets.length;
+ }
+ }
+ return false;
+ },
+ _createContent: function() {
+ if (this._clientDiv) { return; }
+ var parent = this._parent;
+ var parentDocument = this._parentDocument;
+ var frameDocument = this._frameDocument;
+ var body = frameDocument.body;
+ this._setThemeClass(this._themeClass, true);
+ body.style.margin = "0px";
+ body.style.borderWidth = "0px";
+ body.style.padding = "0px";
+
+ var textArea;
+ if (isPad) {
+ var touchDiv = parentDocument.createElement("DIV");
+ this._touchDiv = touchDiv;
+ touchDiv.style.position = "absolute";
+ touchDiv.style.border = "0px";
+ touchDiv.style.padding = "0px";
+ touchDiv.style.margin = "0px";
+ touchDiv.style.zIndex = "2";
+ touchDiv.style.overflow = "hidden";
+ touchDiv.style.background="transparent";
+ touchDiv.style.WebkitUserSelect = "none";
+ parent.appendChild(touchDiv);
+
+ textArea = parentDocument.createElement("TEXTAREA");
+ this._textArea = textArea;
+ textArea.style.position = "absolute";
+ textArea.style.whiteSpace = "pre";
+ textArea.style.left = "-1000px";
+ textArea.tabIndex = 1;
+ textArea.autocapitalize = "off";
+ textArea.autocorrect = "off";
+ textArea.className = "viewContainer";
+ textArea.style.background = "transparent";
+ textArea.style.color = "transparent";
+ textArea.style.border = "0px";
+ textArea.style.padding = "0px";
+ textArea.style.margin = "0px";
+ textArea.style.borderRadius = "0px";
+ textArea.style.WebkitAppearance = "none";
+ textArea.style.WebkitTapHighlightColor = "transparent";
+ touchDiv.appendChild(textArea);
+ }
+ if (isFirefox) {
+ var clipboardDiv = frameDocument.createElement("DIV");
+ this._clipboardDiv = clipboardDiv;
+ clipboardDiv.style.position = "fixed";
+ clipboardDiv.style.whiteSpace = "pre";
+ clipboardDiv.style.left = "-1000px";
+ body.appendChild(clipboardDiv);
+ }
+
+ var viewDiv = frameDocument.createElement("DIV");
+ viewDiv.className = "view";
+ this._viewDiv = viewDiv;
+ viewDiv.id = "viewDiv";
+ viewDiv.tabIndex = -1;
+ viewDiv.style.overflow = "auto";
+ viewDiv.style.position = "absolute";
+ viewDiv.style.top = "0px";
+ viewDiv.style.borderWidth = "0px";
+ viewDiv.style.margin = "0px";
+ viewDiv.style.outline = "none";
+ body.appendChild(viewDiv);
+
+ var scrollDiv = frameDocument.createElement("DIV");
+ this._scrollDiv = scrollDiv;
+ scrollDiv.id = "scrollDiv";
+ scrollDiv.style.margin = "0px";
+ scrollDiv.style.borderWidth = "0px";
+ scrollDiv.style.padding = "0px";
+ viewDiv.appendChild(scrollDiv);
+
+ if (isFirefox) {
+ var clipDiv = frameDocument.createElement("DIV");
+ this._clipDiv = clipDiv;
+ clipDiv.id = "clipDiv";
+ clipDiv.style.position = "fixed";
+ clipDiv.style.overflow = "hidden";
+ clipDiv.style.margin = "0px";
+ clipDiv.style.borderWidth = "0px";
+ clipDiv.style.padding = "0px";
+ scrollDiv.appendChild(clipDiv);
+
+ var clipScrollDiv = frameDocument.createElement("DIV");
+ this._clipScrollDiv = clipScrollDiv;
+ clipScrollDiv.id = "clipScrollDiv";
+ clipScrollDiv.style.position = "absolute";
+ clipScrollDiv.style.height = "1px";
+ clipScrollDiv.style.top = "-1000px";
+ clipDiv.appendChild(clipScrollDiv);
+ }
+
+ this._setFullSelection(this._fullSelection, true);
+
+ var clientDiv = frameDocument.createElement("DIV");
+ clientDiv.className = "viewContent";
+ this._clientDiv = clientDiv;
+ clientDiv.id = "clientDiv";
+ clientDiv.style.whiteSpace = "pre";
+ clientDiv.style.position = this._clipDiv ? "absolute" : "fixed";
+ clientDiv.style.borderWidth = "0px";
+ clientDiv.style.margin = "0px";
+ clientDiv.style.padding = "0px";
+ clientDiv.style.outline = "none";
+ clientDiv.style.zIndex = "1";
+ if (isPad) {
+ clientDiv.style.WebkitTapHighlightColor = "transparent";
+ }
+ (this._clipDiv || scrollDiv).appendChild(clientDiv);
+
+ if (isFirefox && !clientDiv.setCapture) {
+ var overlayDiv = frameDocument.createElement("DIV");
+ this._overlayDiv = overlayDiv;
+ overlayDiv.id = "overlayDiv";
+ overlayDiv.style.position = clientDiv.style.position;
+ overlayDiv.style.borderWidth = clientDiv.style.borderWidth;
+ overlayDiv.style.margin = clientDiv.style.margin;
+ overlayDiv.style.padding = clientDiv.style.padding;
+ overlayDiv.style.cursor = "text";
+ overlayDiv.style.zIndex = "2";
+ (this._clipDiv || scrollDiv).appendChild(overlayDiv);
+ }
+ if (!isPad) {
+ clientDiv.contentEditable = "true";
+ }
+ this._lineHeight = this._calculateLineHeight();
+ this._viewPadding = this._calculatePadding();
+ if (isIE) {
+ body.style.lineHeight = this._lineHeight + "px";
+ }
+ this._setTabSize(this._tabSize, true);
+ this._hookEvents();
+ var rulers = this._rulers;
+ for (var i=0; i 0 || v > 0) {
+ viewDiv.scrollLeft = h;
+ viewDiv.scrollTop = v;
+ }
+ this.onLoad({type: "Load"});
+ },
+ _defaultOptions: function() {
+ return {
+ parent: {value: undefined, recreate: true, update: null},
+ model: {value: undefined, recreate: false, update: this.setModel},
+ readonly: {value: false, recreate: false, update: null},
+ fullSelection: {value: true, recreate: false, update: this._setFullSelection},
+ tabSize: {value: 8, recreate: false, update: this._setTabSize},
+ expandTab: {value: false, recreate: false, update: null},
+ stylesheet: {value: [], recreate: false, update: this._setStyleSheet},
+ themeClass: {value: undefined, recreate: false, update: this._setThemeClass},
+ sync: {value: false, recreate: false, update: null}
+ };
+ },
+ _destroyFrame: function() {
+ var frame = this._frame;
+ if (!frame) { return; }
+ if (this._loadHandler) {
+ removeHandler(frame, "load", this._loadHandler, !!isFirefox);
+ this._loadHandler = null;
+ }
+ if (this._attrModifiedHandler) {
+ removeHandler(this._parentDocument, "DOMAttrModified", this._attrModifiedHandler);
+ this._attrModifiedHandler = null;
+ }
+ frame.parentNode.removeChild(frame);
+ this._frame = null;
+ },
+ _destroyRuler: function(ruler) {
+ var side = ruler.getLocation();
+ var rulerParent = side === "left" ? this._leftDiv : this._rightDiv;
+ if (rulerParent) {
+ var row = rulerParent.firstChild.rows[0];
+ var cells = row.cells;
+ for (var index = 0; index < cells.length; index++) {
+ var cell = cells[index];
+ if (cell.firstChild._ruler === ruler) { break; }
+ }
+ if (index === cells.length) { return; }
+ row.cells[index]._ruler = undefined;
+ row.deleteCell(index);
+ }
+ },
+ _destroyView: function() {
+ var clientDiv = this._clientDiv;
+ if (!clientDiv) { return; }
+ this._setGrab(null);
+ this._unhookEvents();
+ if (this._windowLoadHandler) {
+ removeHandler(this._frameWindow, "load", this._windowLoadHandler);
+ this._windowLoadHandler = null;
+ }
+
+ /* Destroy timers */
+ if (this._autoScrollTimerID) {
+ clearTimeout(this._autoScrollTimerID);
+ this._autoScrollTimerID = null;
+ }
+ if (this._updateTimer) {
+ clearTimeout(this._updateTimer);
+ this._updateTimer = null;
+ }
+
+ /* Destroy DOM */
+ var parent = this._frameDocument.body;
+ while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); }
+ if (this._touchDiv) {
+ this._parent.removeChild(this._touchDiv);
+ this._touchDiv = null;
+ }
+ this._selDiv1 = null;
+ this._selDiv2 = null;
+ this._selDiv3 = null;
+ this._insertedSelRule = false;
+ this._textArea = null;
+ this._clipboardDiv = null;
+ this._scrollDiv = null;
+ this._viewDiv = null;
+ this._clipDiv = null;
+ this._clipScrollDiv = null;
+ this._clientDiv = null;
+ this._overlayDiv = null;
+ this._leftDiv = null;
+ this._rightDiv = null;
+ this._frameDocument = null;
+ this._frameWindow = null;
+ this.onUnload({type: "Unload"});
+ },
+ _doAutoScroll: function (direction, x, y) {
+ this._autoScrollDir = direction;
+ this._autoScrollX = x;
+ this._autoScrollY = y;
+ if (!this._autoScrollTimerID) {
+ this._autoScrollTimer();
+ }
+ },
+ _endAutoScroll: function () {
+ if (this._autoScrollTimerID) { clearTimeout(this._autoScrollTimerID); }
+ this._autoScrollDir = undefined;
+ this._autoScrollTimerID = undefined;
+ },
+ _fixCaret: function() {
+ var clientDiv = this._clientDiv;
+ if (clientDiv) {
+ var hasFocus = this._hasFocus;
+ this._ignoreFocus = true;
+ if (hasFocus) { clientDiv.blur(); }
+ clientDiv.contentEditable = false;
+ clientDiv.contentEditable = true;
+ if (hasFocus) { clientDiv.focus(); }
+ this._ignoreFocus = false;
+ }
+ },
+ _getBaseText: function(start, end) {
+ var model = this._model;
+ /* This is the only case the view access the base model, alternatively the view could use a event to application to customize the text */
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ end = model.mapOffset(end);
+ model = model.getBaseModel();
+ }
+ return model.getText(start, end);
+ },
+ _getBoundsAtOffset: function (offset) {
+ var model = this._model;
+ var document = this._frameDocument;
+ var clientDiv = this._clientDiv;
+ var lineIndex = model.getLineAtOffset(offset);
+ var dummy;
+ var child = this._getLineNode(lineIndex);
+ if (!child) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var result = null;
+ if (offset < model.getLineEnd(lineIndex)) {
+ var lineOffset = model.getLineStart(lineIndex);
+ var lineChild = child.firstChild;
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ var nodeLength = textNode.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (lineOffset + nodeLength > offset) {
+ var index = offset - lineOffset;
+ var range;
+ if (isRangeRects) {
+ range = document.createRange();
+ range.setStart(textNode, index);
+ range.setEnd(textNode, index + 1);
+ result = range.getBoundingClientRect();
+ } else if (isIE) {
+ range = document.body.createTextRange();
+ range.moveToElementText(lineChild);
+ range.collapse();
+ range.moveEnd("character", index + 1);
+ range.moveStart("character", index);
+ result = range.getBoundingClientRect();
+ } else {
+ var text = textNode.data;
+ lineChild.removeChild(textNode);
+ lineChild.appendChild(document.createTextNode(text.substring(0, index)));
+ var span = document.createElement("SPAN");
+ span.appendChild(document.createTextNode(text.substring(index, index + 1)));
+ lineChild.appendChild(span);
+ lineChild.appendChild(document.createTextNode(text.substring(index + 1)));
+ result = span.getBoundingClientRect();
+ lineChild.innerHTML = "";
+ lineChild.appendChild(textNode);
+ if (!dummy) {
+ /*
+ * Removing the element node that holds the selection start or end
+ * causes the selection to be lost. The fix is to detect this case
+ * and restore the selection.
+ */
+ var s = this._getSelection();
+ if ((lineOffset <= s.start && s.start < lineOffset + nodeLength) || (lineOffset <= s.end && s.end < lineOffset + nodeLength)) {
+ this._updateDOMSelection();
+ }
+ }
+ }
+ if (isIE) {
+ var logicalXDPI = window.screen.logicalXDPI;
+ var deviceXDPI = window.screen.deviceXDPI;
+ result.left = result.left * logicalXDPI / deviceXDPI;
+ result.right = result.right * logicalXDPI / deviceXDPI;
+ }
+ break;
+ }
+ lineOffset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+ }
+ if (!result) {
+ var rect = this._getLineBoundingClientRect(child);
+ result = {left: rect.right, right: rect.right};
+ }
+ if (dummy) { clientDiv.removeChild(dummy); }
+ return result;
+ },
+ _getBottomIndex: function (fullyVisible) {
+ var child = this._bottomChild;
+ if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
+ var rect = child.getBoundingClientRect();
+ var clientRect = this._clientDiv.getBoundingClientRect();
+ if (rect.bottom > clientRect.bottom) {
+ child = this._getLinePrevious(child) || child;
+ }
+ }
+ return child.lineIndex;
+ },
+ _getFrameHeight: function() {
+ return this._frameDocument.documentElement.clientHeight;
+ },
+ _getFrameWidth: function() {
+ return this._frameDocument.documentElement.clientWidth;
+ },
+ _getClientHeight: function() {
+ var viewPad = this._getViewPadding();
+ return Math.max(0, this._viewDiv.clientHeight - viewPad.top - viewPad.bottom);
+ },
+ _getClientWidth: function() {
+ var viewPad = this._getViewPadding();
+ return Math.max(0, this._viewDiv.clientWidth - viewPad.left - viewPad.right);
+ },
+ _getClipboardText: function (event, handler) {
+ var delimiter = this._model.getLineDelimiter();
+ var clipboadText, text;
+ if (this._frameWindow.clipboardData) {
+ //IE
+ clipboadText = [];
+ text = this._frameWindow.clipboardData.getData("Text");
+ this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+ text = clipboadText.join("");
+ if (handler) { handler(text); }
+ return text;
+ }
+ if (isFirefox) {
+ this._ignoreFocus = true;
+ var document = this._frameDocument;
+ var clipboardDiv = this._clipboardDiv;
+ clipboardDiv.innerHTML = " ";
+ clipboardDiv.firstChild.focus();
+ var self = this;
+ var _getText = function() {
+ var noteText = self._getTextFromElement(clipboardDiv);
+ clipboardDiv.innerHTML = "";
+ clipboadText = [];
+ self._convertDelimiter(noteText, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+ return clipboadText.join("");
+ };
+
+ /* Try execCommand first. Works on firefox with clipboard permission. */
+ var result = false;
+ this._ignorePaste = true;
+
+ /* Do not try execCommand if middle-click is used, because if we do, we get the clipboard text, not the primary selection text. */
+ if (!isLinux || this._lastMouseButton !== 2) {
+ try {
+ result = document.execCommand("paste", false, null);
+ } catch (ex) {
+ /* Firefox can throw even when execCommand() works, see bug 362835. */
+ result = clipboardDiv.childNodes.length > 1 || clipboardDiv.firstChild && clipboardDiv.firstChild.childNodes.length > 0;
+ }
+ }
+ this._ignorePaste = false;
+ if (!result) {
+ /* Try native paste in DOM, works for firefox during the paste event. */
+ if (event) {
+ setTimeout(function() {
+ self.focus();
+ text = _getText();
+ if (text && handler) {
+ handler(text);
+ }
+ self._ignoreFocus = false;
+ }, 0);
+ return null;
+ } else {
+ /* no event and no clipboard permission, paste can't be performed */
+ this.focus();
+ this._ignoreFocus = false;
+ return "";
+ }
+ }
+ this.focus();
+ this._ignoreFocus = false;
+ text = _getText();
+ if (text && handler) {
+ handler(text);
+ }
+ return text;
+ }
+ //webkit
+ if (event && event.clipboardData) {
+ /*
+ * Webkit (Chrome/Safari) allows getData during the paste event
+ * Note: setData is not allowed, not even during copy/cut event
+ */
+ clipboadText = [];
+ text = event.clipboardData.getData("text/plain");
+ this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);});
+ text = clipboadText.join("");
+ if (text && handler) {
+ handler(text);
+ }
+ return text;
+ } else {
+ //TODO try paste using extension (Chrome only)
+ }
+ return "";
+ },
+ _getDOMText: function(lineIndex) {
+ var child = this._getLineNode(lineIndex);
+ var lineChild = child.firstChild;
+ var text = "";
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ while (textNode) {
+ if (lineChild.ignoreChars) {
+ for (var i = 0; i < textNode.length; i++) {
+ var ch = textNode.data.substring(i, i + 1);
+ if (ch !== " ") {
+ text += ch;
+ }
+ }
+ } else {
+ text += textNode.data;
+ }
+ textNode = textNode.nextSibling;
+ }
+ lineChild = lineChild.nextSibling;
+ }
+ return text;
+ },
+ _getTextFromElement: function(element) {
+ var document = element.ownerDocument;
+ var window = document.defaultView;
+ if (!window.getSelection) {
+ return element.innerText || element.textContent;
+ }
+
+ var newRange = document.createRange();
+ newRange.selectNode(element);
+
+ var selection = window.getSelection();
+ var oldRanges = [], i;
+ for (i = 0; i < selection.rangeCount; i++) {
+ oldRanges.push(selection.getRangeAt(i));
+ }
+
+ this._ignoreSelect = true;
+ selection.removeAllRanges();
+ selection.addRange(newRange);
+
+ var text = selection.toString();
+
+ selection.removeAllRanges();
+ for (i = 0; i < oldRanges.length; i++) {
+ selection.addRange(oldRanges[i]);
+ }
+
+ this._ignoreSelect = false;
+ return text;
+ },
+ _getViewPadding: function() {
+ return this._viewPadding;
+ },
+ _getLineBoundingClientRect: function (child) {
+ var rect = child.getBoundingClientRect();
+ var lastChild = child.lastChild;
+ //Remove any artificial trailing whitespace in the line
+ while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) {
+ lastChild = lastChild.previousSibling;
+ }
+ if (!lastChild) {
+ return {left: rect.left, top: rect.top, right: rect.left, bottom: rect.bottom};
+ }
+ var lastRect = lastChild.getBoundingClientRect();
+ return {left: rect.left, top: rect.top, right: lastRect.right, bottom: rect.bottom};
+ },
+ _getLineHeight: function() {
+ return this._lineHeight;
+ },
+ _getLineNode: function (lineIndex) {
+ var clientDiv = this._clientDiv;
+ var child = clientDiv.firstChild;
+ while (child) {
+ if (lineIndex === child.lineIndex) {
+ return child;
+ }
+ child = child.nextSibling;
+ }
+ return undefined;
+ },
+ _getLineNext: function (lineNode) {
+ var node = lineNode ? lineNode.nextSibling : this._clientDiv.firstChild;
+ while (node && node.lineIndex === -1) {
+ node = node.nextSibling;
+ }
+ return node;
+ },
+ _getLinePrevious: function (lineNode) {
+ var node = lineNode ? lineNode.previousSibling : this._clientDiv.lastChild;
+ while (node && node.lineIndex === -1) {
+ node = node.previousSibling;
+ }
+ return node;
+ },
+ _getOffset: function (offset, unit, direction) {
+ if (unit === "line") {
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ if (direction > 0) {
+ return model.getLineEnd(lineIndex);
+ }
+ return model.getLineStart(lineIndex);
+ }
+ if (unit === "wordend") {
+ return this._getOffset_W3C(offset, unit, direction);
+ }
+ return isIE ? this._getOffset_IE(offset, unit, direction) : this._getOffset_W3C(offset, unit, direction);
+ },
+ _getOffset_W3C: function (offset, unit, direction) {
+ function _isPunctuation(c) {
+ return (33 <= c && c <= 47) || (58 <= c && c <= 64) || (91 <= c && c <= 94) || c === 96 || (123 <= c && c <= 126);
+ }
+ function _isWhitespace(c) {
+ return c === 32 || c === 9;
+ }
+ if (unit === "word" || unit === "wordend") {
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ var lineText = model.getLine(lineIndex);
+ var lineStart = model.getLineStart(lineIndex);
+ var lineEnd = model.getLineEnd(lineIndex);
+ var lineLength = lineText.length;
+ var offsetInLine = offset - lineStart;
+
+
+ var c, previousPunctuation, previousLetterOrDigit, punctuation, letterOrDigit;
+ if (direction > 0) {
+ if (offsetInLine === lineLength) { return lineEnd; }
+ c = lineText.charCodeAt(offsetInLine);
+ previousPunctuation = _isPunctuation(c);
+ previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
+ offsetInLine++;
+ while (offsetInLine < lineLength) {
+ c = lineText.charCodeAt(offsetInLine);
+ punctuation = _isPunctuation(c);
+ if (unit === "wordend") {
+ if (!punctuation && previousPunctuation) { break; }
+ } else {
+ if (punctuation && !previousPunctuation) { break; }
+ }
+ letterOrDigit = !punctuation && !_isWhitespace(c);
+ if (unit === "wordend") {
+ if (!letterOrDigit && previousLetterOrDigit) { break; }
+ } else {
+ if (letterOrDigit && !previousLetterOrDigit) { break; }
+ }
+ previousLetterOrDigit = letterOrDigit;
+ previousPunctuation = punctuation;
+ offsetInLine++;
+ }
+ } else {
+ if (offsetInLine === 0) { return lineStart; }
+ offsetInLine--;
+ c = lineText.charCodeAt(offsetInLine);
+ previousPunctuation = _isPunctuation(c);
+ previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c);
+ while (0 < offsetInLine) {
+ c = lineText.charCodeAt(offsetInLine - 1);
+ punctuation = _isPunctuation(c);
+ if (unit === "wordend") {
+ if (punctuation && !previousPunctuation) { break; }
+ } else {
+ if (!punctuation && previousPunctuation) { break; }
+ }
+ letterOrDigit = !punctuation && !_isWhitespace(c);
+ if (unit === "wordend") {
+ if (letterOrDigit && !previousLetterOrDigit) { break; }
+ } else {
+ if (!letterOrDigit && previousLetterOrDigit) { break; }
+ }
+ previousLetterOrDigit = letterOrDigit;
+ previousPunctuation = punctuation;
+ offsetInLine--;
+ }
+ }
+ return lineStart + offsetInLine;
+ }
+ return offset + direction;
+ },
+ _getOffset_IE: function (offset, unit, direction) {
+ var document = this._frameDocument;
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ var clientDiv = this._clientDiv;
+ var dummy;
+ var child = this._getLineNode(lineIndex);
+ if (!child) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var result = 0, range, length;
+ var lineOffset = model.getLineStart(lineIndex);
+ if (offset === model.getLineEnd(lineIndex)) {
+ range = document.body.createTextRange();
+ range.moveToElementText(child.lastChild);
+ length = range.text.length;
+ range.moveEnd(unit, direction);
+ result = offset + range.text.length - length;
+ } else if (offset === lineOffset && direction < 0) {
+ result = lineOffset;
+ } else {
+ var lineChild = child.firstChild;
+ while (lineChild) {
+ var textNode = lineChild.firstChild;
+ var nodeLength = textNode.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (lineOffset + nodeLength > offset) {
+ range = document.body.createTextRange();
+ if (offset === lineOffset && direction < 0) {
+ range.moveToElementText(lineChild.previousSibling);
+ } else {
+ range.moveToElementText(lineChild);
+ range.collapse();
+ range.moveEnd("character", offset - lineOffset);
+ }
+ length = range.text.length;
+ range.moveEnd(unit, direction);
+ result = offset + range.text.length - length;
+ break;
+ }
+ lineOffset = nodeLength + lineOffset;
+ lineChild = lineChild.nextSibling;
+ }
+ }
+ if (dummy) { clientDiv.removeChild(dummy); }
+ return result;
+ },
+ _getOffsetToX: function (offset) {
+ return this._getBoundsAtOffset(offset).left;
+ },
+ _getPadding: function (node) {
+ var left,top,right,bottom;
+ if (node.currentStyle) {
+ left = node.currentStyle.paddingLeft;
+ top = node.currentStyle.paddingTop;
+ right = node.currentStyle.paddingRight;
+ bottom = node.currentStyle.paddingBottom;
+ } else if (this._frameWindow.getComputedStyle) {
+ var style = this._frameWindow.getComputedStyle(node, null);
+ left = style.getPropertyValue("padding-left");
+ top = style.getPropertyValue("padding-top");
+ right = style.getPropertyValue("padding-right");
+ bottom = style.getPropertyValue("padding-bottom");
+ }
+ return {
+ left: parseInt(left, 10),
+ top: parseInt(top, 10),
+ right: parseInt(right, 10),
+ bottom: parseInt(bottom, 10)
+ };
+ },
+ _getScroll: function() {
+ var viewDiv = this._viewDiv;
+ return {x: viewDiv.scrollLeft, y: viewDiv.scrollTop};
+ },
+ _getSelection: function () {
+ return this._selection.clone();
+ },
+ _getTopIndex: function (fullyVisible) {
+ var child = this._topChild;
+ if (fullyVisible && this._getClientHeight() > this._getLineHeight()) {
+ var rect = child.getBoundingClientRect();
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ if (rect.top < viewRect.top + viewPad.top) {
+ child = this._getLineNext(child) || child;
+ }
+ }
+ return child.lineIndex;
+ },
+ _getXToOffset: function (lineIndex, x) {
+ var model = this._model;
+ var lineStart = model.getLineStart(lineIndex);
+ var lineEnd = model.getLineEnd(lineIndex);
+ if (lineStart === lineEnd) {
+ return lineStart;
+ }
+ var document = this._frameDocument;
+ var clientDiv = this._clientDiv;
+ var dummy;
+ var child = this._getLineNode(lineIndex);
+ if (!child) {
+ child = dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ }
+ var lineRect = this._getLineBoundingClientRect(child);
+ if (x < lineRect.left) { x = lineRect.left; }
+ if (x > lineRect.right) { x = lineRect.right; }
+ /*
+ * Bug in IE 8 and earlier. The coordinates of getClientRects() are relative to
+ * the browser window. The fix is to convert to the frame window before using it.
+ */
+ var deltaX = 0, rects;
+ if (isIE < 9) {
+ rects = child.getClientRects();
+ var minLeft = rects[0].left;
+ for (var i=1; i 1) {
+ var mid = Math.floor((high + low) / 2);
+ start = low + 1;
+ end = mid === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : mid + 1;
+ if (isRangeRects) {
+ range.setStart(textNode, start);
+ range.setEnd(textNode, end);
+ } else {
+ range.moveToElementText(lineChild);
+ range.move("character", start);
+ range.moveEnd("character", end - start);
+ }
+ rects = range.getClientRects();
+ var found = false;
+ for (var k = 0; k < rects.length; k++) {
+ rect = rects[k];
+ var rangeLeft = rect.left * logicalXDPI / deviceXDPI - deltaX;
+ var rangeRight = rect.right * logicalXDPI / deviceXDPI - deltaX;
+ if (rangeLeft <= x && x < rangeRight) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ high = mid;
+ } else {
+ low = mid;
+ }
+ }
+ offset += high;
+ start = high;
+ end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : Math.min(high + 1, textNode.length);
+ if (isRangeRects) {
+ range.setStart(textNode, start);
+ range.setEnd(textNode, end);
+ } else {
+ range.moveToElementText(lineChild);
+ range.move("character", start);
+ range.moveEnd("character", end - start);
+ }
+ rect = range.getClientRects()[0];
+ //TODO test for character trailing (wrong for bidi)
+ if (x > ((rect.left * logicalXDPI / deviceXDPI - deltaX) + ((rect.right - rect.left) * logicalXDPI / deviceXDPI / 2))) {
+ offset++;
+ }
+ } else {
+ var newText = [];
+ for (var q = 0; q < nodeLength; q++) {
+ newText.push("");
+ if (q === nodeLength - 1) {
+ newText.push(textNode.data.substring(q));
+ } else {
+ newText.push(textNode.data.substring(q, q + 1));
+ }
+ newText.push(" ");
+ }
+ lineChild.innerHTML = newText.join("");
+ var rangeChild = lineChild.firstChild;
+ while (rangeChild) {
+ rect = rangeChild.getBoundingClientRect();
+ if (rect.left <= x && x < rect.right) {
+ //TODO test for character trailing (wrong for bidi)
+ if (x > rect.left + (rect.right - rect.left) / 2) {
+ offset++;
+ }
+ break;
+ }
+ offset++;
+ rangeChild = rangeChild.nextSibling;
+ }
+ if (!dummy) {
+ lineChild.innerHTML = "";
+ lineChild.appendChild(textNode);
+ /*
+ * Removing the element node that holds the selection start or end
+ * causes the selection to be lost. The fix is to detect this case
+ * and restore the selection.
+ */
+ var s = this._getSelection();
+ if ((offset <= s.start && s.start < offset + nodeLength) || (offset <= s.end && s.end < offset + nodeLength)) {
+ this._updateDOMSelection();
+ }
+ }
+ }
+ break done;
+ }
+ }
+ offset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+ if (dummy) { clientDiv.removeChild(dummy); }
+ return Math.min(lineEnd, Math.max(lineStart, offset));
+ },
+ _getYToLine: function (y) {
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ y -= viewRect.top + viewPad.top;
+ var lineHeight = this._getLineHeight();
+ var lineIndex = Math.floor((y + this._getScroll().y) / lineHeight);
+ var lineCount = this._model.getLineCount();
+ return Math.max(0, Math.min(lineCount - 1, lineIndex));
+ },
+ _getOffsetBounds: function(offset) {
+ var model = this._model;
+ var lineIndex = model.getLineAtOffset(offset);
+ var lineHeight = this._getLineHeight();
+ var scroll = this._getScroll();
+ var viewPad = this._getViewPadding();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var bounds = this._getBoundsAtOffset(offset);
+ var left = bounds.left;
+ var right = bounds.right;
+ var top = (lineIndex * lineHeight) - scroll.y + viewRect.top + viewPad.top;
+ var bottom = top + lineHeight;
+ return {left: left, top: top, right: right, bottom: bottom};
+ },
+ _getVisible: function() {
+ var temp = this._parent;
+ var parentDocument = temp.ownerDocument;
+ while (temp !== parentDocument) {
+ var hidden;
+ if (isIE < 9) {
+ hidden = temp.currentStyle && temp.currentStyle.display === "none";
+ } else {
+ var tempStyle = parentDocument.defaultView.getComputedStyle(temp, null);
+ hidden = tempStyle && tempStyle.getPropertyValue("display") === "none";
+ }
+ if (hidden) { return "hidden"; }
+ temp = temp.parentNode;
+ if (!temp) { return "disconnected"; }
+ }
+ return "visible";
+ },
+ _hitOffset: function (offset, x, y) {
+ var bounds = this._getOffsetBounds(offset);
+ var left = bounds.left;
+ var right = bounds.right;
+ var top = bounds.top;
+ var bottom = bounds.bottom;
+ var area = 20;
+ left -= area;
+ top -= area;
+ right += area;
+ bottom += area;
+ return (left <= x && x <= right && top <= y && y <= bottom);
+ },
+ _hookEvents: function() {
+ var self = this;
+ this._modelListener = {
+ /** @private */
+ onChanging: function(modelChangingEvent) {
+ self._onModelChanging(modelChangingEvent);
+ },
+ /** @private */
+ onChanged: function(modelChangedEvent) {
+ self._onModelChanged(modelChangedEvent);
+ }
+ };
+ this._model.addEventListener("Changing", this._modelListener.onChanging);
+ this._model.addEventListener("Changed", this._modelListener.onChanged);
+
+ var clientDiv = this._clientDiv;
+ var viewDiv = this._viewDiv;
+ var body = this._frameDocument.body;
+ var handlers = this._handlers = [];
+ var resizeNode = isIE < 9 ? this._frame : this._frameWindow;
+ var focusNode = isPad ? this._textArea : (isIE || isFirefox ? this._clientDiv: this._frameWindow);
+ handlers.push({target: this._frameWindow, type: "unload", handler: function(e) { return self._handleUnload(e);}});
+ handlers.push({target: resizeNode, type: "resize", handler: function(e) { return self._handleResize(e);}});
+ handlers.push({target: focusNode, type: "blur", handler: function(e) { return self._handleBlur(e);}});
+ handlers.push({target: focusNode, type: "focus", handler: function(e) { return self._handleFocus(e);}});
+ handlers.push({target: viewDiv, type: "scroll", handler: function(e) { return self._handleScroll(e);}});
+ if (isPad) {
+ var touchDiv = this._touchDiv;
+ var textArea = this._textArea;
+ handlers.push({target: textArea, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
+ handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }});
+ handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }});
+ handlers.push({target: textArea, type: "click", handler: function(e) { return self._handleTextAreaClick(e); }});
+ handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }});
+ handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }});
+ handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }});
+ } else {
+ var topNode = this._overlayDiv || this._clientDiv;
+ var grabNode = isIE ? clientDiv : this._frameWindow;
+ handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}});
+ handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}});
+ handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}});
+ handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}});
+ handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}});
+ handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}});
+ handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}});
+ handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}});
+ handlers.push({target: clientDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
+ handlers.push({target: clientDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
+ handlers.push({target: clientDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
+ handlers.push({target: grabNode, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
+ handlers.push({target: grabNode, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
+ handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}});
+ handlers.push({target: body, type: "mouseup", handler: function(e) { return self._handleBodyMouseUp(e);}});
+ handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}});
+ handlers.push({target: topNode, type: "drag", handler: function(e) { return self._handleDrag(e);}});
+ handlers.push({target: topNode, type: "dragend", handler: function(e) { return self._handleDragEnd(e);}});
+ handlers.push({target: topNode, type: "dragenter", handler: function(e) { return self._handleDragEnter(e);}});
+ handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}});
+ handlers.push({target: topNode, type: "dragleave", handler: function(e) { return self._handleDragLeave(e);}});
+ handlers.push({target: topNode, type: "drop", handler: function(e) { return self._handleDrop(e);}});
+ if (isChrome) {
+ handlers.push({target: this._parentDocument, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}});
+ handlers.push({target: this._parentDocument, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}});
+ }
+ if (isIE) {
+ handlers.push({target: this._frameDocument, type: "activate", handler: function(e) { return self._handleDocFocus(e); }});
+ }
+ if (isFirefox) {
+ handlers.push({target: this._frameDocument, type: "focus", handler: function(e) { return self._handleDocFocus(e); }});
+ }
+ if (!isIE && !isOpera) {
+ var wheelEvent = isFirefox ? "DOMMouseScroll" : "mousewheel";
+ handlers.push({target: this._viewDiv, type: wheelEvent, handler: function(e) { return self._handleMouseWheel(e); }});
+ }
+ if (isFirefox && !isWindows) {
+ handlers.push({target: this._clientDiv, type: "DOMCharacterDataModified", handler: function (e) { return self._handleDataModified(e); }});
+ }
+ if (this._overlayDiv) {
+ handlers.push({target: this._overlayDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}});
+ handlers.push({target: this._overlayDiv, type: "mouseover", handler: function(e) { return self._handleMouseOver(e);}});
+ handlers.push({target: this._overlayDiv, type: "mouseout", handler: function(e) { return self._handleMouseOut(e);}});
+ handlers.push({target: this._overlayDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e); }});
+ }
+ if (!isW3CEvents) {
+ handlers.push({target: this._clientDiv, type: "dblclick", handler: function(e) { return self._handleDblclick(e); }});
+ }
+ }
+ for (var i=0; i start) {
+ if (selection.end > start && selection.start < start + removedCharCount) {
+ // selection intersects replaced text. set caret behind text change
+ selection.setCaret(start + addedCharCount);
+ } else {
+ // move selection to keep same text selected
+ selection.start += addedCharCount - removedCharCount;
+ selection.end += addedCharCount - removedCharCount;
+ }
+ this._setSelection(selection, false, false);
+ }
+
+ var model = this._model;
+ var startLine = model.getLineAtOffset(start);
+ var child = this._getLineNext();
+ while (child) {
+ var lineIndex = child.lineIndex;
+ if (startLine <= lineIndex && lineIndex <= startLine + removedLineCount) {
+ if (startLine === lineIndex && !child.modelChangedEvent && !child.lineRemoved) {
+ child.modelChangedEvent = modelChangedEvent;
+ child.lineChanged = true;
+ } else {
+ child.lineRemoved = true;
+ child.lineChanged = false;
+ child.modelChangedEvent = null;
+ }
+ }
+ if (lineIndex > startLine + removedLineCount) {
+ child.lineIndex = lineIndex + addedLineCount - removedLineCount;
+ }
+ child = this._getLineNext(child);
+ }
+ if (startLine <= this._maxLineIndex && this._maxLineIndex <= startLine + removedLineCount) {
+ this._checkMaxLineIndex = this._maxLineIndex;
+ this._maxLineIndex = -1;
+ this._maxLineWidth = 0;
+ }
+ this._updatePage();
+ },
+ _onModelChanging: function(modelChangingEvent) {
+ modelChangingEvent.type = "ModelChanging";
+ this.onModelChanging(modelChangingEvent);
+ modelChangingEvent.type = "Changing";
+ },
+ _queueUpdatePage: function() {
+ if (this._updateTimer) { return; }
+ var self = this;
+ this._updateTimer = setTimeout(function() {
+ self._updateTimer = null;
+ self._updatePage();
+ }, 0);
+ },
+ _reset: function() {
+ this._maxLineIndex = -1;
+ this._maxLineWidth = 0;
+ this._columnX = -1;
+ this._topChild = null;
+ this._bottomChild = null;
+ this._partialY = 0;
+ this._setSelection(new Selection (0, 0, false), false, false);
+ if (this._viewDiv) {
+ this._viewDiv.scrollLeft = 0;
+ this._viewDiv.scrollTop = 0;
+ }
+ var clientDiv = this._clientDiv;
+ if (clientDiv) {
+ var child = clientDiv.firstChild;
+ while (child) {
+ child.lineRemoved = true;
+ child = child.nextSibling;
+ }
+ /*
+ * Bug in Firefox. For some reason, the caret does not show after the
+ * view is refreshed. The fix is to toggle the contentEditable state and
+ * force the clientDiv to loose and receive focus if it is focused.
+ */
+ if (isFirefox) {
+ this._ignoreFocus = false;
+ var hasFocus = this._hasFocus;
+ if (hasFocus) { clientDiv.blur(); }
+ clientDiv.contentEditable = false;
+ clientDiv.contentEditable = true;
+ if (hasFocus) { clientDiv.focus(); }
+ this._ignoreFocus = false;
+ }
+ }
+ },
+ _resizeTouchDiv: function() {
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var parentRect = this._frame.getBoundingClientRect();
+ var temp = this._frame;
+ while (temp) {
+ if (temp.style && temp.style.top) { break; }
+ temp = temp.parentNode;
+ }
+ var parentTop = parentRect.top;
+ if (temp) {
+ parentTop -= temp.getBoundingClientRect().top;
+ } else {
+ parentTop += this._parentDocument.body.scrollTop;
+ }
+ temp = this._frame;
+ while (temp) {
+ if (temp.style && temp.style.left) { break; }
+ temp = temp.parentNode;
+ }
+ var parentLeft = parentRect.left;
+ if (temp) {
+ parentLeft -= temp.getBoundingClientRect().left;
+ } else {
+ parentLeft += this._parentDocument.body.scrollLeft;
+ }
+ var touchDiv = this._touchDiv;
+ touchDiv.style.left = (parentLeft + viewRect.left) + "px";
+ touchDiv.style.top = (parentTop + viewRect.top) + "px";
+ touchDiv.style.width = viewRect.width + "px";
+ touchDiv.style.height = viewRect.height + "px";
+ },
+ _scrollView: function (pixelX, pixelY) {
+ /*
+ * Always set _ensureCaretVisible to false so that the view does not scroll
+ * to show the caret when scrollView is not called from showCaret().
+ */
+ this._ensureCaretVisible = false;
+
+ /*
+ * Scrolling is done only by setting the scrollLeft and scrollTop fields in the
+ * view div. This causes an updatePage from the scroll event. In some browsers
+ * this event is asynchronous and forcing update page to run synchronously
+ * leads to redraw problems.
+ * On Chrome 11, the view redrawing at times when holding PageDown/PageUp key.
+ * On Firefox 4 for Linux, the view redraws the first page when holding
+ * PageDown/PageUp key, but it will not redraw again until the key is released.
+ */
+ var viewDiv = this._viewDiv;
+ if (pixelX) { viewDiv.scrollLeft += pixelX; }
+ if (pixelY) { viewDiv.scrollTop += pixelY; }
+ },
+ _setClipboardText: function (text, event) {
+ var clipboardText;
+ if (this._frameWindow.clipboardData) {
+ //IE
+ clipboardText = [];
+ this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
+ return this._frameWindow.clipboardData.setData("Text", clipboardText.join(""));
+ }
+ /* Feature in Chrome, clipboardData.setData is no-op on Chrome even though it returns true */
+ if (isChrome || isFirefox || !event) {
+ var window = this._frameWindow;
+ var document = this._frameDocument;
+ var child = document.createElement("PRE");
+ child.style.position = "fixed";
+ child.style.left = "-1000px";
+ this._convertDelimiter(text,
+ function(t) {
+ child.appendChild(document.createTextNode(t));
+ },
+ function() {
+ child.appendChild(document.createElement("BR"));
+ }
+ );
+ child.appendChild(document.createTextNode(" "));
+ this._clientDiv.appendChild(child);
+ var range = document.createRange();
+ range.setStart(child.firstChild, 0);
+ range.setEndBefore(child.lastChild);
+ var sel = window.getSelection();
+ if (sel.rangeCount > 0) { sel.removeAllRanges(); }
+ sel.addRange(range);
+ var self = this;
+ /** @ignore */
+ var cleanup = function() {
+ if (child && child.parentNode === self._clientDiv) {
+ self._clientDiv.removeChild(child);
+ }
+ self._updateDOMSelection();
+ };
+ var result = false;
+ /*
+ * Try execCommand first, it works on firefox with clipboard permission,
+ * chrome 5, safari 4.
+ */
+ this._ignoreCopy = true;
+ try {
+ result = document.execCommand("copy", false, null);
+ } catch (e) {}
+ this._ignoreCopy = false;
+ if (!result) {
+ if (event) {
+ setTimeout(cleanup, 0);
+ return false;
+ }
+ }
+ /* no event and no permission, copy can not be done */
+ cleanup();
+ return true;
+ }
+ if (event && event.clipboardData) {
+ //webkit
+ clipboardText = [];
+ this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);});
+ return event.clipboardData.setData("text/plain", clipboardText.join(""));
+ }
+ },
+ _setDOMSelection: function (startNode, startOffset, endNode, endOffset) {
+ var window = this._frameWindow;
+ var document = this._frameDocument;
+ var startLineNode, startLineOffset, endLineNode, endLineOffset;
+ var offset = 0;
+ var lineChild = startNode.firstChild;
+ var node, nodeLength, model = this._model;
+ var startLineEnd = model.getLine(startNode.lineIndex).length;
+ while (lineChild) {
+ node = lineChild.firstChild;
+ nodeLength = node.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (offset + nodeLength > startOffset || offset + nodeLength >= startLineEnd) {
+ startLineNode = node;
+ startLineOffset = startOffset - offset;
+ if (lineChild.ignoreChars && nodeLength > 0 && startLineOffset === nodeLength) {
+ startLineOffset += lineChild.ignoreChars;
+ }
+ break;
+ }
+ offset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+ offset = 0;
+ lineChild = endNode.firstChild;
+ var endLineEnd = this._model.getLine(endNode.lineIndex).length;
+ while (lineChild) {
+ node = lineChild.firstChild;
+ nodeLength = node.length;
+ if (lineChild.ignoreChars) {
+ nodeLength -= lineChild.ignoreChars;
+ }
+ if (nodeLength + offset > endOffset || offset + nodeLength >= endLineEnd) {
+ endLineNode = node;
+ endLineOffset = endOffset - offset;
+ if (lineChild.ignoreChars && nodeLength > 0 && endLineOffset === nodeLength) {
+ endLineOffset += lineChild.ignoreChars;
+ }
+ break;
+ }
+ offset += nodeLength;
+ lineChild = lineChild.nextSibling;
+ }
+
+ this._setDOMFullSelection(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd);
+ if (isPad) { return; }
+
+ var range;
+ if (window.getSelection) {
+ //W3C
+ range = document.createRange();
+ range.setStart(startLineNode, startLineOffset);
+ range.setEnd(endLineNode, endLineOffset);
+ var sel = window.getSelection();
+ this._ignoreSelect = false;
+ if (sel.rangeCount > 0) { sel.removeAllRanges(); }
+ sel.addRange(range);
+ this._ignoreSelect = true;
+ } else if (document.selection) {
+ //IE < 9
+ var body = document.body;
+
+ /*
+ * Bug in IE. For some reason when text is deselected the overflow
+ * selection at the end of some lines does not get redrawn. The
+ * fix is to create a DOM element in the body to force a redraw.
+ */
+ var child = document.createElement("DIV");
+ body.appendChild(child);
+ body.removeChild(child);
+
+ range = body.createTextRange();
+ range.moveToElementText(startLineNode.parentNode);
+ range.moveStart("character", startLineOffset);
+ var endRange = body.createTextRange();
+ endRange.moveToElementText(endLineNode.parentNode);
+ endRange.moveStart("character", endLineOffset);
+ range.setEndPoint("EndToStart", endRange);
+ this._ignoreSelect = false;
+ range.select();
+ this._ignoreSelect = true;
+ }
+ },
+ _setDOMFullSelection: function(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd) {
+ var model = this._model;
+ if (this._selDiv1) {
+ var startLineBounds, l;
+ startLineBounds = this._getLineBoundingClientRect(startNode);
+ if (startOffset === 0) {
+ l = startLineBounds.left;
+ } else {
+ if (startOffset >= startLineEnd) {
+ l = startLineBounds.right;
+ } else {
+ this._ignoreDOMSelection = true;
+ l = this._getBoundsAtOffset(model.getLineStart(startNode.lineIndex) + startOffset).left;
+ this._ignoreDOMSelection = false;
+ }
+ }
+ var textArea = this._textArea;
+ if (textArea && isPad) {
+ textArea.selectionStart = textArea.selectionEnd = 0;
+ var rect = this._frame.getBoundingClientRect();
+ var touchRect = this._touchDiv.getBoundingClientRect();
+ var viewBounds = this._viewDiv.getBoundingClientRect();
+ if (!(viewBounds.left <= l && l <= viewBounds.left + viewBounds.width &&
+ viewBounds.top <= startLineBounds.top && startLineBounds.top <= viewBounds.top + viewBounds.height) ||
+ !(startNode === endNode && startOffset === endOffset))
+ {
+ textArea.style.left = "-1000px";
+ } else {
+ textArea.style.left = (l - 4 + rect.left - touchRect.left) + "px";
+ }
+ textArea.style.top = (startLineBounds.top + rect.top - touchRect.top) + "px";
+ textArea.style.width = "6px";
+ textArea.style.height = (startLineBounds.bottom - startLineBounds.top) + "px";
+ }
+
+ var selDiv = this._selDiv1;
+ selDiv.style.width = "0px";
+ selDiv.style.height = "0px";
+ selDiv = this._selDiv2;
+ selDiv.style.width = "0px";
+ selDiv.style.height = "0px";
+ selDiv = this._selDiv3;
+ selDiv.style.width = "0px";
+ selDiv.style.height = "0px";
+ if (!(startNode === endNode && startOffset === endOffset)) {
+ var handleWidth = isPad ? 2 : 0;
+ var handleBorder = handleWidth + "px blue solid";
+ var viewPad = this._getViewPadding();
+ var clientRect = this._clientDiv.getBoundingClientRect();
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ var left = viewRect.left + viewPad.left;
+ var right = clientRect.right;
+ var top = viewRect.top + viewPad.top;
+ var bottom = clientRect.bottom;
+ var hd = 0, vd = 0;
+ if (this._clipDiv) {
+ var clipRect = this._clipDiv.getBoundingClientRect();
+ hd = clipRect.left - this._clipDiv.scrollLeft;
+ vd = clipRect.top;
+ }
+ var r;
+ var endLineBounds = this._getLineBoundingClientRect(endNode);
+ if (endOffset === 0) {
+ r = endLineBounds.left;
+ } else {
+ if (endOffset >= endLineEnd) {
+ r = endLineBounds.right;
+ } else {
+ this._ignoreDOMSelection = true;
+ r = this._getBoundsAtOffset(model.getLineStart(endNode.lineIndex) + endOffset).left;
+ this._ignoreDOMSelection = false;
+ }
+ }
+ var sel1Div = this._selDiv1;
+ var sel1Left = Math.min(right, Math.max(left, l));
+ var sel1Top = Math.min(bottom, Math.max(top, startLineBounds.top));
+ var sel1Right = right;
+ var sel1Bottom = Math.min(bottom, Math.max(top, startLineBounds.bottom));
+ sel1Div.style.left = (sel1Left - hd) + "px";
+ sel1Div.style.top = (sel1Top - vd) + "px";
+ sel1Div.style.width = Math.max(0, sel1Right - sel1Left) + "px";
+ sel1Div.style.height = Math.max(0, sel1Bottom - sel1Top) + (isPad ? 1 : 0) + "px";
+ if (isPad) {
+ sel1Div.style.borderLeft = handleBorder;
+ sel1Div.style.borderRight = "0px";
+ }
+ if (startNode === endNode) {
+ sel1Right = Math.min(r, right);
+ sel1Div.style.width = Math.max(0, sel1Right - sel1Left - handleWidth * 2) + "px";
+ if (isPad) {
+ sel1Div.style.borderRight = handleBorder;
+ }
+ } else {
+ var sel3Left = left;
+ var sel3Top = Math.min(bottom, Math.max(top, endLineBounds.top));
+ var sel3Right = Math.min(right, Math.max(left, r));
+ var sel3Bottom = Math.min(bottom, Math.max(top, endLineBounds.bottom));
+ var sel3Div = this._selDiv3;
+ sel3Div.style.left = (sel3Left - hd) + "px";
+ sel3Div.style.top = (sel3Top - vd) + "px";
+ sel3Div.style.width = Math.max(0, sel3Right - sel3Left - handleWidth) + "px";
+ sel3Div.style.height = Math.max(0, sel3Bottom - sel3Top) + "px";
+ if (isPad) {
+ sel3Div.style.borderRight = handleBorder;
+ }
+ if (sel3Top - sel1Bottom > 0) {
+ var sel2Div = this._selDiv2;
+ sel2Div.style.left = (left - hd) + "px";
+ sel2Div.style.top = (sel1Bottom - vd) + "px";
+ sel2Div.style.width = Math.max(0, right - left) + "px";
+ sel2Div.style.height = Math.max(0, sel3Top - sel1Bottom) + (isPad ? 1 : 0) + "px";
+ }
+ }
+ }
+ }
+ },
+ _setGrab: function (target) {
+ if (target === this._grabControl) { return; }
+ if (target) {
+ if (target.setCapture) { target.setCapture(); }
+ this._grabControl = target;
+ } else {
+ if (this._grabControl.releaseCapture) { this._grabControl.releaseCapture(); }
+ this._grabControl = null;
+ }
+ },
+ _setLinksVisible: function(visible) {
+ if (this._linksVisible === visible) { return; }
+ this._linksVisible = visible;
+ /*
+ * Feature in IE. The client div looses focus and does not regain it back
+ * when the content editable flag is reset. The fix is to remember that it
+ * had focus when the flag is cleared and give focus back to the div when
+ * the flag is set.
+ */
+ if (isIE && visible) {
+ this._hadFocus = this._hasFocus;
+ }
+ var clientDiv = this._clientDiv;
+ clientDiv.contentEditable = !visible;
+ if (this._hadFocus && !visible) {
+ clientDiv.focus();
+ }
+ if (this._overlayDiv) {
+ this._overlayDiv.style.zIndex = visible ? "-1" : "1";
+ }
+ var document = this._frameDocument;
+ var line = this._getLineNext();
+ while (line) {
+ if (line.hasLink) {
+ var lineChild = line.firstChild;
+ while (lineChild) {
+ var next = lineChild.nextSibling;
+ var style = lineChild.viewStyle;
+ if (style && style.tagName === "A") {
+ line.replaceChild(this._createSpan(line, document, lineChild.firstChild.data, style), lineChild);
+ }
+ lineChild = next;
+ }
+ }
+ line = this._getLineNext(line);
+ }
+ },
+ _setSelection: function (selection, scroll, update, pageScroll) {
+ if (selection) {
+ this._columnX = -1;
+ if (update === undefined) { update = true; }
+ var oldSelection = this._selection;
+ if (!oldSelection.equals(selection)) {
+ this._selection = selection;
+ var e = {
+ type: "Selection",
+ oldValue: {start:oldSelection.start, end:oldSelection.end},
+ newValue: {start:selection.start, end:selection.end}
+ };
+ this.onSelection(e);
+ }
+ /*
+ * Always showCaret(), even when the selection is not changing, to ensure the
+ * caret is visible. Note that some views do not scroll to show the caret during
+ * keyboard navigation when the selection does not chanage. For example, line down
+ * when the caret is already at the last line.
+ */
+ if (scroll) { update = !this._showCaret(false, pageScroll); }
+
+ /*
+ * Sometimes the browser changes the selection
+ * as result of method calls or "leaked" events.
+ * The fix is to set the visual selection even
+ * when the logical selection is not changed.
+ */
+ if (update) { this._updateDOMSelection(); }
+ }
+ },
+ _setSelectionTo: function (x, y, extent, drag) {
+ var model = this._model, offset;
+ var selection = this._getSelection();
+ var lineIndex = this._getYToLine(y);
+ if (this._clickCount === 1) {
+ offset = this._getXToOffset(lineIndex, x);
+ if (drag && !extent) {
+ if (selection.start <= offset && offset < selection.end) {
+ this._dragOffset = offset;
+ return false;
+ }
+ }
+ selection.extend(offset);
+ if (!extent) { selection.collapse(); }
+ } else {
+ var word = (this._clickCount & 1) === 0;
+ var start, end;
+ if (word) {
+ offset = this._getXToOffset(lineIndex, x);
+ if (this._doubleClickSelection) {
+ if (offset >= this._doubleClickSelection.start) {
+ start = this._doubleClickSelection.start;
+ end = this._getOffset(offset, "wordend", +1);
+ } else {
+ start = this._getOffset(offset, "word", -1);
+ end = this._doubleClickSelection.end;
+ }
+ } else {
+ start = this._getOffset(offset, "word", -1);
+ end = this._getOffset(start, "wordend", +1);
+ }
+ } else {
+ if (this._doubleClickSelection) {
+ var doubleClickLine = model.getLineAtOffset(this._doubleClickSelection.start);
+ if (lineIndex >= doubleClickLine) {
+ start = model.getLineStart(doubleClickLine);
+ end = model.getLineEnd(lineIndex);
+ } else {
+ start = model.getLineStart(lineIndex);
+ end = model.getLineEnd(doubleClickLine);
+ }
+ } else {
+ start = model.getLineStart(lineIndex);
+ end = model.getLineEnd(lineIndex);
+ }
+ }
+ selection.setCaret(start);
+ selection.extend(end);
+ }
+ this._setSelection(selection, true, true);
+ return true;
+ },
+ _setStyleSheet: function(stylesheet) {
+ var oldstylesheet = this._stylesheet;
+ if (!(oldstylesheet instanceof Array)) {
+ oldstylesheet = [oldstylesheet];
+ }
+ this._stylesheet = stylesheet;
+ if (!(stylesheet instanceof Array)) {
+ stylesheet = [stylesheet];
+ }
+ var document = this._frameDocument;
+ var documentStylesheet = document.styleSheets;
+ var head = document.getElementsByTagName("head")[0];
+ var changed = false;
+ var i = 0, sheet, oldsheet, documentSheet, ownerNode, styleNode, textNode;
+ while (i < stylesheet.length) {
+ if (i >= oldstylesheet.length) { break; }
+ sheet = stylesheet[i];
+ oldsheet = oldstylesheet[i];
+ if (sheet !== oldsheet) {
+ if (this._isLinkURL(sheet)) {
+ return true;
+ } else {
+ documentSheet = documentStylesheet[i+1];
+ ownerNode = documentSheet.ownerNode;
+ styleNode = document.createElement('STYLE');
+ textNode = document.createTextNode(sheet);
+ styleNode.appendChild(textNode);
+ head.replaceChild(styleNode, ownerNode);
+ changed = true;
+ }
+ }
+ i++;
+ }
+ if (i < oldstylesheet.length) {
+ while (i < oldstylesheet.length) {
+ sheet = oldstylesheet[i];
+ if (this._isLinkURL(sheet)) {
+ return true;
+ } else {
+ documentSheet = documentStylesheet[i+1];
+ ownerNode = documentSheet.ownerNode;
+ head.removeChild(ownerNode);
+ changed = true;
+ }
+ i++;
+ }
+ } else {
+ while (i < stylesheet.length) {
+ sheet = stylesheet[i];
+ if (this._isLinkURL(sheet)) {
+ return true;
+ } else {
+ styleNode = document.createElement('STYLE');
+ textNode = document.createTextNode(sheet);
+ styleNode.appendChild(textNode);
+ head.appendChild(styleNode);
+ changed = true;
+ }
+ i++;
+ }
+ }
+ if (changed) {
+ this._updateStyle();
+ }
+ return false;
+ },
+ _setFullSelection: function(fullSelection, init) {
+ this._fullSelection = fullSelection;
+
+ /*
+ * Bug in IE 8. For some reason, during scrolling IE does not reflow the elements
+ * that are used to compute the location for the selection divs. This causes the
+ * divs to be placed at the wrong location. The fix is to disabled full selection for IE8.
+ */
+ if (isIE < 9) {
+ this._fullSelection = false;
+ }
+ if (isWebkit) {
+ this._fullSelection = true;
+ }
+ var parent = this._clipDiv || this._scrollDiv;
+ if (!parent) {
+ return;
+ }
+ if (!isPad && !this._fullSelection) {
+ if (this._selDiv1) {
+ parent.removeChild(this._selDiv1);
+ this._selDiv1 = null;
+ }
+ if (this._selDiv2) {
+ parent.removeChild(this._selDiv2);
+ this._selDiv2 = null;
+ }
+ if (this._selDiv3) {
+ parent.removeChild(this._selDiv3);
+ this._selDiv3 = null;
+ }
+ return;
+ }
+
+ if (!this._selDiv1 && (isPad || (this._fullSelection && !isWebkit))) {
+ var frameDocument = this._frameDocument;
+ this._hightlightRGB = "Highlight";
+ var selDiv1 = frameDocument.createElement("DIV");
+ this._selDiv1 = selDiv1;
+ selDiv1.id = "selDiv1";
+ selDiv1.style.position = this._clipDiv ? "absolute" : "fixed";
+ selDiv1.style.borderWidth = "0px";
+ selDiv1.style.margin = "0px";
+ selDiv1.style.padding = "0px";
+ selDiv1.style.outline = "none";
+ selDiv1.style.background = this._hightlightRGB;
+ selDiv1.style.width = "0px";
+ selDiv1.style.height = "0px";
+ selDiv1.style.zIndex = "0";
+ parent.appendChild(selDiv1);
+ var selDiv2 = frameDocument.createElement("DIV");
+ this._selDiv2 = selDiv2;
+ selDiv2.id = "selDiv2";
+ selDiv2.style.position = this._clipDiv ? "absolute" : "fixed";
+ selDiv2.style.borderWidth = "0px";
+ selDiv2.style.margin = "0px";
+ selDiv2.style.padding = "0px";
+ selDiv2.style.outline = "none";
+ selDiv2.style.background = this._hightlightRGB;
+ selDiv2.style.width = "0px";
+ selDiv2.style.height = "0px";
+ selDiv2.style.zIndex = "0";
+ parent.appendChild(selDiv2);
+ var selDiv3 = frameDocument.createElement("DIV");
+ this._selDiv3 = selDiv3;
+ selDiv3.id = "selDiv3";
+ selDiv3.style.position = this._clipDiv ? "absolute" : "fixed";
+ selDiv3.style.borderWidth = "0px";
+ selDiv3.style.margin = "0px";
+ selDiv3.style.padding = "0px";
+ selDiv3.style.outline = "none";
+ selDiv3.style.background = this._hightlightRGB;
+ selDiv3.style.width = "0px";
+ selDiv3.style.height = "0px";
+ selDiv3.style.zIndex = "0";
+ parent.appendChild(selDiv3);
+
+ /*
+ * Bug in Firefox. The Highlight color is mapped to list selection
+ * background instead of the text selection background. The fix
+ * is to map known colors using a table or fallback to light blue.
+ */
+ if (isFirefox && isMac) {
+ var style = this._frameWindow.getComputedStyle(selDiv3, null);
+ var rgb = style.getPropertyValue("background-color");
+ switch (rgb) {
+ case "rgb(119, 141, 168)": rgb = "rgb(199, 208, 218)"; break;
+ case "rgb(127, 127, 127)": rgb = "rgb(198, 198, 198)"; break;
+ case "rgb(255, 193, 31)": rgb = "rgb(250, 236, 115)"; break;
+ case "rgb(243, 70, 72)": rgb = "rgb(255, 176, 139)"; break;
+ case "rgb(255, 138, 34)": rgb = "rgb(255, 209, 129)"; break;
+ case "rgb(102, 197, 71)": rgb = "rgb(194, 249, 144)"; break;
+ case "rgb(140, 78, 184)": rgb = "rgb(232, 184, 255)"; break;
+ default: rgb = "rgb(180, 213, 255)"; break;
+ }
+ this._hightlightRGB = rgb;
+ selDiv1.style.background = rgb;
+ selDiv2.style.background = rgb;
+ selDiv3.style.background = rgb;
+ if (!this._insertedSelRule) {
+ var styleSheet = frameDocument.styleSheets[0];
+ styleSheet.insertRule("::-moz-selection {background: " + rgb + "; }", 0);
+ this._insertedSelRule = true;
+ }
+ }
+ if (!init) {
+ this._updateDOMSelection();
+ }
+ }
+ },
+ _setTabSize: function (tabSize, init) {
+ this._tabSize = tabSize;
+ this._customTabSize = undefined;
+ var clientDiv = this._clientDiv;
+ if (isOpera) {
+ if (clientDiv) { clientDiv.style.OTabSize = this._tabSize+""; }
+ } else if (isFirefox >= 4) {
+ if (clientDiv) { clientDiv.style.MozTabSize = this._tabSize+""; }
+ } else if (this._tabSize !== 8) {
+ this._customTabSize = this._tabSize;
+ if (!init) {
+ this.redrawLines();
+ }
+ }
+ },
+ _setThemeClass: function (themeClass, init) {
+ this._themeClass = themeClass;
+ var document = this._frameDocument;
+ if (document) {
+ var viewContainerClass = "viewContainer";
+ if (this._themeClass) { viewContainerClass += " " + this._themeClass; }
+ document.body.className = viewContainerClass;
+ if (!init) {
+ this._updateStyle();
+ }
+ }
+ },
+ _showCaret: function (allSelection, pageScroll) {
+ if (!this._clientDiv) { return; }
+ var model = this._model;
+ var selection = this._getSelection();
+ var scroll = this._getScroll();
+ var caret = selection.getCaret();
+ var start = selection.start;
+ var end = selection.end;
+ var startLine = model.getLineAtOffset(start);
+ var endLine = model.getLineAtOffset(end);
+ var endInclusive = Math.max(Math.max(start, model.getLineStart(endLine)), end - 1);
+ var viewPad = this._getViewPadding();
+
+ var clientWidth = this._getClientWidth();
+ var leftEdge = viewPad.left;
+ var rightEdge = viewPad.left + clientWidth;
+ var bounds = this._getBoundsAtOffset(caret === start ? start : endInclusive);
+ var left = bounds.left;
+ var right = bounds.right;
+ var minScroll = clientWidth / 4;
+ if (allSelection && !selection.isEmpty() && startLine === endLine) {
+ bounds = this._getBoundsAtOffset(caret === end ? start : endInclusive);
+ var selectionWidth = caret === start ? bounds.right - left : right - bounds.left;
+ if ((clientWidth - minScroll) > selectionWidth) {
+ if (left > bounds.left) { left = bounds.left; }
+ if (right < bounds.right) { right = bounds.right; }
+ }
+ }
+ var viewRect = this._viewDiv.getBoundingClientRect();
+ left -= viewRect.left;
+ right -= viewRect.left;
+ var pixelX = 0;
+ if (left < leftEdge) {
+ pixelX = Math.min(left - leftEdge, -minScroll);
+ }
+ if (right > rightEdge) {
+ var maxScroll = this._scrollDiv.scrollWidth - scroll.x - clientWidth;
+ pixelX = Math.min(maxScroll, Math.max(right - rightEdge, minScroll));
+ }
+
+ var pixelY = 0;
+ var topIndex = this._getTopIndex(true);
+ var bottomIndex = this._getBottomIndex(true);
+ var caretLine = model.getLineAtOffset(caret);
+ var clientHeight = this._getClientHeight();
+ if (!(topIndex <= caretLine && caretLine <= bottomIndex)) {
+ var lineHeight = this._getLineHeight();
+ var selectionHeight = allSelection ? (endLine - startLine) * lineHeight : 0;
+ pixelY = caretLine * lineHeight;
+ pixelY -= scroll.y;
+ if (pixelY + lineHeight > clientHeight) {
+ pixelY -= clientHeight - lineHeight;
+ if (caret === start && start !== end) {
+ pixelY += Math.min(clientHeight - lineHeight, selectionHeight);
+ }
+ } else {
+ if (caret === end) {
+ pixelY -= Math.min (clientHeight - lineHeight, selectionHeight);
+ }
+ }
+ if (pageScroll) {
+ if (pageScroll > 0) {
+ if (pixelY > 0) {
+ pixelY = Math.max(pixelY, pageScroll);
+ }
+ } else {
+ if (pixelY < 0) {
+ pixelY = Math.min(pixelY, pageScroll);
+ }
+ }
+ }
+ }
+
+ if (pixelX !== 0 || pixelY !== 0) {
+ this._scrollView (pixelX, pixelY);
+ /*
+ * When the view scrolls it is possible that one of the scrollbars can show over the caret.
+ * Depending on the browser scrolling can be synchronous (Safari), in which case the change
+ * can be detected before showCaret() returns. When scrolling is asynchronous (most browsers),
+ * the detection is done during the next update page.
+ */
+ if (clientHeight !== this._getClientHeight() || clientWidth !== this._getClientWidth()) {
+ this._showCaret();
+ } else {
+ this._ensureCaretVisible = true;
+ }
+ return true;
+ }
+ return false;
+ },
+ _startIME: function () {
+ if (this._imeOffset !== -1) { return; }
+ var selection = this._getSelection();
+ if (!selection.isEmpty()) {
+ this._modifyContent({text: "", start: selection.start, end: selection.end}, true);
+ }
+ this._imeOffset = selection.start;
+ },
+ _unhookEvents: function() {
+ this._model.removeEventListener("Changing", this._modelListener.onChanging);
+ this._model.removeEventListener("Changed", this._modelListener.onChanged);
+ this._modelListener = null;
+ for (var i=0; i lastNode.lineIndex) {
+ topNode = lastNode;
+ topOffset = 0;
+ } else {
+ topNode = this._getLineNode(startLine);
+ topOffset = selection.start - model.getLineStart(startLine);
+ }
+
+ if (endLine < firstNode.lineIndex) {
+ bottomNode = firstNode;
+ bottomOffset = 0;
+ } else if (endLine > lastNode.lineIndex) {
+ bottomNode = lastNode;
+ bottomOffset = 0;
+ } else {
+ bottomNode = this._getLineNode(endLine);
+ bottomOffset = selection.end - model.getLineStart(endLine);
+ }
+ this._setDOMSelection(topNode, topOffset, bottomNode, bottomOffset);
+ },
+ _updatePage: function(hScrollOnly) {
+ if (this._redrawCount > 0) { return; }
+ if (this._updateTimer) {
+ clearTimeout(this._updateTimer);
+ this._updateTimer = null;
+ hScrollOnly = false;
+ }
+ var clientDiv = this._clientDiv;
+ if (!clientDiv) { return; }
+ var model = this._model;
+ var scroll = this._getScroll();
+ var viewPad = this._getViewPadding();
+ var lineCount = model.getLineCount();
+ var lineHeight = this._getLineHeight();
+ var firstLine = Math.max(0, scroll.y) / lineHeight;
+ var topIndex = Math.floor(firstLine);
+ var lineStart = Math.max(0, topIndex - 1);
+ var top = Math.round((firstLine - lineStart) * lineHeight);
+ var partialY = this._partialY = Math.round((firstLine - topIndex) * lineHeight);
+ var scrollWidth, scrollHeight = lineCount * lineHeight;
+ var leftWidth, clientWidth, clientHeight;
+ if (hScrollOnly) {
+ clientWidth = this._getClientWidth();
+ clientHeight = this._getClientHeight();
+ leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
+ scrollWidth = Math.max(this._maxLineWidth, clientWidth);
+ } else {
+ var document = this._frameDocument;
+ var frameWidth = this._getFrameWidth();
+ var frameHeight = this._getFrameHeight();
+ document.body.style.width = frameWidth + "px";
+ document.body.style.height = frameHeight + "px";
+
+ /* Update view height in order to have client height computed */
+ var viewDiv = this._viewDiv;
+ viewDiv.style.height = Math.max(0, (frameHeight - viewPad.top - viewPad.bottom)) + "px";
+ clientHeight = this._getClientHeight();
+ var linesPerPage = Math.floor((clientHeight + partialY) / lineHeight);
+ var bottomIndex = Math.min(topIndex + linesPerPage, lineCount - 1);
+ var lineEnd = Math.min(bottomIndex + 1, lineCount - 1);
+
+ var lineIndex, lineWidth;
+ var child = clientDiv.firstChild;
+ while (child) {
+ lineIndex = child.lineIndex;
+ var nextChild = child.nextSibling;
+ if (!(lineStart <= lineIndex && lineIndex <= lineEnd) || child.lineRemoved || child.lineIndex === -1) {
+ if (this._mouseWheelLine === child) {
+ child.style.display = "none";
+ child.lineIndex = -1;
+ } else {
+ clientDiv.removeChild(child);
+ }
+ }
+ child = nextChild;
+ }
+
+ child = this._getLineNext();
+ var frag = document.createDocumentFragment();
+ for (lineIndex=lineStart; lineIndex<=lineEnd; lineIndex++) {
+ if (!child || child.lineIndex > lineIndex) {
+ this._createLine(frag, null, document, lineIndex, model);
+ } else {
+ if (frag.firstChild) {
+ clientDiv.insertBefore(frag, child);
+ frag = document.createDocumentFragment();
+ }
+ if (child && child.lineChanged) {
+ child = this._createLine(frag, child, document, lineIndex, model);
+ child.lineChanged = false;
+ }
+ child = this._getLineNext(child);
+ }
+ }
+ if (frag.firstChild) { clientDiv.insertBefore(frag, child); }
+
+ /*
+ * Feature in WekKit. Webkit limits the width of the lines
+ * computed below to the width of the client div. This causes
+ * the lines to be wrapped even though "pre" is set. The fix
+ * is to set the width of the client div to a larger number
+ * before computing the lines width. Note that this value is
+ * reset to the appropriate value further down.
+ */
+ if (isWebkit) {
+ clientDiv.style.width = (0x7FFFF).toString() + "px";
+ }
+
+ var rect;
+ child = this._getLineNext();
+ while (child) {
+ lineWidth = child.lineWidth;
+ if (lineWidth === undefined) {
+ rect = this._getLineBoundingClientRect(child);
+ lineWidth = child.lineWidth = rect.right - rect.left;
+ }
+ if (lineWidth >= this._maxLineWidth) {
+ this._maxLineWidth = lineWidth;
+ this._maxLineIndex = child.lineIndex;
+ }
+ if (child.lineIndex === topIndex) { this._topChild = child; }
+ if (child.lineIndex === bottomIndex) { this._bottomChild = child; }
+ if (this._checkMaxLineIndex === child.lineIndex) { this._checkMaxLineIndex = -1; }
+ child = this._getLineNext(child);
+ }
+ if (this._checkMaxLineIndex !== -1) {
+ lineIndex = this._checkMaxLineIndex;
+ this._checkMaxLineIndex = -1;
+ if (0 <= lineIndex && lineIndex < lineCount) {
+ var dummy = this._createLine(clientDiv, null, document, lineIndex, model);
+ rect = this._getLineBoundingClientRect(dummy);
+ lineWidth = rect.right - rect.left;
+ if (lineWidth >= this._maxLineWidth) {
+ this._maxLineWidth = lineWidth;
+ this._maxLineIndex = lineIndex;
+ }
+ clientDiv.removeChild(dummy);
+ }
+ }
+
+ // Update rulers
+ this._updateRuler(this._leftDiv, topIndex, bottomIndex);
+ this._updateRuler(this._rightDiv, topIndex, bottomIndex);
+
+ leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0;
+ var rightWidth = this._rightDiv ? this._rightDiv.scrollWidth : 0;
+ viewDiv.style.left = leftWidth + "px";
+ viewDiv.style.width = Math.max(0, frameWidth - leftWidth - rightWidth - viewPad.left - viewPad.right) + "px";
+ if (this._rightDiv) {
+ this._rightDiv.style.left = (frameWidth - rightWidth) + "px";
+ }
+ /* Need to set the height first in order for the width to consider the vertical scrollbar */
+ var scrollDiv = this._scrollDiv;
+ scrollDiv.style.height = scrollHeight + "px";
+ /*
+ * TODO if frameHeightWithoutHScrollbar < scrollHeight < frameHeightWithHScrollbar and the horizontal bar is visible,
+ * then the clientWidth is wrong because the vertical scrollbar is showing. To correct code should hide both scrollbars
+ * at this point.
+ */
+ clientWidth = this._getClientWidth();
+ var width = Math.max(this._maxLineWidth, clientWidth);
+ /*
+ * Except by IE 8 and earlier, all other browsers are not allocating enough space for the right padding
+ * in the scrollbar. It is possible this a bug since all other paddings are considered.
+ */
+ scrollWidth = width;
+ if (!isIE || isIE >= 9) { width += viewPad.right; }
+ scrollDiv.style.width = width + "px";
+ if (this._clipScrollDiv) {
+ this._clipScrollDiv.style.width = width + "px";
+ }
+ /* Get the left scroll after setting the width of the scrollDiv as this can change the horizontal scroll offset. */
+ scroll = this._getScroll();
+ var rulerHeight = clientHeight + viewPad.top + viewPad.bottom;
+ this._updateRulerSize(this._leftDiv, rulerHeight);
+ this._updateRulerSize(this._rightDiv, rulerHeight);
+ }
+ var left = scroll.x;
+ var clipDiv = this._clipDiv;
+ var overlayDiv = this._overlayDiv;
+ var clipLeft, clipTop;
+ if (clipDiv) {
+ clipDiv.scrollLeft = left;
+ clipLeft = leftWidth + viewPad.left;
+ clipTop = viewPad.top;
+ var clipWidth = clientWidth;
+ var clipHeight = clientHeight;
+ var clientLeft = 0, clientTop = -top;
+ if (scroll.x === 0) {
+ clipLeft -= viewPad.left;
+ clipWidth += viewPad.left;
+ clientLeft = viewPad.left;
+ }
+ if (scroll.x + clientWidth === scrollWidth) {
+ clipWidth += viewPad.right;
+ }
+ if (scroll.y === 0) {
+ clipTop -= viewPad.top;
+ clipHeight += viewPad.top;
+ clientTop += viewPad.top;
+ }
+ if (scroll.y + clientHeight === scrollHeight) {
+ clipHeight += viewPad.bottom;
+ }
+ clipDiv.style.left = clipLeft + "px";
+ clipDiv.style.top = clipTop + "px";
+ clipDiv.style.width = clipWidth + "px";
+ clipDiv.style.height = clipHeight + "px";
+ clientDiv.style.left = clientLeft + "px";
+ clientDiv.style.top = clientTop + "px";
+ clientDiv.style.width = scrollWidth + "px";
+ clientDiv.style.height = (clientHeight + top) + "px";
+ if (overlayDiv) {
+ overlayDiv.style.left = clientDiv.style.left;
+ overlayDiv.style.top = clientDiv.style.top;
+ overlayDiv.style.width = clientDiv.style.width;
+ overlayDiv.style.height = clientDiv.style.height;
+ }
+ } else {
+ clipLeft = left;
+ clipTop = top;
+ var clipRight = left + clientWidth;
+ var clipBottom = top + clientHeight;
+ if (clipLeft === 0) { clipLeft -= viewPad.left; }
+ if (clipTop === 0) { clipTop -= viewPad.top; }
+ if (clipRight === scrollWidth) { clipRight += viewPad.right; }
+ if (scroll.y + clientHeight === scrollHeight) { clipBottom += viewPad.bottom; }
+ clientDiv.style.clip = "rect(" + clipTop + "px," + clipRight + "px," + clipBottom + "px," + clipLeft + "px)";
+ clientDiv.style.left = (-left + leftWidth + viewPad.left) + "px";
+ clientDiv.style.width = (isWebkit ? scrollWidth : clientWidth + left) + "px";
+ if (!hScrollOnly) {
+ clientDiv.style.top = (-top + viewPad.top) + "px";
+ clientDiv.style.height = (clientHeight + top) + "px";
+ }
+ if (overlayDiv) {
+ overlayDiv.style.clip = clientDiv.style.clip;
+ overlayDiv.style.left = clientDiv.style.left;
+ overlayDiv.style.width = clientDiv.style.width;
+ if (!hScrollOnly) {
+ overlayDiv.style.top = clientDiv.style.top;
+ overlayDiv.style.height = clientDiv.style.height;
+ }
+ }
+ }
+ this._updateDOMSelection();
+
+ /*
+ * If the client height changed during the update page it means that scrollbar has either been shown or hidden.
+ * When this happens update page has to run again to ensure that the top and bottom lines div are correct.
+ *
+ * Note: On IE, updateDOMSelection() has to be called before getting the new client height because it
+ * forces the client area to be recomputed.
+ */
+ var ensureCaretVisible = this._ensureCaretVisible;
+ this._ensureCaretVisible = false;
+ if (clientHeight !== this._getClientHeight()) {
+ this._updatePage();
+ if (ensureCaretVisible) {
+ this._showCaret();
+ }
+ }
+ if (isPad) {
+ var self = this;
+ setTimeout(function() {self._resizeTouchDiv();}, 0);
+ }
+ },
+ _updateRulerSize: function (divRuler, rulerHeight) {
+ if (!divRuler) { return; }
+ var partialY = this._partialY;
+ var lineHeight = this._getLineHeight();
+ var cells = divRuler.firstChild.rows[0].cells;
+ for (var i = 0; i < cells.length; i++) {
+ var div = cells[i].firstChild;
+ var offset = lineHeight;
+ if (div._ruler.getOverview() === "page") { offset += partialY; }
+ div.style.top = -offset + "px";
+ div.style.height = (rulerHeight + offset) + "px";
+ div = div.nextSibling;
+ }
+ divRuler.style.height = rulerHeight + "px";
+ },
+ _updateRuler: function (divRuler, topIndex, bottomIndex) {
+ if (!divRuler) { return; }
+ var cells = divRuler.firstChild.rows[0].cells;
+ var lineHeight = this._getLineHeight();
+ var parentDocument = this._frameDocument;
+ var viewPad = this._getViewPadding();
+ for (var i = 0; i < cells.length; i++) {
+ var div = cells[i].firstChild;
+ var ruler = div._ruler;
+ if (div.rulerChanged) {
+ this._applyStyle(ruler.getRulerStyle(), div);
+ }
+
+ var widthDiv;
+ var child = div.firstChild;
+ if (child) {
+ widthDiv = child;
+ child = child.nextSibling;
+ } else {
+ widthDiv = parentDocument.createElement("DIV");
+ widthDiv.style.visibility = "hidden";
+ div.appendChild(widthDiv);
+ }
+ var lineIndex, annotation;
+ if (div.rulerChanged) {
+ if (widthDiv) {
+ lineIndex = -1;
+ annotation = ruler.getWidestAnnotation();
+ if (annotation) {
+ this._applyStyle(annotation.style, widthDiv);
+ if (annotation.html) {
+ widthDiv.innerHTML = annotation.html;
+ }
+ }
+ widthDiv.lineIndex = lineIndex;
+ widthDiv.style.height = (lineHeight + viewPad.top) + "px";
+ }
+ }
+
+ var overview = ruler.getOverview(), lineDiv, frag, annotations;
+ if (overview === "page") {
+ annotations = ruler.getAnnotations(topIndex, bottomIndex + 1);
+ while (child) {
+ lineIndex = child.lineIndex;
+ var nextChild = child.nextSibling;
+ if (!(topIndex <= lineIndex && lineIndex <= bottomIndex) || child.lineChanged) {
+ div.removeChild(child);
+ }
+ child = nextChild;
+ }
+ child = div.firstChild.nextSibling;
+ frag = parentDocument.createDocumentFragment();
+ for (lineIndex=topIndex; lineIndex<=bottomIndex; lineIndex++) {
+ if (!child || child.lineIndex > lineIndex) {
+ lineDiv = parentDocument.createElement("DIV");
+ annotation = annotations[lineIndex];
+ if (annotation) {
+ this._applyStyle(annotation.style, lineDiv);
+ if (annotation.html) {
+ lineDiv.innerHTML = annotation.html;
+ }
+ lineDiv.annotation = annotation;
+ }
+ lineDiv.lineIndex = lineIndex;
+ lineDiv.style.height = lineHeight + "px";
+ frag.appendChild(lineDiv);
+ } else {
+ if (frag.firstChild) {
+ div.insertBefore(frag, child);
+ frag = parentDocument.createDocumentFragment();
+ }
+ if (child) {
+ child = child.nextSibling;
+ }
+ }
+ }
+ if (frag.firstChild) { div.insertBefore(frag, child); }
+ } else {
+ var buttonHeight = isPad ? 0 : 17;
+ var clientHeight = this._getClientHeight ();
+ var lineCount = this._model.getLineCount ();
+ var contentHeight = lineHeight * lineCount;
+ var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight;
+ var divHeight;
+ if (contentHeight < trackHeight) {
+ divHeight = lineHeight;
+ } else {
+ divHeight = trackHeight / lineCount;
+ }
+ if (div.rulerChanged) {
+ var count = div.childNodes.length;
+ while (count > 1) {
+ div.removeChild(div.lastChild);
+ count--;
+ }
+ annotations = ruler.getAnnotations(0, lineCount);
+ frag = parentDocument.createDocumentFragment();
+ for (var prop in annotations) {
+ lineIndex = prop >>> 0;
+ if (lineIndex < 0) { continue; }
+ lineDiv = parentDocument.createElement("DIV");
+ annotation = annotations[prop];
+ this._applyStyle(annotation.style, lineDiv);
+ lineDiv.style.position = "absolute";
+ lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineIndex * divHeight) + "px";
+ if (annotation.html) {
+ lineDiv.innerHTML = annotation.html;
+ }
+ lineDiv.annotation = annotation;
+ lineDiv.lineIndex = lineIndex;
+ frag.appendChild(lineDiv);
+ }
+ div.appendChild(frag);
+ } else if (div._oldTrackHeight !== trackHeight) {
+ lineDiv = div.firstChild ? div.firstChild.nextSibling : null;
+ while (lineDiv) {
+ lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineDiv.lineIndex * divHeight) + "px";
+ lineDiv = lineDiv.nextSibling;
+ }
+ }
+ div._oldTrackHeight = trackHeight;
+ }
+ div.rulerChanged = false;
+ div = div.nextSibling;
+ }
+ },
+ _updateStyle: function () {
+ var document = this._frameDocument;
+ if (isIE) {
+ document.body.style.lineHeight = "normal";
+ }
+ this._lineHeight = this._calculateLineHeight();
+ this._viewPadding = this._calculatePadding();
+ if (isIE) {
+ document.body.style.lineHeight = this._lineHeight + "px";
+ }
+ this.redraw();
+ }
+ };//end prototype
+ mEventTarget.EventTarget.addMixin(TextView.prototype);
+
+ return {TextView: TextView};
+});
+
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors:
+ * Felipe Heidrich (IBM Corporation) - initial API and implementation
+ * Silenio Quarti (IBM Corporation) - initial API and implementation
+ ******************************************************************************/
+
+/*global define */
+
+define("orion/textview/textDND", [], function() {
+
+ function TextDND(view, undoStack) {
+ this._view = view;
+ this._undoStack = undoStack;
+ this._dragSelection = null;
+ this._dropOffset = -1;
+ this._dropText = null;
+ var self = this;
+ this._listener = {
+ onDragStart: function (evt) {
+ self._onDragStart(evt);
+ },
+ onDragEnd: function (evt) {
+ self._onDragEnd(evt);
+ },
+ onDragEnter: function (evt) {
+ self._onDragEnter(evt);
+ },
+ onDragOver: function (evt) {
+ self._onDragOver(evt);
+ },
+ onDrop: function (evt) {
+ self._onDrop(evt);
+ },
+ onDestroy: function (evt) {
+ self._onDestroy(evt);
+ }
+ };
+ view.addEventListener("DragStart", this._listener.onDragStart);
+ view.addEventListener("DragEnd", this._listener.onDragEnd);
+ view.addEventListener("DragEnter", this._listener.onDragEnter);
+ view.addEventListener("DragOver", this._listener.onDragOver);
+ view.addEventListener("Drop", this._listener.onDrop);
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ }
+ TextDND.prototype = {
+ destroy: function() {
+ var view = this._view;
+ if (!view) { return; }
+ view.removeEventListener("DragStart", this._listener.onDragStart);
+ view.removeEventListener("DragEnd", this._listener.onDragEnd);
+ view.removeEventListener("DragEnter", this._listener.onDragEnter);
+ view.removeEventListener("DragOver", this._listener.onDragOver);
+ view.removeEventListener("Drop", this._listener.onDrop);
+ view.removeEventListener("Destroy", this._listener.onDestroy);
+ this._view = null;
+ },
+ _onDestroy: function(e) {
+ this.destroy();
+ },
+ _onDragStart: function(e) {
+ var view = this._view;
+ var selection = view.getSelection();
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ selection.start = model.mapOffset(selection.start);
+ selection.end = model.mapOffset(selection.end);
+ model = model.getBaseModel();
+ }
+ var text = model.getText(selection.start, selection.end);
+ if (text) {
+ this._dragSelection = selection;
+ e.event.dataTransfer.effectAllowed = "copyMove";
+ e.event.dataTransfer.setData("Text", text);
+ }
+ },
+ _onDragEnd: function(e) {
+ var view = this._view;
+ if (this._dragSelection) {
+ if (this._undoStack) { this._undoStack.startCompoundChange(); }
+ var move = e.event.dataTransfer.dropEffect === "move";
+ if (move) {
+ view.setText("", this._dragSelection.start, this._dragSelection.end);
+ }
+ if (this._dropText) {
+ var text = this._dropText;
+ var offset = this._dropOffset;
+ if (move) {
+ if (offset >= this._dragSelection.end) {
+ offset -= this._dragSelection.end - this._dragSelection.start;
+ } else if (offset >= this._dragSelection.start) {
+ offset = this._dragSelection.start;
+ }
+ }
+ view.setText(text, offset, offset);
+ view.setSelection(offset, offset + text.length);
+ this._dropText = null;
+ this._dropOffset = -1;
+ }
+ if (this._undoStack) { this._undoStack.endCompoundChange(); }
+ }
+ this._dragSelection = null;
+ },
+ _onDragEnter: function(e) {
+ this._onDragOver(e);
+ },
+ _onDragOver: function(e) {
+ var types = e.event.dataTransfer.types;
+ if (types) {
+ var allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain") !== -1;
+ if (!allowed) {
+ e.event.dataTransfer.dropEffect = "none";
+ }
+ }
+ },
+ _onDrop: function(e) {
+ var view = this._view;
+ var text = e.event.dataTransfer.getData("Text");
+ if (text) {
+ var offset = view.getOffsetAtLocation(e.x, e.y);
+ if (this._dragSelection) {
+ this._dropOffset = offset;
+ this._dropText = text;
+ } else {
+ view.setText(text, offset, offset);
+ view.setSelection(offset, offset + text.length);
+ }
+ }
+ }
+ };
+
+ return {TextDND: TextDND};
+});/*******************************************************************************
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*jslint */
+/*global define */
+
+define("orion/editor/htmlGrammar", [], function() {
+
+ /**
+ * Provides a grammar that can do some very rough syntax highlighting for HTML.
+ * @class orion.syntax.HtmlGrammar
+ */
+ function HtmlGrammar() {
+ /**
+ * Object containing the grammar rules.
+ * @public
+ * @type Object
+ */
+ return {
+ "name": "HTML",
+ "scopeName": "source.html",
+ "uuid": "3B5C76FB-EBB5-D930-F40C-047D082CE99B",
+ "patterns": [
+ // TODO unicode?
+ {
+ "match": "]+>",
+ "name": "entity.name.tag.doctype.html"
+ },
+ {
+ "begin": "",
+ "beginCaptures": {
+ "0": { "name": "punctuation.definition.comment.html" }
+ },
+ "endCaptures": {
+ "0": { "name": "punctuation.definition.comment.html" }
+ },
+ "patterns": [
+ {
+ "match": "--",
+ "name": "invalid.illegal.badcomment.html"
+ }
+ ],
+ "contentName": "comment.block.html"
+ },
+ { // startDelimiter + tagName
+ "match": "<[A-Za-z0-9_\\-:]+(?= ?)",
+ "name": "entity.name.tag.html"
+ },
+ { "include": "#attrName" },
+ { "include": "#qString" },
+ { "include": "#qqString" },
+ // TODO attrName, qString, qqString should be applied first while inside a tag
+ { // startDelimiter + slash + tagName + endDelimiter
+ "match": "[A-Za-z0-9_\\-:]+>",
+ "name": "entity.name.tag.html"
+ },
+ { // end delimiter of open tag
+ "match": ">",
+ "name": "entity.name.tag.html"
+ } ],
+ "repository": {
+ "attrName": { // attribute name
+ "match": "[A-Za-z\\-:]+(?=\\s*=\\s*['\"])",
+ "name": "entity.other.attribute.name.html"
+ },
+ "qqString": { // double quoted string
+ "match": "(\")[^\"]+(\")",
+ "name": "string.quoted.double.html"
+ },
+ "qString": { // single quoted string
+ "match": "(')[^']+(\')",
+ "name": "string.quoted.single.html"
+ }
+ }
+ };
+ }
+
+ return {HtmlGrammar: HtmlGrammar};
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ ******************************************************************************/
+
+/*jslint regexp:false laxbreak:true*/
+/*global define */
+
+define("orion/editor/textMateStyler", ['orion/editor/regex'], function(mRegex) {
+
+var RegexUtil = {
+ // Rules to detect some unsupported Oniguruma features
+ unsupported: [
+ {regex: /\(\?[ims\-]:/, func: function(match) { return "option on/off for subexp"; }},
+ {regex: /\(\?<([=!])/, func: function(match) { return (match[1] === "=") ? "lookbehind" : "negative lookbehind"; }},
+ {regex: /\(\?>/, func: function(match) { return "atomic group"; }}
+ ],
+
+ /**
+ * @param {String} str String giving a regular expression pattern from a TextMate grammar.
+ * @param {String} [flags] [ismg]+
+ * @returns {RegExp}
+ */
+ toRegExp: function(str) {
+ function fail(feature, match) {
+ throw new Error("Unsupported regex feature \"" + feature + "\": \"" + match[0] + "\" at index: "
+ + match.index + " in " + match.input);
+ }
+ // Turns an extended regex pattern into a normal one
+ function normalize(/**String*/ str) {
+ var result = "";
+ var insideCharacterClass = false;
+ var len = str.length;
+ for (var i=0; i < len; ) {
+ var chr = str[i];
+ if (!insideCharacterClass && chr === "#") {
+ // skip to eol
+ while (i < len && chr !== "\r" && chr !== "\n") {
+ chr = str[++i];
+ }
+ } else if (!insideCharacterClass && /\s/.test(chr)) {
+ // skip whitespace
+ while (i < len && /\s/.test(chr)) {
+ chr = str[++i];
+ }
+ } else if (chr === "\\") {
+ result += chr;
+ if (!/\s/.test(str[i+1])) {
+ result += str[i+1];
+ i += 1;
+ }
+ i += 1;
+ } else if (chr === "[") {
+ insideCharacterClass = true;
+ result += chr;
+ i += 1;
+ } else if (chr === "]") {
+ insideCharacterClass = false;
+ result += chr;
+ i += 1;
+ } else {
+ result += chr;
+ i += 1;
+ }
+ }
+ return result;
+ }
+
+ var flags = "";
+ var i;
+
+ // Handle global "x" flag (whitespace/comments)
+ str = RegexUtil.processGlobalFlag("x", str, function(subexp) {
+ return normalize(subexp);
+ });
+
+ // Handle global "i" flag (case-insensitive)
+ str = RegexUtil.processGlobalFlag("i", str, function(subexp) {
+ flags += "i";
+ return subexp;
+ });
+
+ // Check for remaining unsupported syntax
+ for (i=0; i < this.unsupported.length; i++) {
+ var match;
+ if ((match = this.unsupported[i].regex.exec(str))) {
+ fail(this.unsupported[i].func(match), match);
+ }
+ }
+
+ return new RegExp(str, flags);
+ },
+
+ /**
+ * Checks if flag applies to entire pattern. If so, obtains replacement string by calling processor
+ * on the unwrapped pattern. Handles 2 possible syntaxes: (?f)pat and (?f:pat)
+ */
+ processGlobalFlag: function(/**String*/ flag, /**String*/ str, /**Function*/ processor) {
+ function getMatchingCloseParen(/*String*/pat, /*Number*/start) {
+ var depth = 0,
+ len = pat.length,
+ flagStop = -1;
+ for (var i=start; i < len && flagStop === -1; i++) {
+ switch (pat[i]) {
+ case "\\":
+ i++; // escape: skip next char
+ break;
+ case "(":
+ depth++;
+ break;
+ case ")":
+ depth--;
+ if (depth === 0) {
+ flagStop = i;
+ }
+ break;
+ }
+ }
+ return flagStop;
+ }
+ var flag1 = "(?" + flag + ")",
+ flag2 = "(?" + flag + ":";
+ if (str.substring(0, flag1.length) === flag1) {
+ return processor(str.substring(flag1.length));
+ } else if (str.substring(0, flag2.length) === flag2) {
+ var flagStop = getMatchingCloseParen(str, 0);
+ if (flagStop < str.length-1) {
+ throw new Error("Only a " + flag2 + ") group that encloses the entire regex is supported in: " + str);
+ }
+ return processor(str.substring(flag2.length, flagStop));
+ }
+ return str;
+ },
+
+ hasBackReference: function(/**RegExp*/ regex) {
+ return (/\\\d+/).test(regex.source);
+ },
+
+ /** @returns {RegExp} A regex made by substituting any backreferences in regex for the value of the property
+ * in sub with the same name as the backreferenced group number. */
+ getSubstitutedRegex: function(/**RegExp*/ regex, /**Object*/ sub, /**Boolean*/ escape) {
+ escape = (typeof escape === "undefined") ? true : false;
+ var exploded = regex.source.split(/(\\\d+)/g);
+ var array = [];
+ for (var i=0; i < exploded.length; i++) {
+ var term = exploded[i];
+ var backrefMatch = /\\(\d+)/.exec(term);
+ if (backrefMatch) {
+ var text = sub[backrefMatch[1]] || "";
+ array.push(escape ? mRegex.escape(text) : text);
+ } else {
+ array.push(term);
+ }
+ }
+ return new RegExp(array.join(""));
+ },
+
+ /**
+ * Builds a version of regex with every non-capturing term converted into a capturing group. This is a workaround
+ * for JavaScript's lack of API to get the index at which a matched group begins in the input string.
+ * Using the "groupified" regex, we can sum the lengths of matches from consuming groups 1..n-1 to obtain the
+ * starting index of group n. (A consuming group is a capturing group that is not inside a lookahead assertion).
+ * Example: groupify(/(a+)x+(b+)/) === /(a+)(x+)(b+)/
+ * Example: groupify(/(?:x+(a+))b+/) === /(?:(x+)(a+))(b+)/
+ * @param {RegExp} regex The regex to groupify.
+ * @param {Object} [backRefOld2NewMap] Optional. If provided, the backreference numbers in regex will be updated using the
+ * properties of this object rather than the new group numbers of regex itself.
+ * [0] {RegExp} The groupified version of the input regex.
+ * [1] {Object} A map containing old-group to new-group info. Each property is a capturing group number of regex
+ * and its value is the corresponding capturing group number of [0].
+ * [2] {Object} A map indicating which capturing groups of [0] are also consuming groups. If a group number is found
+ * as a property in this object, then it's a consuming group.
+ */
+ groupify: function(regex, backRefOld2NewMap) {
+ var NON_CAPTURING = 1,
+ CAPTURING = 2,
+ LOOKAHEAD = 3,
+ NEW_CAPTURING = 4;
+ var src = regex.source,
+ len = src.length;
+ var groups = [],
+ lookaheadDepth = 0,
+ newGroups = [],
+ oldGroupNumber = 1,
+ newGroupNumber = 1;
+ var result = [],
+ old2New = {},
+ consuming = {};
+ for (var i=0; i < len; i++) {
+ var curGroup = groups[groups.length-1];
+ var chr = src[i];
+ switch (chr) {
+ case "(":
+ // If we're in new capturing group, close it since ( signals end-of-term
+ if (curGroup === NEW_CAPTURING) {
+ groups.pop();
+ result.push(")");
+ newGroups[newGroups.length-1].end = i;
+ }
+ var peek2 = (i + 2 < len) ? (src[i+1] + "" + src[i+2]) : null;
+ if (peek2 === "?:" || peek2 === "?=" || peek2 === "?!") {
+ // Found non-capturing group or lookahead assertion. Note that we preserve non-capturing groups
+ // as such, but any term inside them will become a new capturing group (unless it happens to
+ // also be inside a lookahead).
+ var groupType;
+ if (peek2 === "?:") {
+ groupType = NON_CAPTURING;
+ } else {
+ groupType = LOOKAHEAD;
+ lookaheadDepth++;
+ }
+ groups.push(groupType);
+ newGroups.push({ start: i, end: -1, type: groupType /*non capturing*/ });
+ result.push(chr);
+ result.push(peek2);
+ i += peek2.length;
+ } else {
+ groups.push(CAPTURING);
+ newGroups.push({ start: i, end: -1, type: CAPTURING, oldNum: oldGroupNumber, num: newGroupNumber });
+ result.push(chr);
+ if (lookaheadDepth === 0) {
+ consuming[newGroupNumber] = null;
+ }
+ old2New[oldGroupNumber] = newGroupNumber;
+ oldGroupNumber++;
+ newGroupNumber++;
+ }
+ break;
+ case ")":
+ var group = groups.pop();
+ if (group === LOOKAHEAD) { lookaheadDepth--; }
+ newGroups[newGroups.length-1].end = i;
+ result.push(chr);
+ break;
+ case "*":
+ case "+":
+ case "?":
+ case "}":
+ // Unary operator. If it's being applied to a capturing group, we need to add a new capturing group
+ // enclosing the pair
+ var op = chr;
+ var prev = src[i-1],
+ prevIndex = i-1;
+ if (chr === "}") {
+ for (var j=i-1; src[j] !== "{" && j >= 0; j--) {}
+ prev = src[j-1];
+ prevIndex = j-1;
+ op = src.substring(j, i+1);
+ }
+ var lastGroup = newGroups[newGroups.length-1];
+ if (prev === ")" && (lastGroup.type === CAPTURING || lastGroup.type === NEW_CAPTURING)) {
+ // Shove in the new group's (, increment num/start in from [lastGroup.start .. end]
+ result.splice(lastGroup.start, 0, "(");
+ result.push(op);
+ result.push(")");
+ var newGroup = { start: lastGroup.start, end: result.length-1, type: NEW_CAPTURING, num: lastGroup.num };
+ for (var k=0; k < newGroups.length; k++) {
+ group = newGroups[k];
+ if (group.type === CAPTURING || group.type === NEW_CAPTURING) {
+ if (group.start >= lastGroup.start && group.end <= prevIndex) {
+ group.start += 1;
+ group.end += 1;
+ group.num = group.num + 1;
+ if (group.type === CAPTURING) {
+ old2New[group.oldNum] = group.num;
+ }
+ }
+ }
+ }
+ newGroups.push(newGroup);
+ newGroupNumber++;
+ break;
+ } else {
+ // Fallthrough to default
+ }
+ default:
+ if (chr !== "|" && curGroup !== CAPTURING && curGroup !== NEW_CAPTURING) {
+ // Not in a capturing group, so make a new one to hold this term.
+ // Perf improvement: don't create the new group if we're inside a lookahead, since we don't
+ // care about them (nothing inside a lookahead actually consumes input so we don't need it)
+ if (lookaheadDepth === 0) {
+ groups.push(NEW_CAPTURING);
+ newGroups.push({ start: i, end: -1, type: NEW_CAPTURING, num: newGroupNumber });
+ result.push("(");
+ consuming[newGroupNumber] = null;
+ newGroupNumber++;
+ }
+ }
+ result.push(chr);
+ if (chr === "\\") {
+ var peek = src[i+1];
+ // Eat next so following iteration doesn't think it's a real special character
+ result.push(peek);
+ i += 1;
+ }
+ break;
+ }
+ }
+ while (groups.length) {
+ // Close any remaining new capturing groups
+ groups.pop();
+ result.push(")");
+ }
+ var newRegex = new RegExp(result.join(""));
+
+ // Update backreferences so they refer to the new group numbers. Use backRefOld2NewMap if provided
+ var subst = {};
+ backRefOld2NewMap = backRefOld2NewMap || old2New;
+ for (var prop in backRefOld2NewMap) {
+ if (backRefOld2NewMap.hasOwnProperty(prop)) {
+ subst[prop] = "\\" + backRefOld2NewMap[prop];
+ }
+ }
+ newRegex = this.getSubstitutedRegex(newRegex, subst, false);
+
+ return [newRegex, old2New, consuming];
+ },
+
+ /** @returns {Boolean} True if the captures object assigns scope to a matching group other than "0". */
+ complexCaptures: function(capturesObj) {
+ if (!capturesObj) { return false; }
+ for (var prop in capturesObj) {
+ if (capturesObj.hasOwnProperty(prop)) {
+ if (prop !== "0") {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+};
+
+ /**
+ * @name orion.editor.TextMateStyler
+ * @class A styler that knows how to apply a subset of the TextMate grammar format to style a line.
+ *
+ * Styling from a grammar:
+ * Each scope name given in the grammar is converted to an array of CSS class names. For example
+ * a region of text with scope keyword.control.php will be assigned the CSS classes
+ * keyword, keyword-control, keyword-control-php
+ *
+ * A CSS file can give rules matching any of these class names to provide generic or more specific styling.
+ * For example,
+ * .keyword { font-color: blue; }
+ * colors all keywords blue, while
+ * .keyword-control-php { font-weight: bold; }
+ * bolds only PHP control keywords.
+ *
+ * This is useful when using grammars that adhere to TextMate's
+ * scope name conventions ,
+ * as a single CSS rule can provide consistent styling to similar constructs across different languages.
+ *
+ * Top-level grammar constructs:
+ * patterns, repository (with limitations, see "Other Features") are supported.
+ * scopeName, firstLineMatch, foldingStartMarker, foldingStopMarker are not supported.
+ * fileTypes is not supported. When using the Orion service registry, the "orion.edit.highlighter"
+ * service serves a similar purpose.
+ *
+ *
+ * Regular expression constructs:
+ *
+ * match patterns are supported.
+ * begin .. end patterns are supported.
+ * The "extended" regex forms (?x) and (?x:...) are supported, but only when they
+ * apply to the entire regex pattern.
+ * Matching is done using native JavaScript RegExps. As a result, many features of the Oniguruma regex
+ * engine used by TextMate are not supported.
+ * Unsupported features include:
+ * Named captures
+ * Setting flags inside subgroups (eg. (?i:a)b)
+ * Lookbehind and negative lookbehind
+ * Subexpression call
+ * etc.
+ *
+ *
+ *
+ *
+ * Scope-assignment constructs:
+ *
+ * captures, beginCaptures, endCaptures are supported.
+ * name and contentName are supported.
+ *
+ *
+ * Other features:
+ *
+ * applyEndPatternLast is supported.
+ * include is supported, but only when it references a rule in the current grammar's repository.
+ * Including $self, $base, or rule.from.another.grammar is not supported.
+ *
+ *
+ * @description Creates a new TextMateStyler.
+ * @extends orion.editor.AbstractStyler
+ * @param {orion.textview.TextView} textView The TextView to provide styling for.
+ * @param {Object} grammar The TextMate grammar to use for styling the TextView, as a JavaScript object. You can
+ * produce this object by running a PList-to-JavaScript conversion tool on a TextMate .tmLanguage file.
+ * @param {Object[]} [externalGrammars] Additional grammar objects that will be used to resolve named rule references.
+ */
+ function TextMateStyler(textView, grammar, externalGrammars) {
+ this.initialize(textView);
+ // Copy grammar object(s) since we will mutate them
+ this.grammar = this.copy(grammar);
+ this.externalGrammars = externalGrammars ? this.copy(externalGrammars) : [];
+
+ this._styles = {}; /* key: {String} scopeName, value: {String[]} cssClassNames */
+ this._tree = null;
+ this._allGrammars = {}; /* key: {String} scopeName of grammar, value: {Object} grammar */
+ this.preprocess(this.grammar);
+ }
+ TextMateStyler.prototype = /** @lends orion.editor.TextMateStyler.prototype */ {
+ initialize: function(textView) {
+ this.textView = textView;
+ var self = this;
+ this._listener = {
+ onModelChanged: function(e) {
+ self.onModelChanged(e);
+ },
+ onDestroy: function(e) {
+ self.onDestroy(e);
+ },
+ onLineStyle: function(e) {
+ self.onLineStyle(e);
+ }
+ };
+ textView.addEventListener("ModelChanged", this._listener.onModelChanged);
+ textView.addEventListener("Destroy", this._listener.onDestroy);
+ textView.addEventListener("LineStyle", this._listener.onLineStyle);
+ textView.redrawLines();
+ },
+ onDestroy: function(/**eclipse.DestroyEvent*/ e) {
+ this.destroy();
+ },
+ destroy: function() {
+ if (this.textView) {
+ this.textView.removeEventListener("ModelChanged", this._listener.onModelChanged);
+ this.textView.removeEventListener("Destroy", this._listener.onDestroy);
+ this.textView.removeEventListener("LineStyle", this._listener.onLineStyle);
+ this.textView = null;
+ }
+ this.grammar = null;
+ this._styles = null;
+ this._tree = null;
+ this._listener = null;
+ },
+ /** @private */
+ copy: function(obj) {
+ return JSON.parse(JSON.stringify(obj));
+ },
+ /** @private */
+ preprocess: function(grammar) {
+ var stack = [grammar];
+ for (; stack.length !== 0; ) {
+ var rule = stack.pop();
+ if (rule._resolvedRule && rule._typedRule) {
+ continue;
+ }
+// console.debug("Process " + (rule.include || rule.name));
+
+ // Look up include'd rule, create typed *Rule instance
+ rule._resolvedRule = this._resolve(rule);
+ rule._typedRule = this._createTypedRule(rule);
+
+ // Convert the scope names to styles and cache them for later
+ this.addStyles(rule.name);
+ this.addStyles(rule.contentName);
+ this.addStylesForCaptures(rule.captures);
+ this.addStylesForCaptures(rule.beginCaptures);
+ this.addStylesForCaptures(rule.endCaptures);
+
+ if (rule._resolvedRule !== rule) {
+ // Add include target
+ stack.push(rule._resolvedRule);
+ }
+ if (rule.patterns) {
+ // Add subrules
+ for (var i=0; i < rule.patterns.length; i++) {
+ stack.push(rule.patterns[i]);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Adds eclipse.Style objects for scope to our _styles cache.
+ * @param {String} scope A scope name, like "constant.character.php".
+ */
+ addStyles: function(scope) {
+ if (scope && !this._styles[scope]) {
+ this._styles[scope] = [];
+ var scopeArray = scope.split(".");
+ for (var i = 0; i < scopeArray.length; i++) {
+ this._styles[scope].push(scopeArray.slice(0, i + 1).join("-"));
+ }
+ }
+ },
+ /** @private */
+ addStylesForCaptures: function(/**Object*/ captures) {
+ for (var prop in captures) {
+ if (captures.hasOwnProperty(prop)) {
+ var scope = captures[prop].name;
+ this.addStyles(scope);
+ }
+ }
+ },
+ /**
+ * A rule that contains subrules ("patterns" in TextMate parlance) but has no "begin" or "end".
+ * Also handles top level of grammar.
+ * @private
+ */
+ ContainerRule: (function() {
+ function ContainerRule(/**Object*/ rule) {
+ this.rule = rule;
+ this.subrules = rule.patterns;
+ }
+ ContainerRule.prototype.valueOf = function() { return "aa"; };
+ return ContainerRule;
+ }()),
+ /**
+ * A rule that is delimited by "begin" and "end" matches, which may be separated by any number of
+ * lines. This type of rule may contain subrules, which apply only inside the begin .. end region.
+ * @private
+ */
+ BeginEndRule: (function() {
+ function BeginEndRule(/**Object*/ rule) {
+ this.rule = rule;
+ // TODO: the TextMate blog claims that "end" is optional.
+ this.beginRegex = RegexUtil.toRegExp(rule.begin);
+ this.endRegex = RegexUtil.toRegExp(rule.end);
+ this.subrules = rule.patterns || [];
+
+ this.endRegexHasBackRef = RegexUtil.hasBackReference(this.endRegex);
+
+ // Deal with non-0 captures
+ var complexCaptures = RegexUtil.complexCaptures(rule.captures);
+ var complexBeginEnd = RegexUtil.complexCaptures(rule.beginCaptures) || RegexUtil.complexCaptures(rule.endCaptures);
+ this.isComplex = complexCaptures || complexBeginEnd;
+ if (this.isComplex) {
+ var bg = RegexUtil.groupify(this.beginRegex);
+ this.beginRegex = bg[0];
+ this.beginOld2New = bg[1];
+ this.beginConsuming = bg[2];
+
+ var eg = RegexUtil.groupify(this.endRegex, this.beginOld2New /*Update end's backrefs to begin's new group #s*/);
+ this.endRegex = eg[0];
+ this.endOld2New = eg[1];
+ this.endConsuming = eg[2];
+ }
+ }
+ BeginEndRule.prototype.valueOf = function() { return this.beginRegex; };
+ return BeginEndRule;
+ }()),
+ /**
+ * A rule with a "match" pattern.
+ * @private
+ */
+ MatchRule: (function() {
+ function MatchRule(/**Object*/ rule) {
+ this.rule = rule;
+ this.matchRegex = RegexUtil.toRegExp(rule.match);
+ this.isComplex = RegexUtil.complexCaptures(rule.captures);
+ if (this.isComplex) {
+ var mg = RegexUtil.groupify(this.matchRegex);
+ this.matchRegex = mg[0];
+ this.matchOld2New = mg[1];
+ this.matchConsuming = mg[2];
+ }
+ }
+ MatchRule.prototype.valueOf = function() { return this.matchRegex; };
+ return MatchRule;
+ }()),
+ /**
+ * @param {Object} rule A rule from the grammar.
+ * @returns {MatchRule|BeginEndRule|ContainerRule}
+ * @private
+ */
+ _createTypedRule: function(rule) {
+ if (rule.match) {
+ return new this.MatchRule(rule);
+ } else if (rule.begin) {
+ return new this.BeginEndRule(rule);
+ } else {
+ return new this.ContainerRule(rule);
+ }
+ },
+ /**
+ * Resolves a rule from the grammar (which may be an include) into the real rule that it points to.
+ * @private
+ */
+ _resolve: function(rule) {
+ var resolved = rule;
+ if (rule.include) {
+ if (rule.begin || rule.end || rule.match) {
+ throw new Error("Unexpected regex pattern in \"include\" rule " + rule.include);
+ }
+ var name = rule.include;
+ if (name[0] === "#") {
+ resolved = this.grammar.repository && this.grammar.repository[name.substring(1)];
+ if (!resolved) { throw new Error("Couldn't find included rule " + name + " in grammar repository"); }
+ } else if (name === "$self") {
+ resolved = this.grammar;
+ } else if (name === "$base") {
+ // $base is only relevant when including rules from foreign grammars
+ throw new Error("Include \"$base\" is not supported");
+ } else {
+ resolved = this._allGrammars[name];
+ if (!resolved) {
+ for (var i=0; i < this.externalGrammars.length; i++) {
+ var grammar = this.externalGrammars[i];
+ if (grammar.scopeName === name) {
+ this.preprocess(grammar);
+ this._allGrammars[name] = grammar;
+ resolved = grammar;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return resolved;
+ },
+ /** @private */
+ ContainerNode: (function() {
+ function ContainerNode(parent, rule) {
+ this.parent = parent;
+ this.rule = rule;
+ this.children = [];
+
+ this.start = null;
+ this.end = null;
+ }
+ ContainerNode.prototype.addChild = function(child) {
+ this.children.push(child);
+ };
+ ContainerNode.prototype.valueOf = function() {
+ var r = this.rule;
+ return "ContainerNode { " + (r.include || "") + " " + (r.name || "") + (r.comment || "") + "}";
+ };
+ return ContainerNode;
+ }()),
+ /** @private */
+ BeginEndNode: (function() {
+ function BeginEndNode(parent, rule, beginMatch) {
+ this.parent = parent;
+ this.rule = rule;
+ this.children = [];
+
+ this.setStart(beginMatch);
+ this.end = null; // will be set eventually during parsing (may be EOF)
+ this.endMatch = null; // may remain null if we never match our "end" pattern
+
+ // Build a new regex if the "end" regex has backrefs since they refer to matched groups of beginMatch
+ if (rule.endRegexHasBackRef) {
+ this.endRegexSubstituted = RegexUtil.getSubstitutedRegex(rule.endRegex, beginMatch);
+ } else {
+ this.endRegexSubstituted = null;
+ }
+ }
+ BeginEndNode.prototype.addChild = function(child) {
+ this.children.push(child);
+ };
+ /** @return {Number} This node's index in its parent's "children" list */
+ BeginEndNode.prototype.getIndexInParent = function(node) {
+ return this.parent ? this.parent.children.indexOf(this) : -1;
+ };
+ /** @param {RegExp.match} beginMatch */
+ BeginEndNode.prototype.setStart = function(beginMatch) {
+ this.start = beginMatch.index;
+ this.beginMatch = beginMatch;
+ };
+ /** @param {RegExp.match|Number} endMatchOrLastChar */
+ BeginEndNode.prototype.setEnd = function(endMatchOrLastChar) {
+ if (endMatchOrLastChar && typeof(endMatchOrLastChar) === "object") {
+ var endMatch = endMatchOrLastChar;
+ this.endMatch = endMatch;
+ this.end = endMatch.index + endMatch[0].length;
+ } else {
+ var lastChar = endMatchOrLastChar;
+ this.endMatch = null;
+ this.end = lastChar;
+ }
+ };
+ BeginEndNode.prototype.shiftStart = function(amount) {
+ this.start += amount;
+ this.beginMatch.index += amount;
+ };
+ BeginEndNode.prototype.shiftEnd = function(amount) {
+ this.end += amount;
+ if (this.endMatch) { this.endMatch.index += amount; }
+ };
+ BeginEndNode.prototype.valueOf = function() {
+ return "{" + this.rule.beginRegex + " range=" + this.start + ".." + this.end + "}";
+ };
+ return BeginEndNode;
+ }()),
+ /** Pushes rules onto stack such that rules[startFrom] is on top
+ * @private
+ */
+ push: function(/**Array*/ stack, /**Array*/ rules) {
+ if (!rules) { return; }
+ for (var i = rules.length; i > 0; ) {
+ stack.push(rules[--i]);
+ }
+ },
+ /** Executes regex on text, and returns the match object with its index
+ * offset by the given amount.
+ * @returns {RegExp.match}
+ * @private
+ */
+ exec: function(/**RegExp*/ regex, /**String*/ text, /**Number*/ offset) {
+ var match = regex.exec(text);
+ if (match) { match.index += offset; }
+ regex.lastIndex = 0; // Just in case
+ return match;
+ },
+ /** @returns {Number} The position immediately following the match.
+ * @private
+ */
+ afterMatch: function(/**RegExp.match*/ match) {
+ return match.index + match[0].length;
+ },
+ /**
+ * @returns {RegExp.match} If node is a BeginEndNode and its rule's "end" pattern matches the text.
+ * @private
+ */
+ getEndMatch: function(/**Node*/ node, /**String*/ text, /**Number*/ offset) {
+ if (node instanceof this.BeginEndNode) {
+ var rule = node.rule;
+ var endRegex = node.endRegexSubstituted || rule.endRegex;
+ if (!endRegex) { return null; }
+ return this.exec(endRegex, text, offset);
+ }
+ return null;
+ },
+ /** Called once when file is first loaded to build the parse tree. Tree is updated incrementally thereafter
+ * as buffer is modified.
+ * @private
+ */
+ initialParse: function() {
+ var last = this.textView.getModel().getCharCount();
+ // First time; make parse tree for whole buffer
+ var root = new this.ContainerNode(null, this.grammar._typedRule);
+ this._tree = root;
+ this.parse(this._tree, false, 0);
+ },
+ onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) {
+ var addedCharCount = e.addedCharCount,
+ addedLineCount = e.addedLineCount,
+ removedCharCount = e.removedCharCount,
+ removedLineCount = e.removedLineCount,
+ start = e.start;
+ if (!this._tree) {
+ this.initialParse();
+ } else {
+ var model = this.textView.getModel();
+ var charCount = model.getCharCount();
+
+ // For rs, we must rewind to the line preceding the line 'start' is on. We can't rely on start's
+ // line since it may've been changed in a way that would cause a new beginMatch at its lineStart.
+ var rs = model.getLineEnd(model.getLineAtOffset(start) - 1); // may be < 0
+ var fd = this.getFirstDamaged(rs, rs);
+ rs = rs === -1 ? 0 : rs;
+ var stoppedAt;
+ if (fd) {
+ // [rs, re] is the region we need to verify. If we find the structure of the tree
+ // has changed in that area, then we may need to reparse the rest of the file.
+ stoppedAt = this.parse(fd, true, rs, start, addedCharCount, removedCharCount);
+ } else {
+ // FIXME: fd == null ?
+ stoppedAt = charCount;
+ }
+ this.textView.redrawRange(rs, stoppedAt);
+ }
+ },
+ /** @returns {BeginEndNode|ContainerNode} The result of taking the first (smallest "start" value)
+ * node overlapping [start,end] and drilling down to get its deepest damaged descendant (if any).
+ * @private
+ */
+ getFirstDamaged: function(start, end) {
+ // If start === 0 we actually have to start from the root because there is no position
+ // we can rely on. (First index is damaged)
+ if (start < 0) {
+ return this._tree;
+ }
+
+ var nodes = [this._tree];
+ var result = null;
+ while (nodes.length) {
+ var n = nodes.pop();
+ if (!n.parent /*n is root*/ || this.isDamaged(n, start, end)) {
+ // n is damaged by the edit, so go into its children
+ // Note: If a node is damaged, then some of its descendents MAY be damaged
+ // If a node is undamaged, then ALL of its descendents are undamaged
+ if (n instanceof this.BeginEndNode) {
+ result = n;
+ }
+ // Examine children[0] last
+ for (var i=0; i < n.children.length; i++) {
+ nodes.push(n.children[i]);
+ }
+ }
+ }
+ return result || this._tree;
+ },
+ /** @returns true If n overlaps the interval [start,end].
+ * @private
+ */
+ isDamaged: function(/**BeginEndNode*/ n, start, end) {
+ // Note strict > since [2,5] doesn't overlap [5,7]
+ return (n.start <= end && n.end > start);
+ },
+ /**
+ * Builds tree from some of the buffer content
+ *
+ * TODO cleanup params
+ * @param {BeginEndNode|ContainerNode} origNode The deepest node that overlaps [rs,rs], or the root.
+ * @param {Boolean} repairing
+ * @param {Number} rs See _onModelChanged()
+ * @param {Number} [editStart] Only used for repairing === true
+ * @param {Number} [addedCharCount] Only used for repairing === true
+ * @param {Number} [removedCharCount] Only used for repairing === true
+ * @returns {Number} The end position that redrawRange should be called for.
+ * @private
+ */
+ parse: function(origNode, repairing, rs, editStart, addedCharCount, removedCharCount) {
+ var model = this.textView.getModel();
+ var lastLineStart = model.getLineStart(model.getLineCount() - 1);
+ var eof = model.getCharCount();
+ var initialExpected = this.getInitialExpected(origNode, rs);
+
+ // re is best-case stopping point; if we detect change to tree, we must continue past it
+ var re = -1;
+ if (repairing) {
+ origNode.repaired = true;
+ origNode.endNeedsUpdate = true;
+ var lastChild = origNode.children[origNode.children.length-1];
+ var delta = addedCharCount - removedCharCount;
+ var lastChildLineEnd = lastChild ? model.getLineEnd(model.getLineAtOffset(lastChild.end + delta)) : -1;
+ var editLineEnd = model.getLineEnd(model.getLineAtOffset(editStart + removedCharCount));
+ re = Math.max(lastChildLineEnd, editLineEnd);
+ }
+ re = (re === -1) ? eof : re;
+
+ var expected = initialExpected;
+ var node = origNode;
+ var matchedChildOrEnd = false;
+ var pos = rs;
+ var redrawEnd = -1;
+ while (node && (!repairing || (pos < re))) {
+ var matchInfo = this.getNextMatch(model, node, pos);
+ if (!matchInfo) {
+ // Go to next line, if any
+ pos = (pos >= lastLineStart) ? eof : model.getLineStart(model.getLineAtOffset(pos) + 1);
+ }
+ var match = matchInfo && matchInfo.match,
+ rule = matchInfo && matchInfo.rule,
+ isSub = matchInfo && matchInfo.isSub,
+ isEnd = matchInfo && matchInfo.isEnd;
+ if (isSub) {
+ pos = this.afterMatch(match);
+ if (rule instanceof this.BeginEndRule) {
+ matchedChildOrEnd = true;
+ // Matched a child. Did we expect that?
+ if (repairing && rule === expected.rule && node === expected.parent) {
+ // Yes: matched expected child
+ var foundChild = expected;
+ foundChild.setStart(match);
+ // Note: the 'end' position for this node will either be matched, or fixed up by us post-loop
+ foundChild.repaired = true;
+ foundChild.endNeedsUpdate = true;
+ node = foundChild; // descend
+ expected = this.getNextExpected(expected, "begin");
+ } else {
+ if (repairing) {
+ // No: matched unexpected child.
+ this.prune(node, expected);
+ repairing = false;
+ }
+
+ // Add the new child (will replace 'expected' in node's children list)
+ var subNode = new this.BeginEndNode(node, rule, match);
+ node.addChild(subNode);
+ node = subNode; // descend
+ }
+ } else {
+ // Matched a MatchRule; no changes to tree required
+ }
+ } else if (isEnd || pos === eof) {
+ if (node instanceof this.BeginEndNode) {
+ if (match) {
+ matchedChildOrEnd = true;
+ redrawEnd = Math.max(redrawEnd, node.end); // if end moved up, must still redraw to its old value
+ node.setEnd(match);
+ pos = this.afterMatch(match);
+ // Matched node's end. Did we expect that?
+ if (repairing && node === expected && node.parent === expected.parent) {
+ // Yes: found the expected end of node
+ node.repaired = true;
+ delete node.endNeedsUpdate;
+ expected = this.getNextExpected(expected, "end");
+ } else {
+ if (repairing) {
+ // No: found an unexpected end
+ this.prune(node, expected);
+ repairing = false;
+ }
+ }
+ } else {
+ // Force-ending a BeginEndNode that runs until eof
+ node.setEnd(eof);
+ delete node.endNeedsUpdate;
+ }
+ }
+ node = node.parent; // ascend
+ }
+
+ if (repairing && pos >= re && !matchedChildOrEnd) {
+ // Reached re without matching any begin/end => initialExpected itself was removed => repair fail
+ this.prune(origNode, initialExpected);
+ repairing = false;
+ }
+ } // end loop
+ // TODO: do this for every node we end?
+ this.removeUnrepairedChildren(origNode, repairing, rs);
+
+ //console.debug("parsed " + (pos - rs) + " of " + model.getCharCount + "buf");
+ this.cleanup(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount);
+ if (repairing) {
+ return Math.max(redrawEnd, pos);
+ } else {
+ return pos; // where we stopped reparsing
+ }
+ },
+ /** Helper for parse() in the repair case. To be called when ending a node, as any children that
+ * lie in [rs,node.end] and were not repaired must've been deleted.
+ * @private
+ */
+ removeUnrepairedChildren: function(node, repairing, start) {
+ if (repairing) {
+ var children = node.children;
+ var removeFrom = -1;
+ for (var i=0; i < children.length; i++) {
+ var child = children[i];
+ if (!child.repaired && this.isDamaged(child, start, Number.MAX_VALUE /*end doesn't matter*/)) {
+ removeFrom = i;
+ break;
+ }
+ }
+ if (removeFrom !== -1) {
+ node.children.length = removeFrom;
+ }
+ }
+ },
+ /** Helper for parse() in the repair case
+ * @private
+ */
+ cleanup: function(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount) {
+ var i, node, maybeRepairedNodes;
+ if (repairing) {
+ // The repair succeeded, so update stale begin/end indices by simple translation.
+ var delta = addedCharCount - removedCharCount;
+ // A repaired node's end can't exceed re, but it may exceed re-delta+1.
+ // TODO: find a way to guarantee disjoint intervals for repaired vs unrepaired, then stop using flag
+ var maybeUnrepairedNodes = this.getIntersecting(re-delta+1, eof);
+ maybeRepairedNodes = this.getIntersecting(rs, re);
+ // Handle unrepaired nodes. They are those intersecting [re-delta+1, eof] that don't have the flag
+ for (i=0; i < maybeUnrepairedNodes.length; i++) {
+ node = maybeUnrepairedNodes[i];
+ if (!node.repaired && node instanceof this.BeginEndNode) {
+ node.shiftEnd(delta);
+ node.shiftStart(delta);
+ }
+ }
+ // Translate 'end' index of repaired node whose 'end' was not matched in loop (>= re)
+ for (i=0; i < maybeRepairedNodes.length; i++) {
+ node = maybeRepairedNodes[i];
+ if (node.repaired && node.endNeedsUpdate) {
+ node.shiftEnd(delta);
+ }
+ delete node.endNeedsUpdate;
+ delete node.repaired;
+ }
+ } else {
+ // Clean up after ourself
+ maybeRepairedNodes = this.getIntersecting(rs, re);
+ for (i=0; i < maybeRepairedNodes.length; i++) {
+ delete maybeRepairedNodes[i].repaired;
+ }
+ }
+ },
+ /**
+ * @param model {orion.textview.TextModel}
+ * @param node {Node}
+ * @param pos {Number}
+ * @param [matchRulesOnly] {Boolean} Optional, if true only "match" subrules will be considered.
+ * @returns {Object} A match info object with properties:
+ * {Boolean} isEnd
+ * {Boolean} isSub
+ * {RegExp.match} match
+ * {(Match|BeginEnd)Rule} rule
+ * @private
+ */
+ getNextMatch: function(model, node, pos, matchRulesOnly) {
+ var lineIndex = model.getLineAtOffset(pos);
+ var lineEnd = model.getLineEnd(lineIndex);
+ var line = model.getText(pos, lineEnd);
+
+ var stack = [],
+ expandedContainers = [],
+ subMatches = [],
+ subrules = [];
+ this.push(stack, node.rule.subrules);
+ while (stack.length) {
+ var next = stack.length ? stack.pop() : null;
+ var subrule = next && next._resolvedRule._typedRule;
+ if (subrule instanceof this.ContainerRule && expandedContainers.indexOf(subrule) === -1) {
+ // Expand ContainerRule by pushing its subrules on
+ expandedContainers.push(subrule);
+ this.push(stack, subrule.subrules);
+ continue;
+ }
+ if (subrule && matchRulesOnly && !(subrule.matchRegex)) {
+ continue;
+ }
+ var subMatch = subrule && this.exec(subrule.matchRegex || subrule.beginRegex, line, pos);
+ if (subMatch) {
+ subMatches.push(subMatch);
+ subrules.push(subrule);
+ }
+ }
+
+ var bestSub = Number.MAX_VALUE,
+ bestSubIndex = -1;
+ for (var i=0; i < subMatches.length; i++) {
+ var match = subMatches[i];
+ if (match.index < bestSub) {
+ bestSub = match.index;
+ bestSubIndex = i;
+ }
+ }
+
+ if (!matchRulesOnly) {
+ // See if the "end" pattern of the active begin/end node matches.
+ // TODO: The active begin/end node may not be the same as the node that holds the subrules
+ var activeBENode = node;
+ var endMatch = this.getEndMatch(node, line, pos);
+ if (endMatch) {
+ var doEndLast = activeBENode.rule.applyEndPatternLast;
+ var endWins = bestSubIndex === -1 || (endMatch.index < bestSub) || (!doEndLast && endMatch.index === bestSub);
+ if (endWins) {
+ return {isEnd: true, rule: activeBENode.rule, match: endMatch};
+ }
+ }
+ }
+ return bestSubIndex === -1 ? null : {isSub: true, rule: subrules[bestSubIndex], match: subMatches[bestSubIndex]};
+ },
+ /**
+ * Gets the node corresponding to the first match we expect to see in the repair.
+ * @param {BeginEndNode|ContainerNode} node The node returned via getFirstDamaged(rs,rs) -- may be the root.
+ * @param {Number} rs See _onModelChanged()
+ * Note that because rs is a line end (or 0, a line start), it will intersect a beginMatch or
+ * endMatch either at their 0th character, or not at all. (begin/endMatches can't cross lines).
+ * This is the only time we rely on the start/end values from the pre-change tree. After this
+ * we only look at node ordering, never use the old indices.
+ * @returns {Node}
+ * @private
+ */
+ getInitialExpected: function(node, rs) {
+ // TODO: Kind of weird.. maybe ContainerNodes should have start & end set, like BeginEndNodes
+ var i, child;
+ if (node === this._tree) {
+ // get whichever of our children comes after rs
+ for (i=0; i < node.children.length; i++) {
+ child = node.children[i]; // BeginEndNode
+ if (child.start >= rs) {
+ return child;
+ }
+ }
+ } else if (node instanceof this.BeginEndNode) {
+ if (node.endMatch) {
+ // Which comes next after rs: our nodeEnd or one of our children?
+ var nodeEnd = node.endMatch.index;
+ for (i=0; i < node.children.length; i++) {
+ child = node.children[i]; // BeginEndNode
+ if (child.start >= rs) {
+ break;
+ }
+ }
+ if (child && child.start < nodeEnd) {
+ return child; // Expect child as the next match
+ }
+ } else {
+ // No endMatch => node goes until eof => it end should be the next match
+ }
+ }
+ return node; // We expect node to end, so it should be the next match
+ },
+ /**
+ * Helper for repair() to tell us what kind of event we expect next.
+ * @param {Node} expected Last value returned by this method.
+ * @param {String} event "begin" if the last value of expected was matched as "begin",
+ * or "end" if it was matched as an end.
+ * @returns {Node} The next expected node to match, or null.
+ * @private
+ */
+ getNextExpected: function(/**Node*/ expected, event) {
+ var node = expected;
+ if (event === "begin") {
+ var child = node.children[0];
+ if (child) {
+ return child;
+ } else {
+ return node;
+ }
+ } else if (event === "end") {
+ var parent = node.parent;
+ if (parent) {
+ var nextSibling = parent.children[parent.children.indexOf(node) + 1];
+ if (nextSibling) {
+ return nextSibling;
+ } else {
+ return parent;
+ }
+ }
+ }
+ return null;
+ },
+ /** Helper for parse() when repairing. Prunes out the unmatched nodes from the tree so we can continue parsing.
+ * @private
+ */
+ prune: function(/**BeginEndNode|ContainerNode*/ node, /**Node*/ expected) {
+ var expectedAChild = expected.parent === node;
+ if (expectedAChild) {
+ // Expected child wasn't matched; prune it and all siblings after it
+ node.children.length = expected.getIndexInParent();
+ } else if (node instanceof this.BeginEndNode) {
+ // Expected node to end but it didn't; set its end unknown and we'll match it eventually
+ node.endMatch = null;
+ node.end = null;
+ }
+ // Reparsing from node, so prune the successors outside of node's subtree
+ if (node.parent) {
+ node.parent.children.length = node.getIndexInParent() + 1;
+ }
+ },
+ onLineStyle: function(/**eclipse.LineStyleEvent*/ e) {
+ function byStart(r1, r2) {
+ return r1.start - r2.start;
+ }
+
+ if (!this._tree) {
+ // In some cases it seems onLineStyle is called before onModelChanged, so we need to parse here
+ this.initialParse();
+ }
+ var lineStart = e.lineStart,
+ model = this.textView.getModel(),
+ lineEnd = model.getLineEnd(e.lineIndex);
+
+ var rs = model.getLineEnd(model.getLineAtOffset(lineStart) - 1); // may be < 0
+ var node = this.getFirstDamaged(rs, rs);
+
+ var scopes = this.getLineScope(model, node, lineStart, lineEnd);
+ e.ranges = this.toStyleRanges(scopes);
+ // Editor requires StyleRanges must be in ascending order by 'start', or else some will be ignored
+ e.ranges.sort(byStart);
+ },
+ /** Runs parse algorithm on [start, end] in the context of node, assigning scope as we find matches.
+ * @private
+ */
+ getLineScope: function(model, node, start, end) {
+ var pos = start;
+ var expected = this.getInitialExpected(node, start);
+ var scopes = [],
+ gaps = [];
+ while (node && (pos < end)) {
+ var matchInfo = this.getNextMatch(model, node, pos);
+ if (!matchInfo) {
+ break; // line is over
+ }
+ var match = matchInfo && matchInfo.match,
+ rule = matchInfo && matchInfo.rule,
+ isSub = matchInfo && matchInfo.isSub,
+ isEnd = matchInfo && matchInfo.isEnd;
+ if (match.index !== pos) {
+ // gap [pos..match.index]
+ gaps.push({ start: pos, end: match.index, node: node});
+ }
+ if (isSub) {
+ pos = this.afterMatch(match);
+ if (rule instanceof this.BeginEndRule) {
+ // Matched a "begin", assign its scope and descend into it
+ this.addBeginScope(scopes, match, rule);
+ node = expected; // descend
+ expected = this.getNextExpected(expected, "begin");
+ } else {
+ // Matched a child MatchRule;
+ this.addMatchScope(scopes, match, rule);
+ }
+ } else if (isEnd) {
+ pos = this.afterMatch(match);
+ // Matched and "end", assign its end scope and go up
+ this.addEndScope(scopes, match, rule);
+ expected = this.getNextExpected(expected, "end");
+ node = node.parent; // ascend
+ }
+ }
+ if (pos < end) {
+ gaps.push({ start: pos, end: end, node: node });
+ }
+ var inherited = this.getInheritedLineScope(gaps, start, end);
+ return scopes.concat(inherited);
+ },
+ /** @private */
+ getInheritedLineScope: function(gaps, start, end) {
+ var scopes = [];
+ for (var i=0; i < gaps.length; i++) {
+ var gap = gaps[i];
+ var node = gap.node;
+ while (node) {
+ // if node defines a contentName or name, apply it
+ var rule = node.rule.rule;
+ var name = rule.name,
+ contentName = rule.contentName;
+ // TODO: if both are given, we don't resolve the conflict. contentName always wins
+ var scope = contentName || name;
+ if (scope) {
+ this.addScopeRange(scopes, gap.start, gap.end, scope);
+ break;
+ }
+ node = node.parent;
+ }
+ }
+ return scopes;
+ },
+ /** @private */
+ addBeginScope: function(scopes, match, typedRule) {
+ var rule = typedRule.rule;
+ this.addCapturesScope(scopes, match, (rule.beginCaptures || rule.captures), typedRule.isComplex, typedRule.beginOld2New, typedRule.beginConsuming);
+ },
+ /** @private */
+ addEndScope: function(scopes, match, typedRule) {
+ var rule = typedRule.rule;
+ this.addCapturesScope(scopes, match, (rule.endCaptures || rule.captures), typedRule.isComplex, typedRule.endOld2New, typedRule.endConsuming);
+ },
+ /** @private */
+ addMatchScope: function(scopes, match, typedRule) {
+ var rule = typedRule.rule,
+ name = rule.name,
+ captures = rule.captures;
+ if (captures) {
+ // captures takes priority over name
+ this.addCapturesScope(scopes, match, captures, typedRule.isComplex, typedRule.matchOld2New, typedRule.matchConsuming);
+ } else {
+ this.addScope(scopes, match, name);
+ }
+ },
+ /** @private */
+ addScope: function(scopes, match, name) {
+ if (!name) { return; }
+ scopes.push({start: match.index, end: this.afterMatch(match), scope: name });
+ },
+ /** @private */
+ addScopeRange: function(scopes, start, end, name) {
+ if (!name) { return; }
+ scopes.push({start: start, end: end, scope: name });
+ },
+ /** @private */
+ addCapturesScope: function(/**Array*/scopes, /*RegExp.match*/ match, /**Object*/captures, /**Boolean*/isComplex, /**Object*/old2New, /**Object*/consuming) {
+ if (!captures) { return; }
+ if (!isComplex) {
+ this.addScope(scopes, match, captures[0] && captures[0].name);
+ } else {
+ // apply scopes captures[1..n] to matching groups [1]..[n] of match
+
+ // Sum up the lengths of preceding consuming groups to get the start offset for each matched group.
+ var newGroupStarts = {1: 0};
+ var sum = 0;
+ for (var num = 1; match[num] !== undefined; num++) {
+ if (consuming[num] !== undefined) {
+ sum += match[num].length;
+ }
+ if (match[num+1] !== undefined) {
+ newGroupStarts[num + 1] = sum;
+ }
+ }
+ // Map the group numbers referred to in captures object to the new group numbers, and get the actual matched range.
+ var start = match.index;
+ for (var oldGroupNum = 1; captures[oldGroupNum]; oldGroupNum++) {
+ var scope = captures[oldGroupNum].name;
+ var newGroupNum = old2New[oldGroupNum];
+ var groupStart = start + newGroupStarts[newGroupNum];
+ // Not every capturing group defined in regex need match every time the regex is run.
+ // eg. (a)|b matches "b" but group 1 is undefined
+ if (typeof match[newGroupNum] !== "undefined") {
+ var groupEnd = groupStart + match[newGroupNum].length;
+ this.addScopeRange(scopes, groupStart, groupEnd, scope);
+ }
+ }
+ }
+ },
+ /** @returns {Node[]} In depth-first order
+ * @private
+ */
+ getIntersecting: function(start, end) {
+ var result = [];
+ var nodes = this._tree ? [this._tree] : [];
+ while (nodes.length) {
+ var n = nodes.pop();
+ var visitChildren = false;
+ if (n instanceof this.ContainerNode) {
+ visitChildren = true;
+ } else if (this.isDamaged(n, start, end)) {
+ visitChildren = true;
+ result.push(n);
+ }
+ if (visitChildren) {
+ var len = n.children.length;
+// for (var i=len-1; i >= 0; i--) {
+// nodes.push(n.children[i]);
+// }
+ for (var i=0; i < len; i++) {
+ nodes.push(n.children[i]);
+ }
+ }
+ }
+ return result.reverse();
+ },
+ /**
+ * Applies the grammar to obtain the {@link eclipse.StyleRange[]} for the given line.
+ * @returns eclipse.StyleRange[]
+ * @private
+ */
+ toStyleRanges: function(/**ScopeRange[]*/ scopeRanges) {
+ var styleRanges = [];
+ for (var i=0; i < scopeRanges.length; i++) {
+ var scopeRange = scopeRanges[i];
+ var classNames = this._styles[scopeRange.scope];
+ if (!classNames) { throw new Error("styles not found for " + scopeRange.scope); }
+ var classNamesString = classNames.join(" ");
+ styleRanges.push({start: scopeRange.start, end: scopeRange.end, style: {styleClass: classNamesString}});
+// console.debug("{start " + styleRanges[i].start + ", end " + styleRanges[i].end + ", style: " + styleRanges[i].style.styleClass + "}");
+ }
+ return styleRanges;
+ }
+ };
+
+ return {
+ RegexUtil: RegexUtil,
+ TextMateStyler: TextMateStyler
+ };
+});
+/*******************************************************************************
+ * @license
+ * Copyright (c) 2010, 2011 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License v1.0
+ * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution
+ * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html).
+ *
+ * Contributors: IBM Corporation - initial API and implementation
+ * Alex Lakatos - fix for bug#369781
+ ******************************************************************************/
+
+/*global document window navigator define */
+
+define("examples/textview/textStyler", ['orion/textview/annotations'], function(mAnnotations) {
+
+ var JS_KEYWORDS =
+ ["break",
+ "case", "class", "catch", "continue", "const",
+ "debugger", "default", "delete", "do",
+ "else", "enum", "export", "extends",
+ "false", "finally", "for", "function",
+ "if", "implements", "import", "in", "instanceof", "interface",
+ "let",
+ "new", "null",
+ "package", "private", "protected", "public",
+ "return",
+ "static", "super", "switch",
+ "this", "throw", "true", "try", "typeof",
+ "undefined",
+ "var", "void",
+ "while", "with",
+ "yield"];
+
+ var JAVA_KEYWORDS =
+ ["abstract",
+ "boolean", "break", "byte",
+ "case", "catch", "char", "class", "continue",
+ "default", "do", "double",
+ "else", "extends",
+ "false", "final", "finally", "float", "for",
+ "if", "implements", "import", "instanceof", "int", "interface",
+ "long",
+ "native", "new", "null",
+ "package", "private", "protected", "public",
+ "return",
+ "short", "static", "super", "switch", "synchronized",
+ "this", "throw", "throws", "transient", "true", "try",
+ "void", "volatile",
+ "while"];
+
+ var CSS_KEYWORDS =
+ ["alignment-adjust", "alignment-baseline", "animation", "animation-delay", "animation-direction", "animation-duration",
+ "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "appearance",
+ "azimuth", "backface-visibility", "background", "background-attachment", "background-clip", "background-color",
+ "background-image", "background-origin", "background-position", "background-repeat", "background-size", "baseline-shift",
+ "binding", "bleed", "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", "border", "border-bottom",
+ "border-bottom-color", "border-bottom-left-radius", "border-bottom-right-radius", "border-bottom-style", "border-bottom-width",
+ "border-collapse", "border-color", "border-image", "border-image-outset", "border-image-repeat", "border-image-slice",
+ "border-image-source", "border-image-width", "border-left", "border-left-color", "border-left-style", "border-left-width",
+ "border-radius", "border-right", "border-right-color", "border-right-style", "border-right-width", "border-spacing", "border-style",
+ "border-top", "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", "border-top-width",
+ "border-width", "bottom", "box-align", "box-decoration-break", "box-direction", "box-flex", "box-flex-group", "box-lines",
+ "box-ordinal-group", "box-orient", "box-pack", "box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
+ "caption-side", "clear", "clip", "color", "color-profile", "column-count", "column-fill", "column-gap", "column-rule",
+ "column-rule-color", "column-rule-style", "column-rule-width", "column-span", "column-width", "columns", "content", "counter-increment",
+ "counter-reset", "crop", "cue", "cue-after", "cue-before", "cursor", "direction", "display", "dominant-baseline",
+ "drop-initial-after-adjust", "drop-initial-after-align", "drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size",
+ "drop-initial-value", "elevation", "empty-cells", "fit", "fit-position", "flex-align", "flex-flow", "flex-inline-pack", "flex-order",
+ "flex-pack", "float", "float-offset", "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style",
+ "font-variant", "font-weight", "grid-columns", "grid-rows", "hanging-punctuation", "height", "hyphenate-after",
+ "hyphenate-before", "hyphenate-character", "hyphenate-lines", "hyphenate-resource", "hyphens", "icon", "image-orientation",
+ "image-rendering", "image-resolution", "inline-box-align", "left", "letter-spacing", "line-height", "line-stacking",
+ "line-stacking-ruby", "line-stacking-shift", "line-stacking-strategy", "list-style", "list-style-image", "list-style-position",
+ "list-style-type", "margin", "margin-bottom", "margin-left", "margin-right", "margin-top", "mark", "mark-after", "mark-before",
+ "marker-offset", "marks", "marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed", "marquee-style", "max-height",
+ "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", "nav-left", "nav-right", "nav-up", "opacity", "orphans",
+ "outline", "outline-color", "outline-offset", "outline-style", "outline-width", "overflow", "overflow-style", "overflow-x",
+ "overflow-y", "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", "page", "page-break-after", "page-break-before",
+ "page-break-inside", "page-policy", "pause", "pause-after", "pause-before", "perspective", "perspective-origin", "phonemes", "pitch",
+ "pitch-range", "play-during", "position", "presentation-level", "punctuation-trim", "quotes", "rendering-intent", "resize",
+ "rest", "rest-after", "rest-before", "richness", "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", "ruby-position",
+ "ruby-span", "size", "speak", "speak-header", "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", "table-layout",
+ "target", "target-name", "target-new", "target-position", "text-align", "text-align-last", "text-decoration", "text-emphasis",
+ "text-height", "text-indent", "text-justify", "text-outline", "text-shadow", "text-transform", "text-wrap", "top", "transform",
+ "transform-origin", "transform-style", "transition", "transition-delay", "transition-duration", "transition-property",
+ "transition-timing-function", "unicode-bidi", "vertical-align", "visibility", "voice-balance", "voice-duration", "voice-family",
+ "voice-pitch", "voice-pitch-range", "voice-rate", "voice-stress", "voice-volume", "volume", "white-space", "white-space-collapse",
+ "widows", "width", "word-break", "word-spacing", "word-wrap", "z-index"
+ ];
+
+ // Scanner constants
+ var UNKOWN = 1;
+ var KEYWORD = 2;
+ var STRING = 3;
+ var SINGLELINE_COMMENT = 4;
+ var MULTILINE_COMMENT = 5;
+ var DOC_COMMENT = 6;
+ var WHITE = 7;
+ var WHITE_TAB = 8;
+ var WHITE_SPACE = 9;
+ var HTML_MARKUP = 10;
+ var DOC_TAG = 11;
+ var TASK_TAG = 12;
+
+ // Styles
+ var singleCommentStyle = {styleClass: "token_singleline_comment"};
+ var multiCommentStyle = {styleClass: "token_multiline_comment"};
+ var docCommentStyle = {styleClass: "token_doc_comment"};
+ var htmlMarkupStyle = {styleClass: "token_doc_html_markup"};
+ var tasktagStyle = {styleClass: "token_task_tag"};
+ var doctagStyle = {styleClass: "token_doc_tag"};
+ var stringStyle = {styleClass: "token_string"};
+ var keywordStyle = {styleClass: "token_keyword"};
+ var spaceStyle = {styleClass: "token_space"};
+ var tabStyle = {styleClass: "token_tab"};
+ var caretLineStyle = {styleClass: "line_caret"};
+
+ function Scanner (keywords, whitespacesVisible) {
+ this.keywords = keywords;
+ this.whitespacesVisible = whitespacesVisible;
+ this.setText("");
+ }
+
+ Scanner.prototype = {
+ getOffset: function() {
+ return this.offset;
+ },
+ getStartOffset: function() {
+ return this.startOffset;
+ },
+ getData: function() {
+ return this.text.substring(this.startOffset, this.offset);
+ },
+ getDataLength: function() {
+ return this.offset - this.startOffset;
+ },
+ _default: function(c) {
+ var keywords = this.keywords;
+ switch (c) {
+ case 32: // SPACE
+ case 9: // TAB
+ if (this.whitespacesVisible) {
+ return c === 32 ? WHITE_SPACE : WHITE_TAB;
+ }
+ do {
+ c = this._read();
+ } while(c === 32 || c === 9);
+ this._unread(c);
+ return WHITE;
+ case 123: // {
+ case 125: // }
+ case 40: // (
+ case 41: // )
+ case 91: // [
+ case 93: // ]
+ case 60: // <
+ case 62: // >
+ // BRACKETS
+ return c;
+ default:
+ var isCSS = this.isCSS;
+ if ((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)) { //LETTER OR UNDERSCORE OR NUMBER
+ var off = this.offset - 1;
+ do {
+ c = this._read();
+ } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)); //LETTER OR UNDERSCORE OR NUMBER
+ this._unread(c);
+ if (keywords.length > 0) {
+ var word = this.text.substring(off, this.offset);
+ //TODO slow
+ for (var i=0; i comment
+ c = this._read();
+ if (!this.isCSS) {
+ if (c === 47) { // SLASH -> single line
+ while (true) {
+ c = this._read();
+ if ((c === -1) || (c === 10) || (c === 13)) {
+ this._unread(c);
+ return SINGLELINE_COMMENT;
+ }
+ }
+ }
+ }
+ if (c === 42) { // STAR -> multi line
+ c = this._read();
+ var token = MULTILINE_COMMENT;
+ if (c === 42) {
+ token = DOC_COMMENT;
+ }
+ while (true) {
+ while (c === 42) {
+ c = this._read();
+ if (c === 47) {
+ return token;
+ }
+ }
+ if (c === -1) {
+ this._unread(c);
+ return token;
+ }
+ c = this._read();
+ }
+ }
+ this._unread(c);
+ return UNKOWN;
+ case 39: // SINGLE QUOTE -> char const
+ while(true) {
+ c = this._read();
+ switch (c) {
+ case 39:
+ return STRING;
+ case 13:
+ case 10:
+ case -1:
+ this._unread(c);
+ return STRING;
+ case 92: // BACKSLASH
+ c = this._read();
+ break;
+ }
+ }
+ break;
+ case 34: // DOUBLE QUOTE -> string
+ while(true) {
+ c = this._read();
+ switch (c) {
+ case 34: // DOUBLE QUOTE
+ return STRING;
+ case 13:
+ case 10:
+ case -1:
+ this._unread(c);
+ return STRING;
+ case 92: // BACKSLASH
+ c = this._read();
+ break;
+ }
+ }
+ break;
+ default:
+ return this._default(c);
+ }
+ }
+ },
+ setText: function(text) {
+ this.text = text;
+ this.offset = 0;
+ this.startOffset = 0;
+ }
+ };
+
+ function WhitespaceScanner () {
+ Scanner.call(this, null, true);
+ }
+ WhitespaceScanner.prototype = new Scanner(null);
+ WhitespaceScanner.prototype.nextToken = function() {
+ this.startOffset = this.offset;
+ while (true) {
+ var c = this._read();
+ switch (c) {
+ case -1: return null;
+ case 32: // SPACE
+ return WHITE_SPACE;
+ case 9: // TAB
+ return WHITE_TAB;
+ default:
+ do {
+ c = this._read();
+ } while(!(c === 32 || c === 9 || c === -1));
+ this._unread(c);
+ return UNKOWN;
+ }
+ }
+ };
+
+ function CommentScanner (whitespacesVisible) {
+ Scanner.call(this, null, whitespacesVisible);
+ }
+ CommentScanner.prototype = new Scanner(null);
+ CommentScanner.prototype.setType = function(type) {
+ this._type = type;
+ };
+ CommentScanner.prototype.nextToken = function() {
+ this.startOffset = this.offset;
+ while (true) {
+ var c = this._read();
+ switch (c) {
+ case -1: return null;
+ case 32: // SPACE
+ case 9: // TAB
+ if (this.whitespacesVisible) {
+ return c === 32 ? WHITE_SPACE : WHITE_TAB;
+ }
+ do {
+ c = this._read();
+ } while(c === 32 || c === 9);
+ this._unread(c);
+ return WHITE;
+ case 60: // <
+ if (this._type === DOC_COMMENT) {
+ do {
+ c = this._read();
+ } while(!(c === 62 || c === -1)); // >
+ if (c === 62) {
+ return HTML_MARKUP;
+ }
+ }
+ return UNKOWN;
+ case 64: // @
+ if (this._type === DOC_COMMENT) {
+ do {
+ c = this._read();
+ } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57)); //LETTER OR UNDERSCORE OR NUMBER
+ this._unread(c);
+ return DOC_TAG;
+ }
+ return UNKOWN;
+ case 84: // T
+ if ((c = this._read()) === 79) { // O
+ if ((c = this._read()) === 68) { // D
+ if ((c = this._read()) === 79) { // O
+ c = this._read();
+ if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57))) {
+ this._unread(c);
+ return TASK_TAG;
+ }
+ this._unread(c);
+ } else {
+ this._unread(c);
+ }
+ } else {
+ this._unread(c);
+ }
+ } else {
+ this._unread(c);
+ }
+ //FALL THROUGH
+ default:
+ do {
+ c = this._read();
+ } while(!(c === 32 || c === 9 || c === -1 || c === 60 || c === 64 || c === 84));
+ this._unread(c);
+ return UNKOWN;
+ }
+ }
+ };
+
+ function FirstScanner () {
+ Scanner.call(this, null, false);
+ }
+ FirstScanner.prototype = new Scanner(null);
+ FirstScanner.prototype._default = function(c) {
+ while(true) {
+ c = this._read();
+ switch (c) {
+ case 47: // SLASH
+ case 34: // DOUBLE QUOTE
+ case 39: // SINGLE QUOTE
+ case -1:
+ this._unread(c);
+ return UNKOWN;
+ }
+ }
+ };
+
+ function TextStyler (view, lang, annotationModel) {
+ this.commentStart = "/*";
+ this.commentEnd = "*/";
+ var keywords = [];
+ switch (lang) {
+ case "java": keywords = JAVA_KEYWORDS; break;
+ case "js": keywords = JS_KEYWORDS; break;
+ case "css": keywords = CSS_KEYWORDS; break;
+ }
+ this.whitespacesVisible = false;
+ this.detectHyperlinks = true;
+ this.highlightCaretLine = false;
+ this.foldingEnabled = true;
+ this.detectTasks = true;
+ this._scanner = new Scanner(keywords, this.whitespacesVisible);
+ this._firstScanner = new FirstScanner();
+ this._commentScanner = new CommentScanner(this.whitespacesVisible);
+ this._whitespaceScanner = new WhitespaceScanner();
+ //TODO these scanners are not the best/correct way to parse CSS
+ if (lang === "css") {
+ this._scanner.isCSS = true;
+ this._firstScanner.isCSS = true;
+ }
+ this.view = view;
+ this.annotationModel = annotationModel;
+ this._bracketAnnotations = undefined;
+
+ var self = this;
+ this._listener = {
+ onChanged: function(e) {
+ self._onModelChanged(e);
+ },
+ onDestroy: function(e) {
+ self._onDestroy(e);
+ },
+ onLineStyle: function(e) {
+ self._onLineStyle(e);
+ },
+ onSelection: function(e) {
+ self._onSelection(e);
+ }
+ };
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ model.getBaseModel().addEventListener("Changed", this._listener.onChanged);
+ } else {
+ //TODO still needed to keep the event order correct (styler before view)
+ view.addEventListener("ModelChanged", this._listener.onChanged);
+ }
+ view.addEventListener("Selection", this._listener.onSelection);
+ view.addEventListener("Destroy", this._listener.onDestroy);
+ view.addEventListener("LineStyle", this._listener.onLineStyle);
+ this._computeComments ();
+ this._computeFolding();
+ view.redrawLines();
+ }
+
+ TextStyler.prototype = {
+ getClassNameForToken: function(token) {
+ switch (token) {
+ case "singleLineComment": return singleCommentStyle.styleClass;
+ case "multiLineComment": return multiCommentStyle.styleClass;
+ case "docComment": return docCommentStyle.styleClass;
+ case "docHtmlComment": return htmlMarkupStyle.styleClass;
+ case "tasktag": return tasktagStyle.styleClass;
+ case "doctag": return doctagStyle.styleClass;
+ case "string": return stringStyle.styleClass;
+ case "keyword": return keywordStyle.styleClass;
+ case "space": return spaceStyle.styleClass;
+ case "tab": return tabStyle.styleClass;
+ case "caretLine": return caretLineStyle.styleClass;
+ }
+ return null;
+ },
+ destroy: function() {
+ var view = this.view;
+ if (view) {
+ var model = view.getModel();
+ if (model.getBaseModel) {
+ model.getBaseModel().removeEventListener("Changed", this._listener.onChanged);
+ } else {
+ view.removeEventListener("ModelChanged", this._listener.onChanged);
+ }
+ view.removeEventListener("Selection", this._listener.onSelection);
+ view.removeEventListener("Destroy", this._listener.onDestroy);
+ view.removeEventListener("LineStyle", this._listener.onLineStyle);
+ this.view = null;
+ }
+ },
+ setHighlightCaretLine: function(highlight) {
+ this.highlightCaretLine = highlight;
+ },
+ setWhitespacesVisible: function(visible) {
+ this.whitespacesVisible = visible;
+ this._scanner.whitespacesVisible = visible;
+ this._commentScanner.whitespacesVisible = visible;
+ },
+ setDetectHyperlinks: function(enabled) {
+ this.detectHyperlinks = enabled;
+ },
+ setFoldingEnabled: function(enabled) {
+ this.foldingEnabled = enabled;
+ },
+ setDetectTasks: function(enabled) {
+ this.detectTasks = enabled;
+ },
+ _binarySearch: function (array, offset, inclusive, low, high) {
+ var index;
+ if (low === undefined) { low = -1; }
+ if (high === undefined) { high = array.length; }
+ while (high - low > 1) {
+ index = Math.floor((high + low) / 2);
+ if (offset <= array[index].start) {
+ high = index;
+ } else if (inclusive && offset < array[index].end) {
+ high = index;
+ break;
+ } else {
+ low = index;
+ }
+ }
+ return high;
+ },
+ _computeComments: function() {
+ var model = this.view.getModel();
+ if (model.getBaseModel) { model = model.getBaseModel(); }
+ this.comments = this._findComments(model.getText());
+ },
+ _computeFolding: function() {
+ if (!this.foldingEnabled) { return; }
+ var view = this.view;
+ var viewModel = view.getModel();
+ if (!viewModel.getBaseModel) { return; }
+ var annotationModel = this.annotationModel;
+ if (!annotationModel) { return; }
+ annotationModel.removeAnnotations("orion.annotation.folding");
+ var add = [];
+ var baseModel = viewModel.getBaseModel();
+ var comments = this.comments;
+ for (var i=0; i ", {styleClass: "annotation expanded"},
+ "
", {styleClass: "annotation collapsed"});
+ },
+ _computeTasks: function(type, commentStart, commentEnd) {
+ if (!this.detectTasks) { return; }
+ var annotationModel = this.annotationModel;
+ if (!annotationModel) { return; }
+ var view = this.view;
+ var viewModel = view.getModel(), baseModel = viewModel;
+ if (viewModel.getBaseModel) { baseModel = viewModel.getBaseModel(); }
+ var annotations = annotationModel.getAnnotations(commentStart, commentEnd);
+ var remove = [];
+ var annotationType = "orion.annotation.task";
+ while (annotations.hasNext()) {
+ var annotation = annotations.next();
+ if (annotation.type === annotationType) {
+ remove.push(annotation);
+ }
+ }
+ var add = [];
+ var scanner = this._commentScanner;
+ scanner.setText(baseModel.getText(commentStart, commentEnd));
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + commentStart;
+ if (token === TASK_TAG) {
+ var end = baseModel.getLineEnd(baseModel.getLineAtOffset(tokenStart));
+ if (type !== SINGLELINE_COMMENT) {
+ end = Math.min(end, commentEnd - this.commentEnd.length);
+ }
+ add.push({
+ start: tokenStart,
+ end: end,
+ type: annotationType,
+ title: baseModel.getText(tokenStart, end),
+ style: {styleClass: "annotation task"},
+ html: "
",
+ overviewStyle: {styleClass: "annotationOverview task"},
+ rangeStyle: {styleClass: "annotationRange task"}
+ });
+ }
+ }
+ annotationModel.replaceAnnotations(remove, add);
+ },
+ _getLineStyle: function(lineIndex) {
+ if (this.highlightCaretLine) {
+ var view = this.view;
+ var model = view.getModel();
+ var selection = view.getSelection();
+ if (selection.start === selection.end && model.getLineAtOffset(selection.start) === lineIndex) {
+ return caretLineStyle;
+ }
+ }
+ return null;
+ },
+ _getStyles: function(model, text, start) {
+ if (model.getBaseModel) {
+ start = model.mapOffset(start);
+ }
+ var end = start + text.length;
+
+ var styles = [];
+
+ // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc)
+ var offset = start, comments = this.comments;
+ var startIndex = this._binarySearch(comments, start, true);
+ for (var i = startIndex; i < comments.length; i++) {
+ if (comments[i].start >= end) { break; }
+ var commentStart = comments[i].start;
+ var commentEnd = comments[i].end;
+ if (offset < commentStart) {
+ this._parse(text.substring(offset - start, commentStart - start), offset, styles);
+ }
+ var style = comments[i].type === DOC_COMMENT ? docCommentStyle : multiCommentStyle;
+ if (this.whitespacesVisible || this.detectHyperlinks) {
+ var s = Math.max(offset, commentStart);
+ var e = Math.min(end, commentEnd);
+ this._parseComment(text.substring(s - start, e - start), s, styles, style, comments[i].type);
+ } else {
+ styles.push({start: commentStart, end: commentEnd, style: style});
+ }
+ offset = commentEnd;
+ }
+ if (offset < end) {
+ this._parse(text.substring(offset - start, end - start), offset, styles);
+ }
+ if (model.getBaseModel) {
+ for (var j = 0; j < styles.length; j++) {
+ var length = styles[j].end - styles[j].start;
+ styles[j].start = model.mapOffset(styles[j].start, true);
+ styles[j].end = styles[j].start + length;
+ }
+ }
+ return styles;
+ },
+ _parse: function(text, offset, styles) {
+ var scanner = this._scanner;
+ scanner.setText(text);
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + offset;
+ var style = null;
+ switch (token) {
+ case KEYWORD: style = keywordStyle; break;
+ case STRING:
+ if (this.whitespacesVisible) {
+ this._parseString(scanner.getData(), tokenStart, styles, stringStyle);
+ continue;
+ } else {
+ style = stringStyle;
+ }
+ break;
+ case DOC_COMMENT:
+ this._parseComment(scanner.getData(), tokenStart, styles, docCommentStyle, token);
+ continue;
+ case SINGLELINE_COMMENT:
+ this._parseComment(scanner.getData(), tokenStart, styles, singleCommentStyle, token);
+ continue;
+ case MULTILINE_COMMENT:
+ this._parseComment(scanner.getData(), tokenStart, styles, multiCommentStyle, token);
+ continue;
+ case WHITE_TAB:
+ if (this.whitespacesVisible) {
+ style = tabStyle;
+ }
+ break;
+ case WHITE_SPACE:
+ if (this.whitespacesVisible) {
+ style = spaceStyle;
+ }
+ break;
+ }
+ styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
+ }
+ },
+ _parseComment: function(text, offset, styles, s, type) {
+ var scanner = this._commentScanner;
+ scanner.setText(text);
+ scanner.setType(type);
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + offset;
+ var style = s;
+ switch (token) {
+ case WHITE_TAB:
+ if (this.whitespacesVisible) {
+ style = tabStyle;
+ }
+ break;
+ case WHITE_SPACE:
+ if (this.whitespacesVisible) {
+ style = spaceStyle;
+ }
+ break;
+ case HTML_MARKUP:
+ style = htmlMarkupStyle;
+ break;
+ case DOC_TAG:
+ style = doctagStyle;
+ break;
+ case TASK_TAG:
+ style = tasktagStyle;
+ break;
+ default:
+ if (this.detectHyperlinks) {
+ style = this._detectHyperlinks(scanner.getData(), tokenStart, styles, style);
+ }
+ }
+ if (style) {
+ styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
+ }
+ }
+ },
+ _parseString: function(text, offset, styles, s) {
+ var scanner = this._whitespaceScanner;
+ scanner.setText(text);
+ var token;
+ while ((token = scanner.nextToken())) {
+ var tokenStart = scanner.getStartOffset() + offset;
+ var style = s;
+ switch (token) {
+ case WHITE_TAB:
+ if (this.whitespacesVisible) {
+ style = tabStyle;
+ }
+ break;
+ case WHITE_SPACE:
+ if (this.whitespacesVisible) {
+ style = spaceStyle;
+ }
+ break;
+ }
+ if (style) {
+ styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style});
+ }
+ }
+ },
+ _detectHyperlinks: function(text, offset, styles, s) {
+ var href = null, index, linkStyle;
+ if ((index = text.indexOf("://")) > 0) {
+ href = text;
+ var start = index;
+ while (start > 0) {
+ var c = href.charCodeAt(start - 1);
+ if (!((97 <= c && c <= 122) || (65 <= c && c <= 90) || 0x2d === c || (48 <= c && c <= 57))) { //LETTER OR DASH OR NUMBER
+ break;
+ }
+ start--;
+ }
+ if (start > 0) {
+ var brackets = "\"\"''(){}[]<>";
+ index = brackets.indexOf(href.substring(start - 1, start));
+ if (index !== -1 && (index & 1) === 0 && (index = href.lastIndexOf(brackets.substring(index + 1, index + 2))) !== -1) {
+ var end = index;
+ linkStyle = this._clone(s);
+ linkStyle.tagName = "A";
+ linkStyle.attributes = {href: href.substring(start, end)};
+ styles.push({start: offset, end: offset + start, style: s});
+ styles.push({start: offset + start, end: offset + end, style: linkStyle});
+ styles.push({start: offset + end, end: offset + text.length, style: s});
+ return null;
+ }
+ }
+ } else if (text.toLowerCase().indexOf("bug#") === 0) {
+ href = "https://bugs.eclipse.org/bugs/show_bug.cgi?id=" + parseInt(text.substring(4), 10);
+ }
+ if (href) {
+ linkStyle = this._clone(s);
+ linkStyle.tagName = "A";
+ linkStyle.attributes = {href: href};
+ return linkStyle;
+ }
+ return s;
+ },
+ _clone: function(obj) {
+ if (!obj) { return obj; }
+ var newObj = {};
+ for (var p in obj) {
+ if (obj.hasOwnProperty(p)) {
+ var value = obj[p];
+ newObj[p] = value;
+ }
+ }
+ return newObj;
+ },
+ _findComments: function(text, offset) {
+ offset = offset || 0;
+ var scanner = this._firstScanner, token;
+ scanner.setText(text);
+ var result = [];
+ while ((token = scanner.nextToken())) {
+ if (token === MULTILINE_COMMENT || token === DOC_COMMENT) {
+ var comment = {
+ start: scanner.getStartOffset() + offset,
+ end: scanner.getOffset() + offset,
+ type: token
+ };
+ result.push(comment);
+ //TODO can we avoid this work if edition does not overlap comment?
+ this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset);
+ }
+ if (token === SINGLELINE_COMMENT) {
+ //TODO can we avoid this work if edition does not overlap comment?
+ this._computeTasks(token, scanner.getStartOffset() + offset, scanner.getOffset() + offset);
+ }
+ }
+ return result;
+ },
+ _findMatchingBracket: function(model, offset) {
+ var brackets = "{}()[]<>";
+ var bracket = model.getText(offset, offset + 1);
+ var bracketIndex = brackets.indexOf(bracket, 0);
+ if (bracketIndex === -1) { return -1; }
+ var closingBracket;
+ if (bracketIndex & 1) {
+ closingBracket = brackets.substring(bracketIndex - 1, bracketIndex);
+ } else {
+ closingBracket = brackets.substring(bracketIndex + 1, bracketIndex + 2);
+ }
+ var lineIndex = model.getLineAtOffset(offset);
+ var lineText = model.getLine(lineIndex);
+ var lineStart = model.getLineStart(lineIndex);
+ var lineEnd = model.getLineEnd(lineIndex);
+ brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
+ for (var i=0; i
= 0 ? 1 : -1;
+ if (brackets[i] * sign === offset) {
+ var level = 1;
+ if (bracketIndex & 1) {
+ i--;
+ for (; i>=0; i--) {
+ sign = brackets[i] >= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[i] * sign;
+ }
+ }
+ lineIndex -= 1;
+ while (lineIndex >= 0) {
+ lineText = model.getLine(lineIndex);
+ lineStart = model.getLineStart(lineIndex);
+ lineEnd = model.getLineEnd(lineIndex);
+ brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
+ for (var j=brackets.length - 1; j>=0; j--) {
+ sign = brackets[j] >= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[j] * sign;
+ }
+ }
+ lineIndex--;
+ }
+ } else {
+ i++;
+ for (; i= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[i] * sign;
+ }
+ }
+ lineIndex += 1;
+ var lineCount = model.getLineCount ();
+ while (lineIndex < lineCount) {
+ lineText = model.getLine(lineIndex);
+ lineStart = model.getLineStart(lineIndex);
+ lineEnd = model.getLineEnd(lineIndex);
+ brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd);
+ for (var k=0; k= 0 ? 1 : -1;
+ level += sign;
+ if (level === 0) {
+ return brackets[k] * sign;
+ }
+ }
+ lineIndex++;
+ }
+ }
+ break;
+ }
+ }
+ return -1;
+ },
+ _findBrackets: function(bracket, closingBracket, text, textOffset, start, end) {
+ var result = [];
+ var bracketToken = bracket.charCodeAt(0);
+ var closingBracketToken = closingBracket.charCodeAt(0);
+ // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc)
+ var offset = start, scanner = this._scanner, token, comments = this.comments;
+ var startIndex = this._binarySearch(comments, start, true);
+ for (var i = startIndex; i < comments.length; i++) {
+ if (comments[i].start >= end) { break; }
+ var commentStart = comments[i].start;
+ var commentEnd = comments[i].end;
+ if (offset < commentStart) {
+ scanner.setText(text.substring(offset - start, commentStart - start));
+ while ((token = scanner.nextToken())) {
+ if (token === bracketToken) {
+ result.push(scanner.getStartOffset() + offset - start + textOffset);
+ } else if (token === closingBracketToken) {
+ result.push(-(scanner.getStartOffset() + offset - start + textOffset));
+ }
+ }
+ }
+ offset = commentEnd;
+ }
+ if (offset < end) {
+ scanner.setText(text.substring(offset - start, end - start));
+ while ((token = scanner.nextToken())) {
+ if (token === bracketToken) {
+ result.push(scanner.getStartOffset() + offset - start + textOffset);
+ } else if (token === closingBracketToken) {
+ result.push(-(scanner.getStartOffset() + offset - start + textOffset));
+ }
+ }
+ }
+ return result;
+ },
+ _onDestroy: function(e) {
+ this.destroy();
+ },
+ _onLineStyle: function (e) {
+ if (e.textView === this.view) {
+ e.style = this._getLineStyle(e.lineIndex);
+ }
+ e.ranges = this._getStyles(e.textView.getModel(), e.lineText, e.lineStart);
+ },
+ _onSelection: function(e) {
+ var oldSelection = e.oldValue;
+ var newSelection = e.newValue;
+ var view = this.view;
+ var model = view.getModel();
+ var lineIndex;
+ if (this.highlightCaretLine) {
+ var oldLineIndex = model.getLineAtOffset(oldSelection.start);
+ lineIndex = model.getLineAtOffset(newSelection.start);
+ var newEmpty = newSelection.start === newSelection.end;
+ var oldEmpty = oldSelection.start === oldSelection.end;
+ if (!(oldLineIndex === lineIndex && oldEmpty && newEmpty)) {
+ if (oldEmpty) {
+ view.redrawLines(oldLineIndex, oldLineIndex + 1);
+ }
+ if ((oldLineIndex !== lineIndex || !oldEmpty) && newEmpty) {
+ view.redrawLines(lineIndex, lineIndex + 1);
+ }
+ }
+ }
+ if (!this.annotationModel) { return; }
+ var remove = this._bracketAnnotations, add, caret;
+ if (newSelection.start === newSelection.end && (caret = view.getCaretOffset()) > 0) {
+ var mapCaret = caret - 1;
+ if (model.getBaseModel) {
+ mapCaret = model.mapOffset(mapCaret);
+ model = model.getBaseModel();
+ }
+ var bracket = this._findMatchingBracket(model, mapCaret);
+ if (bracket !== -1) {
+ add = [{
+ start: bracket,
+ end: bracket + 1,
+ type: "orion.annotation.matchingBracket",
+ title: "Matching Bracket",
+ html: "
",
+ overviewStyle: {styleClass: "annotationOverview matchingBracket"},
+ rangeStyle: {styleClass: "annotationRange matchingBracket"}
+ },
+ {
+ start: mapCaret,
+ end: mapCaret + 1,
+ type: "orion.annotation.currentBracket",
+ title: "Current Bracket",
+ html: "
",
+ overviewStyle: {styleClass: "annotationOverview currentBracket"},
+ rangeStyle: {styleClass: "annotationRange currentBracket"}
+ }];
+ }
+ }
+ this._bracketAnnotations = add;
+ this.annotationModel.replaceAnnotations(remove, add);
+ },
+ _onModelChanged: function(e) {
+ var start = e.start;
+ var removedCharCount = e.removedCharCount;
+ var addedCharCount = e.addedCharCount;
+ var changeCount = addedCharCount - removedCharCount;
+ var view = this.view;
+ var viewModel = view.getModel();
+ var baseModel = viewModel.getBaseModel ? viewModel.getBaseModel() : viewModel;
+ var end = start + removedCharCount;
+ var charCount = baseModel.getCharCount();
+ var commentCount = this.comments.length;
+ var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(start));
+ var commentStart = this._binarySearch(this.comments, lineStart, true);
+ var commentEnd = this._binarySearch(this.comments, end, false, commentStart - 1, commentCount);
+
+ var ts;
+ if (commentStart < commentCount && this.comments[commentStart].start <= lineStart && lineStart < this.comments[commentStart].end) {
+ ts = this.comments[commentStart].start;
+ if (ts > start) { ts += changeCount; }
+ } else {
+ if (commentStart === commentCount && commentCount > 0 && charCount - changeCount === this.comments[commentCount - 1].end) {
+ ts = this.comments[commentCount - 1].start;
+ } else {
+ ts = lineStart;
+ }
+ }
+ var te;
+ if (commentEnd < commentCount) {
+ te = this.comments[commentEnd].end;
+ if (te > start) { te += changeCount; }
+ commentEnd += 1;
+ } else {
+ commentEnd = commentCount;
+ te = charCount;//TODO could it be smaller?
+ }
+ var text = baseModel.getText(ts, te), comment;
+ var newComments = this._findComments(text, ts), i;
+ for (i = commentStart; i < this.comments.length; i++) {
+ comment = this.comments[i];
+ if (comment.start > start) { comment.start += changeCount; }
+ if (comment.start > start) { comment.end += changeCount; }
+ }
+ var redraw = (commentEnd - commentStart) !== newComments.length;
+ if (!redraw) {
+ for (i=0; i start) {
+ annotationStart -= changeCount;
+ }
+ if (annotationEnd > start) {
+ annotationEnd -= changeCount;
+ }
+ if (annotationStart <= start && start < annotationEnd && annotationStart <= end && end < annotationEnd) {
+ var startLine = baseModel.getLineAtOffset(annotation.start);
+ var endLine = baseModel.getLineAtOffset(annotation.end);
+ if (startLine !== endLine) {
+ if (!annotation.expanded) {
+ annotation.expand();
+ annotationModel.modifyAnnotation(annotation);
+ }
+ } else {
+ annotationModel.removeAnnotation(annotation);
+ }
+ }
+ }
+ }
+ }
+ var add = [];
+ for (i = 0; i < newComments.length; i++) {
+ comment = newComments[i];
+ for (var j = 0; j < all.length; j++) {
+ if (all[j].start === comment.start && all[j].end === comment.end) {
+ break;
+ }
+ }
+ if (j === all.length) {
+ annotation = this._createFoldingAnnotation(viewModel, baseModel, comment.start, comment.end);
+ if (annotation) {
+ add.push(annotation);
+ }
+ }
+ }
+ annotationModel.replaceAnnotations(remove, add);
+ }
+ }
+ };
+
+ return {TextStyler: TextStyler};
+});
diff --git a/scratchpad/content/overlay.js b/scratchpad/content/overlay.js
new file mode 100644
index 00000000..00747998
--- /dev/null
+++ b/scratchpad/content/overlay.js
@@ -0,0 +1,11 @@
+/* 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/. */
+
+Components.utils.import("resource://scratchpad/scratchpad-manager.jsm");
+
+function toScratchpad() {
+ ScratchpadManager.openScratchpad();
+}
+
+
diff --git a/scratchpad/content/overlay.xul b/scratchpad/content/overlay.xul
new file mode 100644
index 00000000..8f4d055d
--- /dev/null
+++ b/scratchpad/content/overlay.xul
@@ -0,0 +1,31 @@
+
+
+
+
+
+%overlayDTD;
+]>
+
+
+
+
+
+
+
+
+
+
diff --git a/scratchpad/content/scratchpad.js b/scratchpad/content/scratchpad.js
new file mode 100644
index 00000000..955328fa
--- /dev/null
+++ b/scratchpad/content/scratchpad.js
@@ -0,0 +1,1127 @@
+/* vim:set ts=2 sw=2 sts=2 et:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Scratchpad.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Rob Campbell (original author)
+ * Erik Vold
+ * David Dahl
+ * Mihai Sucan
+ * Kenny Heaton
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+/*
+ * Original version history can be found here:
+ * https://github.com/mozilla/workspace
+ *
+ * Copied and relicensed from the Public Domain.
+ * See bug 653934 for details.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=653934
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://scratchpad/PropertyPanel.jsm");
+Cu.import("resource://scratchpad/source-editor.jsm");
+Cu.import("resource://scratchpad/scratchpad-manager.jsm");
+
+
+const SCRATCHPAD_CONTEXT_CONTENT = 1;
+const SCRATCHPAD_CONTEXT_BROWSER = 2;
+const SCRATCHPAD_L10N = "chrome://scratchpad/locale/scratchpad.properties";
+const DEVTOOLS_CHROME_ENABLED = "scratchpad.chrome.enabled";
+const BUTTON_POSITION_SAVE = 0;
+const BUTTON_POSITION_CANCEL = 1;
+const BUTTON_POSITION_DONT_SAVE = 2;
+
+/**
+ * The scratchpad object handles the Scratchpad window functionality.
+ */
+var Scratchpad = {
+ _initialWindowTitle: document.title,
+
+ /**
+ * The script execution context. This tells Scratchpad in which context the
+ * script shall execute.
+ *
+ * Possible values:
+ * - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
+ * tab content window object.
+ * - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
+ * currently active chrome window object.
+ */
+ executionContext: SCRATCHPAD_CONTEXT_CONTENT,
+
+ /**
+ * Tells if this Scratchpad is initialized and ready for use.
+ * @boolean
+ * @see addObserver
+ */
+ initialized: false,
+
+ /**
+ * Retrieve the xul:notificationbox DOM element. It notifies the user when
+ * the current code execution context is SCRATCHPAD_CONTEXT_BROWSER.
+ */
+ get notificationBox() document.getElementById("scratchpad-notificationbox"),
+
+ /**
+ * Get the selected text from the editor.
+ *
+ * @return string
+ * The selected text.
+ */
+ get selectedText() this.editor.getSelectedText(),
+
+ /**
+ * Get the editor content, in the given range. If no range is given you get
+ * the entire editor content.
+ *
+ * @param number [aStart=0]
+ * Optional, start from the given offset.
+ * @param number [aEnd=content char count]
+ * Optional, end offset for the text you want. If this parameter is not
+ * given, then the text returned goes until the end of the editor
+ * content.
+ * @return string
+ * The text in the given range.
+ */
+ getText: function SP_getText(aStart, aEnd)
+ {
+ return this.editor.getText(aStart, aEnd);
+ },
+
+ /**
+ * Replace text in the source editor with the given text, in the given range.
+ *
+ * @param string aText
+ * The text you want to put into the editor.
+ * @param number [aStart=0]
+ * Optional, the start offset, zero based, from where you want to start
+ * replacing text in the editor.
+ * @param number [aEnd=char count]
+ * Optional, the end offset, zero based, where you want to stop
+ * replacing text in the editor.
+ */
+ setText: function SP_setText(aText, aStart, aEnd)
+ {
+ this.editor.setText(aText, aStart, aEnd);
+ },
+
+ /**
+ * Set the filename in the scratchpad UI and object
+ *
+ * @param string aFilename
+ * The new filename
+ */
+ setFilename: function SP_setFilename(aFilename)
+ {
+ this.filename = aFilename;
+ this._updateTitle();
+ },
+
+ /**
+ * Update the Scratchpad window title based on the current state.
+ * @private
+ */
+ _updateTitle: function SP__updateTitle()
+ {
+ if (this.filename) {
+ document.title = (this.editor && this.editor.dirty ? "*" : "") +
+ this.filename;
+ } else {
+ document.title = this._initialWindowTitle;
+ }
+ },
+
+ /**
+ * Get the current state of the scratchpad. Called by the
+ * Scratchpad Manager for session storing.
+ *
+ * @return object
+ * An object with 3 properties: filename, text, and
+ * executionContext.
+ */
+ getState: function SP_getState()
+ {
+ return {
+ filename: this.filename,
+ text: this.getText(),
+ executionContext: this.executionContext,
+ saved: !this.editor.dirty,
+ };
+ },
+
+ /**
+ * Set the filename and execution context using the given state. Called
+ * when scratchpad is being restored from a previous session.
+ *
+ * @param object aState
+ * An object with filename and executionContext properties.
+ */
+ setState: function SP_getState(aState)
+ {
+ if (aState.filename) {
+ this.setFilename(aState.filename);
+ }
+ if (this.editor) {
+ this.editor.dirty = !aState.saved;
+ }
+
+ if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+ this.setBrowserContext();
+ }
+ else {
+ this.setContentContext();
+ }
+ },
+
+ /**
+ * Get the most recent chrome window of type navigator:browser.
+ */
+ get browserWindow() {
+ // Web Browsers
+ var chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
+
+ // Interlink
+ if (!chromeWindow) {
+ chromeWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ }
+
+ // Make sure we have a window
+ if (!chromeWindow) {
+ return null;
+ }
+
+ return chromeWindow;
+ },
+
+ /**
+ * Reference to the last chrome window of type navigator:browser. We use this
+ * to check if the chrome window changed since the last code evaluation.
+ */
+ _previousWindow: null,
+
+ /**
+ * Get the gBrowser object of the most recent browser window.
+ */
+ get gBrowser()
+ {
+ let recentWin = this.browserWindow;
+ return recentWin ? recentWin.gBrowser : null;
+ },
+
+ /**
+ * Cached Cu.Sandbox object for the active tab content window object.
+ */
+ _contentSandbox: null,
+
+ /**
+ * Get the Cu.Sandbox object for the active tab content window object. Note
+ * that the returned object is cached for later reuse. The cached object is
+ * kept only for the current location in the current tab of the current
+ * browser window and it is reset for each context switch,
+ * navigator:browser window switch, tab switch or navigation.
+ */
+ get contentSandbox()
+ {
+ if (!this.browserWindow || !this.gBrowser) {
+ var errorString = this.strings.GetStringFromName("browserWindow.unavailable");
+ Services.prompt.alert(null, "Content Environment", errorString);
+ Cu.reportError(errorString);
+ return;
+ }
+
+ if (!this._contentSandbox ||
+ this.browserWindow != this._previousBrowserWindow ||
+ this._previousBrowser != this.gBrowser.selectedBrowser ||
+ this._previousLocation != this.gBrowser.contentWindow.location.href) {
+ let contentWindow = this.gBrowser.selectedBrowser.contentWindow;
+ this._contentSandbox = new Cu.Sandbox(contentWindow,
+ { sandboxPrototype: contentWindow, wantXrays: false,
+ sandboxName: 'scratchpad-content'});
+
+ this._previousBrowserWindow = this.browserWindow;
+ this._previousBrowser = this.gBrowser.selectedBrowser;
+ this._previousLocation = contentWindow.location.href;
+ }
+
+ return this._contentSandbox;
+ },
+
+ /**
+ * Cached Cu.Sandbox object for the most recently active navigator:browser
+ * chrome window object.
+ */
+ _chromeSandbox: null,
+
+ /**
+ * Get the Cu.Sandbox object for the most recently active navigator:browser
+ * chrome window object. Note that the returned object is cached for later
+ * reuse. The cached object is kept only for the current browser window and it
+ * is reset for each context switch or navigator:browser window switch.
+ */
+ get chromeSandbox()
+ {
+ if (!this.browserWindow) {
+ var errorString = this.strings.GetStringFromName("chromeWindow.unavailable");
+ Services.prompt.alert(null, "Chrome Environment", errorString);
+ Cu.reportError(errorString);
+ return;
+ }
+
+ if (!this._chromeSandbox ||
+ this.browserWindow != this._previousBrowserWindow) {
+ this._chromeSandbox = new Cu.Sandbox(this.browserWindow,
+ { sandboxPrototype: this.browserWindow, wantXrays: false,
+ sandboxName: 'scratchpad-chrome'});
+
+ this._previousBrowserWindow = this.browserWindow;
+ }
+
+ return this._chromeSandbox;
+ },
+
+ /**
+ * Drop the editor selection.
+ */
+ deselect: function SP_deselect()
+ {
+ this.editor.dropSelection();
+ },
+
+ /**
+ * Select a specific range in the Scratchpad editor.
+ *
+ * @param number aStart
+ * Selection range start.
+ * @param number aEnd
+ * Selection range end.
+ */
+ selectRange: function SP_selectRange(aStart, aEnd)
+ {
+ this.editor.setSelection(aStart, aEnd);
+ },
+
+ /**
+ * Get the current selection range.
+ *
+ * @return object
+ * An object with two properties, start and end, that give the
+ * selection range (zero based offsets).
+ */
+ getSelectionRange: function SP_getSelection()
+ {
+ return this.editor.getSelection();
+ },
+
+ /**
+ * Evaluate a string in the active tab content window.
+ *
+ * @param string aString
+ * The script you want evaluated.
+ * @return mixed
+ * The script evaluation result.
+ */
+ evalInContentSandbox: function SP_evalInContentSandbox(aString)
+ {
+ let error, result;
+ try {
+ result = Cu.evalInSandbox(aString, this.contentSandbox, "1.8",
+ "Scratchpad", 1);
+ }
+ catch (ex) {
+ error = ex;
+ }
+
+ return [error, result];
+ },
+
+ /**
+ * Evaluate a string in the most recent navigator:browser chrome window.
+ *
+ * @param string aString
+ * The script you want evaluated.
+ * @return mixed
+ * The script evaluation result.
+ */
+ evalInChromeSandbox: function SP_evalInChromeSandbox(aString)
+ {
+ let error, result;
+ try {
+ result = Cu.evalInSandbox(aString, this.chromeSandbox, "1.8",
+ "Scratchpad", 1);
+ }
+ catch (ex) {
+ error = ex;
+ }
+
+ return [error, result];
+ },
+
+ /**
+ * Evaluate a string in the currently desired context, that is either the
+ * chrome window or the tab content window object.
+ *
+ * @param string aString
+ * The script you want to evaluate.
+ * @return mixed
+ * The script evaluation result.
+ */
+ evalForContext: function SP_evaluateForContext(aString)
+ {
+ return this.executionContext == SCRATCHPAD_CONTEXT_CONTENT ?
+ this.evalInContentSandbox(aString) :
+ this.evalInChromeSandbox(aString);
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ * @return mixed
+ * The script evaluation result.
+ */
+ execute: function SP_execute()
+ {
+ let selection = this.selectedText || this.getText();
+ let [error, result] = this.evalForContext(selection);
+ return [selection, error, result];
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context.
+ */
+ run: function SP_run()
+ {
+ let [selection, error, result] = this.execute();
+
+ if (!error) {
+ this.deselect();
+ } else {
+ this.writeAsErrorComment(error);
+ }
+
+ return [selection, error, result];
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The resulting object is opened up in the Property Panel
+ * for inspection.
+ */
+ inspect: function SP_inspect()
+ {
+ let [selection, error, result] = this.execute();
+
+ if (!error) {
+ this.deselect();
+ this.openPropertyPanel(selection, result);
+ } else {
+ this.writeAsErrorComment(error);
+ }
+ },
+
+ /**
+ * Execute the selected text (if any) or the entire editor content in the
+ * current context. The evaluation result is inserted into the editor after
+ * the selected text, or at the end of the editor content if there is no
+ * selected text.
+ */
+ display: function SP_display()
+ {
+ let [selectedText, error, result] = this.execute();
+
+ if (!error) {
+ this.writeAsComment(result);
+ } else {
+ this.writeAsErrorComment(error);
+ }
+ },
+
+ /**
+ * Write out a value at the next line from the current insertion point.
+ * The comment block will always be preceded by a newline character.
+ * @param object aValue
+ * The Object to write out as a string
+ */
+ writeAsComment: function SP_writeAsComment(aValue)
+ {
+ let selection = this.getSelectionRange();
+ let insertionPoint = selection.start != selection.end ?
+ selection.end : // after selected text
+ this.editor.getCharCount(); // after text end
+
+ let newComment = "\n/*\n" + aValue + "\n*/";
+
+ this.setText(newComment, insertionPoint, insertionPoint);
+
+ // Select the new comment.
+ this.selectRange(insertionPoint, insertionPoint + newComment.length);
+ },
+
+ /**
+ * Write out an error at the current insertion point as a block comment
+ * @param object aValue
+ * The Error object to write out the message and stack trace
+ */
+ writeAsErrorComment: function SP_writeAsErrorComment(aError)
+ {
+ let stack = aError.stack || aError.fileName + ":" + aError.lineNumber;
+ let newComment = "Exception: " + aError.message + "\n" + stack.replace(/\n$/, "");
+
+ this.writeAsComment(newComment);
+ },
+
+ /**
+ * Open the Property Panel to inspect the given object.
+ *
+ * @param string aEvalString
+ * The string that was evaluated. This is re-used when the user updates
+ * the properties list, by clicking the Update button.
+ * @param object aOutputObject
+ * The object to inspect, which is the aEvalString evaluation result.
+ * @return object
+ * The PropertyPanel object instance.
+ */
+ openPropertyPanel: function SP_openPropertyPanel(aEvalString, aOutputObject)
+ {
+ let self = this;
+ let propPanel;
+ // The property panel has a button:
+ // `Update`: reexecutes the string executed on the command line. The
+ // result will be inspected by this panel.
+ let buttons = [];
+
+ // If there is a evalString passed to this function, then add a `Update`
+ // button to the panel so that the evalString can be reexecuted to update
+ // the content of the panel.
+ if (aEvalString !== null) {
+ buttons.push({
+ label: this.strings.
+ GetStringFromName("propertyPanel.updateButton.label"),
+ accesskey: this.strings.
+ GetStringFromName("propertyPanel.updateButton.accesskey"),
+ oncommand: function () {
+ let [error, result] = self.evalForContext(aEvalString);
+
+ if (!error) {
+ propPanel.treeView.data = result;
+ }
+ }
+ });
+ }
+
+ let doc = this.browserWindow.document;
+ let parent = doc.getElementById("mainPopupSet");
+ let title = aOutputObject.toString();
+ propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons);
+
+ let panel = propPanel.panel;
+ panel.setAttribute("class", "scratchpad_propertyPanel");
+ panel.openPopup(null, "after_pointer", 0, 0, false, false);
+ panel.sizeTo(200, 400);
+
+ return propPanel;
+ },
+
+ // Menu Operations
+
+ /**
+ * Open a new Scratchpad window.
+ *
+ * @return nsIWindow
+ */
+ openScratchpad: function SP_openScratchpad()
+ {
+ return ScratchpadManager.openScratchpad();
+ },
+
+ /**
+ * Export the textbox content to a file.
+ *
+ * @param nsILocalFile aFile
+ * The file where you want to save the textbox content.
+ * @param boolean aNoConfirmation
+ * If the file already exists, ask for confirmation?
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file save fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file save completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the export operation.
+ */
+ exportToFile: function SP_exportToFile(aFile, aNoConfirmation, aSilentError,
+ aCallback)
+ {
+ if (!aNoConfirmation && aFile.exists() &&
+ !window.confirm(this.strings.
+ GetStringFromName("export.fileOverwriteConfirmation"))) {
+ return;
+ }
+
+ let fs = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ let modeFlags = 0x02 | 0x08 | 0x20;
+ fs.init(aFile, modeFlags, 420 /* 0644 */, fs.DEFER_OPEN);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let input = converter.convertToInputStream(this.getText());
+
+ let self = this;
+ NetUtil.asyncCopy(input, fs, function(aStatus) {
+ if (!aSilentError && !Components.isSuccessCode(aStatus)) {
+ window.alert(self.strings.GetStringFromName("saveFile.failed"));
+ }
+
+ if (aCallback) {
+ aCallback.call(self, aStatus);
+ }
+ });
+ },
+
+ /**
+ * Read the content of a file and put it into the textbox.
+ *
+ * @param nsILocalFile aFile
+ * The file you want to save the textbox content into.
+ * @param boolean aSilentError
+ * True if you do not want to display an error when file load fails,
+ * false otherwise.
+ * @param function aCallback
+ * Optional function you want to call when file load completes. It will
+ * get the following arguments:
+ * 1) the nsresult status code for the import operation.
+ * 2) the data that was read from the file, if any.
+ */
+ importFromFile: function SP_importFromFile(aFile, aSilentError, aCallback)
+ {
+ // Prevent file type detection.
+ let channel = NetUtil.newChannel(aFile);
+ channel.contentType = "application/javascript";
+
+ let self = this;
+ NetUtil.asyncFetch(channel, function(aInputStream, aStatus) {
+ let content = null;
+
+ if (Components.isSuccessCode(aStatus)) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ content = NetUtil.readInputStreamToString(aInputStream,
+ aInputStream.available());
+ content = converter.ConvertToUnicode(content);
+ self.setText(content);
+ self.editor.resetUndo();
+ }
+ else if (!aSilentError) {
+ window.alert(self.strings.GetStringFromName("openFile.failed"));
+ }
+
+ if (aCallback) {
+ aCallback.call(self, aStatus, content);
+ }
+ });
+ },
+
+ /**
+ * Open a file to edit in the Scratchpad.
+ */
+ openFile: function SP_openFile()
+ {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, this.strings.GetStringFromName("openFile.title"),
+ Ci.nsIFilePicker.modeOpen);
+ fp.defaultString = "";
+ if (fp.show() != Ci.nsIFilePicker.returnCancel) {
+ this.setFilename(fp.file.path);
+ this.importFromFile(fp.file, false);
+ }
+ },
+
+ /**
+ * Save the textbox content to the currently open file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFile: function SP_saveFile(aCallback)
+ {
+ if (!this.filename) {
+ return this.saveFileAs(aCallback);
+ }
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.filename);
+
+ this.exportToFile(file, true, false, function(aStatus) {
+ if (Components.isSuccessCode(aStatus)) {
+ this.editor.dirty = false;
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ },
+
+ /**
+ * Save the textbox content to a new file.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ saveFileAs: function SP_saveFileAs(aCallback)
+ {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, this.strings.GetStringFromName("saveFileAs"),
+ Ci.nsIFilePicker.modeSave);
+ fp.defaultString = "scratchpad.js";
+ if (fp.show() != Ci.nsIFilePicker.returnCancel) {
+ this.setFilename(fp.file.path);
+
+ this.exportToFile(fp.file, true, false, function(aStatus) {
+ if (Components.isSuccessCode(aStatus)) {
+ this.editor.dirty = false;
+ }
+ if (aCallback) {
+ aCallback(aStatus);
+ }
+ });
+ }
+ },
+
+ /**
+ * Open the Error Console.
+ */
+ openErrorConsole: function SP_openErrorConsole()
+ {
+ this.browserWindow.toJavaScriptConsole();
+ },
+
+ /**
+ * Open the Web Console.
+ */
+ openWebConsole: function SP_openWebConsole()
+ {
+ if (!this.browserWindow.HUDConsoleUI.getOpenHUD()) {
+ this.browserWindow.HUDConsoleUI.toggleHUD();
+ }
+ this.browserWindow.focus();
+ },
+
+ /**
+ * Set the current execution context to be the active tab content window.
+ */
+ setContentContext: function SP_setContentContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_CONTENT) {
+ return;
+ }
+
+ let content = document.getElementById("sp-menu-content");
+ document.getElementById("sp-menu-browser").removeAttribute("checked");
+ content.setAttribute("checked", true);
+ this.executionContext = SCRATCHPAD_CONTEXT_CONTENT;
+ this.notificationBox.removeAllNotifications(false);
+ this.resetContext();
+ },
+
+ /**
+ * Set the current execution context to be the most recent chrome window.
+ */
+ setBrowserContext: function SP_setBrowserContext()
+ {
+ if (this.executionContext == SCRATCHPAD_CONTEXT_BROWSER) {
+ return;
+ }
+
+ let browser = document.getElementById("sp-menu-browser");
+ document.getElementById("sp-menu-content").removeAttribute("checked");
+ browser.setAttribute("checked", true);
+ this.executionContext = SCRATCHPAD_CONTEXT_BROWSER;
+ this.notificationBox.appendNotification(
+ this.strings.GetStringFromName("browserContext.notification"),
+ SCRATCHPAD_CONTEXT_BROWSER,
+ null,
+ this.notificationBox.PRIORITY_WARNING_HIGH,
+ null);
+ this.resetContext();
+ },
+
+ /**
+ * Reset the cached Cu.Sandbox object for the current context.
+ */
+ resetContext: function SP_resetContext()
+ {
+ this._chromeSandbox = null;
+ this._contentSandbox = null;
+ this._previousWindow = null;
+ this._previousBrowser = null;
+ this._previousLocation = null;
+ },
+
+ /**
+ * Gets the ID of the inner window of the given DOM window object.
+ *
+ * @param nsIDOMWindow aWindow
+ * @return integer
+ * the inner window ID
+ */
+ getInnerWindowId: function SP_getInnerWindowId(aWindow)
+ {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ },
+
+ /**
+ * The Scratchpad window load event handler. This method
+ * initializes the Scratchpad window and source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onLoad: function SP_onLoad(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+ let chrome = true;
+ if (chrome) {
+ let environmentMenu = document.getElementById("sp-environment-menu");
+ let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole");
+ let browserContextCommand = document.getElementById("sp-menu-content");
+ let chromeContextCommand = document.getElementById("sp-cmd-browserContext");
+ environmentMenu.removeAttribute("hidden");
+ chromeContextCommand.removeAttribute("disabled");
+ errorConsoleCommand.removeAttribute("disabled");
+ if (!this.gBrowser) {
+ this.setBrowserContext();
+ }
+ }
+
+ let state = null;
+ let initialText = this.strings.GetStringFromName("scratchpadIntro");
+ if ("arguments" in window &&
+ window.arguments[0] instanceof Ci.nsIDialogParamBlock) {
+ state = JSON.parse(window.arguments[0].GetString(0));
+ this.setState(state);
+ initialText = state.text;
+ }
+
+ this.editor = new SourceEditor();
+
+ let config = {
+ mode: SourceEditor.MODES.JAVASCRIPT,
+ showLineNumbers: true,
+ initialText: initialText,
+ contextMenu: "scratchpad-text-popup",
+ };
+
+ let editorPlaceholder = document.getElementById("scratchpad-editor");
+ this.editor.init(editorPlaceholder, config,
+ this._onEditorLoad.bind(this, state));
+ },
+
+ /**
+ * The load event handler for the source editor. This method does post-load
+ * editor initialization.
+ *
+ * @private
+ * @param object aState
+ * The initial Scratchpad state object.
+ */
+ _onEditorLoad: function SP__onEditorLoad(aState)
+ {
+ this.editor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ this.editor.focus();
+ this.editor.setCaretOffset(this.editor.getCharCount());
+ if (aState) {
+ this.editor.dirty = !aState.saved;
+ }
+
+ this.initialized = true;
+
+ this._triggerObservers("Ready");
+ },
+
+ /**
+ * Insert text at the current caret location.
+ *
+ * @param string aText
+ * The text you want to insert.
+ */
+ insertTextAtCaret: function SP_insertTextAtCaret(aText)
+ {
+ let caretOffset = this.editor.getCaretOffset();
+ this.setText(aText, caretOffset, caretOffset);
+ this.editor.setCaretOffset(caretOffset + aText.length);
+ },
+
+ /**
+ * The Source Editor DirtyChanged event handler. This function updates the
+ * Scratchpad window title to show an asterisk when there are unsaved changes.
+ *
+ * @private
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @param object aEvent
+ * The DirtyChanged event object.
+ */
+ _onDirtyChanged: function SP__onDirtyChanged(aEvent)
+ {
+ Scratchpad._updateTitle();
+ },
+
+ /**
+ * Undo the last action of the user.
+ */
+ undo: function SP_undo()
+ {
+ this.editor.undo();
+ },
+
+ /**
+ * Redo the previously undone action.
+ */
+ redo: function SP_redo()
+ {
+ this.editor.redo();
+ },
+
+ /**
+ * The Scratchpad window unload event handler. This method unloads/destroys
+ * the source editor.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onUnload: function SP_onUnload(aEvent)
+ {
+ if (aEvent.target != document) {
+ return;
+ }
+
+ this.resetContext();
+ this.editor.removeEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ this.editor.destroy();
+ this.editor = null;
+ this.initialized = false;
+ },
+
+ /**
+ * Prompt to save scratchpad if it has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved. The callback
+ * receives three arguments:
+ * - toClose (boolean) - tells if the window should be closed.
+ * - saved (boolen) - tells if the file has been saved.
+ * - status (number) - the file save status result (if the file was
+ * saved).
+ * @return boolean
+ * Whether the window should be closed
+ */
+ promptSave: function SP_promptSave(aCallback)
+ {
+ if (this.filename && this.editor.dirty) {
+ let ps = Services.prompt;
+ let flags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_SAVE +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL +
+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_DONT_SAVE;
+
+ let button = ps.confirmEx(window,
+ this.strings.GetStringFromName("confirmClose.title"),
+ this.strings.GetStringFromName("confirmClose"),
+ flags, null, null, null, null, {});
+
+ if (button == BUTTON_POSITION_CANCEL) {
+ if (aCallback) {
+ aCallback(false, false);
+ }
+ return false;
+ }
+
+ if (button == BUTTON_POSITION_SAVE) {
+ this.saveFile(function(aStatus) {
+ if (aCallback) {
+ aCallback(true, true, aStatus);
+ }
+ });
+ return true;
+ }
+ }
+
+ if (aCallback) {
+ aCallback(true, false);
+ }
+ return true;
+ },
+
+ /**
+ * Handler for window close event. Prompts to save scratchpad if
+ * there are unsaved changes.
+ *
+ * @param nsIDOMEvent aEvent
+ */
+ onClose: function SP_onClose(aEvent)
+ {
+ if (this._skipClosePrompt) {
+ return;
+ }
+
+ this.promptSave(function(aShouldClose, aSaved, aStatus) {
+ let shouldClose = aShouldClose;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldClose = false;
+ }
+
+ if (shouldClose) {
+ this._skipClosePrompt = true;
+ window.close();
+ }
+ }.bind(this));
+ aEvent.preventDefault();
+ },
+
+ /**
+ * Close the scratchpad window. Prompts before closing if the scratchpad
+ * has unsaved changes.
+ *
+ * @param function aCallback
+ * Optional function you want to call when file is saved
+ */
+ close: function SP_close(aCallback)
+ {
+ this.promptSave(function(aShouldClose, aSaved, aStatus) {
+ let shouldClose = aShouldClose;
+ if (aSaved && !Components.isSuccessCode(aStatus)) {
+ shouldClose = false;
+ }
+
+ if (shouldClose) {
+ this._skipClosePrompt = true;
+ window.close();
+ }
+ if (aCallback) {
+ aCallback();
+ }
+ }.bind(this));
+ },
+
+ _observers: [],
+
+ /**
+ * Add an observer for Scratchpad events.
+ *
+ * The observer implements IScratchpadObserver := {
+ * onReady: Called when the Scratchpad and its SourceEditor are ready.
+ * Arguments: (Scratchpad aScratchpad)
+ * }
+ *
+ * All observer handlers are optional.
+ *
+ * @param IScratchpadObserver aObserver
+ * @see removeObserver
+ */
+ addObserver: function SP_addObserver(aObserver)
+ {
+ this._observers.push(aObserver);
+ },
+
+ /**
+ * Remove an observer for Scratchpad events.
+ *
+ * @param IScratchpadObserver aObserver
+ * @see addObserver
+ */
+ removeObserver: function SP_removeObserver(aObserver)
+ {
+ let index = this._observers.indexOf(aObserver);
+ if (index != -1) {
+ this._observers.splice(index, 1);
+ }
+ },
+
+ /**
+ * Trigger named handlers in Scratchpad observers.
+ *
+ * @param string aName
+ * Name of the handler to trigger.
+ * @param Array aArgs
+ * Optional array of arguments to pass to the observer(s).
+ * @see addObserver
+ */
+ _triggerObservers: function SP_triggerObservers(aName, aArgs)
+ {
+ // insert this Scratchpad instance as the first argument
+ if (!aArgs) {
+ aArgs = [this];
+ } else {
+ aArgs.unshift(this);
+ }
+
+ // trigger all observers that implement this named handler
+ for (let i = 0; i < this._observers.length; ++i) {
+ let observer = this._observers[i];
+ let handler = observer["on" + aName];
+ if (handler) {
+ handler.apply(observer, aArgs);
+ }
+ }
+ },
+
+ openDocumentationPage: function SP_openDocumentationPage()
+ {
+ let url = this.strings.GetStringFromName("help.openDocumentationPage");
+ let newTab = this.gBrowser.addTab(url);
+ this.browserWindow.focus();
+ this.gBrowser.selectedTab = newTab;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(Scratchpad, "strings", function () {
+ return Services.strings.createBundle(SCRATCHPAD_L10N);
+});
+
+addEventListener("load", Scratchpad.onLoad.bind(Scratchpad), false);
+addEventListener("unload", Scratchpad.onUnload.bind(Scratchpad), false);
+addEventListener("close", Scratchpad.onClose.bind(Scratchpad), false);
diff --git a/scratchpad/content/scratchpad.xul b/scratchpad/content/scratchpad.xul
new file mode 100644
index 00000000..87c4a62a
--- /dev/null
+++ b/scratchpad/content/scratchpad.xul
@@ -0,0 +1,299 @@
+
+
+
+
+ %scratchpadDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scratchpad/content/source-editor-overlay.xul b/scratchpad/content/source-editor-overlay.xul
new file mode 100644
index 00000000..d17dd8f7
--- /dev/null
+++ b/scratchpad/content/source-editor-overlay.xul
@@ -0,0 +1,237 @@
+
+
+
+ %editMenuStrings;
+
+ %sourceEditorStrings;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef XP_UNIX
+
+#else
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scratchpad/install.rdf b/scratchpad/install.rdf
new file mode 100644
index 00000000..aa2e224a
--- /dev/null
+++ b/scratchpad/install.rdf
@@ -0,0 +1,39 @@
+
+
+
+
+#filter substitution
+
+
+
+
+#ifdef MOZ_DISABLE_PLATFORM
+
+
+
+
+
+
+#else
+
+
+
+ true
+#endif
+
+
diff --git a/scratchpad/jar.mn b/scratchpad/jar.mn
new file mode 100644
index 00000000..abb82c5b
--- /dev/null
+++ b/scratchpad/jar.mn
@@ -0,0 +1,42 @@
+# 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/.
+
+[.] chrome.jar:
+% content scratchpad %content/
+ content/overlay.js (content/overlay.js)
+ content/overlay.xul (content/overlay.xul)
+ content/scratchpad.js (content/scratchpad.js)
+ content/scratchpad.xul (content/scratchpad.xul)
+ content/source-editor-overlay.xul (content/source-editor-overlay.xul)
+ content/orion/LICENSE (content/orion/LICENSE)
+ content/orion/orion.css (content/orion/orion.css)
+ content/orion/orion.js (content/orion/orion.js)
+
+% locale scratchpad en-US %locale/
+ locale/overlay.dtd (locale/overlay.dtd)
+ locale/scratchpad.dtd (locale/scratchpad.dtd)
+ locale/scratchpad.properties (locale/scratchpad.properties)
+ locale/sourceeditor.dtd (locale/sourceeditor.dtd)
+ locale/sourceeditor.properties (locale/sourceeditor.properties)
+
+% skin scratchpad classic/1.0 %skin/classic/
+ skin/classic/orion.css (skin/classic/orion.css)
+ skin/classic/orion-container.css (skin/classic/orion-container.css)
+ skin/classic/scratchpad.css (skin/classic/scratchpad.css)
+ skin/classic/orion-breakpoint.png (skin/classic/orion-breakpoint.png)
+ skin/classic/orion-debug-location.png (skin/classic/orion-debug-location.png)
+ skin/classic/orion-task.png (skin/classic/orion-task.png)
+
+% skin scratchpad modern/1.0 %skin/modern/
+ skin/modern/orion.css (skin/modern/orion.css)
+ skin/modern/orion-container.css (skin/modern/orion-container.css)
+ skin/modern/scratchpad.css (skin/modern/scratchpad.css)
+ skin/modern/orion-breakpoint.png (skin/modern/orion-breakpoint.png)
+ skin/modern/orion-debug-location.png (skin/modern/orion-debug-location.png)
+ skin/modern/orion-task.png (skin/modern/orion-task.png)
+
+% resource scratchpad modules/
+
+% overlay chrome://browser/content/browser.xul chrome://profile-switcher/content/overlay.xul
+% overlay chrome://messenger/content/messenger.xul chrome://profile-switcher/content/overlay.xul
\ No newline at end of file
diff --git a/scratchpad/locale/overlay.dtd b/scratchpad/locale/overlay.dtd
new file mode 100644
index 00000000..476ebcbb
--- /dev/null
+++ b/scratchpad/locale/overlay.dtd
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/scratchpad/locale/scratchpad.dtd b/scratchpad/locale/scratchpad.dtd
new file mode 100644
index 00000000..d515aa26
--- /dev/null
+++ b/scratchpad/locale/scratchpad.dtd
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scratchpad/locale/scratchpad.properties b/scratchpad/locale/scratchpad.properties
new file mode 100644
index 00000000..f1e8e352
--- /dev/null
+++ b/scratchpad/locale/scratchpad.properties
@@ -0,0 +1,61 @@
+# LOCALIZATION NOTE These strings are used inside the JavaScript scratchpad
+# which is available from the Web Developer sub-menu -> 'Scratchpad'.
+#
+# The correct localization of this file might be to keep it in
+# English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best
+# documentation on web development on the web.
+
+# LOCALIZATION NOTE (propertyPanel.updateButton.label): Used in the Property
+# Panel that is opened by the Scratchpad window when inspecting an object. This
+# is the Update button label.
+propertyPanel.updateButton.label=Update
+propertyPanel.updateButton.accesskey=U
+
+# LOCALIZATION NOTE (export.fileOverwriteConfirmation): This is displayed when
+# the user attempts to save to an already existing file.
+export.fileOverwriteConfirmation=File exists. Overwrite?
+
+# LOCALIZATION NOTE (browserWindow.unavailable): This error message is shown
+# when Scratchpad does not find any recently active window of navigator:browser
+# type.
+browserWindow.unavailable=Scratchpad cannot find a browser element to execute the code in.\n\n Please ensure a browser widnow is open or switch to Chrome Environment for non-browser applications.
+chromeWindow.unavailable=Scratchpad cannot find a known chrome window to execute the code in. This is normally the application's main window.\n\n If you closed it, please reopen it.
+
+# LOCALIZATION NOTE (openFile.title): This is the file picker title, when you
+# open a file from Scratchpad.
+openFile.title=Open File
+
+# LOCALIZATION NOTE (openFile.failed): This is the message displayed when file
+# open fails.
+openFile.failed=Failed to read the file.
+
+# LOCALIZATION NOTE (saveFileAs): This is the file picker title, when you save
+# a file in Scratchpad.
+saveFileAs=Save File As
+
+# LOCALIZATION NOTE (saveFile.failed): This is the message displayed when file
+# save fails.
+saveFile.failed=The file save operation failed.
+
+# LOCALIZATION NOTE (confirmClose): This is message in the prompt dialog when
+# you try to close a scratchpad with unsaved changes.
+confirmClose=Do you want to save the changes you made to this scratchpad?
+
+# LOCALIZATION NOTE (confirmClose.title): This is title of the prompt dialog when
+# you try to close a scratchpad with unsaved changes.
+confirmClose.title=Unsaved Changes
+
+# LOCALIZATION NOTE (scratchpadIntro): This is a multi-line comment explaining
+# how to use the Scratchpad. Note that this should be a valid JavaScript
+# comment inside /* and */.
+scratchpadIntro=/*\n * This is a JavaScript Scratchpad.\n *\n * Enter some JavaScript, then Right Click or choose from the Execute Menu:\n * 1. Run to evaluate the selected text,\n * 2. Inspect to bring up an Object Inspector on the result, or,\n * 3. Display to insert the result in a comment after the selection.\n */\n\n
+
+# LOCALIZATION NOTE (notification.browserContext): This is the message displayed
+# over the top of the editor when the user has switched to browser context.
+browserContext.notification=This scratchpad executes in the chrome context.
+
+# LOCALIZATION NOTE (help.openDocumentationPage): This returns a localized link with
+# documentation for Scratchpad on MDN.
+help.openDocumentationPage=https://developer.mozilla.org/en/Tools/Scratchpad
diff --git a/scratchpad/locale/sourceeditor.dtd b/scratchpad/locale/sourceeditor.dtd
new file mode 100644
index 00000000..e512028b
--- /dev/null
+++ b/scratchpad/locale/sourceeditor.dtd
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/scratchpad/locale/sourceeditor.properties b/scratchpad/locale/sourceeditor.properties
new file mode 100644
index 00000000..35746064
--- /dev/null
+++ b/scratchpad/locale/sourceeditor.properties
@@ -0,0 +1,46 @@
+# LOCALIZATION NOTE These strings are used inside the Source Editor component.
+# This component is used whenever source code is displayed for the purpose of
+# being edited, inside the Firefox developer tools - current examples are the
+# Scratchpad and the Style Editor tools.
+
+# LOCALIZATION NOTE The correct localization of this file might be to keep it
+# in English, or another language commonly spoken among web developers.
+# You want to make that choice consistent across the developer tools.
+# A good criteria is the language in which you'd find the best documentation
+# on web development on the web.
+
+# LOCALIZATION NOTE (findCmd.promptTitle): This is the dialog title used
+# when the user wants to search for a string in the code. You can
+# access this feature by pressing Ctrl-F on Windows/Linux or Cmd-F on Mac.
+findCmd.promptTitle=Find…
+
+# LOCALIZATION NOTE (gotoLineCmd.promptMessage): This is the message shown when
+# the user wants to search for a string in the code. You can
+# access this feature by pressing Ctrl-F on Windows/Linux or Cmd-F on Mac.
+findCmd.promptMessage=Search for:
+
+# LOCALIZATION NOTE (gotoLineCmd.promptTitle): This is the dialog title used
+# when the user wants to jump to a specific line number in the code. You can
+# access this feature by pressing Ctrl-J on Windows/Linux or Cmd-J on Mac.
+gotoLineCmd.promptTitle=Go to line…
+
+# LOCALIZATION NOTE (gotoLineCmd.promptMessage): This is the message shown when
+# the user wants to jump to a specific line number in the code. You can
+# access this feature by pressing Ctrl-J on Windows/Linux or Cmd-J on Mac.
+gotoLineCmd.promptMessage=Jump to line number:
+
+# LOCALIZATION NOTE (annotation.breakpoint.title): This is the text shown in
+# front of any breakpoint annotation when it is displayed as a tooltip in one of
+# the editor gutters. This feature is used in the JavaScript Debugger.
+annotation.breakpoint.title=Breakpoint: %S
+
+# LOCALIZATION NOTE (annotation.currentLine): This is the text shown in
+# a tooltip displayed in any of the editor gutters when the user hovers the
+# current line.
+annotation.currentLine=Current line
+
+# LOCALIZATION NOTE (annotation.debugLocation.title): This is the text shown in
+# a tooltip displayed in any of the editor gutters when the user hovers the
+# current debugger location. The debugger can pause the JavaScript execution at
+# user-defined lines.
+annotation.debugLocation.title=Current step: %S
diff --git a/scratchpad/modules/PropertyPanel.jsm b/scratchpad/modules/PropertyPanel.jsm
new file mode 100644
index 00000000..fde4ebef
--- /dev/null
+++ b/scratchpad/modules/PropertyPanel.jsm
@@ -0,0 +1,612 @@
+/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is DevTools (HeadsUpDisplay) Console Code
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Rob Campbell
+ * Julian Viereck
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var EXPORTED_SYMBOLS = ["PropertyPanel", "PropertyTreeView",
+ "namesAndValuesOf", "isNonNativeGetter"];
+
+///////////////////////////////////////////////////////////////////////////
+//// Helper for PropertyTreeView
+
+const TYPE_OBJECT = 0, TYPE_FUNCTION = 1, TYPE_ARRAY = 2, TYPE_OTHER = 3;
+
+/**
+ * Figures out the type of aObject and the string to display in the tree.
+ *
+ * @param object aObject
+ * The object to operate on.
+ * @returns object
+ * A object with the form:
+ * {
+ * type: TYPE_OBJECT || TYPE_FUNCTION || TYPE_ARRAY || TYPE_OTHER,
+ * display: string for displaying the object in the tree
+ * }
+ */
+function presentableValueFor(aObject)
+{
+ if (aObject === null || aObject === undefined) {
+ return {
+ type: TYPE_OTHER,
+ display: aObject === undefined ? "undefined" : "null"
+ };
+ }
+
+ let presentable;
+ switch (aObject.constructor && aObject.constructor.name) {
+ case "Array":
+ return {
+ type: TYPE_ARRAY,
+ display: "Array"
+ };
+
+ case "String":
+ return {
+ type: TYPE_OTHER,
+ display: "\"" + aObject + "\""
+ };
+
+ case "Date":
+ case "RegExp":
+ case "Number":
+ case "Boolean":
+ return {
+ type: TYPE_OTHER,
+ display: aObject
+ };
+
+ case "Iterator":
+ return {
+ type: TYPE_OTHER,
+ display: "Iterator"
+ };
+
+ case "Function":
+ presentable = aObject.toString();
+ return {
+ type: TYPE_FUNCTION,
+ display: presentable.substring(0, presentable.indexOf(')') + 1)
+ };
+
+ default:
+ presentable = aObject.toString();
+ let m = /^\[object (\S+)\]/.exec(presentable);
+
+ try {
+ if (typeof aObject == "object" && typeof aObject.next == "function" &&
+ m && m[1] == "Generator") {
+ return {
+ type: TYPE_OTHER,
+ display: m[1]
+ };
+ }
+ }
+ catch (ex) {
+ // window.history.next throws in the typeof check above.
+ return {
+ type: TYPE_OBJECT,
+ display: m ? m[1] : "Object"
+ };
+ }
+
+ if (typeof aObject == "object" && typeof aObject.__iterator__ == "function") {
+ return {
+ type: TYPE_OTHER,
+ display: "Iterator"
+ };
+ }
+
+ return {
+ type: TYPE_OBJECT,
+ display: m ? m[1] : "Object"
+ };
+ }
+}
+
+/**
+ * Tells if the given function is native or not.
+ *
+ * @param function aFunction
+ * The function you want to check if it is native or not.
+ *
+ * @return boolean
+ * True if the given function is native, false otherwise.
+ */
+function isNativeFunction(aFunction)
+{
+ return typeof aFunction == "function" && !("prototype" in aFunction);
+}
+
+/**
+ * Tells if the given property of the provided object is a non-native getter or
+ * not.
+ *
+ * @param object aObject
+ * The object that contains the property.
+ *
+ * @param string aProp
+ * The property you want to check if it is a getter or not.
+ *
+ * @return boolean
+ * True if the given property is a getter, false otherwise.
+ */
+function isNonNativeGetter(aObject, aProp) {
+ if (typeof aObject != "object") {
+ return false;
+ }
+ let desc;
+ while (aObject) {
+ try {
+ if (desc = Object.getOwnPropertyDescriptor(aObject, aProp)) {
+ break;
+ }
+ }
+ catch (ex) {
+ // Native getters throw here. See bug 520882.
+ if (ex.name == "NS_ERROR_XPC_BAD_CONVERT_JS" ||
+ ex.name == "NS_ERROR_XPC_BAD_OP_ON_WN_PROTO") {
+ return false;
+ }
+ throw ex;
+ }
+ aObject = Object.getPrototypeOf(aObject);
+ }
+ if (desc && desc.get && !isNativeFunction(desc.get)) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Get an array of property name value pairs for the tree.
+ *
+ * @param object aObject
+ * The object to get properties for.
+ * @returns array of object
+ * Objects have the name, value, display, type, children properties.
+ */
+function namesAndValuesOf(aObject)
+{
+ let pairs = [];
+ let value, presentable;
+
+ let isDOMDocument = aObject instanceof Ci.nsIDOMDocument;
+
+ for (var propName in aObject) {
+ // See bug 632275: skip deprecated width and height properties.
+ if (isDOMDocument && (propName == "width" || propName == "height")) {
+ continue;
+ }
+
+ // Also skip non-native getters.
+ if (isNonNativeGetter(aObject, propName)) {
+ value = ""; // Value is never displayed.
+ presentable = {type: TYPE_OTHER, display: "Getter"};
+ }
+ else {
+ try {
+ value = aObject[propName];
+ presentable = presentableValueFor(value);
+ }
+ catch (ex) {
+ continue;
+ }
+ }
+
+ let pair = {};
+ pair.name = propName;
+ pair.display = propName + ": " + presentable.display;
+ pair.type = presentable.type;
+ pair.value = value;
+
+ // Convert the pair.name to a number for later sorting.
+ pair.nameNumber = parseFloat(pair.name)
+ if (isNaN(pair.nameNumber)) {
+ pair.nameNumber = false;
+ }
+
+ pairs.push(pair);
+ }
+
+ pairs.sort(function(a, b)
+ {
+ // Sort numbers.
+ if (a.nameNumber !== false && b.nameNumber === false) {
+ return -1;
+ }
+ else if (a.nameNumber === false && b.nameNumber !== false) {
+ return 1;
+ }
+ else if (a.nameNumber !== false && b.nameNumber !== false) {
+ return a.nameNumber - b.nameNumber;
+ }
+ // Sort string.
+ else if (a.name < b.name) {
+ return -1;
+ }
+ else if (a.name > b.name) {
+ return 1;
+ }
+ else {
+ return 0;
+ }
+ });
+
+ return pairs;
+}
+
+///////////////////////////////////////////////////////////////////////////
+//// 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 = [];
+};
+
+PropertyTreeView.prototype = {
+
+ /**
+ * Stores the visible rows of the tree.
+ */
+ _rows: null,
+
+ /**
+ * Stores the nsITreeBoxObject for this tree.
+ */
+ _treeBox: null,
+
+ /**
+ * Use this setter to update the content of the tree.
+ *
+ * @param object aObject
+ * The new object to be displayed in the tree.
+ * @returns void
+ */
+ set data(aObject) {
+ let oldLen = this._rows.length;
+ this._rows = this.getChildItems(aObject, true);
+ if (this._treeBox) {
+ this._treeBox.beginUpdateBatch();
+ if (oldLen) {
+ this._treeBox.rowCountChanged(0, -oldLen);
+ }
+ this._treeBox.rowCountChanged(0, this._rows.length);
+ this._treeBox.endUpdateBatch();
+ }
+ },
+
+ /**
+ * Generates the child items for the treeView of a given aItem. If there is
+ * already a children property on the aItem, this cached one is returned.
+ *
+ * @param object aItem
+ * An item of the tree's elements to generate the children for.
+ * @param boolean aRootElement
+ * If set, aItem is handled as an JS object and not as an item
+ * element of the tree.
+ * @returns array of objects
+ * Child items of aItem.
+ */
+ getChildItems: function(aItem, aRootElement)
+ {
+ // If item.children is an array, then the children has already been
+ // computed and can get returned directly.
+ // Skip this checking if aRootElement is true. It could happen, that aItem
+ // is passed as ({children:[1,2,3]}) which would be true, although these
+ // "kind" of children has no value/type etc. data as needed to display in
+ // the tree. As the passed ({children:[1,2,3]}) are instanceof
+ // itsWindow.Array and not this modules's global Array
+ // aItem.children instanceof Array can't be true, but for saftey the
+ // !aRootElement is kept here.
+ if (!aRootElement && aItem && aItem.children instanceof Array) {
+ return aItem.children;
+ }
+
+ let pairs;
+ let newPairLevel;
+
+ if (!aRootElement) {
+ newPairLevel = aItem.level + 1;
+ aItem = aItem.value;
+ }
+ else {
+ newPairLevel = 0;
+ }
+
+ pairs = namesAndValuesOf(aItem);
+
+ for each (var pair in pairs) {
+ pair.level = newPairLevel;
+ pair.isOpened = false;
+ pair.children = pair.type == TYPE_OBJECT || pair.type == TYPE_FUNCTION ||
+ pair.type == TYPE_ARRAY;
+ }
+
+ return pairs;
+ },
+
+ /** nsITreeView interface implementation **/
+
+ selection: null,
+
+ get rowCount() { return this._rows.length; },
+ setTree: function(treeBox) { this._treeBox = treeBox; },
+ getCellText: function(idx, column) { return this._rows[idx].display; },
+ getLevel: function(idx) { return this._rows[idx].level; },
+ isContainer: function(idx) { return !!this._rows[idx].children; },
+ 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)
+ {
+ var item = this._rows[idx];
+ if (!item.children) {
+ return;
+ }
+
+ this._treeBox.beginUpdateBatch();
+ if (item.isOpened) {
+ 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);
+ }
+ }
+ else {
+ item.isOpened = true;
+
+ var toInsert = this.getChildItems(item);
+ item.children = toInsert;
+ this._rows.splice.apply(this._rows, [idx + 1, 0].concat(toInsert));
+
+ this._treeBox.rowCountChanged(idx + 1, toInsert.length);
+ }
+ this._treeBox.invalidateRow(idx);
+ this._treeBox.endUpdateBatch();
+ },
+
+ 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; }
+};
+
+///////////////////////////////////////////////////////////////////////////
+//// 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.
+ *
+ * @param nsIDOMNode aParent
+ * Parent node to append the created panel to.
+ * @param nsIDOMDocument aDocument
+ * Document to create the new nodes on.
+ * @param string aTitle
+ * Title for the panel.
+ * @param string aObject
+ * Object to display in the tree.
+ * @param array of objects aButtons
+ * Array with buttons to display at the bottom of the panel.
+ */
+function PropertyPanel(aParent, aDocument, aTitle, aObject, aButtons)
+{
+ // Create the underlying panel
+ this.panel = createElement(aDocument, "panel", {
+ label: aTitle,
+ titlebar: "normal",
+ noautofocus: "true",
+ noautohide: "true",
+ close: "true",
+ });
+
+ // Create the tree.
+ let tree = this.tree = createElement(aDocument, "tree", {
+ flex: 1,
+ hidecolumnpicker: "true"
+ });
+
+ let treecols = aDocument.createElement("treecols");
+ appendChild(aDocument, treecols, "treecol", {
+ primary: "true",
+ flex: 1,
+ hideheader: "true",
+ ignoreincolumnpicker: "true"
+ });
+ tree.appendChild(treecols);
+
+ tree.appendChild(aDocument.createElement("treechildren"));
+ this.panel.appendChild(tree);
+
+ // Create the footer.
+ let footer = createElement(aDocument, "hbox", { align: "end" });
+ appendChild(aDocument, footer, "spacer", { flex: 1 });
+
+ // The footer can have butttons.
+ let self = this;
+ if (aButtons) {
+ aButtons.forEach(function(button) {
+ let buttonNode = appendChild(aDocument, footer, "button", {
+ label: button.label,
+ accesskey: button.accesskey || "",
+ class: button.class || "",
+ });
+ buttonNode.addEventListener("command", button.oncommand, false);
+ });
+ }
+
+ appendChild(aDocument, 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 poped up panel and removes
+ * it from the browser DOM.
+ *
+ * @returns void
+ */
+PropertyPanel.prototype.destroy = function PP_destroy()
+{
+ this.panel.parentNode.removeChild(this.panel);
+ this.treeView = null;
+ this.panel = null;
+ this.tree = null;
+
+ if (this.linkNode) {
+ this.linkNode._panelOpen = false;
+ this.linkNode = null;
+ }
+}
diff --git a/scratchpad/modules/scratchpad-manager.jsm b/scratchpad/modules/scratchpad-manager.jsm
new file mode 100644
index 00000000..a469f59c
--- /dev/null
+++ b/scratchpad/modules/scratchpad-manager.jsm
@@ -0,0 +1,174 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Scratchpad
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Heather Arthur (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["ScratchpadManager"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const SCRATCHPAD_WINDOW_URL = "chrome://scratchpad/content/scratchpad.xul";
+const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+ * The ScratchpadManager object opens new Scratchpad windows and manages the state
+ * of open scratchpads for session restore. There's only one ScratchpadManager in
+ * the life of the browser.
+ */
+var ScratchpadManager = {
+
+ _scratchpads: [],
+
+ /**
+ * Get the saved states of open scratchpad windows. Called by
+ * session restore.
+ *
+ * @return array
+ * The array of scratchpad states.
+ */
+ getSessionState: function SPM_getSessionState()
+ {
+ return this._scratchpads;
+ },
+
+ /**
+ * Restore scratchpad windows from the scratchpad session store file.
+ * Called by session restore.
+ *
+ * @param function aSession
+ * The session object with scratchpad states.
+ *
+ * @return array
+ * The restored scratchpad windows.
+ */
+ restoreSession: function SPM_restoreSession(aSession)
+ {
+ if (!Array.isArray(aSession)) {
+ return [];
+ }
+
+ let wins = [];
+ aSession.forEach(function(state) {
+ let win = this.openScratchpad(state);
+ wins.push(win);
+ }, this);
+
+ return wins;
+ },
+
+ /**
+ * Iterate through open scratchpad windows and save their states.
+ */
+ saveOpenWindows: function SPM_saveOpenWindows() {
+ this._scratchpads = [];
+
+ let enumerator = Services.wm.getEnumerator("devtools:scratchpad");
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ if (!win.closed && win.Scratchpad.initialized) {
+ this._scratchpads.push(win.Scratchpad.getState());
+ }
+ }
+ },
+
+ /**
+ * Open a new scratchpad window with an optional initial state.
+ *
+ * @param object aState
+ * Optional. The initial state of the scratchpad, an object
+ * with properties filename, text, and executionContext.
+ *
+ * @return nsIDomWindow
+ * The opened scratchpad window.
+ */
+ openScratchpad: function SPM_openScratchpad(aState)
+ {
+ let params = null;
+ if (aState) {
+ if (typeof aState != 'object') {
+ return;
+ }
+ params = Cc["@mozilla.org/embedcomp/dialogparam;1"]
+ .createInstance(Ci.nsIDialogParamBlock);
+ params.SetNumberStrings(1);
+ params.SetString(0, JSON.stringify(aState));
+ }
+ let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank",
+ SCRATCHPAD_WINDOW_FEATURES, params);
+ // Only add shutdown observer if we've opened a scratchpad window
+ ShutdownObserver.init();
+
+ return win;
+ }
+};
+
+
+/**
+ * The ShutdownObserver listens for app shutdown and saves the current state
+ * of the scratchpads for session restore.
+ */
+var ShutdownObserver = {
+ _initialized: false,
+
+ init: function SDO_init()
+ {
+ if (this._initialized) {
+ return;
+ }
+
+ Services.obs.addObserver(this, "quit-application-granted", false);
+ this._initialized = true;
+ },
+
+ observe: function SDO_observe(aMessage, aTopic, aData)
+ {
+ if (aTopic == "quit-application-granted") {
+ ScratchpadManager.saveOpenWindows();
+ this.uninit();
+ }
+ },
+
+ uninit: function SDO_uninit()
+ {
+ Services.obs.removeObserver(this, "quit-application-granted");
+ }
+};
diff --git a/scratchpad/modules/source-editor-orion.jsm b/scratchpad/modules/source-editor-orion.jsm
new file mode 100644
index 00000000..895b68c9
--- /dev/null
+++ b/scratchpad/modules/source-editor-orion.jsm
@@ -0,0 +1,2061 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Source Editor component (Orion editor).
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Mihai Sucan (original author)
+ * Kenny Heaton
+ * Spyros Livathinos
+ * Allen Eubank
+ * Girish Sharma
+ * Pranav Ravichandran
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+"use strict";
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://scratchpad/source-editor-ui.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1",
+ "nsIClipboardHelper");
+
+const ORION_SCRIPT = "chrome://scratchpad/content/orion/orion.js";
+const ORION_IFRAME = "data:text/html;charset=utf8," +
+ "" +
+ " " +
+ "" +
+ "
" +
+ "";
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Maximum allowed vertical offset for the line index when you call
+ * SourceEditor.setCaretPosition().
+ *
+ * @type number
+ */
+const VERTICAL_OFFSET = 3;
+
+/**
+ * The primary selection update delay. On Linux, the X11 primary selection is
+ * updated to hold the currently selected text.
+ *
+ * @type number
+ */
+const PRIMARY_SELECTION_DELAY = 100;
+
+/**
+ * Predefined themes for syntax highlighting. This objects maps
+ * SourceEditor.THEMES to Orion CSS files.
+ */
+const ORION_THEMES = {
+ mozilla: ["chrome://scratchpad/skin/orion.css"],
+};
+
+/**
+ * Known Orion editor events you can listen for. This object maps several of the
+ * SourceEditor.EVENTS to Orion events.
+ */
+const ORION_EVENTS = {
+ ContextMenu: "ContextMenu",
+ TextChanged: "ModelChanged",
+ Selection: "Selection",
+ Focus: "Focus",
+ Blur: "Blur",
+ MouseOver: "MouseOver",
+ MouseOut: "MouseOut",
+ MouseMove: "MouseMove",
+};
+
+/**
+ * Known Orion annotation types.
+ */
+const ORION_ANNOTATION_TYPES = {
+ currentBracket: "orion.annotation.currentBracket",
+ matchingBracket: "orion.annotation.matchingBracket",
+ breakpoint: "orion.annotation.breakpoint",
+ task: "orion.annotation.task",
+ currentLine: "orion.annotation.currentLine",
+ debugLocation: "mozilla.annotation.debugLocation",
+};
+
+/**
+ * Default key bindings in the Orion editor.
+ */
+const DEFAULT_KEYBINDINGS = [
+ {
+ action: "enter",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_ENTER,
+ },
+ {
+ action: "undo",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_Z,
+ accel: true,
+ },
+ {
+ action: "redo",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_Z,
+ accel: true,
+ shift: true,
+ },
+ {
+ action: "Unindent Lines",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_TAB,
+ shift: true,
+ },
+ {
+ action: "Move Lines Up",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_UP,
+ ctrl: Services.appinfo.OS == "Darwin",
+ alt: true,
+ },
+ {
+ action: "Move Lines Down",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_DOWN,
+ ctrl: Services.appinfo.OS == "Darwin",
+ alt: true,
+ },
+ {
+ action: "Comment/Uncomment",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_SLASH,
+ accel: true,
+ },
+ {
+ action: "Move to Bracket Opening",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET,
+ accel: true,
+ },
+ {
+ action: "Move to Bracket Closing",
+ code: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET,
+ accel: true,
+ },
+];
+
+var EXPORTED_SYMBOLS = ["SourceEditor"];
+
+/**
+ * The SourceEditor object constructor. The SourceEditor component allows you to
+ * provide users with an editor tailored to the specific needs of editing source
+ * code, aimed primarily at web developers.
+ *
+ * The editor used here is Eclipse Orion (see http://www.eclipse.org/orion).
+ *
+ * @constructor
+ */
+function SourceEditor() {
+ /* Update the SourceEditor defaults from user preferences.
+
+ SourceEditor.DEFAULTS.tabSize =
+ Services.prefs.getIntPref(SourceEditor.PREFS.TAB_SIZE);
+ SourceEditor.DEFAULTS.expandTab =
+ Services.prefs.getBoolPref(SourceEditor.PREFS.EXPAND_TAB); */
+
+ this._onOrionSelection = this._onOrionSelection.bind(this);
+ this._onTextChanged = this._onTextChanged.bind(this);
+ this._onOrionContextMenu = this._onOrionContextMenu.bind(this);
+
+ this._eventTarget = {};
+ this._eventListenersQueue = [];
+ this.ui = new SourceEditorUI(this);
+}
+
+SourceEditor.prototype = {
+ _view: null,
+ _iframe: null,
+ _model: null,
+ _undoStack: null,
+ _linesRuler: null,
+ _annotationRuler: null,
+ _overviewRuler: null,
+ _styler: null,
+ _annotationStyler: null,
+ _annotationModel: null,
+ _dragAndDrop: null,
+ _currentLineAnnotation: null,
+ _primarySelectionTimeout: null,
+ _mode: null,
+ _expandTab: null,
+ _tabSize: null,
+ _iframeWindow: null,
+ _eventTarget: null,
+ _eventListenersQueue: null,
+ _contextMenu: null,
+ _dirty: false,
+
+ /**
+ * The Source Editor user interface manager.
+ * @type object
+ * An instance of the SourceEditorUI.
+ */
+ ui: null,
+
+ /**
+ * The editor container element.
+ * @type nsIDOMElement
+ */
+ parentElement: null,
+
+ /**
+ * Initialize the editor.
+ *
+ * @param nsIDOMElement aElement
+ * The DOM element where you want the editor to show.
+ * @param object aConfig
+ * Editor configuration object. See SourceEditor.DEFAULTS for the
+ * available configuration options.
+ * @param function [aCallback]
+ * Function you want to execute once the editor is loaded and
+ * initialized.
+ * @see SourceEditor.DEFAULTS
+ */
+ init: function SE_init(aElement, aConfig, aCallback)
+ {
+ if (this._iframe) {
+ throw new Error("SourceEditor is already initialized!");
+ }
+
+ let doc = aElement.ownerDocument;
+
+ this._iframe = doc.createElementNS(XUL_NS, "iframe");
+ this._iframe.flex = 1;
+
+ let onIframeLoad = (function() {
+ this._iframe.removeEventListener("load", onIframeLoad, true);
+ this._onIframeLoad();
+ }).bind(this);
+
+ this._iframe.addEventListener("load", onIframeLoad, true);
+
+ this._iframe.setAttribute("src", ORION_IFRAME);
+
+ aElement.appendChild(this._iframe);
+ this.parentElement = aElement;
+
+ this._config = {};
+ for (let key in SourceEditor.DEFAULTS) {
+ this._config[key] = key in aConfig ?
+ aConfig[key] :
+ SourceEditor.DEFAULTS[key];
+ }
+
+ // TODO: Bug 725677 - Remove the deprecated placeholderText option from the
+ // Source Editor initialization.
+ if (aConfig.placeholderText) {
+ this._config.initialText = aConfig.placeholderText;
+ Services.console.logStringMessage("SourceEditor.init() was called with the placeholderText option which is deprecated, please use initialText.");
+ }
+
+ this._onReadyCallback = aCallback;
+ this.ui.init();
+ },
+
+ /**
+ * The editor iframe load event handler.
+ * @private
+ */
+ _onIframeLoad: function SE__onIframeLoad()
+ {
+ this._iframeWindow = this._iframe.contentWindow.wrappedJSObject;
+ let window = this._iframeWindow;
+ let config = this._config;
+
+ Services.scriptloader.loadSubScript(ORION_SCRIPT, window, "utf8");
+
+ let TextModel = window.require("orion/textview/textModel").TextModel;
+ let TextView = window.require("orion/textview/textView").TextView;
+
+ this._expandTab = config.expandTab;
+ this._tabSize = config.tabSize;
+
+ let theme = config.theme;
+ let stylesheet = theme in ORION_THEMES ? ORION_THEMES[theme] : theme;
+
+ this._model = new TextModel(config.initialText);
+ this._view = new TextView({
+ model: this._model,
+ parent: "editor",
+ stylesheet: stylesheet,
+ tabSize: this._tabSize,
+ expandTab: this._expandTab,
+ readonly: config.readOnly,
+ themeClass: "mozilla" + (config.readOnly ? " readonly" : ""),
+ });
+
+ let onOrionLoad = function() {
+ this._view.removeEventListener("Load", onOrionLoad);
+ this._onOrionLoad();
+ }.bind(this);
+
+ this._view.addEventListener("Load", onOrionLoad);
+ if (config.highlightCurrentLine || Services.appinfo.OS == "Linux") {
+ this.addEventListener(SourceEditor.EVENTS.SELECTION,
+ this._onOrionSelection);
+ }
+ this.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
+ this._onTextChanged);
+
+ if (typeof config.contextMenu == "string") {
+ let chromeDocument = this.parentElement.ownerDocument;
+ this._contextMenu = chromeDocument.getElementById(config.contextMenu);
+ } else if (typeof config.contextMenu == "object" ) {
+ this._contextMenu = config._contextMenu;
+ }
+ if (this._contextMenu) {
+ this.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
+ this._onOrionContextMenu);
+ }
+
+ let KeyBinding = window.require("orion/textview/keyBinding").KeyBinding;
+ let TextDND = window.require("orion/textview/textDND").TextDND;
+ let Rulers = window.require("orion/textview/rulers");
+ let LineNumberRuler = Rulers.LineNumberRuler;
+ let AnnotationRuler = Rulers.AnnotationRuler;
+ let OverviewRuler = Rulers.OverviewRuler;
+ let UndoStack = window.require("orion/textview/undoStack").UndoStack;
+ let AnnotationModel = window.require("orion/textview/annotations").AnnotationModel;
+
+ this._annotationModel = new AnnotationModel(this._model);
+
+ if (config.showAnnotationRuler) {
+ this._annotationRuler = new AnnotationRuler(this._annotationModel, "left",
+ {styleClass: "ruler annotations"});
+ this._annotationRuler.onClick = this._annotationRulerClick.bind(this);
+ this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint);
+ this._annotationRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation);
+ this._view.addRuler(this._annotationRuler);
+ }
+
+ if (config.showLineNumbers) {
+ let rulerClass = this._annotationRuler ?
+ "ruler lines linesWithAnnotations" :
+ "ruler lines";
+
+ this._linesRuler = new LineNumberRuler(this._annotationModel, "left",
+ {styleClass: rulerClass}, {styleClass: "rulerLines odd"},
+ {styleClass: "rulerLines even"});
+
+ this._linesRuler.onClick = this._linesRulerClick.bind(this);
+ this._linesRuler.onDblClick = this._linesRulerDblClick.bind(this);
+ this._view.addRuler(this._linesRuler);
+ }
+
+ if (config.showOverviewRuler) {
+ this._overviewRuler = new OverviewRuler(this._annotationModel, "right",
+ {styleClass: "ruler overview"});
+ this._overviewRuler.onClick = this._overviewRulerClick.bind(this);
+
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.breakpoint);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation);
+ this._overviewRuler.addAnnotationType(ORION_ANNOTATION_TYPES.task);
+ this._view.addRuler(this._overviewRuler);
+ }
+
+ this.setMode(config.mode);
+
+ this._undoStack = new UndoStack(this._view, config.undoLimit);
+
+ this._dragAndDrop = new TextDND(this._view, this._undoStack);
+
+ let actions = {
+ "undo": [this.undo, this],
+ "redo": [this.redo, this],
+ "tab": [this._doTab, this],
+ "Unindent Lines": [this._doUnindentLines, this],
+ "enter": [this._doEnter, this],
+ "Find...": [this.ui.find, this.ui],
+ "Find Next Occurrence": [this.ui.findNext, this.ui],
+ "Find Previous Occurrence": [this.ui.findPrevious, this.ui],
+ "Goto Line...": [this.ui.gotoLine, this.ui],
+ "Move Lines Down": [this._moveLines, this],
+ "Comment/Uncomment": [this._doCommentUncomment, this],
+ "Move to Bracket Opening": [this._moveToBracketOpening, this],
+ "Move to Bracket Closing": [this._moveToBracketClosing, this],
+ };
+
+ for (let name in actions) {
+ let action = actions[name];
+ this._view.setAction(name, action[0].bind(action[1]));
+ }
+
+ this._view.setAction("Move Lines Up", this._moveLines.bind(this, true));
+
+ let keys = (config.keys || []).concat(DEFAULT_KEYBINDINGS);
+ keys.forEach(function(aKey) {
+ // In Orion mod1 refers to Cmd on Macs and Ctrl on Windows and Linux.
+ // So, if ctrl is in aKey we use it on Windows and Linux, otherwise
+ // we use aKey.accel for mod1.
+ let mod1 = Services.appinfo.OS != "Darwin" &&
+ "ctrl" in aKey ? aKey.ctrl : aKey.accel;
+ let binding = new KeyBinding(aKey.code, mod1, aKey.shift, aKey.alt,
+ aKey.ctrl);
+ this._view.setKeyBinding(binding, aKey.action);
+
+ if (aKey.callback) {
+ this._view.setAction(aKey.action, aKey.callback);
+ }
+ }, this);
+
+ this._initEventTarget();
+ },
+
+ /**
+ * Initialize the private Orion EventTarget object. This is used for tracking
+ * our own event listeners for events outside of Orion's scope.
+ * @private
+ */
+ _initEventTarget: function SE__initEventTarget()
+ {
+ let EventTarget =
+ this._iframeWindow.require("orion/textview/eventTarget").EventTarget;
+ EventTarget.addMixin(this._eventTarget);
+
+ this._eventListenersQueue.forEach(function(aRequest) {
+ if (aRequest[0] == "add") {
+ this.addEventListener(aRequest[1], aRequest[2]);
+ } else {
+ this.removeEventListener(aRequest[1], aRequest[2]);
+ }
+ }, this);
+
+ this._eventListenersQueue = [];
+ },
+
+ /**
+ * Dispatch an event to the SourceEditor event listeners. This covers only the
+ * SourceEditor-specific events.
+ *
+ * @private
+ * @param object aEvent
+ * The event object to dispatch to all listeners.
+ */
+ _dispatchEvent: function SE__dispatchEvent(aEvent)
+ {
+ this._eventTarget.dispatchEvent(aEvent);
+ },
+
+ /**
+ * The Orion "Load" event handler. This is called when the Orion editor
+ * completes the initialization.
+ * @private
+ */
+ _onOrionLoad: function SE__onOrionLoad()
+ {
+ this.ui.onReady();
+ if (this._onReadyCallback) {
+ this._onReadyCallback(this);
+ this._onReadyCallback = null;
+ }
+ },
+
+ /**
+ * The "tab" editor action implementation. This adds support for expanded tabs
+ * to spaces, and support for the indentation of multiple lines at once.
+ * @private
+ */
+ _doTab: function SE__doTab()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let indent = "\t";
+ let selection = this.getSelection();
+ let model = this._model;
+ let firstLine = model.getLineAtOffset(selection.start);
+ let firstLineStart = this.getLineStart(firstLine);
+ let lastLineOffset = selection.end > selection.start ?
+ selection.end - 1 : selection.end;
+ let lastLine = model.getLineAtOffset(lastLineOffset);
+
+ if (this._expandTab) {
+ let offsetFromLineStart = firstLine == lastLine ?
+ selection.start - firstLineStart : 0;
+ let spaces = this._tabSize - (offsetFromLineStart % this._tabSize);
+ indent = (new Array(spaces + 1)).join(" ");
+ }
+
+ // Do selection indentation.
+ if (firstLine != lastLine) {
+ let lines = [""];
+ let lastLineEnd = this.getLineEnd(lastLine, true);
+ let selectedLines = lastLine - firstLine + 1;
+
+ for (let i = firstLine; i <= lastLine; i++) {
+ lines.push(model.getLine(i, true));
+ }
+
+ this.startCompoundChange();
+
+ this.setText(lines.join(indent), firstLineStart, lastLineEnd);
+
+ let newSelectionStart = firstLineStart == selection.start ?
+ selection.start : selection.start + indent.length;
+ let newSelectionEnd = selection.end + (selectedLines * indent.length);
+
+ this._view.setSelection(newSelectionStart, newSelectionEnd);
+
+ this.endCompoundChange();
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * The "Unindent lines" editor action implementation. This method is invoked
+ * when the user presses Shift-Tab.
+ * @private
+ */
+ _doUnindentLines: function SE__doUnindentLines()
+ {
+ if (this.readOnly) {
+ return true;
+ }
+
+ let indent = "\t";
+
+ let selection = this.getSelection();
+ let model = this._model;
+ let firstLine = model.getLineAtOffset(selection.start);
+ let lastLineOffset = selection.end > selection.start ?
+ selection.end - 1 : selection.end;
+ let lastLine = model.getLineAtOffset(lastLineOffset);
+
+ if (this._expandTab) {
+ indent = (new Array(this._tabSize + 1)).join(" ");
+ }
+
+ let lines = [];
+ for (let line, i = firstLine; i <= lastLine; i++) {
+ line = model.getLine(i, true);
+ if (line.indexOf(indent) != 0) {
+ return true;
+ }
+ lines.push(line.substring(indent.length));
+ }
+
+ let firstLineStart = this.getLineStart(firstLine);
+ let lastLineStart = this.getLineStart(lastLine);
+ let lastLineEnd = this.getLineEnd(lastLine, true);
+
+ this.startCompoundChange();
+
+ this.setText(lines.join(""), firstLineStart, lastLineEnd);
+
+ let selectedLines = lastLine - firstLine + 1;
+ let newSelectionStart = firstLineStart == selection.start ?
+ selection.start :
+ Math.max(firstLineStart,
+ selection.start - indent.length);
+ let newSelectionEnd = selection.end - (selectedLines * indent.length) +
+ (selection.end == lastLineStart + 1 ? 1 : 0);
+ if (firstLine == lastLine) {
+ newSelectionEnd = Math.max(lastLineStart, newSelectionEnd);
+ }
+ this._view.setSelection(newSelectionStart, newSelectionEnd);
+
+ this.endCompoundChange();
+
+ return true;
+ },
+
+ /**
+ * The editor Enter action implementation, which adds simple automatic
+ * indentation based on the previous line when the user presses the Enter key.
+ * @private
+ */
+ _doEnter: function SE__doEnter()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+ if (selection.start != selection.end) {
+ return false;
+ }
+
+ let model = this._model;
+ let lineIndex = model.getLineAtOffset(selection.start);
+ let lineText = model.getLine(lineIndex, true);
+ let lineStart = this.getLineStart(lineIndex);
+ let index = 0;
+ let lineOffset = selection.start - lineStart;
+ while (index < lineOffset && /[ \t]/.test(lineText.charAt(index))) {
+ index++;
+ }
+
+ if (!index) {
+ return false;
+ }
+
+ let prefix = lineText.substring(0, index);
+ index = lineOffset;
+ while (index < lineText.length &&
+ /[ \t]/.test(lineText.charAt(index++))) {
+ selection.end++;
+ }
+
+ this.setText(this.getLineDelimiter() + prefix, selection.start,
+ selection.end);
+ return true;
+ },
+
+ /**
+ * Move lines upwards or downwards, relative to the current caret location.
+ *
+ * @private
+ * @param boolean aLineAbove
+ * True if moving lines up, false to move lines down.
+ */
+ _moveLines: function SE__moveLines(aLineAbove)
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let model = this._model;
+ let selection = this.getSelection();
+ let firstLine = model.getLineAtOffset(selection.start);
+ if (firstLine == 0 && aLineAbove) {
+ return true;
+ }
+
+ let lastLine = model.getLineAtOffset(selection.end);
+ let firstLineStart = this.getLineStart(firstLine);
+ let lastLineStart = this.getLineStart(lastLine);
+ if (selection.start != selection.end && lastLineStart == selection.end) {
+ lastLine--;
+ }
+ if (!aLineAbove && (lastLine + 1) == this.getLineCount()) {
+ return true;
+ }
+
+ let lastLineEnd = this.getLineEnd(lastLine, true);
+ let text = this.getText(firstLineStart, lastLineEnd);
+
+ if (aLineAbove) {
+ let aboveLine = firstLine - 1;
+ let aboveLineStart = this.getLineStart(aboveLine);
+
+ this.startCompoundChange();
+ if (lastLine == (this.getLineCount() - 1)) {
+ let delimiterStart = this.getLineEnd(aboveLine);
+ let delimiterEnd = this.getLineEnd(aboveLine, true);
+ let lineDelimiter = this.getText(delimiterStart, delimiterEnd);
+ text += lineDelimiter;
+ this.setText("", firstLineStart - lineDelimiter.length, lastLineEnd);
+ } else {
+ this.setText("", firstLineStart, lastLineEnd);
+ }
+ this.setText(text, aboveLineStart, aboveLineStart);
+ this.endCompoundChange();
+ this.setSelection(aboveLineStart, aboveLineStart + text.length);
+ } else {
+ let belowLine = lastLine + 1;
+ let belowLineEnd = this.getLineEnd(belowLine, true);
+
+ let insertAt = belowLineEnd - lastLineEnd + firstLineStart;
+ let lineDelimiter = "";
+ if (belowLine == this.getLineCount() - 1) {
+ let delimiterStart = this.getLineEnd(lastLine);
+ lineDelimiter = this.getText(delimiterStart, lastLineEnd);
+ text = lineDelimiter + text.substr(0, text.length -
+ lineDelimiter.length);
+ }
+ this.startCompoundChange();
+ this.setText("", firstLineStart, lastLineEnd);
+ this.setText(text, insertAt, insertAt);
+ this.endCompoundChange();
+ this.setSelection(insertAt + lineDelimiter.length,
+ insertAt + text.length);
+ }
+ return true;
+ },
+
+ /**
+ * The Orion Selection event handler. The current caret line is
+ * highlighted and for Linux users the selected text is copied into the X11
+ * PRIMARY buffer.
+ *
+ * @private
+ * @param object aEvent
+ * The Orion Selection event object.
+ */
+ _onOrionSelection: function SE__onOrionSelection(aEvent)
+ {
+ if (this._config.highlightCurrentLine) {
+ this._highlightCurrentLine(aEvent);
+ }
+
+ if (Services.appinfo.OS == "Linux") {
+ let window = this.parentElement.ownerDocument.defaultView;
+
+ if (this._primarySelectionTimeout) {
+ window.clearTimeout(this._primarySelectionTimeout);
+ }
+ this._primarySelectionTimeout =
+ window.setTimeout(this._updatePrimarySelection.bind(this),
+ PRIMARY_SELECTION_DELAY);
+ }
+ },
+
+ /**
+ * The TextChanged event handler which tracks the dirty state of the editor.
+ *
+ * @see SourceEditor.EVENTS.TEXT_CHANGED
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @see SourceEditor.dirty
+ * @private
+ */
+ _onTextChanged: function SE__onTextChanged()
+ {
+ this._updateDirty();
+ },
+
+ /**
+ * The Orion contextmenu event handler. This method opens the default or
+ * the custom context menu popup at the pointer location.
+ *
+ * @param object aEvent
+ * The contextmenu event object coming from Orion. This object should
+ * hold the screenX and screenY properties.
+ */
+ _onOrionContextMenu: function SE__onOrionContextMenu(aEvent)
+ {
+ if (this._contextMenu.state == "closed") {
+ this._contextMenu.openPopupAtScreen(aEvent.screenX || 0,
+ aEvent.screenY || 0, true);
+ }
+ },
+
+ /**
+ * Update the dirty state of the editor based on the undo stack.
+ * @private
+ */
+ _updateDirty: function SE__updateDirty()
+ {
+ this.dirty = !this._undoStack.isClean();
+ },
+
+ /**
+ * Update the X11 PRIMARY buffer to hold the current selection.
+ * @private
+ */
+ _updatePrimarySelection: function SE__updatePrimarySelection()
+ {
+ this._primarySelectionTimeout = null;
+
+ let text = this.getSelectedText();
+ if (!text) {
+ return;
+ }
+
+ clipboardHelper.copyStringToClipboard(text,
+ Ci.nsIClipboard.kSelectionClipboard);
+ },
+
+ /**
+ * Highlight the current line using the Orion annotation model.
+ *
+ * @private
+ * @param object aEvent
+ * The Selection event object.
+ */
+ _highlightCurrentLine: function SE__highlightCurrentLine(aEvent)
+ {
+ let annotationModel = this._annotationModel;
+ let model = this._model;
+ let oldAnnotation = this._currentLineAnnotation;
+ let newSelection = aEvent.newValue;
+
+ let collapsed = newSelection.start == newSelection.end;
+ if (!collapsed) {
+ if (oldAnnotation) {
+ annotationModel.removeAnnotation(oldAnnotation);
+ this._currentLineAnnotation = null;
+ }
+ return;
+ }
+
+ let line = model.getLineAtOffset(newSelection.start);
+ let lineStart = this.getLineStart(line);
+ let lineEnd = this.getLineEnd(line);
+
+ let title = oldAnnotation ? oldAnnotation.title :
+ SourceEditorUI.strings.GetStringFromName("annotation.currentLine");
+
+ this._currentLineAnnotation = {
+ start: lineStart,
+ end: lineEnd,
+ type: ORION_ANNOTATION_TYPES.currentLine,
+ title: title,
+ html: "
",
+ overviewStyle: {styleClass: "annotationOverview currentLine"},
+ lineStyle: {styleClass: "annotationLine currentLine"},
+ };
+
+ annotationModel.replaceAnnotations(oldAnnotation ? [oldAnnotation] : null,
+ [this._currentLineAnnotation]);
+ },
+
+ /**
+ * The click event handler for the lines gutter. This function allows the user
+ * to jump to a line or to perform line selection while holding the Shift key
+ * down.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the click event occurred.
+ * @param object aEvent
+ * The DOM click event object.
+ */
+ _linesRulerClick: function SE__linesRulerClick(aLineIndex, aEvent)
+ {
+ if (aLineIndex === undefined) {
+ return;
+ }
+
+ if (aEvent.shiftKey) {
+ let model = this._model;
+ let selection = this.getSelection();
+ let selectionLineStart = model.getLineAtOffset(selection.start);
+ let selectionLineEnd = model.getLineAtOffset(selection.end);
+ let newStart = aLineIndex <= selectionLineStart ?
+ this.getLineStart(aLineIndex) : selection.start;
+ let newEnd = aLineIndex <= selectionLineStart ?
+ selection.end : this.getLineEnd(aLineIndex);
+ this.setSelection(newStart, newEnd);
+ } else {
+ this.setCaretPosition(aLineIndex);
+ }
+ },
+
+ /**
+ * The dblclick event handler for the lines gutter. This function selects the
+ * whole line where the event occurred.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the double click event occurred.
+ * @param object aEvent
+ * The DOM dblclick event object.
+ */
+ _linesRulerDblClick: function SE__linesRulerDblClick(aLineIndex)
+ {
+ if (aLineIndex === undefined) {
+ return;
+ }
+
+ let newStart = this.getLineStart(aLineIndex);
+ let newEnd = this.getLineEnd(aLineIndex);
+ this.setSelection(newStart, newEnd);
+ },
+
+ /**
+ * Highlight the Orion annotations. This updates the annotation styler as
+ * needed.
+ * @private
+ */
+ _highlightAnnotations: function SE__highlightAnnotations()
+ {
+ if (this._annotationStyler) {
+ this._annotationStyler.destroy();
+ this._annotationStyler = null;
+ }
+
+ let AnnotationStyler =
+ this._iframeWindow.require("orion/textview/annotations").AnnotationStyler;
+
+ let styler = new AnnotationStyler(this._view, this._annotationModel);
+ this._annotationStyler = styler;
+
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.matchingBracket);
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentBracket);
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.task);
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.debugLocation);
+
+ if (this._config.highlightCurrentLine) {
+ styler.addAnnotationType(ORION_ANNOTATION_TYPES.currentLine);
+ }
+ },
+
+ /**
+ * Retrieve the list of Orion Annotations filtered by type for the given text range.
+ *
+ * @private
+ * @param string aType
+ * The annotation type to filter annotations for. Use one of the keys
+ * in ORION_ANNOTATION_TYPES.
+ * @param number aStart
+ * Offset from where to start finding the annotations.
+ * @param number aEnd
+ * End offset for retrieving the annotations.
+ * @return array
+ * The array of annotations, filtered by type, within the given text
+ * range.
+ */
+ _getAnnotationsByType: function SE__getAnnotationsByType(aType, aStart, aEnd)
+ {
+ let annotations = this._annotationModel.getAnnotations(aStart, aEnd);
+ let annotation, result = [];
+ while (annotation = annotations.next()) {
+ if (annotation.type == ORION_ANNOTATION_TYPES[aType]) {
+ result.push(annotation);
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * The click event handler for the annotation ruler.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the click event occurred.
+ * @param object aEvent
+ * The DOM click event object.
+ */
+ _annotationRulerClick: function SE__annotationRulerClick(aLineIndex, aEvent)
+ {
+ if (aLineIndex === undefined || aLineIndex == -1) {
+ return;
+ }
+
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+ let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd);
+ if (annotations.length > 0) {
+ this.removeBreakpoint(aLineIndex);
+ } else {
+ this.addBreakpoint(aLineIndex);
+ }
+ },
+
+ /**
+ * The click event handler for the overview ruler. When the user clicks on an
+ * annotation the editor jumps to the associated line.
+ *
+ * @private
+ * @param number aLineIndex
+ * The line index where the click event occurred.
+ * @param object aEvent
+ * The DOM click event object.
+ */
+ _overviewRulerClick: function SE__overviewRulerClick(aLineIndex, aEvent)
+ {
+ if (aLineIndex === undefined || aLineIndex == -1) {
+ return;
+ }
+
+ let model = this._model;
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+ let annotations = this._annotationModel.getAnnotations(lineStart, lineEnd);
+ let annotation = annotations.next();
+
+ // Jump to the line where annotation is. If the annotation is specific to
+ // a substring part of the line, then select the substring.
+ if (!annotation || lineStart == annotation.start && lineEnd == annotation.end) {
+ this.setSelection(lineStart, lineStart);
+ } else {
+ this.setSelection(annotation.start, annotation.end);
+ }
+ },
+
+ /**
+ * Get the editor element.
+ *
+ * @return nsIDOMElement
+ * In this implementation a xul:iframe holds the editor.
+ */
+ get editorElement() {
+ return this._iframe;
+ },
+
+ /**
+ * Helper function to retrieve the strings used for comments in the current
+ * editor mode.
+ *
+ * @private
+ * @return object
+ * An object that holds the following properties:
+ * - line: the comment string used for the start of a single line
+ * comment.
+ * - blockStart: the comment string used for the start of a comment
+ * block.
+ * - blockEnd: the comment string used for the end of a block comment.
+ * Null is returned for unsupported editor modes.
+ */
+ _getCommentStrings: function SE__getCommentStrings()
+ {
+ let line = "";
+ let blockCommentStart = "";
+ let blockCommentEnd = "";
+
+ switch (this.getMode()) {
+ case SourceEditor.MODES.JAVASCRIPT:
+ line = "//";
+ blockCommentStart = "/*";
+ blockCommentEnd = "*/";
+ break;
+ case SourceEditor.MODES.CSS:
+ blockCommentStart = "/*";
+ blockCommentEnd = "*/";
+ break;
+ case SourceEditor.MODES.HTML:
+ case SourceEditor.MODES.XML:
+ blockCommentStart = "";
+ break;
+ default:
+ return null;
+ }
+ return {line: line, blockStart: blockCommentStart, blockEnd: blockCommentEnd};
+ },
+
+ /**
+ * Decide whether to comment the selection/current line or to uncomment it.
+ *
+ * @private
+ */
+ _doCommentUncomment: function SE__doCommentUncomment()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let commentObject = this._getCommentStrings();
+ if (!commentObject) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+ let model = this._model;
+ let firstLine = model.getLineAtOffset(selection.start);
+ let lastLine = model.getLineAtOffset(selection.end);
+
+ // Checks for block comment.
+ let firstLineText = model.getLine(firstLine);
+ let lastLineText = model.getLine(lastLine);
+ let openIndex = firstLineText.indexOf(commentObject.blockStart);
+ let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd);
+ if (openIndex != -1 && closeIndex != -1 &&
+ (firstLine != lastLine ||
+ (closeIndex - openIndex) >= commentObject.blockStart.length)) {
+ return this._doUncomment();
+ }
+
+ if (!commentObject.line) {
+ return this._doComment();
+ }
+
+ // If the selection is not a block comment, check for the first and the last
+ // lines to be line commented.
+ let firstLastCommented = [firstLineText,
+ lastLineText].every(function(aLineText) {
+ let openIndex = aLineText.indexOf(commentObject.line);
+ if (openIndex != -1) {
+ let textUntilComment = aLineText.slice(0, openIndex);
+ if (!textUntilComment || /^\s+$/.test(textUntilComment)) {
+ return true;
+ }
+ }
+ return false;
+ });
+ if (firstLastCommented) {
+ return this._doUncomment();
+ }
+
+ // If we reach here, then we have to comment the selection/line.
+ return this._doComment();
+ },
+
+ /**
+ * Wrap the selected text in comments. If nothing is selected the current
+ * caret line is commented out. Single line and block comments depend on the
+ * current editor mode.
+ *
+ * @private
+ */
+ _doComment: function SE__doComment()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let commentObject = this._getCommentStrings();
+ if (!commentObject) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+
+ if (selection.start == selection.end) {
+ let selectionLine = this._model.getLineAtOffset(selection.start);
+ let lineStartOffset = this.getLineStart(selectionLine);
+ if (commentObject.line) {
+ this.setText(commentObject.line, lineStartOffset, lineStartOffset);
+ } else {
+ let lineEndOffset = this.getLineEnd(selectionLine);
+ this.startCompoundChange();
+ this.setText(commentObject.blockStart, lineStartOffset, lineStartOffset);
+ this.setText(commentObject.blockEnd,
+ lineEndOffset + commentObject.blockStart.length,
+ lineEndOffset + commentObject.blockStart.length);
+ this.endCompoundChange();
+ }
+ } else {
+ this.startCompoundChange();
+ this.setText(commentObject.blockStart, selection.start, selection.start);
+ this.setText(commentObject.blockEnd,
+ selection.end + commentObject.blockStart.length,
+ selection.end + commentObject.blockStart.length);
+ this.endCompoundChange();
+ }
+
+ return true;
+ },
+
+ /**
+ * Uncomment the selected text. If nothing is selected the current caret line
+ * is umcommented. Single line and block comments depend on the current editor
+ * mode.
+ *
+ * @private
+ */
+ _doUncomment: function SE__doUncomment()
+ {
+ if (this.readOnly) {
+ return false;
+ }
+
+ let commentObject = this._getCommentStrings();
+ if (!commentObject) {
+ return false;
+ }
+
+ let selection = this.getSelection();
+ let firstLine = this._model.getLineAtOffset(selection.start);
+ let lastLine = this._model.getLineAtOffset(selection.end);
+
+ // Uncomment a block of text.
+ let firstLineText = this._model.getLine(firstLine);
+ let lastLineText = this._model.getLine(lastLine);
+ let openIndex = firstLineText.indexOf(commentObject.blockStart);
+ let closeIndex = lastLineText.lastIndexOf(commentObject.blockEnd);
+ if (openIndex != -1 && closeIndex != -1 &&
+ (firstLine != lastLine ||
+ (closeIndex - openIndex) >= commentObject.blockStart.length)) {
+ let firstLineStartOffset = this.getLineStart(firstLine);
+ let lastLineStartOffset = this.getLineStart(lastLine);
+ let openOffset = firstLineStartOffset + openIndex;
+ let closeOffset = lastLineStartOffset + closeIndex;
+
+ this.startCompoundChange();
+ this.setText("", closeOffset, closeOffset + commentObject.blockEnd.length);
+ this.setText("", openOffset, openOffset + commentObject.blockStart.length);
+ this.endCompoundChange();
+
+ return true;
+ }
+
+ if (!commentObject.line) {
+ return true;
+ }
+
+ // If the selected text is not a block of comment, then uncomment each line.
+ this.startCompoundChange();
+ let lineCaret = firstLine;
+ while (lineCaret <= lastLine) {
+ let currentLine = this._model.getLine(lineCaret);
+ let lineStart = this.getLineStart(lineCaret);
+ let openIndex = currentLine.indexOf(commentObject.line);
+ let openOffset = lineStart + openIndex;
+ let textUntilComment = this.getText(lineStart, openOffset);
+ if (openIndex != -1 &&
+ (!textUntilComment || /^\s+$/.test(textUntilComment))) {
+ this.setText("", openOffset, openOffset + commentObject.line.length);
+ }
+ lineCaret++;
+ }
+ this.endCompoundChange();
+
+ return true;
+ },
+
+ /**
+ * Helper function for _moveToBracket{Opening/Closing} to find the offset of
+ * matching bracket.
+ *
+ * @param number aOffset
+ * The offset of the bracket for which you want to find the bracket.
+ * @private
+ */
+ _getMatchingBracketIndex: function SE__getMatchingBracketIndex(aOffset)
+ {
+ return this._styler._findMatchingBracket(this._model, aOffset);
+ },
+
+ /**
+ * Move the cursor to the matching opening bracket if at corresponding closing
+ * bracket, otherwise move to the opening bracket for the current block of code.
+ *
+ * @private
+ */
+ _moveToBracketOpening: function SE__moveToBracketOpening()
+ {
+ let mode = this.getMode();
+ // Returning early if not in JavaScipt or CSS mode.
+ if (mode != SourceEditor.MODES.JAVASCRIPT &&
+ mode != SourceEditor.MODES.CSS) {
+ return false;
+ }
+
+ let caretOffset = this.getCaretOffset() - 1;
+ let matchingIndex = this._getMatchingBracketIndex(caretOffset);
+
+ // If the caret is not at the closing bracket "}", find the index of the
+ // opening bracket "{" for the current code block.
+ if (matchingIndex == -1 || matchingIndex > caretOffset) {
+ let text = this.getText();
+ let closingOffset = text.indexOf("}", caretOffset);
+ while (closingOffset > -1) {
+ let closingMatchingIndex = this._getMatchingBracketIndex(closingOffset);
+ if (closingMatchingIndex < caretOffset && closingMatchingIndex != -1) {
+ matchingIndex = closingMatchingIndex;
+ break;
+ }
+ closingOffset = text.indexOf("}", closingOffset + 1);
+ }
+ }
+
+ if (matchingIndex > -1) {
+ this.setCaretOffset(matchingIndex);
+ }
+
+ return true;
+ },
+
+ /**
+ * Moves the cursor to the matching closing bracket if at corresponding opening
+ * bracket, otherwise move to the closing bracket for the current block of code.
+ *
+ * @private
+ */
+ _moveToBracketClosing: function SE__moveToBracketClosing()
+ {
+ let mode = this.getMode();
+ // Returning early if not in JavaScipt or CSS mode.
+ if (mode != SourceEditor.MODES.JAVASCRIPT &&
+ mode != SourceEditor.MODES.CSS) {
+ return false;
+ }
+
+ let caretOffset = this.getCaretOffset();
+ let matchingIndex = this._getMatchingBracketIndex(caretOffset - 1);
+
+ // If the caret is not at the opening bracket "{", find the index of the
+ // closing bracket "}" for the current code block.
+ if (matchingIndex == -1 || matchingIndex < caretOffset) {
+ let text = this.getText();
+ let openingOffset = text.lastIndexOf("{", caretOffset);
+ while (openingOffset > -1) {
+ let openingMatchingIndex = this._getMatchingBracketIndex(openingOffset);
+ if (openingMatchingIndex > caretOffset) {
+ matchingIndex = openingMatchingIndex;
+ break;
+ }
+ openingOffset = text.lastIndexOf("{", openingOffset - 1);
+ }
+ }
+
+ if (matchingIndex > -1) {
+ this.setCaretOffset(matchingIndex);
+ }
+
+ return true;
+ },
+
+ /**
+ * Add an event listener to the editor. You can use one of the known events.
+ *
+ * @see SourceEditor.EVENTS
+ *
+ * @param string aEventType
+ * The event type you want to listen for.
+ * @param function aCallback
+ * The function you want executed when the event is triggered.
+ */
+ addEventListener: function SE_addEventListener(aEventType, aCallback)
+ {
+ if (this._view && aEventType in ORION_EVENTS) {
+ this._view.addEventListener(ORION_EVENTS[aEventType], aCallback);
+ } else if (this._eventTarget.addEventListener) {
+ this._eventTarget.addEventListener(aEventType, aCallback);
+ } else {
+ this._eventListenersQueue.push(["add", aEventType, aCallback]);
+ }
+ },
+
+ /**
+ * Remove an event listener from the editor. You can use one of the known
+ * events.
+ *
+ * @see SourceEditor.EVENTS
+ *
+ * @param string aEventType
+ * The event type you have a listener for.
+ * @param function aCallback
+ * The function you have as the event handler.
+ */
+ removeEventListener: function SE_removeEventListener(aEventType, aCallback)
+ {
+ if (this._view && aEventType in ORION_EVENTS) {
+ this._view.removeEventListener(ORION_EVENTS[aEventType], aCallback);
+ } else if (this._eventTarget.removeEventListener) {
+ this._eventTarget.removeEventListener(aEventType, aCallback);
+ } else {
+ this._eventListenersQueue.push(["remove", aEventType, aCallback]);
+ }
+ },
+
+ /**
+ * Undo a change in the editor.
+ *
+ * @return boolean
+ * True if there was a change undone, false otherwise.
+ */
+ undo: function SE_undo()
+ {
+ let result = this._undoStack.undo();
+ this.ui._onUndoRedo();
+ return result;
+ },
+
+ /**
+ * Redo a change in the editor.
+ *
+ * @return boolean
+ * True if there was a change redone, false otherwise.
+ */
+ redo: function SE_redo()
+ {
+ let result = this._undoStack.redo();
+ this.ui._onUndoRedo();
+ return result;
+ },
+
+ /**
+ * Check if there are changes that can be undone.
+ *
+ * @return boolean
+ * True if there are changes that can be undone, false otherwise.
+ */
+ canUndo: function SE_canUndo()
+ {
+ return this._undoStack.canUndo();
+ },
+
+ /**
+ * Check if there are changes that can be repeated.
+ *
+ * @return boolean
+ * True if there are changes that can be repeated, false otherwise.
+ */
+ canRedo: function SE_canRedo()
+ {
+ return this._undoStack.canRedo();
+ },
+
+ /**
+ * Reset the Undo stack.
+ */
+ resetUndo: function SE_resetUndo()
+ {
+ this._undoStack.reset();
+ this._updateDirty();
+ this.ui._onUndoRedo();
+ },
+
+ /**
+ * Set the "dirty" state of the editor. Set this to false when you save the
+ * text being edited. The dirty state will become true once the user makes
+ * changes to the text.
+ *
+ * @param boolean aNewValue
+ * The new dirty state: true if the text is not saved, false if you
+ * just saved the text.
+ */
+ set dirty(aNewValue)
+ {
+ if (aNewValue == this._dirty) {
+ return;
+ }
+
+ let event = {
+ type: SourceEditor.EVENTS.DIRTY_CHANGED,
+ oldValue: this._dirty,
+ newValue: aNewValue,
+ };
+
+ this._dirty = aNewValue;
+ if (!this._dirty && !this._undoStack.isClean()) {
+ this._undoStack.markClean();
+ }
+ this._dispatchEvent(event);
+ },
+
+ /**
+ * Get the editor "dirty" state. This tells if the text is considered saved or
+ * not.
+ *
+ * @see SourceEditor.EVENTS.DIRTY_CHANGED
+ * @return boolean
+ * True if there are changes which are not saved, false otherwise.
+ */
+ get dirty()
+ {
+ return this._dirty;
+ },
+
+ /**
+ * Start a compound change in the editor. Compound changes are grouped into
+ * only one change that you can undo later, after you invoke
+ * endCompoundChange().
+ */
+ startCompoundChange: function SE_startCompoundChange()
+ {
+ this._undoStack.startCompoundChange();
+ },
+
+ /**
+ * End a compound change in the editor.
+ */
+ endCompoundChange: function SE_endCompoundChange()
+ {
+ this._undoStack.endCompoundChange();
+ },
+
+ /**
+ * Focus the editor.
+ */
+ focus: function SE_focus()
+ {
+ this._view.focus();
+ },
+
+ /**
+ * Get the first visible line number.
+ *
+ * @return number
+ * The line number, counting from 0.
+ */
+ getTopIndex: function SE_getTopIndex()
+ {
+ return this._view.getTopIndex();
+ },
+
+ /**
+ * Set the first visible line number.
+ *
+ * @param number aTopIndex
+ * The line number, counting from 0.
+ */
+ setTopIndex: function SE_setTopIndex(aTopIndex)
+ {
+ this._view.setTopIndex(aTopIndex);
+ },
+
+ /**
+ * Check if the editor has focus.
+ *
+ * @return boolean
+ * True if the editor is focused, false otherwise.
+ */
+ hasFocus: function SE_hasFocus()
+ {
+ return this._view.hasFocus();
+ },
+
+ /**
+ * Get the editor content, in the given range. If no range is given you get
+ * the entire editor content.
+ *
+ * @param number [aStart=0]
+ * Optional, start from the given offset.
+ * @param number [aEnd=content char count]
+ * Optional, end offset for the text you want. If this parameter is not
+ * given, then the text returned goes until the end of the editor
+ * content.
+ * @return string
+ * The text in the given range.
+ */
+ getText: function SE_getText(aStart, aEnd)
+ {
+ return this._view.getText(aStart, aEnd);
+ },
+
+ /**
+ * Get the start character offset of the line with index aLineIndex.
+ *
+ * @param number aLineIndex
+ * Zero based index of the line.
+ * @return number
+ * Line start offset or -1 if out of range.
+ */
+ getLineStart: function SE_getLineStart(aLineIndex)
+ {
+ return this._model.getLineStart(aLineIndex);
+ },
+
+ /**
+ * Get the end character offset of the line with index aLineIndex,
+ * excluding the end offset. When the line delimiter is present,
+ * the offset is the start offset of the next line or the char count.
+ * Otherwise, it is the offset of the line delimiter.
+ *
+ * @param number aLineIndex
+ * Zero based index of the line.
+ * @param boolean [aIncludeDelimiter = false]
+ * Optional, whether or not to include the line delimiter.
+ * @return number
+ * Line end offset or -1 if out of range.
+ */
+ getLineEnd: function SE_getLineEnd(aLineIndex, aIncludeDelimiter)
+ {
+ return this._model.getLineEnd(aLineIndex, aIncludeDelimiter);
+ },
+
+ /**
+ * Get the number of characters in the editor content.
+ *
+ * @return number
+ * The number of editor content characters.
+ */
+ getCharCount: function SE_getCharCount()
+ {
+ return this._model.getCharCount();
+ },
+
+ /**
+ * Get the selected text.
+ *
+ * @return string
+ * The currently selected text.
+ */
+ getSelectedText: function SE_getSelectedText()
+ {
+ let selection = this.getSelection();
+ return this.getText(selection.start, selection.end);
+ },
+
+ /**
+ * Replace text in the source editor with the given text, in the given range.
+ *
+ * @param string aText
+ * The text you want to put into the editor.
+ * @param number [aStart=0]
+ * Optional, the start offset, zero based, from where you want to start
+ * replacing text in the editor.
+ * @param number [aEnd=char count]
+ * Optional, the end offset, zero based, where you want to stop
+ * replacing text in the editor.
+ */
+ setText: function SE_setText(aText, aStart, aEnd)
+ {
+ this._view.setText(aText, aStart, aEnd);
+ },
+
+ /**
+ * Drop the current selection / deselect.
+ */
+ dropSelection: function SE_dropSelection()
+ {
+ this.setCaretOffset(this.getCaretOffset());
+ },
+
+ /**
+ * Select a specific range in the editor.
+ *
+ * @param number aStart
+ * Selection range start.
+ * @param number aEnd
+ * Selection range end.
+ */
+ setSelection: function SE_setSelection(aStart, aEnd)
+ {
+ this._view.setSelection(aStart, aEnd, true);
+ },
+
+ /**
+ * Get the current selection range.
+ *
+ * @return object
+ * An object with two properties, start and end, that give the
+ * selection range (zero based offsets).
+ */
+ getSelection: function SE_getSelection()
+ {
+ return this._view.getSelection();
+ },
+
+ /**
+ * Get the current caret offset.
+ *
+ * @return number
+ * The current caret offset.
+ */
+ getCaretOffset: function SE_getCaretOffset()
+ {
+ return this._view.getCaretOffset();
+ },
+
+ /**
+ * Set the caret offset.
+ *
+ * @param number aOffset
+ * The new caret offset you want to set.
+ */
+ setCaretOffset: function SE_setCaretOffset(aOffset)
+ {
+ this._view.setCaretOffset(aOffset, true);
+ },
+
+ /**
+ * Get the caret position.
+ *
+ * @return object
+ * An object that holds two properties:
+ * - line: the line number, counting from 0.
+ * - col: the column number, counting from 0.
+ */
+ getCaretPosition: function SE_getCaretPosition()
+ {
+ let offset = this.getCaretOffset();
+ let line = this._model.getLineAtOffset(offset);
+ let lineStart = this.getLineStart(line);
+ let column = offset - lineStart;
+ return {line: line, col: column};
+ },
+
+ /**
+ * Set the caret position: line and column.
+ *
+ * @param number aLine
+ * The new caret line location. Line numbers start from 0.
+ * @param number [aColumn=0]
+ * Optional. The new caret column location. Columns start from 0.
+ * @param number [aAlign=0]
+ * Optional. Position of the line with respect to viewport.
+ * Allowed values are:
+ * SourceEditor.VERTICAL_ALIGN.TOP target line at top of view.
+ * SourceEditor.VERTICAL_ALIGN.CENTER target line at center of view.
+ * SourceEditor.VERTICAL_ALIGN.BOTTOM target line at bottom of view.
+ */
+ setCaretPosition: function SE_setCaretPosition(aLine, aColumn, aAlign)
+ {
+ let editorHeight = this._view.getClientArea().height;
+ let lineHeight = this._view.getLineHeight();
+ let linesVisible = Math.floor(editorHeight/lineHeight);
+ let halfVisible = Math.round(linesVisible/2);
+ let firstVisible = this.getTopIndex();
+ let lastVisible = this._view.getBottomIndex();
+ let caretOffset = this.getLineStart(aLine) + (aColumn || 0);
+
+ this._view.setSelection(caretOffset, caretOffset, false);
+
+ // If the target line is in view, skip the vertical alignment part.
+ if (aLine <= lastVisible && aLine >= firstVisible) {
+ this._view.showSelection();
+ return;
+ }
+
+ // Setting the offset so that the line always falls in the upper half
+ // of visible lines (lower half for BOTTOM aligned).
+ // VERTICAL_OFFSET is the maximum allowed value.
+ let offset = Math.min(halfVisible, VERTICAL_OFFSET);
+
+ let topIndex;
+ switch (aAlign) {
+ case this.VERTICAL_ALIGN.CENTER:
+ topIndex = Math.max(aLine - halfVisible, 0);
+ break;
+
+ case this.VERTICAL_ALIGN.BOTTOM:
+ topIndex = Math.max(aLine - linesVisible + offset, 0);
+ break;
+
+ default: // this.VERTICAL_ALIGN.TOP.
+ topIndex = Math.max(aLine - offset, 0);
+ break;
+ }
+ // Bringing down the topIndex to total lines in the editor if exceeding.
+ topIndex = Math.min(topIndex, this.getLineCount());
+ this.setTopIndex(topIndex);
+
+ let location = this._view.getLocationAtOffset(caretOffset);
+ this._view.setHorizontalPixel(location.x);
+ },
+
+ /**
+ * Get the line count.
+ *
+ * @return number
+ * The number of lines in the document being edited.
+ */
+ getLineCount: function SE_getLineCount()
+ {
+ return this._model.getLineCount();
+ },
+
+ /**
+ * Get the line delimiter used in the document being edited.
+ *
+ * @return string
+ * The line delimiter.
+ */
+ getLineDelimiter: function SE_getLineDelimiter()
+ {
+ return this._model.getLineDelimiter();
+ },
+
+ /**
+ * Get the indentation string used in the document being edited.
+ *
+ * @return string
+ * The indentation string.
+ */
+ getIndentationString: function SE_getIndentationString()
+ {
+ if (this._expandTab) {
+ return (new Array(this._tabSize + 1)).join(" ");
+ }
+ return "\t";
+ },
+
+ /**
+ * Set the source editor mode to the file type you are editing.
+ *
+ * @param string aMode
+ * One of the predefined SourceEditor.MODES.
+ */
+ setMode: function SE_setMode(aMode)
+ {
+ if (this._styler) {
+ this._styler.destroy();
+ this._styler = null;
+ }
+
+ let window = this._iframeWindow;
+
+ switch (aMode) {
+ case SourceEditor.MODES.JAVASCRIPT:
+ case SourceEditor.MODES.CSS:
+ let TextStyler =
+ window.require("examples/textview/textStyler").TextStyler;
+
+ this._styler = new TextStyler(this._view, aMode, this._annotationModel);
+ this._styler.setFoldingEnabled(false);
+ break;
+
+ case SourceEditor.MODES.HTML:
+ case SourceEditor.MODES.XML:
+ let TextMateStyler =
+ window.require("orion/editor/textMateStyler").TextMateStyler;
+ let HtmlGrammar =
+ window.require("orion/editor/htmlGrammar").HtmlGrammar;
+ this._styler = new TextMateStyler(this._view, new HtmlGrammar());
+ break;
+ }
+
+ this._highlightAnnotations();
+ this._mode = aMode;
+ },
+
+ /**
+ * Get the current source editor mode.
+ *
+ * @return string
+ * Returns one of the predefined SourceEditor.MODES.
+ */
+ getMode: function SE_getMode()
+ {
+ return this._mode;
+ },
+
+ /**
+ * Setter for the read-only state of the editor.
+ * @param boolean aValue
+ * Tells if you want the editor to read-only or not.
+ */
+ set readOnly(aValue)
+ {
+ this._view.setOptions({
+ readonly: aValue,
+ themeClass: "mozilla" + (aValue ? " readonly" : ""),
+ });
+ },
+
+ /**
+ * Getter for the read-only state of the editor.
+ * @type boolean
+ */
+ get readOnly()
+ {
+ return this._view.getOptions("readonly");
+ },
+
+ /**
+ * Set the current debugger location at the given line index. This is useful in
+ * a debugger or in any other context where the user needs to track the
+ * current state, where a debugger-like environment is at.
+ *
+ * @param number aLineIndex
+ * Line index of the current debugger location, starting from 0.
+ * Use any negative number to clear the current location.
+ */
+ setDebugLocation: function SE_setDebugLocation(aLineIndex)
+ {
+ let annotations = this._getAnnotationsByType("debugLocation", 0,
+ this.getCharCount());
+ if (annotations.length > 0) {
+ annotations.forEach(this._annotationModel.removeAnnotation,
+ this._annotationModel);
+ }
+ if (aLineIndex < 0) {
+ return;
+ }
+
+ let lineStart = this._model.getLineStart(aLineIndex);
+ let lineEnd = this._model.getLineEnd(aLineIndex);
+ let lineText = this._model.getLine(aLineIndex);
+ let title = SourceEditorUI.strings.
+ formatStringFromName("annotation.debugLocation.title",
+ [lineText], 1);
+
+ let annotation = {
+ type: ORION_ANNOTATION_TYPES.debugLocation,
+ start: lineStart,
+ end: lineEnd,
+ title: title,
+ style: {styleClass: "annotation debugLocation"},
+ html: "
",
+ overviewStyle: {styleClass: "annotationOverview debugLocation"},
+ rangeStyle: {styleClass: "annotationRange debugLocation"},
+ lineStyle: {styleClass: "annotationLine debugLocation"},
+ };
+ this._annotationModel.addAnnotation(annotation);
+ },
+
+ /**
+ * Retrieve the current debugger line index configured for this editor.
+ *
+ * @return number
+ * The line index starting from 0 where the current debugger is
+ * paused. If no debugger location has been set -1 is returned.
+ */
+ getDebugLocation: function SE_getDebugLocation()
+ {
+ let annotations = this._getAnnotationsByType("debugLocation", 0,
+ this.getCharCount());
+ if (annotations.length > 0) {
+ return this._model.getLineAtOffset(annotations[0].start);
+ }
+ return -1;
+ },
+
+ /**
+ * Add a breakpoint at the given line index.
+ *
+ * @param number aLineIndex
+ * Line index where to add the breakpoint (starts from 0).
+ * @param string [aCondition]
+ * Optional breakpoint condition.
+ */
+ addBreakpoint: function SE_addBreakpoint(aLineIndex, aCondition)
+ {
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+
+ let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd);
+ if (annotations.length > 0) {
+ return;
+ }
+
+ let lineText = this._model.getLine(aLineIndex);
+ let title = SourceEditorUI.strings.
+ formatStringFromName("annotation.breakpoint.title",
+ [lineText], 1);
+
+ let annotation = {
+ type: ORION_ANNOTATION_TYPES.breakpoint,
+ start: lineStart,
+ end: lineEnd,
+ breakpointCondition: aCondition,
+ title: title,
+ style: {styleClass: "annotation breakpoint"},
+ html: "
",
+ overviewStyle: {styleClass: "annotationOverview breakpoint"},
+ rangeStyle: {styleClass: "annotationRange breakpoint"}
+ };
+ this._annotationModel.addAnnotation(annotation);
+
+ let event = {
+ type: SourceEditor.EVENTS.BREAKPOINT_CHANGE,
+ added: [{line: aLineIndex, condition: aCondition}],
+ removed: [],
+ };
+
+ this._dispatchEvent(event);
+ },
+
+ /**
+ * Remove the current breakpoint from the given line index.
+ *
+ * @param number aLineIndex
+ * Line index from where to remove the breakpoint (starts from 0).
+ * @return boolean
+ * True if a breakpoint was removed, false otherwise.
+ */
+ removeBreakpoint: function SE_removeBreakpoint(aLineIndex)
+ {
+ let lineStart = this.getLineStart(aLineIndex);
+ let lineEnd = this.getLineEnd(aLineIndex);
+
+ let event = {
+ type: SourceEditor.EVENTS.BREAKPOINT_CHANGE,
+ added: [],
+ removed: [],
+ };
+
+ let annotations = this._getAnnotationsByType("breakpoint", lineStart, lineEnd);
+
+ annotations.forEach(function(annotation) {
+ this._annotationModel.removeAnnotation(annotation);
+ event.removed.push({line: aLineIndex,
+ condition: annotation.breakpointCondition});
+ }, this);
+
+ if (event.removed.length > 0) {
+ this._dispatchEvent(event);
+ }
+
+ return event.removed.length > 0;
+ },
+
+ /**
+ * Get the list of breakpoints in the Source Editor instance.
+ *
+ * @return array
+ * The array of breakpoints. Each item is an object with two
+ * properties: line and condition.
+ */
+ getBreakpoints: function SE_getBreakpoints()
+ {
+ let annotations = this._getAnnotationsByType("breakpoint", 0,
+ this.getCharCount());
+ let breakpoints = [];
+
+ annotations.forEach(function(annotation) {
+ breakpoints.push({line: this._model.getLineAtOffset(annotation.start),
+ condition: annotation.breakpointCondition});
+ }, this);
+
+ return breakpoints;
+ },
+
+ /**
+ * Destroy/uninitialize the editor.
+ */
+ destroy: function SE_destroy()
+ {
+ if (this._config.highlightCurrentLine || Services.appinfo.OS == "Linux") {
+ this.removeEventListener(SourceEditor.EVENTS.SELECTION,
+ this._onOrionSelection);
+ }
+ this._onOrionSelection = null;
+
+ this.removeEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
+ this._onTextChanged);
+ this._onTextChanged = null;
+
+ if (this._contextMenu) {
+ this.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU,
+ this._onOrionContextMenu);
+ this._contextMenu = null;
+ }
+ this._onOrionContextMenu = null;
+
+ if (this._primarySelectionTimeout) {
+ let window = this.parentElement.ownerDocument.defaultView;
+ window.clearTimeout(this._primarySelectionTimeout);
+ this._primarySelectionTimeout = null;
+ }
+
+ this._view.destroy();
+ this.ui.destroy();
+ this.ui = null;
+
+ this.parentElement.removeChild(this._iframe);
+ this.parentElement = null;
+ this._iframeWindow = null;
+ this._iframe = null;
+ this._undoStack = null;
+ this._styler = null;
+ this._linesRuler = null;
+ this._annotationRuler = null;
+ this._overviewRuler = null;
+ this._dragAndDrop = null;
+ this._annotationModel = null;
+ this._annotationStyler = null;
+ this._currentLineAnnotation = null;
+ this._eventTarget = null;
+ this._eventListenersQueue = null;
+ this._view = null;
+ this._model = null;
+ this._config = null;
+ this._lastFind = null;
+ },
+};
diff --git a/scratchpad/modules/source-editor-ui.jsm b/scratchpad/modules/source-editor-ui.jsm
new file mode 100644
index 00000000..82fce6a8
--- /dev/null
+++ b/scratchpad/modules/source-editor-ui.jsm
@@ -0,0 +1,365 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Source Editor component.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Mihai Sucan (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+var EXPORTED_SYMBOLS = ["SourceEditorUI"];
+
+/**
+ * The Source Editor component user interface.
+ */
+function SourceEditorUI(aEditor)
+{
+ this.editor = aEditor;
+ this._onDirtyChanged = this._onDirtyChanged.bind(this);
+}
+
+SourceEditorUI.prototype = {
+ /**
+ * Initialize the user interface. This is called by the SourceEditor.init()
+ * method.
+ */
+ init: function SEU_init()
+ {
+ this._ownerWindow = this.editor.parentElement.ownerDocument.defaultView;
+ },
+
+ /**
+ * The UI onReady function is executed once the Source Editor completes
+ * initialization and it is ready for usage. Currently this code sets up the
+ * nsIController.
+ */
+ onReady: function SEU_onReady()
+ {
+ if (this._ownerWindow.controllers) {
+ this._controller = new SourceEditorController(this.editor);
+ this._ownerWindow.controllers.insertControllerAt(0, this._controller);
+ this.editor.addEventListener(this.editor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ }
+ },
+
+ /**
+ * The "go to line" command UI. This displays a prompt that allows the user to
+ * input the line number to jump to.
+ */
+ gotoLine: function SEU_gotoLine()
+ {
+ let oldLine = this.editor.getCaretPosition ?
+ this.editor.getCaretPosition().line : null;
+ let newLine = {value: oldLine !== null ? oldLine + 1 : ""};
+
+ let result = Services.prompt.prompt(this._ownerWindow,
+ SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptTitle"),
+ SourceEditorUI.strings.GetStringFromName("gotoLineCmd.promptMessage"),
+ newLine, null, {});
+
+ newLine.value = parseInt(newLine.value);
+ if (result && !isNaN(newLine.value) && --newLine.value != oldLine) {
+ if (this.editor.getLineCount) {
+ let lines = this.editor.getLineCount() - 1;
+ this.editor.setCaretPosition(Math.max(0, Math.min(lines, newLine.value)));
+ } else {
+ this.editor.setCaretPosition(Math.max(0, newLine.value));
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * The "find" command UI. This displays a prompt that allows the user to input
+ * the string to search for in the code. By default the current selection is
+ * used as a search string, or the last search string.
+ */
+ find: function SEU_find()
+ {
+ let str = {value: this.editor.getSelectedText()};
+ if (!str.value && this.editor.lastFind) {
+ str.value = this.editor.lastFind.str;
+ }
+
+ let result = Services.prompt.prompt(this._ownerWindow,
+ SourceEditorUI.strings.GetStringFromName("findCmd.promptTitle"),
+ SourceEditorUI.strings.GetStringFromName("findCmd.promptMessage"),
+ str, null, {});
+
+ if (result && str.value) {
+ let start = this.editor.getSelection().end;
+ let pos = this.editor.find(str.value, {ignoreCase: true, start: start});
+ if (pos == -1) {
+ this.editor.find(str.value, {ignoreCase: true});
+ }
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * Find the next occurrence of the last search string.
+ */
+ findNext: function SEU_findNext()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind) {
+ this.editor.findNext(true);
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * Find the previous occurrence of the last search string.
+ */
+ findPrevious: function SEU_findPrevious()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind) {
+ this.editor.findPrevious(true);
+ this._onFind();
+ }
+
+ return true;
+ },
+
+ /**
+ * This executed after each find/findNext/findPrevious operation.
+ * @private
+ */
+ _onFind: function SEU__onFind()
+ {
+ let lastFind = this.editor.lastFind;
+ if (lastFind && lastFind.index > -1) {
+ this.editor.setSelection(lastFind.index, lastFind.index + lastFind.str.length);
+ }
+
+ if (this._ownerWindow.goUpdateCommand) {
+ this._ownerWindow.goUpdateCommand("cmd_findAgain");
+ this._ownerWindow.goUpdateCommand("cmd_findPrevious");
+ }
+ },
+
+ /**
+ * This is executed after each undo/redo operation.
+ * @private
+ */
+ _onUndoRedo: function SEU__onUndoRedo()
+ {
+ if (this._ownerWindow.goUpdateCommand) {
+ this._ownerWindow.goUpdateCommand("se-cmd-undo");
+ this._ownerWindow.goUpdateCommand("se-cmd-redo");
+ }
+ },
+
+ /**
+ * The DirtyChanged event handler for the editor. This tracks the editor state
+ * changes to make sure the Source Editor overlay Undo/Redo commands are kept
+ * up to date.
+ * @private
+ */
+ _onDirtyChanged: function SEU__onDirtyChanged()
+ {
+ this._onUndoRedo();
+ },
+
+ /**
+ * Destroy the SourceEditorUI instance. This is called by the
+ * SourceEditor.destroy() method.
+ */
+ destroy: function SEU_destroy()
+ {
+ if (this._ownerWindow.controllers) {
+ this.editor.removeEventListener(this.editor.EVENTS.DIRTY_CHANGED,
+ this._onDirtyChanged);
+ }
+
+ this._ownerWindow = null;
+ this.editor = null;
+ this._controller = null;
+ },
+};
+
+/**
+ * The Source Editor nsIController implements features that need to be available
+ * from XUL commands.
+ *
+ * @constructor
+ * @param object aEditor
+ * SourceEditor object instance for which the controller is instanced.
+ */
+function SourceEditorController(aEditor)
+{
+ this._editor = aEditor;
+}
+
+SourceEditorController.prototype = {
+ /**
+ * Check if a command is supported by the controller.
+ *
+ * @param string aCommand
+ * The command name you want to check support for.
+ * @return boolean
+ * True if the command is supported, false otherwise.
+ */
+ supportsCommand: function SEC_supportsCommand(aCommand)
+ {
+ let result;
+
+ switch (aCommand) {
+ case "cmd_find":
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ case "cmd_gotoLine":
+ case "se-cmd-undo":
+ case "se-cmd-redo":
+ case "se-cmd-cut":
+ case "se-cmd-paste":
+ case "se-cmd-delete":
+ case "se-cmd-selectAll":
+ result = true;
+ break;
+ default:
+ result = false;
+ break;
+ }
+
+ return result;
+ },
+
+ /**
+ * Check if a command is enabled or not.
+ *
+ * @param string aCommand
+ * The command name you want to check if it is enabled or not.
+ * @return boolean
+ * True if the command is enabled, false otherwise.
+ */
+ isCommandEnabled: function SEC_isCommandEnabled(aCommand)
+ {
+ let result;
+
+ switch (aCommand) {
+ case "cmd_find":
+ case "cmd_gotoLine":
+ case "se-cmd-selectAll":
+ result = true;
+ break;
+ case "cmd_findAgain":
+ case "cmd_findPrevious":
+ result = this._editor.lastFind && this._editor.lastFind.lastFound != -1;
+ break;
+ case "se-cmd-undo":
+ result = this._editor.canUndo();
+ break;
+ case "se-cmd-redo":
+ result = this._editor.canRedo();
+ break;
+ case "se-cmd-cut":
+ case "se-cmd-delete": {
+ let selection = this._editor.getSelection();
+ result = selection.start != selection.end && !this._editor.readOnly;
+ break;
+ }
+ case "se-cmd-paste": {
+ let window = this._editor._view._frameWindow;
+ let controller = window.controllers.getControllerForCommand("cmd_paste");
+ result = !this._editor.readOnly &&
+ controller.isCommandEnabled("cmd_paste");
+ break;
+ }
+ default:
+ result = false;
+ break;
+ }
+
+ return result;
+ },
+
+ /**
+ * Perform a command.
+ *
+ * @param string aCommand
+ * The command name you want to execute.
+ * @return void
+ */
+ doCommand: function SEC_doCommand(aCommand)
+ {
+ switch (aCommand) {
+ case "cmd_find":
+ this._editor.ui.find();
+ break;
+ case "cmd_findAgain":
+ this._editor.ui.findNext();
+ break;
+ case "cmd_findPrevious":
+ this._editor.ui.findPrevious();
+ break;
+ case "cmd_gotoLine":
+ this._editor.ui.gotoLine();
+ break;
+ case "se-cmd-selectAll":
+ this._editor._view.invokeAction("selectAll");
+ break;
+ case "se-cmd-undo":
+ this._editor.undo();
+ break;
+ case "se-cmd-redo":
+ this._editor.redo();
+ break;
+ case "se-cmd-cut":
+ this._editor.ui._ownerWindow.goDoCommand("cmd_cut");
+ break;
+ case "se-cmd-paste":
+ this._editor.ui._ownerWindow.goDoCommand("cmd_paste");
+ break;
+ case "se-cmd-delete": {
+ let selection = this._editor.getSelection();
+ this._editor.setText("", selection.start, selection.end);
+ break;
+ }
+ }
+ },
+
+ onEvent: function() { }
+};
diff --git a/scratchpad/modules/source-editor.jsm b/scratchpad/modules/source-editor.jsm
new file mode 100644
index 00000000..9947c3d3
--- /dev/null
+++ b/scratchpad/modules/source-editor.jsm
@@ -0,0 +1,482 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Source Editor component.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Mihai Sucan (original author)
+ * Spyros Livathinos
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK *****/
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://scratchpad/source-editor-ui.jsm");
+
+const PREF_EDITOR_COMPONENT = "orion";
+const SOURCEEDITOR_L10N = "chrome://scratchpad/locale/sourceeditor.properties";
+
+var component = PREF_EDITOR_COMPONENT;
+var obj = {};
+try {
+ if (component == "ui") {
+ throw new Error("The ui editor component is not available.");
+ }
+ Cu.import("resource://scratchpad/source-editor-" + component + ".jsm", obj);
+} catch (ex) {
+ Cu.reportError(ex);
+ Cu.reportError("SourceEditor component failed to load: " + component);
+}
+
+// Export the SourceEditor.
+var SourceEditor = obj.SourceEditor;
+var EXPORTED_SYMBOLS = ["SourceEditor"];
+
+// Add the constants used by all SourceEditors.
+
+XPCOMUtils.defineLazyGetter(SourceEditorUI, "strings", function() {
+ return Services.strings.createBundle(SOURCEEDITOR_L10N);
+});
+
+/**
+ * Known SourceEditor preferences.
+ */
+SourceEditor.PREFS = {
+ TAB_SIZE: "devtools.editor.tabsize",
+ EXPAND_TAB: "devtools.editor.expandtab",
+ COMPONENT: PREF_EDITOR_COMPONENT,
+};
+
+/**
+ * Predefined source editor modes for JavaScript, CSS and other languages.
+ */
+SourceEditor.MODES = {
+ JAVASCRIPT: "js",
+ CSS: "css",
+ TEXT: "text",
+ HTML: "html",
+ XML: "xml",
+};
+
+/**
+ * Predefined themes for syntax highlighting.
+ */
+SourceEditor.THEMES = {
+ MOZILLA: "mozilla",
+};
+
+/**
+ * Source editor configuration defaults.
+ * @see SourceEditor.init
+ */
+SourceEditor.DEFAULTS = {
+ /**
+ * The text you want shown when the editor opens up.
+ * @type string
+ */
+ initialText: "",
+
+ /**
+ * The editor mode, based on the file type you want to edit. You can use one of
+ * the predefined modes.
+ *
+ * @see SourceEditor.MODES
+ * @type string
+ */
+ mode: SourceEditor.MODES.TEXT,
+
+ /**
+ * The syntax highlighting theme you want. You can use one of the predefined
+ * themes, or you can point to your CSS file.
+ *
+ * @see SourceEditor.THEMES.
+ * @type string
+ */
+ theme: SourceEditor.THEMES.MOZILLA,
+
+ /**
+ * How many steps should the undo stack hold.
+ * @type number
+ */
+ undoLimit: 200,
+
+ /**
+ * Define how many spaces to use for a tab character. This value is overridden
+ * by a user preference, see SourceEditor.PREFS.TAB_SIZE.
+ *
+ * @type number
+ */
+ tabSize: 2,
+
+ /**
+ * Tells if you want tab characters to be expanded to spaces. This value is
+ * overridden by a user preference, see SourceEditor.PREFS.EXPAND_TAB.
+ * @type boolean
+ */
+ expandTab: true,
+
+ /**
+ * Tells if you want the editor to be read only or not.
+ * @type boolean
+ */
+ readOnly: false,
+
+ /**
+ * Display the line numbers gutter.
+ * @type boolean
+ */
+ showLineNumbers: false,
+
+ /**
+ * Display the annotations gutter/ruler. This gutter currently supports
+ * annotations of breakpoint type.
+ * @type boolean
+ */
+ showAnnotationRuler: false,
+
+ /**
+ * Display the overview gutter/ruler. This gutter presents an overview of the
+ * current annotations in the editor, for example the breakpoints.
+ * @type boolean
+ */
+ showOverviewRuler: false,
+
+ /**
+ * Highlight the current line.
+ * @type boolean
+ */
+ highlightCurrentLine: true,
+
+ /**
+ * An array of objects that allows you to define custom editor keyboard
+ * bindings. Each object can have:
+ * - action - name of the editor action to invoke.
+ * - code - keyCode for the shortcut.
+ * - accel - boolean for the Accel key (Cmd on Macs, Ctrl on Linux/Windows).
+ * - ctrl - boolean for the Control key
+ * - shift - boolean for the Shift key.
+ * - alt - boolean for the Alt key.
+ * - callback - optional function to invoke, if the action is not predefined
+ * in the editor.
+ * @type array
+ */
+ keys: null,
+
+ /**
+ * The editor context menu you want to display when the user right-clicks
+ * within the editor. This property can be:
+ * - a string that tells the ID of the xul:menupopup you want. This needs to
+ * be available within the editor parentElement.ownerDocument.
+ * - an nsIDOMElement object reference pointing to the xul:menupopup you
+ * want to open when the contextmenu event is fired.
+ *
+ * Set this property to a falsey value to disable the default context menu.
+ *
+ * @see SourceEditor.EVENTS.CONTEXT_MENU for more control over the contextmenu
+ * event.
+ * @type string|nsIDOMElement
+ */
+ contextMenu: "sourceEditorContextMenu",
+};
+
+/**
+ * Known editor events you can listen for.
+ */
+SourceEditor.EVENTS = {
+ /**
+ * The contextmenu event is fired when the editor context menu is invoked. The
+ * event object properties:
+ * - x - the pointer location on the x axis, relative to the document the
+ * user is editing.
+ * - y - the pointer location on the y axis, relative to the document the
+ * user is editing.
+ * - screenX - the pointer location on the x axis, relative to the screen.
+ * This value comes from the DOM contextmenu event.screenX property.
+ * - screenY - the pointer location on the y axis, relative to the screen.
+ * This value comes from the DOM contextmenu event.screenY property.
+ *
+ * @see SourceEditor.DEFAULTS.contextMenu
+ */
+ CONTEXT_MENU: "ContextMenu",
+
+ /**
+ * The TextChanged event is fired when the editor content changes. The event
+ * object properties:
+ * - start - the character offset in the document where the change has
+ * occured.
+ * - removedCharCount - the number of characters removed from the document.
+ * - addedCharCount - the number of characters added to the document.
+ */
+ TEXT_CHANGED: "TextChanged",
+
+ /**
+ * The Selection event is fired when the editor selection changes. The event
+ * object properties:
+ * - oldValue - the old selection range.
+ * - newValue - the new selection range.
+ * Both ranges are objects which hold two properties: start and end.
+ */
+ SELECTION: "Selection",
+
+ /**
+ * The focus event is fired when the editor is focused.
+ */
+ FOCUS: "Focus",
+
+ /**
+ * The blur event is fired when the editor goes out of focus.
+ */
+ BLUR: "Blur",
+
+ /**
+ * The MouseMove event is sent when the user moves the mouse over a line.
+ * The event object properties:
+ * - event - the DOM mousemove event object.
+ * - x and y - the mouse coordinates relative to the document being edited.
+ */
+ MOUSE_MOVE: "MouseMove",
+
+ /**
+ * The MouseOver event is sent when the mouse pointer enters a line.
+ * The event object properties:
+ * - event - the DOM mouseover event object.
+ * - x and y - the mouse coordinates relative to the document being edited.
+ */
+ MOUSE_OVER: "MouseOver",
+
+ /**
+ * This MouseOut event is sent when the mouse pointer exits a line.
+ * The event object properties:
+ * - event - the DOM mouseout event object.
+ * - x and y - the mouse coordinates relative to the document being edited.
+ */
+ MOUSE_OUT: "MouseOut",
+
+ /**
+ * The BreakpointChange event is fired when a new breakpoint is added or when
+ * a breakpoint is removed - either through API use or through the editor UI.
+ * Event object properties:
+ * - added - array that holds the new breakpoints.
+ * - removed - array that holds the breakpoints that have been removed.
+ * Each object in the added/removed arrays holds two properties: line and
+ * condition.
+ */
+ BREAKPOINT_CHANGE: "BreakpointChange",
+
+ /**
+ * The DirtyChanged event is fired when the dirty state of the editor is
+ * changed. The dirty state of the editor tells if the are text changes that
+ * have not been saved yet. Event object properties: oldValue and newValue.
+ * Both are booleans telling the old dirty state and the new state,
+ * respectively.
+ */
+ DIRTY_CHANGED: "DirtyChanged",
+};
+
+/**
+ * Allowed vertical alignment options for the line index
+ * when you call SourceEditor.setCaretPosition().
+ */
+SourceEditor.VERTICAL_ALIGN = {
+ TOP: 0,
+ CENTER: 1,
+ BOTTOM: 2,
+};
+
+/**
+ * Extend a destination object with properties from a source object.
+ *
+ * @param object aDestination
+ * @param object aSource
+ */
+function extend(aDestination, aSource)
+{
+ for (let name in aSource) {
+ if (!aDestination.hasOwnProperty(name)) {
+ aDestination[name] = aSource[name];
+ }
+ }
+}
+
+/**
+ * Add methods common to all components.
+ */
+extend(SourceEditor.prototype, {
+ // Expose the static constants on the SourceEditor instances.
+ EVENTS: SourceEditor.EVENTS,
+ MODES: SourceEditor.MODES,
+ THEMES: SourceEditor.THEMES,
+ DEFAULTS: SourceEditor.DEFAULTS,
+ VERTICAL_ALIGN: SourceEditor.VERTICAL_ALIGN,
+
+ _lastFind: null,
+
+ /**
+ * Find a string in the editor.
+ *
+ * @param string aString
+ * The string you want to search for. If |aString| is not given the
+ * currently selected text is used.
+ * @param object [aOptions]
+ * Optional find options:
+ * - start: (integer) offset to start searching from. Default: 0 if
+ * backwards is false. If backwards is true then start = text.length.
+ * - ignoreCase: (boolean) tells if you want the search to be case
+ * insensitive or not. Default: false.
+ * - backwards: (boolean) tells if you want the search to go backwards
+ * from the given |start| offset. Default: false.
+ * @return integer
+ * The offset where the string was found.
+ */
+ find: function SE_find(aString, aOptions)
+ {
+ if (typeof(aString) != "string") {
+ return -1;
+ }
+
+ aOptions = aOptions || {};
+
+ let str = aOptions.ignoreCase ? aString.toLowerCase() : aString;
+
+ let text = this.getText();
+ if (aOptions.ignoreCase) {
+ text = text.toLowerCase();
+ }
+
+ let index = aOptions.backwards ?
+ text.lastIndexOf(str, aOptions.start) :
+ text.indexOf(str, aOptions.start);
+
+ let lastFoundIndex = index;
+ if (index == -1 && this.lastFind && this.lastFind.index > -1 &&
+ this.lastFind.str === aString &&
+ this.lastFind.ignoreCase === !!aOptions.ignoreCase) {
+ lastFoundIndex = this.lastFind.index;
+ }
+
+ this._lastFind = {
+ str: aString,
+ index: index,
+ lastFound: lastFoundIndex,
+ ignoreCase: !!aOptions.ignoreCase,
+ };
+
+ return index;
+ },
+
+ /**
+ * Find the next occurrence of the last search operation.
+ *
+ * @param boolean aWrap
+ * Tells if you want to restart the search from the beginning of the
+ * document if the string is not found.
+ * @return integer
+ * The offset where the string was found.
+ */
+ findNext: function SE_findNext(aWrap)
+ {
+ if (!this.lastFind && this.lastFind.lastFound == -1) {
+ return -1;
+ }
+
+ let options = {
+ start: this.lastFind.lastFound + this.lastFind.str.length,
+ ignoreCase: this.lastFind.ignoreCase,
+ };
+
+ let index = this.find(this.lastFind.str, options);
+ if (index == -1 && aWrap) {
+ options.start = 0;
+ index = this.find(this.lastFind.str, options);
+ }
+
+ return index;
+ },
+
+ /**
+ * Find the previous occurrence of the last search operation.
+ *
+ * @param boolean aWrap
+ * Tells if you want to restart the search from the end of the
+ * document if the string is not found.
+ * @return integer
+ * The offset where the string was found.
+ */
+ findPrevious: function SE_findPrevious(aWrap)
+ {
+ if (!this.lastFind && this.lastFind.lastFound == -1) {
+ return -1;
+ }
+
+ let options = {
+ start: this.lastFind.lastFound - this.lastFind.str.length,
+ ignoreCase: this.lastFind.ignoreCase,
+ backwards: true,
+ };
+
+ let index;
+ if (options.start > 0) {
+ index = this.find(this.lastFind.str, options);
+ } else {
+ index = this._lastFind.index = -1;
+ }
+
+ if (index == -1 && aWrap) {
+ options.start = this.getCharCount() - 1;
+ index = this.find(this.lastFind.str, options);
+ }
+
+ return index;
+ },
+});
+
+/**
+ * Retrieve the last find operation result. This object holds the following
+ * properties:
+ * - str: the last search string.
+ * - index: stores the result of the most recent find operation. This is the
+ * index in the text where |str| was found or -1 otherwise.
+ * - lastFound: tracks the index where |str| was last found, throughout
+ * multiple find operations. This can be -1 if |str| was never found in the
+ * document.
+ * - ignoreCase: tells if the search was case insensitive or not.
+ * @type object
+ */
+Object.defineProperty(SourceEditor.prototype, "lastFind", {
+ get: function() { return this._lastFind; },
+ enumerable: true,
+ configurable: true,
+});
+
diff --git a/scratchpad/moz.build b/scratchpad/moz.build
new file mode 100644
index 00000000..900fc1ba
--- /dev/null
+++ b/scratchpad/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# 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/.
+
+if not CONFIG['MOZ_DISABLE_PLATFORM']:
+ include('confvars.configure')
+ ConfVars('moz.build')
+ DIST_SUBDIR = 'extensions/%s' % DEFINES['ADDON_ID']
+ USE_EXTENSION_MANIFEST = True
+ DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+ DEFINES['MOZ_APP_ID'] = CONFIG['MOZ_APP_ID']
+
+if CONFIG['MOZ_DISABLE_PLATFORM']:
+ DEFINES['MOZ_DISABLE_PLATFORM'] = 1
+
+FINAL_TARGET_PP_FILES += ['install.rdf']
+
+JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file
diff --git a/scratchpad/moz.configure b/scratchpad/moz.configure
new file mode 100644
index 00000000..fe82fcc4
--- /dev/null
+++ b/scratchpad/moz.configure
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+include('../build/moz.configure/platform.configure')
+include('confvars.configure')
+ConfVars('moz.configure')
diff --git a/scratchpad/skin/classic/orion-breakpoint.png b/scratchpad/skin/classic/orion-breakpoint.png
new file mode 100644
index 00000000..85f73d97
Binary files /dev/null and b/scratchpad/skin/classic/orion-breakpoint.png differ
diff --git a/scratchpad/skin/classic/orion-container.css b/scratchpad/skin/classic/orion-container.css
new file mode 100644
index 00000000..02f9f66e
--- /dev/null
+++ b/scratchpad/skin/classic/orion-container.css
@@ -0,0 +1,39 @@
+/* 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/. */
+
+.viewTooltip {
+ display: none; /* TODO: add tooltips support, see bug 721752 */
+ font-family: monospace;
+ font-size: 13px;
+ background-color: InfoBackground;
+ color: InfoText;
+ padding: 2px;
+ border-radius: 4px;
+ border: 1px solid black;
+ z-index: 100;
+ position: fixed;
+ overflow: hidden;
+ white-space: pre;
+}
+
+.viewTooltip em {
+ font-style: normal;
+ font-weight: bold;
+}
+
+.annotationHTML {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.annotationHTML.task {
+ background-image: url("chrome://scratchpad/skin/orion-task.png");
+}
+.annotationHTML.breakpoint {
+ background-image: url("chrome://scratchpad/skin/orion-breakpoint.png");
+}
diff --git a/scratchpad/skin/classic/orion-debug-location.png b/scratchpad/skin/classic/orion-debug-location.png
new file mode 100644
index 00000000..ebb8d8d8
Binary files /dev/null and b/scratchpad/skin/classic/orion-debug-location.png differ
diff --git a/scratchpad/skin/classic/orion-task.png b/scratchpad/skin/classic/orion-task.png
new file mode 100644
index 00000000..42dbc00b
Binary files /dev/null and b/scratchpad/skin/classic/orion-task.png differ
diff --git a/scratchpad/skin/classic/orion.css b/scratchpad/skin/classic/orion.css
new file mode 100644
index 00000000..da75926e
--- /dev/null
+++ b/scratchpad/skin/classic/orion.css
@@ -0,0 +1,191 @@
+/* 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/. */
+
+.viewContainer {
+ background: #cddae5; /* This will be seen as the continuation of the ruler */
+ font-family: monospace;
+ font-size: inherit; /* inherit browser's default monospace font size */
+}
+
+.view {
+ color: black; /* Default text color */
+ background: #f0f0ff; /* Background of the editor */
+ padding-left: 4px;
+}
+
+.readonly > .view {
+ background: #f0f0ff;
+}
+
+.ruler {
+ background: #cddae5;
+ color: #7a8a99;
+}
+.ruler.annotations {
+ width: 16px;
+ padding-left: 4px;
+}
+.ruler.lines {
+ border-right: 1px solid #b4c4d3;
+ min-width: 1.4em;
+ padding-left: 4px;
+ padding-right: 4px;
+ text-align: end;
+}
+
+.ruler.linesWithAnnotations {
+ min-width: 0;
+ padding-left: 0;
+}
+
+.ruler.overview {
+ border-left: 1px solid #b4c4d3;
+ width: 14px;
+ text-align: start;
+}
+
+/* Styles for the annotation ruler (first line) */
+.annotationHTML {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.annotationHTML.task {
+ background-image: url("chrome://scratchpad/skin/orion-task.png");
+}
+.annotationHTML.breakpoint {
+ background-image: url("chrome://scratchpad/skin/orion-breakpoint.png");
+}
+.annotationHTML.debugLocation {
+ background-image: url("chrome://scratchpad/skin/orion-debug-location.png");
+}
+
+/* Styles for the overview ruler */
+.annotationOverview {
+ cursor: pointer;
+ border-radius: 2px;
+ left: 2px;
+ width: 8px;
+}
+.annotationOverview.task {
+ background-color: lightgreen;
+ border: 1px solid green;
+}
+.annotationOverview.breakpoint {
+ background-color: lightblue;
+ border: 1px solid blue;
+}
+.annotationOverview.debugLocation {
+ background-color: white;
+ border: 1px solid green;
+}
+.annotationOverview.currentBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+.annotationOverview.matchingBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+
+/* Styles for text range */
+.annotationRange {
+ background-repeat: repeat-x;
+ background-position: left bottom;
+}
+.annotationRange.task {
+ outline: 1px dashed rgba(0, 255, 0, 0.5);
+}
+.annotationRange.matchingBracket {
+ outline: 1px solid grey;
+}
+
+.token_singleline_comment {
+ color: #45a946; /* green */
+}
+
+.token_multiline_comment {
+ color: #45a946; /* green */
+}
+
+.token_doc_comment {
+ color: #45a946; /* green */
+}
+
+.token_doc_html_markup {
+ color: #dd0058; /* purple */
+}
+
+.token_doc_tag {
+ color: #dd0058; /* purple */
+}
+
+.token_task_tag { /* "TODO" */
+ color: black;
+ background: yellow;
+}
+
+.token_string {
+ color: #1e66b1; /* blue */
+ font-style: italic;
+}
+
+.token_keyword {
+ color: #dd0058; /* purple */
+}
+
+.token_space {
+ /* images/white_space.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAAABnRSTlMA/wAAAACkwsAdAAAAIUlEQVR4nGP4z8CAC+GUIEXuABhgkTuABEiRw2cmae4EAH05X7xDolNRAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+.token_tab {
+ /* images/white_tab.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAIAAACJ2loDAAAABnRSTlMA/wD/AP83WBt9AAAAMklEQVR4nGP4TwRgoK6i52c3bz5w6zMSA6tJn28d2Lx589nnCAYu63AaSLxJRLoJPwAAeNk0aG4opfMAAAAASUVORK5CYII=");
+ background-repeat: no-repeat;
+ background-position: left center;
+}
+
+.line_caret,
+.annotationLine.currentLine { /* Current line */
+ background: #dae2ee; /* lighter than the background */
+}
+
+.readonly .line_caret,
+.readonly .annotationLine.currentLine {
+ background: #cddae5; /* a bit darker than the background */
+}
+
+/* Styling for html syntax highlighting */
+.entity-name-tag {
+ color: #dd0058; /* purple */
+}
+
+.entity-other-attribute-name {
+ color: #dd0058; /* purple */
+}
+
+.punctuation-definition-comment {
+ color: #45a946; /* green */
+}
+
+.comment {
+ color: #45a946; /* green */
+}
+
+.string-quoted {
+ color: #1e66b1; /* blue */
+ font-style: italic;
+}
+
+.invalid {
+ color: red;
+ font-weight: bold;
+}
diff --git a/scratchpad/skin/classic/scratchpad.css b/scratchpad/skin/classic/scratchpad.css
new file mode 100644
index 00000000..2f5f653e
--- /dev/null
+++ b/scratchpad/skin/classic/scratchpad.css
@@ -0,0 +1,6 @@
+/* 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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
diff --git a/scratchpad/skin/modern/orion-breakpoint.png b/scratchpad/skin/modern/orion-breakpoint.png
new file mode 100644
index 00000000..85f73d97
Binary files /dev/null and b/scratchpad/skin/modern/orion-breakpoint.png differ
diff --git a/scratchpad/skin/modern/orion-container.css b/scratchpad/skin/modern/orion-container.css
new file mode 100644
index 00000000..02f9f66e
--- /dev/null
+++ b/scratchpad/skin/modern/orion-container.css
@@ -0,0 +1,39 @@
+/* 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/. */
+
+.viewTooltip {
+ display: none; /* TODO: add tooltips support, see bug 721752 */
+ font-family: monospace;
+ font-size: 13px;
+ background-color: InfoBackground;
+ color: InfoText;
+ padding: 2px;
+ border-radius: 4px;
+ border: 1px solid black;
+ z-index: 100;
+ position: fixed;
+ overflow: hidden;
+ white-space: pre;
+}
+
+.viewTooltip em {
+ font-style: normal;
+ font-weight: bold;
+}
+
+.annotationHTML {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.annotationHTML.task {
+ background-image: url("chrome://scratchpad/skin/orion-task.png");
+}
+.annotationHTML.breakpoint {
+ background-image: url("chrome://scratchpad/skin/orion-breakpoint.png");
+}
diff --git a/scratchpad/skin/modern/orion-debug-location.png b/scratchpad/skin/modern/orion-debug-location.png
new file mode 100644
index 00000000..ebb8d8d8
Binary files /dev/null and b/scratchpad/skin/modern/orion-debug-location.png differ
diff --git a/scratchpad/skin/modern/orion-task.png b/scratchpad/skin/modern/orion-task.png
new file mode 100644
index 00000000..42dbc00b
Binary files /dev/null and b/scratchpad/skin/modern/orion-task.png differ
diff --git a/scratchpad/skin/modern/orion.css b/scratchpad/skin/modern/orion.css
new file mode 100644
index 00000000..da75926e
--- /dev/null
+++ b/scratchpad/skin/modern/orion.css
@@ -0,0 +1,191 @@
+/* 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/. */
+
+.viewContainer {
+ background: #cddae5; /* This will be seen as the continuation of the ruler */
+ font-family: monospace;
+ font-size: inherit; /* inherit browser's default monospace font size */
+}
+
+.view {
+ color: black; /* Default text color */
+ background: #f0f0ff; /* Background of the editor */
+ padding-left: 4px;
+}
+
+.readonly > .view {
+ background: #f0f0ff;
+}
+
+.ruler {
+ background: #cddae5;
+ color: #7a8a99;
+}
+.ruler.annotations {
+ width: 16px;
+ padding-left: 4px;
+}
+.ruler.lines {
+ border-right: 1px solid #b4c4d3;
+ min-width: 1.4em;
+ padding-left: 4px;
+ padding-right: 4px;
+ text-align: end;
+}
+
+.ruler.linesWithAnnotations {
+ min-width: 0;
+ padding-left: 0;
+}
+
+.ruler.overview {
+ border-left: 1px solid #b4c4d3;
+ width: 14px;
+ text-align: start;
+}
+
+/* Styles for the annotation ruler (first line) */
+.annotationHTML {
+ cursor: pointer;
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: middle;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.annotationHTML.task {
+ background-image: url("chrome://scratchpad/skin/orion-task.png");
+}
+.annotationHTML.breakpoint {
+ background-image: url("chrome://scratchpad/skin/orion-breakpoint.png");
+}
+.annotationHTML.debugLocation {
+ background-image: url("chrome://scratchpad/skin/orion-debug-location.png");
+}
+
+/* Styles for the overview ruler */
+.annotationOverview {
+ cursor: pointer;
+ border-radius: 2px;
+ left: 2px;
+ width: 8px;
+}
+.annotationOverview.task {
+ background-color: lightgreen;
+ border: 1px solid green;
+}
+.annotationOverview.breakpoint {
+ background-color: lightblue;
+ border: 1px solid blue;
+}
+.annotationOverview.debugLocation {
+ background-color: white;
+ border: 1px solid green;
+}
+.annotationOverview.currentBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+.annotationOverview.matchingBracket {
+ background-color: lightgray;
+ border: 1px solid red;
+}
+
+/* Styles for text range */
+.annotationRange {
+ background-repeat: repeat-x;
+ background-position: left bottom;
+}
+.annotationRange.task {
+ outline: 1px dashed rgba(0, 255, 0, 0.5);
+}
+.annotationRange.matchingBracket {
+ outline: 1px solid grey;
+}
+
+.token_singleline_comment {
+ color: #45a946; /* green */
+}
+
+.token_multiline_comment {
+ color: #45a946; /* green */
+}
+
+.token_doc_comment {
+ color: #45a946; /* green */
+}
+
+.token_doc_html_markup {
+ color: #dd0058; /* purple */
+}
+
+.token_doc_tag {
+ color: #dd0058; /* purple */
+}
+
+.token_task_tag { /* "TODO" */
+ color: black;
+ background: yellow;
+}
+
+.token_string {
+ color: #1e66b1; /* blue */
+ font-style: italic;
+}
+
+.token_keyword {
+ color: #dd0058; /* purple */
+}
+
+.token_space {
+ /* images/white_space.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAIAAABv85FHAAAABnRSTlMA/wAAAACkwsAdAAAAIUlEQVR4nGP4z8CAC+GUIEXuABhgkTuABEiRw2cmae4EAH05X7xDolNRAAAAAElFTkSuQmCC");
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+
+.token_tab {
+ /* images/white_tab.png */
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAJCAIAAACJ2loDAAAABnRSTlMA/wD/AP83WBt9AAAAMklEQVR4nGP4TwRgoK6i52c3bz5w6zMSA6tJn28d2Lx589nnCAYu63AaSLxJRLoJPwAAeNk0aG4opfMAAAAASUVORK5CYII=");
+ background-repeat: no-repeat;
+ background-position: left center;
+}
+
+.line_caret,
+.annotationLine.currentLine { /* Current line */
+ background: #dae2ee; /* lighter than the background */
+}
+
+.readonly .line_caret,
+.readonly .annotationLine.currentLine {
+ background: #cddae5; /* a bit darker than the background */
+}
+
+/* Styling for html syntax highlighting */
+.entity-name-tag {
+ color: #dd0058; /* purple */
+}
+
+.entity-other-attribute-name {
+ color: #dd0058; /* purple */
+}
+
+.punctuation-definition-comment {
+ color: #45a946; /* green */
+}
+
+.comment {
+ color: #45a946; /* green */
+}
+
+.string-quoted {
+ color: #1e66b1; /* blue */
+ font-style: italic;
+}
+
+.invalid {
+ color: red;
+ font-weight: bold;
+}
diff --git a/scratchpad/skin/modern/scratchpad.css b/scratchpad/skin/modern/scratchpad.css
new file mode 100644
index 00000000..3af0f393
--- /dev/null
+++ b/scratchpad/skin/modern/scratchpad.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+#sp-toolbar {
+ background-image: url("chrome://communicator/skin/toolbar/prtb-bg-noline.gif");
+}
\ No newline at end of file