diff --git a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusExtensionsStep.java b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusExtensionsStep.java index f0ab6da47..60677a117 100644 --- a/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusExtensionsStep.java +++ b/src/main/java/com/redhat/devtools/intellij/quarkus/module/QuarkusExtensionsStep.java @@ -39,6 +39,7 @@ import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import java.awt.*; +import java.awt.event.*; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; @@ -69,6 +70,21 @@ public void customizeRenderer(JTree tree, Object value, boolean selected, boolea } } + private static class SelectedExtensionsCellRenderer extends ColoredListCellRenderer { + + @Override + protected void customizeCellRenderer(@NotNull JList list, QuarkusExtension extension, int index, boolean selected, boolean hasFocus) { + append(extension.getName()); + } + + @Override + public Component getListCellRendererComponent(JList list, QuarkusExtension value, int index, boolean selected, boolean hasFocus) { + super.getListCellRendererComponent(list, value, index, selected, hasFocus); + setAlignmentX(Component.LEFT_ALIGNMENT); + return this; + } + } + private static class ExtensionsTree extends CheckboxTree { public ExtensionsTree(CheckedTreeNode root) { @@ -155,20 +171,7 @@ public Dimension getMaximumSize() { selectedExtensions.setBackground(null); selectedExtensions.setAlignmentX(Component.LEFT_ALIGNMENT); selectedExtensions.setModel(new SelectedExtensionsModel(categories)); - ColoredListCellRenderer selectedExtensionRenderer = new ColoredListCellRenderer() { - @Override - protected void customizeCellRenderer(@NotNull JList list, QuarkusExtension extension, int index, boolean selected, boolean hasFocus) { - append(extension.getName()); - } - - @Override - public Component getListCellRendererComponent(JList list, QuarkusExtension value, int index, boolean selected, boolean hasFocus) { - super.getListCellRendererComponent(list, value, index, selected, hasFocus); - setAlignmentX(Component.LEFT_ALIGNMENT); - return this; - } - }; - selectedExtensions.setCellRenderer(selectedExtensionRenderer); + selectedExtensions.setCellRenderer(new SelectedExtensionsCellRenderer()); JPanel selectedExtensionsPanel = new JPanel(); selectedExtensionsPanel.setLayout(new BoxLayout(selectedExtensionsPanel, BoxLayout.Y_AXIS)); @@ -181,54 +184,31 @@ public Component getListCellRendererComponent(JList extensionsPanel.setSecondComponent(new JBScrollPane(selectedExtensionsPanel)); panel.add(extensionsPanel); - filter.addDocumentListener(new DocumentAdapter() { - @Override - protected void textChanged(@NotNull DocumentEvent e) { - ApplicationManager.getApplication().invokeLater(() -> { - extensionsTree.setModel(new DefaultTreeModel(getModel(categories, filter, platformCheckbox.isSelected()))); - expandTree(extensionsTree); - }); - } - }); - platformCheckbox.addItemListener(e -> { - ApplicationManager.getApplication().invokeLater(() -> { - extensionsTree.setModel(new DefaultTreeModel(getModel(categories, filter, platformCheckbox.isSelected()))); - expandTree(extensionsTree); - }); - }); - extensionDetailTextPane.addHyperlinkListener(new HyperlinkListener() { - @Override - public void hyperlinkUpdate(HyperlinkEvent e) { - if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - BrowserUtil.browse(e.getURL()); - } - } - }); - extensionsTree.addCheckboxTreeListener(new CheckboxTreeListener() { - @Override - public void nodeStateChanged(@NotNull CheckedTreeNode node) { - QuarkusExtension extension = (QuarkusExtension) node.getUserObject(); - if (extension == null) { - // Since ExtensionsTree doesn't extend CheckboxTreeBase directly, - // you can't customize its CheckboxTreeBase.CheckPolicy, - // so CheckboxTreeHelper.adjustParentsAndChildren basically calls nodeStateChanged(node.getParent()) - // which doesn't hold a QuarkusExtension and leads to https://github.com/redhat-developer/intellij-quarkus/issues/639 - // So we bail here. - return; - } - extension.setSelected(node.isChecked()); - selectedExtensions.setModel(new SelectedExtensionsModel(categories)); - } - }); + filter.addDocumentListener(onDocumentChanged(filter, platformCheckbox, categories, extensionsTree)); + platformCheckbox.addItemListener(onItemChanged(filter, platformCheckbox, categories, extensionsTree)); + extensionDetailTextPane.addHyperlinkListener(onHyperlinkClicked()); + + extensionsTree.addCheckboxTreeListener(onNodeCheckedStateChanged(categories, selectedExtensions)); + + //(Un)Check extension on double-click + extensionsTree.addMouseListener(onAvailableExtensionClicked(extensionsTree)); + + //Unselect extensions on double-click + selectedExtensions.addMouseListener(onSelectedExtensionClicked(categories, extensionsTree, selectedExtensions)); + + //Unselect extensions when pressing the DELETE or BACKSPACE key + selectedExtensions.addKeyListener(onSelectedExtensionsKeyPressed(categories, extensionsTree, selectedExtensions)); + extensionsTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { if (e.getNewLeadSelectionPath() != null) { Object comp = ((DefaultMutableTreeNode) e.getNewLeadSelectionPath().getLastPathComponent()).getUserObject(); if (comp instanceof QuarkusExtension) { - StringBuilder builder = new StringBuilder("" + ((QuarkusExtension) comp).getDescription() + "."); - if (StringUtils.isNotBlank(((QuarkusExtension) comp).getGuide())) { - builder.append(" Click to open guide"); + QuarkusExtension extension = (QuarkusExtension) comp; + StringBuilder builder = new StringBuilder("").append(extension.getDescription()).append("."); + if (StringUtils.isNotBlank(extension.getGuide())) { + builder.append(" Click to open guide"); } builder.append(""); extensionDetailTextPane.setText(builder.toString()); @@ -243,6 +223,117 @@ public void valueChanged(TreeSelectionEvent e) { return outerPanel; } + private KeyListener onSelectedExtensionsKeyPressed(List categories, CheckboxTree extensionsTree, JList selectedExtensions) { + return new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_DELETE || e.getKeyCode() == KeyEvent.VK_BACK_SPACE) { + boolean requiresModelRefresh = false; + for (QuarkusExtension extension : selectedExtensions.getSelectedValuesList()) { + requiresModelRefresh = unselectExtension(extensionsTree, extension) || requiresModelRefresh; + } + selectedExtensions.clearSelection(); + if (requiresModelRefresh) { + // Some extensions were not visible in the tree so didn't trigger a selectedExtension model refresh + // so we force it manually + selectedExtensions.setModel(new SelectedExtensionsModel(categories)); + } + } + } + }; + } + + @NotNull + private MouseAdapter onSelectedExtensionClicked(List categories, CheckboxTree extensionsTree, JBList selectedExtensions) { + return new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int selectedIndex = selectedExtensions.getSelectedIndex(); + if (selectedIndex > -1) { + QuarkusExtension extension = selectedExtensions.getModel().getElementAt(selectedIndex); + if (unselectExtension(extensionsTree, extension)) { + // The extensions was not visible in the tree so didn't trigger a selectedExtension model refresh + // so we force it manually + selectedExtensions.setModel(new SelectedExtensionsModel(categories)); + } + ; + } + } + } + }; + } + + @NotNull + private static MouseAdapter onAvailableExtensionClicked(CheckboxTree extensionsTree) { + return new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + TreePath path = extensionsTree.getPathForLocation(e.getX(), e.getY()); + if (path != null && path.getLastPathComponent() instanceof CheckedTreeNode) { + var treeNode = (CheckedTreeNode) path.getLastPathComponent(); + extensionsTree.setNodeState(treeNode, !treeNode.isChecked()); + } + } + } + }; + } + + @NotNull + private static CheckboxTreeListener onNodeCheckedStateChanged(List categories, JBList selectedExtensions) { + return new CheckboxTreeListener() { + @Override + public void nodeStateChanged(@NotNull CheckedTreeNode node) { + QuarkusExtension extension = (QuarkusExtension) node.getUserObject(); + if (extension == null) { + // Since ExtensionsTree doesn't extend CheckboxTreeBase directly, + // you can't customize its CheckboxTreeBase.CheckPolicy, + // so CheckboxTreeHelper.adjustParentsAndChildren basically calls nodeStateChanged(node.getParent()) + // which doesn't hold a QuarkusExtension and leads to https://github.com/redhat-developer/intellij-quarkus/issues/639 + // So we bail here. + return; + } + extension.setSelected(node.isChecked()); + selectedExtensions.setModel(new SelectedExtensionsModel(categories)); + } + }; + } + + @NotNull + private static HyperlinkListener onHyperlinkClicked() { + return new HyperlinkListener() { + @Override + public void hyperlinkUpdate(HyperlinkEvent e) { + if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + BrowserUtil.browse(e.getURL()); + } + } + }; + } + + @NotNull + private ItemListener onItemChanged(SearchTextField filter, JCheckBox platformCheckbox, List categories, CheckboxTree extensionsTree) { + return e -> { + ApplicationManager.getApplication().invokeLater(() -> { + extensionsTree.setModel(new DefaultTreeModel(getModel(categories, filter, platformCheckbox.isSelected()))); + expandTree(extensionsTree); + }); + }; + } + + @NotNull + private DocumentAdapter onDocumentChanged(SearchTextField filter, JCheckBox platformCheckbox, List categories, CheckboxTree extensionsTree) { + return new DocumentAdapter() { + @Override + protected void textChanged(@NotNull DocumentEvent e) { + ApplicationManager.getApplication().invokeLater(() -> { + extensionsTree.setModel(new DefaultTreeModel(getModel(categories, filter, platformCheckbox.isSelected()))); + expandTree(extensionsTree); + }); + } + }; + } + private void expandTree(JTree tree) { TreeNode root = (TreeNode) tree.getModel().getRoot(); TreePath rootPath = new TreePath(root); @@ -274,6 +365,34 @@ private CheckedTreeNode getModel(List categories, SearchTextFie return root; } + /** + * Unselects a selected extension from the extension tree. Returns true if the extension was not found in the tree, false otherwise. + */ + private boolean unselectExtension(@NotNull CheckboxTree extensionsTree, @NotNull QuarkusExtension extension) { + var treeNodes = findTreeNodesForExtension(extensionsTree, extension); + for (var treeNode : treeNodes) { + extensionsTree.setNodeState(treeNode, false); + } + extension.setSelected(false); + return treeNodes.isEmpty(); + } + + /** + * Find CheckedTreeNode for a given extension, as it can belong to several categories + */ + private @NotNull Set findTreeNodesForExtension(@NotNull CheckboxTree extensionsTree, @NotNull QuarkusExtension extension) { + DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) extensionsTree.getModel().getRoot(); + Enumeration enumeration = rootNode.depthFirstEnumeration(); + Set nodes = new HashSet<>(); + while (enumeration.hasMoreElements()) { + TreeNode node = enumeration.nextElement(); + if (node instanceof CheckedTreeNode && ((CheckedTreeNode)node).getUserObject() == extension) { + nodes.add( (CheckedTreeNode)node); + } + } + return nodes; + } + /** * Use reflection to get IntelliJ specific HTML editor kit as it has moved in 2020.1 *