home *** CD-ROM | disk | FTP | other *** search
/ PC World 2007 December / PCWorld_2007-12_cd.bin / audio-video / songbird / Songbird_0.3_windows-i686.exe / components / sbFeathersManager.js < prev    next >
Text File  |  2007-10-27  |  39KB  |  1,290 lines

  1. /**
  2. //
  3. // BEGIN SONGBIRD GPL
  4. // 
  5. // This file is part of the Songbird web player.
  6. //
  7. // Copyright(c) 2005-2007 POTI, Inc.
  8. // http://songbirdnest.com
  9. // 
  10. // This file may be licensed under the terms of of the
  11. // GNU General Public License Version 2 (the "GPL").
  12. // 
  13. // Software distributed under the License is distributed 
  14. // on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either 
  15. // express or implied. See the GPL for the specific language 
  16. // governing rights and limitations.
  17. //
  18. // You should have received a copy of the GPL along with this 
  19. // program. If not, go to http://www.gnu.org/licenses/gpl.html
  20. // or write to the Free Software Foundation, Inc., 
  21. // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. // 
  23. // END SONGBIRD GPL
  24. //
  25.  */
  26.  
  27. /**
  28.  * \file sbFeathersManager.js
  29.  * \brief Coordinates the loading of feathers (combination of skin and XUL window layout)
  30.  */ 
  31.  
  32.  
  33. //
  34. // TODO:
  35. //  * add onSwitchCompleted, change onSwitchRequested to allow feedback
  36. //  * Explore skin/layout versioning issues?
  37. // 
  38.  
  39. const CONTRACTID = "@songbirdnest.com/songbird/feathersmanager;1";
  40. const CLASSNAME = "Songbird Feathers Manager Service Interface";
  41. const CID = Components.ID("{99f24350-a67f-11db-befa-0800200c9a66}");
  42. const IID = Components.interfaces.sbIFeathersManager;
  43.  
  44.  
  45. const RDFURI_ADDON_ROOT               = "urn:songbird:addon:root" 
  46. const PREFIX_NS_SONGBIRD              = "http://www.songbirdnest.com/2007/addon-metadata-rdf#";
  47.  
  48. // Fallback layouts/skin, used by previousSkinName and previousLayoutURL
  49. // Changes to the shipped feathers must be reflected here
  50. // and in test_feathersManager.js
  51. const DEFAULT_MAIN_LAYOUT_URL         = "chrome://songbird/content/feathers/basic-layouts/xul/mainplayer.xul";
  52. const DEFAULT_SECONDARY_LAYOUT_URL    = "chrome://songbird/content/feathers/basic-layouts/xul/miniplayer.xul";
  53. const DEFAULT_SKIN_NAME               = "rubberducky/0.2";
  54.  
  55. const WINDOWTYPE_SONGBIRD_PLAYER      = "Songbird:Main";
  56. const WINDOWTYPE_SONGBIRD_CORE        = "Songbird:Core";
  57.  
  58.  
  59.  
  60.  
  61. /**
  62.  * /class ArrayEnumerator
  63.  * /brief Converts a js array into an nsISimpleEnumerator
  64.  */
  65. function ArrayEnumerator(array) 
  66. {
  67.   this.data = array;
  68. }
  69. ArrayEnumerator.prototype = {
  70.  
  71.   index: 0,
  72.   
  73.   getNext: function() {
  74.     return this.data[this.index++];
  75.   },
  76.   
  77.   hasMoreElements: function() {
  78.     if (this.index < this.data.length)
  79.       return true;
  80.     else
  81.       return false;
  82.   },
  83.   
  84.   QueryInterface: function(iid)
  85.   {
  86.     if (!iid.equals(Components.interfaces.nsISimpleEnumerator) &&
  87.         !iid.equals(Components.interfaces.nsISupports))
  88.       throw Components.results.NS_ERROR_NO_INTERFACE;
  89.     return this;
  90.   }
  91. }
  92.  
  93.  
  94.  
  95. /**
  96.  * Debug helper that serializes an RDF datasource to the console
  97.  */
  98. function dumpDS(prefix, ds) {
  99.   var outputStream = {
  100.     data: "",
  101.     close : function(){},
  102.     flush : function(){},
  103.     write : function (buffer,count){
  104.       this.data += buffer;
  105.       return count;
  106.     },
  107.     writeFrom : function (stream,count){},
  108.     isNonBlocking: false
  109.   }
  110.  
  111.   var serializer = Components.classes["@mozilla.org/rdf/xml-serializer;1"]
  112.                            .createInstance(Components.interfaces.nsIRDFXMLSerializer);
  113.   serializer.init(ds);
  114.  
  115.   serializer.QueryInterface(Components.interfaces.nsIRDFXMLSource);
  116.   serializer.Serialize(outputStream);
  117.   
  118.   outputStream.data.split('\n').forEach( function(line) {
  119.     dump(prefix + line + "\n");
  120.   });
  121. }
  122.  
  123.  
  124.  
  125.  
  126. /**
  127.  * sbISkinDescription
  128.  */
  129. function SkinDescription() {};
  130. SkinDescription.prototype = {
  131.   // TODO Expand?
  132.   requiredProperties: [ "name", "internalName" ],
  133.   optionalProperties: [ ],
  134.   
  135.   QueryInterface: function(iid) {
  136.     if (!iid.equals(Components.interfaces.sbISkinDescription)) 
  137.       throw Components.results.NS_ERROR_NO_INTERFACE;
  138.     return this;
  139.   }
  140. };
  141.  
  142. /**
  143.  * sbILayoutDescription
  144.  */
  145. function LayoutDescription() {};
  146. LayoutDescription.prototype = {
  147.   // TODO Expand?
  148.   requiredProperties: [ "name", "url" ],
  149.   optionalProperties: [ ],
  150.   
  151.   QueryInterface: function(iid) {
  152.     if (!iid.equals(Components.interfaces.sbILayoutDescription)) 
  153.       throw Components.results.NS_ERROR_NO_INTERFACE;
  154.     return this;
  155.   }
  156. };
  157.  
  158.  
  159. /**
  160.  * Static function that verifies the contents of the given description
  161.  *
  162.  * Example:
  163.  *   try {
  164.  *     LayoutDescription.verify(layout);
  165.  *   } catch (e) {
  166.  *     reportError(e);
  167.  *   }
  168.  *
  169.  */
  170. LayoutDescription.verify = SkinDescription.verify = function( description ) 
  171. {
  172.   for (var i = 0; i < this.prototype.requiredProperties.length; i++) {
  173.     var property = this.prototype.requiredProperties[i];
  174.     if (! (typeof(description[property]) == 'string'
  175.              && description[property].length > 0)) 
  176.     {
  177.       throw("Invalid description. '" + property + "' is a required property.");
  178.     }
  179.   }
  180. }
  181.  
  182.  
  183.  
  184.  
  185.  
  186.  
  187. /**
  188.  * /class AddonMetadataReader
  189.  * Responsible for reading addon metadata and performing 
  190.  * registration with FeathersManager
  191.  */
  192. function AddonMetadataReader() {
  193.   //debug("AddonMetadataReader: ctor\n");
  194.   this._RDF = Components.classes["@mozilla.org/rdf/rdf-service;1"]
  195.                         .getService(Components.interfaces.nsIRDFService);
  196.   this._datasource = this._RDF.GetDataSourceBlocking("rdf:addon-metadata");
  197.   this._feathersManager = Components.classes[CONTRACTID].getService(IID);
  198.     
  199.   this._resources = {
  200.     root: this._RDF.GetResource(RDFURI_ADDON_ROOT),
  201.     // Helper to convert a string array into 
  202.     // RDF resources in this object
  203.     addSongbirdResources: function(list){
  204.       for (var i = 0; i < list.length; i++) {
  205.         this[list[i]] = this._RDF.GetResource(PREFIX_NS_SONGBIRD + list[i]);
  206.       }
  207.     },
  208.     _RDF: this._RDF
  209.   };
  210.   
  211.   // Make RDF resources for all properties expected
  212.   // in the feathers portion of an install.rdf      
  213.   this._resources.addSongbirdResources(SkinDescription.prototype.requiredProperties);
  214.   this._resources.addSongbirdResources(SkinDescription.prototype.optionalProperties);
  215.   this._resources.addSongbirdResources(LayoutDescription.prototype.requiredProperties);
  216.   this._resources.addSongbirdResources(LayoutDescription.prototype.optionalProperties);
  217.   this._resources.addSongbirdResources(
  218.      [ "compatibleSkin", 
  219.        "compatibleLayout", 
  220.        "showChrome",
  221.        "feathers",
  222.        "skin",
  223.        "layout",
  224.        "layoutURL" ] );
  225. }
  226. AddonMetadataReader.prototype = {
  227.  
  228.   _RDF: null,
  229.   
  230.   _feathersManager: null,
  231.  
  232.   // Addon metadata rdf datasource
  233.   _datasource: null,
  234.  
  235.   // Hash of addon metadata RDF resources
  236.   _resources: null,
  237.  
  238.   /**
  239.    * Populate FeathersManager using addon metadata
  240.    */
  241.   loadFeathers: function loadFeathers() {
  242.     //debug("AddonMetadataReader: loadFeathers\n");
  243.     
  244.     // Get addon list
  245.     var containerUtils = Components.classes["@mozilla.org/rdf/container-utils;1"]
  246.                            .getService(Components.interfaces.nsIRDFContainerUtils);
  247.     var container = containerUtils.MakeSeq(this._datasource, this._resources.root);
  248.     var addons = container.GetElements();
  249.     
  250.     // Search all addons for feathers metadata
  251.     while (addons.hasMoreElements()) {
  252.       var addon = addons.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
  253.       //debug("AddonMetadataReader.loadFeathers: - processing " + addon.Value + "\n");
  254.       try {
  255.       
  256.         if (this._datasource.hasArcOut(addon, this._resources.feathers)) {
  257.           var feathersTarget = this._datasource.GetTarget(addon, this._resources.feathers, true)
  258.                                    .QueryInterface(Components.interfaces.nsIRDFResource);
  259.           
  260.           // Process all skin metadata
  261.           var items = this._datasource.GetTargets(feathersTarget, this._resources.skin, true)
  262.           while (items.hasMoreElements()) {
  263.             var item = items.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
  264.             this._processSkin(addon, item);
  265.           }
  266.  
  267.           // Process all layout metadata
  268.           var items = this._datasource.GetTargets(feathersTarget, this._resources.layout, true)
  269.           while (items.hasMoreElements()) {
  270.             var item = items.getNext().QueryInterface(Components.interfaces.nsIRDFResource);
  271.             this._processLayout(addon, item);
  272.           }
  273.         }
  274.         
  275.       } catch (e) {
  276.         this._reportErrors("", [  "An error occurred while processing " +
  277.                     "extension " + addon.Value + ".  Exception: " + e  ]);
  278.       }
  279.     }
  280.   },
  281.   
  282.   
  283.   /**
  284.    * Extract skin metadata
  285.    */
  286.   _processSkin: function _processSkin(addon, skin) {
  287.     var description = new SkinDescription();
  288.     
  289.     // Array of error messages
  290.     var errorList = [];
  291.     
  292.     // Fill the description object
  293.     this._populateDescription(skin, description, errorList);
  294.     
  295.     try {
  296.       SkinDescription.verify(description);
  297.     } catch (e) {
  298.       errorList.push(e.toString());
  299.     }
  300.     
  301.     // If errors were encountered, then do not submit 
  302.     // to the Feathers Manager
  303.     if (errorList.length > 0) {
  304.       this._reportErrors(
  305.           "Ignoring skin addon in the install.rdf of extension " +
  306.           addon.Value + ". Message: ", errorList);
  307.       return;
  308.     }
  309.     
  310.     // Submit description
  311.     this._feathersManager.registerSkin(description);
  312.     //debug("AddonMetadataReader: registered skin " + description.internalName
  313.     //        + " from addon " + addon.Value + " \n");
  314.     
  315.     // Get compatibility information
  316.     var identifiers, showChromeInstructions;
  317.     [identifiers, showChromeInstructions] =
  318.         this._getCompatibility(addon, skin, "compatibleLayout", "layoutURL", errorList);
  319.  
  320.     // Report errors
  321.     if (errorList.length > 0) {
  322.       this._reportErrors(
  323.           "Error finding compatibility information for skin " +
  324.           description.name + " in the install.rdf " +
  325.           "of extension " + addon.Value + ". Message: ", errorList);
  326.     }
  327.      
  328.     // Assert compatibility    
  329.     for (var i = 0; i < identifiers.length; i++) {
  330.       this._feathersManager.assertCompatibility(
  331.                identifiers[i],
  332.                description.internalName, 
  333.                showChromeInstructions[i]);
  334.     }
  335.   },
  336.  
  337.  
  338.   /**
  339.    * Extract layout metadata
  340.    */
  341.   _processLayout: function _processLayout(addon, layout) {
  342.     var description = new LayoutDescription();
  343.     
  344.     // Array of error messages
  345.     var errorList = [];
  346.     
  347.     // Fill the description object
  348.     this._populateDescription(layout, description, errorList);
  349.     
  350.     try {
  351.       LayoutDescription.verify(description);
  352.     } catch (e) {
  353.       errorList.push(e.toString());
  354.     }
  355.     
  356.     // If errors were encountered, then do not submit 
  357.     // to the Feathers Manager
  358.     if (errorList.length > 0) {
  359.       this._reportErrors(
  360.           "Ignoring layout addon in the install.rdf of extension " +
  361.           addon.Value + ". Message: ", errorList);
  362.       return;
  363.     }
  364.     
  365.     // Submit description
  366.     this._feathersManager.registerLayout(description);
  367.     //debug("AddonMetadataReader: registered layout " + description.name +
  368.     //     " from addon " + addon.Value + "\n");    
  369.     
  370.     // Get compatibility information
  371.     var identifiers, showChromeInstructions;
  372.     [identifiers, showChromeInstructions] =
  373.         this._getCompatibility(addon, layout, "compatibleSkin", "internalName", errorList);
  374.  
  375.     // Report errors
  376.     if (errorList.length > 0) {
  377.       this._reportErrors(
  378.           "Error finding compatibility information for layout " +
  379.           description.name + " in the install.rdf " +
  380.           "of extension " + addon.Value + ". Message: ", errorList);
  381.     }
  382.     
  383.     // Assert compatibility      
  384.     for (var i = 0; i < identifiers.length; i++) {
  385.       this._feathersManager.assertCompatibility(
  386.                description.url,
  387.                identifiers[i], 
  388.                showChromeInstructions[i]);
  389.     }
  390.   
  391.   },
  392.  
  393.  
  394.  
  395.   /**
  396.    * \brief Populate a description object by looking up requiredProperties and
  397.    *        optionalProperties in a the given rdf source.
  398.    *
  399.    * \param source RDF resource from which to obtain property values
  400.    * \param description Object with requiredProperties and optionalProperties arrays
  401.    * \param errorList An array to which error messages should be added
  402.    */
  403.   _populateDescription: function _populateDescription(source, description, errorList) {
  404.  
  405.     for (var i = 0; i < description.requiredProperties.length; i++) {
  406.       this._requireProperty(source, description, 
  407.                 description.requiredProperties[i], errorList);
  408.     }
  409.     for (var i = 0; i < description.optionalProperties.length; i++) {
  410.       this._copyProperty(source, description, 
  411.                 description.optionalProperties[i], errorList);
  412.     }
  413.   },
  414.   
  415.   
  416.   /**
  417.    * \brief Attempts to copy a property from an RDFResource into a
  418.    *        container object and reports an error on failure.
  419.    *
  420.    * \param source RDF resource from which to obtain the value
  421.    * \param description Container object to receive the value
  422.    * \param property String property to be copied
  423.    * \param errorList An array to which error messages should be added
  424.    */
  425.   _requireProperty: function _requireProperty(source, description, property, errorList) {
  426.     this._copyProperty(source, description, property);
  427.     if (description[property] == undefined) 
  428.     {
  429.       errorList.push(
  430.         property + " is a required property."
  431.       );
  432.     }
  433.   },
  434.  
  435.   /**
  436.    * \brief Copies a property from an RDFResource into a container object.
  437.    *
  438.    * \param source RDF resource from which to obtain the value
  439.    * \param description Container object to receive the value
  440.    * \param property String property to be copied
  441.    */
  442.   _copyProperty: function _copyProperty(source, description, property) {
  443.     description[property] = this._getProperty(source, property);
  444.   },
  445.  
  446.  
  447.   /**
  448.    * \brief Copies a property from an RDFResource into a container object.
  449.    *
  450.    * \param source RDF resource from which to obtain the value
  451.    * \param description Container object to receive the value
  452.    * \param property String property to be copied
  453.    */  
  454.   _getProperty: function _getProperty(source, property) {
  455.     //debug("AddonMetadataReader._getProperty " + source.Value + " " + property + "\n");
  456.     var target = this._datasource.GetTarget(source, this._resources[property], true);
  457.     if ( target instanceof Components.interfaces.nsIRDFInt
  458.          || target instanceof Components.interfaces.nsIRDFLiteral 
  459.          || target instanceof Components.interfaces.nsIRDFResource )
  460.     {
  461.       return target.Value;
  462.     }
  463.     return undefined;
  464.   },
  465.  
  466.  
  467.   /**
  468.    * \brief Extracts compatibility information for a feathers item into 
  469.    *        an array of identifiers and matching array of booleans 
  470.    *        indicating whether chrome should be shown
  471.    *
  472.    * \param addon RDF resource from the addon description
  473.    * \param source RDF resource for the feathers item
  474.    * \param resourceName Container object to receive the value
  475.    * \param idProperty String property to be copied
  476.    * \param errorList An array to which error messages should be added
  477.    * \return [array of identifiers, array of showChrome bools]
  478.    */
  479.   _getCompatibility: function _getCompatibility(addon, source, resourceName, idProperty, errorList) {
  480.     // List of internalName or layoutURL identifiers
  481.     var identifiers = [];
  482.     // Matching list of showChrome hints
  483.     var showChromeInstructions = [];
  484.     
  485.     // Look at all compatibility rules of type resourceName
  486.     var targets = this._datasource.GetTargets(source, this._resources[resourceName], true);
  487.     while (targets.hasMoreElements()) {
  488.       var target = targets.getNext();
  489.       
  490.       // Target must be a resource / contain a description
  491.       if (! (target instanceof Components.interfaces.nsIRDFResource)) {
  492.         
  493.         errorList.push("The install.rdf for " + addon.Value 
  494.               + " is providing an incomplete " + resourceName 
  495.               + " section.");
  496.               
  497.         // Skip this section
  498.         continue;        
  499.       }
  500.       target = target.QueryInterface(Components.interfaces.nsIRDFResource);
  501.       
  502.       // Get the identifier for the compatible feather item
  503.       var id = this._getProperty(target, idProperty);
  504.       
  505.       // Must provide an identifier
  506.       if (id == undefined) {
  507.         errorList.push("Extension " + addon.Value 
  508.               + " must provide " + idProperty 
  509.               + " in " + resourceName + " descriptions");
  510.               
  511.         // Skip this rule since it is incomplete
  512.         continue;
  513.       }
  514.       
  515.       // Should chrome be shown with this id?
  516.       var showChrome = this._getProperty(target, "showChrome") == "true";
  517.       
  518.       // Store compatibility rule
  519.       identifiers.push(id);
  520.       showChromeInstructions.push(showChrome);
  521.     }
  522.     
  523.     //debug("AddonMetadataReader: found " + identifiers.length + " " 
  524.     //      + resourceName + " rule(s) for addon " 
  525.     //      + addon.Value + "\n");
  526.  
  527.     return [identifiers, showChromeInstructions];
  528.   },
  529.   
  530.   
  531.   /**
  532.    * \brief Dump a list of errors to the console and jsconsole
  533.    *
  534.    * \param contextMessage Additional prefix to use before every line
  535.    * \param errorList Array of error messages
  536.    */
  537.   _reportErrors: function _reportErrors(contextMessage, errorList) {
  538.     var consoleService = Components.classes["@mozilla.org/consoleservice;1"].
  539.          getService(Components.interfaces.nsIConsoleService);
  540.     for (var i = 0; i  < errorList.length; i++) {
  541.       consoleService.logStringMessage("Feathers Metadata Reader: " 
  542.                                        + contextMessage + errorList[i]);
  543.       dump("FeathersMetadataReader: " + contextMessage + errorList[i] + "\n");
  544.     }
  545.   }
  546. }
  547.  
  548.  
  549.  
  550.  
  551.  
  552.  
  553.  
  554.  
  555.  
  556.  
  557.  
  558. /**
  559.  * /class FeathersManager
  560.  * /brief Coordinates the loading of feathers
  561.  *
  562.  * Acts as a registry for skins and layout (known as feathers)
  563.  * and manages compatibility and selection.
  564.  *
  565.  * \sa sbIFeathersManager
  566.  */
  567. function FeathersManager() {
  568.  
  569.   var os      = Components.classes["@mozilla.org/observer-service;1"]
  570.                       .getService(Components.interfaces.nsIObserverService);
  571.   // We need to unhook things on shutdown
  572.   os.addObserver(this, "quit-application", false);
  573.   
  574.   this._skins = {};
  575.   this._layouts = {};
  576.   this._mappings = {};
  577.   this._listeners = [];
  578. };
  579. FeathersManager.prototype = {
  580.   constructor: FeathersManager,
  581.   
  582.   _layoutDataRemote: null,
  583.   _skinDataRemote: null,
  584.  
  585.   _previousLayoutDataRemote: null,
  586.   _previousSkinDataRemote: null,
  587.   
  588.   _showChromeDataRemote: null,
  589.  
  590.   _switching: false,
  591.  
  592.   
  593.   // Hash of skin descriptions keyed by internalName (e.g. classic/1.0)
  594.   _skins: null,
  595.   
  596.   // Hash of layout descriptions keyed by URL
  597.   _layouts: null,
  598.   
  599.   
  600.   // Hash of layout URL to hash of compatible skin internalNames, pointing to 
  601.   // showChrome value.  
  602.   //
  603.   // eg
  604.   // {  
  605.   //     mainwin.xul: {
  606.   //       blueskin: true,
  607.   //       redskin: false,
  608.   //     }
  609.   // }
  610.   //
  611.   // Compatibility is determined by whether or not a internalName
  612.   // key is *defined* in the hash, not the actual value it points to.
  613.   // In the above example false means "don't show chrome"
  614.   _mappings: null,
  615.   
  616.   
  617.   // Array of sbIFeathersChangeListeners
  618.   _listeners: null,
  619.  
  620.   _layoutCount: 0,
  621.   _skinCount: 0,
  622.   
  623.   _initialized: false,
  624.   
  625.  
  626.   /**
  627.    * Initializes dataremotes and triggers the AddonMetadataReader
  628.    * to explore installed extensions and register any feathers.
  629.    *
  630.    * Note that this function is not run until a get method is called
  631.    * on the feathers manager.  This is to defer loading the metadata
  632.    * as long as possible and avoid impacting startup time.
  633.    * 
  634.    */
  635.   _init: function init() {
  636.   
  637.     // If already initialized do nothing
  638.     if (this._initialized) {
  639.       return;
  640.     }
  641.     
  642.     // Make dataremotes to persist feathers settings
  643.     var createDataRemote =  new Components.Constructor(
  644.                   "@songbirdnest.com/Songbird/DataRemote;1",
  645.                   Components.interfaces.sbIDataRemote, "init");
  646.  
  647.     this._layoutDataRemote = createDataRemote("feathers.selectedLayout", null);
  648.     this._skinDataRemote = createDataRemote("selectedSkin", "general.skins.");
  649.     
  650.     this._previousLayoutDataRemote = createDataRemote("feathers.previousLayout", null);
  651.     this._previousSkinDataRemote = createDataRemote("feathers.previousSkin", null);
  652.     
  653.     // TODO: Rename accessibility.enabled?
  654.     this._showChromeDataRemote = createDataRemote("accessibility.enabled", null);
  655.     
  656.     // Load the feathers metadata
  657.     var metadataReader = new AddonMetadataReader();
  658.     metadataReader.loadFeathers();
  659.     
  660.     // If no layout url has been specified, set to default
  661.     if (this._layoutDataRemote.stringValue == "") {
  662.       this._layoutDataRemote.stringValue = DEFAULT_MAIN_LAYOUT_URL;
  663.     }
  664.     
  665.     this._initialized = true;
  666.   },
  667.   
  668.   /**
  669.    * Called on xpcom-shutdown
  670.    */
  671.   _deinit: function deinit() {
  672.     this._skins = null;
  673.     this._layouts = null;
  674.     this._mappings = null;
  675.     this._listeners = null;
  676.     this._layoutDataRemote = null;
  677.     this._skinDataRemote = null;
  678.     this._previousLayoutDataRemote = null;
  679.     this._previousSkinDataRemote = null;
  680.     this._showChromeDataRemote = null;
  681.   },
  682.     
  683.   /**
  684.    * \sa sbIFeathersManager
  685.    */
  686.   get currentSkinName() {
  687.     this._init();
  688.     return this._skinDataRemote.stringValue;
  689.   },
  690.   
  691.   
  692.   /**
  693.    * \sa sbIFeathersManager
  694.    */  
  695.   get currentLayoutURL() {
  696.     this._init();
  697.     return this._layoutDataRemote.stringValue;
  698.   },
  699.   
  700.   
  701.   /**
  702.    * \sa sbIFeathersManager
  703.    */  
  704.   get previousSkinName() {
  705.     this._init();
  706.     
  707.     // Test to make sure the previous skin exists
  708.     var skin = this.getSkinDescription(this._previousSkinDataRemote.stringValue);
  709.     
  710.     // If the skin exists, then return the skin name
  711.     if (skin) {
  712.       return skin.internalName;
  713.     }
  714.     
  715.     // Otherwise, return the default skin
  716.     return DEFAULT_SKIN_NAME;
  717.   },
  718.   
  719.   
  720.   /**
  721.    * \sa sbIFeathersManager
  722.    */  
  723.   get previousLayoutURL() {
  724.     this._init();
  725.     
  726.     // Test to make sure the previous layout exists
  727.     var layout = this.getLayoutDescription(this._previousLayoutDataRemote.stringValue);
  728.     
  729.     // If the layout exists, then return the url/identifier
  730.     if (layout) {
  731.       return layout.url;
  732.     }
  733.     
  734.     // Otherwise, return the default 
  735.     
  736.     // Use the main default unless it is currently
  737.     // active. This way if the user reverts for the
  738.     // first time they will end up in the miniplayer.
  739.     var layoutURL = DEFAULT_MAIN_LAYOUT_URL;
  740.     if (this.currentLayoutURL == layoutURL) {
  741.       layoutURL = DEFAULT_SECONDARY_LAYOUT_URL;
  742.     }
  743.     
  744.     return layoutURL;    
  745.   },
  746.  
  747.  
  748.   /**
  749.    * \sa sbIFeathersManager
  750.    */ 
  751.   get skinCount() {
  752.     this._init();
  753.     return this._skinCount;
  754.   },
  755.   
  756.   /**
  757.    * \sa sbIFeathersManager
  758.    */  
  759.   get layoutCount() {
  760.     this._init();
  761.     return this._layoutCount;
  762.   },
  763.  
  764.  
  765.   /**
  766.    * \sa sbIFeathersManager
  767.    */
  768.   getSkinDescriptions: function getSkinDescriptions() {
  769.     this._init();      
  770.     // Copy all the descriptions into an array, and then return an enumerator
  771.     return new ArrayEnumerator( [this._skins[key] for (key in this._skins)] );
  772.   },
  773.  
  774.   /**
  775.    * \sa sbIFeathersManager
  776.    */
  777.   getLayoutDescriptions: function getLayoutDescriptions() {
  778.     this._init();        
  779.     // Copy all the descriptions into an array, and then return an enumerator
  780.     return new ArrayEnumerator( [this._layouts[key] for (key in this._layouts)] );
  781.   },
  782.   
  783.   
  784.   /**
  785.    * \sa sbIFeathersManager
  786.    */  
  787.   registerSkin: function registerSkin(skinDesc) {
  788.     SkinDescription.verify(skinDesc);
  789.     
  790.     if (this._skins[skinDesc.internalName] == null) {
  791.       this._skinCount++;
  792.     }
  793.     this._skins[skinDesc.internalName] = skinDesc;
  794.     
  795.     // Notify observers
  796.     this._onUpdate();
  797.   },
  798.  
  799.   /**
  800.    * \sa sbIFeathersManager
  801.    */
  802.   unregisterSkin: function unregisterSkin(skinDesc) {
  803.     if (this._skins[skinDesc.internalName]) {
  804.       delete this._skins[skinDesc.internalName];
  805.       this._skinCount--;
  806.       
  807.       // Notify observers
  808.       this._onUpdate();
  809.     }
  810.   },
  811.  
  812.   /**
  813.    * \sa sbIFeathersManager
  814.    */
  815.   getSkinDescription: function getSkinDescription(internalName) {
  816.     this._init();
  817.     return this._skins[internalName];
  818.   },
  819.   
  820.   
  821.   /**
  822.    * \sa sbIFeathersManager
  823.    */  
  824.   registerLayout: function registerLayout(layoutDesc) {
  825.     LayoutDescription.verify(layoutDesc);
  826.    
  827.     if (this._layouts[layoutDesc.url] == null) {
  828.       this._layoutCount++;
  829.     }
  830.     this._layouts[layoutDesc.url] = layoutDesc;
  831.     
  832.     // Notify observers
  833.     this._onUpdate();
  834.   },
  835.  
  836.   /**
  837.    * \sa sbIFeathersManager
  838.    */
  839.   unregisterLayout: function unregisterLayout(layoutDesc) {
  840.     if (this._layouts[layoutDesc.url]) {
  841.       delete this._layouts[layoutDesc.url];
  842.       this._layoutCount--;
  843.       
  844.       // Notify observers
  845.       this._onUpdate();  
  846.     }  
  847.   },
  848.     
  849.   /**
  850.    * \sa sbIFeathersManager
  851.    */    
  852.   getLayoutDescription: function getLayoutDescription(url) {
  853.     this._init();
  854.     return this._layouts[url];
  855.   }, 
  856.  
  857.   
  858.   /**
  859.    * \sa sbIFeathersManager
  860.    */
  861.   assertCompatibility: 
  862.   function assertCompatibility(layoutURL, internalName, showChrome) {
  863.     if (! (typeof(layoutURL) == "string" && typeof(internalName) == 'string')) {
  864.       throw Components.results.NS_ERROR_INVALID_ARG;
  865.     }
  866.     if (this._mappings[layoutURL] == null) {
  867.       this._mappings[layoutURL] = {};
  868.     }
  869.     this._mappings[layoutURL][internalName] = showChrome;
  870.     
  871.     // Notify observers
  872.     this._onUpdate();
  873.   },
  874.  
  875.   /**
  876.    * \sa sbIFeathersManager
  877.    */
  878.   unassertCompatibility: function unassertCompatibility(layoutURL, internalName) {
  879.     if (this._mappings[layoutURL]) {
  880.       delete this._mappings[layoutURL][internalName];
  881.       
  882.       // Notify observers
  883.       this._onUpdate();
  884.     }  
  885.   },
  886.   
  887.  
  888.   /**
  889.    * \sa sbIFeathersManager
  890.    */
  891.   isChromeEnabled: function isChromeEnabled(layoutURL, internalName) {
  892.     this._init();
  893.     
  894.     // TEMP fix for the Mac to enable the titlebar on the main window.
  895.     // See Bug 4363
  896.     var sysInfo = Components.classes["@mozilla.org/system-info;1"]
  897.                             .getService(Components.interfaces.nsIPropertyBag2);
  898.     var platform = sysInfo.getProperty("name");
  899.     
  900.     if (this._mappings[layoutURL]) {
  901.       return this._mappings[layoutURL][internalName] == true;
  902.     }
  903.    
  904.     return false; 
  905.   },
  906.  
  907.  
  908.   /**
  909.    * \sa sbIFeathersManager
  910.    */
  911.   getSkinsForLayout: function getSkinsForLayout(layoutURL) {
  912.     this._init();
  913.  
  914.     var skins = [];
  915.     
  916.     // Find skin descriptions that are compatible with the given layout.
  917.     if (this._mappings[layoutURL]) {
  918.       for (internalName in this._mappings[layoutURL]) {
  919.         var desc = this.getSkinDescription(internalName);
  920.         if (desc) {
  921.           skins.push(desc);
  922.         }
  923.       }
  924.     }   
  925.     return new ArrayEnumerator( skins );
  926.   },
  927.   
  928.   
  929.   /**
  930.    * \sa sbIFeathersManager
  931.    */
  932.   getLayoutsForSkin: function getLayoutsForSkin(internalName) {
  933.     this._init();
  934.  
  935.     var layouts = [];
  936.     
  937.     // Find skin descriptions that are compatible with the given layout.
  938.     for (var layout in this._mappings) {
  939.       if (internalName in this._mappings[layout]) {
  940.         var desc = this.getLayoutDescription(layout);
  941.         if (desc) {
  942.           layouts.push(desc);
  943.         }      
  944.       }
  945.     }
  946.       
  947.     return new ArrayEnumerator( layouts );
  948.   },
  949.  
  950.  
  951.   /**
  952.    * \sa sbIFeathersManager
  953.    */
  954.   switchFeathers: function switchFeathers(layoutURL, internalName) {
  955.     // don't allow this call if we're already switching
  956.     if (this._switching) {
  957.       return;
  958.     }
  959.  
  960.     this._init();
  961.  
  962.     layoutDescription = this.getLayoutDescription(layoutURL);
  963.     skinDescription = this.getSkinDescription(internalName);
  964.     
  965.     // Make sure we know about the requested skin and layout
  966.     if (layoutDescription == null || skinDescription == null) {
  967.       throw new Components.Exception("Unknown layout/skin passed to switchFeathers");
  968.     }
  969.     
  970.     // Check compatibility.
  971.     // True/false refer to the showChrome value, so check for undefined
  972.     // to determine compatibility.
  973.     if (this._mappings[layoutURL][internalName] === undefined) {
  974.       throw new Components.Exception("Skin [" + internalName + "] and Layout [" + layoutURL +
  975.             " are not compatible");
  976.     } 
  977.     
  978.     // Notify that a select is about to occur
  979.     this._onSelect(layoutDescription, skinDescription);
  980.     
  981.     // Remember the current feathers so that we can revert later if needed
  982.     this._previousLayoutDataRemote.stringValue = this.currentLayoutURL;
  983.     this._previousSkinDataRemote.stringValue = this.currentSkinName;
  984.  
  985.     // close the player window *before* changing the skin
  986.     // otherwise Gecko tries to load an image that will go away right after and crashes
  987.     // (songbird bug 3965)
  988.     this._closePlayerWindow();
  989.     
  990.     var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
  991.     var callback = new FeathersManager_switchFeathers_callback(this, layoutURL, internalName);
  992.     this._switching = true;
  993.     timer.initWithCallback(callback, 0, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
  994.   },
  995.   
  996.   
  997.   /**
  998.    * \sa sbIFeathersManager
  999.    * Relaunch the main window
  1000.    */
  1001.   openPlayerWindow: function openPlayerWindow() {
  1002.     
  1003.     // First, check to make sure the current
  1004.     // feathers are valid
  1005.     var layoutDescription = this.getLayoutDescription(this.currentLayoutURL);
  1006.     var skinDescription = this.getSkinDescription(this.currentSkinName);
  1007.     if (layoutDescription == null || skinDescription == null) {
  1008.       // The current feathers are invalid. Switch to the defaults.
  1009.       this.switchFeathers(DEFAULT_MAIN_LAYOUT_URL, DEFAULT_SKIN_NAME);
  1010.       return;
  1011.     }
  1012.     
  1013.     
  1014.     var windowMediator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
  1015.                                    .getService(Components.interfaces.nsIWindowMediator);
  1016.  
  1017.     // The core window (plugin host) is the only window which cannot be shut down
  1018.     var coreWindow = windowMediator.getMostRecentWindow(WINDOWTYPE_SONGBIRD_CORE);  
  1019.  
  1020.     // If no core window exists, then we are probably in test mode.
  1021.     // Therefore do nothing.
  1022.     if (coreWindow == null) {
  1023.       dump("FeathersManager.openPlayerWindow: unable to find window of type Songbird:Core. Test mode?\n");
  1024.       return;
  1025.     }
  1026.  
  1027.     // Determine window features.  If chrome is enabled, make resizable.
  1028.     // Otherwise remove the titlebar.
  1029.     var chromeFeatures = "chrome,modal=no,toolbar=yes,popup=no";    
  1030.     var showChrome = this.isChromeEnabled(this.currentLayoutURL, this.currentSkinName);
  1031.     if (showChrome) {
  1032.        chromeFeatures += ",resizable=yes";
  1033.     } else {
  1034.        chromeFeatures += ",titlebar=no";
  1035.     }
  1036.     
  1037.     // Set the global chrome (window border and title) flag
  1038.     this._setChromeEnabled(showChrome);
  1039.     
  1040.     // Open the new player window
  1041.     var newMainWin = coreWindow.open(this.currentLayoutURL, "", chromeFeatures);
  1042.     newMainWin.focus();
  1043.   },
  1044.   
  1045.   
  1046.   /**
  1047.    * \sa sbIFeathersManager
  1048.    */  
  1049.   addListener: function addListener(listener) {
  1050.     if (! (listener instanceof Components.interfaces.sbIFeathersManagerListener))
  1051.     {
  1052.       throw Components.results.NS_ERROR_INVALID_ARG;
  1053.     }
  1054.     this._listeners.push(listener);
  1055.   },
  1056.   
  1057.   /**
  1058.    * \sa sbIFeathersManager
  1059.    */  
  1060.   removeListener: function removeListener(listener) {
  1061.     var index = this._listeners.indexOf(listener);
  1062.     if (index > -1) {
  1063.       this._listeners.splice(index,1);
  1064.     }
  1065.   },
  1066.  
  1067.  
  1068.   /**
  1069.    * Close all player windows (except the plugin host)
  1070.    */
  1071.   _closePlayerWindow: function _closePlayerWindow() {
  1072.     var windowMediator = Components.classes["@mozilla.org/appshell/window-mediator;1"]
  1073.                                    .getService(Components.interfaces.nsIWindowMediator);
  1074.  
  1075.     // The core window (plugin host) is the only window which cannot be shut down
  1076.     var coreWindow = windowMediator.getMostRecentWindow(WINDOWTYPE_SONGBIRD_CORE);  
  1077.     
  1078.     // If no core window exists, then we are probably in test mode.
  1079.     // Therefore do nothing.
  1080.     if (coreWindow == null) {
  1081.       dump("FeathersManager._closePlayerWindow: unable to find window of type Songbird:Core. Test mode?\n");
  1082.       return;
  1083.     }
  1084.  
  1085.     // Close all open windows other than the core, dominspector, and venkman.
  1086.     // This is needed in order to reset window chrome settings.
  1087.     var playerWindows = windowMediator.getEnumerator(null);
  1088.     while (playerWindows.hasMoreElements()) {
  1089.       var window = playerWindows.getNext();
  1090.       
  1091.       if (window != coreWindow) {
  1092.         
  1093.         // Don't close domi or other debug windows... that's just annoying
  1094.         var isDebugWindow = false;
  1095.         try {    
  1096.           var windowElement = window.document.documentElement;
  1097.           var windowID = windowElement.getAttribute("id");
  1098.           if (windowID == "JSConsoleWindow" || 
  1099.               windowID == "winInspectorMain" || 
  1100.               windowID == "venkman-window") 
  1101.           {
  1102.             isDebugWindow = true;
  1103.           }
  1104.         } catch (e) {}
  1105.         
  1106.         if (!isDebugWindow) {
  1107.           // Ask nicely.  The window should be able to cancel the onunload if
  1108.           // it chooses.
  1109.           window.close();
  1110.         }
  1111.       }
  1112.     }
  1113.   },
  1114.  
  1115.       
  1116.   /**
  1117.    * Indicates to the rest of the system whether or not to 
  1118.    * enable titlebars when opening windows
  1119.    */
  1120.   _setChromeEnabled: function _setChromeEnabled(enabled) {
  1121.  
  1122.     // Set the global chrome (window border and title) flag
  1123.     this._showChromeDataRemote.boolValue = enabled;
  1124.  
  1125.     var prefs = Components.classes["@mozilla.org/preferences-service;1"]
  1126.                           .getService(Components.interfaces.nsIPrefBranch);
  1127.  
  1128.     // Set the flags used to open the core window on startup.
  1129.     // Do a replacement in order to preserve whatever other features 
  1130.     // were specified.
  1131.     try {
  1132.       var titlebarRegEx = /(titlebar=)(no|yes)/;
  1133.       var replacement = (enabled) ? "$1yes" : "$1no";
  1134.       var defaultChromeFeatures = prefs.getCharPref("toolkit.defaultChromeFeatures");
  1135.       prefs.setCharPref("toolkit.defaultChromeFeatures",
  1136.               defaultChromeFeatures.replace(titlebarRegEx, replacement));
  1137.     } catch (e) {
  1138.       dump("\nFeathersManager._setChromeEnabled: Error setting defaultChromeFeatures pref! " +
  1139.             e.toString + "\n");
  1140.     }
  1141.   },      
  1142.       
  1143.  
  1144.   /**
  1145.    * Broadcasts an update event to all registered listeners
  1146.    */
  1147.   _onUpdate: function onUpdate() {
  1148.     this._listeners.forEach( function (listener) {
  1149.       listener.onFeathersUpdate();
  1150.     });
  1151.   },
  1152.  
  1153.  
  1154.   /**
  1155.    * Broadcasts an select (feathers switch) event to all registered listeners
  1156.    */
  1157.   _onSelect: function onSelect(layoutDesc, skinDesc) {
  1158.     // Verify args
  1159.     layoutDesc = layoutDesc.QueryInterface(Components.interfaces.sbILayoutDescription);
  1160.     skinDesc = skinDesc.QueryInterface(Components.interfaces.sbISkinDescription);
  1161.     
  1162.     // Broadcast notification
  1163.     this._listeners.forEach( function (listener) {
  1164.       listener.onFeathersSelectRequest(layoutDesc, skinDesc);
  1165.     });
  1166.   },
  1167.  
  1168.   _flushXULPrototypeCache: function flushXULPrototypeCache() {
  1169.     var prefs = Components.classes["@mozilla.org/preferences-service;1"]
  1170.                           .getService(Components.interfaces.nsIPrefBranch);
  1171.     var disabled = false;
  1172.     var userPref = false;
  1173.  
  1174.     try {
  1175.       disabled = prefs.getBoolPref("nglayout.debug.disable_xul_cache");
  1176.       userPref = true;
  1177.     }
  1178.     catch(e) {
  1179.     }
  1180.  
  1181.     if (!disabled) {
  1182.       prefs.setBoolPref("nglayout.debug.disable_xul_cache", true);
  1183.       prefs.setBoolPref("nglayout.debug.disable_xul_cache", false);
  1184.       if (!userPref) {
  1185.         prefs.clearUserPref("nglayout.debug.disable_xul_cache");
  1186.       }
  1187.     }
  1188.   },
  1189.  
  1190.   /**
  1191.    * Called by the observer service. Looks for XRE shutdown messages 
  1192.    */
  1193.   observe: function(subject, topic, data) {
  1194.     var os      = Components.classes["@mozilla.org/observer-service;1"]
  1195.                       .getService(Components.interfaces.nsIObserverService);
  1196.     switch (topic) {
  1197.     case "quit-application":
  1198.       os.removeObserver(this, "quit-application");
  1199.       this._deinit();
  1200.       break;
  1201.     }
  1202.   },
  1203.  
  1204.   /**
  1205.    * See nsISupports.idl
  1206.    */
  1207.   QueryInterface: function(iid) {
  1208.     if (!iid.equals(IID) &&
  1209.         !iid.equals(Components.interfaces.nsIObserver) && 
  1210.         !iid.equals(Components.interfaces.nsISupports))
  1211.       throw Components.results.NS_ERROR_NO_INTERFACE;
  1212.     return this;
  1213.   }
  1214. }; // FeathersManager.prototype
  1215.  
  1216. /**
  1217.  * Callback helper for FeathersManager::switchFeathers
  1218.  * This is needed to make sure the window is really closed before we switch skins
  1219.  */
  1220. function FeathersManager_switchFeathers_callback(aFeathersManager,
  1221.                                                  aLayoutURL,
  1222.                                                  aInternalName) {
  1223.   this.feathersManager = aFeathersManager;
  1224.   this.layoutURL = aLayoutURL;
  1225.   this.internalName = aInternalName;
  1226. }
  1227.  
  1228. FeathersManager_switchFeathers_callback.prototype = {
  1229.   /**
  1230.    * \sa nsITimerCallback
  1231.    */
  1232.   notify: function FeathersManager_switchFeathers_callback_notify() {
  1233.     // Set new values
  1234.     this.feathersManager._layoutDataRemote.stringValue = this.layoutURL;
  1235.     this.feathersManager._skinDataRemote.stringValue = this.internalName;
  1236.  
  1237.     this.feathersManager._flushXULPrototypeCache();
  1238.     this.feathersManager.openPlayerWindow();
  1239.     this.feathersManager._switching = false;
  1240.     this.feathersManager = null;
  1241.   }
  1242. }; // FeathersManager_switchFeathers_callback.prototype
  1243.  
  1244.  
  1245.  
  1246.  
  1247.  
  1248. /**
  1249.  * ----------------------------------------------------------------------------
  1250.  * Registration for XPCOM
  1251.  * ----------------------------------------------------------------------------
  1252.  */
  1253. var gModule = {
  1254.   registerSelf: function(componentManager, fileSpec, location, type) {
  1255.     componentManager = componentManager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
  1256.  
  1257.     componentManager.registerFactoryLocation(CID, CLASSNAME, CONTRACTID,
  1258.                                                fileSpec, location, type);
  1259.   },
  1260.  
  1261.   getClassObject: function(componentManager, cid, iid) {
  1262.     if (!iid.equals(Components.interfaces.nsIFactory))
  1263.       throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  1264.  
  1265.     if (cid.equals(CID)) {
  1266.        return { 
  1267.           createInstance: function(outer, iid) {
  1268.             if (outer != null)
  1269.               throw Components.results.NS_ERROR_NO_AGGREGATION;
  1270.               
  1271.             // Make the feathers manager  
  1272.             return (new FeathersManager()).QueryInterface(iid);;
  1273.           }
  1274.        };
  1275.     }
  1276.     
  1277.     throw Components.results.NS_ERROR_NO_INTERFACE;
  1278.   },
  1279.  
  1280.   canUnload: function(componentManager) { 
  1281.     return true; 
  1282.   }
  1283. }; // gModule
  1284.  
  1285. function NSGetModule(comMgr, fileSpec) {
  1286.   return gModule;
  1287. } // NSGetModule
  1288.  
  1289.  
  1290.