Difference between revisions of "Dive into Mozilla Modifying Firefox using an Extension Lab"

From CDOT Wiki
Jump to: navigation, search
(Planning the extension)
Line 118: Line 118:
 
Finally, we've got all the pieces in place and can write some code.
 
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 to have it get executed at the right times.  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 [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 have the code to accomplish this get called in a sort of global event listener, the '''window's load event'''.  We'll add code to remove our event listeners in the '''window's unload event''' too.
  
Question: does moving a tab break our tab position state code?  Do we also need to update position on TabMove???
+
To keep things clean we'll write all this code in a custom object called '''AddTabBeside'''.  Here is '''AddTabBeside''':
 
 
 
 
 
 
 
 
Start by creating a new extension, either by hand, or using Ted Mielczarek's wonderful wizard:
 
 
 
http://ted.mielczarek.org/code/mozilla/extensionwiz/
 
 
 
 
 
Directory structure:
 
 
 
'''addtabbeside/'''
 
  content.manifest
 
  install.rdf
 
  '''content/'''
 
    firefoxOverlay.xul
 
    overlay.js  
 
 
 
 
 
Here is the '''content.manifest''' file:
 
 
 
content addtabbeside content/
 
overlay chrome://browser/content/browser.xul chrome://addtabbeside/content/firefoxOverlay.xul
 
 
 
 
 
 
 
 
 
Here is the '''install.rdf''' file for the extension:
 
 
 
<pre>
 
<?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>
 
</pre>
 
 
 
 
 
Here is firefoxOverlay.xul:
 
 
 
<pre>
 
<?xml version="1.0" encoding="UTF-8"?>
 
  <overlay id="addtabbeside-overlay"
 
            xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
  <script src="overlay.js"/>
 
</overlay>
 
</pre>
 
 
 
 
 
Load Listener code:
 
 
 
http://developer.mozilla.org/en/docs/Extension_Frequently_Asked_Questions#Why_doesn.27t_my_script_run_properly.3F
 
 
 
 
 
 
 
Reading through the rest of the '''moveTab''' method, we see that when a tab is successfully created, the '''TabOpen''' event is dispatched
 
 
 
(see ...)
 
 
 
// 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;
 
 
 
This is useful information, because it tells us that if we want to know when a tab has been created, we have to add a listener for this event.
 
 
 
 
 
moveTabTo
 
 
 
http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#1958
 
 
 
 
 
Create a file named: '''addtabbeside@senecac.on.ca'''
 
 
 
This file should contain the full path to your extension, for example:
 
 
 
C:\temp\addtabbeside
 
 
 
Now put this file 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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
'''overlay.js'''
 
  
 
  var AddTabBeside = {
 
  var AddTabBeside = {
Line 247: Line 146:
 
    
 
    
 
   onUnload: function() {
 
   onUnload: function() {
 +
    // Remove our listeners
 
     var container = gBrowser.tabContainer;
 
     var container = gBrowser.tabContainer;
 
     container.removeEventListener("TabOpen", this.onTabOpen, false);
 
     container.removeEventListener("TabOpen", this.onTabOpen, false);
Line 253: Line 153:
 
    
 
    
 
   onTabSelect: function (e) {
 
   onTabSelect: function (e) {
     // when a different tab is selected, remember which one.  This is
+
     // When a different tab is selected, remember which one.  This is
 
     // necessary because when a new tab is created, it will get pushed
 
     // 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.
 
     // to the end of the list, but we need to know where to put it.
Line 275: Line 175:
 
  };
 
  };
 
   
 
   
 +
// Insure that our code gets loaded at start-up
 
  window.addEventListener("load", function(e) { AddTabBeside.onLoad(e); }, false);
 
  window.addEventListener("load", function(e) { AddTabBeside.onLoad(e); }, false);
  
Line 285: Line 186:
  
  
 +
Question: does moving a tab break our tab position state code?  Do we also need to update position on TabMove???
  
  
  
It's one thing to say you'd like to change the browser's behaviour, but quite another to actually do it.  The change you have in mind might be quite simple, in the end (ours is).  But you still have to figure out where that simple code needs to go.  That can be difficult.  However, difficult isn't the same as impossible.
 
  
How do you begin?  First, let's start at the top and find some UI notation we can search for in the code.  In our case, we can focus on the various methods for creating a new tab:
 
  
* CTRL+T
 
* Right-Click an existing tab and select New Tab
 
* File > New Tab
 
  
The second and third methods are useful, as they provide us with a unique string we can search for in the code.  Before we can change anything, we have to search and read existing code in order to understand where to begin--this is the standard pattern for open source and Mozilla development.
 
  
==Search 1 - finding a UI string==
 
  
We're looking for a unique string--"New Tab"==, so we'll use [http://lxr.mozilla.org LXR's] '''Text Search''' feature.  Here are the results you get when you search for "New Tab":
 
  
<blockquote>http://lxr.mozilla.org/seamonkey/search?string=New+Tab</blockquote>
 
  
Lots of results, many of which point to comments in the code.  However, the first result looks interesting:
+
Start by creating a new extension, either by hand, or using Ted Mielczarek's wonderful wizard:  
  
<blockquote>http://lxr.mozilla.org/seamonkey/source/toolkit/locales/en-US/chrome/global/tabbrowser.dtd#2</blockquote>
+
http://ted.mielczarek.org/code/mozilla/extensionwiz/
  
Here we see the DTD file describing the key/value pairs for the en-US localized strings.  Mozilla uses this technique to allow localizers to translate strings in an application into many different languages without having to change hard-coded strings in the code (you can read more about localization, DTDs, and Entities [http://developer.mozilla.org/en/docs/XUL_Tutorial:Localization here]) 
 
  
Looking closely at '''tabbrowser.dtd''' we see that our English string, "New Tab", has the following entity:
+
Directory structure:
  
  <!ENTITY newTab.label          "New Tab">
+
  '''addtabbeside/'''
 +
  content.manifest
 +
  install.rdf
 +
  '''content/'''
 +
    firefoxOverlay.xul
 +
    overlay.js  
  
This is good information, because it allows us to repeat our search with an entity instead of a string, which should help us get closer to the code we're after.
 
  
==Search 2 - finding an ENTITY==
+
Here is the '''content.manifest''' file:
  
Repeating the search with the '''newTab.label''' ENTITY value instead of the "New Tab" string makes a big difference--we have many fewer hits:
+
content addtabbeside content/
 +
overlay chrome://browser/content/browser.xul chrome://addtabbeside/content/firefoxOverlay.xul
  
<blockquote>http://lxr.mozilla.org/seamonkey/search?string=newTab.label</blockquote>
 
  
Not surprisingly, the first result is the same DTD file (i.e., tabbrowser.dtd) we already found.  The second result looks interesting, though:
 
  
<blockquote>http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#80</blockquote>
 
  
Here we see the code to generate the pop-up context menu for a tab (i.e., what you get when you right-click on a tab in the browser):
+
Here is the '''install.rdf''' file for the extension:
  
<pre>
+
<pre>
  <xul:menuitem label="&newTab.label;" accesskey="&newTab.accesskey;"
+
  <?xml version="1.0" encoding="UTF-8"?>
              xbl:inherits="oncommand=onnewtab"/>
+
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  </pre>
+
  <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>
 +
</pre>
  
Having found the appropriate entity value, we also notice the use of a function name, '''onnewtab'''.  This line of code says that the xul:menuitem will inherit the '''oncommand''' value from its parent (you can read more about XBL attribute inheritance [http://developer.mozilla.org/en/docs/XUL_Tutorial:XBL_Attribute_Inheritance here]).  In other words, when this menu item is clicked, call the '''onnewtab''' function.
 
  
==Search 3 - finding a Function==
+
Here is firefoxOverlay.xul:
  
Armed with this new information, we are even closer to finding the right spot to begin working. We've gone from UI string to XML ENTITY to functionAll we have to do now is find that function:
+
<pre>
 +
<?xml version="1.0" encoding="UTF-8"?>
 +
  <overlay id="addtabbeside-overlay"
 +
            xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 +
  <script src="overlay.js"/>
 +
  </overlay>
 +
</pre>
  
<blockquote>http://lxr.mozilla.org/seamonkey/search?string=onnewtab</blockquote>
 
  
This returns many results for things we aren't interested in, including files rooted in /suite, /db, etc.  Since we are interested in finding this behaviour in Firefox, we need to focus on the files rooted in '''/browser'''.  One looks particularly interesting:
 
  
<blockquote>http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser.xul#503</blockquote>
+
Create a file named: '''addtabbeside@senecac.on.ca'''
  
In this case, the tabbrowser widget has the onnewtab property set to another function, '''BrowserOpenTab();''' (i.e., Firefox seems to handle tab creation in a non-standard way, providing its own method instead of using the default).  Since we want to find the definition of this function, we search for '''"function BrowserOpenTab("''', which returns two results:
+
This file should contain the full path to your extension, for example:
  
<blockquote>http://lxr.mozilla.org/seamonkey/search?string=function+browseropentab%28</blockquote>
+
C:\temp\addtabbeside
  
Again, we're interested in Firefox (i.e., browser) instead of SeaMonkey (i.e., suite), so we skip to the second result:
+
Now put this file in your development profile's extensions directory (NOTE: replace ''Username'' with your username and ''dev-profile'' with your development profile name):
  
<blockquote>http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser.js#1802</blockquote>
+
C:\Documents and Settings\''Username''\Application Data\Mozilla\Firefox\Profiles\''dev-profile''\extensions
 
 
This shows us that we need to be looking for yet another function, '''loadOneTab()'''.  Another search:
 
  
<blockquote>http://lxr.mozilla.org/seamonkey/search?string=loadonetab</blockquote>
 
  
The first result is not surprising, and we're back to the tabbrowser widget.  The '''loadOneTab''' method calls another method to actually create and insert the new tab:
 
  
var tab = this.addTab(aURI, aReferrerURI, aCharset, aPostData, owner, aAllowThirdPartyFixup);
 
  
Since '''addTab''' is a method of '''this''' we can search within the current document (CTRL+F) to find the '''addTab''' method.  Finally we've found the right spot!
 
  
<blockquote>http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#1160</blockquote>
 
  
this.mTabContainer.appendChild(t);
 
  
Now all that we have to do is modify it to insert rather than append.
 
  
=The 'How': the necessary changes to the code=
 
  
There are different ways you could go about making this change, and someone with more experience using tabbrowser might recommend a different strategy or outcome.  I decided to work on something that I knew nothing about in order to highlight the process one goes through, or at least the process I went through, when working with someone else's code.  Since my goal is to show you how to do this, I also discuss my errors and mistakes below--they are an important part of the process too.
 
  
==First Attempt==
 
  
The goal is to make as small a change as possible, since the existing code works well--I just want it to work slightly different.  I'm also not interested in reading all of the code in order to make such a small change.  I want to leverage as much of what is already there as I can.
 
  
I assume that the '''appendChild()''' method is responsible for the behaviour I don't like (i.e., adding new tabs to the end of the list).  I'm not sure what to replace it with, so I do another search inside tabbrowser.xml (i.e., using CTRL+F) looking for other methods/attributes of '''mTabContainer'''.  I come-up with some interesting options:
 
  
index = this.mTabContainer.selectedIndex;
 
...
 
this.mTabContainer.insertBefore(aTab, this.mTabContainer.childNodes.item(aIndex));
 
...
 
var position = this.mTabContainer.childNodes.length-1;
 
  
I decide that I can probably accomplish my goal using these alone, and so start working on a solution.  Here is my first attempt, showing the changes to '''mozilla/toolkit/content/widgets/tabbrowser.xml''' and the '''addTab''' method:
 
  
// Insert tab after current tab, not at end.
+
==First Attempt==
if (this.mTabContainer.childNodes.length == 0) {
 
this.mTabContainer.appendChild(t);
 
} else {
 
var currentTabIndex = this.mTabContainer.selectedIndex;
 
  this.mTabContainer.insertBefore(t, currentTabIndex + 1);
 
}
 
 
 
I then repackage the toolkit.jar file (change ''objdir'' to your objdir name):
 
 
 
$ cd mozilla/''objdir''/toolkit/content
 
$ make
 
 
 
then run the browser to test (NOTE: ''minefield'' is my testing profile):
 
 
 
$ ../../dist/bin/firefox.exe -p minefield --no-remote
 
 
 
I try to create a new tab using '''File > New Tab''' and nothing happens.
 
  
 
==Second Attempt==
 
==Second Attempt==
 
Clearly my code has some problems, since I've completely broken addTab.  I decide to look for clues in the '''Error Console''' (Tools > Error Console) and notice the following exception whenever I try to add a new tab:
 
 
<code>Error: uncaught exception: [Exception... "Could not convert JavaScript argument"  nsresult: "0x80570009 (NS_ERROR_XPC_BAD_CONVERT_JS)"  location: "JS frame :: chrome://global/content/bindings/tabbrowser.xml :: addTab :: line 1161"  data: no]</code>
 
 
I make a guess that childNodes.length is not zero, but 1 by default (i.e., there is always at least one tab, even if it isn't visible).  A quick modification to the code, and I test again:
 
 
if (this.mTabContainer.childNodes.length '''== 1''') {
 
...
 
  
 
==Third Attempt==
 
==Third Attempt==
  
This works, but only the first time I create a new tab.  Clearly I still have some misconceptions about how '''mTabContainer.selectedIndex''' and '''mTabContainer.insertBefore()''' really work.
 
 
I can't yet see how my code is wrong, but the exception I'm getting clearly indicates that I've got some sort of type conversion problem.  I decide to look again at the code examples in tabbrowser.xml that I'm using as a guide, specifically '''insertChild()'''.
 
 
After a few seconds the error is obvious: I've used an Integer where a Tab was required.  Here is the corrected code:
 
 
// Insert tab after current tab, not at end.
 
if (this.mTabContainer.childNodes.length == 1) {
 
  this.mTabContainer.appendChild(t);
 
} else {
 
  var currentTabIndex = this.mTabContainer.selectedIndex;
 
  this.mTabContainer.insertBefore(t, '''this.mTabContainer.childNodes.item(currentTabIndex + 1)''');
 
}
 
  
 
==Success, and some bugs==
 
==Success, and some bugs==
  
After repackaging the toolkit.jar file and running the browser, I'm able to confirm that this last change has been successful.  Opening a new tab now works in the way I originally described.  I make a few more tests to insure that I haven't broken anything else, for example, what happens if I am on the last tab and not in the middle.  This works, which makes me realize that using '''append()''' is probably not necessary at all, and I can safely shorten my code down to the following:
 
 
// Insert tab after current tab, not at end.
 
var currentTabIndex = this.mTabContainer.selectedIndex;
 
this.mTabContainer.insertBefore(t, this.mTabContainer.childNodes.item(currentTabIndex + 1));
 
 
This means that six lines of code become two, and with that reduction in number of lines, hopefully a reduction in new bugs I've added (NOTE: within reason, favour fewer rather than more lines of code).
 
 
Speaking of bugs, a closer read of '''addTab''' (see [http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#1219 line 1219]) would indicate that we've introduced a few with our new positioning code:
 
 
// wire up a progress listener for the new browser object.
 
var position = '''this.mTabContainer.childNodes.length-1;'''
 
var tabListener = this.mTabProgressListener(t, b, blank);
 
...
 
this.mTabListeners[position] = tabListener;
 
this.mTabFilters[position] = filter;
 
...
 
t._tPos = position;
 
 
Where the assumption before was that the newly created tab was at the end of the list, the new code breaks that.  Therefore, we also need to update the value of '''position'''
 
 
// wire up a progress listener for the new browser object.
 
var position = currentTabIndex + 1
 
 
No other obvious defects are visible from our changes.
 
  
 
=Reflections=
 
=Reflections=
 
The change I was making was simple enough that I didn't bother looking at any documentation or using the JavaScript debugger.  I found out afterward that tabbrowser has good  [http://developer.mozilla.org/en/docs/XUL:tabbrowser documentation on MDC].
 
 
Another trick worth trying when you're making lots of JavaScript changes like this is to add the following line to your .mozconfig file:
 
 
ac_add_options --enable-chrome-format=flat
 
 
This will cause the .jar files to be expanded so that you can edit the .xml/.js/.xul files in place and skip the repackaging step above (see http://www.mozilla.org/build/jar-packaging.html).  If you also use the [http://ted.mielczarek.org/code/mozilla/extensiondev/index.html Extension Developer's extension] you can reload the chrome without restarting the browser.
 
  
  
Line 476: Line 296:
 
* [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 11:56, 14 March 2007

Dive into Mozilla > Dive into Mozilla Day 5 > Modifying Firefox using an Extension Lab


...in progress...


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 it's 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 users an extension to install, 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. 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, so we need 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 this:

// 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 t (i.e., 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 code for tabbrowser 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 to have it get executed at the right times. 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 have the code to accomplish this get called in a sort of global event listener, the window's load event. We'll add code to remove our event listeners in the window's unload event too.

To keep things clean we'll write all this code in a custom object called AddTabBeside. Here is AddTabBeside:

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);





Question: does moving a tab break our tab position state code? Do we also need to update position on TabMove???






Start by creating a new extension, either by hand, or using Ted Mielczarek's wonderful wizard:

http://ted.mielczarek.org/code/mozilla/extensionwiz/


Directory structure:

addtabbeside/
  content.manifest
  install.rdf
  content/
    firefoxOverlay.xul
    overlay.js  


Here is the content.manifest file:

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



Here is the install.rdf file for the extension:

 <?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>


Here is firefoxOverlay.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="overlay.js"/>
 </overlay>


Create a file named: addtabbeside@senecac.on.ca

This file should contain the full path to your extension, for example:

C:\temp\addtabbeside

Now put this file 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








First Attempt

Second Attempt

Third Attempt

Success, and some bugs

Reflections

Resources