How to fix right click JTree selection and JPopupMenu so your JTree feels native

April 26th, 2011 Nick Posted in java, swing No Comments »

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.*;
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 com.sun.java.swing.plaf.windows.WindowsLookAndFeel;

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:

protected void setSelectedItemsOnPopupTrigger(MouseEvent e) {
    int row = table.rowAtPoint(e.getPoint());
    boolean selected = table.getSelectionModel().isSelectedIndex(row);
    if ( ! selected ) {
      table.getSelectionModel().setSelectionInterval(row, row);
    }
}

AddThis Social Bookmark Button

JTable setRowHeight causes slow repainting

June 6th, 2009 Nick Posted in java, swing 7 Comments »

I’ve spent way too much time in the last few months fixing and optimising table rendering performance for a high performance Java application I’m working on at the moment. The problems were particularly hard to find becuase the app supports customization of almost every aspect of JTable UI, with custom rendering for rows, columns and individual cells according to user preferences. Some users would get terrible performance on some days, which would seem to disappear the next.

Some of the issues were hardware and graphics driver related, and there have been various minor and major victories along the way (more about these later), but just yesterday I found a very significant defect in JTable.tableChanged().

The problem occurs when you call setRowHeight(int row, int height) on a JTable, to set a customised table row height. You only need to do this once, and that causes JTable to initialize its internal rowModel to maintain the custom row heights. From that point onwards, any update to the TableModel (even if it is just an update event for a single cell()) will be processed by JTable.tableChanged() in such a way that the entire table component is marked as dirty. So this forces the RepaintManager to repaint the entire table every time, even for single cell updates.

The point at which everything appears to go wrong is at JTable line 4396 (in jdk 1.6.0_14) and line 3021 (in jdk 1.5.0_17), which is part of the processing for TableModel update events

// The totalRowHeight calculated below will be incorrect if
// there are variable height rows. Repaint the visible region,
// but don't return as a revalidate may be necessary as well.
if (rowModel != null) {
    repaint();
}

Essentially, if there is an internal rowModel set up (i.e the user has configured some custom row heights), then the call to repaint() in this snippit causes the entire JTable to be marked as dirty – so the RepaintManager repaints the entire table component rather than just the cells affected by an update.

The effect of this is dramatic. When I run my test class (included below):

***** Running test for table Standard JTable under 1.5.0_19
With all rows default height total time spent repainting table 24 millis
Setting custom row height for one row
With one custom row height total time spent repainting table 5427 millis
Thats 226 times slower than it should be to paint the table!

Here is a Fix for the issue

I’m pretty surprised this issue hasn’t been fixed already, since custom row heights can’t be all that rare. I have produced a workaround by extending JTable to override the tableChanged() handling for update events. Fixing this has solved major problems for our users whose tables were configured to require custom row heights.

This works fine for for me in 1.5.*.
It will also work for jdk 1.6.* but it does not support the 1.6 table row sorting (you’d need to find a way to fire sortedTableChanged() events to the sortManager to do this when the updates are processed)

   import javax.swing.table.TableModel;
import javax.swing.event.TableModelEvent;
import javax.swing.*;
import java.awt.*;


/**
 * Extends JTable to fix the broken repainting for updates when there are custom height rows
 * See http://www.objectdefinitions.com/odblog/2009/jtable-setrowheight-causes-slow-repainting/
 *
 * This fix does not support row sorting in jdk1.6 -
 * to support that would require firing sortedTableChanged to sortManager
 */

public class FixedForRowHeightJTable extends JTable {

    public FixedForRowHeightJTable() {
    }

    public FixedForRowHeightJTable(TableModel tableModel) {
        super(tableModel);
    }

    public void tableChanged(TableModelEvent e) {
        //if just an update, and not a data or structure changed event or an insert or delete, use the fixed row update handling
        //otherwise call super.tableChanged to let the standard JTable update handling manage it
        if ( e != null &&
            e.getType() == TableModelEvent.UPDATE &&
            e.getFirstRow() != TableModelEvent.HEADER_ROW &&
            e.getLastRow() != Integer.MAX_VALUE) {

            handleRowUpdate(e);
        } else {
            super.tableChanged(e);
        }
    }

