Scaffolding template tags for Django forms

We love Django here at Isotoma, and we love using Django’s awesome form classes to generate self-generating, self-validating, [X]HTML forms.

However, in practically every new Django project I find myself doing the same thing over and over again (and I know others do too): breaking the display of a Django form instance up into individual fields, with appropriate mark-up wrappers.

Effectively I keep recreating the output of BaseForm.as_p/as_ul/as_table with template tags and mark-up.

For example, outputting a login form, rather than doing:

{{ form.as_p }}

We would do:

<p>
{% if form.username.errors %}
  {% for error in form.username.errors %}
    {{ error }}
  {% endfor %}
{% endif %}
{{ form.username.label }} {{ form.username }}
</p>
<p>
{% if form.password.errors %}
  {% for error in form.password.errors %}
    {{ error }}
  {% endfor %}
{% endif %}
{{ form.password.label }} {{ form.password }}
</p>

Why would you want to do this? There are several reasons, but generally it’s to apply custom mark-up to a particular element (notice I said mark-up, not styling, that can be done with the generated field IDs), as well as completely customising the output of the form (using <div>‘s instead etc.), and also because some designers tend to prefer this way of looking at a template.

“But”, you might say, “Django already creates all this for us with the handy as_p/as_ul/as_table methods, can you just take the ouput from that?”
Well, yes, in fact on a project a couple of weeks ago that’s exactly what I did, outputting as_p in a template, and then editing the source chucked out in a browser.
Which gave me the idea to create a simple little tool to do this for me, but with the Django template tags for dynamically outputting the field labels and fields themselves.

I created django-form-scaffold to do just this, and now I can do this from a Python shell:

>>> from dfs import scaffold
>>> from MyProject.MyApp.forms import MyForm
>>> form = MyForm()
>>> # We can pass either an instance of our form class
>>> # or the class itself, but better to pass an instance.
>>> print scaffold.as_p(form)

{% if form.email.errors %}{% for error in form.email.errors %}
{{ error }}{% endfor %}{% endif %}
<p>{{ form.email.label }} {{ form.email }}</p>
{% if form.password1.errors %}{% for error in form.password1.errors %}
{{ error }}{% endfor %}{% endif %}
<p&gtl{{ form.password1.label }} {{ form.password1 }}</p>
{% if form.password2.errors %}{% for error in form.password2.errors %}
{{ error }}{% endfor %}{% endif %}
<p>{{ form.password2.label }} {{ form.password2 }}</p>

Copy and paste this into a template, tweak, and Robert’s your mother’s brother.

As well as as_p(), the dfs.scaffold module also has the equivalent functions as_ul(), as_table, and an extra as_div() function.

Annoying CSS3 Baseline Alignment Problem in Firefox

CSS3 transform enables the rotation of elements including HTML text. If you intend to use it you should be aware that Firefox 3.6.8 and below has very poor baseline alignment.

The heading is just about acceptable. Content text is not.

Firefox 3.6.8:

Webkit:

So be warned if you intend to use CSS transform on text.

CSS3 Flexbox

CSS3 brings us a host of new features. Whilst most people will be familiar with gradients, text-shadow etc. I’d like to draw your attention to one of the lesser known modules in CSS3: flexbox. What’s interesting about Flexbox is that you can use it now if you use a Gecko or Webkit based browser (no IE sadly and Opera’s support isn’t there yet).

What does Flexbox bring us that we didn’t have before? Well it gives us a whole raft of new ways of controlling layout and flow. What we now achieve with floats we can do far more successfully and with more control with Flexbox.

I’ve never liked floats, they always seemed a hacky way of structuring HTML. Unfortunately with the meager positional CSS available, floats ended up being the best way of creating flexible fluid layouts. Let’s look at some examples.

display: box

To define an element as a flexbox we set the display to box.

.flexbox {
display: -moz-box;
display: -webkit-box;
display: box;
}

Example 1

box-orient

Notice that by default flexbox elements are aligned horizontally. If we want to change that to vertical:

.rule {
-moz-box-orient: vertical;
-webkit-box-orient: vertical;
box-orient: vertical;
}

Example 2

Box-orient is inherited i.e. child elements will also be aligned horizontally.

Example 3

Observant viewer will have noticed that Firefox (3.6.8) ignores the width and height setting on the child boxes, whilst Webkit respects it but overflows the content.

