View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.view.swing.table;
12  
13  import com.jsql.util.LogLevelUtil;
14  import com.jsql.view.swing.popupmenu.JPopupMenuTable;
15  import com.jsql.view.swing.text.JTextFieldPlaceholder;
16  import com.jsql.view.swing.util.UiStringUtil;
17  import org.apache.commons.lang3.StringUtils;
18  import org.apache.logging.log4j.LogManager;
19  import org.apache.logging.log4j.Logger;
20  
21  import javax.swing.*;
22  import javax.swing.event.DocumentEvent;
23  import javax.swing.event.DocumentListener;
24  import javax.swing.table.DefaultTableCellRenderer;
25  import javax.swing.table.TableCellRenderer;
26  import javax.swing.table.TableModel;
27  import javax.swing.table.TableRowSorter;
28  import java.awt.*;
29  import java.awt.event.*;
30  import java.util.Comparator;
31  import java.util.HashSet;
32  import java.util.Set;
33  import java.util.regex.Pattern;
34  
35  /**
36   * Display a table for database values. Add keyboard shortcut, mouse icon, text
37   * and header formatting.
38   */
39  public class PanelTable extends JPanel {
40      
41      private static final Logger LOGGER = LogManager.getRootLogger();
42      
43      /**
44       * Table to display in the panel.
45       */
46      private final JTable tableValues;
47  
48      /**
49       * Create a panel containing a table to display injection values.
50       * 
51       * @param data Array 2D with injection table data
52       * @param columnNames Names of columns from database
53       */
54      public PanelTable(String[][] data, String[] columnNames) {
55          super(new BorderLayout());
56  
57          this.tableValues = new JTable(data, columnNames) {
58              @Override
59              public boolean isCellEditable(int row, int column) {
60                  return false;
61              }
62          };
63          this.tableValues.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
64          this.tableValues.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
65          this.tableValues.setColumnSelectionAllowed(true);
66          this.tableValues.setRowHeight(20);
67          this.tableValues.setRowSelectionAllowed(true);
68          this.tableValues.setCellSelectionEnabled(true);
69  
70          this.initRenderer();
71  
72          this.tableValues.getTableHeader().setReorderingAllowed(false);
73  
74          this.initMouseEvent();
75          this.initTabShortcut();
76  
77          var columnAdjuster = new AdjusterTableColumn(this.tableValues);
78          columnAdjuster.adjustColumns();
79  
80          final TableRowSorter<TableModel> rowSorter = new TableRowSorter<>(this.tableValues.getModel());
81          this.tableValues.setRowSorter(rowSorter);
82          this.initTableScroller();
83          this.initPanelSearch(rowSorter);
84  
85          Comparator<Object> comparatorNumeric = new ComparatorColumn<>();
86          for (var i = 0 ; i < this.tableValues.getColumnCount() ; i++) {
87              rowSorter.setComparator(i, comparatorNumeric);
88          }
89      }
90  
91      private void initMouseEvent() {
92          this.tableValues.setDragEnabled(true);
93          this.tableValues.addMouseListener(new MouseAdapter() {
94              @Override
95              public void mousePressed(MouseEvent e) {
96                  PanelTable.this.tableValues.requestFocusInWindow();
97                  if (SwingUtilities.isRightMouseButton(e)) {
98                      // Keep selection when multiple cells are selected,
99                      // move focus only
100                     var p = e.getPoint();
101                     var rowNumber = PanelTable.this.tableValues.rowAtPoint(p);
102                     var colNumber = PanelTable.this.tableValues.columnAtPoint(p);
103                     DefaultListSelectionModel modelRow = (DefaultListSelectionModel) PanelTable.this.tableValues.getSelectionModel();
104                     DefaultListSelectionModel modelColumn = (DefaultListSelectionModel) PanelTable.this.tableValues.getColumnModel().getSelectionModel();
105                     modelRow.moveLeadSelectionIndex(rowNumber);
106                     modelColumn.moveLeadSelectionIndex(colNumber);
107                 }
108             }
109         });
110     }
111 
112     private void initRenderer() {
113         final TableCellRenderer cellRendererHeader = this.tableValues.getTableHeader().getDefaultRenderer();
114         this.tableValues.getTableHeader().setDefaultRenderer(
115             (JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) -> cellRendererHeader.getTableCellRendererComponent(
116                 table,
117                 UiStringUtil.detectUtf8HtmlNoWrap(StringUtils.SPACE + value + StringUtils.SPACE),
118                 isSelected,
119                 hasFocus,
120                 row,
121                 column
122             )
123         );
124 
125         final var cellRendererDefault = new DefaultTableCellRenderer();
126         this.tableValues.setDefaultRenderer(
127             this.tableValues.getColumnClass(2),
128             (JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) -> {
129                 // Prepare cell value to be utf8 inspected
130                 String cellValue = value != null ? value.toString() : StringUtils.EMPTY;
131                 // Fix #90481: NullPointerException on getTableCellRendererComponent()
132                 // Fix #96072: IllegalArgumentException on getTableCellRendererComponent()
133                 try {
134                     return cellRendererDefault.getTableCellRendererComponent(
135                         table, UiStringUtil.detectUtf8HtmlNoWrap(cellValue), isSelected, hasFocus, row, column
136                     );
137                 } catch (IllegalArgumentException | NullPointerException e) {
138                     LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
139                     return null;
140                 }
141             }
142         );
143     }
144 
145     private void initTableScroller() {
146         var scroller = new JScrollPane(this.tableValues);
147         var tableFixedColumn = new FixedColumnTable();
148         tableFixedColumn.fixColumnSize(2, scroller);
149         this.add(scroller, BorderLayout.CENTER);
150     }
151 
152     private void initPanelSearch(final TableRowSorter<TableModel> rowSorter) {
153         final var panelSearch = new JPanel(new BorderLayout());
154 
155         final JTextField textFilter = new JTextFieldPlaceholder("Find in table");
156         panelSearch.add(textFilter, BorderLayout.CENTER);
157 
158         Action actionShowSearchTable = new ActionShowSearch(panelSearch, textFilter);
159         String keySearch = "search";
160         this.tableValues.getActionMap().put(keySearch, actionShowSearchTable);
161         this.tableValues.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK), keySearch);
162 
163         Action actionCloseSearch = new ActionCloseSearch(textFilter, panelSearch, this);
164         String keyClose = "close";
165         textFilter.getActionMap().put(keyClose, actionCloseSearch);
166         textFilter.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), keyClose);
167 
168         // Fix #43974: PatternSyntaxException on regexFilter() => Pattern.quote()
169         textFilter.getDocument().addDocumentListener(new DocumentListener() {
170             private void insertUpdateFixed() {
171                 String text = textFilter.getText();
172                 if (text.trim().isEmpty()) {
173                     rowSorter.setRowFilter(null);
174                 } else {
175                     rowSorter.setRowFilter(RowFilter.regexFilter("(?i)" + Pattern.quote(text)));
176                 }
177             }
178             @Override
179             public void insertUpdate(DocumentEvent e) {
180                 this.insertUpdateFixed();
181             }
182             @Override
183             public void removeUpdate(DocumentEvent e) {
184                 this.insertUpdateFixed();
185             }
186             @Override
187             public void changedUpdate(DocumentEvent e) {
188                 throw new UnsupportedOperationException("Not supported yet.");
189             }
190         });
191 
192         this.tableValues.setComponentPopupMenu(new JPopupMenuTable(this.tableValues, actionShowSearchTable));
193         
194         JButton buttonCloseSearch = new ButtonClose();
195         buttonCloseSearch.addActionListener(actionCloseSearch);
196         panelSearch.add(buttonCloseSearch, BorderLayout.EAST);
197         panelSearch.setVisible(false);
198         this.add(panelSearch, BorderLayout.SOUTH);
199     }
200 
201     private void initTabShortcut() {
202         this.tableValues.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
203             KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
204             null
205         );
206         this.tableValues.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
207             KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK),
208             null
209         );
210 
211         Set<AWTKeyStroke> forward = new HashSet<>(
212             this.tableValues.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)
213         );
214         forward.add(KeyStroke.getKeyStroke("TAB"));
215         this.tableValues.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forward);
216         
217         Set<AWTKeyStroke> backward = new HashSet<>(
218             this.tableValues.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)
219         );
220         backward.add(KeyStroke.getKeyStroke("shift TAB"));
221         this.tableValues.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, backward);
222     }
223 
224     /**
225      * Select every cell.
226      */
227     public void selectTable() {
228         this.tableValues.selectAll();
229     }
230 
231     /**
232      * Perform copy event on current table.
233      */
234     public void copyTable() {
235         var actionEvent = new ActionEvent(this.tableValues, ActionEvent.ACTION_PERFORMED, "copy");
236         this.tableValues.getActionMap().get(actionEvent.getActionCommand()).actionPerformed(actionEvent);
237     }
238 
239     
240     // Getter and setter
241     
242     public JTable getTableValues() {
243         return this.tableValues;
244     }
245 }