001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
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}