So if you want child elements to be aligned vertically, you’ll need to set the containing element to box-orient:vertical.

box-ordinal-group

We can reorder elements as we like, using box-ordinal-group.

#box-2 {
-moz-box-ordinal-group: 1;
-webkit-box-ordinal-group: 1;
box-ordinal-group: 1;
}

Example 4

The immediate application of this should be obvious, for the first time we have way of defining display order without reverting to all the hacks we’ve had to use up to now (floats, relative positioning, negative margins etc). Powerful stuff.

box-flex

Box-flex allow the content to expand to fill the available space.

.rule {
-moz-box-flex: 1;
-webkit-box-flex: 1;
box-flex: 1;
}

example 5

Notice once again the difference between Webkit and Gecko; whilst Webkit expands the main block to the browser window, Gecko expands to fit the content.

Box-flex fills space in proportion to the value given. Quoting the specs:

“All flex is relative. For example, a child with a box-flex of 2 is twice as flexible as a child with a box-flex of 1.”

To explain this better lets look at an example:

#box-1 {
box-flex: 1;
}
#box-2 {
box-flex: 2;
}
#box-3 {
box-flex: 7;
}

Example 6

For ease of calculation I’ve given the H1 child element a width of 50px, and the containing element a width of 600px. So the available space is:
600px – 150px = 450px

Working out the ratio is:
1 + 2 + 7 = 10

450px / 10 = 45px

this produces:

box 1 width: 50px + (45px * 1) = 95px
box 2 width: 50px + (45px * 2) = 120px
box 3 width: 50px + (45px * 7 ) = 365px

Further more you can set relative spacing whilst having fixed width paddings and margins. If you tried that with floats you’d either have to use percentage margins or have boxes bouncing all over the place.

box-align

Box-align works in the opposite direction to the orient setting. i.e. if the element is set to horizontal, box-align controls alignment in the vertical and vica versa.

#main {
-moz-box-align: stretch;
-webkit-box-align: stretch;
box-align: stretch;
}
#box-1 {
-moz-box-align: start;
-webkit-box-align: start;
box-align: start;
}
#box-2 {
-moz-box-align: center;
-webkit-box-align: center;
box-align: center;
}
#box-3 {
-moz-box-align: end;
-webkit-box-align: end;
box-align: end;
}

example 7

box-direction

This reverses the order of elements.

#main {
-moz-box-direction: reverse;
-webkit-box-direction: reverse;
box-direction: reverse;
}

Example 8

Notice the inconsistency between Gecko and Webkit again. Gecko aligns the right hand column, whilst Webkit aligns to left but reorders the elements.

box-pack

Box-pack controls alignment in the direction set by orient.

#main {
-moz-box-pack: justify;
-webkit-box-pack: justify;
box-pack: justify;
}
#box-1 {
-moz-box-pack: center;
-webkit-box-pack: center;
box-pack: center;
}
#box-2 {
-moz-box-pack: start;
-webkit-box-pack: start;
box-pack: start;
}
#box-3 {
-moz-box-pack: end;
-webkit-box-pack: end;
box-pack: end;
}

example 9

Justify is a nice attribute, equally spacing elements within the parent container.

Final Example

example 10

The classic 3 column layout. Notice the navigation appearing first, also note that the background colour extends to the full height of all boxes.

Sadly flexbox is not supported by IE and looks unlikely to be supported by IE9. So it will be a while before these techniques become mainstream.

Querying Webtrends ODBC from the command line with WebtrendsQT

As I alluded to yesterday, and in my post about SQLAWebtrends, I’ve recently been doing a lot of work with the Webtrends analytics service, concerned mostly with getting data out of it via the old Windows ODBC drivers.

While turn around on new data available from reports could cause Methuselah to yawn, it could still be exceedingly time consuming loading up a spreadsheet app, defining queries in an ODBC query builder, and waiting for data to populate sheets; or at best writing several Python functions to query the last data; I would still have to spend tedious amounts of time tweaking and re-tweaking queries for different reports and/or datasets.

This lead me to make WebtrendsQT, a psql/mysql-like command line query tool for Webtrends using pyODBC.