    /**
     * This borrows most of the logic from the superclass handling of update events, but changes the calculation of the height
     * for the dirty region to provide proper handling for repainting custom height rows
     */

    private void handleRowUpdate(TableModelEvent e) {
        int modelColumn = e.getColumn();
        int start = e.getFirstRow();
        int end = e.getLastRow();

        Rectangle dirtyRegion;
        if (modelColumn == TableModelEvent.ALL_COLUMNS) {
            // 1 or more rows changed
            dirtyRegion = new Rectangle(0, start * getRowHeight(),
                                        getColumnModel().getTotalColumnWidth(), 0);
        }
        else {
            // A cell or column of cells has changed.
            // Unlike the rest of the methods in the JTable, the TableModelEvent
            // uses the coordinate system of the model instead of the view.
            // This is the only place in the JTable where this "reverse mapping"
            // is used.
            int column = convertColumnIndexToView(modelColumn);
            dirtyRegion = getCellRect(start, column, false);
        }

        // Now adjust the height of the dirty region
        dirtyRegion.height = 0;
        for ( int row=start; row <= end; row ++ ) {
            dirtyRegion.height += getRowHeight(row);  //THIS IS CHANGED TO CALCULATE THE DIRTY REGION HEIGHT CORRECTLY
        }
        repaint(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height);
    }

}

The following Test demonstrates the problem, and that FixedForRowHeightJTable solves it

This test pops up a JTable in a frame and then changes cell values to trigger repaints.
While doing this we replace the standard RepaintManager with a TimingRepaintManager, which keeps track of the total time spent by the Swing event thread painting the table
We first run the test for a standard JTable, then for my fixed version, with the following results:

***** Running test for table Standard JTable under 1.5.0_19
With all rows default height total time spent repainting table 24 millis
Setting custom row height for one row
With one custom row height total time spent repainting table 5427 millis

***** Running test for table FixedForRowHeight JTable under 1.5.0_19
With all rows default height total time spent repainting table 2 millis
Setting custom row height for one row
With one custom row height total time spent repainting table 25 millis

Needless to say, I’ve submitted a bug report for this one!
(although technically once might argue its an RFE, since the table still gets painted, anything which degrades performance this much is a bug in my book!)

import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;

/**
 * Created by IntelliJ IDEA.
 * User: Nick Ebbutt
 * Date: 06-Jun-2009
 * Time: 13:34:58
 *
 * A demonstration of JTable repainting bug for custom row heights
 */

public class DemoForJTableCustomRowHeightRepaintBug {

    private static final int COL_COUNT = 40;
    private static final int ROW_COUNT = 50;
    private static TimingRepaintManager timingRepaintManager;
    private static JTable table;
    private static FixedForRowHeightJTable fixedTable;

