001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.gui;
021
022import java.awt.Component;
023import java.awt.Dimension;
024import java.awt.FontMetrics;
025import java.awt.event.ActionEvent;
026import java.awt.event.MouseAdapter;
027import java.awt.event.MouseEvent;
028import java.io.Serial;
029import java.util.ArrayDeque;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.Deque;
033import java.util.EventObject;
034import java.util.List;
035import java.util.stream.Collectors;
036
037import javax.swing.AbstractAction;
038import javax.swing.Action;
039import javax.swing.JTable;
040import javax.swing.JTextArea;
041import javax.swing.JTree;
042import javax.swing.KeyStroke;
043import javax.swing.LookAndFeel;
044import javax.swing.table.TableCellEditor;
045import javax.swing.tree.TreePath;
046
047import com.puppycrawl.tools.checkstyle.api.DetailAST;
048import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
049import com.puppycrawl.tools.checkstyle.xpath.ElementNode;
050import com.puppycrawl.tools.checkstyle.xpath.RootNode;
051import com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator;
052import net.sf.saxon.trans.XPathException;
053
054/**
055 * This example shows how to create a simple TreeTable component,
056 * by using a JTree as a renderer (and editor) for the cells in a particular column in the JTable.
057 * <a href=
058 * "https://docs.oracle.com/cd/E48246_01/apirefs.1111/e13403/oracle/ide/controls/TreeTableModel.html">
059 * Original&nbsp;Source&nbsp;Location</a>
060 *
061 * @noinspection ThisEscapedInObjectConstruction
062 * @noinspectionreason ThisEscapedInObjectConstruction - only reference is used and not
063 *      accessed until initialized
064 */
065public final class TreeTable extends JTable {
066
067    /** A unique serial version identifier. */
068    @Serial
069    private static final long serialVersionUID = -8493693409423365387L;
070    /** The newline character. */
071    private static final String NEWLINE = "\n";
072    /** A subclass of JTree. */
073    private final TreeTableCellRenderer tree;
074    /** JTextArea editor. */
075    private JTextArea editor;
076    /** JTextArea xpathEditor. */
077    private JTextArea xpathEditor;
078    /** Line position map. */
079    private List<Integer> linePositionList;
080
081    /**
082     * Creates TreeTable base on TreeTableModel.
083     *
084     * @param treeTableModel Tree table model
085     */
086    public TreeTable(ParseTreeTableModel treeTableModel) {
087        // Create the tree. It will be used as a renderer and editor.
088        tree = new TreeTableCellRenderer(this, treeTableModel);
089
090        // Install a tableModel representing the visible rows in the tree.
091        setModel(new TreeTableModelAdapter(treeTableModel, tree));
092
093        // Force the JTable and JTree to share their row selection models.
094        final ListToTreeSelectionModelWrapper selectionWrapper = new
095                ListToTreeSelectionModelWrapper(this);
096        tree.setSelectionModel(selectionWrapper);
097        setSelectionModel(selectionWrapper.getListSelectionModel());
098
099        // Install the tree editor renderer and editor.
100        setDefaultRenderer(ParseTreeTableModel.class, tree);
101        setDefaultEditor(ParseTreeTableModel.class, new TreeTableCellEditor());
102
103        // No grid.
104        setShowGrid(false);
105
106        // No intercell spacing
107        setIntercellSpacing(new Dimension(0, 0));
108
109        // And update the height of the trees row to match that of
110        // the table.
111        if (tree.getRowHeight() < 1) {
112            // Metal looks better like this.
113            final int height = getRowHeight();
114            setRowHeight(height);
115        }
116
117        setColumnsInitialWidth();
118
119        final Action expand = new AbstractAction() {
120            @Serial
121            private static final long serialVersionUID = -5859674518660156121L;
122
123            @Override
124            public void actionPerformed(ActionEvent event) {
125                expandSelectedNode();
126            }
127        };
128        final KeyStroke stroke = KeyStroke.getKeyStroke("ENTER");
129        final String command = "expand/collapse";
130        getInputMap().put(stroke, command);
131        getActionMap().put(command, expand);
132
133        addMouseListener(new MouseAdapter() {
134            @Override
135            public void mouseClicked(MouseEvent event) {
136                if (event.getClickCount() == 2) {
137                    expandSelectedNode();
138                }
139            }
140        });
141    }
142
143    /**
144     * Do expansion of a tree node.
145     */
146    private void expandSelectedNode() {
147        final TreePath selected = tree.getSelectionPath();
148        makeCodeSelection();
149        generateXpath();
150
151        if (tree.isExpanded(selected)) {
152            tree.collapsePath(selected);
153        }
154        else {
155            tree.expandPath(selected);
156        }
157        tree.setSelectionPath(selected);
158    }
159
160    /**
161     * Make selection of code in a text area.
162     */
163    private void makeCodeSelection() {
164        new CodeSelector(tree.getLastSelectedPathComponent(), editor, linePositionList).select();
165    }
166
167    /**
168     * Generate Xpath.
169     */
170    private void generateXpath() {
171        if (tree.getLastSelectedPathComponent() instanceof final DetailAST ast) {
172            final String xpath = XpathQueryGenerator.generateXpathQuery(ast);
173            xpathEditor.setText(xpath);
174        }
175        else {
176            xpathEditor.setText("Xpath is not supported yet for javadoc nodes");
177        }
178    }
179
180    /**
181     * Set initial value of width for columns in table.
182     */
183    private void setColumnsInitialWidth() {
184        final FontMetrics fontMetrics = getFontMetrics(getFont());
185        // Six character string to contain "Column" column.
186        final int widthOfSixCharacterString = fontMetrics.stringWidth("MMMMMM");
187        // Padding must be added to width for columns to make them fully
188        // visible in table header.
189        final int padding = 10;
190        final int widthOfColumnContainingSixCharacterString =
191                widthOfSixCharacterString + padding;
192        getColumn("Line").setMaxWidth(widthOfColumnContainingSixCharacterString);
193        getColumn("Column").setMaxWidth(widthOfColumnContainingSixCharacterString);
194        final int preferredTreeColumnWidth =
195                Math.toIntExact(Math.round(getPreferredSize().getWidth() * 0.6));
196        getColumn("Tree").setPreferredWidth(preferredTreeColumnWidth);
197        // Twenty-eight character string to contain "Type" column
198        final int widthOfTwentyEightCharacterString =
199                fontMetrics.stringWidth("MMMMMMMMMMMMMMMMMMMMMMMMMMMM");
200        final int preferredTypeColumnWidth = widthOfTwentyEightCharacterString + padding;
201        getColumn("Type").setPreferredWidth(preferredTypeColumnWidth);
202    }
203
204    /**
205     * Select Node by Xpath.
206     */
207    public void selectNodeByXpath() {
208        final DetailAST rootAST = (DetailAST) tree.getModel().getRoot();
209        if (rootAST.hasChildren()) {
210            final String xpath = xpathEditor.getText();
211
212            try {
213                final Deque<DetailAST> nodes =
214                        XpathUtil.getXpathItems(xpath, new RootNode(rootAST))
215                              .stream()
216                              .map(ElementNode.class::cast)
217                              .map(ElementNode::getUnderlyingNode)
218                              .collect(Collectors.toCollection(ArrayDeque::new));
219                updateTreeTable(xpath, nodes);
220            }
221            catch (XPathException exception) {
222                xpathEditor.setText(xpathEditor.getText() + NEWLINE + exception.getMessage());
223            }
224        }
225        else {
226            xpathEditor.setText("No file Opened");
227        }
228    }
229
230    /**
231     * Updates the Treetable by expanding paths in the tree and highlighting 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 see the node.
252     *
253     * @param node node to expand table by
254     */
255    private void expandTreeTableByPath(DetailAST node) {
256        TreePath path = new TreePath(node);
257        path = path.pathByAddingChild(node);
258        if (!tree.isExpanded(path)) {
259            tree.expandPath(path);
260        }
261        tree.setSelectionPath(path);
262    }
263
264    /**
265     * Generates a String with all matching XPath queries separated by newlines.
266     *
267     * @param nodes deque of nodes to generate queries for
268     * @return complete text of all XPath expressions separated by newlines.
269     */
270    private static String getAllMatchingXpathQueriesText(Deque<DetailAST> nodes) {
271        return nodes.stream()
272                .map(XpathQueryGenerator::generateXpathQuery)
273                .collect(Collectors.joining(NEWLINE, "", NEWLINE));
274    }
275
276    /**
277     * Overridden to message super and forward the method to the tree.
278     * Since the tree is not actually in the component hierarchy it will
279     * never receive this unless we forward it in this manner.
280     */
281    @Override
282    public void updateUI() {
283        super.updateUI();
284        if (tree != null) {
285            tree.updateUI();
286        }
287        // Use the tree's default foreground and background colors in the
288        // table.
289        LookAndFeel.installColorsAndFont(this, "Tree.background",
290                "Tree.foreground", "Tree.font");
291    }
292
293    /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
294     * paint the editor. The UI currently uses different techniques to
295     * paint the renderers and editors and overriding setBounds() below
296     * is not the right thing to do for an editor. Returning -1 for the
297     * editing row in this case, ensures the editor is never painted.
298     */
299    @Override
300    public int getEditingRow() {
301        int rowIndex = -1;
302        final Class<?> editingClass = getColumnClass(editingColumn);
303        if (editingClass != ParseTreeTableModel.class) {
304            rowIndex = editingRow;
305        }
306        return rowIndex;
307    }
308
309    /**
310     * Overridden to pass the new rowHeight to the tree.
311     */
312    @Override
313    public void setRowHeight(int newRowHeight) {
314        super.setRowHeight(newRowHeight);
315        if (tree != null && tree.getRowHeight() != newRowHeight) {
316            tree.setRowHeight(getRowHeight());
317        }
318    }
319
320    /**
321     * Returns tree.
322     *
323     * @return the tree that is being shared between the model.
324     */
325    public JTree getTree() {
326        return tree;
327    }
328
329    /**
330     * Sets text area editor.
331     *
332     * @param textArea JTextArea component.
333     */
334    public void setEditor(JTextArea textArea) {
335        editor = textArea;
336    }
337
338    /**
339     * Sets text area xpathEditor.
340     *
341     * @param xpathTextArea JTextArea component.
342     */
343    public void setXpathEditor(JTextArea xpathTextArea) {
344        xpathEditor = xpathTextArea;
345    }
346
347    /**
348     * Sets line positions.
349     *
350     * @param linePositionList positions of lines.
351     */
352    public void setLinePositionList(Collection<Integer> linePositionList) {
353        this.linePositionList = new ArrayList<>(linePositionList);
354    }
355
356    /**
357     * TreeTableCellEditor implementation. Component returned is the JTree.
358     */
359    private final class TreeTableCellEditor extends BaseCellEditor implements
360            TableCellEditor {
361
362        /**
363         * Creates a new {@code TreeTableCellEditor} instance.
364         */
365        private TreeTableCellEditor() {
366            // no code by default
367        }
368
369        @Override
370        public Component getTableCellEditorComponent(JTable table,
371                Object value,
372                boolean isSelected,
373                int row, int column) {
374            return tree;
375        }
376
377        /**
378         * Overridden to return false, and if the event is a mouse event
379         * it is forwarded to the tree.
380         *
381         * <p>The behavior for this is debatable, and should really be offered
382         * as a property. By returning false, all keyboard actions are
383         * implemented in terms of the table. By returning true, the
384         * tree would get a chance to do something with the keyboard
385         * events. For the most part this is ok. But for certain keys,
386         * such as left/right, the tree will expand/collapse where as
387         * the table focus should really move to a different column. Page
388         * up/down should also be implemented in terms of the table.
389         * By returning false this also has the added benefit that clicking
390         * outside of the bounds of the tree node, but still in the tree
391         * column will select the row, whereas if this returned true that wouldn't be the case.
392         *
393         * <p>By returning false we are also enforcing the policy that
394         * the tree will never be editable (at least by a key sequence).
395         *
396         * @see TableCellEditor
397         */
398        @Override
399        public boolean isCellEditable(EventObject event) {
400            if (event instanceof final MouseEvent mouseEvent) {
401                for (int counter = getColumnCount() - 1; counter >= 0;
402                     counter--) {
403                    if (getColumnClass(counter) == ParseTreeTableModel.class) {
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}