WebtrendsQT is mostly just the ODBC extra tool provided by pyODBC, with some WT-specific changes. Namely the introduction of a “\p” command, which issues the {Call wtGetProfileList()} stored procedure against the WTSystem schema (via the system_cursor property), returning a list of profiles.
Similarly do_l (the handler for “\l”) instead of listing real schemas, lists the Webtrends ODBC equivalent templates.

do_c (“\c”) will work as you’d expect, taking a “schema” (e.g. template), and changing cursor to point to it, but also takes profile GUID as an optional first option to switch both profile and template (profiles define the data source and which report templates are available).

It took me some time to figure out that PyODBC‘s lovely columns() method wouldn’t work with the Webtrends driver, as some metadata isn’t provided by the driver and causes a segfault. Instead my hack is to use the DB API Cursor.description to get name and type details for columns on a table, unfortunately in order to get this information I need a cursor that specifically targets the table in question; and to get around this I make a simple query against the table that won’t return any information, but will still return a cursor:

@memoized()
def get_columns(self, name):
    columns = [['Column name', 'Type', 'Size',]]
    row = self.cursor.execute(
        'SELECT * FROM %s LIMIT 0' % (name,)
    ).fetchone()
    for r in row.cursor_description:
        columns.append(
            [r[0],
            self.db_types[r[1]],
            r[3],]
        )
    return columns

cursor_description is PyODBC’s special “always available even after query-set has been closed” reference to the cursor.description instance.

Unlike pyDBCLI.extras.odbc, WebtrendsQT takes a set of arguments rather than a single DSN string, due to the ODBC driver requiring a specific set of details to connect.

You most likely just want to install and run the tool under Windows, which if you have any experience with Python on Windows should be easy enough using easy_install or the included setup.py; if however you don’t have any Python-Windows experience and just want to get up and running with WebtrendsQT, the FAQ has a 5 step simple guide, including a pre-rolled pair of Windows scripts, that will install everything and create a batch script with all the Python paths set up to use.
When installed just type wtqt in the cmd.exe Window, provided by the batch script, and away you go.

C:\Users\test\Desktop> wtqt

ERROR: Must have a profile GUID, -p

Usage: wtqt.py [-u <user>] [-p <pass>] -d <system DSN> -h <host> [-P <port>] -t <template> -p <profile>

Options:
  -d, --systemdsn: Predefined system DSN
  -p, --profile : Webtrends profile GUID
  -t, --template : Template/schema
  -h, --host : Webtrends web instance
  -P, --port : Optional server port (default: 80)
  -u, --username: Optional username
  -k, --password: Optional password

Writing interactive command line DB query tools with pyDBCLI

While some people can’t navigate a relational database without reaching for a GUI, the vast majority of us spend a good proportion of our lives inside interactive command line interfaces, such as psql or mysql.

What do you do, however, if there isn’t a CLI query tool available for the DB you’re working with?
I had this exact same problem recently with a project, the main reason for the lack of any such tooling is because I wasn’t actually dealing with a real DB at all, but an ODBC interface to a web service that exposed reporting data as if it were tables in a DB. Rather than spend the rest of my life messing around with queries in a hooked up spreadsheet, or repeatedly writing one off Python snippets, I decided to write my own psql-like tool.

Thus the first version of pyDBCLI was born; well not really, my first tool really only handled querying Webtrends (I’ll save that for another post), while pyDBCLI is a base class for making such tools as long as you have a DB API compatible cursor to query.
pyDBCLI is based on the fantastic cmd.Cmd, so you can extend pretty much exactly as you would would Cmd, except with some extra properties such as cursor and multi_prompt are provided.

In order to make a Cmd based tool behave more like psql I ended up overriding the parseline method with regular expressions to handle escaped commands such as “\d” and “\c”, fuzzing them if not escaped, or un-escaping them if escaped so that Cmd’s unmodified parseline method can handle parsing and dispatching to defined do_* methods.

The other main change was modifying the default method to dispatch command lines to the a query method for querying against the DB API cursor; as well as this default and several other commands will detect an unfinished SQL query and wait for a finishing character (“;” by default) before sending it (or doing anything) else. This is what the multi_prompt property is for, the prompt property is replaced with multi_prompt when a query spans more than one line, and is set back again when the query is finished and executed.

2 example tools are bundled with pyDBCLI, in the extras package:

  • odbc – a tool to query an ODBC exposed data source, using PyODBC; takes PyODBC compatible DSN strings.
  • litecli – a tool to query a SQLite database; SQLite has it’s own CLI tool to do this, which is very well rounded and much better than litecli, but this is provided as a fairly functional example tool.

