001/* DefaultTableColumnModel.java --
002   Copyright (C) 2002, 2004, 2005, 2006,  Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.table;
040
041import java.beans.PropertyChangeEvent;
042import java.beans.PropertyChangeListener;
043import java.io.Serializable;
044import java.util.Enumeration;
045import java.util.EventListener;
046import java.util.Vector;
047
048import javax.swing.DefaultListSelectionModel;
049import javax.swing.JTable;
050import javax.swing.ListSelectionModel;
051import javax.swing.event.ChangeEvent;
052import javax.swing.event.EventListenerList;
053import javax.swing.event.ListSelectionEvent;
054import javax.swing.event.ListSelectionListener;
055import javax.swing.event.TableColumnModelEvent;
056import 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 */
065public 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}