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:
Also, for reference, you may want to take a look at Preferences System and the Preferences API.
Download the sample
You can download a copy of this sample to look over, or to use as the basis for your own extension.
Get the code here: Download the sample
Update the manifests
The install manifest and chrome 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 install.rdf
file:
<em:optionsURL>chrome://stockwatcher2/content/options.xul</em:optionsURL>
This line establishes the URL of the XUL file that describes the options dialog.
Please note that if you are using code from this tutorial to add to an existing extension, you must uninstall and reinstall your extension to enable the Preferences button for your extension in the Add-ons list.
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:
pref("extensions.stockwatcher2.symbol", "GOOG");
The standard for third-party preferences, such as those used in extensions, is to use the string "extensions", a period, the name of the extension, another period, then a preference name, as seen in the example above.
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 StockWatcher.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.
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("extensions.stockwatcher2."); this.prefs.addObserver("", this, false); this.tickerSymbol = this.prefs.getCharPref("symbol").toUpperCase(); this.refreshInformation(); window.setInterval(this.refreshInformation, 10*60*1000); } },
Our object 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 two steps:
- First, we get the Preferences service. This component handles preference management for Firefox and any extensions.
- Second, we call
nsIPrefService.getBranch()
. 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 "extensions.stockwatcher2" branch.
After getting the preference branch for our extension, we call the nsISupports.QueryInterface()
method on it to be able to use the methods of the nsIPrefBranch2
interface.
The next step is to register a preference observer by calling the addObserver()
method to establish that whenever any events occur on the preferences, our object (this) receives 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 nsIPrefBranch.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 StockWatcher.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 StockWatcher.observe()
function is called whenever an event occurs on the preference branch we're watching. For details on how observers work, read up on the nsIObserver
interface.
observe: function(subject, topic, data) { if (topic != "nsPref:changed") { return; } switch(data) { case "symbol": this.tickerSymbol = this.prefs.getCharPref("symbol").toUpperCase(); this.refreshInformation(); break; } },
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 nsIPrefBranch.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.
watchStock: function(newSymbol) { this.prefs.setCharPref("symbol", newSymbol); },
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.
Note that this call results in our StockWatcher.observe()
method being invoked and displayed stock information being updated.
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.
refreshInformation: function() { // Because we may be called as a callback, we can't rely on // "this" referring to the right object, so we need to reference // it by its full name var symbol = StockWatcher.tickerSymbol; var fullUrl = "http://quote.yahoo.com/d/quotes.csv?f=sl1d1t1c1ohgv&e=.csv&s=" + symbol; function infoReceived() { var samplePanel = document.getElementById('stockwatcher2'); var output = httpRequest.responseText; if (output.length) { // Remove any whitespace from the end of the string output = output.replace(/\W*$/, ""); // 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]; } } var httpRequest = new XMLHttpRequest(); httpRequest.open("GET", fullUrl, true); httpRequest.onload = infoReceived; httpRequest.send(null); } }
Note that we use StockWatcher.tickerSymbol
here instead of this.tickerSymbol
to get the stock symbol to watch. We do this because since refreshInformation()
is usually called as a callback from setInterval
. In such cases this
doesn't refer to the right object. See Method binding for detailed explanation.
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.
window.addEventListener("load", function(e) { StockWatcher.startup(); }, false); window.addEventListener("unload", function(e) { StockWatcher.shutdown(); }, false);
Design the preference dialog
Now that we've written all the code, we need to build the XUL file for the options dialog.
<?xml version="1.0"?> <?xml-stylesheet href="chrome://global/skin/" type="text/css"?> <prefwindow id="stockwatcher2-prefs" title="StockWatcher 2 Options" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <prefpane id="sw2-stock-pane" label="Stock Settings"> <preferences> <preference id="pref_symbol" name="extensions.stockwatcher2.symbol" type="string"/> </preferences> <hbox align="center"> <label control="symbol" value="Stock to watch: "/> <textbox preference="pref_symbol" id="symbol" maxlength="4"/> </hbox> </prefpane> </prefwindow>
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 "extensions.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 inside it should be positioned horizontally, 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 "extensions.stockwatcher2.symbol" preference. This lets the preference value automatically be updated to reflect the content of the textbox.
Alternative method: Inline Options
Requires Gecko 7(Firefox 7 / Thunderbird 7 / SeaMonkey 2.4)You could use Inline Options for this preference. You must set em:optionsType
to 2 in your install.rdf. Your options.xul
file would look like this:
<?xml version="1.0" ?> <vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <setting pref="extensions.stockwatcher2.symbol" type="string" title="Stock to watch" /> </vbox>
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:
<statusbar id="status-bar"> <statusbarpanel id="stockwatcher2" label="Loading..." context="stockmenu" onclick="StockWatcher.refreshInformation()" /> </statusbar>
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.
Defining the menu is also easy. All we need to do is add a popupset
describing the menu to the statusbar, as follows:
<popupset> <menupopup id="stockmenu"> <menuitem label="Refresh Now" default="true" oncommand="StockWatcher.refreshInformation()"/> <menuseparator/> <menuitem label="Apple (AAPL)" oncommand="StockWatcher.watchStock('AAPL')"/> <menuitem label="Google (GOOG)" oncommand="StockWatcher.watchStock('GOOG')"/> <menuitem label="Microsoft (MSFT)" oncommand="StockWatcher.watchStock('MSFT')"/> <menuitem label="Yahoo! (YHOO)" oncommand="StockWatcher.watchStock('YHOO')"/> </menupopup> </popupset>
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.
For a more detailed tutorial on creating popup menus, see XUL Tutorial:Popup Menus.
override chrome://myaddon/content/options.xul chrome://myaddon/content/oldOptions.xul application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} appversion<=6.*
Examples
- GitHub - Gist :: _ff-addon-template-bootstrapPrefsSkeleton - This Gist here is a fully working example of a fully funcitonal preferences skeleton, it uses the observer example from above. This example also uses a technique to trigger an onChange function that can be defined per preference and has the old value and along with the new value of the preference. It also uses a technique so you can just do prefs.preferenceName.setval('blah') instead of having to branch.setCharPref etc.