Wednesday, December 20, 2006

componentMoved and componentResized on JFrame

While trying to store the state of an applications I found some oddity's when you add a ComponentListener to a JFrame like so:



frame.addComponentListener(new ComponentListener() {
public void componentResized(ComponentEvent e) {
// do something like save the size in preferences
System.out.println("RESIZED");

}

public void componentMoved(ComponentEvent e) {
// do something like save the location in preferences
System.out.println("MOVED");
}

public void componentShown(ComponentEvent e) {

}

public void componentHidden(ComponentEvent e) {

}
});


Now if you resize the frame, you will see RESIZE printed to the console only a single time after you release the mouse. If you move the frame, you will see MOVE printed repeatedly, many times over. Move gets fired as you are dragging. So the problem here is that you may only want to perform the action after the move is completed. You might think that you could add a MouseListener to the frame and catch the mouseReleased event, but the mouse events are not fired on the window's title bar unfortunately so that does not work.

A Swing developer asked about this in an Ask the Experts Transcript, and I quote the response from the AWT Technical Lead, Oleg Sukhodolsky:


This is not a bug. Such behavior was introduced to compensate for some
performance problems we had in the past. Every resize event for a top level
causes relay outing. This operation could be very expensive. The good news for
you is that this is controllable behavior -- you can either use the
Toolkit.setDynamicLayout() method or set the awt.dynamicLayoutSupported Java
system property to true.
Soooo, how do you get around this? Well one idea is to use a javax.swing.Timer to make the action occur every couple of seconds while you are dragging and it will only fire once after you've stopped dragging.


public class WindowPrefsListener implements ComponentListener {
private Timer timer;
private JFrame frame;
private int counter;
private static final int DELAY = 2000;

public WindowPrefsListener(JFrame frame) {
this.frame = frame;
timer = new Timer(DELAY, new AbstractAction() {
public void actionPerformed(ActionEvent e) {
System.out.println("timer action " + counter++ + "!");
savePrefs();
}
});
timer.setRepeats(false);
}

private void savePrefs() {
Prefs.saveSize(frame);
Prefs.saveLocation(frame);

}

/**
* compoment resized actually works normally with a single event after the mouse is released,
* but supposedly it might behave differently on different platforms.
*
* @param e
*/
public void componentResized(ComponentEvent e) {
timer.start();

}

public void componentMoved(ComponentEvent e) {
timer.start();
}

public void componentShown(ComponentEvent e) {

}

public void componentHidden(ComponentEvent e) {

}
}
And that's that. Try it out, you'll see that the event gets fired at most once every two seconds (since the DELAY is set to two seconds).

Closing Tabs with Swing

So I've been trying to find an elegant way to close tabs in Swing and it turns out to be pretty tough. Not tough to implement, but tough to do it elegantly.

It's very common these days to have an X on the tab or on the tab bar like the following screenshots from your favorite web browsers:

Firefox 1.X


Firefox 2.X


Internet Explorer 7.X


But this isn't supported in Swing because you can currently only have text, an icon, or both. You can't put arbitrary Component's into a tab. There are some workarounds (hacks?) out there, but they are not pretty.

This isn't really the end of the world because many applications make do without it and you barely even notice that they don't have the X. Possibly because they limit the number of tabs that will be visible at a time. Intellij Idea is a good example of this. You can only close tabs by right clicking on the tab or using a keyboard shortcut. But since it's limited to something like 10 tabs by default (configurable in your settings), the tabs never get out of hand you don't really need to close them.

Intellij Idea


How to Close Tabs with a Right Click Context Menu


So here's a quick tip on how to do it with a right click context menu.

Make a class that implements MouseListener and ActionListener

public class CloseTabListener implements MouseListener, ActionListener {

Make a JPopupMenu:

popup = new JPopupMenu();
JMenuItem menuItem = new JMenuItem("Close Tab");
menuItem.addActionListener(this);
popup.add(menuItem);

Implement mousePressed and mouseReleased like so:

public void mousePressed(MouseEvent e) {
showPopup(e);
}
private void showPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
source = e.getSource();
clickPoint = e.getPoint();
popup.show(e.getComponent(),
e.getX(), e.getY());
}
}
public void mouseReleased(MouseEvent e) {
showPopup(e); // here because different platforms handle popups differently
}
In the actionPerformed method:

public void actionPerformed(ActionEvent e) {
for(int i = 1; i < rect =" tabbedPane.getUI().getTabBounds(tabbedPane,">


Then finally you have to register this listener on your JTabbedPane:

tabbedPane.addMouseListener(new CloseTabListener(tabbedPane));


And that should do it. It's a good solution until Java 6 Mustang comes out with better tabs.

How to Close Tabs with a Keyboard Shortcut


And while you're at it, you might as well add the ctrl-w keyboard shortcut too, by creating a KeyListener that implements keyTyped() like this:

public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == '') { // ctrl-w - close tab
tabbedPane.remove(tabbedPane.getSelectedIndex());
}
}