Archives For March 2014

This morning I woke up still thinking in Java and wondering why I have to live with the positively ancient and hideous Metal Look-and-Feel (LaF). Turns out I don’t have to. As you can see above, I’ve managed to turn on the Windows 8.1 LaF on my simple test framework, with just a few extra lines of code. And I’ve re-discovered it’s portable across all the environments I want to run this on (at least, the ones I care about), primarily Windows 7 and 8 and various Linux distributions and desktops. But first, the code. This is a modification to the source file TabbedTables.java, first seen in the prior post.

    private static void createGUI() {
        try {
            // Set system Java L&F (Windows for Windows, Gnome for Linux...)
            //
            UIManager.setLookAndFeel(
                    UIManager.getSystemLookAndFeelClassName());
        }
        catch (UnsupportedLookAndFeelException |
                ClassNotFoundException |
                InstantiationException |
                IllegalAccessException exception) {
            // silently handle exception
        }

        JTabbedPane tabbedPane = new JTabbedPane();
        ImageIcon icon = createImageIcon("images/image.gif");

        tabbedPane.addTab("Tab 1", icon, makeTable(), "Sample 1");
        tabbedPane.setTabComponentAt(0, new CloseTabControl(tabbedPane));
        tabbedPane.setMnemonicAt(0, KeyEvent.VK_1);

        tabbedPane.addTab("Tab 2", icon, makeTable(), "Sample 2");
        tabbedPane.setTabComponentAt(1, new CloseTabControl(tabbedPane));
        tabbedPane.setMnemonicAt(1, KeyEvent.VK_2);

        JFrame frame = new JFrame("Test tabs and tables");

        frame.setContentPane(tabbedPane);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

Lines 2 through 13 are the new lines added. If you’re lazy and don’t care about defensive programming practices then you can toss the try/catch block and just copy lines 5 and 6 and be done with it. But I prefer to write a little more robustly if possible, so that try/catch block is a very small price to pay. I believe in quality code, especially in the final shipping product. In the process of adding this code block, I also removed a line of code; JFrame.setDefaultLookAndFeelDecorated(false);  . This line is no longer needed.

This code block also illustrates an interesting feature of NetBeans 8. The catch block (lines 8 through 13) is a multiple exception catch block that was introduced in Java 7. In earlier versions of Java you needed to write separate catch blocks for every exception, an onerous task, even if you had an IDE that would automatically assist you in creating them all. Once written you had all those individual catch blocks to maintain into the future. It’s no wonder that a lot of Java programmers eschewed writing try/catch unless it was absolutely necessary (as in, javac refused to compile the code). While this doesn’t remove all the pain, multiple exception catch makes it a lot easier to write, and in particular, easier to read in the future.

How does NetBeans 8 fit into this? I was given a recommendation by the NetBeans 8 IDE, and when I selected that from its dialog, it automatically reformatted the code for me. This is one key reason why a good IDE like NetBeans or IntelliJ IDE are at times vital to fast and accurate Java programming. Those tools support you in writing up-to-date compliant code, and teach you a few things along the way.

Here’s a few screen captures of the exact same code running on Linux. Both were running in VMware. The top screen capture is CentOS 6.5 (equivalent to RHEL 6.5) running Gnome 2, and the bottom is Linux Mint 15 running its Cinnamon desktop. Of note are the tabs. They don’t look quite right, and that’s because of the “hand-drawn” close button icon. I need to dig a little deeper and learn how to use a given LaF icon, using the “hand drawn” version when I can’t find it. That will make the tabs look a lot better. But for the time being, this is Good Enough.

NOTE: All my Linux installations (hardware and virtual) are now running Java 8. Even my little Raspberry Pi.

While waiting out the rain and tornado watches here in Orlando, I spent my time indoors hacking together a test framework to learn, yet again, how to drop tables (JTable) into tabbed panes (JTabbedPane) and provide a simple way to close individual tabs when needed. I decided to do all this Java hackery with NetBeans 8. NB8 was released at the same time as Java 8, and is meant to support the latest Java 8 features, including lambda expressions. Because this is research for a bigger project at work, and we’re still on Java 7 update 51, I had to forgo using all the latest features, especially lambda expressions.

I needed a framework to investigate (and frankly, re-learn) how to build tables, render data in them, and dismiss them. It’s been years since I had to do anything non-trivial with the Java Foundation Classes, and that had me scrambling through a few tutorials to come back up to speed. I knew what I wanted, just not quite how to achieve it.

For this first step, I wanted to:

  1. Create tabs with individual close buttons on each tab. You should be able to dismiss the tabs in any arbitrary order.
  2. Render independent tables on each tab.
  3. Alternately render row color, using the colors wheat2 and white (the default) in this case.

I pretty much achieved all those goals by the end of the day (in between doing all the other little Saturday chores that needed doing). What follows is the code I hacked together for this example. I will not claim this is the best example of Java programming, far from it. If you’re searching for the best idiomatic Java, then you should probably look elsewhere. My experiences with Java goes back to the mid-1990s and Java 1, with all that that implies.

As for NetBeans 8, for the most part it didn’t get in my way as I was hacking this prototype code together. It was fast and reasonably fluid, although I could have done with a bit less autocomplete. Right now I have three Java IDEs on my Windows notebook; Netbeans 8, IntelliJ IDE 13.1.1, and Eclipse. I’ve got Eclipse because of a lot of “legacy” Android projects I started there. I had every intention of moving out of Eclipse and into IntelliJ/Android Studio, but Android Studio is still under heavy development and I have real needs now, both in Android and regular Java. I’m not too happy with NetBeans 8 and its dropping Scala support, which it had in version 7.4 and earlier. IntelliJ has that support, for the latest Scala version, and I’m happy with that. IntelliJ 13.1.1 also support Java 8. If I had my way I’d migrate all my work to IntelliJ and step up from the Community edition to a fully paid-for IntelliJ license. Maybe in the near future.

I’m posting all this as much for my future benefit as for anyone else who’s interested. I don’t want to have to go scrambling to re-discover this again.

package tabbedtables;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.KeyEvent;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellRenderer;

/**
 *
 * @author
 */
public class TabbedTables {
    public static Object[][] sampleData =
    {
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
        { "sample", "sample", "sample", "sample" },
    };

    private static JPanel makeTable() {
        JTable table = new JTable(new TableModel(sampleData)) {
            @Override
            public Component prepareRenderer(TableCellRenderer renderer, int row, int col) {
                Component component = super.prepareRenderer(renderer, row, col);
                //
                // Alternate row color
                //
                if (!isRowSelected(row))
                    component.setBackground(row % 2 == 0 ? getBackground() : Color.decode("0xEED8AE"));
                return component;
            }
        };
        JPanel panel = new JPanel(new GridLayout(1,0));
        table.setPreferredScrollableViewportSize(new Dimension(800, 600));
        table.setFillsViewportHeight(true);
        panel.add(new JScrollPane(table));
        return panel;
    }

    private static ImageIcon createImageIcon(String path) {
        java.net.URL imageURL = TabbedTables.class.getResource(path);
        if (imageURL != null) return new ImageIcon(imageURL);
        return null;
    }

    private static void createGUI() {
        JTabbedPane tabbedPane = new JTabbedPane();
        ImageIcon icon = createImageIcon("images/image.gif");

        tabbedPane.addTab("Tab 1", icon, makeTable(), "Sample 1");
        tabbedPane.setTabComponentAt(0, new CloseTabControl(tabbedPane));
        tabbedPane.setMnemonicAt(0, KeyEvent.VK_1);

        tabbedPane.addTab("Tab 2", icon, makeTable(), "Sample 2");
        tabbedPane.setTabComponentAt(1, new CloseTabControl(tabbedPane));
        tabbedPane.setMnemonicAt(1, KeyEvent.VK_2);

        JFrame.setDefaultLookAndFeelDecorated(false);
        JFrame frame = new JFrame("Test tabs and tables");

        frame.setContentPane(tabbedPane);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(800, 600);
        frame.setVisible(true);
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createGUI();
            }
        });
    }
}
package tabbedtables;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.plaf.basic.BasicButtonUI;