    public static void main(String[] args) throws Exception {
        SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                timingRepaintManager = new TimingRepaintManager();
                RepaintManager.setCurrentManager(timingRepaintManager);
                table = new JTable(new TestTableModel());
                fixedTable = new FixedForRowHeightJTable(new TestTableModel());
            }
        });

        runTestForTable("Standard JTable", table);
        runTestForTable("FixedForRowHeight JTable", fixedTable);
    }

    private static void runTestForTable(String description, final JTable table) throws Exception {
        System.out.println("\n***** Running test for table " + description + " under " + System.getProperty("java.version"));
        showTable(table, description);
        timingRepaintManager.resetTotalRepaintTime();
        runTableModelUpdates(table.getModel());

        System.out.println("With all rows default height total time spent repainting table " + timingRepaintManager.getTotalRepaintTime() + " millis");

        System.out.println("Setting custom row height for one row");
        setRandomRowHeight(table);

        timingRepaintManager.resetTotalRepaintTime();
        runTableModelUpdates(table.getModel());
        System.out.println("With one custom row height total time spent repainting table " + timingRepaintManager.getTotalRepaintTime() + " millis");
    }

    private static void setRandomRowHeight(final JTable table) throws InterruptedException, InvocationTargetException {
        //now just set one row to a custom height and run the repaint timing test again
        SwingUtilities.invokeAndWait(
            new Runnable() {
                public void run() {
                    table.setRowHeight(getRandomRowOrCol(ROW_COUNT), 100);
                }
            }
        );
    }

    public static void showTable(final JTable t, final String name) throws Exception {
        SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                JFrame f = new JFrame(name);
                f.getContentPane().add(new JScrollPane(t));
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                f.setSize(new Dimension(1024,768));
                f.setVisible(true);
            }
        });
    }

    public static void runTableModelUpdates(final TableModel tableModel) {
        for ( int loop=0; loop < 500; loop++) {
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        tableModel.setValueAt("Wibble", getRandomRowOrCol(ROW_COUNT), getRandomRowOrCol(COL_COUNT));
                    }
                });
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    //record the total time spent repainting
    private static class TimingRepaintManager extends RepaintManager {
        private volatile long totalTime;

        public void paintDirtyRegions() {
            long startTime = System.currentTimeMillis();
            super.paintDirtyRegions();
            totalTime += System.currentTimeMillis() - startTime;
        }

        public long getTotalRepaintTime() {
            return totalTime;
        }

        public void resetTotalRepaintTime() {
            this.totalTime = 0;
        }
    }


    private static class TestTableModel extends DefaultTableModel {
        public TestTableModel() {
            setColumnCount(COL_COUNT);
            for (int row=0; row < ROW_COUNT; row++) {
                addRow(createRow(row));
            }
        }

        private Object[] createRow(int row) {
            String[] colNames = new String[COL_COUNT];
            for ( int col=0; col<COL_COUNT; col++) {
                colNames[col] = "Cell " + row + ":" + col;
            }
            return colNames;
        }
    }

    public static int getRandomRowOrCol(int maxVal) {
        return (int)(Math.random() * maxVal);
    }

    /**
     * Extends JTable to fix the broken repainting for updates when there are custom height rows
     *
     * This fix does not support row sorting in jdk1.6 -
     * to support that would require firing sortedTableChanged to sortManager
     */

    private static class FixedForRowHeightJTable extends JTable {

        public FixedForRowHeightJTable(TableModel tableModel) {
            super(tableModel);
        }

        public void tableChanged(TableModelEvent e) {

            //if just an update, and not a data or structure changed event or an insert or delete, use the fixed row update handling
            //otherwise call super.tableChanged to let the standard JTable update handling manage it
            if ( e != null &&
                e.getType() == TableModelEvent.UPDATE &&
                e.getFirstRow() != TableModelEvent.HEADER_ROW &&
                e.getLastRow() != Integer.MAX_VALUE) {

                handleRowUpdate(e);
            } else {
                super.tableChanged(e);
            }
        }

        /**
         * This borrows most of the logic from the superclass handling of update events, but changes the calculation of the height
         * for the dirty region to provide proper handling for repainting custom height rows
         */

        private void handleRowUpdate(TableModelEvent e) {
            int modelColumn = e.getColumn();
            int start = e.getFirstRow();
            int end = e.getLastRow();

            Rectangle dirtyRegion;
            if (modelColumn == TableModelEvent.ALL_COLUMNS) {
                // 1 or more rows changed
                dirtyRegion = new Rectangle(0, start * getRowHeight(),
                                            getColumnModel().getTotalColumnWidth(), 0);
            }
            else {
                // A cell or column of cells has changed.
                // Unlike the rest of the methods in the JTable, the TableModelEvent
                // uses the coordinate system of the model instead of the view.
                // This is the only place in the JTable where this "reverse mapping"
                // is used.
                int column = convertColumnIndexToView(modelColumn);
                dirtyRegion = getCellRect(start, column, false);
            }

            // Now adjust the height of the dirty region
            dirtyRegion.height = 0;
            for ( int row=start; row <= end; row ++ ) {
                dirtyRegion.height += getRowHeight(row);  //THIS IS CHANGED TO CALCULATE THE DIRTY REGION HEIGHT CORRECTLY
            }
            repaint(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height);
        }
    }

}
AddThis Social Bookmark Button

Workaround for bug id 6753651 – find path to jar in cache under webstart

October 2nd, 2008 Nick Posted in java, swing 5 Comments »

