home *** CD-ROM | disk | FTP | other *** search
/ Freelog 112 / FreelogNo112-NovembreDecembre2012.iso / Multimedia / Songbird / Songbird_2.0.0-2311_windows-i686-msvc8.exe / components / sbServicePaneService.js < prev    next >
Text File  |  2012-06-08  |  35KB  |  1,084 lines

  1. /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* vim: set sw=2 :miv */
  3. /*
  4.  *=BEGIN SONGBIRD GPL
  5.  *
  6.  * This file is part of the Songbird web player.
  7.  *
  8.  * Copyright(c) 2005-2010 POTI, Inc.
  9.  * http://www.songbirdnest.com
  10.  *
  11.  * This file may be licensed under the terms of of the
  12.  * GNU General Public License Version 2 (the ``GPL'').
  13.  *
  14.  * Software distributed under the License is distributed
  15.  * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
  16.  * express or implied. See the GPL for the specific language
  17.  * governing rights and limitations.
  18.  *
  19.  * You should have received a copy of the GPL along with this
  20.  * program. If not, go to http://www.gnu.org/licenses/gpl.html
  21.  * or write to the Free Software Foundation, Inc.,
  22.  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  23.  *
  24.  *=END SONGBIRD GPL
  25.  */
  26.  
  27. /**
  28.  * \file ServicePaneService.js
  29.  * \brief the service pane service manages the tree behind the service pane
  30.  */
  31.  
  32. const Cc = Components.classes;
  33. const Ci = Components.interfaces;
  34. const Cr = Components.results;
  35. const Ce = Components.Exception;
  36. const Cu = Components.utils;
  37.  
  38. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  39. Components.utils.import("resource://app/jsmodules/ArrayConverter.jsm");
  40. Components.utils.import("resource://app/jsmodules/DebugUtils.jsm");
  41. Components.utils.import("resource://app/jsmodules/StringUtils.jsm");
  42.  
  43. const SP="http://songbirdnest.com/rdf/servicepane#";
  44.  
  45. const LOG = DebugUtils.generateLogFunction("sbServicePaneService");
  46.  
  47. function deprecationWarning(msg) {
  48.   // The code that called a deprecated function is two stack frames above us
  49.   let caller = Components.stack;
  50.   if (caller)
  51.     caller = caller.caller;
  52.   if (caller)
  53.     caller = caller.caller;
  54.  
  55.   // Skip empty frames
  56.   while (caller && !caller.filename)
  57.     caller = caller.caller;
  58.  
  59.   let consoleService = Cc["@mozilla.org/consoleservice;1"]
  60.                          .getService(Ci.nsIConsoleService);
  61.   let scriptError = Cc["@mozilla.org/scripterror;1"]
  62.                       .createInstance(Ci.nsIScriptError);
  63.   scriptError.init(msg,
  64.                    caller ? caller.filename : null,
  65.                    caller ? caller.sourceLine : null,
  66.                    caller ? caller.lineNumber : null,
  67.                    0,
  68.                    Ci.nsIScriptError.warningFlag,
  69.                    "sbServicePaneService");
  70.   consoleService.logMessage(scriptError);
  71. }
  72.  
  73. function ServicePaneNode(servicePane, comparisonFunction) {
  74.   this._servicePane = servicePane;
  75.   this._comparisonFunction = comparisonFunction;
  76.   this._attributes = {__proto__: null};
  77.   this._childNodes = [];
  78.   this._notificationQueue = [];
  79.  
  80.   // Allow accessing internal properties when we get a wrapped node passed
  81.   this.wrappedJSObject = this;
  82. }
  83.  
  84. ServicePaneNode.prototype = {
  85.   _attributes: null,
  86.   _childNodes: null,
  87.   _parentNode: null,
  88.   _nodeIndex: null,
  89.   _servicePane: null,
  90.   _comparisonFunction: null,
  91.   _stringBundle: null,
  92.   _stringBundleURI: null,
  93.   _isInTree: false,
  94.   _listeners: null,
  95.   _eventListeners: null,
  96.   _notificationQueue: null,
  97.   _notificationQueueBusy: false,
  98.  
  99.   QueryInterface: XPCOMUtils.generateQI([Ci.sbIServicePaneNode]),
  100.  
  101.   // Generic properties
  102.  
  103.   get isContainer() {
  104.     deprecationWarning("sbIServicePaneNode.isContainer is deprecated and may " +
  105.                        "be removed in future. All nodes are containers now.");
  106.     return this.getAttribute("isContainer") != "false";
  107.   },
  108.  
  109.   get properties() {
  110.     deprecationWarning("sbIServicePaneNode.properties is deprecated and " +
  111.                        "may be removed in future. Consider using " +
  112.                        "sbIServicePaneNode.className instead.");
  113.     return this.className;
  114.  
  115.   },
  116.   set properties(aValue) {
  117.     deprecationWarning("sbIServicePaneNode.properties is deprecated and " +
  118.                        "may be removed in future. Consider using " +
  119.                        "sbIServicePaneNode.className instead.");
  120.     return (this.className = aValue);
  121.   },
  122.  
  123.   get displayName() {
  124.     // Only names beginning with & should be translated
  125.     let name = this.name;
  126.     if (!name || name[0] != "&")
  127.       return name;
  128.  
  129.     // Cache the string bundle so that we don't retrieve it more than once
  130.     if (!this._stringBundle) {
  131.       let stringbundleURI = this.stringbundle;
  132.  
  133.       if (!stringbundleURI)  {
  134.         // Try module's stringbundle
  135.         let contractid = this.contractid;
  136.         if (contractid && contractid in this._servicePane._modulesByContractId) {
  137.           try {
  138.             let module = this._servicePane._modulesByContractId[contractid];
  139.             stringbundleURI = module.stringbundle;
  140.           }
  141.           catch (e) {
  142.             Components.utils.reportError(e);
  143.           }
  144.         }
  145.       }
  146.  
  147.       try {
  148.         this._stringBundle = new SBStringBundle(stringbundleURI);
  149.         this._stringBundleURI = stringbundleURI;
  150.       }
  151.       catch (e) {
  152.         LOG("sbServicePaneNode.displayName: failed retrieving string bundle " + stringbundleURI);
  153.         Components.utils.reportError(e);
  154.       }
  155.     }
  156.  
  157.     // Try to get displayed name from string bundle - fall back to key name
  158.     if (this._stringBundle)
  159.       return this._stringBundle.get(name.substr(1), name);
  160.  
  161.     return name;
  162.   },
  163.  
  164.   get isInTree() this._isInTree,
  165.   set isInTree(aValue) {
  166.     // Convert the parameter to a real boolean value, for sake of comparisons
  167.     aValue = !!aValue;
  168.  
  169.     if (aValue == this._isInTree)
  170.       return aValue;
  171.  
  172.     // Set the value and propagate the change to child nodes
  173.     this._isInTree = aValue;
  174.     for each (let child in this._childNodes)
  175.       child.isInTree = aValue;
  176.  
  177.     // Update service pane data
  178.     let notificationMethod = (aValue ? "_registerNode" : "_unregisterNode");
  179.     for each (let attr in ["id", "url", "contentPrefix"])
  180.       if (attr in this._attributes)
  181.         this._servicePane[notificationMethod](attr, this._attributes[attr], this);
  182.  
  183.     return this._isInTree;
  184.   },
  185.  
  186.   // Attribute functions without namespace
  187.  
  188.   hasAttribute: function(aName) aName in this._attributes,
  189.  
  190.   getAttribute: function(aName) {
  191.     if (aName in this._attributes)
  192.       return this._attributes[aName];
  193.     else
  194.       return null;
  195.   },
  196.  
  197.   setAttribute: function(aName, aValue) {
  198.     if (this.isInTree && (aName == "id" || aName == "url" || aName == "contentPrefix")) {
  199.       if (aName in this._attributes)
  200.         this._servicePane._unregisterNode(aName, this._attributes[aName], this);
  201.       if (aValue !== null)
  202.         this._servicePane._registerNode(aName, aValue, this);
  203.     }
  204.     else if (aName == "stringbundle" || aName == "contractid")
  205.       delete this._stringBundle;
  206.  
  207.     let oldValue = (aName in this._attributes ? this._attributes[aName] : null);
  208.  
  209.     if (aValue === null)
  210.       delete this._attributes[aName];
  211.     else
  212.       this._attributes[aName] = aValue;
  213.  
  214.     // Trigger mutation listeners
  215.     let attr = aName;
  216.     let namespace = null;
  217.     if (/(.*):/.test(attr))
  218.       [namespace, attr] = [RegExp.$1, RegExp.rightContext];
  219.     this._notifyMutationListeners("attrModified", [this, attr, namespace,
  220.                                                    oldValue, aValue]);
  221.  
  222.     return aValue;
  223.   },
  224.  
  225.   removeAttribute: function(aName) {
  226.     if (this.isInTree && (aName == "id" || aName == "url" || aName == "contentPrefix") &&
  227.                           aName in this._attributes) {
  228.       this._servicePane._unregisterNode(aName, this._attributes[aName], this);
  229.     }
  230.     else if (aName == "stringbundle" || aName == "contractid")
  231.       delete this._stringBundle;
  232.  
  233.     let oldValue = (aName in this._attributes ? this._attributes[aName] : null);
  234.     delete this._attributes[aName];
  235.  
  236.     // Trigger mutation listeners
  237.     let attr = aName;
  238.     let namespace = null;
  239.     if (/(.*):/.test(attr))
  240.       [namespace, attr] = [RegExp.$1, RegExp.rightContext];
  241.     this._notifyMutationListeners("attrModified", [this, attr, namespace,
  242.                                                    oldValue, null]);
  243.   },
  244.  
  245.   // Attribute functions with namespace - these simply combine namespace and
  246.   // attribute name and fall back to the regular attribute functions
  247.  
  248.   hasAttributeNS: function(aNamespace, aName)
  249.     this.hasAttribute(aNamespace + ":" + aName),
  250.  
  251.   getAttributeNS: function(aNamespace, aName)
  252.     this.getAttribute(aNamespace + ":" + aName),
  253.  
  254.   setAttributeNS: function(aNamespace, aName, aValue)
  255.     this.setAttribute(aNamespace + ":" + aName, aValue),
  256.  
  257.   removeAttributeNS: function(aNamespace, aName)
  258.     this.removeAttribute(aNamespace + ":" + aName),
  259.  
  260.   // DOM Properties
  261.  
  262.   get attributes() {
  263.     let attrNames = [];
  264.     for (let name in this._attributes)
  265.       attrNames.push(name);
  266.     return ArrayConverter.stringEnumerator(attrNames);
  267.   },
  268.  
  269.   get childNodes() ArrayConverter.enumerator(this._childNodes),
  270.  
  271.   get firstChild() {
  272.     if (this._childNodes.length)
  273.       return this._childNodes[0];
  274.     else
  275.       return null;
  276.   },
  277.  
  278.   get lastChild() {
  279.     if (this._childNodes.length)
  280.       return this._childNodes[this._childNodes.length - 1];
  281.     else
  282.       return null;
  283.   },
  284.  
  285.   get parentNode() this._parentNode,
  286.  
  287.   get previousSibling() {
  288.     let parent = this._parentNode;
  289.     if (!parent)
  290.       return null;
  291.  
  292.     let index = this._nodeIndex - 1;
  293.     if (index >= 0)
  294.       return parent._childNodes[index];
  295.     else
  296.       return null;
  297.   },
  298.  
  299.   get nextSibling() {
  300.     let parent = this._parentNode;
  301.     if (!parent)
  302.       return null;
  303.  
  304.     let index = this._nodeIndex + 1;
  305.     if (index < parent._childNodes.length)
  306.       return parent._childNodes[index];
  307.     else
  308.       return null;
  309.   },
  310.  
  311.   // DOM methods
  312.  
  313.   appendChild: function(aChild) {
  314.     return this.insertBefore(aChild, null);
  315.   },
  316.  
  317.   insertBefore: function(aChild, aBefore) {
  318.     // Unwrap parameters so that we can access internal properties
  319.     if (aChild)
  320.       aChild = aChild.wrappedJSObject;
  321.     if (aBefore)
  322.       aBefore = aBefore.wrappedJSObject;
  323.  
  324.     if (!aChild)
  325.       throw Ce("Node to be inserted/appended is a required parameter");
  326.  
  327.     function checkConditions() {
  328.       // Don't check aBefore if we have a custom comparison function, we won't
  329.       // use it anyway in that case.
  330.       if (!this._comparisonFunction && aBefore && aBefore._parentNode != this)
  331.         throw Ce("Cannot insert before a node that isn't a child");
  332.  
  333.       for (let parent = this; parent; parent = parent._parentNode)
  334.         if (parent == aChild)
  335.           throw Ce("Cannot insert/append a node to its child");
  336.     }
  337.  
  338.     checkConditions.apply(this);
  339.  
  340.     if (!this._comparisonFunction && aBefore == aChild)
  341.       return aChild;    // nothing to do
  342.  
  343.     if (aChild._parentNode) {
  344.       aChild._parentNode.removeChild(aChild);
  345.       if (aChild._parentNode)
  346.         throw Ce("Mutation listener prevented removal of node from its previous location");
  347.  
  348.       // Check conditions again, mutation listeners might have modified the tree
  349.       checkConditions.apply(this);
  350.     }
  351.     if (this._comparisonFunction) {
  352.       // Use comparison function to determine node position
  353.       let index = 0;
  354.       for (; index < this._childNodes.length; index++)
  355.         if (this._comparisonFunction(aChild, this._childNodes[index]) < 0)
  356.           break;
  357.       if (index < this._childNodes.length)
  358.         aBefore = this._childNodes[index];
  359.       else
  360.         aBefore = null;
  361.     }
  362.  
  363.     let index = aBefore ? aBefore._nodeIndex : this._childNodes.length;
  364.     for (let i = index; i < this._childNodes.length; i++)
  365.       this._childNodes[i]._nodeIndex++;
  366.  
  367.     this._childNodes.splice(index, 0, aChild);
  368.     aChild._nodeIndex = index;
  369.     aChild._parentNode = this;
  370.     aChild.isInTree = this.isInTree;
  371.  
  372.     // Trigger mutation listeners
  373.     aChild._notifyMutationListeners("nodeInserted", [aChild, this, aBefore]);
  374.  
  375.     return aChild;
  376.   },
  377.  
  378.   removeChild: function(aChild) {
  379.     // Unwrap parameters so that we can access internal properties
  380.     if (aChild)
  381.       aChild = aChild.wrappedJSObject;
  382.  
  383.     if (!aChild || aChild._parentNode != this)
  384.       throw Ce("Cannot remove a node that isn't a child");
  385.  
  386.     let wasInTree = aChild.isInTree;
  387.     let oldParentNode = aChild._parentNode;
  388.  
  389.     let index = aChild._nodeIndex;
  390.     for (let i = index + 1; i < this._childNodes.length; i++)
  391.       this._childNodes[i]._nodeIndex--;
  392.  
  393.     this._childNodes.splice(index, 1);
  394.     delete aChild._nodeIndex;
  395.     delete aChild._parentNode;
  396.     aChild.isInTree = null;
  397.  
  398.     // Trigger mutation listeners
  399.     aChild._notifyMutationListeners("nodeRemoved", [aChild, this], wasInTree, oldParentNode);
  400.  
  401.     return aChild;
  402.   },
  403.  
  404.   replaceChild: function(aChild, aOldChild) {
  405.     // Unwrap parameters so that we can access internal properties
  406.     if (aChild)
  407.       aChild = aChild.wrappedJSObject;
  408.     if (aOldChild)
  409.       aOldChild = aOldChild.wrappedJSObject;
  410.  
  411.     if (!aOldChild || aOldChild._parentNode != this)
  412.       throw Ce("Cannot replace a node that isn't a child");
  413.  
  414.     let insertBefore = aOldChild.nextSibling;
  415.     this.removeChild(aOldChild);
  416.     this.insertBefore(aChild, insertBefore);
  417.   },
  418.  
  419.   addEventListener: function(aListener) {
  420.     if (this._eventListeners == null)
  421.       this._eventListeners = [];
  422.  
  423.     // Don't add listeners twice
  424.     for each (let listener in this._eventListeners)
  425.       if (listener == aListener)
  426.         return;
  427.  
  428.     this._eventListeners.push(aListener);
  429.   },
  430.  
  431.   removeEventListener: function(aListener) {
  432.     if (this._eventListeners == null)
  433.       return;
  434.  
  435.     for (let i = 0; i < this._eventListeners.length; i++)
  436.       if (this._eventListeners[i] == aListener)
  437.         this._eventListeners.splice(i--, 1);
  438.  
  439.     if (this._eventListeners.length == 0)
  440.       this._eventListeners = null;
  441.   },
  442.  
  443.   dispatchEvent: function(aEventName) {
  444.     for each (let listener in this._eventListeners)
  445.       listener.handleEvent(aEventName);
  446.   },
  447.  
  448.   addMutationListener: function(aListener) {
  449.     if (this._listeners == null)
  450.       this._listeners = [];
  451.  
  452.     // Don't add listeners twice
  453.     for each (let listener in this._listeners)
  454.       if (listener == aListener)
  455.         return;
  456.  
  457.     this._listeners.push(aListener);
  458.   },
  459.  
  460.   removeMutationListener: function(aListener) {
  461.     if (this._listeners == null)
  462.       return;
  463.  
  464.     for (let i = 0; i < this._listeners.length; i++)
  465.       if (this._listeners[i] == aListener)
  466.         this._listeners.splice(i--, 1);
  467.  
  468.     if (this._listeners.length == 0)
  469.       this._listeners = null;
  470.   },
  471.  
  472.   _notifyMutationListeners: function(aMethod, aArgs, aIsInTree, aParentNode) {
  473.     // Don't trigger listeners if we haven't been added to the service pane tree
  474.     let isInTree = (typeof aIsInTree != "undefined" ? aIsInTree : this.isInTree);
  475.     if (!isInTree)
  476.       return;
  477.  
  478.     // Queue the notification before sending it out, just in case one of the
  479.     // listeners decides to mess with the DOM structure. Add to the queue
  480.     // of this node and all of its parent nodes (bubbling).
  481.     let notification = [aMethod, aArgs];
  482.     let nodes = [];
  483.     let node = this;
  484.     let parent = (typeof aParentNode != "undefined" ? aParentNode : node._parentNode);
  485.     while (node) {
  486.       if (node._listeners != null) {
  487.         nodes.push(node);
  488.         node._notificationQueue.push(notification);
  489.       }
  490.  
  491.       node = parent;
  492.       if (node)
  493.         parent = node._parentNode;
  494.     }
  495.  
  496.     // Now actually trigger listeners for all nodes
  497.     for each (let node in nodes)
  498.       node._processNotificationQueue();
  499.   },
  500.  
  501.   _processNotificationQueue: function() {
  502.     if (this._listeners == null)
  503.       return;
  504.  
  505.     // Protect against reentrance
  506.     if (this._notificationQueueBusy)
  507.       return;
  508.     this._notificationQueueBusy = true;
  509.  
  510.     // Create a local copy of the listeners in case the list is modified while
  511.     // we are calling listeners.
  512.     let listeners = this._listeners.slice();
  513.  
  514.     while (this._notificationQueue.length) {
  515.       let [method, args] = this._notificationQueue.shift();
  516.       for each (let listener in listeners) {
  517.         try {
  518.           listener[method].apply(listener, args);
  519.         }
  520.         catch (e) {
  521.           Cu.reportError(e);
  522.         }
  523.       }
  524.     }
  525.  
  526.     this._notificationQueueBusy = false;
  527.   },
  528.  
  529.   // Attribute shortcuts
  530.  
  531.   get id() this.getAttribute("id"),
  532.   set id(aValue) this.setAttribute("id", aValue),
  533.   get className() this.getAttribute("class"),
  534.   set className(aValue) this.setAttribute("class", aValue),
  535.   get url() this.getAttribute("url"),
  536.   set url(aValue) this.setAttribute("url", aValue),
  537.   get contentPrefix() this.getAttribute("contentPrefix"),
  538.   set contentPrefix(aValue) this.setAttribute("contentPrefix", aValue),
  539.   get image() this.getAttribute("image"),
  540.   set image(aValue) this.setAttribute("image", aValue),
  541.   get name() this.getAttribute("name"),
  542.   set name(aValue) this.setAttribute("name", aValue),
  543.   get tooltip() this.getAttribute("tooltip"),
  544.   set tooltip(aValue) this.setAttribute("tooltip", aValue),
  545.   get hidden() this.getAttribute("hidden") == "true",
  546.   set hidden(aValue) {
  547.     return this.setAttribute("hidden", aValue ? "true" : "false");
  548.   },
  549.   get editable() this.getAttribute("editable") == "true",
  550.   set editable(aValue) this.setAttribute("editable", aValue ? "true" : "false"),
  551.   get isOpen() this.getAttribute("isOpen") != "false",
  552.   set isOpen(aValue) this.setAttribute("isOpen", aValue ? "true" : "false"),
  553.   get contractid() this.getAttribute("contractid"),
  554.   set contractid(aValue) this.setAttribute("contractid", aValue),
  555.   get searchtype() { return this.getAttribute("searchtype") || "internal"; },
  556.   set searchtype(aValue) this.setAttribute("searchtype", aValue),
  557.   get stringbundle() this.getAttribute("stringbundle"),
  558.   set stringbundle(aValue) this.setAttribute("stringbundle", aValue),
  559.   get dndDragTypes() this.getAttribute("dndDragTypes"),
  560.   set dndDragTypes(aValue) this.setAttribute("dndDragTypes", aValue),
  561.   get dndAcceptNear() this.getAttribute("dndAcceptNear"),
  562.   set dndAcceptNear(aValue) this.setAttribute("dndAcceptNear", aValue),
  563.   get dndAcceptIn() this.getAttribute("dndAcceptIn"),
  564.   set dndAcceptIn(aValue) this.setAttribute("dndAcceptIn", aValue),
  565. };
  566.  
  567.  
  568. /**
  569.  * \brief Class translating sbIServicePaneMutationListener calls into
  570.  * sbIServicePaneListener calls.
  571.  */
  572. function MutationEventTranslator(aListener) {
  573.   this.listener = aListener;
  574. }
  575. MutationEventTranslator.prototype = {
  576.   listener: null,
  577.  
  578.   QueryInterface: XPCOMUtils.generateQI([Ci.sbIServicePaneMutationListener]),
  579.  
  580.   attrModified: function(aNode, aAttrName, aNamespace, aOldValue, aNewValue) {
  581.     if (aNode.id)
  582.       this.listener.nodePropertyChanged(aNode.id,
  583.                                         aNamespace == null ? aAttrName : aNamespace + aAttrName);
  584.   },
  585.  
  586.   nodeInserted: function() {},
  587.   nodeRemoved: function() {}
  588. }
  589.  
  590. function ServicePaneService () {
  591.   LOG("Service pane initialization started");
  592.  
  593.   // Create root node - children are sorted automatically
  594.  
  595.   this._nodesById = {__proto__: null};
  596.   this._nodesByUrl = {__proto__: null};
  597.   this._nodesByContentPrefix = {__proto__: null};
  598.   this._root = new ServicePaneNode(this, function(aNode1, aNode2) {
  599.     // Nodes with lower weight go first
  600.     let weight1 = parseInt(aNode1.getAttributeNS(SP, 'Weight')) || 0;
  601.     let weight2 = parseInt(aNode2.getAttributeNS(SP, 'Weight')) || 0;
  602.     if (weight1 != weight2)
  603.       return weight1 - weight2;
  604.  
  605.     // For equal weight, sort alphabetically
  606.     let name1 = aNode1.displayName;
  607.     let name2 = aNode2.displayName;
  608.     if (name1 < name2)
  609.       return -1;
  610.     else if (name1 > name2)
  611.       return 1;
  612.  
  613.     return 0;
  614.   });
  615.   this._root.isInTree = true;
  616.  
  617.   // Initialize modules
  618.  
  619.   let catMgr = Cc["@mozilla.org/categorymanager;1"]
  620.                  .getService(Ci.nsICategoryManager);
  621.   let enumerator = catMgr.enumerateCategory("service-pane");
  622.   let moduleEntries = ArrayConverter.JSArray(enumerator).map(
  623.     function(entry) entry.QueryInterface(Ci.nsISupportsCString).data
  624.   );
  625.   moduleEntries.sort();
  626.  
  627.   this._modules = [];
  628.   this._modulesByContractId = {__proto__: null};
  629.   this._categoryEntriesCache = {__proto__: null};
  630.   for each (let entry in moduleEntries)
  631.     this._loadModule(catMgr, entry);
  632.   LOG("Service pane initialization completed successfully");
  633.  
  634.   // Listen for modules being added or removed
  635.   let observerService = Cc["@mozilla.org/observer-service;1"]
  636.                           .getService(Ci.nsIObserverService);
  637.   observerService.addObserver(this, "xpcom-category-entry-added", true);
  638.   observerService.addObserver(this, "xpcom-category-entry-removed", true);
  639.   observerService.addObserver(this, "quit-application", false);
  640. }
  641.  
  642. ServicePaneService.prototype = {
  643.   // XPCOM component info
  644.   classID: Components.ID("{eb5c665a-bfe2-49f1-a747-cd3554e55606}"),
  645.   classDescription: "Songbird Service Pane Service",
  646.   contractID: "@songbirdnest.com/servicepane/service;1",
  647.  
  648.   _modules: null,
  649.   _modulesByContractId: null,
  650.   _categoryEntriesCache: null,
  651.   _root: null,
  652.   _nodesById: null,
  653.   _nodesByUrl: null,
  654.   _nodeIndex: 0,
  655.  
  656.   get root() this._root,
  657.  
  658.   QueryInterface: XPCOMUtils.generateQI([Ci.sbIServicePaneService,
  659.                                          Ci.nsIObserver,
  660.                                          Ci.nsISupportsWeakReference]),
  661.  
  662.   observe: function(subject, topic, data) {
  663.     switch (topic) {
  664.       case "xpcom-category-entry-added":
  665.         if (data == "service-pane" && subject instanceof Ci.nsISupportsCString) {
  666.           let catMgr = Cc["@mozilla.org/categorymanager;1"]
  667.                          .getService(Ci.nsICategoryManager);
  668.           this._loadModule(catMgr, subject.data);
  669.         }
  670.         break;
  671.       case "xpcom-category-entry-removed":
  672.         if (data == "service-pane" && subject instanceof Ci.nsISupportsCString) {
  673.           this._removeModule(subject.data);
  674.         }
  675.         break;
  676.       case "quit-application":
  677.         this._shutdown();
  678.         break;
  679.     }
  680.   },
  681.  
  682.   _loadModule: function ServicePaneService__loadModule(catMgr, entry) {
  683.     let contractId = catMgr.getCategoryEntry("service-pane", entry);
  684.     this._categoryEntriesCache[entry] = contractId;
  685.  
  686.     // Don't load if already loaded
  687.     if (contractId in this._modulesByContractId)
  688.       return;
  689.  
  690.     LOG("Trying to load service pane module: " + contractId);
  691.     try {
  692.       let module = Cc[contractId].getService(Ci.sbIServicePaneModule);
  693.       module.servicePaneInit(this);
  694.       this._modules.push(module);
  695.       this._modulesByContractId[contractId] = module;
  696.       LOG("Service pane module successfully initialized");
  697.     } catch (e) {
  698.       LOG("Error instantiating service pane module: " + e);
  699.     }
  700.   },
  701.  
  702.   _removeModule: function ServicePaneService__removeModule(entry) {
  703.     // Don't bother if we don't know this module
  704.     if (!(entry in this._categoryEntriesCache))
  705.       return;
  706.     let contractId = this._categoryEntriesCache[entry];
  707.     if (!(contractId in this._modulesByContractId))
  708.       return;
  709.  
  710.     let module = this._modulesByContractId[contractId];
  711.     for (let i = 0; i < this._modules.length; i++)
  712.       if (this._modules[i] == module)
  713.         this._modules.splice(i--, 1);
  714.     delete this._modulesByContractId[contractId];
  715.     LOG("Service pane module " + contractId + " removed");
  716.   },
  717.  
  718.   init: function ServicePaneService_init() {
  719.     deprecationWarning("sbIServicePaneService.init() is deprecated, you no " +
  720.                        "longer need to call it.");
  721.   },
  722.  
  723.   _clearNodeListeners: function ServicePaneService__clearNodeListeners(aNode) {
  724.     if (aNode._eventListeners) {
  725.       delete aNode._eventListeners;
  726.     }
  727.     for each (let child in aNode.childNodes) {
  728.       this._clearNodeListeners(child);
  729.     }
  730.   },
  731.   _shutdown: function ServicePaneService__shutdown() {
  732.     this._clearNodeListeners(this.root);
  733.  
  734.     let observerService = Cc["@mozilla.org/observer-service;1"]
  735.                             .getService(Ci.nsIObserverService);
  736.     observerService.removeObserver(this, "quit-application");
  737.     observerService.removeObserver(this, "xpcom-category-entry-added");
  738.     observerService.removeObserver(this, "xpcom-category-entry-removed");
  739.     for each (let module in this._modules) {
  740.       try {
  741.         module.shutdown();
  742.       } catch (e) {
  743.         // Components.utils.reportError can't report to anywhere visible at
  744.         // this point, since to get here we must have closed the Error Console.
  745.         // Dump to stderr instead.
  746.         dump("**********************************************\n");
  747.         dump(e + "\n");
  748.         dump("**********************************************\n");
  749.       }
  750.     }
  751.     this._modules = [];
  752.     this._modulesByContractId = {};
  753.   },
  754.  
  755.   createNode: function ServicePaneService_createNode() {
  756.     return new ServicePaneNode(this, null);
  757.   },
  758.  
  759.   _registerNode: function ServicePaneService__registerNode(
  760.                                                       aAttr, aValue, aNode) {
  761.     let table;
  762.     switch (aAttr) {
  763.       case "id":
  764.         table = this._nodesById;
  765.         break;
  766.       case "url":
  767.         table = this._nodesByUrl;
  768.         break;
  769.       case "contentPrefix":
  770.         table = this._nodesByContentPrefix;
  771.         break;
  772.       default:
  773.         return;
  774.     }
  775.  
  776.     if (!(aValue in table))
  777.       table[aValue] = [];
  778.  
  779.     table[aValue].push(aNode);
  780.   },
  781.  
  782.   _unregisterNode: function ServicePaneService__unregisterNode(
  783.                                                       aAttr, aValue, aNode) {
  784.     let table;
  785.     switch (aAttr) {
  786.       case "id":
  787.         table = this._nodesById;
  788.         break;
  789.       case "url":
  790.         table = this._nodesByUrl;
  791.         break;
  792.       case "contentPrefix":
  793.         table = this._nodesByContentPrefix;
  794.         break;
  795.       default:
  796.         return;
  797.     }
  798.  
  799.     if (aValue in table)
  800.     {
  801.       let list = table[aValue];
  802.       for (let i = 0; i < list.length; i++)
  803.         if (list[i] == aNode)
  804.           list.splice(i--, 1);
  805.     }
  806.   },
  807.  
  808.   getNode: function ServicePaneService_getNode(aId) {
  809.     if (aId in this._nodesById && this._nodesById[aId].length) {
  810.       if (this._nodesById[aId].length > 1) {
  811.         // Warn if multiple nodes with same ID exist
  812.         let caller = Components.stack.caller;
  813.  
  814.         // Skip empty frames
  815.         while (caller && !caller.filename)
  816.           caller = caller.caller;
  817.  
  818.         let consoleService = Cc["@mozilla.org/consoleservice;1"]
  819.                                .getService(Ci.nsIConsoleService);
  820.         let scriptError = Cc["@mozilla.org/scripterror;1"]
  821.                             .createInstance(Ci.nsIScriptError);
  822.         scriptError.init("Multiple service pane nodes with ID '" + aId + "' exist, only returning one node.",
  823.                          caller ? caller.filename : null,
  824.                          caller ? caller.sourceLine : null,
  825.                          caller ? caller.lineNumber : null,
  826.                          0,
  827.                          Ci.nsIScriptError.warningFlag,
  828.                          "sbServicePaneService");
  829.         consoleService.logMessage(scriptError);
  830.       }
  831.       return this._nodesById[aId][0];
  832.     }
  833.     else
  834.       return null;
  835.   },
  836.  
  837.   getNodeForURL: function ServicePaneService_getNodeForURL(aUrl, aMatchLevel) {
  838.  
  839.     // Check to see if aUrl (a string url spec) begins with a node's
  840.     // contentPrefix. Only do this if the caller specifically requests a
  841.     // prefix match.
  842.     var prefixMatch = null;
  843.     if (!(typeof(aMatchLevel) == 'undefined') &&
  844.         aMatchLevel == Ci.sbIServicePaneService.URL_MATCH_PREFIX)
  845.     {
  846.       for (let prefix in this._nodesByContentPrefix) {
  847.         if (aUrl.indexOf(prefix) == 0 &&
  848.             this._nodesByContentPrefix[prefix].length)
  849.         {
  850.           prefixMatch = this._nodesByContentPrefix[prefix][0];
  851.         }
  852.       }
  853.     }
  854.  
  855.     // Check for an exact url match. This is done for both URL_MATCH_PREFIX and
  856.     // URL_MATCH_EXACT intentionally so that calls using URL_MATCH_PREFIX will
  857.     // still find a node if the url attribute is an exact match (even in the
  858.     // absence of a contentPrefix attribute)
  859.     if (aUrl in this._nodesByUrl && this._nodesByUrl[aUrl].length) {
  860.       // We prefer exact matches to prefix matches
  861.       return this._nodesByUrl[aUrl][0];
  862.     }
  863.     else {
  864.       // We didn't find an exact match. Return a prefix match if we found one.
  865.       // prefixMatch will always be null if the caller didn't pass in
  866.       // aMatchLevel or they passed URL_MATCH_EXACT
  867.       return prefixMatch;
  868.     }
  869.   },
  870.  
  871.   getNodesByAttributeNS: function ServicePaneService_getNodesByAttributeNS(
  872.                                                   aNamespace, aName, aValue) {
  873.     function findNodeRecursive(aNode, aAttrName, aValue, aResult) {
  874.       for each (let child in aNode._childNodes) {
  875.         if (child.getAttribute(aAttrName) == aValue)
  876.           aResult.push(child);
  877.  
  878.         findNodeRecursive(child, aAttrName, aValue, aResult);
  879.       }
  880.     }
  881.  
  882.     let result = [];
  883.     let attrName = (aNamespace == null ? aName : aNamespace + ":" + aName);
  884.     findNodeRecursive(this._root, attrName, aValue, result);
  885.     return ArrayConverter.nsIArray(result);
  886.   },
  887.  
  888.   addListener: function ServicePaneService_addListener(aListener) {
  889.     deprecationWarning("sbIServicePaneService.addListener() is deprecated " +
  890.                        "and may be removed in future. Consider using " +
  891.                        "root.addMutationListener() instead.");
  892.     this.root.addMutationListener(new MutationEventTranslator(aListener));
  893.   },
  894.  
  895.   removeListener: function ServicePaneService_removeListener(aListener) {
  896.     deprecationWarning("sbIServicePaneService.addListener() is deprecated " +
  897.                        "and may be removed in future. Consider using " +
  898.                        "root.removeMutationListener() instead.");
  899.  
  900.     // Make a copy of the listeners in case they change while we are in the loop
  901.     let listeners = this.root._listeners.slice();
  902.     for each (let listener in listeners)
  903.       if (listener instanceof MutationEventTranslator && listener.listener == aListener)
  904.         this.root.removeMutationListener(listener);
  905.   },
  906.  
  907.   fillContextMenu: function ServicePaneService_fillContextMenu(
  908.                                           aNode, aContextMenu, aParentWindow) {
  909.     for each (let module in this._modules) {
  910.       try {
  911.         module.fillContextMenu(aNode, aContextMenu, aParentWindow);
  912.       } catch (ex) {
  913.         Components.utils.reportError(ex);
  914.       }
  915.     }
  916.   },
  917.  
  918.   fillNewItemMenu: function ServicePaneService_fillNewItemMenu(
  919.                                           aNode, aContextMenu, aParentWindow) {
  920.     for each (let module in this._modules) {
  921.       try {
  922.         module.fillNewItemMenu(aNode, aContextMenu, aParentWindow);
  923.       } catch (ex) {
  924.         Components.utils.reportError(ex);
  925.       }
  926.     }
  927.   },
  928.  
  929.   onSelectionChanged: function ServicePaneService_onSelectionChanged(
  930.                                           aNode, aContainer, aParentWindow) {
  931.     for each (let module in this._modules) {
  932.       try {
  933.         module.onSelectionChanged(aNode, aContainer, aParentWindow);
  934.       } catch (ex) {
  935.         Components.utils.reportError(ex);
  936.       }
  937.     }
  938.   },
  939.  
  940.   /**
  941.    * Called before a node is renamed by the user.
  942.    * Delegates to the module that owns the given node.
  943.    */
  944.   onBeforeRename: function ServicePaneService_onBeforeRename(aNode) {
  945.     if (!aNode || !aNode.editable)
  946.       return;
  947.  
  948.     // Pass the message on to the node owner
  949.     if (aNode.contractid && aNode.contractid in this._modulesByContractId) {
  950.       let module = this._modulesByContractId[aNode.contractid];
  951.       module.onBeforeRename(aNode);
  952.     }
  953.   },
  954.  
  955.   /**
  956.    * Called when a node is renamed by the user.
  957.    * Delegates to the module that owns the given node.
  958.    */
  959.   onRename: function ServicePaneService_onRename(aNode, aNewName) {
  960.     if (!aNode || !aNode.editable)
  961.       return;
  962.  
  963.     // Pass the message on to the node owner
  964.     if (aNode.contractid && aNode.contractid in this._modulesByContractId) {
  965.       let module = this._modulesByContractId[aNode.contractid];
  966.       module.onRename(aNode, aNewName);
  967.     }
  968.   },
  969.  
  970.   addNode: function ServicePaneService_addNode(aId, aParent, aContainer) {
  971.     deprecationWarning("sbIServicePaneService.addNode() is deprecated and " +
  972.                        "may be removed in future. Consider using " +
  973.                        "sbIServicePaneService.createNode() instead and " +
  974.                        "adding it to the parent yourself.");
  975.  
  976.     if (!aParent)
  977.       throw Ce("You need to supply a parent for addNode().");
  978.  
  979.     if (aId && this.getNode(aId)) {
  980.       // Original implementation returned null for attempts to duplicate a node
  981.       return null;
  982.     }
  983.  
  984.     let node = this.createNode();
  985.     aParent.appendChild(node);
  986.     node.id = (aId !== null ? aId : "_generatedNodeId" + ++this._nodeIndex);
  987.     if (!aContainer)
  988.       node.setAttribute("isContainer", "false");
  989.  
  990.     return node;
  991.   },
  992.  
  993.   removeNode: function ServicePaneService_removeNode(aNode) {
  994.     deprecationWarning("sbIServicePaneService.removeNode() is deprecated and " +
  995.                        "may be removed in future. Consider using " +
  996.                        "sbIServicePaneNode.removeChild() instead.");
  997.  
  998.     if (!aNode)
  999.       throw Ce("You need to supply a node for removeNode().");
  1000.     if (!aNode.parentNode)
  1001.       throw Ce("Cannot remove a node that doesn't have a parent.");
  1002.  
  1003.     aNode.parentNode.removeChild(aNode);
  1004.   },
  1005.  
  1006.   sortNode: function ServicePaneService_sortNode() {
  1007.     deprecationWarning("sbIServicePaneService.sortNode() is deprecated, you no " +
  1008.                        "longer need to call it.");
  1009.   },
  1010.  
  1011.   save: function ServicePaneService_save() {
  1012.     deprecationWarning("sbIServicePaneService.save() is deprecated, you no " +
  1013.                        "longer need to call it.");
  1014.   },
  1015.  
  1016.   canDrop: function ServicePaneService_canDrop(
  1017.                                   aNode, aDragSession, aOrientation, aWindow) {
  1018.     if (!aNode) {
  1019.       return false;
  1020.     }
  1021.  
  1022.     LOG("canDrop(" + aNode.id + ")");
  1023.  
  1024.     // let the module that owns this node handle this
  1025.     if (aNode.contractid && aNode.contractid in this._modulesByContractId) {
  1026.       let module = this._modulesByContractId[aNode.contractid];
  1027.       return module.canDrop(aNode, aDragSession, aOrientation, aWindow);
  1028.     }
  1029.     return false;
  1030.   },
  1031.  
  1032.   onDrop: function ServicePaneService_onDrop(
  1033.                                     aNode, aDragSession, aOrientation, aWindow) {
  1034.     if (!aNode) {
  1035.       return;
  1036.     }
  1037.  
  1038.     LOG("onDrop(" + aNode.id + ")");
  1039.  
  1040.     // or let the module that owns this node handle it
  1041.     if (aNode.contractid && aNode.contractid in this._modulesByContractId) {
  1042.       let module = this._modulesByContractId[aNode.contractid];
  1043.       module.onDrop(aNode, aDragSession, aOrientation, aWindow);
  1044.     }
  1045.   },
  1046.  
  1047.   onDragGesture: function ServicePaneService_onDragGesture(aNode, aDataTransfer) {
  1048.     if (!aNode) {
  1049.       return false;
  1050.     }
  1051.  
  1052.     LOG("onDragGesture(" + aNode.id + ")");
  1053.  
  1054.     if (!aNode.id) {
  1055.       Cu.reportError(new Exception("Cannot drag a service pane node without ID"));
  1056.       return false;
  1057.     }
  1058.  
  1059.     let success = false;
  1060.  
  1061.     // get drag types from the node data
  1062.     if (aNode.dndDragTypes) {
  1063.       let types = aNode.dndDragTypes.split(',');
  1064.       for each (let type in types) {
  1065.         aDataTransfer.setData(type, aNode.id);
  1066.         success = true;
  1067.       }
  1068.     }
  1069.  
  1070.     if (aNode.contractid && aNode.contractid in this._modulesByContractId) {
  1071.       let module = this._modulesByContractId[aNode.contractid];
  1072.       if (module.onDragGesture(aNode, aDataTransfer)) {
  1073.         success = true;
  1074.       }
  1075.     }
  1076.  
  1077.     LOG(" success=" + success);
  1078.  
  1079.     return success;
  1080.   }
  1081. };
  1082.  
  1083. var NSGetModule = XPCOMUtils.generateNSGetModule([ServicePaneService]);
  1084.