Tomorrow I’ll discuss the original reason I whipped up pyDBCLI: creating a tool for querying Webtrends, via ODBC, quickly and with more ease.

Squid and Facebook

Lenny (Debian 5.0) and Lucid (Ubuntu 10.04 LTS) both ship with Squid 2.7 as their default Squid version.  If you plan on deploying squid as a web proxy in a modern mixed browser environment you should seriously consider Squid 3.0.  If you can’t (or, like me, you’d already spent some time getting Squid 2.7 set up just right) you are likely to find users reporting that Facebook is returning blank pages or that links within Facebook don’t work.  Looking at Facebook in a browser with debugging tools installed you’ll see lots of cross domain Javascript errors.

At this point you should either upgrade to Squid 3 (which doesn’t exhibit this behaviour) or make the following change to your squid.conf:

  • Add server_http11 on

Facebook uses different methods of shipping the Javascript depending on whether the client has made an HTTP/1.0 or an HTTP/1.1 request.  By default Squid 2.7 alters the request to be HTTP/1.0, so Facebook sends it’s fallback methods to the modern browser, which then proceeds to break.  The change above stops Squid rewriting that part of the request, ensuring that Facebook ships the right content to your users.

Hacking Trac 0.12

We use Trac at Isotoma. Actually that’s a bit of an understatement, we don’t just use it, it’s integral to our project development process.

Trac offers a wiki, ticketing system and svn browser out of the box, but it’s the increasingly featureful plugins API that allows you to adapt, hack and install to fit your purpose.

Home grown tweaks

We’re hopping a couple of versions and upgrading to 0.12 in the next few weeks and with it being a system that everyone uses daily, there’s been more than a few enhancement requests for the new installation.

Here are a couple of the plugins that made it through the requirements gathering process:

Isotoma Theme

Of course natively you can tweak the static content and templates to your hearts content. The TracThemeEngine plugin wraps this up into a neater package so you can customise your install to add extra markup, follow your own style, or even make it look like Alta Vista circa 1997. We plumped for following our own style.

HotKeys Plugin

My muscle memory often causes an involuntary “ctrl+s” when editing wiki entries. One too many save dialogs prompted this plugin which maps scripts to keyboard shortcuts using the JQuery HotKeys plugin (the version of jquery.hotkeys is from John Boxall’s fork on github, which allows inputs and textareas to be bound).

QuickTicket Plugin

RTM like input for quickly adding tickets.

As a side note, this plugin also hijacks a keyboard shortcut (“ctrl+q”  - used by Firefox, amongst others, for “quit”). An abominable accessibility crime to be sure, but we’re in the rare position here that we are our target audience.

WikiTable Macro

Adapted from work by optilude on Trac-Hacks, this macro allows SQL to be included in wiki pages to show query results in a table. We added DSN mappings that you add to trac.ini allowing you to query databases other than the current instance’s.

Third party plugins from Trac-Hacks

The invaluable Trac-Hacks should be the first port of call for looking into expanding on Trac’s functionality. For reference, here are a few of the plugins we’ll be implementing come the roll out:

Batch Modify Plugin

Allows users to modify several tickets together in one shot.

Timing and Estimation Plugin

For estimation and time tracking. We’re using the permissions branch.

Git Plugin

Essential. Enables Git support for the versioning system backend.

Further reading

The documentation on the Edgewall’s Trac site is pretty comprehensive:
http://trac.edgewall.org/wiki/TracDev/PluginDevelopment

For help on the Genshi template language and content parser, again, you could do worse than looking at the documentation:
http://genshi.edgewall.org/

Join the mailing list here:
http://groups.google.com/group/trac-dev/

And finally, 9 times out of 10, someone will have written a plugin for it and submitted it to Trac-Hacks:
http://trac-hacks.org/

Querying Webtrends analytics via ODBC with SQLAlchemy

Webtrends is a web traffic analytics package, similar to Google Analytics. Recently we had the requirement of being able to pull data out of reports on a Webtrends instance.
Luckily enough they have a nice RESTful data extraction API; not so luckily it is only available for Webtrends Analytics 9 instances, while we were limited in our requirements to Webtrends 8.