/**
 * Creates a button to be added to the tab on a JTabbedPane allowing the end
 * user to close any arbitrary tab. This is a feature that's been available for
 * about a decade (at least) on just about any major application you care to
 * name, such as Firefox and Chrome.
 */
public class CloseTabControl extends JPanel {
    private JTabbedPane parentTabbedPane;

    /**
     * Constructed with the parent JTabbledPane reference.
     * @param pane parent JTabbedPane
     */
    public CloseTabControl(final JTabbedPane pane) {
        super(new FlowLayout(FlowLayout.LEFT,0,0));

        if (pane == null) throw new NullPointerException("JTabbedPane is null");

        parentTabbedPane = pane;
        setOpaque(false);

        JLabel label = new JLabel() {
            @Override
            public String getText() {
                int index = pane.indexOfTabComponent(CloseTabControl.this);
                if (index != -1) return pane.getTitleAt(index);
                return null;
            }
        };

        label.setBorder(BorderFactory.createEmptyBorder(0,0,0,5));
        add(label);
        add(new TabButton());
        setBorder(BorderFactory.createEmptyBorder(2,0,0,0));
    }

    // This is where the key work gets done. Create a button and then
    // associate mouse listeners to indicate when the mouse moves into and
    // out of a given instance, as well as close the associated tab when
    // mouse clicked.
    //
    private class TabButton extends JButton implements ActionListener {
        public TabButton() {
            initTabButton();
        }

