Difference between revisions of "Real World Mozilla Adding Chrome to FirstXpcom Lab"

From CDOT Wiki
Jump to: navigation, search
Line 1: Line 1:
 
[[Dive into Mozilla]] > [[Dive into Mozilla Day 5]] > Adding Chrome to FirstXpcom Lab
 
[[Dive into Mozilla]] > [[Dive into Mozilla Day 5]] > Adding Chrome to FirstXpcom Lab
 
'''IN PROGRESS...'''
 
  
 
=Introduction=
 
=Introduction=
Line 185: Line 183:
  
 
In the former case we use '''createInstance''', which gives us a new unique instance. In the latter we use '''getService''', which returns a shared instance of an existing component (i.e., a Singleton).  Unlike IFirstXpcom, which can be created many times by different callers, the nsIAlertsService is a shared component, because only one pop-up message at a time can be shown to the user.
 
In the former case we use '''createInstance''', which gives us a new unique instance. In the latter we use '''getService''', which returns a shared instance of an existing component (i.e., a Singleton).  Unlike IFirstXpcom, which can be created many times by different callers, the nsIAlertsService is a shared component, because only one pop-up message at a time can be shown to the user.
 +
 +
<blockquote>NOTE: Because this is JavaScript and not C++, there is no need to recompile or start/stop the browser when you make a change to your files.  Using the [http://ted.mielczarek.org/code/mozilla/extensiondev/index.html Extension Developer's extension], you can simply reload all chrome: '''Tools > Extension Developer > Reload All Chrome'''</blockquote>
  
 
== Creating the dialog ==
 
== Creating the dialog ==
Line 250: Line 250:
 
</pre>
 
</pre>
  
When the menu item is clicked, the dialog will be shown and the values the user enters returned.  The '''showDialog''' function begins by packaging up IN (we use '''inn'' because '''in''' is a keyword in JavaScript) and OUT variables.  This allows us to pass multiple variables from/to the dialog.  In this case, we pass the value of our component's '''name''' attribute to the dialog, so it can be displayed in a textbox for editing.  The '''out''' variable will contain the updated values as entered in the dialog's textboxes (the code to do this will be discussed shortly).
+
When the menu item is clicked, the dialog will be shown and the values the user enters returned.  The '''showDialog''' function begins by packaging up IN (we use '''inn''' because '''in''' is a keyword in JavaScript) and OUT variables.  This allows us to pass multiple variables from/to the dialog.  In this case, we pass the value of our component's '''name''' attribute to the dialog, so it can be displayed in a textbox for editing.  The '''out''' variable will contain the updated values as entered in the dialog's textboxes (the code to do this will be discussed shortly).
  
We actually display the dialog using '''window.openDialog''', which takes a URI to our dialog's XUL file, as well as a list of options (e.g., dialog is modal, resizable, etc.) and our '''params''' object, containing the inn and out variables.  Execution will block until the user clicks OK or Cancel, or closes the window.
+
We actually display the dialog using '''window.openDialog''', which takes a URI to our dialog's XUL file, as well as a list of options (e.g., dialog is modal, resizable, etc.) and our '''params''' object, containing the inn and out variables.  Execution will block on '''focus()''' until the user clicks OK or Cancel, or closes the window.
  
 
Here's the complete XUL file for our dialog, this time with the JavaScript added:   
 
Here's the complete XUL file for our dialog, this time with the JavaScript added:   
Line 285: Line 285:
 
       window.arguments[0].out = {name:nameValue, value:increaseValue};
 
       window.arguments[0].out = {name:nameValue, value:increaseValue};
  
      return true;
+
      return true;
 
     }
 
     }
 
   </script>
 
   </script>
Line 299: Line 299:
 
</pre>
 
</pre>
  
 +
In the dialog element we've wired the '''onLoad''' and '''onOK''' functions.  The '''onLoad''' function is used to extract the parameters passed in with the call to '''window.openDialog()''' and set values in the UI.
  
 +
The '''onOK''' function occurs when the user clicks the accept button (i.e., OK).  When this happens the values from the textboxes are obtained and packaged up in the '''out''' parameter we sent in earlier.  This is how we pass values back to the main window, and our extension code in '''firstxpcomchrome'''.
  
 +
= Integrating with the build systetm =
  
 +
TODO -- see http://developer.mozilla.org/en/docs/JAR_Manifests for a discussion of jar.mn files in the build system.
  
 +
=Reflections=
  
=Reflections=
+
We've now gone through a complete cycle, first developing a C++ XPCOM component, then writing unit tests and throw-away code in the JavaScript shell, before finally creating a complete XUL/JS extension with a custom UI.  The lessons learned along the way are useful even if you don't plan on doing all of it again: C++ developers have gained an awareness of the types of issues XUL/JS developers will face; and XUL/JS developers have a better understanding of what is happening when they use components and interfaces through script.
 +
 
 +
This simple component and extension can be used as the foundation for your next extension project.  We've really only scratched the surface.
  
 
=Resources=
 
=Resources=
Line 310: Line 317:
 
* [http://developer.mozilla.org/en/docs/Extensions Extensions on MDC]
 
* [http://developer.mozilla.org/en/docs/Extensions Extensions on MDC]
 
* [http://kb.mozillazine.org/Extension_development Extension Development on Mozillazine]
 
* [http://kb.mozillazine.org/Extension_development Extension Development on Mozillazine]
* [http://developer.mozilla.org/en/docs/XUL_Event_Propagation XUL Event Propagation]
 

Revision as of 14:35, 23 March 2007

Dive into Mozilla > Dive into Mozilla Day 5 > Adding Chrome to FirstXpcom Lab

Introduction

Previously we created an XPCOM component called FirstXpcom, tested it in the JavaScript Shell, and wrote xpcshell unit tests. However, we haven't really done anything with it yet. Ideally we'd like to add our component to the browser and create some UI in order to allow the user to access its functionality.

We'll work in stages to create a simple UI for accessing FirstXpcom, first as a separate chrome extension, before integrating it with the build system. Our goal will be to add a custom dialog box to the browser, accessible via the menu bar. This dialog box will allow the user to access the functionality in our XPCOM component via JavaScript that we'll write.

Creating the FirstXPCOM Chrome Extension

Generating the extension automatically

We've now gone through the process of creating an extension by hand twice (e.g., writing install.rdf, creating the proper directory structure, etc.), so we won't cover the process again. Instead, we'll use a handy on-line wizard to do the work for us. Use the following values/options:

  • Your Name: Your Name
  • Extension Name: First XPCOM Chrome
  • Extension Short Name: firstxpcomchrome
  • Extension ID: firstxpcomchrome@senecac.on.ca
  • Version: 0.1
  • Target Applications Firefox Minimum Version=2.0.0.* Maximum Version=3.0a3pre

After clicking Click Create Extension, navigate to your desktop and locate the generated file, firstxpcomchrome.zip. Extract this file somewhere (e.g., c:\temp\firstxpcomchrome).

Now create a file in your profile's extension folder (%Application Data%\Mozilla\Firefox\Profile\<development_profile>\extensions) named firstxpcomchrome@senecac.on.ca. This file should contain the full path to your unzipped extension (perhaps c:\temp\firstxpcomchrome):

$ cd Application\ Data/Mozilla/Firefox/Profiles/development_profile/extensions
$ echo c:\temp\firstxpcomchrome > firstxpcomchrome@senecac.on.ca

Restart Firefox and try out your new extension. By default you should have a new red menu item, Tools > Your localized menuitem (you should also see firstxpcomchrome listed in the Add-on manager, along with firstxpcom).

Review of XUL Overlays

The extension wizard generated our extension's generic structure and files, but also created a browser overlay and associated JavaScript file--these are what allow the custom menu item to be added. The files that interest us most are:

  • firstxpcomchrome/content/firefoxOverlay.xul
  • firstxpcomchrome/content/overlay.js
  • firstxpcomchrome/chrome.manifest

Together the firefoxOverlay.xul and chrome.manifest files provide a way to add the custom menu item to the browser's Tools menu. In chrome.manifest we see:

overlay chrome://browser/content/browser.xul chrome://firstxpcomchrome/content/firefoxOverlay.xul

This says to overlay the XUL found in firefoxOverlay.xul with browser.xul--the XUL file defining the browser's UI. In firefoxOverlay.xul we find the following overlay:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="chrome://firstxpcomchrome/skin/overlay.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "chrome://firstxpcomchrome/locale/firstxpcomchrome.dtd">
<overlay id="firstxpcomchrome-overlay"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script src="overlay.js"/>
  <stringbundleset id="stringbundleset">
    <stringbundle id="firstxpcomchrome-strings" src="chrome://firstxpcomchrome/locale/firstxpcomchrome.properties"/>
  </stringbundleset>

  <menupopup id="menu_ToolsPopup">
    <menuitem id="firstxpcomchrome-hello" label="&firstxpcomchrome.label;" 
              oncommand="firstxpcomchrome.onMenuItemCommand(event);"/>
  </menupopup>
</overlay>

Exploring the other generated files

The extension wizard also generated localization code, which you can see being used above, for example:

<!DOCTYPE overlay SYSTEM "chrome://firstxpcomchrome/locale/firstxpcomchrome.dtd">
...
    <menuitem ... label="&firstxpcomchrome.label;" ... /> 

Here an ENTITY from the referenced DTD is used instead of a hard-coded string. Looking at the file firstxpcomchrome/locale/en-US/firstxpcomchrome.dtd we can see the mapping to English, which is what gets displayed at runtime:

<!ENTITY firstxpcomchrome.label "Your localized menuitem">

You'll notice too that the menu item appears red. This is caused by a custom CSS property defined in firstxpcomchrome/skin/overlay.css. You can see it being referenced in firefoxOverlay.xul

<?xml-stylesheet href="chrome://firstxpcomchrome/skin/overlay.css" type="text/css"?>

The property itself, which modifies firstxpcomchrome-hello is defined as follows:

/* This is just an example.  You shouldn't do this. */
#firstxpcomchrome-hello
{
  color: red ! important;
}

Using skins and localization are topics in their own right. We'll discuss them again in future labs.

Adding our own code

The firefoxOverlay.xul file specifies that when the menu item is the firstxpcomchrome.onMenuItemCommand method will be called:

    <menuitem id="firstxpcomchrome-hello" label="&firstxpcomchrome.label;" 
              oncommand="firstxpcomchrome.onMenuItemCommand(event);"/>

This is perfect for our needs. It means that we need to add our code to firstxpcomchrome.onMenuItemCommand and it will be called a the appropriate time.

Here is firstxpcomchrome/content/overlay.js:

var firstxpcomchrome = {
  total: 0,
  firstxpcom: null,
  
  onLoad: function(e) {
    // initialization code
    this.initialized = true;
    this.firstxpcom = Components.classes["@senecac.on.ca/firstxpcom;1"]
	                 .createInstance(Components.interfaces.IFirstXpcom);
    this.firstxpcom.name = "First XPCOM";
  },

  onMenuItemCommand: function(e) {
    result = this.showDialog();
	
    // see if user clicked Cancel or OK
    if (!result)
      return;
	
    this.total = this.firstxpcom.add(this.total, result.value);
    this.firstxpcom.name = result.name;
	
    // Use the Alerts Service to display the results to the user.
    var alertsService = Components.classes["@mozilla.org/alerts-service;1"]
                           .getService(Components.interfaces.nsIAlertsService);
    alertsService.showAlertNotification(null, this.firstxpcom.name, this.total, 
                                        false, "", null);
  },
  
  showDialog: function() {
    var params = {inn: {name:this.firstxpcom.name}, out: null};
    window.openDialog("chrome://firstxpcomchrome/content/firstxpcomdialog.xul", "",
                      "chrome, dialog, modal, resizable=yes", params).focus();
    return params.out
  },
};
window.addEventListener("load", function(e) { firstxpcomchrome.onLoad(e); }, false);

Let's begin with the final line, a load listener which insures that our code is run when the browser starts-up. Our object's onLoad function takes care of general initialization tasks, including creating an instance of FirstXpcom that we'll use throughout the life of our extension:

firstxpcom: null,
... 
onLoad: function(e) {
  // initialization code
  this.initialized = true;
  this.firstxpcom = Components.classes["@senecac.on.ca/firstxpcom;1"]
	                 .createInstance(Components.interfaces.IFirstXpcom);
  this.firstxpcom.name = "First XPCOM";

Just as we did in our unit tests and with the JS Shell, we create an instance of firstxpcom and then QI (i.e., "query interface") it to IFirstXpcom. Now we can call its methods, for example, setting the name attribute. We do the same later in onMenuCommand:

this.total = this.firstxpcom.add(this.total, result.value);
this.firstxpcom.name = result.name;

Accessing our C++ XPCOM methods is as easy as calling any other JavaScript function.

The first part of onMenuCommand deals with displaying and using the dialog box we'll write, which we'll return to below. However, let's skip to the end of this function and discuss the following code:

// Use the Alerts Service to display the results to the user.
var alertsService = Components.classes["@mozilla.org/alerts-service;1"]
                          .getService(Components.interfaces.nsIAlertsService);
alertsService.showAlertNotification(null, this.firstxpcom.name, this.total, 
                                    false, "", null);

I chose to use the nsIAlertsService , which creates an animated pop-up over the task list, rather than displaying the info to the user with an alert() for a number of reasons. First, I want to show that now you know how to create an XPCOM component in C++, and also how to use it in JavaScript, you can use any of the hundreds of objects and interfaces available in the Mozilla platform--the nsIAlertsService is no different from IFirstXpcom. I also wanted to draw your attention to another method of instantiating a component in JavaScript. Compare the following two code snippets:

this.firstxpcom   = Components.classes["@senecac.on.ca/firstxpcom;1"]
                       .createInstance(Components.interfaces.IFirstXpcom);

var alertsService = Components.classes["@mozilla.org/alerts-service;1"]
                       .getService(Components.interfaces.nsIAlertsService);

In the former case we use createInstance, which gives us a new unique instance. In the latter we use getService, which returns a shared instance of an existing component (i.e., a Singleton). Unlike IFirstXpcom, which can be created many times by different callers, the nsIAlertsService is a shared component, because only one pop-up message at a time can be shown to the user.

NOTE: Because this is JavaScript and not C++, there is no need to recompile or start/stop the browser when you make a change to your files. Using the Extension Developer's extension, you can simply reload all chrome: Tools > Extension Developer > Reload All Chrome

Creating the dialog

Now let's focus on the code to create and use the dialog box. We've seen XUL files a number of times used to define overlays. However, we haven't done any UI work with them yet. XUL does for chrome what HTML does for content, namely, it allows the developer to use a declarative XML syntax in order to define a UI, and then add functionality with JavaScript.

XUL is well documented elsewhere, so we won't attempt to cover it here. Instead, we'll jump into creating a simple dialog using a handful of XUL widgets. Experimenting with XUL is greatly simplified by using Mark Finkle's XUL Explorer application (itself written in XUL and running on XULRunner). In all, we'll use the following XUL:

Together they can be used to create firstxpcomchrome/content/firstxpcomdialog.xul:

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<dialog
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  id="firstXpcomDialog"
  title="FirstXpcom Dialog"
  buttons="accept,cancel"
  buttonlabelaccept="OK"
  buttonlabelcancel="Cancel">

  <grid>
    <columns><column/><column/></columns>
    <rows>
      <row align="center"><label value="Name:"/><textbox id="name"/></row>
      <row align="center"><label value="Increase Value By:"/><textbox id="increase"/></row>
    </rows>
  </grid>

</dialog>

This is a simple dialog, and creating a more elaborate one is left to the reader as an exercise. Now that the dialog's structure is complete, we need to add JavaScript to make it work with the rest of our extension.

Coding the dialog

Let's return to the code we skipped in onMenuCommand related to the dialog:

  onMenuItemCommand: function(e) {
    result = this.showDialog();
	
    // see if user clicked Cancel or OK
    if (!result)
      return;
	
    this.total = this.firstxpcom.add(this.total, result.value);
    this.firstxpcom.name = result.name;
    ...
  },
  
  showDialog: function() {
    var params = {inn: {name:this.firstxpcom.name}, out: null};
    window.openDialog("chrome://firstxpcomchrome/content/firstxpcomdialog.xul", "",
                      "chrome, dialog, modal, resizable=yes", params).focus();
    return params.out
  },

When the menu item is clicked, the dialog will be shown and the values the user enters returned. The showDialog function begins by packaging up IN (we use inn because in is a keyword in JavaScript) and OUT variables. This allows us to pass multiple variables from/to the dialog. In this case, we pass the value of our component's name attribute to the dialog, so it can be displayed in a textbox for editing. The out variable will contain the updated values as entered in the dialog's textboxes (the code to do this will be discussed shortly).

We actually display the dialog using window.openDialog, which takes a URI to our dialog's XUL file, as well as a list of options (e.g., dialog is modal, resizable, etc.) and our params object, containing the inn and out variables. Execution will block on focus() until the user clicks OK or Cancel, or closes the window.

Here's the complete XUL file for our dialog, this time with the JavaScript added:

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<dialog
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  id="firstXpcomDialog"
  title="FirstXpcom Dialog"
  buttons="accept,cancel"
  buttonlabelaccept="OK"
  buttonlabelcancel="Cancel" 
  ondialogaccept="return onOK();"
  onload="onLoad();">

  <script>
    function onLoad() {
      // Get the values passed in via params.inn
      document.getElementById("name").value = window.arguments[0].inn.name;
      document.getElementById("increase").focus();
    }

    function onOK() {
      // These variables aren't strictly necessary, but are included for 
      // improved readability.  They hold the textbox values.
      var nameValue = document.getElementById("name").value
      var increaseValue = document.getElementById("increase").value

      // Package-up the two "return" values so they can be accessed via params.out
      window.arguments[0].out = {name:nameValue, value:increaseValue};

      return true;	
    }
  </script>

  <grid>
    <columns><column/><column/></columns>
    <rows>
      <row align="center"><label value="Name:"/><textbox id="name"/></row>
      <row align="center"><label value="Increase Value By:"/><textbox id="increase"/></row>
    </rows>
  </grid>
</dialog>

In the dialog element we've wired the onLoad and onOK functions. The onLoad function is used to extract the parameters passed in with the call to window.openDialog() and set values in the UI.

The onOK function occurs when the user clicks the accept button (i.e., OK). When this happens the values from the textboxes are obtained and packaged up in the out parameter we sent in earlier. This is how we pass values back to the main window, and our extension code in firstxpcomchrome.

Integrating with the build systetm

TODO -- see http://developer.mozilla.org/en/docs/JAR_Manifests for a discussion of jar.mn files in the build system.

Reflections

We've now gone through a complete cycle, first developing a C++ XPCOM component, then writing unit tests and throw-away code in the JavaScript shell, before finally creating a complete XUL/JS extension with a custom UI. The lessons learned along the way are useful even if you don't plan on doing all of it again: C++ developers have gained an awareness of the types of issues XUL/JS developers will face; and XUL/JS developers have a better understanding of what is happening when they use components and interfaces through script.

This simple component and extension can be used as the foundation for your next extension project. We've really only scratched the surface.

Resources