Prior to Webtrends 9 the official data extraction method is a Windows-only ODBC driver, primarily used for connecting Excel spreadsheets and Microsoft ADO applications. The driver provides a pseudo-relational-database interface to pre-made reports on a Webtrends instance, which you can query using a simple SQL-subset.
Notice I use qualifiers like “pseudo” and “interface”, that’s because what’s really going on in the background is the driver makes an HTTP call (with details such as the SQL being sent) to a web service on the Webtrends instance, and the web service returns some binary data representing data in the queried report, which the driver then returns as a table. The reports themselves aren’t actually real tables in a real database, although I’m sure this is how they’re represented somewhere in the Webtrends system, what we get back is a set of aggregated data normally used to display pretty graphs and bar charts in the web interface.

To make life easier for us, as we were already using SQLAlchemy for querying PostgreSQL tables, and we would need to mock our Webtrends data at some point, it made sense to be able to use SQLAlchemy for all the data objects; with that in mind I made the SQLAWebtrends dialect.
After installing SQLAWebtrends you can create ORM classes matching the Webtrends reports you want to query, and then, using a specially formatted DSN, run queries against them as you would any other DB.

Considerations in making a dialect for Webtrends:

  • Nearly all the special features of SQLAlchemy from full unicode support, to field binding and various row counting hacks, need to be disabled as the feature-set provided by the Windows ODBC driver are extremely limited.
  • Method for getting meta-data such as table names and columns needed to be overridden and done using Microsoft ADO compatible method.
  • Some combination of PyODBC’s column pre-binding and the Webtrends driver’s complete lack of features means any attempt at binding will fail, so being executing queries I needed to “unbind” them, replacing ? placeholders with actual data, and relying on inbuilt filtering method to provide any sort of field escaping/filtering.
  • LIMIT clauses are also extremely, erm, limited for want of better words. So that needed overriding too.
  • Finally, SQLAlchemy likes to wrap every value, include names, in quotes; Webtrends doesn’t like this, and quite frankly borks, so we disable this functionality too.
  • Bonus point: PyODBC rocks.

Caveats for use:

  • Your SQLAlchemy models for Webtrends reports shouldn’t include primary key columns, because frankly there probably aren’t any unique primary keys in the reports, but this doesn’t matter as SQLAlchemy won’t care as long as Webtrends doesn’t complain (which it won’t).
  • As with any other SQLAlchemy model you can call properties in the ORM class anything you like, but the underlying table column names need to match up to the column names in the Webtrends report.
  • The ODBC driver and web service don’t support JOINs, so you can’t use these with your ORM models either.
  • The iterator wrapper around the PyODBC cursor instance returned by queries will only ever returns one row unless you call .yield_per(1) on the query-set. I haven’t had time to figure out why this is the case, but I suspect it’s something to do with row pre-buffering, which is disabled as a consequence of yield_per.
  • Every now and again you’ll see rows with lots of blank values in them, except that any number values (measures in Webtrends) will be higher. If you look closely these are actually sums of all the values following it up until the next row of seemingly blank data. These are aggregated summary rows, displaying sums of the data for that particular subset of data (depending on which field is the “dimension” for the report, a sort of primary key used in creating reports). Unless you’re after this data as well, I find the best thing is to just do a quick check for so many blank fields and skip these rows.

Example using models and running a query:

from sqlalchemy.orm import mapper, sessionmaker
from sqlalchemy import create_engine
from sqlalchemy import String, MetaData, Column, Table

# You would probably setup your username, password
# host etc. here, including the profile and template
# you want to query.

# Create the DB connection
engine = create_engine(
    "webtrends+pyodbc://%s:%s@%s:80/%s?dsn=Webtrends&profile_guid=%s" %
        {user, password, host, template, profile}
)
metadata = MetaData(bind=engine)
Session = sessionmaker(bind=engine)
session = Session()

# Table schema
wt_user_report = Table('UsersByPages', metadata,
    Column('User', String, nullable=True),
    Column('PagesURLs', String, nullable=True),
    Column('PageHits', String, nullable=True),
    Column('TimePeriod', String, nullable=True),
    Column('StartDate', String, nullable=True),
    Column('EndDate', String, nullable=True)
)

# ORM class
class WTUserReport(object):
    pass
mapper(WTUserReport, wt_user_report)

