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