Creating a Custom Column

One of the new and exciting features available to Thunderbird Extension developers in Thunderbird 2.0 is the ability to easily create and handle custom columns in Thunderbird's main view.
The following step through will get you well on the road to creating your own columns and populating them with custom data.

Getting Started

In this example we will be developing a small extension that will be adding a column that will display the "Reply-To:" field of an email (if it exists, it if often not set). If you are unfamiliar with the setup and creation of an extension please read Building a Thunderbird Extension.

Adding A Column

By far the easiest part is the physical addition of the column. For this we overlay messenger.xul, by placing the following line in our chrome.manifest file:

overlay chrome://messenger/content/messenger.xul chrome://replyto_col/content/replyto_col_overlay.xul

Now that our overlay is set up we need to connect a column to the current columns that exist. Looking in messenger.xul reveals that the columns reside inside a tree with the id "threadTree" whose columns (treecols) reside in "threadCols". To attach our column we add another treecol tag ("colReplyTo") with the settings that we would like to use. Don't forget to add in a splitter before the column to ensure that the user can easily resize and drag our column around.

Our replyto_col_overlay.xul should now contain something similar to this:

<?xml version="1.0"?>
<overlay id="sample"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <tree id="threadTree">
    <treecols id="threadCols">
    <splitter class="tree-splitter" />
    <treecol id="colReplyTo" persist="hidden ordinal width"
           currentView="unthreaded" flex="2"
           label="Reply-To" tooltiptext="Click to sort by the Reply-To header" />
    </treecols>
  </tree>

  <!-- include our javascript file -->
  <script type="text/javascript" src="chrome://replyto_col/content/replyto_col.js"/> 
</overlay>

That's it! Our new column exists and is ready to be used. Unfortunately the new column doesn't have any data in it and it doesn't react to sort clicks on it... yet.

The Column Handler

Now that our column physically exists it's time to give it a bit of personality.

The nsIMsgCustomColumnHandler Interface

To handle a column we need to implement the nsIMsgCustomColumnHandler interface which defines the basic set of functionality that must be implemented to handle a column. Note that an object that implements this interface can also take control of an existing, built-in column, but more about that later.

nsIMsgCustomColumnHandler extends (inherits from) nsITreeView and as such needs to implement functions from both. The following is a list of functions that must be implemented to ensure that your extension works well with Thunderbird. Rest assured that these functions will be called - so implement them, even with empty function.

From nsITreeView:

  • getCellProperties(row, col, props): optionally modify the props array
  • getRowProperties(row, props): optionally modify the props array
  • getImageSrc(row, col): return a string (or null)
  • getCellText(row, col): return a string representing the actual text to display in the column

From nsIMsgCustomColumnHandler:

  • getSortStringForRow(hdr): return the string value that the column will be sorted by
  • getSortLongForRow(hdr): return the long value that the column will be sorted by
  • isString(): return true / false

Warning! Do not get confused between GetCellText() and GetSortString/LongForRow()! Though they sound similar you may not want to return the same value from both. GetCellText() is the text that is displayed to the user while GetSort*ForRow() is what is used internally when sorting by your column

A Simple Implementation

Objects in Javascript are just "advanced" variables, so an implementation of the nsIMsgCustomColumnHandler interface looks like:

var columnHandler = {
   getCellText:         function(row, col) {
      //get the message's header so that we can extract the reply to field
      var hdr = gDBView.getMsgHdrAt(row);
      return hdr.getStringProperty("replyTo");
   },
   getSortStringForRow: function(hdr) {return hdr.getStringProperty("replyTo");},
   isString:            function() {return true;},

   getCellProperties:   function(row, col, props){},
   getRowProperties:    function(row, props){},
   getImageSrc:         function(row, col) {return null;},
   getSortLongForRow:   function(hdr) {return 0;}
}

Basically, all we are doing here is making sure that both the text that is displayed to the user (getCellText()) and the string we sort according to (when the user decided to sort the view by our custom column) are identical.

Putting the Pieces together

We now have our column and our handler object - it's time to put everything together. For this we use a new function, addColumnHandler that takes a column ID and a handler object. Our implementation will need to call something similar to:

gDBView.addColumnHandler("colReplyTo", columnHandler);

Setting the Custom Column Handler

We've added a special event that gets fired whenever a view is created. When that event fires, you should set up the custom column handler on the new view.

window.addEventListener("load", doOnceLoaded, false);

function doOnceLoaded() {
  var ObserverService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
  ObserverService.addObserver(CreateDbObserver, "MsgCreateDBView", false);
}

var CreateDbObserver = {
  // Components.interfaces.nsIObserver
  observe: function(aMsgFolder, aTopic, aData)
  {
     addCustomColumnHandler();
  }
}


In this example we have a function addCustomColumnHandler() that is called whenever the event is fired. This function is a simple one liner along the lines of:

function addCustomColumnHandler() {
   gDBView.addColumnHandler("colReplyTo", columnHandler);
}

Wrap Up

As you have seen it is quite easy to add your own custom column handler. Most of the dirty tasks (displaying and sorting) are done for you in the background - so that all that's left to you, the Extension developer is to populate the code with your feature.

Don't forget that columns aren't restricted to text - they can also load images (via getImageSrc()) so that your extension can display a new visual cue to the user.

As you develop your custom extension, please revisit this article and add any helpful hints that you find along the way!

Caveats

Some weird behaviors have been witnessed, namely failing to be notified of the MsgCreateDBView event when Enigmail is installed [protz]. What happens is:

  • usually, the event sequence is: jsm load -> onload -> MsgCreateDBView
  • with Enigmail, the event sequence becomes jsm load -> MsgCreateDBView -> onload

To workaround this, you can either i) manually run addCustomHandler() at the end of your onload handler or ii) register CreateDbObserver when your .jsm is loaded. Some extensions (e.g. rkent's junquilla) use i).