View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.gui;
21  
22  import java.awt.Component;
23  import java.awt.Dimension;
24  import java.awt.FontMetrics;
25  import java.awt.event.ActionEvent;
26  import java.awt.event.MouseAdapter;
27  import java.awt.event.MouseEvent;
28  import java.io.Serial;
29  import java.util.ArrayDeque;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Deque;
33  import java.util.EventObject;
34  import java.util.List;
35  import java.util.stream.Collectors;
36  
37  import javax.swing.AbstractAction;
38  import javax.swing.Action;
39  import javax.swing.JTable;
40  import javax.swing.JTextArea;
41  import javax.swing.JTree;
42  import javax.swing.KeyStroke;
43  import javax.swing.LookAndFeel;
44  import javax.swing.table.TableCellEditor;
45  import javax.swing.tree.TreePath;
46  
47  import com.puppycrawl.tools.checkstyle.api.DetailAST;
48  import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
49  import com.puppycrawl.tools.checkstyle.xpath.ElementNode;
50  import com.puppycrawl.tools.checkstyle.xpath.RootNode;
51  import com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator;
52  import net.sf.saxon.trans.XPathException;
53  
54  /**
55   * This example shows how to create a simple TreeTable component,
56   * by using a JTree as a renderer (and editor) for the cells in a
57   * particular column in the JTable.
58   * <a href=
59   * "https://docs.oracle.com/cd/E48246_01/apirefs.1111/e13403/oracle/ide/controls/TreeTableModel.html">
60   * Original&nbsp;Source&nbsp;Location</a>
61   *
62   * @noinspection ThisEscapedInObjectConstruction
63   * @noinspectionreason ThisEscapedInObjectConstruction - only reference is used and not
64   *      accessed until initialized
65   */
66  public final class TreeTable extends JTable {
67  
68      /** A unique serial version identifier. */
69      @Serial
70      private static final long serialVersionUID = -8493693409423365387L;
71      /** The newline character. */
72      private static final String NEWLINE = "\n";
73      /** A subclass of JTree. */
74      private final TreeTableCellRenderer tree;
75      /** JTextArea editor. */
76      private JTextArea editor;
77      /** JTextArea xpathEditor. */
78      private JTextArea xpathEditor;
79      /** Line position map. */
80      private List<Integer> linePositionList;
81  
82      /**
83       * Creates TreeTable base on TreeTableModel.
84       *
85       * @param treeTableModel Tree table model
86       */
87      public TreeTable(ParseTreeTableModel treeTableModel) {
88          // Create the tree. It will be used as a renderer and editor.
89          tree = new TreeTableCellRenderer(this, treeTableModel);
90  
91          // Install a tableModel representing the visible rows in the tree.
92          setModel(new TreeTableModelAdapter(treeTableModel, tree));
93  
94          // Force the JTable and JTree to share their row selection models.
95          final ListToTreeSelectionModelWrapper selectionWrapper = new
96                  ListToTreeSelectionModelWrapper(this);
97          tree.setSelectionModel(selectionWrapper);
98          setSelectionModel(selectionWrapper.getListSelectionModel());
99  
100         // Install the tree editor renderer and editor.
101         setDefaultRenderer(ParseTreeTableModel.class, tree);
102         setDefaultEditor(ParseTreeTableModel.class, new TreeTableCellEditor());
103 
104         // No grid.
105         setShowGrid(false);
106 
107         // No intercell spacing
108         setIntercellSpacing(new Dimension(0, 0));
109 
110         // And update the height of the trees row to match that of
111         // the table.
112         if (tree.getRowHeight() < 1) {
113             // Metal looks better like this.
114             final int height = getRowHeight();
115             setRowHeight(height);
116         }
117 
118         setColumnsInitialWidth();
119 
120         final Action expand = new AbstractAction() {
121             @Serial
122             private static final long serialVersionUID = -5859674518660156121L;
123 
124             @Override
125             public void actionPerformed(ActionEvent event) {
126                 expandSelectedNode();
127             }
128         };
129         final KeyStroke stroke = KeyStroke.getKeyStroke("ENTER");
130         final String command = "expand/collapse";
131         getInputMap().put(stroke, command);
132         getActionMap().put(command, expand);
133 
134         addMouseListener(new MouseAdapter() {
135             @Override
136             public void mouseClicked(MouseEvent event) {
137                 if (event.getClickCount() == 2) {
138                     expandSelectedNode();
139                 }
140             }
141         });
142     }
143 
144     /**
145      * Do expansion of a tree node.
146      */
147     private void expandSelectedNode() {
148         final TreePath selected = tree.getSelectionPath();
149         makeCodeSelection();
150         generateXpath();
151 
152         if (tree.isExpanded(selected)) {
153             tree.collapsePath(selected);
154         }
155         else {
156             tree.expandPath(selected);
157         }
158         tree.setSelectionPath(selected);
159     }
160 
161     /**
162      * Make selection of code in a text area.
163      */
164     private void makeCodeSelection() {
165         new CodeSelector(tree.getLastSelectedPathComponent(), editor, linePositionList).select();
166     }
167 
168     /**
169      * Generate Xpath.
170      */
171     private void generateXpath() {
172         if (tree.getLastSelectedPathComponent() instanceof DetailAST ast) {
173             final String xpath = XpathQueryGenerator.generateXpathQuery(ast);
174             xpathEditor.setText(xpath);
175         }
176         else {
177             xpathEditor.setText("Xpath is not supported yet for javadoc nodes");
178         }
179     }
180 
181     /**
182      * Set initial value of width for columns in table.
183      */
184     private void setColumnsInitialWidth() {
185         final FontMetrics fontMetrics = getFontMetrics(getFont());
186         // Six character string to contain "Column" column.
187         final int widthOfSixCharacterString = fontMetrics.stringWidth("XXXXXX");
188         // Padding must be added to width for columns to make them fully
189         // visible in table header.
190         final int padding = 10;
191         final int widthOfColumnContainingSixCharacterString =
192                 widthOfSixCharacterString + padding;
193         getColumn("Line").setMaxWidth(widthOfColumnContainingSixCharacterString);
194         getColumn("Column").setMaxWidth(widthOfColumnContainingSixCharacterString);
195         final int preferredTreeColumnWidth =
196                 Math.toIntExact(Math.round(getPreferredSize().getWidth() * 0.6));
197         getColumn("Tree").setPreferredWidth(preferredTreeColumnWidth);
198         // Twenty-eight character string to contain "Type" column
199         final int widthOfTwentyEightCharacterString =
200                 fontMetrics.stringWidth("XXXXXXXXXXXXXXXXXXXXXXXXXXXX");
201         final int preferredTypeColumnWidth = widthOfTwentyEightCharacterString + padding;
202         getColumn("Type").setPreferredWidth(preferredTypeColumnWidth);
203     }
204 
205     /**
206      * Select Node by Xpath.
207      */
208     public void selectNodeByXpath() {
209         final DetailAST rootAST = (DetailAST) tree.getModel().getRoot();
210         if (rootAST.hasChildren()) {
211             final String xpath = xpathEditor.getText();
212 
213             try {
214                 final Deque<DetailAST> nodes =
215                         XpathUtil.getXpathItems(xpath, new RootNode(rootAST))
216                               .stream()
217                               .map(ElementNode.class::cast)
218                               .map(ElementNode::getUnderlyingNode)
219                               .collect(Collectors.toCollection(ArrayDeque::new));
220                 updateTreeTable(xpath, nodes);
221             }
222             catch (XPathException exception) {
223                 xpathEditor.setText(xpathEditor.getText() + NEWLINE + exception.getMessage());
224             }
225         }
226         else {
227             xpathEditor.setText("No file Opened");
228         }
229     }
230 
231     /**
232      * Updates the Treetable by expanding paths in the tree and highlighting
233      * associated code.
234      *
235      * @param xpath the XPath query to show in case of no match
236      * @param nodes the deque of DetailAST nodes to match in TreeTable and XPath editor
237      */
238     private void updateTreeTable(String xpath, Deque<DetailAST> nodes) {
239         if (nodes.isEmpty()) {
240             xpathEditor.setText("No elements matching XPath query '"
241                     + xpath + "' found.");
242         }
243         else {
244             for (DetailAST node : nodes) {
245                 expandTreeTableByPath(node);
246                 makeCodeSelection();
247             }
248             xpathEditor.setText(getAllMatchingXpathQueriesText(nodes));
249         }
250     }
251 
252     /**
253      * Expands path in tree table to given node so that user can
254      * see the node.
255      *
256      * @param node node to expand table by
257      */
258     private void expandTreeTableByPath(DetailAST node) {
259         TreePath path = new TreePath(node);
260         path = path.pathByAddingChild(node);
261         if (!tree.isExpanded(path)) {
262             tree.expandPath(path);
263         }
264         tree.setSelectionPath(path);
265     }
266 
267     /**
268      * Generates a String with all matching XPath queries separated
269      * by newlines.
270      *
271      * @param nodes deque of nodes to generate queries for
272      * @return complete text of all XPath expressions separated by newlines.
273      */
274     private static String getAllMatchingXpathQueriesText(Deque<DetailAST> nodes) {
275         return nodes.stream()
276                 .map(XpathQueryGenerator::generateXpathQuery)
277                 .collect(Collectors.joining(NEWLINE, "", NEWLINE));
278     }
279 
280     /**
281      * Overridden to message super and forward the method to the tree.
282      * Since the tree is not actually in the component hierarchy it will
283      * never receive this unless we forward it in this manner.
284      */
285     @Override
286     public void updateUI() {
287         super.updateUI();
288         if (tree != null) {
289             tree.updateUI();
290         }
291         // Use the tree's default foreground and background colors in the
292         // table.
293         LookAndFeel.installColorsAndFont(this, "Tree.background",
294                 "Tree.foreground", "Tree.font");
295     }
296 
297     /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
298      * paint the editor. The UI currently uses different techniques to
299      * paint the renderers and editors and overriding setBounds() below
300      * is not the right thing to do for an editor. Returning -1 for the
301      * editing row in this case, ensures the editor is never painted.
302      */
303     @Override
304     public int getEditingRow() {
305         int rowIndex = -1;
306         final Class<?> editingClass = getColumnClass(editingColumn);
307         if (editingClass != ParseTreeTableModel.class) {
308             rowIndex = editingRow;
309         }
310         return rowIndex;
311     }
312 
313     /**
314      * Overridden to pass the new rowHeight to the tree.
315      */
316     @Override
317     public void setRowHeight(int newRowHeight) {
318         super.setRowHeight(newRowHeight);
319         if (tree != null && tree.getRowHeight() != newRowHeight) {
320             tree.setRowHeight(getRowHeight());
321         }
322     }
323 
324     /**
325      * Returns tree.
326      *
327      * @return the tree that is being shared between the model.
328      */
329     public JTree getTree() {
330         return tree;
331     }
332 
333     /**
334      * Sets text area editor.
335      *
336      * @param textArea JTextArea component.
337      */
338     public void setEditor(JTextArea textArea) {
339         editor = textArea;
340     }
341 
342     /**
343      * Sets text area xpathEditor.
344      *
345      * @param xpathTextArea JTextArea component.
346      */
347     public void setXpathEditor(JTextArea xpathTextArea) {
348         xpathEditor = xpathTextArea;
349     }
350 
351     /**
352      * Sets line positions.
353      *
354      * @param linePositionList positions of lines.
355      */
356     public void setLinePositionList(Collection<Integer> linePositionList) {
357         this.linePositionList = new ArrayList<>(linePositionList);
358     }
359 
360     /**
361      * TreeTableCellEditor implementation. Component returned is the
362      * JTree.
363      */
364     private final class TreeTableCellEditor extends BaseCellEditor implements
365             TableCellEditor {
366 
367         @Override
368         public Component getTableCellEditorComponent(JTable table,
369                 Object value,
370                 boolean isSelected,
371                 int row, int column) {
372             return tree;
373         }
374 
375         /**
376          * Overridden to return false, and if the event is a mouse event
377          * it is forwarded to the tree.
378          *
379          * <p>The behavior for this is debatable, and should really be offered
380          * as a property. By returning false, all keyboard actions are
381          * implemented in terms of the table. By returning true, the
382          * tree would get a chance to do something with the keyboard
383          * events. For the most part this is ok. But for certain keys,
384          * such as left/right, the tree will expand/collapse where as
385          * the table focus should really move to a different column. Page
386          * up/down should also be implemented in terms of the table.
387          * By returning false this also has the added benefit that clicking
388          * outside of the bounds of the tree node, but still in the tree
389          * column will select the row, whereas if this returned true
390          * that wouldn't be the case.
391          *
392          * <p>By returning false we are also enforcing the policy that
393          * the tree will never be editable (at least by a key sequence).
394          *
395          * @see TableCellEditor
396          */
397         @Override
398         public boolean isCellEditable(EventObject event) {
399             if (event instanceof MouseEvent) {
400                 for (int counter = getColumnCount() - 1; counter >= 0;
401                      counter--) {
402                     if (getColumnClass(counter) == ParseTreeTableModel.class) {
403                         final MouseEvent mouseEvent = (MouseEvent) event;
404                         final MouseEvent newMouseEvent = new MouseEvent(tree, mouseEvent.getID(),
405                                 mouseEvent.getWhen(), mouseEvent.getModifiersEx(),
406                                 mouseEvent.getX() - getCellRect(0, counter, true).x,
407                                 mouseEvent.getY(), mouseEvent.getClickCount(),
408                                 mouseEvent.isPopupTrigger());
409                         tree.dispatchEvent(newMouseEvent);
410                         break;
411                     }
412                 }
413             }
414 
415             return false;
416         }
417 
418     }
419 
420 }