Last active
September 2, 2022 12:28
-
-
Save bric3/2a6a1917ec6e1b3c9128f116bfad8f1c to your computer and use it in GitHub Desktop.
Swing dance with JTable to have readonly interactive cells
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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