Monday, January 02, 2006

Getting JTable columns widths to fit the data

I've been stumped on a JTable problem for quite a while now: how to get the columns to fit the width of the data in the cells. Out of the box, JTable uses the same width for all columns in a table. This is very annoying for non-editable tables which just display data. I want my Java tables to looks similar to HTML tables which resize column widths according to the cell contents.

A likely solution is simply that I did not search Google well enough, but information was frustratingly rare to find for me.

I did, however, eventually stumble across the prepareRenderer(TableCellRenderer renderer, int row, int column):Component method (and likewise prepareRenderer(TableCellEditor editor, int row, int column):Component). A flash of insight illuminated the solution at once:

{
    final TableCellRenderer renderer = getTableHeader()
            .getDefaultRenderer();

    for (int i = 0; i < getColumnCount(); ++i)
        getColumnModel().getColumn(i).setPreferredWidth(
                renderer.getTableCellRendererComponent(this,
                        getModel().getColumnName(i), false, false, 0, i)
                        .getPreferredSize().width);
}

public Component prepareRenderer(final TableCellRenderer renderer,
        final int row, final int column) {
    final Component prepareRenderer = super
            .prepareRenderer(renderer, row, column);
    final TableColumn tableColumn = getColumnModel().getColumn(column);

    tableColumn.setPreferredWidth(max(
            prepareRenderer.getPreferredSize().width,
            tableColumn.getPreferredWidth()));

    return prepareRenderer;
}

My solution has two parts. Addressing first prepareRenderer: as the table is drawn, adjust the preferred width of a column to be at least as wide as the widest cell seen so far. This was 90% of my problem right there. Immediately I stopped having truncated cells, and the table adjusted gracefully to new cell data which did not fit the old column widths.

The second part is more obscure: the instance initializer. Although my cell contents were now always visible, the proportions among the columns did not look quite right: narrow columns were not narrow enough with excess slop on either side of column titles. So I instructed the table to shrink column widths down to just that necessary to show the title (prepareRenderer will widen them again as needed for cell contents). Perfect!

What surprises me is that I was unable to find this or a similar solution anywhere in my Google searches. I can scarcely believe this is the first attempt at this approach. Hopefully Sun will include a solution in the JDK—this is not an uncommon JTable problem.

UPDATE: This topic continues to deliver: Alex Boosmeeuw posts with a different approach.

Sunday, January 01, 2006

More enum reinvention in Java

I posted earlier on reinventing the enum in Java for pre-JDK5 code. But I am also annoyed by one limitation in JDK5 enums, than they are not extendable at runtime or otherwise. So I copied the Enum class from the JDK and modified it to support non-final subclasses.

An example class and its extension from the unit tests, also demonstrating the abstract feature present in the JDK enum:

  1     private abstract static class TestEnumA
  2             extends ExtensibleEnum<TestEnumA> {
  3         public static final TestEnumA A = new TestEnumA("A") {
  4             void foo() {
  5             }
  6         };
  7         public static final TestEnumA B = new TestEnumA("B") {
  8             void foo() {
  9             }
 10         };
 11 
 12         static {
 13             new TestEnumA("C") {
 14                 void foo() {
 15                 }
 16             };
 17         }
 18 
 19         abstract void foo();
 20 
 21         public static TestEnumA[] values() {
 22             return ExtensibleEnum.values(TestEnumA.class);
 23         }
 24 
 25         public static TestEnumA valueOf(final String name) {
 26             return ExtensibleEnum.valueOf(TestEnumA.class, name);
 27         }
 28 
 29         protected TestEnumA(final String name) {
 30             super(name);
 31         }
 32     }
 33 
 34     private static abstract class TestEnumB
 35             extends TestEnumA {
 36         public static final TestEnumB D = new TestEnumB("D") {
 37             void foo() {
 38             }
 39         };
 40 
 41         public static TestEnumB[] values() {
 42             return ExtensibleEnum.values(TestEnumB.class);
 43         }
 44 
 45         public static TestEnumB valueOf(final String name) {
 46             return ExtensibleEnum.valueOf(TestEnumB.class, name);
 47         }
 48 
 49         protected TestEnumB(final String name) {
 50             super(name);
 51         }
 52     }
 53 }

Note that if you ask for values() from TestEnumA, they include TestEnumB.D—a TestEnumA by subclassing.

