Revision Content

Introduction

This article takes the Creating a dynamic status bar extension sample to the next level, adding a popup menu that lets you quickly switch between multiple stocks to watch. It also adds a preference dialog that lets you switch to a stock other than one of the ones included in the popup menu.

As before, concepts covered in the previous articles in this series won't be rehashed here, so if you haven't already seen them:

This line establishes the URL of the XUL file that describes the preference dialog.

Establish the defaults

In order to set a default preference for the stock to monitor, we need to add a new folder to our extension's package, called "defaults", which in turn contains another folder called "preferences". Inside that, we create a file, defaults.js, that describes the default value of our preferences:

The JavaScript code

In order to monitor changes to our preferences, we need to install an observer using the nsIPrefBranch2 interface. To do that, we need to reimplement our code into an object.

That involves turning each function into a member of the StockWatcher class. Let's take a look at each function in the class.

startup()

The startup() function is called when our extension is first loaded. Its job is to start up the observer to watch for changes to our preferences, instantiate an object to use to manage our preferences, and install an interval routine to update the stock information periodically.

Our class has two member variables. prefs is configured by startup() to reference our extension's preferences, while tickerSymbol indicates the stock symbol to monitor.

The first thing the startup() function does is to get a reference to the preferences for our extension. This is done in three steps:

First, we get the preferences-service component. This component handles preference management for Firefox and any extensions.

Second, we call the component's getService() method to obtain an interface to the preference service. This lets us actually issue calls on the preferences interface, to access preferences.

Third, we call getBranch() on the preference service interface. This lets us specify a specific branch of the preference tree to access. By default, we would have access to all preferences, but we only want access to those belonging to our own extension, so we specify that we want to access the "stockwatcher2" branch.

After getting an interface to the preference branch for our extension, we call the QueryInterface() method on it to ensure that it supports the <code>nsIPrefBranch2 interface. QueryInterface() will throw an exception if the nsIPrefBranch2 interface isn't supported. In current versions of Firefox, it's supported, but by adding this line, we can abort more or less gracefully on versions that don't support the interface we need.

The next step is to add an observer to the preference interface by calling the addObserver() method to establish that whenever any events occur on the preferences, our class ("this") receive notification. When events occur, such as a preference being altered, our observe() method will be called automatically.

Now that we're monitoring the preferences, we can set up to watch the stock information and display it in the status bar panel.

The first thing we need to do is get the currently configured stock symbol to watch from the preferences. To do so, we call the preference interface's getCharPref() method, specifying that we want the preference named "symbol", which is where we store the user's selection for the stock to watch. We forcibly convert the symbol to upper-case since that's the way stock symbols are normally displayed.

Next, we call our own refreshInformation() method to immediately fetch and display the current information about the stock the extension is configured to monitor. We'll look at the details of how this method works later.

The last thing the startup() method does is to call the window.SetInterval() DOM method to set up a callback that will automatically run our refreshInformation() method every 10 minutes. The interval time is specified in milliseconds.

shutdown()

The shutdown() method deactivates the observer on the preferences. This is also where we would add any other shutdown tasks we need to perform.

shutdown: function()
{
this.prefs.removeObserver("", this);
},

observe()

The observe() function is called whenever an event occurs on our preferences. For details on how observers work, read up on the nsIObserver interface.

The topic parameter indicates what type of event occurred. If it's not nsPref:changed, we simply ignore the event, since all we're interested in is changes to the values of our preferences.

Once we've established that the event is in fact a preference change, we look at the data parameter, which contains the name of the preference that changed. In our example, we only have one preference, but you can monitor as many preferences as you wish here.

If the changed preference is "symbol", we grab the updated value of the preference by calling the preference interface's getCharPref() method, and stash it in our tickerSymbol variable.

Once we've gotten the updated preference, we call refreshInformation() to immediately update the display with the new stock's information.

watchStock()

While we're at it, let's add a method that sets which stock we want to be watching, changing the preference and immediately requesting a refresh of the display. This method will be used when the user uses the popup menu we'll be adding to change what stock they're watching.

The only new information for us here is the call to the preference object's setCharPref() function, which sets the value of the "symbol" preference.

refreshInformation()

