001/* BasicTableHeaderUI.java --
002   Copyright (C) 2004 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.plaf.basic;
040
041import java.awt.Component;
042import java.awt.Cursor;
043import java.awt.Dimension;
044import java.awt.Graphics;
045import java.awt.Rectangle;
046import java.awt.event.ActionEvent;
047import java.awt.event.ActionListener;
048import java.awt.event.MouseEvent;
049
050import javax.swing.CellRendererPane;
051import javax.swing.JComponent;
052import javax.swing.LookAndFeel;
053import javax.swing.Timer;
054import javax.swing.UIManager;
055import javax.swing.border.Border;
056import javax.swing.event.MouseInputListener;
057import javax.swing.plaf.ComponentUI;
058import javax.swing.plaf.TableHeaderUI;
059import javax.swing.table.JTableHeader;
060import javax.swing.table.TableCellRenderer;
061import javax.swing.table.TableColumn;
062import javax.swing.table.TableColumnModel;
063
064/**
065 * Basic pluggable look and feel interface for JTableHeader.
066 */
067public class BasicTableHeaderUI extends TableHeaderUI
068{
069  /**
070   * The width of the space (in both direction) around the column boundary,
071   * where mouse cursor changes shape into "resize"
072   */
073  static int COLUMN_BOUNDARY_TOLERANCE = 3;
074
075  public static ComponentUI createUI(JComponent h)
076  {
077    return new BasicTableHeaderUI();
078  }
079
080  /**
081   * The table header that is using this interface.
082   */
083  protected JTableHeader header;
084
085  /**
086   * The mouse input listener, responsible for mouse manipulations with
087   * the table header.
088   */
089  protected MouseInputListener mouseInputListener;
090
091  /**
092   * Paint the header cell.
093   */
094  protected CellRendererPane rendererPane;
095
096  /**
097   * The header cell border.
098   */
099  private Border cellBorder;
100
101  /**
102   * Original mouse cursor prior to resizing.
103   */
104  private Cursor originalCursor;
105
106  /**
107   * If not null, one of the columns is currently being dragged.
108   */
109  Rectangle draggingHeaderRect;
110
111  /**
112   * Handles column movement and rearrangement by mouse. The same instance works
113   * both as mouse listener and the mouse motion listner.
114   */
115  public class MouseInputHandler
116      implements MouseInputListener
117  {
118    /**
119     * If true, the cursor is being already shown in the alternative "resize"
120     * shape.
121     */
122    boolean showingResizeCursor;
123
124    /**
125     * The position, from where the cursor is dragged during resizing. Double
126     * purpose field (absolute value during resizing and relative offset during
127     * column dragging).
128     */
129    int draggingFrom = - 1;
130
131    /**
132     * The number of the column being dragged.
133     */
134    int draggingColumnNumber;
135
136    /**
137     * The previous preferred width of the column.
138     */
139    int prevPrefWidth = - 1;
140
141    /**
142     * The timer to coalesce column resizing events.
143     */
144    Timer timer;
145
146    /**
147     * Returns without action, part of the MouseInputListener interface.
148     */
149    public void mouseClicked(MouseEvent e)
150    {
151      // Nothing to do.
152    }
153
154    /**
155     * If being in the resizing mode, handle resizing.
156     */
157    public void mouseDragged(MouseEvent e)
158    {
159      TableColumn resizeIt = header.getResizingColumn();
160      if (resizeIt != null && header.getResizingAllowed())
161        {
162          // The timer is intialised on demand.
163          if (timer == null)
164            {
165              // The purpose of timer is to coalesce events. If the queue
166              // is free, the repaint event is fired immediately.
167              timer = new Timer(1, new ActionListener()
168              {
169                public void actionPerformed(ActionEvent e)
170                {
171                  header.getTable().doLayout();
172                }
173              });
174              timer.setRepeats(false);
175              timer.setCoalesce(true);
176            }
177          resizeIt.setPreferredWidth(prevPrefWidth + e.getX() - draggingFrom);
178          timer.restart();
179        }
180      else if (draggingHeaderRect != null && header.getReorderingAllowed())
181        {
182          draggingHeaderRect.x = e.getX() + draggingFrom;
183          header.repaint();
184        }
185    }
186
187    /**
188     * Returns without action, part of the MouseInputListener interface.
189     */
190    public void mouseEntered(MouseEvent e)
191    {
192      // Nothing to do.
193    }
194
195    /**
196     * Reset drag information of the column resizing.
197     */
198    public void mouseExited(MouseEvent e)
199    {
200      // Nothing to do.
201    }
202
203    /**
204     * Change the mouse cursor if the mouse if above the column boundary.
205     */
206    public void mouseMoved(MouseEvent e)
207    {
208      // When dragging, the functionality is handled by the mouseDragged.
209      if (e.getButton() == 0 && header.getResizingAllowed())
210        {
211          TableColumnModel model = header.getColumnModel();
212          int n = model.getColumnCount();
213          if (n < 2)
214            // It must be at least two columns to have at least one boundary.
215            // Otherwise, nothing to do.
216            return;
217
218          boolean onBoundary = false;
219
220          int x = e.getX();
221          int a = x - COLUMN_BOUNDARY_TOLERANCE;
222          int b = x + COLUMN_BOUNDARY_TOLERANCE;
223
224          int p = 0;
225
226          Scan: for (int i = 0; i < n - 1; i++)
227            {
228              p += model.getColumn(i).getWidth();
229
230              if (p >= a && p <= b)
231                {
232                  TableColumn column = model.getColumn(i);
233                  onBoundary = true;
234
235                  draggingFrom = x;
236                  prevPrefWidth = column.getWidth();
237                  header.setResizingColumn(column);
238                  break Scan;
239                }
240            }
241
242          if (onBoundary != showingResizeCursor)
243            {
244              // Change the cursor shape, if needed.
245              if (onBoundary)
246                {
247
248                  originalCursor = header.getCursor();
249                  if (p < x)
250                    header.setCursor(Cursor.getPredefinedCursor(
251                        Cursor.W_RESIZE_CURSOR));
252                  else
253                    header.setCursor(Cursor.getPredefinedCursor(
254                        Cursor.E_RESIZE_CURSOR));
255                }
256              else
257                {
258                  header.setCursor(originalCursor);
259                  header.setResizingColumn(null);
260                }
261
262              showingResizeCursor = onBoundary;
263            }
264        }
265    }
266
267    /**
268     * Starts the dragging/resizing procedure.
269     */
270    public void mousePressed(MouseEvent e)
271    {
272      if (header.getResizingAllowed())
273        {
274          TableColumn resizingColumn = header.getResizingColumn();
275          if (resizingColumn != null)
276            {
277              resizingColumn.setPreferredWidth(resizingColumn.getWidth());
278              return;
279            }
280        }
281
282      if (header.getReorderingAllowed())
283        {
284          TableColumnModel model = header.getColumnModel();
285          int n = model.getColumnCount();
286          if (n < 2)
287            // It must be at least two columns to change the column location.
288            return;
289
290          boolean onBoundary = false;
291
292          int x = e.getX();
293          int p = 0;
294          int col = - 1;
295
296          Scan: for (int i = 0; i < n; i++)
297            {
298              p += model.getColumn(i).getWidth();
299              if (p > x)
300                {
301                  col = i;
302                  break Scan;
303                }
304            }
305          if (col < 0)
306            return;
307
308          TableColumn dragIt = model.getColumn(col);
309          header.setDraggedColumn(dragIt);
310
311          draggingFrom = (p - dragIt.getWidth()) - x;
312          draggingHeaderRect = new Rectangle(header.getHeaderRect(col));
313          draggingColumnNumber = col;
314        }
315    }
316
317    /**
318     * Set all column preferred width to the current width to prevend abrupt
319     * width changes during the next resize.
320     */
321    public void mouseReleased(MouseEvent e)
322    {
323      if (header.getResizingColumn() != null && header.getResizingAllowed())
324        endResizing();
325      if (header.getDraggedColumn() != null &&  header.getReorderingAllowed())
326        endDragging(e);
327    }
328
329    /**
330     * Stop resizing session.
331     */
332    void endResizing()
333    {
334      TableColumnModel model = header.getColumnModel();
335      int n = model.getColumnCount();
336      if (n > 2)
337        {
338          TableColumn c;
339          for (int i = 0; i < n; i++)
340            {
341              c = model.getColumn(i);
342              c.setPreferredWidth(c.getWidth());
343            }
344        }
345      header.setResizingColumn(null);
346      showingResizeCursor = false;
347      if (timer != null)
348        timer.stop();
349      header.setCursor(originalCursor);
350    }
351
352    /**
353     * Stop the dragging session.
354     *
355     * @param e the "mouse release" mouse event, needed to determing the final
356     *          location for the dragged column.
357     */
358    void endDragging(MouseEvent e)
359    {
360      header.setDraggedColumn(null);
361      draggingHeaderRect = null;
362
363      TableColumnModel model = header.getColumnModel();
364
365      // Find where have we dragged the column.
366      int x = e.getX();
367      int p = 0;
368
369      int col = model.getColumnCount() - 1;
370      int n = model.getColumnCount();
371
372      // This loop does not find the column if the mouse if out of the
373      // right boundary of the table header. Then we make this column the
374      // rightmost column.
375      Scan: for (int i = 0; i < n; i++)
376        {
377          p += model.getColumn(i).getWidth();
378          if (p > x)
379            {
380              col = i;
381              break Scan;
382            }
383        }
384
385      header.getTable().moveColumn(draggingColumnNumber, col);
386    }
387  }
388
389  /**
390   * Create and return the mouse input listener.
391   *
392   * @return the mouse listener ({@link MouseInputHandler}, if not overridden.
393   */
394  protected MouseInputListener createMouseInputListener()
395  {
396    return new MouseInputHandler();
397  }
398
399  /**
400   * Construct a new BasicTableHeaderUI, create mouse listeners.
401   */
402  public BasicTableHeaderUI()
403  {
404    mouseInputListener = createMouseInputListener();
405  }
406
407  protected void installDefaults()
408  {
409    LookAndFeel.installColorsAndFont(header, "TableHeader.background",
410                                     "TableHeader.foreground",
411                                     "TableHeader.font");
412    cellBorder = UIManager.getBorder("TableHeader.cellBorder");
413  }
414
415  protected void installKeyboardActions()
416  {
417    // AFAICS, the RI does nothing here.
418  }
419
420  /**
421   * Add the mouse listener and the mouse motion listener to the table
422   * header. The listeners support table column resizing and rearrangement
423   * by mouse.
424   */
425  protected void installListeners()
426  {
427    header.addMouseListener(mouseInputListener);
428    header.addMouseMotionListener(mouseInputListener);
429  }
430
431  public void installUI(JComponent c)
432  {
433    header = (JTableHeader) c;
434    rendererPane = new CellRendererPane();
435    installDefaults();
436    installKeyboardActions();
437    installListeners();
438  }
439
440  protected void uninstallDefaults()
441  {
442    header.setBackground(null);
443    header.setForeground(null);
444    header.setFont(null);
445  }
446
447  protected void uninstallKeyboardActions()
448  {
449    // AFAICS, the RI does nothing here.
450  }
451
452  /**
453   * Remove the previously installed listeners.
454   */
455  protected void uninstallListeners()
456  {
457    header.removeMouseListener(mouseInputListener);
458    header.removeMouseMotionListener(mouseInputListener);
459  }
460
461  public void uninstallUI(JComponent c)
462  {
463    uninstallListeners();
464    uninstallKeyboardActions();
465    uninstallDefaults();
466  }
467
468  /**
469   * Repaint the table header.
470   */
471  public void paint(Graphics gfx, JComponent c)
472  {
473    TableColumnModel cmod = header.getColumnModel();
474    int ncols = cmod.getColumnCount();
475    if (ncols == 0)
476      return;
477
478    Rectangle clip = gfx.getClipBounds();
479    TableCellRenderer defaultRend = header.getDefaultRenderer();
480
481    for (int i = 0; i < ncols; ++i)
482      {
483        Rectangle bounds = header.getHeaderRect(i);
484        if (bounds.intersects(clip))
485          {
486            Rectangle oldClip = gfx.getClipBounds();
487            TableColumn col = cmod.getColumn(i);
488            TableCellRenderer rend = col.getHeaderRenderer();
489            if (rend == null)
490              rend = defaultRend;
491            Object val = col.getHeaderValue();
492            Component comp = rend.getTableCellRendererComponent(header.getTable(),
493                                                                val,
494                                                                false, // isSelected
495                                                                false, // isFocused
496                                                                -1, i);
497            // FIXME: The following settings should be performed in
498            // rend.getTableCellRendererComponent().
499            comp.setFont(header.getFont());
500            comp.setBackground(header.getBackground());
501            comp.setForeground(header.getForeground());
502            if (comp instanceof JComponent)
503              ((JComponent) comp).setBorder(cellBorder);
504            rendererPane.paintComponent(gfx, comp, header, bounds.x, bounds.y,
505                                        bounds.width, bounds.height);
506          }
507      }
508
509    // This displays a running rectangle that is much simplier than the total
510    // animation, as it is seen in Sun's application.
511    // TODO animate the collumn dragging like in Sun's jre.
512    if (draggingHeaderRect != null)
513      {
514        gfx.setColor(header.getForeground());
515        gfx.drawRect(draggingHeaderRect.x, draggingHeaderRect.y + 2,
516            draggingHeaderRect.width - 1, draggingHeaderRect.height - 6);
517      }
518  }
519
520  /**
521   * Get the preferred header size.
522   *
523   * @param ignored unused
524   *
525   * @return the preferred size of the associated header.
526   */
527  public Dimension getPreferredSize(JComponent ignored)
528  {
529    TableColumnModel cmod = header.getColumnModel();
530    TableCellRenderer defaultRend = header.getDefaultRenderer();
531    int ncols = cmod.getColumnCount();
532    Dimension ret = new Dimension(0, 0);
533    int spacing = 0;
534
535    if (header.getTable() != null
536        && header.getTable().getIntercellSpacing() != null)
537      spacing = header.getTable().getIntercellSpacing().width;
538
539    for (int i = 0; i < ncols; ++i)
540      {
541        TableColumn col = cmod.getColumn(i);
542        TableCellRenderer rend = col.getHeaderRenderer();
543        if (rend == null)
544          rend = defaultRend;
545        Object val = col.getHeaderValue();
546        Component comp = rend.getTableCellRendererComponent(header.getTable(),
547                                                            val,
548                                                            false, // isSelected
549                                                            false, // isFocused
550                                                            -1, i);
551        comp.setFont(header.getFont());
552        comp.setBackground(header.getBackground());
553        comp.setForeground(header.getForeground());
554        if (comp instanceof JComponent)
555          ((JComponent) comp).setBorder(cellBorder);
556
557        Dimension d = comp.getPreferredSize();
558        ret.width += spacing;
559        ret.height = Math.max(d.height, ret.height);
560      }
561    ret.width = cmod.getTotalColumnWidth();
562    return ret;
563  }
564
565
566}