Here is the base class. I am also trying out the "Code as HTML" plugin for IDEA—I hope the colorizing is not too much:

  1 /*
  2  * Copyright (c) 2006 B. K. Oxley (binkley) <binkley@alumni.rice.edu> under the
  3  * terms of the Artistic License, including Clause 8.  See
  4  * http://www.opensource.org/licenses/artistic-license.php.
  5  */
  6 
  7 import java.io.Serializable;
  8 import java.lang.reflect.Array;
  9 import java.util.ArrayList;
 10 import java.util.EnumMap;
 11 import java.util.HashMap;
 12 import java.util.List;
 13 import java.util.Map;
 14 
 15 /**
 16  * {@code ExtensibleEnum} <strong>needs documentation</strong>.
 17  *
 18  * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
 19  * @version $Id$
 20  * @todo {@code RuntimeExtensibleEnum} needs documentation
 21  * @noinspection FinalizeDoesntCallSuperFinalize
 22  * @since Dec 31, 2005 8:38:03 AM
 23  */
 24 public abstract class ExtensibleEnum<E extends ExtensibleEnum<E>>
 25         implements Comparable<E>, Serializable {
 26     private static final Map<Class<? extends ExtensibleEnum<?>>,
 27             Map<String, ? extends ExtensibleEnum<?>>> BY_NAME
 28             = new HashMap<Class<? extends ExtensibleEnum<?>>,
 29             Map<String, ? extends ExtensibleEnum<?>>>(4);
 30     private static final Map<Class<? extends ExtensibleEnum<?>>,
 31             List<? extends ExtensibleEnum<?>>> BY_ORDINAL
 32             = new HashMap<Class<? extends ExtensibleEnum<?>>,
 33             List<? extends ExtensibleEnum<?>>>(4);
 34     /**
 35      * The JDK5 enum has ordinals in stepwise order; this implementation relaxes
 36      * the constraint to simply monotonically increasing.
 37      *
 38      * @noinspection StaticNonFinalField
 39      */
 40     private static int ORDINAL;
 41 
 42     /**
 43      * The name of this enum constant, as declared in the enum declaration. Most
 44      * programmers should use the {@link #toString} method rather than accessing
 45      * this field.
 46      */
 47     private final String name;
 48 
 49     /**
 50      * Returns the name of this enum constant, exactly as declared in its enum
 51      * declaration.
 52      * <p/>
 53      * <b>Most programmers should use the {@link #toString} method in preference
 54      * to this one, as the toString method may return a more user-friendly
 55      * name.</b>  This method is designed primarily for use in specialized
 56      * situations where correctness depends on getting the exact name, which
 57      * will not vary from release to release.
 58      *
 59      * @return the name of this enum constant
 60      */
 61     public final String name() {
 62         return name;
 63     }
 64 
 65     /**
 66      * The ordinal of this enumeration constant (its position in the enum
 67      * declaration, where the initial constant is assigned an ordinal of zero).
 68      * <p/>
 69      * Most programmers will have no use for this field.  It is designed for use
 70      * by sophisticated enum-based data structures, such as {@link
 71      * java.util.EnumSet} and {@link EnumMap}.
 72      */
 73     private final int ordinal = ORDINAL++;
 74 
 75     /**
 76      * Returns the ordinal of this enumeration constant (its position in its
 77      * enum declaration, where the initial constant is assigned an ordinal of
 78      * zero).
 79      * <p/>
 80      * Most programmers will have no use for this method.  It is designed for
 81      * use by sophisticated enum-based data structures, such as {@link
 82      * java.util.EnumSet} and {@link EnumMap}.
 83      *
 84      * @return the ordinal of this enumeration constant
 85      */
 86     public final int ordinal() {
 87         return ordinal;
 88     }
 89 
 90     /**
 91      * Sole constructor.  Programmers cannot invoke this constructor. It is for
 92      * use by code emitted by the compiler in response to enum type
 93      * declarations.
 94      *
 95      * @param name - The name of this enum constant, which is the identifier
 96      * used to declare it.
 97      */
 98     protected ExtensibleEnum(final String name) {
 99         if (null == name)
100             throw new NullPointerException("Missing name");
101 
102         this.name = name;
103 
104         new EnumInstall(getDeclaringClass(), name, this).install();
105     }
106 
107     private static class EnumInstall<T extends ExtensibleEnum<T>, U extends T> {
108         private final Class<T> enumType;
109         private final String name;
110         private final U instance;
111 
112         EnumInstall(final Class<T> enumType, final String name,
113                 final U instance) {
114             this.enumType = enumType;
115             this.name = name;
116             this.instance = instance;
117         }
118 
119         void install() {
120             if (!ExtensibleEnum.class.isAssignableFrom(enumType))
121                 return;
122 
123             final EnumInstall<? super T, ? super U> parentInstall
124                     = new EnumInstall(enumType.getSuperclass(), name, instance);
125 
126             parentInstall.install();
127 
128             try {
129                 putEnum(enumType, name, instance);
130 
131             } catch (final IllegalArgumentException e) {
132                 parentInstall.uninstall();
133 
134                 throw e;
135             }
136         }
137 
138         void uninstall() {
139             unputEnum(enumType, name);
140         }
141     }
142 
143     private static <T extends ExtensibleEnum<T>, U extends T> void putEnum(
144             final Class<T> enumType, final String name, final U instance) {
145         if (!BY_NAME.containsKey(enumType)) {
146             BY_NAME.put(enumType, new HashMap<String, T>(4));
147             BY_ORDINAL.put(enumType, new ArrayList<T>(4));
148         }
149 
150         final Map<String, T> nameMap = byName(enumType);
151 
152         if (nameMap.containsKey(name))
153             throw new IllegalArgumentException("Duplicate name: " + name);
154 
155         nameMap.put(name, instance);
156         byOrdinal(enumType).add(instance);
157     }
158 
159     private static <T extends ExtensibleEnum<T>> void unputEnum(
160             final Class<T> enumType, final String name) {
161         if (!BY_NAME.containsKey(enumType))
162             return;
163 
164         byName(enumType).remove(name);
165 
166         final List<T> list = byOrdinal(enumType);
167 
168         list.remove(list.size() - 1);
169     }
170 
171     /**
172      * Returns the name of this enum constant, as contained in the declaration.
173      * This method may be overridden, though it typically isn't necessary or
174      * desirable.  An enum type should override this method when a more
175      * "programmer-friendly" string form exists.
176      *
177      * @return the name of this enum constant
178      */
179     @Override
180     public String toString() {
181         return name;
182     }
183 
184     /**
185      * Returns true if the specified object is equal to this enum constant.
186      *
187      * @param other the object to be compared for equality with this object.
188      *
189      * @return true if the specified object is equal to this enum constant.
190      */
191     @Override
192     public final boolean equals(final Object other) {
193         return this == other;
194     }
195 
196     /**
197      * Returns a hash code for this enum constant.
198      *
199      * @return a hash code for this enum constant.
200      */
201     @Override
202     public final int hashCode() {
203         return System.identityHashCode(this);
204     }
205 
206     /**
207      * Throws CloneNotSupportedException.  This guarantees that enums are never
208      * cloned, which is necessary to preserve their "singleton" status.
209      *
210      * @return (never returns)
211      */
212     @Override
213     protected final Object clone()
214             throws CloneNotSupportedException {
215         throw new CloneNotSupportedException();
216     }
217 
218     /**
219      * Compares this enum with the specified object for order.  Returns a
220      * negative integer, zero, or a positive integer as this object is less
221      * than, equal to, or greater than the specified object.
222      * <p/>
223      * ExtensibleEnum constants are only comparable to other enum constants of
224      * the same enum type.  The natural order implemented by this method is the
225      * order in which the constants are declared.
226      *
227      * @noinspection ObjectEquality
228      */
229     public final int compareTo(final E o) {
230         if (getClass() != o.getClass() // optimization
231                 && getDeclaringClass() != o.getDeclaringClass())
232             throw new ClassCastException();
233 
234         return new Integer(ordinal).compareTo(o.ordinal);
235     }
236 
237     public static <T extends ExtensibleEnum> T[] values(
238             final Class<T> enumType) {
239         final List<T> values = byOrdinal(enumType);
240 
241         return values.toArray((T[]) Array.newInstance(enumType, values.size()));
242     }
243 
244     /**
245      * Returns the enum constant of the specified enum type with the specified
246      * name.  The name must match exactly an identifier used to declare an enum
247      * constant in this type.  (Extraneous whitespace characters are not
248      * permitted.)
249      *
250      * @param enumType the <tt>Class</tt> object of the enum type from which to
251      * return a constant
252      * @param name the name of the constant to return
253      *
254      * @return the enum constant of the specified enum type with the specified
255      *         name
256      *
257      * @throws IllegalArgumentException if the specified enum type has no
258      * constant with the specified name, or the specified class object does not
259      * represent an enum type
260      * @throws NullPointerException if <tt>enumType</tt> or <tt>name</tt> is
261      * null
262      * @since 1.5
263      */
264     public static <T extends ExtensibleEnum> T valueOf(final Class<T> enumType,
265             final String name) {
266         // IDEA says cast is useless; JDK5 javac says it is necessary
267         //noinspection RedundantCast
268         final T result = (T) byName(enumType).get(name);
269 
270         if (null != result)
271             return result;
272         if (null == name)
273             throw new NullPointerException("Name is null");
274 
275         throw new IllegalArgumentException(
276                 "No enum const " + enumType + '.' + name);
277     }
278 
279     /**
280      * enum classes cannot have finalize methods.
281      */
282     @Override
283     protected final void finalize() { }
284 
285     protected Class<?> getDeclaringClass() {
286         final Class<?> enumType = getClass();
287 
288         return enumType.isAnonymousClass()
289                 ? enumType.getSuperclass()
290                 : enumType;
291     }
292 
293     private static <T extends ExtensibleEnum<T>> Map<String, T> byName(
294             final Class<T> enumType) {
295         return (Map<String, T>) BY_NAME.get(enumType);
296     }
297 
298     private static <T extends ExtensibleEnum<T>> List<T> byOrdinal(
299             final Class<T> enumType) {
300         return (List<T>) BY_ORDINAL.get(enumType);
301     }
302 }

Happy New Year!