/**************************************************************************
OmegaT - Computer Assisted Translation (CAT) tool
with fuzzy matching, translation memory, keyword search,
glossaries, and translation leveraging into updated projects.
Copyright (C) 2015 Aaron Madlon-Kay
Home page: http://www.omegat.org/
Support center: http://groups.yahoo.com/group/OmegaT/
This file is part of OmegaT.
OmegaT 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 3 of the License, or
(at your option) any later version.
OmegaT 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, see <http://www.gnu.org/licenses/>.
**************************************************************************/
package org.omegat.util.gui;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyBoundsAdapter;
import java.awt.event.HierarchyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
/**
*
* @author Aaron Madlon-Kay
*/
public final class TableColumnSizer {
private int[] optimalColWidths;
private int remainderColReferenceWidth = -1;
private boolean didManuallyAdjustCols;
private int remainderColumn = 0;
private final boolean fitTableToWidth;
private boolean didApplySizes;
private final JTable table;
private final List<ActionListener> listeners = new CopyOnWriteArrayList<>();
/**
* Automatically optimize the column widths of a table. The {@link #remainderColumn}
* is the index of the column that will receive additional space left over after
* all other columns have been optimally sized. Usually this should be the largest
* column in the table.
* <p>
* When {@link #fitTableToWidth} is false:
* <p>
* All columns will be optimally sized all the time (except the {@link #remainderColumn}
* which may be larger) even if the table then exceeds its parent's width.
* <p>
* When {@link #fitTableToWidth} is true:
* <p>
* Columns will only be optimized if doing so results in the {@link #remainderColumn}
* receiving more space than it would under {@link JTable#AUTO_RESIZE_SUBSEQUENT_COLUMNS}-
* resizing. Resizing behavior will fall back to {@link JTable#AUTO_RESIZE_SUBSEQUENT_COLUMNS}
* under some threshold below which the latter is better.
* <p>
* The result of this is that when widening the table, after the threshold the
* columns will snap into their optimal size and all additional space goes to the
* {@link #remainderColumn}.
* <p>
* Also note that automatic sizing will be disabled if the user manually adjusts column widths.
*
* @param table
* @param remainderColumn
* @param fitTableToWidth
* @return
*/
public static TableColumnSizer autoSize(JTable table, int remainderColumn, boolean fitTableToWidth) {
TableColumnSizer colSizer = new TableColumnSizer(table, remainderColumn, fitTableToWidth);
colSizer.init();
colSizer.adjustTableColumns();
return colSizer;
}
private TableColumnSizer(JTable table, int remainderColumn, boolean fitTableToWidth) {
this.table = table;
this.remainderColumn = Math.max(remainderColumn, 0);
this.fitTableToWidth = fitTableToWidth;
}
/**
* Add various listeners to keep the columns in sync with the table's
* current width.
*/
private void init() {
table.getColumnModel().addColumnModelListener(colListener);
table.getModel().addTableModelListener(modelListener);
table.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
Object oldVal = evt.getOldValue();
Object newVal = evt.getNewValue();
if (newVal != null && newVal.equals(evt.getOldValue())) {
return;
}
boolean shouldAdjust = false;
if (evt.getPropertyName().equals("columnModel")) {
if (newVal != null && newVal instanceof TableColumnModel) {
((TableColumnModel) newVal).addColumnModelListener(colListener);
shouldAdjust = true;
}
if (oldVal != null && oldVal instanceof TableColumnModel) {
((TableColumnModel) oldVal).removeColumnModelListener(colListener);
}
} else if (evt.getPropertyName().equals("model")) {
if (newVal != null && newVal instanceof TableModel) {
((TableModel) newVal).addTableModelListener(modelListener);
shouldAdjust = true;
}
if (oldVal != null && oldVal instanceof TableModel) {
((TableModel) oldVal).removeTableModelListener(modelListener);
}
} else if (evt.getPropertyName().equals("font")) {
if ((newVal != null && !newVal.equals(oldVal)) || (oldVal != null && !oldVal.equals(newVal))) {
shouldAdjust = true;
}
}
if (shouldAdjust) {
reset();
// Queue up the adjustment because when the table has a
// RowSorter things can be out of sync at this point.
SwingUtilities.invokeLater(TableColumnSizer.this::adjustTableColumns);
}
}
});
table.addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorResized(HierarchyEvent e) {
adjustTableColumns();
}
});
}
private final TableModelListener modelListener = new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
reset();
adjustTableColumns();
}
};
private final TableColumnModelListener colListener = new TableColumnModelListener() {
@Override
public void columnAdded(TableColumnModelEvent e) {
}
@Override
public void columnMarginChanged(ChangeEvent e) {
TableColumn col = table.getTableHeader().getResizingColumn();
if (col != null) {
didManuallyAdjustCols = true;
adjustTableColumns();
}
}
@Override
public void columnMoved(TableColumnModelEvent e) {
if (optimalColWidths != null) {
int from = optimalColWidths[e.getFromIndex()];
int to = optimalColWidths[e.getToIndex()];
optimalColWidths[e.getFromIndex()] = to;
optimalColWidths[e.getToIndex()] = from;
}
adjustTableColumns();
}
@Override
public void columnRemoved(TableColumnModelEvent e) {
}
@Override
public void columnSelectionChanged(ListSelectionEvent e) {
}
};
/**
* Calculate the width that the {@link #remainderColumn} would get under
* {@link JTable#AUTO_RESIZE_SUBSEQUENT_COLUMNS}. The result is cached.
*/
private void calculateRemainderColReferenceWidth() {
if (remainderColReferenceWidth != -1) {
return;
}
table.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
remainderColReferenceWidth = table.getColumnModel().getColumn(remainderColumn).getWidth();
}
/**
* Calculate each column's ideal width, based on header and cells.
* Results are cached.
* See: https://tips4java.wordpress.com/2008/11/10/table-column-adjuster/
*/
private void calculateOptimalColWidths() {
if (optimalColWidths != null) {
return;
}
optimalColWidths = new int[table.getColumnCount()];
for (int column = 0; column < table.getColumnCount(); column++) {
TableColumn col = table.getColumnModel().getColumn(column);
col.setMaxWidth(Short.MAX_VALUE);
int preferredWidth = col.getMinWidth();
int maxWidth = col.getMaxWidth();
int startRow = table.getTableHeader() == null ? 0 : -1;
for (int row = startRow; row < table.getRowCount(); row++) {
TableCellRenderer cellRenderer;
Component c;
int margin = 5;
if (row == -1) {
cellRenderer = col.getHeaderRenderer();
if (cellRenderer == null) {
cellRenderer = col.getCellRenderer();
}
if (cellRenderer == null) {
// Headers are usually Strings
cellRenderer = table.getDefaultRenderer(String.class);
}
c = cellRenderer.getTableCellRendererComponent(table, col.getHeaderValue(), false, false, 0,
column);
c.setFont(table.getTableHeader().getFont());
// Add somewhat arbitrary margin to header because it gets truncated at a smaller width
// than a regular cell does (Windows LAF more than OS X LAF).
margin = 10;
} else {
cellRenderer = table.getCellRenderer(row, column);
c = table.prepareRenderer(cellRenderer, row, column);
}
c.setBounds(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
int width = c.getPreferredSize().width + table.getIntercellSpacing().width + margin;
preferredWidth = Math.max(preferredWidth, width);
// We've exceeded the maximum width, no need to check other rows
if (preferredWidth >= maxWidth) {
preferredWidth = maxWidth;
break;
}
}
optimalColWidths[column] = preferredWidth;
}
}
/**
* Get the new suggested width for the {@link #remainderColumn}, based on
* what is left over after all other columns get their optimal widths.
*/
private int calculateProposedRemainderColWidth() {
int otherCols = 0;
for (int i = 0; i < optimalColWidths.length; i++) {
if (i == remainderColumn) {
continue;
}
otherCols += optimalColWidths[i];
}
Component parent = table.getParent();
return parent == null ? 0 : parent.getWidth() - otherCols;
}
/**
* Reset any state that relies on the contents of the table.
*/
public void reset() {
optimalColWidths = null;
remainderColReferenceWidth = -1;
didApplySizes = false;
}
public void setRestoreAutoSizing() {
didManuallyAdjustCols = false;
didApplySizes = false;
}
/**
* Adjust the columns of the table.
*
* If possible, this optimally sizes the columns such that columns greater
* than 0 are only as big as necessary, and the rest of the space goes to
* column 0.
*
* This auto-sizing only happens if it represents an improvement over the
* default sizing (gives more space to the {@link #remainderColumn} than
* it would get with {@link JTable#AUTO_RESIZE_SUBSEQUENT_COLUMNS}),
* and only if the user has not manually adjusted column widths.
*/
public void adjustTableColumns() {
if (table.getColumnCount() == 0) {
return;
}
calculateOptimalColWidths();
ensureTableResizeMode(fitTableToWidth ? JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS : JTable.AUTO_RESIZE_OFF);
int proposedRemainderWidth = calculateProposedRemainderColWidth();
if (shouldAutoSize(proposedRemainderWidth)) {
doAutoSize(proposedRemainderWidth);
} else if (didApplySizes) {
undoAutoSize();
}
notifyListeners();
}
private void doAutoSize(int proposedRemainderWidth) {
if (!fitTableToWidth) {
int width = Math.max(proposedRemainderWidth, optimalColWidths[remainderColumn]);
table.getColumnModel().getColumn(remainderColumn).setPreferredWidth(width);
}
if (didApplySizes) {
return;
}
for (int width, i = 0; i < optimalColWidths.length; i++) {
width = optimalColWidths[i];
TableColumn col = table.getColumnModel().getColumn(i);
if (i == remainderColumn) {
continue;
}
if (fitTableToWidth) {
col.setMaxWidth(width);
}
col.setPreferredWidth(width);
}
didApplySizes = true;
}
private void undoAutoSize() {
if (!didApplySizes) {
return;
}
if (!fitTableToWidth) {
didApplySizes = false;
return;
}
for (int i = 0; i < optimalColWidths.length; i++) {
if (i == remainderColumn) {
continue;
}
TableColumn col = table.getColumnModel().getColumn(i);
// For some reason the column will "jump" to a larger size if we restore
// the max to Integer.MAX_VALUE. This doesn't happen with Short.MAX_VALUE.
col.setMaxWidth(Short.MAX_VALUE);
}
didApplySizes = false;
}
/**
* Set table mode if it isn't already in the desired mode.
*/
private void ensureTableResizeMode(int mode) {
if (table.getAutoResizeMode() == mode) {
return;
}
table.setAutoResizeMode(mode);
}
/**
* Decide if we should apply optimized column sizes given the proposed
* {@link #remainderColumn} width.
*
* @param proposedRemainderWidth
* @return
*/
private boolean shouldAutoSize(int proposedRemainderWidth) {
if (didManuallyAdjustCols) {
return false;
}
if (!fitTableToWidth) {
return true;
}
if (proposedRemainderWidth >= optimalColWidths[remainderColumn]) {
return true;
}
calculateRemainderColReferenceWidth();
return proposedRemainderWidth >= remainderColReferenceWidth;
}
public void addColumnAdjustmentListener(ActionListener listener) {
if (listener == null) {
return;
}
listeners.add(listener);
}
public void removeColumnAdjustmentListener(ActionListener listener) {
if (listener == null) {
return;
}
listeners.remove(listener);
}
private void notifyListeners() {
SwingUtilities.invokeLater(() -> {
ActionEvent event = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "columnsAdjusted");
for (ActionListener listener : listeners) {
listener.actionPerformed(event);
}
});
}
}