001/*
002 * Copyright (c) 2005 Einar Pehrson <einar@pehrson.nu>.
003 *
004 * This file is part of
005 * CleanSheets - a spreadsheet application for the Java platform.
006 *
007 * CleanSheets is free software; you can redistribute it and/or modify
008 * it under the terms of the GNU General Public License as published by
009 * the Free Software Foundation; either version 2 of the License, or
010 * (at your option) any later version.
011 *
012 * CleanSheets is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015 * GNU General Public License for more details.
016 *
017 * You should have received a copy of the GNU General Public License
018 * along with CleanSheets; if not, write to the Free Software
019 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
020 */
021package csheets.ui.grid;
022
023import java.awt.Component;
024import java.awt.Cursor;
025import java.awt.Dimension;
026import java.awt.Graphics;
027import java.awt.Point;
028import java.awt.Rectangle;
029import java.awt.event.MouseEvent;
030import java.util.Arrays;
031
032import javax.swing.CellRendererPane;
033import javax.swing.JComponent;
034import javax.swing.JTable;
035import javax.swing.SwingConstants;
036import javax.swing.event.MouseInputAdapter;
037import javax.swing.event.MouseInputListener;
038import javax.swing.table.TableCellRenderer;
039
040
041/**
042 * The row header for spreadsheet tables. This component emulates the
043 * behaviour of <code>javax.swing.plaf.basic.BasicTableHeaderUI</code>, but
044 * paints the header vertically in stead of horizontally.
045 * @author Einar Pehrson
046 */
047@SuppressWarnings("serial")
048public class RowHeader extends JComponent {
049
050        /** The table to which the row header belongs */
051        private JTable table;
052
053        /** The header's renderer pane */
054        private CellRendererPane rendererPane = new CellRendererPane();
055
056        /** The row header renderer*/
057        private TableCellRenderer renderer
058                = new HeaderRenderer(SwingConstants.VERTICAL);
059
060        /** The width of the row header */
061        private int width = 30;
062
063        /** The minimum height of rows */
064        private int minRowHeight = 5;
065
066        /** The margin around the packed rows of the header. */
067    private int rowMargin = 1;
068
069        /** The index of the row being resized, or -1 */
070        private int resizingRow = -1;
071
072        /**
073         * Creates a new row header for the given table.
074         * @param table the table to which the row header belongs
075         */
076        public RowHeader(JTable table) {
077                this.table = table;
078                add(rendererPane);
079                setPreferredSize(new Dimension(width, table.getRowCount() * table.getRowHeight()));
080                MouseInputListener rowResizer = new RowResizer();
081                addMouseListener(rowResizer);
082                addMouseMotionListener(rowResizer);
083        }
084
085        public void paint(Graphics g) {
086                // Calculates visible area
087                Rectangle bounds = g.getClipBounds(); 
088                Point top = bounds.getLocation();
089                Point bottom = new Point(bounds.x, bounds.y + bounds.height - 1);
090
091                // Finds rows to paint
092                int minRow = table.rowAtPoint(top);
093                int maxRow = table.rowAtPoint(bottom);
094                if (minRow == -1)
095                        minRow =  0;
096                if (maxRow == -1)
097                        maxRow = table.getRowCount()-1;  
098
099                // Paints rows
100                int y = table.getCellRect(minRow, 0, true).y;
101                int[] selectedRows = table.getSelectedRows();
102                for (int row = minRow; row <= maxRow; row++) {
103                        // Fetches component from renderer
104                        boolean selected = Arrays.binarySearch(selectedRows, row) >= 0;
105                        Component c = renderer.getTableCellRendererComponent(
106                                table, null, selected, false, row, -1);
107
108                        // Calculates coordinates and paints component
109                        int rowHeight = table.getRowHeight(row);
110                        rendererPane.paintComponent(g, c, this,
111                                0, y, width, rowHeight, true);
112                        y += rowHeight;
113                }
114        }
115
116        /**
117         * Adjusts the height of the row at the given index to precisely fit all
118         * data being rendered.
119         * @param row the index of the row to auto-resize
120         */
121        public void autoResize(int row) {
122                // Gets width of row header
123                int height = renderer.getTableCellRendererComponent(table, null,
124                                false, false, row, 0).getPreferredSize().height;
125                
126                // Gets maximum width of column data
127                for (int column = 0; column < table.getColumnCount(); column++) {
128                        Component c = table.getCellRenderer(row, column)
129                                .getTableCellRendererComponent(table, table.getValueAt
130                                        (row, column), false, false, row, column);
131                        height = Math.max(height, c.getPreferredSize().height);
132                }
133
134                // Adds margin
135                height += 2 * rowMargin;
136                if (height > minRowHeight) {
137                        table.setRowHeight(resizingRow, height);
138                } else
139                        table.setRowHeight(minRowHeight);
140                repaint();
141        }
142
143        /**
144         * A mouse input listener that enables resizing of rows
145         */
146        protected class RowResizer extends MouseInputAdapter {
147
148                /** The normal cursor */
149                private final Cursor NORMAL_CURSOR = getCursor();
150
151                /** The cursor to display when resizing a row */
152                private final Cursor RESIZE_CURSOR
153                        = Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
154
155                /**
156                 * Selects the clicked row, unless resizing is intended.
157                 * @param e the event that was fired
158                 */
159                public void mousePressed(MouseEvent e) {
160                        // Checks what was done
161                        resizingRow = getResizingRow(e.getPoint());
162                        if (resizingRow == -1) {
163                                int pressedRow = table.rowAtPoint(e.getPoint());
164                                int columns = table.getColumnCount();
165        
166                                // Configures new selection
167                                if (e.isShiftDown())
168                                        table.changeSelection(pressedRow, 0, false, true);
169                                else if (e.isControlDown())
170                                        table.changeSelection(pressedRow, 0, true, false);
171                                else {
172                                        table.changeSelection(pressedRow, columns, false, false);
173                                        table.changeSelection(pressedRow, 0, false, true);
174                                }
175                                repaint();
176                        }
177                }
178
179                /**
180                 * Auto-resizes a column whose border was double-clicked.
181                 * @param e the event that was fired
182                 */
183                public void mouseClicked(MouseEvent e) {
184                        if (e.getClickCount() == 2 && resizingRow != -1)
185                                autoResize(resizingRow);
186                }
187
188                /**
189                 * Sets the appropriate cursor depending on whether the mouse is on
190                 * a row that can be resized.
191                 * @param e the event that was fired
192                 */
193                public void mouseMoved(MouseEvent e) { 
194                        setCursor(getResizingRow(e.getPoint()) == -1
195                                ? NORMAL_CURSOR : RESIZE_CURSOR);
196                }
197
198                /**
199                 * Resizes the row that is dragged
200                 * @param e the event that was fired
201                 */
202                public void mouseDragged(MouseEvent e) {
203                        if (resizingRow != -1) {
204                                int rowHeight = e.getPoint().y
205                                        - table.getCellRect(resizingRow, 0, true).y;
206                                if (rowHeight >= minRowHeight)
207                                        table.setRowHeight(resizingRow, rowHeight);
208                                repaint();
209                        }
210                }
211
212                /**
213                 * Retrieves the index of the row at the given point, if it can be
214                 * resized.
215                 * @param p the point to look at
216                 * @return the index of the row, or -1 if it can not be resized
217                 */
218                private int getResizingRow(Point p) {
219                        // Fetches the row index, and stops if it is invalid
220                        int row = table.rowAtPoint(p);
221                        if (row == -1)
222                                return row;
223
224                        // Fetches the bounding rectangle of the header row
225                        Rectangle r = table.getCellRect(row, 0, true);
226                        r = new Rectangle(0, r.y, width, table.getRowHeight(row));
227                        r.grow(0, -2);
228
229                        // Stops if the point is inside the header row
230                        if (r.contains(p))
231                                return -1;
232
233                        // If above the middle of the row, resize previous row
234                        if (p.y < (r.y + (r.height / 2)))
235                                row--;
236                        return row;
237                }
238        }
239}