PanelTable.java

/*******************************************************************************
 * Copyhacked (H) 2012-2020.
 * This program and the accompanying materials
 * are made available under no term at all, use it like
 * you want, but share and discuss about it
 * every time possible with every body.
 * 
 * Contributors:
 *      ron190 at ymail dot com - initial implementation
 ******************************************************************************/
package com.jsql.view.swing.table;

import com.jsql.util.LogLevelUtil;
import com.jsql.view.swing.popupmenu.JPopupMenuTable;
import com.jsql.view.swing.scrollpane.JScrollIndicator;
import com.jsql.view.swing.tab.ButtonClose;
import com.jsql.view.swing.text.JTextFieldPlaceholder;
import com.jsql.view.swing.util.UiStringUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import java.awt.*;
import java.awt.event.*;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Display a table for database values. Add keyboard shortcut, mouse icon, text
 * and header formatting.
 */
public class PanelTable extends JPanel {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();
    
    /**
     * Table to display in the panel.
     */
    private final JTable tableValues;

    /**
     * Create a panel containing a table to display injection values.
     * 
     * @param data Array 2D with injection table data
     * @param columnNames Names of columns from database
     */
    public PanelTable(String[][] data, String[] columnNames) {
        
        super(new BorderLayout());

        this.tableValues = new JTable(data, columnNames) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };

        this.tableValues.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        this.tableValues.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        this.tableValues.setColumnSelectionAllowed(true);
        this.tableValues.setRowHeight(20);
        this.tableValues.setRowSelectionAllowed(true);
        this.tableValues.setCellSelectionEnabled(true);
        this.tableValues.setGridColor(Color.LIGHT_GRAY);

        this.initializeRenderer();

        this.tableValues.getTableHeader().setReorderingAllowed(false);

        this.initializeMouseEvent();
        this.initializeTabShortcut();

        var columnAdjuster = new AdjusterTableColumn(this.tableValues);
        columnAdjuster.adjustColumns();

        final TableRowSorter<TableModel> rowSorter = new TableRowSorter<>(this.tableValues.getModel());
        this.tableValues.setRowSorter(rowSorter);
        
        this.initializeTableScroller();
        this.initializePanelSearch(rowSorter);

        Comparator<Object> comparatorNumeric = new ComparatorColumn<>();
        