Here is a workaround for the changes in 1.5.0_16 and 1.6.0_07, which prevent you from obtaining a valid URL using Class.getResource(myClassName) under webstart.

This is related to bug id 6753651 raised here

URLs acquired in this way used to include a filesystem path to the jar file in the local webstart cache containing the resource. Some apps relied on this filesystem path.

The behaviour of Class.getResource() under webstart was changed in 1.5.0_16 and 1.6.0_07 due to a security patch.

  • In 1.5.0_16 this breaks the URL (i.e. the URL returned by url.toString() is no longer valid, the file path is removed, which breaks some apps).
  • In 1.6.0_07 the url now uses the http protocol to refer to the jar file on the server – although in this case under the covers webstart will actually use the jar in the local cache if you call URL.openConnection()

One of our apps relied on the file path information in the URL, so we were forced to find a workaround.

This workaround does not fix the broken URL – but it does at least let you use the URL to find a valid path to the jar file on the local filesystem

update Nov 28th 08 – if you want to get a valid URL you might check out David’s suggestions in the comments below this post

To use the workaround first call Class.getResource(myResource) to get a (broken) URL , and then pass that URL into getJarFilePath(URL url). You should get back the path to the jar file in the local webstart cache which contains the resource.

It should work for all JDK/webstart versions up to and including 1.5.0_16 and 1.6.0_07 (subsequent versions have not been tested)

This workaround will only work for signed apps running with all permissions, since it uses reflection to access private methods and fields in classes within webstart.jar.

This workaround is pretty horrid, it certainly feels sinful, but it is the best solution I can find at present, having to fix a mission critical application within the next 24 hours. Since it relies on private implementation details within the webstart jar it may break with future jre versions if that implementation is changed. It is not part of the public API, so changes made in subsequent jdks may break the workaround at any time.

n.b. you may need to call System.setSecurityManager(null) before you call this routine to allow the private field access to work.

     /**
     * This method will return the path to the jar file containing the resource which this URL references
     *
     * It should work with URLs returned by class.getResource() under java 1.5.0_16
     * and 1.6.0_07, as well as maintaining backwards compatibility with previous jres
     *
     * The two jre above contain security patches which make the file path of the jar
     * inaccessible under webstart. This patch works around that by using reflection to access
     * private fields in the webstart.jar where required. This will only work for signed webstart
     * apps running with all security permissions
     *
     * @param jarUrl - url which has jar as the protocol
     * @return path to Jar file for this jarURL
     */

    public static String getJarFilePath(URL jarUrl) {
        JarFile jarFile = getJarFile(jarUrl);
        return findJarPath(jarFile);
    }

    public static JarFile getJarFile(URL jarUrl) {
        try {
            JarURLConnection jarUrlConnection = (JarURLConnection)jarUrl.openConnection();

            //try the getJarFile method first.
            //Under webstart in 1.5.0_16 this is overriden to return null
            JarFile jarFile = jarUrlConnection.getJarFile();

            if ( jarFile == null) {
                jarFile = getJarFileByReflection(jarUrlConnection);
            }
            return jarFile;
        } catch (Throwable t) {
            throw new RuntimeException("Failed to get JarFile from jarUrlConnection", t);
        }
    }

    private static JarFile getJarFileByReflection(JarURLConnection jarUrlConnection) throws Exception {
        //this class only exists in webstart.jar for 1.5.0_16 and later
        Class jnlpConnectionClass = Class.forName("com.sun.jnlp.JNLPCachedJarURLConnection");
        Field jarFileField;
        try {
            jarFileField = jnlpConnectionClass.getDeclaredField("jarFile");
        } catch ( Throwable t) {
            jarFileField = jnlpConnectionClass.getDeclaredField("_jarFile");
        }
        jarUrlConnection.connect(); //this causes the connection to set the jarFile field
        jarFileField.setAccessible(true);
        return (JarFile)jarFileField.get(jarUrlConnection);
    }

    private static String findJarPath(JarFile cachedJarFile) {
        try {
            String name = cachedJarFile.getName();

            //getName is overridden to return "" under 1.6.0_7 so use reflection
            if ( name == null || name.trim().equals("")) {
                Class c = ZipFile.class;
                Field field = c.getDeclaredField("name");
                field.setAccessible(true);
                name = (String)field.get(cachedJarFile);
            }
            return name;
        } catch (Throwable t) {
            throw new RuntimeException("Failed to get find name from jarFile", t);
        }
    }