# Create a query
query = session.query(WTUserReport).filter(
    TimePeriod="2010.m06.d22"
).yield_per(1) # Remember we need this for the iterator to work

# Iterate over the query-set and print some columns
for r in query:
    print "User %s hit %s %s times" % (
        r.User,
        r.PagesURLs,
        r.PageHits,
)

Generating sample unicode values for testing

When writing tests, there’s one thing we’re now sure to do because it’s caught us out so many times before: use unicode everywhere, particularly in Python.

Often, people will just go to Wikipedia and paste bits of unicode chosen at random into sample values, but that doesn’t always make for good readability when the tests you forgot to comment break two months later and you have to revisit them.

I’ve found a simple way to generate test unicode values that make sense, is to use an upside-down text generator, or some other l33t text transformer which produces unicode. Using text detailing whatever the sample value is supposed to represent, it’s still pretty legible at a glance and you’ll hopefully flag up those pesky
UnicodeDecode

errors quicker.

There’s a handy list of different text transformations and websites that will perform them for you on Wikipedia.

Solving the real Alt-Tab problem

In his latest blog post, Aza Raskin – interface design guru, creative lead on Firefox, and son of one of my heroes, Jef Raskin – tackles one of my oldest bugbears, Alt-Tab. Aza is a clever guy, but I was disappointed that his post addressed an issue I don’t perceive as important, while failing to address what I see as the very real problems with Alt-Tab. But let me start with some background.

(I use both Windows and Mac every day, and in this post tend to use Alt-Tab and Cmd-Tab interchangeably.)

(EDIT: I originally gave the window-switching shortcut as Cmd-\ since I use an external keyboard, but I probably confused Mac users who know it as Cmd-~ (tilde). Updated.)

History (something Windows got right)

Windows had Alt-Tab since Windows 1.0, although it was only implemented in its familiar visual form since Windows 3.1 (1992). To this day, I know many people who never use the shortcut, but I personally cannot imagine using a multi-tasking operating system without it.

I found its initial absence on Apple Macs unacceptable. In Mac-based studios during the 90s, I always relied on a third-party extension, Task Switcher, to provide the missing functionality.

When Apple finally introduced native Cmd-Tab (in OS 8.5, I think), they at first got it wrong. It cycled alphabetically through running apps, rather than switching between apps on a most-recently-used (MRU) basis like Windows1, so I had to continue using the extension. They changed it to MRU order in a later OS update. (Thank goodness Microsoft didn’t think to patent it.)

Unfortunately, Macs still suffer from another difference which I’ll come to after the following interlude.

Interlude: Why is most-recently-used (MRU) order better than cycling (and Exposé)?

The first reason is obvious: If Alt-Tab simply cycled through open windows in a fixed sequence (say, alphabetically), it would just require far too much tabbing, on average, to reach the item you wanted.

But Raskin alludes to the more powerful reason: spatial memory. If the shortcut always switches to the most-recently-used (MRU) item, this quickly teaches you to make the switch without thinking or looking at the screen. Spatial memory is awesome because it’s a background faculty, not foreground. Like reaching for your mouse, it does not interrupt your concentration, or where you’re looking on the screen.

This “toggle” behaviour, using a single shortcut key to switch back and forth between two windows only, is worth mentioning as an important feature in itself. It allows the shortcut to be used to compare the contents of two windows, not simply to switch from one to another. (The Undo/Redo shortcut in Photoshop – Cmd-Z for both – also brilliantly uses this principle.)

So, MRU order allows you to switch between two tasks pretty much subconsciously. Personally, I have learned to switch between up to 3 apps without relying on the visual aid (i.e. using spatial memory alone). For switching between more than that I need to look at the interface, but MRU ordering still reduces the number of times I need to Tab.

The trouble with Macs

Cmd-Tab on the Mac works less well than on Windows, due to the Mac’s application-centric model, as opposed to the document-centric model of Windows. In Windows, for example, you can Alt-Tab between two emails, or two browser windows; on the Mac you can’t. To switch between application windows on the Mac you have to use a different shortcut, Cmd-~, which again uses cycling rather than MRU order. On top of that Cmd-Tab on the Mac has the annoying habit of bringing all an application’s windows to the foreground, often covering up the window you were trying to compare against.

I believe window-switching is closer to the mental model of what this shortcut accomplishes. The clue is in the original feature name: “Task Switcher”. It switches between “things I am doing” – I do not care about which applications they happen to be in. So to see it as an “application switcher” is to miss the point.

