Skip to content

Instantly share code, notes, and snippets.

@bric3
Last active September 2, 2022 12:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bric3/2a6a1917ec6e1b3c9128f116bfad8f1c to your computer and use it in GitHub Desktop.
Save bric3/2a6a1917ec6e1b3c9128f116bfad8f1c to your computer and use it in GitHub Desktop.
Swing dance with JTable to have readonly interactive cells
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.Objects;
import javax.swing.*;
import javax.swing.event.MouseInputAdapter;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
public final class InteractiveTableCellsMain extends JPanel {
private InteractiveTableCellsMain() {
super(new BorderLayout());
String wagonsData = "";
Object[][] data = {
{124, wagonsData},
{13, wagonsData},
{78, wagonsData},
{103, wagonsData}
};
var model = new DefaultTableModel(data, new String[]{"Seats", "Train wagons"}) {
@Override
public Class<?> getColumnClass(int column) {
return getValueAt(0, column).getClass();
}
@Override
public boolean isCellEditable(int row, int column) {
return column == 1;
}
};
var table = new JTable(model);
table.setRowHeight(30);
table.setColumnSelectionAllowed(false);
var trainColumn = table.getColumnModel().getColumn(1);
InteractiveTableCell.configureInteractiveCell(
table,
trainColumn,
new InteractiveChartPanel(),
new InteractiveChartPanel()
);
add(new JScrollPane(table));
setPreferredSize(new Dimension(320, 240));
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
var frame = new JFrame("Read-only Interactive Table Cells");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.getContentPane().add(new InteractiveTableCellsMain());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
class InteractiveChartPanel extends JPanel {
private final Rectangle wagon1 = new Rectangle(4, 2, 30, 16);
private final Rectangle wagon2 = new Rectangle(4 + 30 + 4, 2, 30, 16);
private final Rectangle wagon3 = new Rectangle(38 + 30 + 4, 2, 30, 16);
private final Rectangle wagon4 = new Rectangle(72 + 30 + 4, 2, 30, 16);
private Rectangle hoveredWagon = null;
protected InteractiveChartPanel() {
super();
setOpaque(true);
addMouseMotionListener(new MouseInputAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
var location = MouseInfo.getPointerInfo().getLocation();
SwingUtilities.convertPointFromScreen(location, InteractiveChartPanel.this);
var oldHoveredWagon = hoveredWagon;
if (wagon1.contains(location)) {
hoveredWagon = wagon1;
} else if (wagon2.contains(location)) {
hoveredWagon = wagon2;
} else if (wagon3.contains(location)) {
hoveredWagon = wagon3;
} else if (wagon4.contains(location)) {
hoveredWagon = wagon4;
} else {
hoveredWagon = null;
}
if (!Objects.equals(oldHoveredWagon, hoveredWagon)) {
repaint();
}
}
});
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.ORANGE);
g2.fill(wagon1);
g2.fill(wagon2);
g2.fill(wagon3);
g2.fill(wagon4);
if (hoveredWagon != null) {
g2.setColor(Color.ORANGE.darker());
g2.fill(hoveredWagon);
}
}
}
class InteractiveTableCell extends AbstractCellEditor implements TableCellEditor {
public static final String INTERACTIVE_CELLS_KEY = "JTable.interactiveCells";
public static final String INTERACTIVE_CELL_COMPONENT_KEY = "TableCellEditor.interactiveCellComponent";
private final JComponent component;
/**
* Install a special kind of cell editor that is used to make the cells interactive, but read-only.
*
* <p>
* While the editor won't modify the values, it throws instead, the interactive behavior requires
* the {@link javax.swing.table.TableModel#isCellEditable(int, int)} implementation to return true for
* the cells that are wished to be interacted with.
* </p>
* <p>
* Also ideally the cell renderer component would be reused, but this create some sort of flicker.
* So its better to provide different component instance for the renderer (used for paiting) and
* the editor (used for interactivity).
* </p>
*
* @param table the table
* @param tableColumn the column whose cell can be interacted with
* @param interactiveCellComponent the component to be displayed in the cell, the component must not be added to other container.
*/
public static void configureInteractiveCell(
JTable table,
TableColumn tableColumn,
JComponent renderingCellComponent,
JComponent interactiveCellComponent
) {
Objects.requireNonNull(table, "table");
Objects.requireNonNull(tableColumn, "tableColumn");
Objects.requireNonNull(renderingCellComponent, "renderingCellComponent");
Objects.requireNonNull(interactiveCellComponent, "interactiveCellComponent");
if (renderingCellComponent == interactiveCellComponent) {
throw new IllegalArgumentException("The 'renderingCellComponent' and 'interactiveCellComponent' should be distinct instances");
}
if (!Objects.equals(Boolean.TRUE, table.getClientProperty(INTERACTIVE_CELLS_KEY))) {
table.putClientProperty(INTERACTIVE_CELLS_KEY, true);
table.addMouseMotionListener(new MouseInputAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
int r = table.rowAtPoint(e.getPoint());
int c = table.columnAtPoint(e.getPoint());
if (table.isCellEditable(r, c)
&& (table.getEditingRow() != r || table.getEditingColumn() != c) // avoid flickering, when the mouse move over the same cell
) {
// Cancel previous, otherwise editCellAt will invoke stopCellEditing which
// actually get the current value from the editor and set it to the model (see editingStopped)
if (table.isEditing() && r >= 0 && c >= 0) {
table.getCellEditor().cancelCellEditing();
}
table.editCellAt(r, c);
} else {
if (table.isEditing() || r < 0 || c < 0) {
table.getCellEditor().cancelCellEditing();
}
}
}
});
}
tableColumn.setCellRenderer((t, value, isSelected, hasFocus, row, column) -> {
renderingCellComponent.setBackground(isSelected ? t.getSelectionBackground() : t.getBackground());
return renderingCellComponent;
});
tableColumn.setCellEditor(new InteractiveTableCell(interactiveCellComponent));
}
protected InteractiveTableCell(JComponent component) {
this.component = component;
if (!Objects.equals(Boolean.TRUE, component.getClientProperty(INTERACTIVE_CELL_COMPONENT_KEY))) {
component.putClientProperty(INTERACTIVE_CELL_COMPONENT_KEY, true);
this.component.addMouseListener(new MouseInputAdapter() {
@Override
public void mouseExited(MouseEvent e) {
SwingUtilities.invokeLater(InteractiveTableCell.this::fireEditingCanceled);
}
});
}
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
component.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
return component;
}
@Override
public Object getCellEditorValue() {
throw new IllegalStateException("Editing should have been cancelled");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment