Home > Blockchain >  Expandable JTable Cell Editor
Expandable JTable Cell Editor

Time:01-25

I have a rows of data, where I can make actions on a remote server.

I'm displaying these data in a JTable as it avoids the cost of creating a component for each row, and there's a lot.

enter image description here

I have managed to show the component and be able to interact with the cell, (however I still a first click).

enter image description here

But what I'm struggling with is that I want my editor component to be expandable, and of course update the current row height. And of course to revert to regular row height when collapsed. I think I need to register some listeners to the cell editor, but I'm currently not able to correctly do that.

  • ComponentListner::componentResized make the whole table being constantly repainted.
  • editorComponent.addPropertyChangeListener("preferredSize", propertyChangeListener), doesn't always repaint the table
  • editorComponent.addPropertyChangeListener("preferredSize", propertyChangeListener), doesn't always repaint the table

Hence this question.

enter image description here enter image description here

Somewhat out of scope, I'm currently debating if the component should stay expanded when going to another cell (but this can be an expanded property that is part of the model).

Here's a minimal example without my attempts to register listeners on the editor component (in getTableCellEditorComponent).

Thanks in advance for ay pointers.

package io.github.bric3.fireplace.ui;

import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;

public class DynamicCellRow {
    @NotNull
    private static JTable makeTable() {
        var jTable = new JTable(
                new Object[][]{
                        {"a", "charly"},
                        {"b", "tango"}
                },
                new Object[]{"id", "control"}
        );
        {
            var richColumn = jTable.getColumnModel().getColumn(1);
            richColumn.setCellRenderer(new TableCellRenderer() {
                private final ExpandablePanel expandablePanelRenderComponent = new ExpandablePanel();
                @Override
                public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
                    expandablePanelRenderComponent.setValue(value);
                    return updatePreferredRowHeight(table, expandablePanelRenderComponent, row, column);
                }
            });

            richColumn.setCellEditor(new DynamicCellEditor());
        }
        return jTable;
    }

    public static <T extends JComponent> T updatePreferredRowHeight(JTable table, T cellComponent, int row, int column) {
        // Adjust cell height per component
        int originalPreferredHeight = cellComponent.getPreferredSize().height;
        cellComponent.setSize(
                table.getColumnModel().getColumn(column).getWidth(),
                originalPreferredHeight
        );
        int newPreferredHeight = cellComponent.getPreferredSize().height;
        if (table.getRowHeight(row) < newPreferredHeight) {
            table.setRowHeight(row, newPreferredHeight);
        }
        return cellComponent;
    }

    static class ExpandablePanel extends JPanel {

        private final JLabel comp;

        ExpandablePanel() {
            setBorder(BorderFactory.createLineBorder(Color.black));
            setLayout(new GridBagLayout());

            GridBagConstraints gbc = new GridBagConstraints();
            // horizontal left-to-right layout
            gbc.gridx = 0;
            gbc.gridy = 0;

            // resizing behavior
            gbc.weightx = 1;
            gbc.weighty = 1;

            gbc.insets = new Insets(2, 2, 2, 2);
            gbc.fill = GridBagConstraints.BOTH;

            JPanel advanced = new JPanel();
            {
                advanced.setLayout(new BoxLayout(advanced, BoxLayout.Y_AXIS));
                advanced.setBorder(new TitledBorder("Advance Settings"));
                advanced.add(new JCheckBox("Live"));
                advanced.add(new JCheckBox("Condition"));
                advanced.add(new JCheckBox("Disable"));
            }
            advanced.setVisible(false);

            var standard = new JPanel();
            {
                standard.setLayout(new BoxLayout(standard, BoxLayout.X_AXIS));
                comp = new JLabel("Label 1");
                standard.add(comp);
                standard.add(new JButton("Button 1"));
                var expandButton = new JButton(" ");
                expandButton.addActionListener(e -> {
                    if (advanced.isVisible()) {
                        advanced.setVisible(false);
                        expandButton.setText(" ");
                    } else {
                        advanced.setVisible(true);
                        expandButton.setText("-");
                    }
                });
                standard.add(expandButton);
            }
            add(standard, gbc);


            gbc.gridy  ;
            gbc.weighty = 0;
            add(advanced, gbc);
        }

        public void setValue(Object value) {
            comp.setText(value.toString());
        }
    }

    private static class DynamicCellEditor extends AbstractCellEditor implements TableCellEditor {
        Object value;
        @Override
        public Object getCellEditorValue() {
            return value; // not changing
        }

        private final ExpandablePanel expandablePanel = new ExpandablePanel();

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            this.value = value;
            expandablePanel.setValue(value);
            return updatePreferredRowHeight(table, expandablePanel, row, column);
        }
    }


    public static void main(String[] args) {

        var contentPane = new JPanel(new BorderLayout());
        contentPane.add(new JScrollPane(makeTable()));

        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("DynamicCellRow");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(contentPane);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

CodePudding user response:

@kleopatra Thank you! Your tip got me on the right track. I made the following code to precompute the row height during the layout operation.

And when a cell gets edited, the editor is added to the table, when then do a revalidation, and computes the height accordingly.

I had a problem before because setting the height in the renderer actually prevented the correct display of the cell component.

In the following code, notice how doLayout is adjusting the row height, according to cell component preferred size (wether the cell is rendered or edited).

Note I choose to collapse the component once editing is stopped, hence the listener in the DynamicExpndablePanelCellEditor.

import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.border.TitledBorder;
import javax.swing.event.CellEditorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;

public class DynamicCellRow {
    public static void main(String[] args) {

        var contentPane = new JPanel(new BorderLayout());
        contentPane.add(new JScrollPane(makeTable()));

        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("DynamicCellRow");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(contentPane);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    @NotNull
    private static JTable makeTable() {
        var jTable = new JTable(
                new Object[][]{
                        {"a", "charly"},
                        {"b", "tango"}
                },
                new Object[]{"id", "control"}
        ) {
            @Override
            public void doLayout() {
                super.doLayout();
                adjustRowHeights();
            }

            private void adjustRowHeights() {
                for (int row = 0; row < getRowCount(); row  ) {
                    int rowHeight = getRowHeight();

                    for (int column = 0; column < getColumnCount(); column  ) {
                        var editorComponent = getEditorComponent();
                        if (getEditingRow() == row && getEditingColumn() == column && editorComponent != null) {
                            editorComponent.setSize(getColumnModel().getColumn(column).getWidth(), 0);
                            rowHeight = Math.max(rowHeight, editorComponent.getPreferredSize().height);
                        } else {
                            var comp = prepareRenderer(getCellRenderer(row, column), row, column);
                            rowHeight = Math.max(rowHeight, comp.getPreferredSize().height);
                        }
                    }

                    setRowHeight(row, rowHeight);
                }
            }
        };
        {
            var richColumn = jTable.getColumnModel().getColumn(1);
            richColumn.setCellRenderer(new ExpandablePanelCellRenderer());
            richColumn.setCellEditor(new DynamicExpndablePanelCellEditor());
        }
        return jTable;
    }


    static class ExpandablePanel extends JPanel {

        private final JLabel comp;
        private final JPanel advanced;

        ExpandablePanel() {
            setBorder(BorderFactory.createLineBorder(Color.black));
            setLayout(new GridBagLayout());

            GridBagConstraints gbc = new GridBagConstraints();
            // horizontal left-to-right layout
            gbc.gridx = 0;
            gbc.gridy = 0;

            // resizing behavior
            gbc.weightx = 1;
            gbc.weighty = 1;

            gbc.insets = new Insets(2, 2, 2, 2);
            gbc.fill = GridBagConstraints.BOTH;

            advanced = new JPanel();
            {
                advanced.setLayout(new BoxLayout(advanced, BoxLayout.Y_AXIS));
                advanced.setBorder(new TitledBorder("Advance Settings"));
                advanced.add(new JCheckBox("Live"));
                advanced.add(new JCheckBox("Condition"));
                advanced.add(new JCheckBox("Disable"));
            }
            advanced.setVisible(false);

            var standard = new JPanel();
            {
                standard.setLayout(new BoxLayout(standard, BoxLayout.X_AXIS));
                comp = new JLabel("Label 1");
                standard.add(comp);
                standard.add(new JButton("Button 1"));
                var expandButton = new JButton(" ");
                expandButton.addActionListener(e -> {
                    if (advanced.isVisible()) {
                        advanced.setVisible(false);
                        expandButton.setText(" ");
                    } else {
                        advanced.setVisible(true);
                        expandButton.setText("-");
                    }
                });
                standard.add(expandButton);
            }
            add(standard, gbc);


            gbc.gridy  ;
            gbc.weighty = 0;
            add(advanced, gbc);
        }

        public void setValue(Object value) {
            comp.setText(value.toString());
        }

        public void setAdvancedVisibility(boolean visible) {
            advanced.setVisible(visible);
        }
    }

    private static class DynamicExpndablePanelCellEditor extends AbstractCellEditor implements TableCellEditor {
        Object value;
        @Override
        public Object getCellEditorValue() {
            return value; // not changing
        }

        private final ExpandablePanel expandablePanel = new ExpandablePanel();
        {
            addCellEditorListener(new CellEditorListener() {
                @Override
                public void editingStopped(ChangeEvent e) {
                    expandablePanel.setAdvancedVisibility(false);
                }

                @Override
                public void editingCanceled(ChangeEvent e) {
                    expandablePanel.setAdvancedVisibility(false);
                }
            });
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            this.value = value;
            expandablePanel.setValue(value);

            return expandablePanel;
        }
    }

    private static class ExpandablePanelCellRenderer implements TableCellRenderer {
        private final ExpandablePanel expandablePanelRenderComponent = new ExpandablePanel();

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            expandablePanelRenderComponent.setValue(value);
            return expandablePanelRenderComponent;
        }
    }
}
  • Related