Hide and show JTable columns, pt. 2

Now that we’re done with the menu, let’s make it work. The simplest test I can think of clicks on a menu item and checks that the number of visible columns has decreased:

I’ve changed getMenuItems to accept JTable instead of JPopupMenu to make the code less verbose. This test obviously fails as the menu does nothing yet. On one hand, the fix is not so simple. We need to add appropriate listeners to the items. On the other hand, something as silly as this will do the trick:

Okay, this is really silly. But our test passes now, and that means the problem is with our test. Why not use a data provider to parameterize our test?

This is better. Now it fails again. If you’re wondering about DP_COLUMN_INDEXES, it’s just a constant set to “columnIndexes”. Why bother? Because I don’t like hardcoding function names. What if I need to rename it later? Like this, I can name the provider function anything I want. Of course, it’s better to rename and change the constant as well, just to keep it consistent. But that’s just three changes: function name, constant name and the constant literal itself. And even if I forget to touch the constant, nothing breaks.

OK, so now the fix looks like this:

But it’s still obviously wrong. Well, maybe not so obviously, but remember that Swing has two coordinate systems: the view and the model. Columns can be rearranged in the view, so the mapping can change, even though it’s the identity by default. So we need yet another test. But first, we need to decide whether we want the menu items to rearrange when the columns are rearranged. I think not. First of all, it’s too much of a hassle. And it can be confusing for the user too. Besides, the menu contains all columns, and the view only some of them. Where do we put the hidden columns in the menu if they are not a part of the view order? So let’s keep them in the model order.

Now I have refactored setup code into a separate method. I haven’t annotated it with @BeforeMethod because it’s not a universal test fixture: the install method checks that the menu is not null, so obviously we can’t use it even before the test is started. I’ve introduced an @AfterMethod cleanup method, though (not shown), that just sets everything to null, just to be 100% sure that one test can’t possibly use something created by another.

Now to the fix:

I actually got it wrong on the first try—put the conversion call outside the listener. That froze vIndex forever, which is obviously not what we want. Note that this one of the cases where Hungarian notation is tremendously useful: both model and view indexes have the same type, so Hungarian notation lets us see immediately what kind of index we’re looking at.

Now I don’t really like the looks of the install method. And we haven’t even got to showing hidden columns. So let’s refactor it a little bit.

This is much better. More methods, the code is much longer, but each method is pretty clean and readable. Note that I’ve created a field for the JTable. It was captured by the lambda anyway, so I haven’t actually introduced any new state here. Just moved it from the lambda to the top-level class.

Now let’s check that the columns are properly shown (which, of course, they aren’t).

This fails, but with a very obscure message: java.lang.ArrayIndexOutOfBoundsException: -1. Why? Because the second click is trying to hide an already hidden column. Can we do something about this message? Well, not by modifying the testing code. But we can modify the hideColumn method! What should it do when the column is already hidden? Nothing? Or throw an exception? And should we even bother at all? Probably not at this point. Later on, we may want to make that method public or add some other API to hide and show columns programmatically. Then we’ll have to solve this problem. For now, let’s bear with the obscure message and fix the class.

The change is not trivial. I had to add a hash map of removed columns so we can show them later. The key is the model index.

The action listener became a bit large, so I should either refactor it into an inner class or make it smaller. It’s not large enough to justify a class, so I’ll opt for another private method:

This is better.

We got it almost working! One last obvious thing is that we append the column to the end when we show it. This is not good. But where should we put it? The order of columns could have changed since we hid it. There is no perfect solution, so I’ll choose a simple one: put it where it belongs if the columns are not rearranged, otherwise put it somewhere reasonable. With this vague requirement we only need to test that the column reappears in its place if we keep the model order, and that it reappears at all otherwise.

Let’s start with the simple case.

And the fix is:

Now this can easily break if the model index is not a valid view index. How? Well, imagine that we removed some other columns and the column count is now very low. If the column removed was somewhere near the end, its model index may be very well outside the bounds now. Let’s test it.

The fix:

At this point I felt like this thing is going to work now. So I ran the demo I created last time. But as I tried to hide a column I got this:

The funny thing is, there are no our methods in this trace! That shows clear enough that TDD is not a magic silver bullet. Even though from our tests we expected that our code should work fine at least under normal circumstances, it crashes immediately. Why? After investigating a little bit, I think it’s because of a subtle bug in Swing. When we right-click on a column to show the menu, it thinks we’re about to drag that column. Indeed, dragging with right mouse button works, sort of. Sometimes it leaves the column floating in mid-drag. Then, after the column is removed, dragging breaks because the column has no valid view index (hence the index out of bounds exception).

There are different possible workarounds. We could try to install a custom table header that would override the getDraggedColumn method and return null if the column is hidden. But that would prevent users from using their own header. Of course, we could wrap the existing header into our own. But that would require delegating all of its methods to the wrapped instance, and there is a lot of them.

Another possible way is to consume the right click to prevent it from dragging anything. Alas, the default event handler is the first in the line. By the time we consume the event, it’s too late.

A really silly way is to just set dragged column to null whenever we hide it. It’s so simple and stupid it might actually work. Let’s try it.

Yay! It works, and with no visible glitches too.

At this point I’d like to conclude this self-educating tutorial. Of course, there is a lot of things still to be done, such as: provide a way to uninstall it, check what happens if we install it twice on the same table or try to reinstall on another, provide an API to show and hide columns from code, prevent the user from hiding the last visible column (or the header will disappear and it will be impossible to get them back). These are just the ones I can think of right off the bat.

For those interested to improve it or study the full history of its evolution, the code is available at

https://github.com/stachenov/jtable-column-selector

The point at which this tutorial ends is tagged tutorial-pt2.

One thought on “Hide and show JTable columns, pt. 2

Leave a Reply