001 /* DefaultTableColumnModel.java --
002 Copyright (C) 2002, 2004, 2005, 2006, Free Software Foundation, Inc.
003
004 This file is part of GNU Classpath.
005
006 GNU Classpath is free software; you can redistribute it and/or modify
007 it under the terms of the GNU General Public License as published by
008 the Free Software Foundation; either version 2, or (at your option)
009 any later version.
010
011 GNU Classpath is distributed in the hope that it will be useful, but
012 WITHOUT ANY WARRANTY; without even the implied warranty of
013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014 General Public License for more details.
015
016 You should have received a copy of the GNU General Public License
017 along with GNU Classpath; see the file COPYING. If not, write to the
018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019 02110-1301 USA.
020
021 Linking this library statically or dynamically with other modules is
022 making a combined work based on this library. Thus, the terms and
023 conditions of the GNU General Public License cover the whole
024 combination.
025
026 As a special exception, the copyright holders of this library give you
027 permission to link this library with independent modules to produce an
028 executable, regardless of the license terms of these independent
029 modules, and to copy and distribute the resulting executable under
030 terms of your choice, provided that you also meet, for each linked
031 independent module, the terms and conditions of the license of that
032 module. An independent module is a module which is not derived from
033 or based on this library. If you modify this library, you may extend
034 this exception to your version of the library, but you are not
035 obligated to do so. If you do not wish to do so, delete this
036 exception statement from your version. */
037
038
039 package javax.swing.table;
040
041 import java.beans.PropertyChangeEvent;
042 import java.beans.PropertyChangeListener;
043 import java.io.Serializable;
044 import java.util.Enumeration;
045 import java.util.EventListener;
046 import java.util.Vector;
047
048 import javax.swing.DefaultListSelectionModel;
049 import javax.swing.JTable;
050 import javax.swing.ListSelectionModel;
051 import javax.swing.event.ChangeEvent;
052 import javax.swing.event.EventListenerList;
053 import javax.swing.event.ListSelectionEvent;
054 import javax.swing.event.ListSelectionListener;
055 import javax.swing.event.TableColumnModelEvent;
056 import javax.swing.event.TableColumnModelListener;
057
058 /**
059 * A model that stores information about the columns used in a {@link JTable}.
060 *
061 * @see JTable#setColumnModel(TableColumnModel)
062 *
063 * @author Andrew Selkirk
064 */
065 public class DefaultTableColumnModel
066 implements TableColumnModel, PropertyChangeListener, ListSelectionListener,
067 Serializable
068 {
069 private static final long serialVersionUID = 6580012493508960512L;
070
071 /**
072 * Storage for the table columns.
073 */
074 protected Vector<TableColumn> tableColumns;
075
076 /**
077 * A selection model that keeps track of column selections.
078 */
079 protected ListSelectionModel selectionModel;
080
081 /**
082 * The space between the columns (the default value is <code>1</code>).
083 */
084 protected int columnMargin;
085
086 /**
087 * Storage for the listeners registered with the model.
088 */
089 protected EventListenerList listenerList = new EventListenerList();
090
091 /**
092 * A change event used when notifying listeners of a change to the
093 * <code>columnMargin</code> field. This single event is reused for all
094 * notifications (it is lazily instantiated within the
095 * {@link #fireColumnMarginChanged()} method).
096 */
097 protected transient ChangeEvent changeEvent;
098
099 /**
100 * A flag that indicates whether or not columns can be selected.
101 */
102 protected boolean columnSelectionAllowed;
103
104 /**
105 * The total width of all the columns in this model.
106 */
107 protected int totalColumnWidth;
108
109 /**
110 * Creates a new table column model with zero columns. A default column
111 * selection model is created by calling {@link #createSelectionModel()}.
112 * The default value for <code>columnMargin</code> is <code>1</code> and
113 * the default value for <code>columnSelectionAllowed</code> is
114 * <code>false</code>.
115 */
116 public DefaultTableColumnModel()
117 {
118 tableColumns = new Vector();
119 selectionModel = createSelectionModel();
120 selectionModel.addListSelectionListener(this);
121 columnMargin = 1;
122 columnSelectionAllowed = false;
123 }
124
125 /**
126 * Adds a column to the model then calls
127 * {@link #fireColumnAdded(TableColumnModelEvent)} to notify the registered
128 * listeners. The model registers itself with the column as a
129 * {@link PropertyChangeListener} so that changes to the column width will
130 * invalidate the cached {@link #totalColumnWidth} value.
131 *
132 * @param column the column (<code>null</code> not permitted).
133 *
134 * @throws IllegalArgumentException if <code>column</code> is
135 * <code>null</code>.
136 *
137 * @see #removeColumn(TableColumn)
138 */
139 public void addColumn(TableColumn column)
140 {
141 if (column == null)
142 throw new IllegalArgumentException("Null 'col' argument.");
143 tableColumns.add(column);
144 column.addPropertyChangeListener(this);
145 invalidateWidthCache();
146 fireColumnAdded(new TableColumnModelEvent(this, 0,
147 tableColumns.size() - 1));
148 }
149
150 /**
151 * Removes a column from the model then calls
152 * {@link #fireColumnRemoved(TableColumnModelEvent)} to notify the registered
153 * listeners. If the specified column does not belong to the model, or is
154 * <code>null</code>, this method does nothing.
155 *
156 * @param column the column to be removed (<code>null</code> permitted).
157 *
158 * @see #addColumn(TableColumn)
159 */
160 public void removeColumn(TableColumn column)
161 {
162 int index = this.tableColumns.indexOf(column);
163 if (index < 0)
164 return;
165 tableColumns.remove(column);
166 fireColumnRemoved(new TableColumnModelEvent(this, index, 0));
167 column.removePropertyChangeListener(this);
168 invalidateWidthCache();
169 }
170
171 /**
172 * Moves the column at index i to the position specified by index j, then
173 * calls {@link #fireColumnMoved(TableColumnModelEvent)} to notify registered
174 * listeners.
175 *
176 * @param i index of the column that will be moved.
177 * @param j index of the column's new location.
178 *
179 * @throws IllegalArgumentException if <code>i</code> or <code>j</code> are
180 * outside the range <code>0</code> to <code>N-1</code>, where
181 * <code>N</code> is the column count.
182 */
183 public void moveColumn(int i, int j)
184 {
185 int columnCount = getColumnCount();
186 if (i < 0 || i >= columnCount)
187 throw new IllegalArgumentException("Index 'i' out of range.");
188 if (j < 0 || j >= columnCount)
189 throw new IllegalArgumentException("Index 'j' out of range.");
190 TableColumn column = tableColumns.remove(i);
191 tableColumns.add(j, column);
192 fireColumnMoved(new TableColumnModelEvent(this, i, j));
193 }
194
195 /**
196 * Sets the column margin then calls {@link #fireColumnMarginChanged()} to
197 * notify the registered listeners.
198 *
199 * @param margin the column margin.
200 *
201 * @see #getColumnMargin()
202 */
203 public void setColumnMargin(int margin)
204 {
205 columnMargin = margin;
206 fireColumnMarginChanged();
207 }
208
209 /**
210 * Returns the number of columns in the model.
211 *
212 * @return The column count.
213 */
214 public int getColumnCount()
215 {
216 return tableColumns.size();
217 }
218
219 /**
220 * Returns an enumeration of the columns in the model.
221 *
222 * @return An enumeration of the columns in the model.
223 */
224 public Enumeration<TableColumn> getColumns()
225 {
226 return tableColumns.elements();
227 }
228
229 /**
230 * Returns the index of the {@link TableColumn} with the given identifier.
231 *
232 * @param identifier the identifier (<code>null</code> not permitted).
233 *
234 * @return The index of the {@link TableColumn} with the given identifier.
235 *
236 * @throws IllegalArgumentException if <code>identifier</code> is
237 * <code>null</code> or there is no column with that identifier.
238 */
239 public int getColumnIndex(Object identifier)
240 {
241 if (identifier == null)
242 throw new IllegalArgumentException("Null identifier.");
243 int columnCount = tableColumns.size();
244 for (int i = 0; i < columnCount; i++)
245 {
246 TableColumn tc = tableColumns.get(i);
247 if (identifier.equals(tc.getIdentifier()))
248 return i;
249 }
250 throw new IllegalArgumentException("No TableColumn with that identifier.");
251 }
252
253 /**
254 * Returns the column at the specified index.
255 *
256 * @param columnIndex the column index (in the range from <code>0</code> to
257 * <code>N-1</code>, where <code>N</code> is the number of columns in
258 * the model).
259 *
260 * @return The column at the specified index.
261 *
262 * @throws ArrayIndexOutOfBoundsException if <code>i</code> is not within
263 * the specified range.
264 */
265 public TableColumn getColumn(int columnIndex)
266 {
267 return tableColumns.get(columnIndex);
268 }
269
270 /**
271 * Returns the column margin.
272 *
273 * @return The column margin.
274 *
275 * @see #setColumnMargin(int)
276 */
277 public int getColumnMargin()
278 {
279 return columnMargin;
280 }
281
282 /**
283 * Returns the index of the column that contains the specified x-coordinate.
284 * This method assumes that:
285 * <ul>
286 * <li>column zero begins at position zero;</li>
287 * <li>all columns appear in order;</li>
288 * <li>individual column widths are taken into account, but the column margin
289 * is ignored.</li>
290 * </ul>
291 * If no column contains the specified position, this method returns
292 * <code>-1</code>.
293 *
294 * @param x the x-position.
295 *
296 * @return The column index, or <code>-1</code>.
297 */
298 public int getColumnIndexAtX(int x)
299 {
300 for (int i = 0; i < tableColumns.size(); ++i)
301 {
302 int w = (tableColumns.get(i)).getWidth();
303 if (0 <= x && x < w)
304 return i;
305 else
306 x -= w;
307 }
308 return -1;
309 }
310
311 /**
312 * Returns total width of all the columns in the model, ignoring the
313 * {@link #columnMargin}.
314 *
315 * @return The total width of all the columns.
316 */
317 public int getTotalColumnWidth()
318 {
319 if (totalColumnWidth == -1)
320 recalcWidthCache();
321 return totalColumnWidth;
322 }
323
324 /**
325 * Sets the selection model that will be used to keep track of the selected
326 * columns.
327 *
328 * @param model the selection model (<code>null</code> not permitted).
329 *
330 * @throws IllegalArgumentException if <code>model</code> is
331 * <code>null</code>.
332 *
333 * @see #getSelectionModel()
334 */
335 public void setSelectionModel(ListSelectionModel model)
336 {
337 if (model == null)
338 throw new IllegalArgumentException();
339
340 selectionModel.removeListSelectionListener(this);
341 selectionModel = model;
342 selectionModel.addListSelectionListener(this);
343 }
344
345 /**
346 * Returns the selection model used to track table column selections.
347 *
348 * @return The selection model.
349 *
350 * @see #setSelectionModel(ListSelectionModel)
351 */
352 public ListSelectionModel getSelectionModel()
353 {
354 return selectionModel;
355 }
356
357 /**
358 * Sets the flag that indicates whether or not column selection is allowed.
359 *
360 * @param flag the new flag value.
361 *
362 * @see #getColumnSelectionAllowed()
363 */
364 public void setColumnSelectionAllowed(boolean flag)
365 {
366 columnSelectionAllowed = flag;
367 }
368
369 /**
370 * Returns <code>true</code> if column selection is allowed, and
371 * <code>false</code> if column selection is not allowed.
372 *
373 * @return A boolean.
374 *
375 * @see #setColumnSelectionAllowed(boolean)
376 */
377 public boolean getColumnSelectionAllowed()
378 {
379 return columnSelectionAllowed;
380 }
381
382 /**
383 * Returns an array containing the indices of the selected columns.
384 *
385 * @return An array containing the indices of the selected columns.
386 */
387 public int[] getSelectedColumns()
388 {
389 // FIXME: Implementation of this method was taken from private method
390 // JTable.getSelections(), which is used in various places in JTable
391 // including selected row calculations and cannot be simply removed.
392 // This design should be improved to illuminate duplication of code.
393
394 ListSelectionModel lsm = this.selectionModel;
395 int sz = getSelectedColumnCount();
396 int [] ret = new int[sz];
397
398 int lo = lsm.getMinSelectionIndex();
399 int hi = lsm.getMaxSelectionIndex();
400 int j = 0;
401 java.util.ArrayList ls = new java.util.ArrayList();
402 if (lo != -1 && hi != -1)
403 {
404 switch (lsm.getSelectionMode())
405 {
406 case ListSelectionModel.SINGLE_SELECTION:
407 ret[0] = lo;
408 break;
409
410 case ListSelectionModel.SINGLE_INTERVAL_SELECTION:
411 for (int i = lo; i <= hi; ++i)
412 ret[j++] = i;
413 break;
414
415 case ListSelectionModel.MULTIPLE_INTERVAL_SELECTION:
416 for (int i = lo; i <= hi; ++i)
417 if (lsm.isSelectedIndex(i))
418 ret[j++] = i;
419 break;
420 }
421 }
422 return ret;
423 }
424
425 /**
426 * Returns the number of selected columns in the model.
427 *
428 * @return The selected column count.
429 *
430 * @see #getSelectionModel()
431 */
432 public int getSelectedColumnCount()
433 {
434 // FIXME: Implementation of this method was taken from private method
435 // JTable.countSelections(), which is used in various places in JTable
436 // including selected row calculations and cannot be simply removed.
437 // This design should be improved to illuminate duplication of code.
438
439 ListSelectionModel lsm = this.selectionModel;
440 int lo = lsm.getMinSelectionIndex();
441 int hi = lsm.getMaxSelectionIndex();
442 int sum = 0;
443
444 if (lo != -1 && hi != -1)
445 {
446 switch (lsm.getSelectionMode())
447 {
448 case ListSelectionModel.SINGLE_SELECTION:
449 sum = 1;
450 break;
451
452 case ListSelectionModel.SINGLE_INTERVAL_SELECTION:
453 sum = hi - lo + 1;
454 break;
455
456 case ListSelectionModel.MULTIPLE_INTERVAL_SELECTION:
457 for (int i = lo; i <= hi; ++i)
458 if (lsm.isSelectedIndex(i))
459 ++sum;
460 break;
461 }
462 }
463
464 return sum;
465 }
466
467 /**
468 * Registers a listener with the model, so that it will receive
469 * {@link TableColumnModelEvent} notifications.
470 *
471 * @param listener the listener (<code>null</code> ignored).
472 */
473 public void addColumnModelListener(TableColumnModelListener listener)
474 {
475 listenerList.add(TableColumnModelListener.class, listener);
476 }
477
478 /**
479 * Deregisters a listener so that it no longer receives notification of
480 * changes to this model.
481 *
482 * @param listener the listener to remove
483 */
484 public void removeColumnModelListener(TableColumnModelListener listener)
485 {
486 listenerList.remove(TableColumnModelListener.class, listener);
487 }
488
489 /**
490 * Returns an array containing the listeners that are registered with the
491 * model. If there are no listeners, an empty array is returned.
492 *
493 * @return An array containing the listeners that are registered with the
494 * model.
495 *
496 * @see #addColumnModelListener(TableColumnModelListener)
497 * @since 1.4
498 */
499 public TableColumnModelListener[] getColumnModelListeners()
500 {
501 return (TableColumnModelListener[])
502 listenerList.getListeners(TableColumnModelListener.class);
503 }
504
505 /**
506 * Sends the specified {@link TableColumnModelEvent} to all registered
507 * listeners, to indicate that a column has been added to the model. The
508 * event's <code>toIndex</code> attribute should contain the index of the
509 * added column.
510 *
511 * @param e the event.
512 *
513 * @see #addColumn(TableColumn)
514 */
515 protected void fireColumnAdded(TableColumnModelEvent e)
516 {
517 TableColumnModelListener[] listeners = getColumnModelListeners();
518
519 for (int i = 0; i < listeners.length; i++)
520 listeners[i].columnAdded(e);
521 }
522
523 /**
524 * Sends the specified {@link TableColumnModelEvent} to all registered
525 * listeners, to indicate that a column has been removed from the model. The
526 * event's <code>fromIndex</code> attribute should contain the index of the
527 * removed column.
528 *
529 * @param e the event.
530 *
531 * @see #removeColumn(TableColumn)
532 */
533 protected void fireColumnRemoved(TableColumnModelEvent e)
534 {
535 TableColumnModelListener[] listeners = getColumnModelListeners();
536
537 for (int i = 0; i < listeners.length; i++)
538 listeners[i].columnRemoved(e);
539 }
540
541 /**
542 * Sends the specified {@link TableColumnModelEvent} to all registered
543 * listeners, to indicate that a column in the model has been moved. The
544 * event's <code>fromIndex</code> attribute should contain the old column
545 * index, and the <code>toIndex</code> attribute should contain the new
546 * column index.
547 *
548 * @param e the event.
549 *
550 * @see #moveColumn(int, int)
551 */
552 protected void fireColumnMoved(TableColumnModelEvent e)
553 {
554 TableColumnModelListener[] listeners = getColumnModelListeners();
555
556 for (int i = 0; i < listeners.length; i++)
557 listeners[i].columnMoved(e);
558 }
559
560 /**
561 * Sends the specified {@link ListSelectionEvent} to all registered listeners,
562 * to indicate that the column selections have changed.
563 *
564 * @param e the event.
565 *
566 * @see #valueChanged(ListSelectionEvent)
567 */
568 protected void fireColumnSelectionChanged(ListSelectionEvent e)
569 {
570 EventListener [] listeners = getListeners(TableColumnModelListener.class);
571 for (int i = 0; i < listeners.length; ++i)
572 ((TableColumnModelListener) listeners[i]).columnSelectionChanged(e);
573 }
574
575 /**
576 * Sends a {@link ChangeEvent} to the model's registered listeners to
577 * indicate that the column margin was changed.
578 *
579 * @see #setColumnMargin(int)
580 */
581 protected void fireColumnMarginChanged()
582 {
583 EventListener[] listeners = getListeners(TableColumnModelListener.class);
584 if (changeEvent == null && listeners.length > 0)
585 changeEvent = new ChangeEvent(this);
586 for (int i = 0; i < listeners.length; ++i)
587 ((TableColumnModelListener) listeners[i]).columnMarginChanged(changeEvent);
588 }
589
590 /**
591 * Returns an array containing the listeners (of the specified type) that
592 * are registered with this model.
593 *
594 * @param listenerType the listener type (must indicate a subclass of
595 * {@link EventListener}, <code>null</code> not permitted).
596 *
597 * @return An array containing the listeners (of the specified type) that
598 * are registered with this model.
599 */
600 public <T extends EventListener> T[] getListeners(Class<T> listenerType)
601 {
602 return listenerList.getListeners(listenerType);
603 }
604
605 /**
606 * Receives notification of property changes for the columns in the model.
607 * If the <code>width</code> property for any column changes, we invalidate
608 * the {@link #totalColumnWidth} value here.
609 *
610 * @param event the event.
611 */
612 public void propertyChange(PropertyChangeEvent event)
613 {
614 if (event.getPropertyName().equals("width"))
615 invalidateWidthCache();
616 }
617
618 /**
619 * Receives notification of the change to the list selection model, and
620 * responds by calling
621 * {@link #fireColumnSelectionChanged(ListSelectionEvent)}.
622 *
623 * @param e the list selection event.
624 *
625 * @see #getSelectionModel()
626 */
627 public void valueChanged(ListSelectionEvent e)
628 {
629 fireColumnSelectionChanged(e);
630 }
631
632 /**
633 * Creates a default selection model to track the currently selected
634 * column(s). This method is called by the constructor and returns a new
635 * instance of {@link DefaultListSelectionModel}.
636 *
637 * @return A new default column selection model.
638 */
639 protected ListSelectionModel createSelectionModel()
640 {
641 return new DefaultListSelectionModel();
642 }
643
644 /**
645 * Recalculates the total width of the columns, if the cached value is
646 * <code>-1</code>. Otherwise this method does nothing.
647 *
648 * @see #getTotalColumnWidth()
649 */
650 protected void recalcWidthCache()
651 {
652 if (totalColumnWidth == -1)
653 {
654 totalColumnWidth = 0;
655 for (int i = 0; i < tableColumns.size(); ++i)
656 {
657 totalColumnWidth += tableColumns.get(i).getWidth();
658 }
659 }
660 }
661
662 /**
663 * Sets the {@link #totalColumnWidth} field to <code>-1</code>.
664 *
665 * @see #recalcWidthCache()
666 */
667 private void invalidateWidthCache()
668 {
669 totalColumnWidth = -1;
670 }
671 }