667
edits
Changes
no edit summary
=Introduction=
In this walkthrough, we will explore the process of making a small change to the behaviour of Firefox. In so doing, we'll learn how to go from an idea for a top-level UI feature change, to searching and studying the code, to making and testing this change.
The goal of this exercise is to give you confidence in order to try similar things on your own, expose you to some helpful techniques, and to highlight the importance of using existing code in open source and Mozilla.
=The 'What': change the way new tabs get created in Firefox=
Currently, when you create a new tab in Firefox, it is appended to the end of the list of currently opened tabs. However, often one opens a tab in order to work on something associated with the current tab, that is, if I'm working in tab 6, I'd like the new tab to be placed at position 7 and not 21 (assuming there are 20 tabs open).
Here are the steps to reproduce this (commonly shortened to '''STR''', especially in [https://bugzilla.mozilla.org bugzilla):
# Start Firefox and open a series of tabs (i.e., CTRL+T)
# Move to the first tab
# Open another new tab and notice its placement in the list (i.e., it should be last)
=The 'Where': finding the right spot to make the changes=
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":
http://lxr.mozilla.org/seamonkey/search?string=New+Tab
Lots of results, many of which point to comments in the code. However, the first result looks interesting:
http://lxr.mozilla.org/seamonkey/source/toolkit/locales/en-US/chrome/global/tabbrowser.dtd#2
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:
<!ENTITY newTab.label "New Tab">
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==
Repeating the search with the '''newTab.label''' ENTITY value instead of the "New Tab" string makes a big difference--we have many fewer hits:
http://lxr.mozilla.org/seamonkey/search?string=newTab.label
Not surprisingly, the first result is the same DTD file (i.e., tabbrowser.dtd) we already found. The second result looks interesting, though:
http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#80
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):
<pre>
<xul:menuitem label="&newTab.label;" accesskey="&newTab.accesskey;" xbl:inherits="oncommand=onnewtab"/>
</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==
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 function. All we have to do now is find that function:
http://lxr.mozilla.org/seamonkey/search?string=onnewtab
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:
http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser.xul#503
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:
http://lxr.mozilla.org/seamonkey/search?string=function+browseropentab%28
Again, we're interested in Firefox (i.e., browser) instead of SeaMonkey (i.e., suite), so we skip to the second result:
http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser.js#1802
This shows us that we need to be looking for yet another function, '''loadOneTab()'''. Another search:
http://lxr.mozilla.org/seamonkey/search?string=loadonetab
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!
http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#1160
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.
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:
$ 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==
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==
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==
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 (e.g., "what happens if I am on the last tab and not in the middle?").
=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:
--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.
In this walkthrough, we will explore the process of making a small change to the behaviour of Firefox. In so doing, we'll learn how to go from an idea for a top-level UI feature change, to searching and studying the code, to making and testing this change.
The goal of this exercise is to give you confidence in order to try similar things on your own, expose you to some helpful techniques, and to highlight the importance of using existing code in open source and Mozilla.
=The 'What': change the way new tabs get created in Firefox=
Currently, when you create a new tab in Firefox, it is appended to the end of the list of currently opened tabs. However, often one opens a tab in order to work on something associated with the current tab, that is, if I'm working in tab 6, I'd like the new tab to be placed at position 7 and not 21 (assuming there are 20 tabs open).
Here are the steps to reproduce this (commonly shortened to '''STR''', especially in [https://bugzilla.mozilla.org bugzilla):
# Start Firefox and open a series of tabs (i.e., CTRL+T)
# Move to the first tab
# Open another new tab and notice its placement in the list (i.e., it should be last)
=The 'Where': finding the right spot to make the changes=
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":
http://lxr.mozilla.org/seamonkey/search?string=New+Tab
Lots of results, many of which point to comments in the code. However, the first result looks interesting:
http://lxr.mozilla.org/seamonkey/source/toolkit/locales/en-US/chrome/global/tabbrowser.dtd#2
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:
<!ENTITY newTab.label "New Tab">
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==
Repeating the search with the '''newTab.label''' ENTITY value instead of the "New Tab" string makes a big difference--we have many fewer hits:
http://lxr.mozilla.org/seamonkey/search?string=newTab.label
Not surprisingly, the first result is the same DTD file (i.e., tabbrowser.dtd) we already found. The second result looks interesting, though:
http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#80
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):
<pre>
<xul:menuitem label="&newTab.label;" accesskey="&newTab.accesskey;" xbl:inherits="oncommand=onnewtab"/>
</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==
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 function. All we have to do now is find that function:
http://lxr.mozilla.org/seamonkey/search?string=onnewtab
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:
http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser.xul#503
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:
http://lxr.mozilla.org/seamonkey/search?string=function+browseropentab%28
Again, we're interested in Firefox (i.e., browser) instead of SeaMonkey (i.e., suite), so we skip to the second result:
http://lxr.mozilla.org/seamonkey/source/browser/base/content/browser.js#1802
This shows us that we need to be looking for yet another function, '''loadOneTab()'''. Another search:
http://lxr.mozilla.org/seamonkey/search?string=loadonetab
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!
http://lxr.mozilla.org/seamonkey/source/toolkit/content/widgets/tabbrowser.xml#1160
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.
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:
$ 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==
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==
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==
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 (e.g., "what happens if I am on the last tab and not in the middle?").
=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:
--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.