001    /* BasicTableHeaderUI.java --
002       Copyright (C) 2004 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.plaf.basic;
040    
041    import java.awt.Component;
042    import java.awt.Cursor;
043    import java.awt.Dimension;
044    import java.awt.Graphics;
045    import java.awt.Rectangle;
046    import java.awt.event.ActionEvent;
047    import java.awt.event.ActionListener;
048    import java.awt.event.MouseEvent;
049    
050    import javax.swing.CellRendererPane;
051    import javax.swing.JComponent;
052    import javax.swing.LookAndFeel;
053    import javax.swing.Timer;
054    import javax.swing.UIManager;
055    import javax.swing.border.Border;
056    import javax.swing.event.MouseInputListener;
057    import javax.swing.plaf.ComponentUI;
058    import javax.swing.plaf.TableHeaderUI;
059    import javax.swing.table.JTableHeader;
060    import javax.swing.table.TableCellRenderer;
061    import javax.swing.table.TableColumn;
062    import javax.swing.table.TableColumnModel;
063    
064    /**
065     * Basic pluggable look and feel interface for JTableHeader.
066     */
067    public 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    }