AddThis Social Bookmark Button

New Open Source SwingCommand library released

September 29th, 2008 Nick Posted in java, swing No Comments »

I finally completed open sourcing SwingCommand – an open source library which provides support for the command pattern in Swing. In addition to synchronous commands, it provides support for asynchronous commands, which hides away the gory threading details in a similar way to the SwingWorker class – but in SwingCommand the asynchronous commands are integrated into the overall framework, and there are a few extra features, such as composite command support and command execution observers, which I hope make it more flexible.

Reusable Commands

One other key difference from SwingWorker is that in SwingCommand each command instance is reusable (a command can be executed more than once). Each time you call myCommand.execute() a ‘CommandExecution’ instance, which encapsulates the state for that execution. In the SwingWorker provided as part of jdk 1.6, each SwingWorker instance is designed to be executed only once. In SwingCommand there is nothing to stop you creating a new Command instance each time if you prefer, but you also have the flexibility to create a shared Command instance up front, pass references to it around your application and execute it many times if necessary.

Why a new Command library?

There is nothing fundamentally new about using the command pattern in Swing apps, but arguably there is a need for a reusable library to address this problem in a focused manner, and provide a richer set of features than SwingWorker, without attempting to solve every other ui programming issue along the way. SwingCommand has certainly been a useful library in my projects already. It does hide away a lot of the more complex threading issues, typically produces ‘clean code’, and make it easy to build a Swing application around chunks of reusable logic wrapped as Command classes. Integration with java.util.concurrent framework makes it really easy to handle threading issues which would otherwise be complex (e.g to prevent two instances of the same asynchronous command from running simulatenously would be a one line change, by using Executors.newSingleThreadExecutor()).

A milestone

This is the first of Object Definitions’ libraries which I have open sourced. It is fair to say it has been more work than I anticipated to get it to this stage – so it is probably time to crack open a bottle of something fizzy. Once I have recovered from the hangover I’ll post more about it here. In the meantime there is a lot more information on the website and you can download swingcommand library here

AddThis Social Bookmark Button

Fix for log4j bug 45704 – Failed to load logging.xml for JRE 1.5.0_16 and Webstart

September 26th, 2008 Nick Posted in java, swing 1 Comment »

This bug affects webstart applications which use log4j and xml configuration files (e.g. log.xml rather than log4j.properties) in JDK 1.5.0_16 (and later..?)

I think this problem has been occurring since a new security patch was released by Sun, which seems to have changed the behaviour of the URL class for webstart apps.

This security patch affects jdk 1.5 from 1.5.0_16 onwards, and also affects Java SE 6 Update 7 onwards
So this problem probably also exists for log4j under webstart 1.6 update 7 +

What seems to have happened is that the patch has stopped webstart apps from being able to obtain a valid URL object using MyClass.class.getResource().

If you obtain a URL instance in this way when running under webstart from 1.5.0_16, then calling toString on the URL instance gives you a malformed URL without the jar file path information. You only get back the path of the class within the Jar file – the part which usually appears after the exclamation mark – and not the path to the jar file itself. See this thread here for an example of what this looks like

I think the path to the jar file in the webstart cache has been removed from URL.toString() due to the security issues addressed by the patch. However if you call myUrl.openConnection() on the URL instance this still works OK, due to the way URL is implemented internally.

It is very bad that the only way to solve this security issue was to break the behaviour of URL class in this way.
It will surely create no end of problems for many webstart users.

Because of this issue, log4j is currently broken for webstart apps running under the latest jre, at version 1.2.15
There is an open bug id 45704 here.

There is now also a bug raised on Sun’s site

Luckily I think I have a solution (I will confirm this next Monday) and it is a one line change to log4j
The class to change is the DOMConfigurator.

Whereas the PropertyConfigurator does url.openConnection to get a stream (so this should work), the DOMConfigurator uses url.toString() – and gets back the bad webstart URL
This can be fixed at line 762 in DOMConfigurator.java simply by replacing parser.parse(url.toString()) with parser.parse(url.openConnection().getInputStream())

