001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.util;
003
004import java.awt.Component;
005import java.awt.Font;
006import java.util.stream.IntStream;
007
008import javax.swing.JTable;
009import javax.swing.ListSelectionModel;
010import javax.swing.event.ListDataEvent;
011import javax.swing.event.ListSelectionEvent;
012import javax.swing.table.AbstractTableModel;
013import javax.swing.table.TableCellRenderer;
014import javax.swing.table.TableColumn;
015
016import org.openstreetmap.josm.gui.dialogs.IEnabledStateUpdating;
017import org.openstreetmap.josm.spi.preferences.Config;
018
019/**
020 * The class that provide common JTable customization methods
021 * @since 5785
022 */
023public final class TableHelper {
024
025    private TableHelper() {
026        // Hide default constructor for utils classes
027    }
028
029    /**
030     * Wires <code>listener</code> to <code>listSelectionModel</code> in such a way, that
031     * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
032     * on every {@link ListSelectionEvent}.
033     *
034     * @param listener  the listener
035     * @param listSelectionModel  the source emitting {@link ListSelectionEvent}s
036     * @since 15226
037     */
038    public static void adaptTo(final IEnabledStateUpdating listener, ListSelectionModel listSelectionModel) {
039        listSelectionModel.addListSelectionListener(e -> listener.updateEnabledState());
040    }
041
042    /**
043     * Wires <code>listener</code> to <code>listModel</code> in such a way, that
044     * <code>listener</code> receives a {@link IEnabledStateUpdating#updateEnabledState()}
045     * on every {@link ListDataEvent}.
046     *
047     * @param listener the listener
048     * @param listModel the source emitting {@link ListDataEvent}s
049     * @since 15226
050     */
051    public static void adaptTo(final IEnabledStateUpdating listener, AbstractTableModel listModel) {
052        listModel.addTableModelListener(e -> listener.updateEnabledState());
053    }
054
055    static int getColumnHeaderWidth(JTable tbl, int col) {
056        TableColumn tableColumn = tbl.getColumnModel().getColumn(col);
057        TableCellRenderer renderer = tableColumn.getHeaderRenderer();
058
059        if (renderer == null && tbl.getTableHeader() != null)
060            renderer = tbl.getTableHeader().getDefaultRenderer();
061
062        if (renderer == null)
063            return 0;
064
065        Component c = renderer.getTableCellRendererComponent(tbl, tableColumn.getHeaderValue(), false, false, -1, col);
066        return c.getPreferredSize().width;
067    }
068
069    static int getMaxWidth(JTable tbl, int col) {
070        int maxwidth = getColumnHeaderWidth(tbl, col);
071        for (int row = 0; row < tbl.getRowCount(); row++) {
072            TableCellRenderer tcr = tbl.getCellRenderer(row, col);
073            Object val = tbl.getValueAt(row, col);
074            Component comp = tcr.getTableCellRendererComponent(tbl, val, false, false, row, col);
075            maxwidth = Math.max(comp.getPreferredSize().width, maxwidth);
076        }
077        return maxwidth;
078    }
079
080    /**
081     * adjust the preferred width of column col to the maximum preferred width of the cells (including header)
082     * @param tbl table
083     * @param col column index
084     * @param resizable if true, resizing is allowed
085     * @since 15176
086     */
087    public static void adjustColumnWidth(JTable tbl, int col, boolean resizable) {
088        int maxwidth = getMaxWidth(tbl, col);
089        TableColumn column = tbl.getColumnModel().getColumn(col);
090        column.setPreferredWidth(maxwidth);
091        column.setResizable(resizable);
092        if (!resizable) {
093            column.setMaxWidth(maxwidth);
094        }
095    }
096
097    /**
098     * adjust the preferred width of column col to the maximum preferred width of the cells (including header)
099     * requires JTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
100     * @param tbl table
101     * @param col column index
102     * @param maxColumnWidth maximum column width
103     */
104    public static void adjustColumnWidth(JTable tbl, int col, int maxColumnWidth) {
105        int maxwidth = getMaxWidth(tbl, col);
106        tbl.getColumnModel().getColumn(col).setPreferredWidth(Math.min(maxwidth+10, maxColumnWidth));
107    }
108
109    /**
110     * adjust the table's columns to fit their content best
111     * requires JTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
112     * @param tbl table
113     * @since 14476
114     */
115    public static void computeColumnsWidth(JTable tbl) {
116        for (int column = 0; column < tbl.getColumnCount(); column++) {
117            adjustColumnWidth(tbl, column, Integer.MAX_VALUE);
118        }
119    }
120
121    /**
122     * Returns an array of all of the selected indices in the selection model, in increasing order.
123     * Unfortunately this method is not available in OpenJDK before version 11, see
124     * https://bugs.openjdk.java.net/browse/JDK-8199395
125     *
126     * To be removed when we switch to Java 11 or later.
127     *
128     * @param selectionModel list selection model.
129     *
130     * @return all of the selected indices, in increasing order,
131     *         or an empty array if nothing is selected
132     * @see #selectedIndices(ListSelectionModel)
133     * @since 15226
134     */
135    public static int[] getSelectedIndices(ListSelectionModel selectionModel) {
136        return selectedIndices(selectionModel).toArray();
137    }
138
139    /**
140     * Returns a stream of all of the selected indices in the selection model, in increasing order.
141     *
142     * @param selectionModel list selection model.
143     *
144     * @return all of the selected indices, in increasing order,
145     *         or an empty stream if nothing is selected
146     * @since 17773
147     */
148    public static IntStream selectedIndices(ListSelectionModel selectionModel) {
149        if (selectionModel.isSelectionEmpty()) {
150            return IntStream.empty();
151        }
152        return IntStream.rangeClosed(selectionModel.getMinSelectionIndex(), selectionModel.getMaxSelectionIndex())
153                .filter(selectionModel::isSelectedIndex);
154    }
155
156    /**
157     * Selects the given indices in the selection model
158     * @param selectionModel list selection model.
159     * @param indices the indices to select
160     * @see ListSelectionModel#addSelectionInterval(int, int)
161     * @since 16601
162     */
163    public static void setSelectedIndices(ListSelectionModel selectionModel, IntStream indices) {
164        selectionModel.setValueIsAdjusting(true);
165        selectionModel.clearSelection();
166        indices.filter(i -> i >= 0).forEach(i -> selectionModel.addSelectionInterval(i, i));
167        selectionModel.setValueIsAdjusting(false);
168    }
169
170    /**
171     * Sets the table font size based on the font scaling from the preferences
172     * @param table the table
173     * @param parent the parent component used for determining the preference key
174     * @see JTable#setFont(Font)
175     * @see JTable#setRowHeight(int)
176     */
177    public static void setFont(JTable table, Class<? extends Component> parent) {
178        double fontFactor = Config.getPref().getDouble("gui.scale.table.font",
179                Config.getPref().getDouble("gui.scale.table." + parent.getSimpleName() + ".font", 1.0));
180        if (fontFactor == 1.0) {
181            return;
182        }
183        Font font = table.getFont();
184        table.setFont(font.deriveFont((float) (font.getSize2D() * fontFactor)));
185        // need to setRowHeight, see comment in javax.swing.plaf.basic.BasicTableUI.installDefaults
186        table.setRowHeight((int) (table.getRowHeight() * fontFactor));
187    }
188}