This method is slightly revised from previous versions, in that it needs to fetch the preference for the stock to watch and use that to construct the URL to monitor, as well as to construct the string to be displayed in the status bar panel.

Note that we instantiate an nsIPrefService interface here instead of using this.tickerSymbol to get the stock symbol to watch. We do this because since refreshInformation() is usually called as a callback, we can't be certain that this actually refers to the right object.

Once we have the symbol in the local variable symbol, we use that to construct the URL and the string to display in the status bar panel.

Installing the event listeners

The only thing left to do is to install the event listeners needed to run the startup() and shutdown() routines automatically when the browser window is loaded and unloaded.

The <preferences> block establishes all the settings we implement as well as their types. In our case, we have a single preference, the stock symbol to monitor. Preferences are identified by name; in this case, the name is "stockwatcher2.symbol".

The actual user interface is described in the <prefpane> block. The <hbox> element is used to lay out the user interface by indicating that the widgets described within it should be positioned next to each other in the window.

Our dialog has two widgets in it. The first is a label describing the textbox. The second is the textbox itself, in which the user enters the symbol. The preference property ties the textbox to the "pref_symbol" <preference> element and to the "stockwatcher2.symbol" preference. This lets the preference value automatically be updated to reflect the content of the textbox.

Adding the context menu

Adding the contextual menu is easy; all the work that needs doing is done in the stockwatcher2.xul file. The first step is to add the context attribute to the status bar panel:

Each item in the menu has a label property, which specifies the text displayed in the menu, as well as an oncommand property, which indicates the JavaScript code to execute when the user selects that item.

The Refresh Now option calls the StockWatcher.refreshInformation() function, to refresh the display. The rest of the options call the StockWatcher.watchStock() function to start watching a different stock.

Revision Source

