Education/Learning/ModifyBrowser
Contents
Introduction
In this walkthrough, we will explore the process of making a small change to the behaviour of Firefox. The change we will make will be done directly in the Firefox source code (note: this change can also be done as an extension, which we will do in another walkthrough). 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.
Getting the Source
All sample code listed in this walkthrough was taken from mozilla-central revision f4800de50e03. Later changes to Firefox have changed these files. So, to follow along on your computer, download the correct revision of the source:
- hg clone http://hg.mozilla.org/mozilla-central/
- hg update f4800de50e03
- (If you have trouble building, as per bug 478871, try 4aed53dcf692 instead)
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 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 MXR's Text Search feature. Here are the results you get when you search for "New Tab":
http://mxr.mozilla.org/mozilla-central/search?string=New+Tab
Lots of results, many of which point to comments in the code. However, the tabbrowser.dtd result looks interesting:
<!ENTITY newTab.label "New Tab">
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 here)
Looking closely at tabbrowser.dtd we see that our English string, "New Tab", uses newTab.label.
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 fewer hits:
http://mxr.mozilla.org/mozilla-central/search?string=newTab.label
Not surprisingly, the first result is the same DTD file (i.e., tabbrowser.dtd) we already found. The result in tabbrowser.xml looks interesting, though:
<xul:menuitem id="context_newTab" label="&newTab.label;" accesskey="&newTab.accesskey;" xbl:inherits="oncommand=onnewtab"/>
Here we see the code to define the pop-up context menu for a tab (i.e., what you get when you right-click on a tab in the browser)
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 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://mxr.mozilla.org/mozilla-central/search?string=onnewtab
This returns many results for things we aren't interested in, including files rooted in /db. Since we are interested in finding this behaviour in Firefox, we need to focus on the files rooted in /browser. One looks particularly interesting: browser.xul
onnewtab="BrowserOpenTab();"
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 custo 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://mxr.mozilla.org/mozilla-central/search?string=function%20BrowserOpenTab(
Now we've drilled down to the point where we have only one result, the function itself in browser.js:
function BrowserOpenTab() { if (!gBrowser) { // If there are no open browser windows, open a new one window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no", "about:blank"); return; } gBrowser.loadOneTab("about:blank", null, null, null, false, false); if (gURLBar) gURLBar.focus(); }
This shows us that we need to be looking for yet another function, loadOneTab(). Another search:
http://mxr.mozilla.org/mozilla-central/search?string=loadOneTab
The first result is not surprising, and we're back to the tabbrowser widget. The code we really want is in tabbrowser.xml:
<method name="loadOneTab"> <parameter name="aURI"/> <parameter name="aReferrerURI"/> <parameter name="aCharset"/> <parameter name="aPostData"/> <parameter name="aLoadInBackground"/> <parameter name="aAllowThirdPartyFixup"/> <body> <![CDATA[ var bgLoad = (aLoadInBackground != null) ? aLoadInBackground : this.mPrefs.getBoolPref("browser.tabs.loadInBackground"); var owner = bgLoad ? null : this.selectedTab; var tab = this.addTab(aURI, aReferrerURI, aCharset, aPostData, owner, aAllowThirdPartyFixup); if (!bgLoad) this.selectedTab = tab; return tab; ]]> </body> </method>
You might be wondering why the code is contained within an XML file. The tabbrowser code is creating a reusable widget, and its definition and functionality are being defined in place. Firefox will use tabbrowser in order to get its tabbed browser support. Methods such as loadOneTab are part of this widget, and are defined here.
We're still not done our search, however. 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 guess that its definition will be contained in the same file. Searching within the file reveals we are correct, and finally we've found the right spot
this.mTabContainer.appendChild(t);
Now all that we have to do is modify it to insert rather than append at the end of the container.
The 'How': the necessary changes to the code
There are different ways you could go about making this change. Here, we are modifying code that is new to us, and there may be a better way to achieve the same outcome--this is what code reviews tell us. However, for purposes of learning, we will continue to work as seems best to us (don't be afraid to experiment and try things). Along the way we will make some mistakes, and discuss how to identify and correct them.
First Attempt
The goal is to make as small a change as possible, since the existing code works well--we just want it to work slightly different. We're also not interested in reading all of the code in order to make such a small change. We want to leverage as much of what is already there as we can, and modify as little as possible.
We assume that the appendChild() method is responsible for the behaviour we want to change, namely, adding new tabs to the end of the list. We're not sure what to replace it with, so we do another search inside tabbrowser.xml (i.e., using CTRL+F) looking for other methods/attributes of mTabContainer. We 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;
We decide that we can probably accomplish our goal using these alone, and so start working on a solution. Here is a first attempt, showing the changes to browser/base/content/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); }
We then need to rebuild so that our changes get packaged in the browser's .jar file (change objdir to your objdir name):
$ make -C objdir/browser
Next, run your newly built browser to test, using -no-remote (if you have another version of Firefox running) and -profilemanager (in order to create/use a test profile). If you're on Windows and your source tree is located at C:\mozilla-central\src, you would do:
$ export XPCOM_DEBUG_BREAK=warn
This will stop assertions from popping-up a dialog box in debug builds on Windows.
$ C:\mozilla-central\src\objdir\dist\bin\firefox.exe -profilemanager -no-remote
Obviously adjust your path so you get to your objdir.
What is the outcome of our first attempt? We try to create a new tab using File > New Tab and nothing happens.
Second Attempt
Clearly this code has some problems, since we've completely broken addTab. Let's look for clues in the Error Console (Tools > Error Console). Notice the following exception appear whenever we try to add a new tab:
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]
Now we know how to find errors our JavaScript produces. The line number in the source file may be larger than the one in the error message (download chrome://global/content/bindings/tabbrowser.xml in your new copy of Firefox to see the line numbers the browser is using). Since the error is in the else clause, it's clear that that childNodes.length is not zero, but 1 by default (i.e., every window contains at least one tab, even if the tab controls are not visible). A quick modification to the code, and we get:
if (this.mTabContainer.childNodes.length == 1) { ...
Third Attempt
This works, but only the first time I create a new tab. Clearly we still have some misconceptions about how mTabContainer.selectedIndex and mTabContainer.insertBefore() really work.
We can't yet see how our code is wrong, but the exception clearly indicates that we've got some sort of type conversion problem. We decide to look again at the code examples in tabbrowser.xml where these methods and properties get used, specifically insertChild().
After a few seconds the error is obvious: we'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
After rebuilding and running the browser, we're able to confirm that this last change has been successful. Opening a new tab now works in the way we originally described. We make a few more tests to insure that we haven't broken anything else, for example, what happens if you are on the last tab and not in the middle. This works, which says that using append() is probably not necessary at all, and we can safely shorten our 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 we've added.
Speaking of bugs, a closer look at addTab 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;
This will break tab deletion, among other things, since the positions of newly created tabs will be wrong internally. 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, but that doesn't mean we're bug free either. Having someone test your altered browser may reveal things we've missed.
Reflections
The change we made was simple enough that we didn't bother looking at any documentation or using the JavaScript debugger. We could have consulted the excellent documentation for tabbrowser.
We learned an important technique here: looking at existing code is the best way to write new code. This helped us pick-up the style of Mozilla's code, learn where things are located, find method calls and properties we could reuse, etc. The value of Mozilla's platform is not just that it is extensive and powerful, but also that it is open and free for us to study.