Difference between revisions of "Dive into Mozilla Modifying Firefox using an Extension Lab"
(→Reflections) |
|||
Line 5: | Line 5: | ||
In the [[Dive into Mozilla Modifying Firefox Lab|previous lab]] we made a small change to the behaviour of Firefox by modifying the browser's source code. In this lab we explore how to achieve the same effect using an '''extension''' rather than modifying the tree. | In the [[Dive into Mozilla Modifying Firefox Lab|previous lab]] we made a small change to the behaviour of Firefox by modifying the browser's source code. In this lab we explore how to achieve the same effect using an '''extension''' rather than modifying the tree. | ||
− | The goal of this exercise is to expose you to Firefox extensions and to show you how to modify or extend the browser without changing | + | The goal of this exercise is to expose you to Firefox extensions and to show you how to modify or extend the browser without changing its source code directly. Some thought will also be given to the two methods of doing this (i.e., in the tree vs. as an extension), comparing their advantages and disadvantages. |
=The 'What': write a tab creation extension= | =The 'What': write a tab creation extension= | ||
Line 25: | Line 25: | ||
What does this leave? You could [http://en.wikipedia.org/wiki/Fork_(software_development) fork] Firefox, as some people have done, and create your own version of the browser. Obviously this isn't what we'd like to do. Rather, what we need is a mechanism to insert a small change into the browser, and do so in such a way that users can choose to install our code or not. Mozilla provides such a mechanism in the form of Extensions. | What does this leave? You could [http://en.wikipedia.org/wiki/Fork_(software_development) fork] Firefox, as some people have done, and create your own version of the browser. Obviously this isn't what we'd like to do. Rather, what we need is a mechanism to insert a small change into the browser, and do so in such a way that users can choose to install our code or not. Mozilla provides such a mechanism in the form of Extensions. | ||
− | Extensions allow third-party developers (and Mozilla, for that matter) to write add-on packages that users can install to extend or modify the standard browser. By rewriting our earlier code as an extension, we can give users | + | Extensions allow third-party developers (and Mozilla, for that matter) to write add-on packages that users can install to extend or modify the standard browser. By rewriting our earlier code as an extension, we can give our users a small add-on, which will have the same effect as our custom build. For all but the most universal of changes, extensions are the best way for developers to write code that targets the browser. |
==Planning the extension== | ==Planning the extension== | ||
Line 50: | Line 50: | ||
===How to move a tab?=== | ===How to move a tab?=== | ||
− | Now we know that there is a logical time/place to run our code via the '''TabOpen''' event. | + | Now we know that there is a logical time/place to run our code via the '''TabOpen''' event. The next thing to consider is what our code will do. Previously we replaced '''append''' with '''insertBefore''' and stopped the problem before it happened. However, in this case by the time the '''TabOpen''' event is dispatched, the tab will already be created and positioned at the end of the list. |
− | We need another solution | + | We need another solution. It's time to go hunting in tabbrowser's [http://developer.mozilla.org/en/docs/XUL:tabbrowser documentation] and [http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml code]. What we need is a way to reposition a tab after it has been created. A quick look through the docs for [http://developer.mozilla.org/en/docs/XUL:tabbrowser tabbrowser] reveals nothing. However, the [http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml code] is more helpful: |
<method name="moveTabTo"> | <method name="moveTabTo"> | ||
Line 63: | Line 63: | ||
===How to get the newly created tab using code?=== | ===How to get the newly created tab using code?=== | ||
− | Having already studied and worked with the code in '''addTab''', and knowing that new tabs are always appended to the end of the list, we could do | + | Having already studied and worked with the code in '''addTab''', and knowing that new tabs are always appended to the end of the list, we could do the following in order to get the new tab: |
// In an extension, gBrowser is a global reference to the tabbrowser element | // In an extension, gBrowser is a global reference to the tabbrowser element | ||
Line 74: | Line 74: | ||
t.dispatchEvent(evt); | t.dispatchEvent(evt); | ||
− | This means that in the event system, we'll be able to listen for and capture this event, and in so doing get at | + | This means that in the event system, we'll be able to listen for and capture this event, and in so doing get at the tab object via the event's '''target'''. Three lines of code become one: |
var newTab = e.target; | var newTab = e.target; | ||
Line 82: | Line 82: | ||
===How to get the index of the currently selected tab?=== | ===How to get the index of the currently selected tab?=== | ||
− | We know that in order to move a tab using tabbrowser's '''moveTabTo''' method we need the tab object--which we now have--and the index where it should be placed. Looking through the code | + | We know that in order to move a tab using tabbrowser's '''moveTabTo''' method we need the tab object--which we now have--and the index where it should be placed. Looking through the tabbrowser code again, we see [http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#1453 references] to code like this: |
var currentIndex = this.mTabContainer.selectedIndex; | var currentIndex = this.mTabContainer.selectedIndex; | ||
Line 116: | Line 116: | ||
=The 'How': the extension's code= | =The 'How': the extension's code= | ||
− | Now that we have the logic for our code, all that remains is to write the necessary pieces to have it get executed at the right | + | Now that we have the logic for our code, all that remains is to write the necessary pieces in order to have it get executed at the right time. When we modified the code in the tree this wasn't a concern: we assumed that code we changed in '''addTab''' would get executed whenever '''addTab''' got called. With an extension, we have to explicitly tell the browser how and when to execute our code. We do this using '''event listeners'''. |
From our previous research, we know that the methods in tabbrowser cause a variety of events to get dispatched into the event system. These events move through the event system regardless of whether any code notices them--they are passive. In most cases, nothing is done in response to events. However, any developer can decide to do something in response to an event, which is what extension developers must do in order to get their code into the browser. | From our previous research, we know that the methods in tabbrowser cause a variety of events to get dispatched into the event system. These events move through the event system regardless of whether any code notices them--they are passive. In most cases, nothing is done in response to events. However, any developer can decide to do something in response to an event, which is what extension developers must do in order to get their code into the browser. | ||
− | We need to wire our code to a number of events by [http://developer.mozilla.org/en/docs/XUL_Event_Propagation#Adding_an_Event_Listener adding event listeners]. Two of the events we know already, '''TabOpen''' and '''TabSelect'''. However, we also need to | + | We need to wire our code to a number of events by [http://developer.mozilla.org/en/docs/XUL_Event_Propagation#Adding_an_Event_Listener adding event listeners]. Two of the events we know already, '''TabOpen''' and '''TabSelect'''. However, we also need to register our listeners at start-up using the '''window's load event'''. We'll add code to remove our event listeners in the '''window's unload event''' too. |
− | == | + | ==addtabbeside.js== |
− | To keep things clean we'll write all this code in a custom object called '''AddTabBeside'''. Here is ''' | + | To keep things clean we'll write all this code in a custom object called '''AddTabBeside'''. Here is '''addtabbeside.js''': |
var AddTabBeside = { | var AddTabBeside = { | ||
Line 176: | Line 176: | ||
window.addEventListener("load", function(e) { AddTabBeside.onLoad(e); }, false); | window.addEventListener("load", function(e) { AddTabBeside.onLoad(e); }, false); | ||
− | + | This code will become part of an overlay that will be merged with the browser at runtime. But where do we put this file? Firefox needs to know where it is in order to load and run it at startup. We also need to create an installation package for our overlay. | |
==Creating the rest of the extension== | ==Creating the rest of the extension== | ||
− | Firefox extensions are packaged as compressed zip files with a '''.XPI''' extension. This is what you download when you visit http://addons.mozilla.org and choose to install an extension. | + | Firefox extensions are packaged as compressed zip files with a '''.XPI''' extension. This is what you download when you visit http://addons.mozilla.org and choose to install an extension. Because extensions are just .zip files, you can unpack any extension and see how it's built. For example, try saving [https://addons.mozilla.org/firefox/16/ ChatZilla's .xpi] to your computer and decompress it (NOTE: if your built-in unzip program won't do this, try changing the file's extension to .zip). |
− | An extension is a series of files and directories containing JavaScript, XUL, CSS, XML, RDF, and other custom text files. Some of these files define scripts to be executed, such as those we wrote above. Other files contain information ''about'' the extension, | + | An extension is a series of files and directories containing JavaScript, XUL, CSS, XML, RDF, and other custom text files. Some of these files define scripts to be executed, such as those we wrote above. Other files contain information ''about'' the extension, metadata telling the browser how to integrate and install things, what the extension is called, its version number, etc. We need to create these other files now. |
===Extension files and directory structure=== | ===Extension files and directory structure=== | ||
Line 196: | Line 196: | ||
====addtabbeside/chrome/content==== | ====addtabbeside/chrome/content==== | ||
− | The first thing to do is to copy the ''' | + | The first thing to do is to copy the '''addtabbeside.js''' file we wrote earlier to '''addtabbdeside/chrome/content/addtabbeside.js'''. |
This code now needs to get merged into the browser so it can access elements within the application. We do this by providing a [http://developer.mozilla.org/en/docs/XUL_Overlays XUL Overlay] file. A XUL Overlay is a .xul file that specifies XUL fragments to insert at specific merge points within a "master" document. Often this is used to add new UI to an application (e.g., a new menu item), but in our case we'll use it to merge our overlay.js script into the browser. | This code now needs to get merged into the browser so it can access elements within the application. We do this by providing a [http://developer.mozilla.org/en/docs/XUL_Overlays XUL Overlay] file. A XUL Overlay is a .xul file that specifies XUL fragments to insert at specific merge points within a "master" document. Often this is used to add new UI to an application (e.g., a new menu item), but in our case we'll use it to merge our overlay.js script into the browser. | ||
Line 206: | Line 206: | ||
<overlay id="addtabbeside-overlay" | <overlay id="addtabbeside-overlay" | ||
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> | xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> | ||
− | + | <script src="addtabbeside.js"/> | |
− | + | </overlay> | |
</pre> | </pre> | ||
Line 216: | Line 216: | ||
The first is '''addtabbeside/content.manifest''', which is a [http://developer.mozilla.org/en/docs/Chrome_Manifest Chrome Manifest]. This file helps Firefox translate between chrome:// URIs and actual files on disk. It also allows us to specify where our overlay will be merged into the browser: | The first is '''addtabbeside/content.manifest''', which is a [http://developer.mozilla.org/en/docs/Chrome_Manifest Chrome Manifest]. This file helps Firefox translate between chrome:// URIs and actual files on disk. It also allows us to specify where our overlay will be merged into the browser: | ||
− | # Chrome package | + | # Chrome package addtabbeside has it's content in ./chrome/content |
content addtabbeside chrome/content/ | content addtabbeside chrome/content/ | ||
− | # Overlay the | + | # Overlay the browser.xul file with overlay.xul |
overlay chrome://browser/content/browser.xul chrome://addtabbeside/content/overlay.xul | overlay chrome://browser/content/browser.xul chrome://addtabbeside/content/overlay.xul | ||
The first line registers the location for our content (i.e., .xul, .js). The second line registers our overlay, and says that overlay.xul will be merged with browser.xul. Mozilla uses chrome:// URIs to refer to aspects of the interface, and chrome://browser/content/browser.xul ''is'' the browser (try typing it into the address bar). | The first line registers the location for our content (i.e., .xul, .js). The second line registers our overlay, and says that overlay.xul will be merged with browser.xul. Mozilla uses chrome:// URIs to refer to aspects of the interface, and chrome://browser/content/browser.xul ''is'' the browser (try typing it into the address bar). | ||
− | The second metadata file we need to create is '''addtabbeside/install.rdf'''. This is an [http://developer.mozilla.org/en/docs/Install_Manifests install manifest] that tells the Firefox | + | The second metadata file we need to create is '''addtabbeside/install.rdf'''. This is an [http://developer.mozilla.org/en/docs/Install_Manifests install manifest] that tells the Firefox add-on manager how to install the extension, with information like who wrote it, the version number, compatibility information, etc. |
<pre> | <pre> | ||
Line 250: | Line 250: | ||
===Testing the extension=== | ===Testing the extension=== | ||
− | Eventually we'll package our extension properly into a redistributable .xpi. However, while we're testing it's nice to be able to use it in an expanded | + | Eventually we'll package our extension properly into a redistributable .xpi. However, while we're testing it's nice to be able to use it in an expanded form so we can make changes. |
To this end, create a file named '''addtabbeside@senecac.on.ca''' and put it in your development profile's extensions directory (NOTE: replace ''Username'' with your username and ''dev-profile'' with your development profile name): | To this end, create a file named '''addtabbeside@senecac.on.ca''' and put it in your development profile's extensions directory (NOTE: replace ''Username'' with your username and ''dev-profile'' with your development profile name): | ||
Line 256: | Line 256: | ||
C:\Documents and Settings\''Username''\Application Data\Mozilla\Firefox\Profiles\''dev-profile''\extensions\addtabbeside@senecac.on.ca | C:\Documents and Settings\''Username''\Application Data\Mozilla\Firefox\Profiles\''dev-profile''\extensions\addtabbeside@senecac.on.ca | ||
− | This file should contain | + | This file should contain a single line of text--the full path to your extension, for example: |
C:\temp\addtabbeside | C:\temp\addtabbeside | ||
Line 268: | Line 268: | ||
==Success, and some bugs== | ==Success, and some bugs== | ||
− | The rewrite | + | The rewrite to an extension has been a success. In both cases we've managed to achieve the same goal using almost completely different methods. Using an extension we've made it possible to share our changes with any Firefox user worldwide without having to ship a custom build. |
− | However, as was the case in our previous attempt, our code has a bug. Moving existing tabs doesn't update our position state, since we only modify mPreviousIndex when a new tab is selected; moved tabs remain selected, but change their order. | + | However, as was the case in our previous attempt, our code has a bug. Moving existing tabs doesn't update our position state, since we only modify '''mPreviousIndex''' when a new tab is selected; moved tabs remain selected, but change their order (i.e., TabSelect won't get dispatched on a move). |
Luckily we've already stumbled upon the solution to this problem--the '''TabMove''' event. Here is the updated version of '''overlay.js''', with the changes in bold: | Luckily we've already stumbled upon the solution to this problem--the '''TabMove''' event. Here is the updated version of '''overlay.js''', with the changes in bold: | ||
Line 330: | Line 330: | ||
=Reflections= | =Reflections= | ||
− | + | The process of developing extensions is greatly improved with the addition of some settings to the browser, as well as the [http://ted.mielczarek.org/code/mozilla/extensiondev/index.html Extension Developer's extension]. This extension will automate many of the steps described here. It will also allow you to reload the browser's chrome at runtime. This is a great way to test changes to your XUL/JS files without having to restart the browser. | |
− | + | You can read complete details on how to set-up an extension developer's environment [http://kb.mozillazine.org/Setting_up_extension_development_environment here]. | |
=Resources= | =Resources= |
Revision as of 10:55, 15 March 2007
Dive into Mozilla > Dive into Mozilla Day 5 > Modifying Firefox using an Extension Lab
Contents
Introduction
In the previous lab we made a small change to the behaviour of Firefox by modifying the browser's source code. In this lab we explore how to achieve the same effect using an extension rather than modifying the tree.
The goal of this exercise is to expose you to Firefox extensions and to show you how to modify or extend the browser without changing its source code directly. Some thought will also be given to the two methods of doing this (i.e., in the tree vs. as an extension), comparing their advantages and disadvantages.
The 'What': write a tab creation extension
As was previously the case, our goal is to modify Firefox so that new tabs are created (i.e. positioned) beside the current tab instead of being appended to the end of the list.
However, unlike last time where we modified tabbrowser.xml directly, this time we will overlay our changes onto the browser at runtime, and alleviate the need for any direct changes to the code. This is possible using extensions.
The 'Where': figuring out where to put our code
The first thing we should address is why we're doing this at all: why bother creating an extension in order to do what we've already done in the tree? There are a number of answers to this question.
Changes in the tree vs. an extension
The first problem with doing things in the tree is that in order for you to distribute the local changes you've made, people will have to use your custom build of Firefox. While this might seem like a good idea the first time you build the browser, it isn't sustainable long term, and users will always want to get the browser from Mozilla for security fixes, new features, etc.
A logical alternative might be to try and get your change accepted into the tree. This involves filing a bug on https://bugzilla.mozilla.org and then creating and attaching a patch with your changes. The problem here is that even though you might think it is a good idea for tabs to be created in the way we've specified, the community may not--people have already put thought into the way things work now, users are accustomed to it, etc. In this case your bug will likely be marked WONTFIX, which means that your patch won't make it into the tree.
What does this leave? You could fork Firefox, as some people have done, and create your own version of the browser. Obviously this isn't what we'd like to do. Rather, what we need is a mechanism to insert a small change into the browser, and do so in such a way that users can choose to install our code or not. Mozilla provides such a mechanism in the form of Extensions.
Extensions allow third-party developers (and Mozilla, for that matter) to write add-on packages that users can install to extend or modify the standard browser. By rewriting our earlier code as an extension, we can give our users a small add-on, which will have the same effect as our custom build. For all but the most universal of changes, extensions are the best way for developers to write code that targets the browser.
Planning the extension
Having decided to rewrite the code as an extension and remove our changes from the tree, a logical first thought would be: "how can I modify my existing code to create an extension." While we learned some useful things modifying the browser directly, our extension will require a completely new approach to solving the same problem.
Instead of searching for existing code to change, we have to take the browser as a given and find ways to work with it. To that end, let's look again at the code for tabbrowser's addTab method:
var t = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "tab"); ... this.mTabContainer.appendChild(t); ... // Dispatch a new tab notification. We do this once we're // entirely done, so that things are in a consistent state // even if the event listener opens or closes tabs. var evt = document.createEvent("Events"); evt.initEvent("TabOpen", true, false); t.dispatchEvent(evt); return t;
After the new tab is created and appended to the list, an event is dispatched to the new tab element. The developers of tabbrowser.xml wanted their code to be extensible; that is, they wanted to provide a way for future developers to extend the code by inserting a hook. In this case, after a tab is created an opportunity is provided to do something. Their foresight makes our job much easier.
How to move a tab?
Now we know that there is a logical time/place to run our code via the TabOpen event. The next thing to consider is what our code will do. Previously we replaced append with insertBefore and stopped the problem before it happened. However, in this case by the time the TabOpen event is dispatched, the tab will already be created and positioned at the end of the list.
We need another solution. It's time to go hunting in tabbrowser's documentation and code. What we need is a way to reposition a tab after it has been created. A quick look through the docs for tabbrowser reveals nothing. However, the code is more helpful:
<method name="moveTabTo"> <parameter name="aTab"/> <parameter name="aIndex"/> ...
The moveTabTo method takes a tab to be moved, and a new position. This is exactly what we need. Now there are just two things left to figure out: 1) how to get the newly inserted tab; and 2) the index of currently selected tab so we can go one to the right.
How to get the newly created tab using code?
Having already studied and worked with the code in addTab, and knowing that new tabs are always appended to the end of the list, we could do the following in order to get the new tab:
// In an extension, gBrowser is a global reference to the tabbrowser element var container = gBrowser.tabContainer; var lastIndex = container.childNodes.length - 1; var newTab = container.childNodes[lastIndex];
However, looking back at the event dispatching code for TabOpen we see that the event is dispatched to t, where t is the newly created tab:
t.dispatchEvent(evt);
This means that in the event system, we'll be able to listen for and capture this event, and in so doing get at the tab object via the event's target. Three lines of code become one:
var newTab = e.target;
We'll discuss this code in more detail below.
How to get the index of the currently selected tab?
We know that in order to move a tab using tabbrowser's moveTabTo method we need the tab object--which we now have--and the index where it should be placed. Looking through the tabbrowser code again, we see references to code like this:
var currentIndex = this.mTabContainer.selectedIndex;
Obviously we can't use this outside the context of tabbrowser, so in an extension we use gBrowser instead to get a reference to tabbrowser:
gBrowser.tabContainer.selectedIndex
If we want to get the tab to the right of the current tab, we simply do this:
var positionPlusOne = gBrowser.tabContainer.selectedIndex + 1;
Are we done? Not quite. This code will work, but the problem we now face is that our code will run after the tab has been placed at the end of the list, when TabOpen is dispatched. As soon as the new tab is created, it becomes the active tab. That means that selectedIndex will always be the index of the last tab, and moving the last tab one to the right is nonsense.
What we really need is the position of the tab that we were on when he made the new tab. We even know how to get this information, using tabContainer.selectedIndex. What we don't know is how to get this information, since it is lost by the time our code runs.
Saving tab position state
It's clear that we need data from the past--the position of the tab we were on before this new tab was created. We can't go back in time, so we'll have to store this data in a variable for inspection later.
In order to accomplish this, we need another event that tells us when a tab has become the active tab so we can store the position state. A quick search through tabbrowser for other events (e.g., search on "dispatchEvent") shows these possibilities:
- TabSelect
- TabOpen
- TabClose
- NewTab
- TabMove
The first event looks interesting. All we need to do now is have a variable that gets updated with the value of tabContainer.selectedIndex every time TabSelect occurs. Then, we can use this variable's value to calculate the desired position in moveTabTo.
Finally, we've got all the pieces in place and can write some code.
The 'How': the extension's code
Now that we have the logic for our code, all that remains is to write the necessary pieces in order to have it get executed at the right time. When we modified the code in the tree this wasn't a concern: we assumed that code we changed in addTab would get executed whenever addTab got called. With an extension, we have to explicitly tell the browser how and when to execute our code. We do this using event listeners.
From our previous research, we know that the methods in tabbrowser cause a variety of events to get dispatched into the event system. These events move through the event system regardless of whether any code notices them--they are passive. In most cases, nothing is done in response to events. However, any developer can decide to do something in response to an event, which is what extension developers must do in order to get their code into the browser.
We need to wire our code to a number of events by adding event listeners. Two of the events we know already, TabOpen and TabSelect. However, we also need to register our listeners at start-up using the window's load event. We'll add code to remove our event listeners in the window's unload event too.
addtabbeside.js
To keep things clean we'll write all this code in a custom object called AddTabBeside. Here is addtabbeside.js:
var AddTabBeside = { // State info on the last tab to be selected. mPreviousIndex: 0, onLoad: function() { // Add a listener for the TabOpen event, which gets called as // part of addTab in tabbrowser.xml var container = gBrowser.tabContainer; container.addEventListener("TabOpen", this.onTabOpen, false); // Also add a listener for TabSelect so we know when focus changes to a new tab container.addEventListener("TabSelect", this.onTabSelect, false); // Finally, add a listener for shutdown window.addEventListener("unload", this.onUnload, false); }, onUnload: function() { // Remove our listeners var container = gBrowser.tabContainer; container.removeEventListener("TabOpen", this.onTabOpen, false); container.removeEventListener("TabSelect", this.onTabSelect, false); }, onTabSelect: function (e) { // When a different tab is selected, remember which one. This is // necessary because when a new tab is created, it will get pushed // to the end of the list, but we need to know where to put it. this.mPreviousIndex = gBrowser.tabContainer.selectedIndex; }, onTabOpen: function (e) { // Get the newly created tab, which will be last in the list var newTab = e.target; // Move this new tab to the right of the previously selected tab, // checking to see how many tabs there are currently. By default // there is 1 tab, and the first time onTabOpen is called, there will // be 2 (the default plus the newly created tab). In this case, don't // move the new tab, since it is already in the right spot. In all // other cases, move the tab to the right of the current tab. if (gBrowser.tabContainer.childNodes.length > 2) { gBrowser.moveTabTo(newTab, this.mPreviousIndex + 1); } }, }; // Insure that our code gets loaded at start-up window.addEventListener("load", function(e) { AddTabBeside.onLoad(e); }, false);
This code will become part of an overlay that will be merged with the browser at runtime. But where do we put this file? Firefox needs to know where it is in order to load and run it at startup. We also need to create an installation package for our overlay.
Creating the rest of the extension
Firefox extensions are packaged as compressed zip files with a .XPI extension. This is what you download when you visit http://addons.mozilla.org and choose to install an extension. Because extensions are just .zip files, you can unpack any extension and see how it's built. For example, try saving ChatZilla's .xpi to your computer and decompress it (NOTE: if your built-in unzip program won't do this, try changing the file's extension to .zip).
An extension is a series of files and directories containing JavaScript, XUL, CSS, XML, RDF, and other custom text files. Some of these files define scripts to be executed, such as those we wrote above. Other files contain information about the extension, metadata telling the browser how to integrate and install things, what the extension is called, its version number, etc. We need to create these other files now.
Extension files and directory structure
Start by creating the following directory structure:
addtabbeside/ chrome/ content/
Because this is a first extension, we will skip some other directories and files that more complete extensions would include (e.g., localization, skins).
addtabbeside/chrome/content
The first thing to do is to copy the addtabbeside.js file we wrote earlier to addtabbdeside/chrome/content/addtabbeside.js.
This code now needs to get merged into the browser so it can access elements within the application. We do this by providing a XUL Overlay file. A XUL Overlay is a .xul file that specifies XUL fragments to insert at specific merge points within a "master" document. Often this is used to add new UI to an application (e.g., a new menu item), but in our case we'll use it to merge our overlay.js script into the browser.
We need to create addtabbdeside/chrome/content/overlay.xul:
<?xml version="1.0" encoding="UTF-8"?> <overlay id="addtabbeside-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> <script src="addtabbeside.js"/> </overlay>
addtabbeside/
Having added our script and overlay files, we now need to add a couple of metadata files to help Firefox understand and install/load our extension.
The first is addtabbeside/content.manifest, which is a Chrome Manifest. This file helps Firefox translate between chrome:// URIs and actual files on disk. It also allows us to specify where our overlay will be merged into the browser:
# Chrome package addtabbeside has it's content in ./chrome/content content addtabbeside chrome/content/ # Overlay the browser.xul file with overlay.xul overlay chrome://browser/content/browser.xul chrome://addtabbeside/content/overlay.xul
The first line registers the location for our content (i.e., .xul, .js). The second line registers our overlay, and says that overlay.xul will be merged with browser.xul. Mozilla uses chrome:// URIs to refer to aspects of the interface, and chrome://browser/content/browser.xul is the browser (try typing it into the address bar).
The second metadata file we need to create is addtabbeside/install.rdf. This is an install manifest that tells the Firefox add-on manager how to install the extension, with information like who wrote it, the version number, compatibility information, etc.
<?xml version="1.0" encoding="UTF-8"?> <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"> <Description about="urn:mozilla:install-manifest"> <em:id>addtabbeside@senecac.on.ca</em:id> <em:name>Add Tab Beside</em:name> <em:version>0.1</em:version> <em:creator>David Humphrey</em:creator> <em:description>New tabs are created beside the current tab instead of at the end of the tab list.</em:description> <em:targetApplication> <Description> <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- firefox --> <em:minVersion>2.0</em:minVersion> <em:maxVersion>3.0a3pre</em:maxVersion> <!-- trunk build Feb 27, 2007 --> </Description> </em:targetApplication> </Description> </RDF>
Testing the extension
Eventually we'll package our extension properly into a redistributable .xpi. However, while we're testing it's nice to be able to use it in an expanded form so we can make changes.
To this end, create a file named addtabbeside@senecac.on.ca and put it in your development profile's extensions directory (NOTE: replace Username with your username and dev-profile with your development profile name):
C:\Documents and Settings\Username\Application Data\Mozilla\Firefox\Profiles\dev-profile\extensions\addtabbeside@senecac.on.ca
This file should contain a single line of text--the full path to your extension, for example:
C:\temp\addtabbeside
Start your browser and make sure your extension is loaded (check Tools > Add-ons) and working properly by creating some tabs to see where they get positioned. If you're having problems, check the Error Console (Tools > Error Console) for hints.
Packaging the extension
TODO
Success, and some bugs
The rewrite to an extension has been a success. In both cases we've managed to achieve the same goal using almost completely different methods. Using an extension we've made it possible to share our changes with any Firefox user worldwide without having to ship a custom build.
However, as was the case in our previous attempt, our code has a bug. Moving existing tabs doesn't update our position state, since we only modify mPreviousIndex when a new tab is selected; moved tabs remain selected, but change their order (i.e., TabSelect won't get dispatched on a move).
Luckily we've already stumbled upon the solution to this problem--the TabMove event. Here is the updated version of overlay.js, with the changes in bold:
var AddTabBeside = { // State info on the last tab to be selected. mPreviousIndex: 0, onLoad: function() { // Add a listener for the TabOpen event, which gets called as // part of addTab in tabbrowser.xml var container = gBrowser.tabContainer; container.addEventListener("TabOpen", this.onTabOpen, false); // Also add a listener for TabSelect so we know when focus changes to a new tab container.addEventListener("TabSelect", this.onTabSelect, false); // And a listener for TabMove to fix an edge case container.addEventListener("TabMove", this.onTabSelect, false); // Finally, add a listener for shutdown window.addEventListener("unload", this.onUnload, false); }, onUnload: function() { // Remove our listeners var container = gBrowser.tabContainer; container.removeEventListener("TabOpen", this.onTabOpen, false); container.removeEventListener("TabSelect", this.onTabSelect, false); container.removeEventListener("TabMove", this.onTabSelect, false); }, onTabSelect: function (e) { // When a different tab is selected, remember which one. This is // necessary because when a new tab is created, it will get pushed // to the end of the list, but we need to know where to put it. this.mPreviousIndex = gBrowser.tabContainer.selectedIndex; }, onTabOpen: function (e) { // Get the newly created tab, which will be last in the list var newTab = e.target; // Move this new tab to the right of the previously selected tab, // checking to see how many tabs there are currently. By default // there is 1 tab, and the first time onTabOpen is called, there will // be 2 (the default plus the newly created tab). In this case, don't // move the new tab, since it is already in the right spot. In all // other cases, move the tab to the right of the current tab. if (gBrowser.tabContainer.childNodes.length > 2) { gBrowser.moveTabTo(newTab, this.mPreviousIndex + 1); } }, }; // Insure that our code gets loaded at start-up window.addEventListener("load", function(e) { AddTabBeside.onLoad(e); }, false);
Reflections
The process of developing extensions is greatly improved with the addition of some settings to the browser, as well as the Extension Developer's extension. This extension will automate many of the steps described here. It will also allow you to reload the browser's chrome at runtime. This is a great way to test changes to your XUL/JS files without having to restart the browser.
You can read complete details on how to set-up an extension developer's environment here.