I have created a patch of log4j to make this change for my clients, and this seems to fix the issue with their webstart apps. I will submit the change to the log4j team for consideration tonight

Here is a link to the patched file
DOMConfigurator.java

AddThis Social Bookmark Button

Calculating a color gradient

May 12th, 2008 Nick Posted in java, swing No Comments »

For a recent project I needed some code to calculate a colour gradient for different points on a scatter chart. The class below performs a simple interpolation between two colours, if you feed it a value between 0 and 1.

 public class GradientCalculator {
        private Color c1;
        private Color c2;

        public GradientCalculator(Color c1, Color c2) {
            this.c1 = c1;
            this.c2 = c2;
        }

        /**
         * @param ratio - value between 0 and 1
         */

        public Color getColor(double ratio) {
            int red = (int) (c1.getRed() * (1 - ratio) + c2.getRed() * ratio);
            int green = (int) (c1.getGreen() * (1 - ratio) + c2.getGreen() * ratio);
            int blue = (int) (c1.getBlue() * (1 - ratio) + c2.getBlue() * ratio);
            int alpha  =(int) (c1.getAlpha() * (1 - ratio) + c2.getAlpha() * ratio);
            return new Color(red, green, blue, alpha);
        }
    }
AddThis Social Bookmark Button

Drag and drop into empty table not working?

January 25th, 2008 Nick Posted in java, swing No Comments »

This is a known gotcha from the early days of Swing, related to bug id 4310721

The symptom is that while drag and drop works fine into tables which are populated, trying to drop into a table with no rows, or drop data beneath the populated rows, does not work. The cause of this is that the table does not automatically expand to fill the area available to it in the JScrollpane viewport.

As of jdk1.6 this is fixed on JTable itself. You just need to call myJTable.setFillsViewportHeight(true)

If you do not have the great glory of 1.6 available to you, you will have to extend JTable and override the getScrollableTracksViewportHeight() method as below:

public class TrackViewportJTable extends JTable {

    public TrackViewportJTable(TableModel tableModel, TableColumnModel tableColumnModel) {
        super(tableModel, tableColumnModel);
    }

    public TrackViewportJTable(TableModel tableModel) {
        super(tableModel);
    }

    public boolean getScrollableTracksViewportHeight() {
        // fetch the table's parent
        Container viewport = getParent();

        // if the parent is not a viewport, calling this isn't useful
        if (! (viewport instanceof JViewport)) {
            return false;
        }

        // return true if the table's preferred height is smaller
        // than the viewport height, else false
        return getPreferredSize().height < viewport.getHeight();
    }
}

The above is based on the example in Shannon Hickey’s blog.
For a more complete description of the problem, and its solution, see his blog entry here

AddThis Social Bookmark Button

More on swing garbage collection – static references

September 24th, 2007 Nick Posted in java, swing No Comments »

Static references are another common cause of memory leaks, both in Swing applications and other components.

If a static field holds a reference to an object instance, that instance will not be garbage collected until such time as its owning Class instance is garbage collected (which may be never). If have seen several applications which held Maps or other data structures at a static level, and over time the number of objects referenced by the collection gradually grew, resulting in a memory leak. Often items are placed in such collections without the developer realising that the item being added will never be released.

In many cases static fields are overused, and it is always best to question whether a field really needs to be static. When trying to find the cause of a memory leak, it is often a good idea to start by having a look at the tree of references which emanate from each static field reference, and see if this might be the cause.

AddThis Social Bookmark Button

Swing garbage collection – problems with the observer pattern

September 24th, 2007 Nick Posted in java, swing 2 Comments »

Clearly the observer pattern is very important in Swing programming, it is used every time you register a Swing model with a UI component, so that the component can update itself as the model data changes. However, there are inherent dangers in using this pattern without considering garbage collection.

The problem is that most standard implementations of swing models hold strong references to the listeners which register with them. This prevents the listeners from being garbage collected, until the model itself becomes available to the garbage collector. Some important models in an application may have a long expected life time (they may be created when the application starts up and dereferenced only when it exits). If these models hold on to all their listeners, some of which may be temporary screens or dialogs which the user opens and closes, the impact on memory usage can be far-reaching. This issue has been at the core of the most of the severe memory management issues I have seen so far with Swing).

