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    }