It’s also my theory that the deficiencies of task-switching on the Mac spurred the development of Exposé, which I consider a sticking-plaster solution. Exposé is nice, but it does not utilise spatial memory – it forces you to look at the interface.

I admit this is debatable: some people (to my amazement) find application switching on the Mac more natural than window-switching on Windows.

The real problem

As more and more applications adopt a tabbed workspace, Alt-Tab is becoming less useful regardless of which operating system you use. And this is especially serious with browsers, because more and more of our daily tasks happen in browsers nowadays. You’re no longer just “browsing”. Just as often you’re composing documents, managing your calendar, filing bug reports, etc. I often find myself automatically attempting to Alt-Tab between two things I’m doing, but failing because they happen to be in two separate Firefox tabs. And then I have to use the mouse.

In Windows I can improve the situation slightly by opening more browser windows. That way I can use a single window, say, for Google Calendar, one for GMail, one for the blog post I’m writing, a multi-tab one with lots of things I’m reading, etc. On OS X I can’t, since application windows can only be cycled through using Cmd-(Shift)-~.

The keyboard shortcut for switching tabs is usually Ctrl-Tab (on both Mac and Windows), but again, this cycles rather than using MRU. Interestingly, there are a few applications who opted to use MRU with Ctrl-Tab, e.g. oXygen (my favourite HTML editor.) I appreciate it enormously when using the application. Weirdly, Firefox occasionally seems to do this (uses MRU rather than cycling), but this is unreliable and I cannot get it to do so now.

Aza Raskin’s proposal

I’m disappointed that Raskin – evidently a lifelong Mac user – implicitly accepts “application-switching” as the point of the shortcut. As I have tried to explain above, this is fundamentally less useful than task-switching.

In his article he attempts to come up with an improvement to the shortcomings of MRU. (He doesn’t even mention cycling so I assume he is not in favour of it.) MRU’s shortcoming, Raskin says, is that it is only useful for toggling between two things (he says apps, I’d say windows), and frustrates your tendency to form spatial memory habits for more than that. Personally I don’t experience the problem he describes with juggling 3 apps. It’s hard-wired in my spatial memory that Cmd-Tab switches to the last thing, and Cmd-Tab-Tab switches to the last-but-one. For more than this I need to shift my attention to the switcher interface, and spatial memory is no longer of help. But this is infrequent enough not to matter.

He then proposes a “habit-respecting MRU” (HRMRU) to solve this problem I don’t perceive. He ponders using heuristics or even a Markov model to detect users’ habits. Personally I see this failing for the very reasons he himself described – it would just result in a seemingly capricious interface.

But the bigger problem I have with Raskin’s article is that he doesn’t address the real erosion in the usefulness of this shortcut: The loss of MRU due to tabs, and the co-existence of both MRU-ordered switching and cycling. (And the greater problem on Mac OS by having 3 switching modes: Apps, windows and tabs, two of which don’t use MRU.)

My proposal

Tabbed interfaces are not going away. They’re a necessary way of managing the ever-increasing number of windows we have to juggle. If I had to Alt-Tab between all of them, it could number over a 100. So I think two switching modes are inevitable.

So I propose that only two shortcuts are necessary: Alt-Tab / Cmd-Tab for window-switching, and Ctrl-Tab for tab-switching inside a window. (Or document-switching in applications that don’t use tabs.) Both should work exactly the same way: MRU order, with the addition of Shift to reverse the order. There is no reason for application-switching to exist.

This would be a minor change on Windows, but a fundamental one on the Mac. Perhaps, as OS X insists on having 3 switching modes with 3 different shortcut keys, they could at least redefine the second one – Cmd-~ – to be window-switching across all apps (i.e. like Windows) rather than within the current app only. And all should use MRU.

Some people may find MRU order in a tabbed interface confusing, and crave a keyboard shortcut that cycles instead, but then a different application-specific shortcut could always be provided. E.g. Firefox already has Cmd-Alt-Left/Right arrow (Mac) or Ctrl-PgUp/PgDn (Windows). These are more appropriate shortcuts for cycling as their names imply directionality.

  1. Windows actually uses Z-Order, but in practice this generally works like MRU. The behaviour was slightly changed in Vista, but the 6 most recent windows still uses MRU order.