        for (var i = 0 ; i < this.tableValues.getColumnCount() ; i++) {
            rowSorter.setComparator(i, comparatorNumeric);
        }
    }

    private void initializeMouseEvent() {
        
        this.tableValues.setDragEnabled(true);

        this.tableValues.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                
                PanelTable.this.tableValues.requestFocusInWindow();

                if (SwingUtilities.isRightMouseButton(e)) {
                    
                    // Keep selection when multiple cells are selected,
                    // move focus only
                    var p = e.getPoint();

                    var rowNumber = PanelTable.this.tableValues.rowAtPoint(p);
                    var colNumber = PanelTable.this.tableValues.columnAtPoint(p);

                    DefaultListSelectionModel modelRow = (DefaultListSelectionModel) PanelTable.this.tableValues.getSelectionModel();
                    DefaultListSelectionModel modelColumn = (DefaultListSelectionModel) PanelTable.this.tableValues.getColumnModel().getSelectionModel();

                    modelRow.moveLeadSelectionIndex(rowNumber);
                    modelColumn.moveLeadSelectionIndex(colNumber);
                }
            }
        });
    }

    private void initializeRenderer() {
        
        final TableCellRenderer cellRendererHeader = this.tableValues.getTableHeader().getDefaultRenderer();
        final var cellRendererDefault = new DefaultTableCellRenderer();
        
        this.tableValues.getTableHeader().setDefaultRenderer(
            (JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) -> {
                
                JLabel label = (JLabel) cellRendererHeader.getTableCellRendererComponent(
                    table,
                    UiStringUtil.detectUtf8HtmlNoWrap(StringUtils.SPACE + value + StringUtils.SPACE),
                    isSelected,
                    hasFocus,
                    row,
                    column
                );
                
                label.setBorder(
                    BorderFactory.createCompoundBorder(
                        BorderFactory.createMatteBorder(1, 0, 1, 1, Color.LIGHT_GRAY),
                        BorderFactory.createEmptyBorder(0, 5, 0, 5)
                    )
                );
                
                return label;
            }
        );
        
        this.tableValues.setDefaultRenderer(
            this.tableValues.getColumnClass(2),
            (JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) -> {
                
                // Prepare cell value to be utf8 inspected
                String cellValue = value != null ? value.toString() : StringUtils.EMPTY;
                
                // Fix #90481: NullPointerException on getTableCellRendererComponent()
                try {
                    return cellRendererDefault.getTableCellRendererComponent(
                        table, UiStringUtil.detectUtf8HtmlNoWrap(cellValue), isSelected, hasFocus, row, column
                    );
                } catch (NullPointerException e) {
                    
                    LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
                    return null;
                }
            }
        );
    }

    private void initializeTableScroller() {
        
        var scroller = new JScrollIndicator(this.tableValues);
        scroller.getScrollPane().setBorder(BorderFactory.createEmptyBorder(0, 0, -1, -1));
        scroller.getScrollPane().setViewportBorder(BorderFactory.createEmptyBorder(0, 0, -1, -1));
        
        AdjustmentListener singleItemScroll = adjustmentEvent -> {
            
            // The user scrolled the List (using the bar, mouse wheel or something else):
            if (adjustmentEvent.getAdjustmentType() == AdjustmentEvent.TRACK) {
                
                adjustmentEvent.getAdjustable().setBlockIncrement(100);
                adjustmentEvent.getAdjustable().setUnitIncrement(100);
            }
        };

        scroller.getScrollPane().getVerticalScrollBar().addAdjustmentListener(singleItemScroll);
        scroller.getScrollPane().getHorizontalScrollBar().addAdjustmentListener(singleItemScroll);

        var tableFixedColumn = new FixedColumnTable();
        tableFixedColumn.fixColumnSize(2, scroller.getScrollPane());
        
        this.add(scroller, BorderLayout.CENTER);
    }

    private void initializePanelSearch(final TableRowSorter<TableModel> rowSorter) {
        
        final var panelSearch = new JPanel(new BorderLayout());
        panelSearch.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
        
        final JTextField textFilter = new JTextFieldPlaceholder("Find in table");
        panelSearch.add(textFilter, BorderLayout.CENTER);

        Action actionShowSearchTable = new ActionShowSearch(panelSearch, textFilter);
        this.tableValues.getActionMap().put("search", actionShowSearchTable);
        this.tableValues.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK), "search");

        Action actionCloseSearch = new ActionCloseSearch(textFilter, panelSearch, this);
        textFilter.getActionMap().put("close", actionCloseSearch);
        textFilter.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close");

        // Fix #43974: PatternSyntaxException on regexFilter() => Pattern.quote()
        textFilter.getDocument().addDocumentListener(new DocumentListener() {
            
            private void insertUpdateFixed() {
                
                String text = textFilter.getText();

                if (text.trim().isEmpty()) {
                    rowSorter.setRowFilter(null);
                } else {
                    rowSorter.setRowFilter(RowFilter.regexFilter("(?i)" + Pattern.quote(text)));
                }
            }
            
            @Override
            public void insertUpdate(DocumentEvent e) {
                this.insertUpdateFixed();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                this.insertUpdateFixed();
            }

            @Override
            public void changedUpdate(DocumentEvent e) {
                throw new UnsupportedOperationException("Not supported yet.");
            }
        });

        this.tableValues.setComponentPopupMenu(new JPopupMenuTable(this.tableValues, actionShowSearchTable));
        
        JButton buttonCloseSearch = new ButtonClose();
        buttonCloseSearch.addActionListener(actionCloseSearch);
        panelSearch.add(buttonCloseSearch, BorderLayout.EAST);
        
        this.add(panelSearch, BorderLayout.SOUTH);
        
        panelSearch.setVisible(false);
    }

    private void initializeTabShortcut() {
        
        this.tableValues.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
            KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
            null
        );
        this.tableValues.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
            KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK),
            null
        );

        Set<AWTKeyStroke> forward = new HashSet<>(
            this.tableValues.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
        );
        forward.add(KeyStroke.getKeyStroke("TAB"));
        this.tableValues.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forward);
        
        Set<AWTKeyStroke> backward = new HashSet<>(
            this.tableValues.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
        );
        backward.add(KeyStroke.getKeyStroke("shift TAB"));
        this.tableValues.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, backward);
    }

    /**
     * Select every cell.
     */
    public void selectTable() {
        this.tableValues.selectAll();
    }

    /**
     * Perform copy event on current table.
     */
    public void copyTable() {
        
        var nev = new ActionEvent(this.tableValues, ActionEvent.ACTION_PERFORMED, "copy");
        this.tableValues.getActionMap().get(nev.getActionCommand()).actionPerformed(nev);
    }

    
    // Getter and setter
    
    public JTable getTableValues() {
        return this.tableValues;
    }
}