I’ve found a way of fixing two JTree problems which have been driving me up the wall recently:
1. Right clicking tree nodes in windows should select nodes in the tree, before showing the popup menu.
2. When the popup menu is displayed under Windows look and feel, clicking away from it on another tree node dismisses the popup but does not immediately select that node.
Problem 1. is equally relevant to JTables, see the notes at the bottom of this post
These issues seem relatively minor in themselves, but taken together they are enough to make your UI seem clunky and frustrating to use, especially if the JTree is a central component. Solving both of the above problems saves mouse clicks and make the user experience more fluid. Luckily, both of the above problems can be fixed fairly easily, the first by adding some MouseListener code to manually update the selections before the popup is shown, and the second simply by setting a magic property in UIManager –
UIManager.put(“PopupMenu.consumeEventOnClose”, Boolean.FALSE);
So, without more ado, let’s look at some code to fix this..
Here is a complete example which creates a tree with a fixed popup menu. The key bits for the fix are the setting of the magic property and the method setSelectedItemsOnPopupTrigger()
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
/**
* A JTree with mouse listener logic to select items on right click,
* before showing a popup
*
* Also sets the magic PopupMenu property to fix the windows look and
* feel, so that clicking outside the popup will select items in the
* tree first time, rather than even being consumed by the closing popup
*/
public class FixedPopupJTree extends JTree {
static {
//Set the magic property which makes the first click outside the popup
//capable of selecting tree nodes, as well as dismissing the popup.
UIManager.put("PopupMenu.consumeEventOnClose", Boolean.FALSE);
}
public FixedPopupJTree() {
getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
addMouseListener(new ShowPopupMouseListener());
}
private class ShowPopupMouseListener extends MouseAdapter {
public void mousePressed(MouseEvent e) {
showMenuIfPopupTrigger(e);
}
public void mouseClicked(MouseEvent e) {
showMenuIfPopupTrigger(e);
}
public void mouseReleased(MouseEvent e) {
showMenuIfPopupTrigger(e);
}
private void showMenuIfPopupTrigger(final MouseEvent e) {
if (e.isPopupTrigger()) {
//set the new selections before showing the popup
setSelectedItemsOnPopupTrigger(e);
//build an example popup menu from selections
JPopupMenu menu = new JPopupMenu();
for ( TreePath p : getSelectionPaths()) {
menu.add(new JMenuItem(p.getLastPathComponent().toString()));
}
//show the menu, offsetting from the mouse click slightly
menu.show((Component)e.getSource(), e.getX() + 3, e.getY() + 3);
}
}
/**
* Fix for right click not selecting tree nodes -
* We want to implement the following behaviour which matches windows explorer:
* If the item under the click is not already selected, clear the current selections and select the
* item, prior to showing the popup.
* If the item under the click is already selected, keep the current selection(s)
*/
private void setSelectedItemsOnPopupTrigger(MouseEvent e) {
TreePath p = getPathForLocation(e.getX(), e.getY());
if ( ! getSelectionModel().isPathSelected(p)) {
getSelectionModel().setSelectionPath(p);
}
}
}
}
One warning, this solution is based on the Windows look and feel, and aim to match the standard explorer tree behaviour for Windows. It may or may not be appropriate for other platforms – if you are expecting your UI to be used on other platforms, you’ll have to verify for your self whether these techniques are useful.
Here is a simple test program which demonstrates the fixed behaviour. To see what it is like when it is not fixed, comment out the lines in FixedPopupJTree which set the UIManager property, and the mouse listener line which calls setSelectedItemsOnPopupTrigger()
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import java.awt.*;
public class TestFixPopupTree {
//A simple test frame to show the effect of these fixes
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
UIManager.setLookAndFeel(WindowsLookAndFeel.class.getName());
} catch (Exception e) {
e.printStackTrace();
}
FixedPopupJTree fixedPopupJTree = new FixedPopupJTree();
TreeModel treeModel = createExampleTreeModel();
fixedPopupJTree.setModel(treeModel);
JFrame frame = new JFrame();
frame.getContentPane().add(new JScrollPane(fixedPopupJTree));
frame.setSize(new Dimension(300, 600));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null); //centre on screen
frame.setVisible(true);
}
});
}
private static TreeModel createExampleTreeModel() {
DefaultMutableTreeNode root = new DefaultMutableTreeNode("root");
for ( int child = 0; child <=10 ; child++) {
root.add(new DefaultMutableTreeNode("child-" + child));
}
DefaultTreeModel treeModel = new DefaultTreeModel(root);
return treeModel;
}
}
One wonders why the above behaviour isn’t implemeted as standard in the Windows look and feel for the latest JDK (I’m using jdk 1.6.0_22 as I write this). I guess it’s a case of the Swing dev team not wanting to break backwards compatibility. Although it fixes the issues, modifying the behaviour of the ui like this would represent a singificant change to the toolkit. I’m guessing this is why Swing seems to have an increasing number of ‘magic’ properties, such as the JPopupMenu property above. Supporting the use of a new property to change the behaviour is a sure way not to break functionality for existing applications – but it does require the new property to be documented if it is going to be useful, and it requires the developers of new apps to know about the property, and set it – and sadly it seems that’s not the case. Perhaps I should put together a web page to bring together all these magic properties.
In fact, these issues are related to know bugs, but the solutions don’t appear to be documented anywhere else very clearly. The bugs I found so far are below:
Bug 4196497
Bug 6753637
Incidentally, the fix for number 1 above is equally relevant for JTables which support discontiguous selection. In this case, you just need to change the popup mouse listener to update the table selection rather than the tree selection, which you can do in the following way:
int row = table.rowAtPoint(e.getPoint());
boolean selected = table.getSelectionModel().isSelectedIndex(row);
if ( ! selected ) {
table.getSelectionModel().setSelectionInterval(row, row);
}
}