<h2 name="Introduction">Introduction</h2>
<p>This article takes the <a href="en/Creating_a_dynamic_status_bar_extension">Creating a dynamic status bar extension</a> sample to the next level, adding a popup menu that lets you quickly switch between multiple stocks to watch. It also adds a preference dialog that lets you switch to a stock other than one of the ones included in the popup menu.
</p><p>As before, concepts covered in the previous articles in this series won't be rehashed here, so if you haven't already seen them:
</p>
<ul><li> <a href="en/Creating_a_status_bar_extension">Creating a status bar extension</a>
</li><li> <a href="en/Creating_a_dynamic_status_bar_extension">Creating a dynamic status bar extension</a>
</li></ul>
<p>Also, for reference, you may want to take a look at <a href="en/Preferences_System">Preferences System</a>.
</p>
<h2 name="Download_the_sample">Download the sample</h2>
<p>You can download a copy of this sample to look over, or to use as the basis for your own extension.
</p><p><a class="external" href="http://developer.mozilla.org/samples/extension-samples/stockwatcher2.zip">Download the sample</a>
</p>
<h2 name="Update_the_manifests">Update the manifests</h2>
<p>The install manifest and <a href="en/Chrome">chrome</a> manifest need to be updated. By and large, the changes are simply changing the ID of the extension. However, we do need to add one new line to the <code>install.rdf</code> file:
</p>
<pre class="eval"> <span class="plain">&lt;em:optionsURL&gt;chrome://stockwatcher2/content/options.xul&lt;/em:optionsURL&gt;</span>
</pre>
<p>This line establishes the URL of the XUL file that describes the preference dialog.
</p>
<h3 name="Establish_the_defaults">Establish the defaults</h3>
<p>In order to set a default preference for the stock to monitor, we need to add a new folder to our extension's package, called "defaults", which in turn contains another folder called "preferences". Inside that, we create a file, <code>defaults.js</code>, that describes the default value of our preferences:
</p>
<pre class="eval"> pref("stockwatcher2.symbol", "GOOG");
</pre>
<p>To learn more about the preference system, read <a href="en/Preferences_API">Preferences API</a>.
</p>
<h2 name="The_JavaScript_code">The JavaScript code</h2>
<p>In order to monitor changes to our preferences, we need to install an observer using the <a href="en/NsIPrefBranch2">nsIPrefBranch2</a> interface. To do that, we need to reimplement our code into an object.
</p><p>That involves turning each function into a member of the <code>StockWatcher</code> class. Let's take a look at each function in the class.
</p>
<h3 name="startup.28.29">startup()</h3>
<p>The <code>startup()</code> function is called when our extension is first loaded. Its job is to start up the observer to watch for changes to our preferences, instantiate an object to use to manage our preferences, and install an interval routine to update the stock information periodically.
</p>
<pre class="eval"> var StockWatcher = {
prefs: null,
tickerSymbol: "",
// Initialize the extension
startup: function()
{
// Register to receive notifications of preference changes
this.prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService)
.getBranch("stockwatcher2.");
this.prefs.QueryInterface(Components.interfaces.nsIPrefBranch2);
this.prefs.addObserver("", this, false);
this.tickerSymbol = this.prefs.getCharPref("symbol").toUpperCase();
this.refreshInformation();
window.setInterval(this.refreshInformation, 10*60*1000);
},
</pre>
<p>Our class has two member variables. <code>prefs</code> is configured by <code>startup()</code> to reference our extension's preferences, while <code>tickerSymbol</code> indicates the stock symbol to monitor.
</p><p>The first thing the <code>startup()</code> function does is to get a reference to the preferences for our extension. This is done in three steps:
</p>
<ul><li> First, we get the preferences-service component. This component handles preference management for Firefox and any extensions.
</li></ul>
<ul><li> Second, we call the component's <code>getService()</code> method to obtain an interface to the preference service. This lets us actually issue calls on the preferences interface, to access preferences.
</li></ul>
<ul><li> Third, we call <code><a class="external" href="http://www.xulplanet.com/references/xpcomref/ifaces/nsIPrefService.html#method_getBranch">getBranch()</a></code> on the preference service interface. This lets us specify a specific branch of the preference tree to access. By default, we would have access to all preferences, but we only want access to those belonging to our own extension, so we specify that we want to access the "stockwatcher2" branch.
</li></ul>
<p>After getting an interface to the preference branch for our extension, we call the <code><a href="en/NsISupports/QueryInterface"> QueryInterface()</a> method on it to ensure that it supports the &lt;code&gt;nsIPrefBranch2</code> interface. <code>QueryInterface()</code> will throw an exception if the <code>nsIPrefBranch2</code> interface isn't supported. In current versions of Firefox, it's supported, but by adding this line, we can abort more or less gracefully on versions that don't support the interface we need.
</p><p>The next step is to add an observer to the preference interface by calling the <code>addObserver()</code> method to establish that whenever any events occur on the preferences, our class ("this") receive notification. When events occur, such as a preference being altered, our <code>observe()</code> method will be called automatically.
</p><p>Now that we're monitoring the preferences, we can set up to watch the stock information and display it in the status bar panel.
</p><p>The first thing we need to do is get the currently configured stock symbol to watch from the preferences. To do so, we call the preference interface's <code>getCharPref()</code> method, specifying that we want the preference named "symbol", which is where we store the user's selection for the stock to watch. We forcibly convert the symbol to upper-case since that's the way stock symbols are normally displayed.
</p><p>Next, we call our own <code>refreshInformation()</code> method to immediately fetch and display the current information about the stock the extension is configured to monitor. We'll look at the details of how this method works later.
</p><p>The last thing the <code>startup()</code> method does is to call the <code><a href="en/DOM/window.setInterval"> window.SetInterval()</a></code> DOM method to set up a callback that will automatically run our <code>refreshInformation()</code> method every 10 minutes. The interval time is specified in milliseconds.
</p>
<h3 name="shutdown.28.29">shutdown()</h3>
<p>The <code>shutdown()</code> method deactivates the observer on the preferences. This is also where we would add any other shutdown tasks we need to perform.
</p>
<pre class="eval"> shutdown: function()
{
this.prefs.removeObserver("", this);
},
</pre>
<h3 name="observe.28.29">observe()</h3>
<p>The <code>observe()</code> function is called whenever an event occurs on our preferences. For details on how observers work, read up on the <a href="en/NsIObserver">nsIObserver</a> interface.
</p>
<pre class="eval"> observe: function(subject, topic, data)
{
if (topic != "nsPref:changed")
{
return;
}
switch(data)
{
case "symbol":
this.tickerSymbol = this.prefs.getCharPref("symbol").toUpperCase();
this.refreshInformation();
break;
}
},
</pre>
<p>The <code>topic</code> parameter indicates what type of event occurred. If it's not <code>nsPref:changed</code>, we simply ignore the event, since all we're interested in is changes to the values of our preferences.
</p><p>Once we've established that the event is in fact a preference change, we look at the <code>data</code> parameter, which contains the name of the preference that changed. In our example, we only have one preference, but you can monitor as many preferences as you wish here.
</p><p>If the changed preference is "symbol", we grab the updated value of the preference by calling the preference interface's <code>getCharPref()</code> method, and stash it in our <code>tickerSymbol</code> variable.
</p><p>Once we've gotten the updated preference, we call <code>refreshInformation()</code> to immediately update the display with the new stock's information.
</p>
<h3 name="watchStock.28.29">watchStock()</h3>
<p>While we're at it, let's add a method that sets which stock we want to be watching, changing the preference and immediately requesting a refresh of the display. This method will be used when the user uses the popup menu we'll be adding to change what stock they're watching.
</p>
<pre class="eval"> watchStock: function(newSymbol)
{
this.tickerSymbol = newSymbol;
this.prefs.setCharPref("symbol", newSymbol);
this.refreshInformation();
},
</pre>
<p>The only new information for us here is the call to the preference object's <code>setCharPref()</code> function, which sets the value of the "symbol" preference.
</p>
<h3 name="refreshInformation.28.29">refreshInformation()</h3>
<p>This method is slightly revised from previous versions, in that it needs to fetch the preference for the stock to watch and use that to construct the URL to monitor, as well as to construct the string to be displayed in the status bar panel.
</p>
<pre class="eval"> refreshInformation: function()
{
var httpRequest = null;
// Because we may be called as a callback, we can't rely on
// "this" referring to the right object, so we need to get our
// own preferences instance here.
var tickerPrefs = Components.classes["<span class="plain">@mozilla.org/preferences-service;1</span>"]
.getService(Components.interfaces.nsIPrefService)
.getBranch("stockwatcher2.");
var symbol = tickerPrefs.getCharPref("symbol");
var fullUrl = "<span class="plain">http://quote.yahoo.com/d/quotes.csv?f=sl1d1t1c1ohgv&amp;e=.csv&amp;s=</span>"
+ symbol;
function infoReceived()
{
var samplePanel = document.getElementById('stockwatcher2');
var output = httpRequest.responseText;
if (output.length)
{
// Remove newline and/or linefeed from the end of the string
var endIndex = 0;
var position = output.lastIndexOf("\n");
if (position &gt;= 0) {
endIndex = position;
}
position = output.lastIndexOf("\r");
if ((position &lt; endIndex) &amp;&amp; (position &gt;= 0)) {
endIndex = position;
}
output = output.substring(0, endIndex);
// Build the tooltip string
var fieldArray = output.split(",");
samplePanel.label = symbol + ": " + fieldArray[1];
samplePanel.tooltipText = "Chg: " + fieldArray[4] + " | " +
"Open: " + fieldArray[5] + " | " +
"Low: " + fieldArray[6] + " | " +
"High: " + fieldArray[7] + " | " +
"Vol: " + fieldArray[8];
}
}
httpRequest = new XMLHttpRequest();
httpRequest.open("GET", fullUrl, true);
httpRequest.onload = infoReceived;
httpRequest.send(null);
}
}
</pre>
<p>Note that we instantiate an nsIPrefService interface here instead of using <code>this.tickerSymbol</code> to get the stock symbol to watch. We do this because since <code>refreshInformation()</code> is usually called as a callback, we can't be certain that <code>this</code> actually refers to the right object.
</p><p>Once we have the symbol in the local variable <code>symbol</code>, we use that to construct the URL and the string to display in the status bar panel.
</p>
<h3 name="Installing_the_event_listeners">Installing the event listeners</h3>
<p>The only thing left to do is to install the event listeners needed to run the <code>startup()</code> and <code>shutdown()</code> routines automatically when the browser window is loaded and unloaded.
</p>
<pre class="eval">window.addEventListener("load", function(e) { StockWatcher.startup(); }, false);
window.addEventListener("unload", function(e) { StockWatcher.shutdown(); }, false);
</pre>
<h2 name="Design_the_preference_dialog">Design the preference dialog</h2>
<p>Now that we've written all the code, we need to build the XUL file for the preference dialog.
</p>
<pre class="eval">&lt;?xml version="1.0"?&gt;
&lt;?xml-stylesheet href="<span class="plain">chrome://global/skin/</span>" type="text/css"?&gt;
&lt;prefwindow id="stockwatcher2-prefs"
title="StockWatcher 2 Options"
xmlns="<span class="plain">http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul</span>"&gt;
&lt;preferences&gt;
&lt;preference id="pref_symbol" name="stockwatcher2.symbol" type="string"/&gt;
&lt;/preferences&gt;
&lt;prefpane id="sw2-stock-pane" label="Stock Settings"&gt;
&lt;hbox align="center"&gt;
&lt;label control="symbol" value="Stock to watch: "/&gt;
&lt;textbox preference="pref_symbol" id="symbol" maxlength="4"/&gt;
&lt;/hbox&gt;
&lt;/prefpane&gt;
&lt;/prefwindow&gt;
</pre>
<p>The <code>&lt;preferences&gt;</code> block establishes all the settings we implement as well as their types. In our case, we have a single preference, the stock symbol to monitor. Preferences are identified by name; in this case, the name is "stockwatcher2.symbol".
</p><p>The actual user interface is described in the <code>&lt;prefpane&gt;</code> block. The <code>&lt;hbox&gt;</code> element is used to lay out the user interface by indicating that the widgets described within it should be positioned next to each other in the window.
</p><p>Our dialog has two widgets in it. The first is a label describing the textbox. The second is the textbox itself, in which the user enters the symbol. The <code>preference</code> property ties the textbox to the "pref_symbol" &lt;preference&gt; element and to the "stockwatcher2.symbol" preference. This lets the preference value automatically be updated to reflect the content of the textbox.
</p>
<h2 name="Adding_the_context_menu">Adding the context menu</h2>
<p>Adding the contextual menu is easy; all the work that needs doing is done in the <code>stockwatcher2.xul</code> file. The first step is to add the <code>context</code> attribute to the status bar panel:
</p>
<pre class="eval"> &lt;statusbar id="status-bar"&gt;
&lt;statusbarpanel id="stockwatcher2"
label="Loading..."
context="stockmenu"
onclick="StockWatcher.refreshInformation()"
/&gt;
&lt;/statusbar&gt;
</pre>
<p>Now when the user clicks on the status bar panel, the stock information refreshes, but when they right-click on it, a context menu pops up.
</p><p>Defining the menu is also easy. All we need to do is create a <code>popupset</code> describing the menu, as follows:
</p>
<pre class="eval"> &lt;popupset id="mainPopupSet"&gt;
&lt;menupopup id="stockmenu"&gt;
&lt;menuitem label="Refresh Now" default="true"
oncommand="StockWatcher.refreshInformation()"/&gt;
&lt;menuseparator/&gt;
&lt;menuitem label="Apple (AAPL)" oncommand="StockWatcher.watchStock('AAPL')"/&gt;
&lt;menuitem label="Google (GOOG)" oncommand="StockWatcher.watchStock('GOOG')"/&gt;
&lt;menuitem label="Microsoft (MSFT)" oncommand="StockWatcher.watchStock('MSFT')"/&gt;
&lt;menuitem label="Yahoo! (YHOO)" oncommand="StockWatcher.watchStock('YHOO')"/&gt;
&lt;/menupopup&gt;
&lt;/popupset&gt;
</pre>
<p>Each item in the menu has a <code>label</code> property, which specifies the text displayed in the menu, as well as an <code>oncommand</code> property, which indicates the JavaScript code to execute when the user selects that item.
</p><p>The Refresh Now option calls the <code>StockWatcher.refreshInformation()</code> function, to refresh the display. The rest of the options call the <code>StockWatcher.watchStock()</code> function to start watching a different stock.
</p><p>For a more detailed tutorial on creating popup menus, see <a href="en/XUL_Tutorial/Popup_Menus">XUL Tutorial:Popup Menus</a>.
</p>