001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2022 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.util.ArrayDeque;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Deque;
032import java.util.EventObject;
033import java.util.List;
034import java.util.stream.Collectors;
035
036import javax.swing.AbstractAction;
037import javax.swing.Action;
038import javax.swing.JTable;
039import javax.swing.JTextArea;
040import javax.swing.JTree;
041import javax.swing.KeyStroke;
042import javax.swing.LookAndFeel;
043import javax.swing.table.TableCellEditor;
044import javax.swing.tree.TreePath;
045
046import com.puppycrawl.tools.checkstyle.api.DetailAST;
047import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
048import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
049import com.puppycrawl.tools.checkstyle.xpath.RootNode;
050import com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator;
051import net.sf.saxon.trans.XPathException;
052
053/**
054 * This example shows how to create a simple TreeTable component,
055 * by using a JTree as a renderer (and editor) for the cells in a
056 * particular column in the JTable.
057 *
058 * <a href=
059 * "https://docs.oracle.com/cd/E48246_01/apirefs.1111/e13403/oracle/ide/controls/TreeTableModel.html">
060 * Original&nbsp;Source&nbsp;Location</a>
061 *
062 * @noinspection ThisEscapedInObjectConstruction
063 * @noinspectionreason ThisEscapedInObjectConstruction - only reference is used and not
064 *      accessed until initialized
065 */
066public final class TreeTable extends JTable {
067
068    /** A unique serial version identifier. */
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            private static final long serialVersionUID = -5859674518660156121L;
121
122            @Override
123            public void actionPerformed(ActionEvent event) {
124                expandSelectedNode();
125            }
126        };
127        final KeyStroke stroke = KeyStroke.getKeyStroke("ENTER");
128        final String command = "expand/collapse";
129        getInputMap().put(stroke, command);
130        getActionMap().put(command, expand);
131
132        addMouseListener(new MouseAdapter() {
133            @Override
134            public void mouseClicked(MouseEvent event) {
135                if (event.getClickCount() == 2) {
136                    expandSelectedNode();
137                }
138            }
139        });
140    }
141
142    /**
143     * Do expansion of a tree node.
144     */
145    private void expandSelectedNode() {
146        final TreePath selected = tree.getSelectionPath();
147        makeCodeSelection();
148        generateXpath();
149
150        if (tree.isExpanded(selected)) {
151            tree.collapsePath(selected);
152        }
153        else {
154            tree.expandPath(selected);
155        }
156        tree.setSelectionPath(selected);
157    }
158
159    /**
160     * Make selection of code in a text area.
161     */
162    private void makeCodeSelection() {
163        new CodeSelector(tree.getLastSelectedPathComponent(), editor, linePositionList).select();
164    }
165
166    /**
167     * Generate Xpath.
168     */
169    private void generateXpath() {
170        if (tree.getLastSelectedPathComponent() instanceof DetailAST) {
171            final DetailAST ast = (DetailAST) tree.getLastSelectedPathComponent();
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("XXXXXX");
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("XXXXXXXXXXXXXXXXXXXXXXXXXXXX");
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(AbstractNode.class::cast)
217                              .map(AbstractNode::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
232     * associated code.
233     *
234     * @param xpath the XPath query to show in case of no match
235     * @param nodes the deque of DetailAST nodes to match in TreeTable and XPath editor
236     */
237    private void updateTreeTable(String xpath, Deque<DetailAST> nodes) {
238        if (nodes.isEmpty()) {
239            xpathEditor.setText("No elements matching XPath query '"
240                    + xpath + "' found.");
241        }
242        else {
243            for (DetailAST node : nodes) {
244                expandTreeTableByPath(node);
245                makeCodeSelection();
246            }
247            xpathEditor.setText(getAllMatchingXpathQueriesText(nodes));
248        }
249    }
250
251    /**
252     * Expands path in tree table to given node so that user can
253     * see the node.
254     *
255     * @param node node to expand table by
256     */
257    private void expandTreeTableByPath(DetailAST node) {
258        TreePath path = new TreePath(node);
259        path = path.pathByAddingChild(node);
260        if (!tree.isExpanded(path)) {
261            tree.expandPath(path);
262        }
263        tree.setSelectionPath(path);
264    }
265
266    /**
267     * Generates a String with all matching XPath queries separated
268     * by newlines.
269     *
270     * @param nodes deque of nodes to generate queries for
271     * @return complete text of all XPath expressions separated by newlines.
272     */
273    private static String getAllMatchingXpathQueriesText(Deque<DetailAST> nodes) {
274        return nodes.stream()
275                .map(XpathQueryGenerator::generateXpathQuery)
276                .collect(Collectors.joining(NEWLINE, "", NEWLINE));
277    }
278
279    /**
280     * Overridden to message super and forward the method to the tree.
281     * Since the tree is not actually in the component hierarchy it will
282     * never receive this unless we forward it in this manner.
283     */
284    @Override
285    public void updateUI() {
286        super.updateUI();
287        if (tree != null) {
288            tree.updateUI();
289        }
290        // Use the tree's default foreground and background colors in the
291        // table.
292        LookAndFeel.installColorsAndFont(this, "Tree.background",
293                "Tree.foreground", "Tree.font");
294    }
295
296    /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
297     * paint the editor. The UI currently uses different techniques to
298     * paint the renderers and editors and overriding setBounds() below
299     * is not the right thing to do for an editor. Returning -1 for the
300     * editing row in this case, ensures the editor is never painted.
301     */
302    @Override
303    public int getEditingRow() {
304        int rowIndex = -1;
305        final Class<?> editingClass = getColumnClass(editingColumn);
306        if (editingClass != ParseTreeTableModel.class) {
307            rowIndex = editingRow;
308        }
309        return rowIndex;
310    }
311
312    /**
313     * Overridden to pass the new rowHeight to the tree.
314     */
315    @Override
316    public void setRowHeight(int newRowHeight) {
317        super.setRowHeight(newRowHeight);
318        if (tree != null && tree.getRowHeight() != newRowHeight) {
319            tree.setRowHeight(getRowHeight());
320        }
321    }
322
323    /**
324     * Returns tree.
325     *
326     * @return the tree that is being shared between the model.
327     */
328    public JTree getTree() {
329        return tree;
330    }
331
332    /**
333     * Sets text area editor.
334     *
335     * @param textArea JTextArea component.
336     */
337    public void setEditor(JTextArea textArea) {
338        editor = textArea;
339    }
340
341    /**
342     * Sets text area xpathEditor.
343     *
344     * @param xpathTextArea JTextArea component.
345     */
346    public void setXpathEditor(JTextArea xpathTextArea) {
347        xpathEditor = xpathTextArea;
348    }
349
350    /**
351     * Sets line positions.
352     *
353     * @param linePositionList positions of lines.
354     */
355    public void setLinePositionList(Collection<Integer> linePositionList) {
356        this.linePositionList = new ArrayList<>(linePositionList);
357    }
358
359    /**
360     * TreeTableCellEditor implementation. Component returned is the
361     * JTree.
362     */
363    private class TreeTableCellEditor extends BaseCellEditor implements
364            TableCellEditor {
365
366        @Override
367        public Component getTableCellEditorComponent(JTable table,
368                Object value,
369                boolean isSelected,
370                int row, int column) {
371            return tree;
372        }
373
374        /**
375         * Overridden to return false, and if the event is a mouse event
376         * it is forwarded to the tree.
377         *
378         * <p>The behavior for this is debatable, and should really be offered
379         * as a property. By returning false, all keyboard actions are
380         * implemented in terms of the table. By returning true, the
381         * tree would get a chance to do something with the keyboard
382         * events. For the most part this is ok. But for certain keys,
383         * such as left/right, the tree will expand/collapse where as
384         * the table focus should really move to a different column. Page
385         * up/down should also be implemented in terms of the table.
386         * By returning false this also has the added benefit that clicking
387         * outside of the bounds of the tree node, but still in the tree
388         * column will select the row, whereas if this returned true
389         * that wouldn't be the case.
390         *
391         * <p>By returning false we are also enforcing the policy that
392         * the tree will never be editable (at least by a key sequence).
393         *
394         * @see TableCellEditor
395         */
396        @Override
397        public boolean isCellEditable(EventObject event) {
398            if (event instanceof MouseEvent) {
399                for (int counter = getColumnCount() - 1; counter >= 0;
400                     counter--) {
401                    if (getColumnClass(counter) == ParseTreeTableModel.class) {
402                        final MouseEvent mouseEvent = (MouseEvent) event;
403                        final MouseEvent newMouseEvent = new MouseEvent(tree, mouseEvent.getID(),
404                                mouseEvent.getWhen(), mouseEvent.getModifiersEx(),
405                                mouseEvent.getX() - getCellRect(0, counter, true).x,
406                                mouseEvent.getY(), mouseEvent.getClickCount(),
407                                mouseEvent.isPopupTrigger());
408                        tree.dispatchEvent(newMouseEvent);
409                        break;
410                    }
411                }
412            }
413
414            return false;
415        }
416
417    }
418
419}