Most solutions to the problem suggest that the correct approach to solving it would be to modify the models in order to store the listener objects as weak references.

Upgrading models to use weak references

This approach works, but has at least one severe drawback – although you can use the techique freely in your own models, the standard models which Swing provides do not use weak references. This means that you will probably need to extend them, and then modify your code to use the extended versions of the models, or implement a totally redesigned model class. This is non-trivial, and will involve careful analysis of the way the existing models access and update their listener lists. Furthermore, it will be necessary to check the implementation of the models on subsequent updates to the swing core libraries.

Additionally, fixing this problem by updating the models has another drawback – it is inflexible. In most implementations the ‘weak reference model’ wraps all of its registered listeners in a weak reference indescriminately. There may be occasions when you don’t want this to be the case – some listeners may still be functionally important even though they have no references from elsewhere in the application, and you don’t want these to be garbage collected. However there is no way for client classes to opt out when they add their listeners – all listeners added to the ‘weak reference models’ will be held using weak references. There may be workaround to this, but overall it adds to the potential problems.

WeakReferenceListener wrapper class

A preferable solution is to provide a way for the client classes to decide whether weak references are required at the point they add a listener to the model. The following techniques can be used without modifying the model classes at all.

Both techniques below involve wrapping the real listener instance using a weak reference wrapper class. The wrapper class is added to the model, where it acts as a proxy for the real listener instance. The weak reference proxy first tests whether the real listener has been garbage collected, before delegating event method calls.

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.lang.ref.WeakReference;

/**
 * WeakRefActionListener delegates the handling of ActionEvents to an ActionListener wrapped in a weak reference
 */

public class WeakRefActionListener implements ActionListener
{

  private WeakReference<ActionListener> actionListenerDelegate;

  public WeakRefActionListener(ActionListener actionListener)
  {
    this.actionListenerDelegate = new WeakReference<ActionListener>(actionListener);
  }

  public void actionPerformed(ActionEvent e)
  {
    ActionListener delegate = actionListenerDelegate.get();
    if ( delegate  != null )
    {
      delegate.actionPerformed(e);
    }
  }

}

The WeakRefActionListener can be used in the following manner

this.actionListener = new ActionListener()
{
    public void actionPerformed(ActionEvent e)
    {
        System.out.println("Button clicked!");
    }
}

button.addActionListener(
  new WeakRefActionListener( actionListener )
);

There are two things to note:

  • The client class must keep a reference to the listener it creates. It will not work with anonymous listeners. Unless the client class or another object instance holds a reference to the listener, the listener will be eligable for garbage collection immediately once its weak reference proxy has been added to the model.
  • The model will still retain a reference to the weak reference proxy, even when the wrapped listener instance has been garbage collected. This will not create any problems unless many such listeners are added to the model, but in this case performance could degrade as the model iterates through a list of weak reference listeners which are mostly no longer valid. A possible solution to this might make use of a background thread to clean up the invalid WeakRef listeners
AddThis Social Bookmark Button

Tango Desktop Project – great icons for UI developers

September 24th, 2007 Nick Posted in swing No Comments »

Here is a great resource for UI developers.

Tango Desktop Project contains a set of icons which are released under the Creative Commons Attribution Sharealike license

It is generally hard to find a good set of icons for a UI project without going the commercial route, Tango seems to offer a good solution to this problem



AddThis Social Bookmark Button

JTextArea with popup menu – JPopupTextArea

May 17th, 2007 Nick Posted in java, swing 4 Comments »

I recently worked on a project which was using the old AWT TextArea component.
OK, TextArea is pretty basic, but at least it gives you a right click popup menu with simple copy, cut paste functionality, out of the box.

The JTextArea, whilst far more powerful in theory, does not give you this basic functionality, which is a major step backwards, in my book. To achieve the same thing you have to roll your own popup menu. Once you have done this, you would think that simply adding your new JPopupMenu to the JTextArea using the helpful looking add(PopupMenu p) method would be enough.

