001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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.Deque; 031import java.util.EventObject; 032import java.util.List; 033 034import javax.swing.AbstractAction; 035import javax.swing.Action; 036import javax.swing.JTable; 037import javax.swing.JTextArea; 038import javax.swing.JTree; 039import javax.swing.KeyStroke; 040import javax.swing.LookAndFeel; 041import javax.swing.table.TableCellEditor; 042import javax.swing.tree.TreePath; 043 044import com.puppycrawl.tools.checkstyle.api.DetailAST; 045import com.puppycrawl.tools.checkstyle.xpath.XpathQueryGenerator; 046 047/** 048 * This example shows how to create a simple TreeTable component, 049 * by using a JTree as a renderer (and editor) for the cells in a 050 * particular column in the JTable. 051 * 052 * <a href= 053 * "https://docs.oracle.com/cd/E48246_01/apirefs.1111/e13403/oracle/ide/controls/TreeTableModel.html"> 054 * Original Source Location</a> 055 * 056 * @noinspection ThisEscapedInObjectConstruction 057 */ 058public final class TreeTable extends JTable { 059 060 private static final long serialVersionUID = -8493693409423365387L; 061 /** A subclass of JTree. */ 062 private final TreeTableCellRenderer tree; 063 /** JTextArea editor. */ 064 private JTextArea editor; 065 /** JTextArea xpathEditor. */ 066 private JTextArea xpathEditor; 067 /** Line position map. */ 068 private List<Integer> linePositionMap; 069 070 /** 071 * Creates TreeTable base on TreeTableModel. 072 * 073 * @param treeTableModel Tree table model 074 */ 075 public TreeTable(ParseTreeTableModel treeTableModel) { 076 // Create the tree. It will be used as a renderer and editor. 077 tree = new TreeTableCellRenderer(this, treeTableModel); 078 079 // Install a tableModel representing the visible rows in the tree. 080 setModel(new TreeTableModelAdapter(treeTableModel, tree)); 081 082 // Force the JTable and JTree to share their row selection models. 083 final ListToTreeSelectionModelWrapper selectionWrapper = new 084 ListToTreeSelectionModelWrapper(this); 085 tree.setSelectionModel(selectionWrapper); 086 setSelectionModel(selectionWrapper.getListSelectionModel()); 087 088 // Install the tree editor renderer and editor. 089 setDefaultRenderer(ParseTreeTableModel.class, tree); 090 setDefaultEditor(ParseTreeTableModel.class, new TreeTableCellEditor()); 091 092 // No grid. 093 setShowGrid(false); 094 095 // No intercell spacing 096 setIntercellSpacing(new Dimension(0, 0)); 097 098 // And update the height of the trees row to match that of 099 // the table. 100 if (tree.getRowHeight() < 1) { 101 // Metal looks better like this. 102 setRowHeight(getRowHeight()); 103 } 104 105 setColumnsInitialWidth(); 106 107 final Action expand = new AbstractAction() { 108 private static final long serialVersionUID = -5859674518660156121L; 109 110 @Override 111 public void actionPerformed(ActionEvent event) { 112 expandSelectedNode(); 113 } 114 }; 115 final KeyStroke stroke = KeyStroke.getKeyStroke("ENTER"); 116 final String command = "expand/collapse"; 117 getInputMap().put(stroke, command); 118 getActionMap().put(command, expand); 119 120 addMouseListener(new MouseAdapter() { 121 @Override 122 public void mouseClicked(MouseEvent event) { 123 if (event.getClickCount() == 2) { 124 expandSelectedNode(); 125 } 126 } 127 }); 128 } 129 130 /** 131 * Do expansion of a tree node. 132 */ 133 private void expandSelectedNode() { 134 final TreePath selected = tree.getSelectionPath(); 135 makeCodeSelection(); 136 generateXpath(); 137 138 if (tree.isExpanded(selected)) { 139 tree.collapsePath(selected); 140 } 141 else { 142 tree.expandPath(selected); 143 } 144 tree.setSelectionPath(selected); 145 } 146 147 /** 148 * Make selection of code in a text area. 149 */ 150 private void makeCodeSelection() { 151 new CodeSelector(tree.getLastSelectedPathComponent(), editor, linePositionMap).select(); 152 } 153 154 /** 155 * Generate Xpath. 156 */ 157 private void generateXpath() { 158 if (tree.getLastSelectedPathComponent() instanceof DetailAST) { 159 final DetailAST ast = (DetailAST) tree.getLastSelectedPathComponent(); 160 final int beginPos = 4; 161 String xpath = XpathQueryGenerator.generateXpathQuery(ast); 162 final int length = xpath.length(); 163 xpath = xpath.substring(beginPos, length); 164 xpathEditor.setText(xpath); 165 } 166 else { 167 xpathEditor.setText("Xpath is not supported yet for javadoc nodes"); 168 } 169 } 170 171 /** 172 * Set initial value of width for columns in table. 173 */ 174 private void setColumnsInitialWidth() { 175 final FontMetrics fontMetrics = getFontMetrics(getFont()); 176 // Six character string to contain "Column" column. 177 final int widthOfSixCharacterString = fontMetrics.stringWidth("XXXXXX"); 178 // Padding must be added to width for columns to make them fully 179 // visible in table header. 180 final int padding = 10; 181 final int widthOfColumnContainingSixCharacterString = 182 widthOfSixCharacterString + padding; 183 getColumn("Line").setMaxWidth(widthOfColumnContainingSixCharacterString); 184 getColumn("Column").setMaxWidth(widthOfColumnContainingSixCharacterString); 185 final int preferredTreeColumnWidth = 186 Math.toIntExact(Math.round(getPreferredSize().getWidth() * 0.6)); 187 getColumn("Tree").setPreferredWidth(preferredTreeColumnWidth); 188 // Twenty eight character string to contain "Type" column 189 final int widthOfTwentyEightCharacterString = 190 fontMetrics.stringWidth("XXXXXXXXXXXXXXXXXXXXXXXXXXXX"); 191 final int preferredTypeColumnWidth = widthOfTwentyEightCharacterString + padding; 192 getColumn("Type").setPreferredWidth(preferredTypeColumnWidth); 193 } 194 195 /** 196 * Search node by Xpath. 197 * 198 * @param root {@code DetailAST} root ast element 199 * @param xpath {@code String} xpath query 200 * @param nodes {@code Deque<DetailAST>} stack of nodes in selection path 201 * @return {@code boolean} node found or not 202 */ 203 private static boolean search(DetailAST root, String xpath, Deque<DetailAST> nodes) { 204 boolean result = false; 205 if (xpath.equals(XpathQueryGenerator.generateXpathQuery(root))) { 206 nodes.push(root); 207 result = true; 208 } 209 else { 210 DetailAST child = root.getFirstChild(); 211 while (child != null) { 212 if (search(child, xpath, nodes)) { 213 nodes.push(root); 214 result = true; 215 break; 216 } 217 child = child.getNextSibling(); 218 } 219 } 220 return result; 221 } 222 223 /** 224 * Select Node by Xpath. 225 */ 226 public void selectNodeByXpath() { 227 final DetailAST rootAST = (DetailAST) tree.getModel().getRoot(); 228 if (rootAST.hasChildren()) { 229 final String xpath = "/EOF" + xpathEditor.getText(); 230 final Deque<DetailAST> nodes = new ArrayDeque<>(); 231 if (search(rootAST, xpath, nodes)) { 232 TreePath path = new TreePath(nodes.pop()); 233 while (!nodes.isEmpty()) { 234 path = path.pathByAddingChild(nodes.pop()); 235 if (!tree.isExpanded(path)) { 236 tree.expandPath(path); 237 } 238 tree.setSelectionPath(path); 239 makeCodeSelection(); 240 } 241 } 242 else { 243 xpathEditor.setText(xpathEditor.getText() + "\n^ wrong xpath query"); 244 } 245 } 246 else { 247 xpathEditor.setText("No file Opened"); 248 } 249 } 250 251 /** 252 * Overridden to message super and forward the method to the tree. 253 * Since the tree is not actually in the component hierarchy it will 254 * never receive this unless we forward it in this manner. 255 */ 256 @Override 257 public void updateUI() { 258 super.updateUI(); 259 if (tree != null) { 260 tree.updateUI(); 261 } 262 // Use the tree's default foreground and background colors in the 263 // table. 264 LookAndFeel.installColorsAndFont(this, "Tree.background", 265 "Tree.foreground", "Tree.font"); 266 } 267 268 /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to 269 * paint the editor. The UI currently uses different techniques to 270 * paint the renderers and editors and overriding setBounds() below 271 * is not the right thing to do for an editor. Returning -1 for the 272 * editing row in this case, ensures the editor is never painted. 273 */ 274 @Override 275 public int getEditingRow() { 276 int rowIndex = -1; 277 final Class<?> editingClass = getColumnClass(editingColumn); 278 if (editingClass != ParseTreeTableModel.class) { 279 rowIndex = editingRow; 280 } 281 return rowIndex; 282 } 283 284 /** 285 * Overridden to pass the new rowHeight to the tree. 286 */ 287 @Override 288 public void setRowHeight(int newRowHeight) { 289 super.setRowHeight(newRowHeight); 290 if (tree != null && tree.getRowHeight() != newRowHeight) { 291 tree.setRowHeight(getRowHeight()); 292 } 293 } 294 295 /** 296 * Returns tree. 297 * 298 * @return the tree that is being shared between the model. 299 */ 300 public JTree getTree() { 301 return tree; 302 } 303 304 /** 305 * Sets text area editor. 306 * 307 * @param textArea JTextArea component. 308 */ 309 public void setEditor(JTextArea textArea) { 310 editor = textArea; 311 } 312 313 /** 314 * Sets text area xpathEditor. 315 * 316 * @param xpathTextArea JTextArea component. 317 */ 318 public void setXpathEditor(JTextArea xpathTextArea) { 319 xpathEditor = xpathTextArea; 320 } 321 322 /** 323 * Sets line position map. 324 * 325 * @param linePositionMap Line position map. 326 */ 327 public void setLinePositionMap(List<Integer> linePositionMap) { 328 this.linePositionMap = new ArrayList<>(linePositionMap); 329 } 330 331 /** 332 * TreeTableCellEditor implementation. Component returned is the 333 * JTree. 334 */ 335 private class TreeTableCellEditor extends BaseCellEditor implements 336 TableCellEditor { 337 338 @Override 339 public Component getTableCellEditorComponent(JTable table, 340 Object value, 341 boolean isSelected, 342 int row, int column) { 343 return tree; 344 } 345 346 /** 347 * Overridden to return false, and if the event is a mouse event 348 * it is forwarded to the tree. 349 * 350 * <p>The behavior for this is debatable, and should really be offered 351 * as a property. By returning false, all keyboard actions are 352 * implemented in terms of the table. By returning true, the 353 * tree would get a chance to do something with the keyboard 354 * events. For the most part this is ok. But for certain keys, 355 * such as left/right, the tree will expand/collapse where as 356 * the table focus should really move to a different column. Page 357 * up/down should also be implemented in terms of the table. 358 * By returning false this also has the added benefit that clicking 359 * outside of the bounds of the tree node, but still in the tree 360 * column will select the row, whereas if this returned true 361 * that wouldn't be the case. 362 * 363 * <p>By returning false we are also enforcing the policy that 364 * the tree will never be editable (at least by a key sequence). 365 * 366 * @see TableCellEditor 367 */ 368 @Override 369 public boolean isCellEditable(EventObject event) { 370 if (event instanceof MouseEvent) { 371 for (int counter = getColumnCount() - 1; counter >= 0; 372 counter--) { 373 if (getColumnClass(counter) == ParseTreeTableModel.class) { 374 final MouseEvent mouseEvent = (MouseEvent) event; 375 final MouseEvent newMouseEvent = new MouseEvent(tree, mouseEvent.getID(), 376 mouseEvent.getWhen(), mouseEvent.getModifiersEx(), 377 mouseEvent.getX() - getCellRect(0, counter, true).x, 378 mouseEvent.getY(), mouseEvent.getClickCount(), 379 mouseEvent.isPopupTrigger()); 380 tree.dispatchEvent(newMouseEvent); 381 break; 382 } 383 } 384 } 385 386 return false; 387 } 388 389 } 390 391}