        // initTabButton was created to squash the "leak this in constructor"
        // warning, when all of this was in the constructor.
        //
        private void initTabButton() {
            setPreferredSize(new Dimension(20, 20));
            setToolTipText("Close this tab.");
            setUI(new BasicButtonUI());
            setContentAreaFilled(false);
            setFocusable(false);
            setBorder(BorderFactory.createEtchedBorder());
            setBorderPainted(false);
            addMouseListener(buttonMouseListener);
            setRolloverEnabled(true);
            addActionListener(this);
        }

        // This is where the tab is closed when a mouse click event is
        // recieved.
        //
        @Override
        public void actionPerformed(ActionEvent event) {
            int index = parentTabbedPane.indexOfTabComponent(CloseTabControl.this);
            if (index != -1) parentTabbedPane.remove(index);
        }

        @Override
        public void updateUI() {}

        @Override
        protected void paintComponent(Graphics graphics) {
            Graphics2D g2 = (Graphics2D) graphics.create();
            if (getModel().isPressed()) g2.translate(1,1);
            g2.setStroke(new BasicStroke(3));
            g2.setColor(Color.BLACK);
            if (getModel().isRollover()) g2.setColor(Color.RED);
            int tweek = 4;
            g2.drawLine(tweek, tweek, getWidth() - tweek - 1, getHeight() - tweek - 1);
            g2.drawLine(getWidth() - tweek - 1, tweek, tweek, getHeight() - tweek - 1);
            g2.dispose();
        }
    }

    private final static MouseListener buttonMouseListener = new MouseAdapter() {
        @Override
        public void mouseEntered(MouseEvent event) {
            if (event.getComponent() instanceof AbstractButton) {
                ((AbstractButton)event.getComponent()).setBorderPainted(true);
            }
        }

        @Override
        public void mouseExited(MouseEvent event) {
            if (event.getComponent() instanceof AbstractButton) {
                ((AbstractButton)event.getComponent()).setBorderPainted(false);
            }
        }
    };
}
package tabbedtables;

import javax.swing.table.AbstractTableModel;

/**
 *
 */
public class TableModel extends AbstractTableModel {
    private final String[] columnNames = {
        "Column 1", "Column 2", "Column 3", "Column 4"
    };

    private Object[][] data = null;

    public TableModel(Object[][] initialData) {
        data = initialData;
    }

    @Override
    public int getColumnCount() {
        return columnNames.length;
    }

    @Override
    public int getRowCount() {
        return data != null ? data.length : 0 ;
    }

    @Override
    public String getColumnName(int column) {
        return columnNames[column];
    }

    @Override
    public Object getValueAt(int row, int col) {
        return data != null ? data[row][col] : null ;
    }

    @Override
    public Class getColumnClass(int col) {
        return getValueAt(0, col).getClass();
    }

    /**
     * We want to make the tables read-only. We'll use a complex dialog to
     * do any editing. Maybe later I'll do something fancier...
     *
     * @param row
     * @param col
     * @return
     */
    @Override
    public boolean isCellEditable(int row, int col) {
        return false;
    }

    @Override
    public void setValueAt(Object value, int row, int col) {
        if (data != null) {
            data[row][col] = value;
            fireTableCellUpdated(row, col);
        }
    }
}