But nooooo. That method is a hangover from the old AWT days, and that would be way too easy. This appears to be a red herring.

In fact, if you want it to actually show up, you have to write your own MouseListener to listen for the popup trigger event, register it with the JTextArea, and display it yourself.

So, how is this a step forward from the old AWT days, you may well ask?

After all, 99% of all developers out there would probably just like a simple right click popup menu on their text area, to cover the basic copy cut paste actions, without jumping through too many hoops along the way. Don’t get me wrong, I am mad keen on Swing, it is just so flexible. I would just like it to provide a better solution out of the box for the most common cases, more of the time.

Well, in any case, here is a simple subclass of JTextArea which makes this easy.
If you wanted, you could easily modify it to add other actions from the DefaultEditorKit to the menu, such as undo/redo, or set Icons on the Actions.

JPopupTextArea

public class JPopupTextArea extends JTextArea
{
    private HashMap actions;

    public JPopupTextArea()
    {
        addPopupMenu();
    }

    private void addPopupMenu()
    {
        createActionTable();

        JPopupMenu menu = new JPopupMenu();
        menu.add(getActionByName(DefaultEditorKit.copyAction, "Copy"));
        menu.add(getActionByName(DefaultEditorKit.cutAction, "Cut"));
        menu.add(getActionByName(DefaultEditorKit.pasteAction, "Paste"));
        menu.add(new JSeparator());
        menu.add(getActionByName(DefaultEditorKit.selectAllAction, "Select All"));
        add(menu);

        addMouseListener(
           new PopupTriggerMouseListener(
                   menu,
                   this
           )
        );

        //no need to hold the references in the map,
        // we have used the ones we need.
        actions.clear();
    }

    private Action getActionByName(String name, String description) {
        Action a = (Action)(actions.get(name));
        a.putValue(Action.NAME, description);
        return a;
    }


    private void createActionTable() {
        actions = new HashMap();
        Action[] actionsArray = getActions();
        for (int i = 0; i < actionsArray.length; i++) {
            Action a = actionsArray[i];
            actions.put(a.getValue(Action.NAME), a);
        }
    }

    public static class PopupTriggerMouseListener extends MouseAdapter
    {
        private JPopupMenu popup;
        private JComponent component;

        public PopupTriggerMouseListener(JPopupMenu popup, JComponent component)
        {
            this.popup = popup;
            this.component = component;
        }

        //some systems trigger popup on mouse press, others on mouse release, we want to cater for both
        private void showMenuIfPopupTrigger(MouseEvent e)
        {
            if (e.isPopupTrigger())
            {
               popup.show(component, e.getX() + 3, e.getY() + 3);
            }
        }

        //according to the javadocs on isPopupTrigger, checking for popup trigger on mousePressed and mouseReleased
        //should be all  that is required
        //public void mouseClicked(MouseEvent e)  
        //{
        //    showMenuIfPopupTrigger(e);
        //}

        public void mousePressed(MouseEvent e)
        {
            showMenuIfPopupTrigger(e);
        }

        public void mouseReleased(MouseEvent e)
        {
            showMenuIfPopupTrigger(e);
        }

    }

}



AddThis Social Bookmark Button

JTable editor – commit change on loss of focus

April 11th, 2007 Nick Posted in swing 12 Comments »

There is a known issue with JTables whereby the changes made in a cell editor are not committed when focus is lost.

This can result in the table staying in ‘edit mode’ with the stale value in the cell being edited still showing, although the change has not actually been committed to the model.

- this has bug id 4709394
Link to Bug Report

In fact what should happen is for this method on CellEditor to be called when focus is lost:

     /**
     * Tells the editor to stop editing and accept any partially edited
     * value as the value of the editor.  The editor returns false if
     * editing was not stopped; this is useful for editors that validate
     * and can not accept invalid entries.
     *
     * @return  true if editing was stopped; false otherwise
     */

    public boolean stopCellEditing();

So the editor can choose whether to accept the new value and stop editing, or have the editing cancelled without committing.

There is a magic property which you have to set on the JTable instance to turn this feature on:

table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

Setting this hidden property should make the whole thing work as expected.



AddThis Social Bookmark Button