001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 005 * Space Science and Engineering Center (SSEC) 006 * University of Wisconsin - Madison 007 * 1225 W. Dayton Street, Madison, WI 53706, USA 008 * https://www.ssec.wisc.edu/mcidas/ 009 * 010 * All Rights Reserved 011 * 012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and 013 * some McIDAS-V source code is based on IDV and VisAD source code. 014 * 015 * McIDAS-V is free software; you can redistribute it and/or modify 016 * it under the terms of the GNU Lesser Public License as published by 017 * the Free Software Foundation; either version 3 of the License, or 018 * (at your option) any later version. 019 * 020 * McIDAS-V is distributed in the hope that it will be useful, 021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 023 * GNU Lesser Public License for more details. 024 * 025 * You should have received a copy of the GNU Lesser Public License 026 * along with this program. If not, see https://www.gnu.org/licenses/. 027 */ 028package edu.wisc.ssec.mcidasv.ui; 029 030import edu.wisc.ssec.mcidasv.McIDASV; 031 032import java.awt.Color; 033import java.awt.Component; 034import java.awt.Graphics; 035 036import java.beans.PropertyChangeEvent; 037import java.beans.PropertyChangeListener; 038 039import javax.swing.BorderFactory; 040import javax.swing.CellRendererPane; 041import javax.swing.JComponent; 042import javax.swing.JScrollPane; 043import javax.swing.JTable; 044import javax.swing.JViewport; 045import javax.swing.event.ChangeEvent; 046import javax.swing.event.ListSelectionEvent; 047import javax.swing.event.TableColumnModelEvent; 048import javax.swing.event.TableColumnModelListener; 049import javax.swing.table.JTableHeader; 050import javax.swing.table.TableCellRenderer; 051import javax.swing.table.TableColumn; 052import javax.swing.table.TableModel; 053 054public class BetterJTable extends JTable { 055 056 private static final Color ROW_COLOR_LIGHT = new Color(241, 245, 250); 057 private static final Color TABLE_GRID_COLOR = new Color(0xd9d9d9); 058 059 private static final Color DARK_ROW_COLOR_ONE = new Color(32, 28, 29); 060 private static final Color DARK_ROW_COLOR_TWO = new Color(42, 39, 40); 061 062 private static final CellRendererPane CELL_RENDER_PANE = new CellRendererPane(); 063 064 public BetterJTable() { 065 super(); 066 init(); 067 } 068 public BetterJTable(TableModel dm) { 069 super(dm); 070 init(); 071 } 072 073 private void init() { 074// setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 075 setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); 076 setTableHeader(createTableHeader()); 077 getTableHeader().setReorderingAllowed(false); 078 setOpaque(false); 079 if (!McIDASV.isDarkMode()) { 080 setGridColor(TABLE_GRID_COLOR); 081 } 082 // turn off grid painting as we'll handle this manually in order to paint 083 // grid lines over the entire viewport. 084 setShowGrid(false); 085 } 086 087 /** 088 * Creates a JTableHeader that paints the table header background to the right 089 * of the right-most column if neccesasry. 090 */ 091 private JTableHeader createTableHeader() { 092 return new JTableHeader(getColumnModel()) { 093 @Override protected void paintComponent(Graphics g) { 094 super.paintComponent(g); 095 // if this JTableHeader is parented in a JViewport, then paint the 096 // table header background to the right of the last column if 097 // neccessary. 098 JViewport viewport = (JViewport) table.getParent(); 099 if (viewport != null && table.getWidth() < viewport.getWidth()) { 100 int x = table.getWidth(); 101 int width = viewport.getWidth() - table.getWidth(); 102 paintHeader(g, getTable(), x, width); 103 } 104 } 105 }; 106 } 107 108 /** 109 * Paints the given JTable's table default header background at given 110 * x for the given width. 111 */ 112 private static void paintHeader(Graphics g, JTable table, int x, int width) { 113 TableCellRenderer renderer = table.getTableHeader().getDefaultRenderer(); 114 Component component = renderer.getTableCellRendererComponent( 115 table, "", false, false, -1, 2); 116 117 component.setBounds(0, 0, width, table.getTableHeader().getHeight()); 118 119 ((JComponent)component).setOpaque(false); 120 CELL_RENDER_PANE.paintComponent(g, component, null, x, 0, 121 width, table.getTableHeader().getHeight(), true); 122 } 123 124 @Override public Component prepareRenderer(TableCellRenderer renderer, int row, int column) { 125 Component component = super.prepareRenderer(renderer, row, column); 126 // if the rendere is a JComponent and the given row isn't part of a 127 // selection, make the renderer non-opaque so that striped rows show 128 // through. 129 if (component instanceof JComponent) { 130 ((JComponent)component).setOpaque(getSelectionModel().isSelectedIndex(row)); 131 } 132 return component; 133 } 134 135 // Stripe painting Viewport. ////////////////////////////////////////////// 136 137 /** 138 * Creates a JViewport that draws a striped backgroud corresponding to the 139 * row positions of the given JTable. 140 */ 141 public static class StripedViewport extends JViewport { 142 143 private final JTable fTable; 144 145 private final Color COLOR_ONE; 146 private final Color COLOR_TWO; 147 148 public StripedViewport(JTable table) { 149 fTable = table; 150 setOpaque(false); 151 initListeners(); 152 if (McIDASV.isDarkMode()) { 153 COLOR_ONE = DARK_ROW_COLOR_ONE; 154 COLOR_TWO = DARK_ROW_COLOR_TWO; 155 } else { 156 COLOR_ONE = getBackground(); 157 COLOR_TWO = ROW_COLOR_LIGHT; 158 } 159 } 160 161 private void initListeners() { 162 // install a listener to cause the whole table to repaint when 163 // a column is resized. we do this because the extended grid 164 // lines may need to be repainted. this could be cleaned up, 165 // but for now, it works fine. 166 fTable.getColumnModel().addColumnModelListener(new TableColumnModelListener() { 167 @Override public void columnMarginChanged(ChangeEvent e) { 168 repaint(); 169 } 170 171 @Override public void columnAdded(TableColumnModelEvent e) {} 172 173 @Override public void columnRemoved(TableColumnModelEvent e) {} 174 175 @Override public void columnMoved(TableColumnModelEvent e) {} 176 177 @Override public void columnSelectionChanged(ListSelectionEvent e) {} 178 }); 179 PropertyChangeListener listener = createTableColumnWidthListener(); 180 for (int i = 0; i < fTable.getColumnModel().getColumnCount(); i++) { 181 fTable.getColumnModel().getColumn(i).addPropertyChangeListener(listener); 182 } 183 } 184 185 private PropertyChangeListener createTableColumnWidthListener() { 186 return new PropertyChangeListener() { 187 public void propertyChange(PropertyChangeEvent evt) { 188 repaint(); 189 } 190 }; 191 } 192 193 @Override protected void paintComponent(Graphics g) { 194 paintStripedBackground(g); 195 // dark mode just looks better without the vertical lines separating cells 196 if (!McIDASV.isDarkMode()) { 197 paintVerticalGridLines(g); 198 } 199 super.paintComponent(g); 200 } 201 202 private void paintStripedBackground(Graphics g) { 203 // get the row index at the top of the clip bounds (the first row 204 // to paint). 205 int rowAtPoint = fTable.rowAtPoint(g.getClipBounds().getLocation()); 206 // get the y coordinate of the first row to paint. if there are no 207 // rows in the table, start painting at the top of the supplied 208 // clipping bounds. 209 int topY = rowAtPoint < 0 210 ? g.getClipBounds().y : fTable.getCellRect(rowAtPoint,0,true).y; 211 212 // create a counter variable to hold the current row. if there are no 213 // rows in the table, start the counter at 0. 214 int currentRow = rowAtPoint < 0 ? 0 : rowAtPoint; 215 while (topY < g.getClipBounds().y + g.getClipBounds().height) { 216 int bottomY = topY + fTable.getRowHeight(); 217 g.setColor(getRowColor(currentRow)); 218 g.fillRect(g.getClipBounds().x, topY, g.getClipBounds().width, bottomY); 219 topY = bottomY; 220 currentRow++; 221 } 222 } 223 224 private Color getRowColor(int row) { 225 Color c = row % 2 == 0 ? COLOR_TWO : COLOR_ONE; 226 return c; 227 } 228 229 private void paintVerticalGridLines(Graphics g) { 230 // paint the column grid dividers for the non-existent rows. 231 int x = 0; 232 for (int i = 0; i < fTable.getColumnCount(); i++) { 233 TableColumn column = fTable.getColumnModel().getColumn(i); 234 // increase the x position by the width of the current column. 235 x += column.getWidth(); 236 if (!McIDASV.isDarkMode()) { 237 g.setColor(TABLE_GRID_COLOR); 238 } 239 // draw the grid line (not sure what the -1 is for, but BasicTableUI 240 // also does it. 241 g.drawLine(x - 1, g.getClipBounds().y, x - 1, getHeight()); 242 } 243 } 244 } 245 246 public static JScrollPane createStripedJScrollPane(JTable table) { 247 JScrollPane scrollPane = new JScrollPane(table); 248 scrollPane.setViewport(new StripedViewport(table)); 249 scrollPane.getViewport().setView(table); 250 scrollPane.setBorder(BorderFactory.createEmptyBorder()); 251// scrollPane.setCorner(JScrollPane.UPPER_RIGHT_CORNER, 252// createCornerComponent(table)); 253 return scrollPane; 254 } 255 256 /** 257 * Creates a component that paints the header background for use in a 258 * JScrollPane corner. 259 */ 260 private static JComponent createCornerComponent(final JTable table) { 261 return new JComponent() { 262 @Override 263 protected void paintComponent(Graphics g) { 264 paintHeader(g, table, 0, getWidth()); 265 } 266 }; 267 } 268}