/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.andork.swing.table; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Point; import java.awt.Rectangle; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.border.Border; import javax.swing.plaf.UIResource; import javax.swing.table.TableModel; public class SectionHeaderTable extends JTable { public static class DefaultSectionHeaderRenderer extends JLabel implements SectionHeaderRenderer { /** * */ private static final long serialVersionUID = -7420940929891147419L; @Override public void paintSectionHeader(Graphics g, SectionHeaderTable table, int row, Rectangle bounds) { SectionHeaderModel model = table.getSectionHeaderModel(); if (model == null) { return; } Object sectionHeader = model.getSectionHeader(row); if (sectionHeader == null) { return; } setText(sectionHeader.toString()); setBackground(Color.GRAY); setSize(bounds.getSize()); setOpaque(true); g.translate(bounds.x, bounds.y); paintComponent(g); g.translate(-bounds.x, -bounds.y); } } private class SectionHeaderPainter extends JComponent { /** * */ private static final long serialVersionUID = 6098122114122513450L; @Override protected void paintComponent(Graphics g) { if (getRowCount() <= 0 || getColumnCount() <= 0 || sectionHeaderRenderer == null) { return; } Rectangle visible = SectionHeaderTable.this.getVisibleRect(); Point upperLeft = visible.getLocation(); Point lowerRight = new Point(visible.x + visible.width - 1, visible.y + visible.height - 1); int rMin = rowAtPoint(upperLeft); int rMax = rowAtPoint(lowerRight); // This should never happen (as long as our bounds intersect the // clip, // which is why we bail above if that is the case). if (rMin == -1) { rMin = 0; } // If the table does not have enough rows to fill the view we'll get // -1. // (We could also get -1 if our bounds don't intersect the clip, // which is why we bail above if that is the case). // Replace this with the index of the last row. if (rMax == -1) { rMax = getRowCount() - 1; } int headerRow = sectionHeaders.getSectionHeaderRowFor(rMin); Rectangle headerRect = SwingUtilities.convertRectangle(SectionHeaderTable.this, getRowRect(headerRow, true), this); if (headerRow == rMin) { int pushedHeaderRow = sectionHeaders.getSectionHeaderRowFor(headerRow - 1); Rectangle pushedHeaderRect = SwingUtilities.convertRectangle(SectionHeaderTable.this, getRowRect(pushedHeaderRow, true), this); pushedHeaderRect.y = headerRect.y - pushedHeaderRect.height; sectionHeaderRenderer.paintSectionHeader(g, SectionHeaderTable.this, pushedHeaderRow, pushedHeaderRect); } else { headerRect.y = 0; } sectionHeaderRenderer.paintSectionHeader(g, SectionHeaderTable.this, headerRow, headerRect); } } public static interface SectionHeaderRenderer { public void paintSectionHeader(Graphics g, SectionHeaderTable table, int row, Rectangle bounds); } /** * */ private static final long serialVersionUID = -7125400435895145842L; private SectionHeaderModel sectionHeaderModel = null; private final SectionHeaderRowSet sectionHeaders; private SectionHeaderRenderer sectionHeaderRenderer = new DefaultSectionHeaderRenderer(); private final SectionHeaderPainter sectionHeaderPainter = new SectionHeaderPainter(); public SectionHeaderTable() { this(null, null); } public SectionHeaderTable(TableModel dm) { this(dm, dm instanceof SectionHeaderModel ? (SectionHeaderModel) dm : null); } public SectionHeaderTable(TableModel dm, SectionHeaderModel sectionHeaderModel) { super(dm); sectionHeaderPainter.setPreferredSize(new Dimension(0, getRowHeight())); sectionHeaders = new SectionHeaderRowSet(getModel(), null); setSectionHeaderModel(sectionHeaderModel); } /** * If this <code>JTable</code> is the <code>viewportView</code> of an * enclosing <code>JScrollPane</code> (the usual situation), configure this * <code>ScrollPane</code> by, amongst other things, installing the table's * <code>tableHeader</code> as the <code>columnHeaderView</code> of the * scroll pane. When a <code>JTable</code> is added to a * <code>JScrollPane</code> in the usual way, using * <code>new JScrollPane(myTable)</code>, <code>addNotify</code> is called * in the <code>JTable</code> (when the table is added to the viewport). * <code>JTable</code>'s <code>addNotify</code> method in turn calls this * method, which is protected so that this default installation procedure * can be overridden by a subclass. * * @see #addNotify */ @Override protected void configureEnclosingScrollPane() { Container p = getParent(); if (p instanceof JViewport) { Container gp = p.getParent(); if (gp instanceof JScrollPane) { JScrollPane scrollPane = (JScrollPane) gp; // Make certain we are the viewPort's view and not, for // example, the rowHeaderView of the scrollPane - // an implementor of fixed columns might do this. JViewport viewport = scrollPane.getViewport(); if (viewport == null || viewport.getView() != this) { return; } JPanel columnHeader = new JPanel(new BorderLayout()); columnHeader.add(getTableHeader(), BorderLayout.NORTH); columnHeader.add(sectionHeaderPainter, BorderLayout.CENTER); scrollPane.setColumnHeaderView(columnHeader); // scrollPane.getViewport().setBackingStoreEnabled(true); Border border = scrollPane.getBorder(); if (border == null || border instanceof UIResource) { scrollPane.setBorder(UIManager.getBorder("Table.scrollPaneBorder")); } } } } private Rectangle getRowRect(int row, boolean includeSpacing) { int adjRow = Math.max(0, row); Rectangle leftRect = getCellRect(adjRow, 0, includeSpacing); Rectangle rightRect = getCellRect(adjRow, getColumnCount() - 1, includeSpacing); Rectangle rowRect = leftRect.union(rightRect); if (row < 0) { rowRect.y -= rowRect.height; } return rowRect; } public SectionHeaderModel getSectionHeaderModel() { return sectionHeaderModel; } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); paintSectionHeaders(g); } private void paintSectionHeaders(Graphics g) { Rectangle clip = g.getClipBounds(); Rectangle bounds = getBounds(); // account for the fact that the graphics has already been translated // into the table's bounds bounds.x = bounds.y = 0; if (getRowCount() <= 0 || getColumnCount() <= 0 || // this check prevents us from painting the entire table // when the clip doesn't intersect our bounds at all !bounds.intersects(clip)) { return; } Point upperLeft = clip.getLocation(); Point lowerRight = new Point(clip.x + clip.width - 1, clip.y + clip.height - 1); int rMin = rowAtPoint(upperLeft); int rMax = rowAtPoint(lowerRight); // This should never happen (as long as our bounds intersect the clip, // which is why we bail above if that is the case). if (rMin == -1) { rMin = 0; } // If the table does not have enough rows to fill the view we'll get -1. // (We could also get -1 if our bounds don't intersect the clip, // which is why we bail above if that is the case). // Replace this with the index of the last row. if (rMax == -1) { rMax = getRowCount() - 1; } if (sectionHeaderRenderer != null) { for (int row = rMin; row <= rMax; row++) { if (sectionHeaders.isSectionHeaderRow(row)) { Rectangle leftRect = getCellRect(row, 0, true); Rectangle rightRect = getCellRect(row, getColumnCount() - 1, true); Rectangle rowRect = leftRect.union(rightRect); sectionHeaderRenderer.paintSectionHeader(g, this, row, rowRect); } } } } @Override public void setModel(TableModel model) { super.setModel(model); if (sectionHeaders != null) { sectionHeaders.setTableModel(model); } } public void setSectionHeaderModel(SectionHeaderModel model) { if (sectionHeaders != null) { sectionHeaderModel = model; sectionHeaders.setSectionHeaderModel(model); repaint(); } } }