Skip to content

Commit

Permalink
feat: improve extension (de)selection in Quarkus wizard (#989)
Browse files Browse the repository at this point in the history
- (de)select extension on double-click in extensions treeview
- deselect extension on double-click in selected extensions list
- deselect selection of extension on DELETE or BACKSPACE keypress in selected extensions list


Signed-off-by: Fred Bricon <[email protected]>
Signed-off-by: Andre Dietisheim <[email protected]>
Co-authored-by: Andre Dietisheim <[email protected]>
  • Loading branch information
fbricon and adietish authored Jul 3, 2023
1 parent 4c4e571 commit 1c9f7f7
Showing 1 changed file with 175 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,21 @@ public void customizeRenderer(JTree tree, Object value, boolean selected, boolea
}
}

private static class SelectedExtensionsCellRenderer extends ColoredListCellRenderer<QuarkusExtension> {

@Override
protected void customizeCellRenderer(@NotNull JList<? extends QuarkusExtension> list, QuarkusExtension extension, int index, boolean selected, boolean hasFocus) {
append(extension.getName());
}

@Override
public Component getListCellRendererComponent(JList<? extends QuarkusExtension> 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) {
Expand Down Expand Up @@ -155,20 +171,7 @@ public Dimension getMaximumSize() {
selectedExtensions.setBackground(null);
selectedExtensions.setAlignmentX(Component.LEFT_ALIGNMENT);
selectedExtensions.setModel(new SelectedExtensionsModel(categories));
ColoredListCellRenderer<QuarkusExtension> selectedExtensionRenderer = new ColoredListCellRenderer<QuarkusExtension>() {
@Override
protected void customizeCellRenderer(@NotNull JList<? extends QuarkusExtension> list, QuarkusExtension extension, int index, boolean selected, boolean hasFocus) {
append(extension.getName());
}

@Override
public Component getListCellRendererComponent(JList<? extends QuarkusExtension> 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));
Expand All @@ -181,54 +184,31 @@ public Component getListCellRendererComponent(JList<? extends QuarkusExtension>
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("<html><body>" + ((QuarkusExtension) comp).getDescription() + ".");
if (StringUtils.isNotBlank(((QuarkusExtension) comp).getGuide())) {
builder.append(" <a href=\"" + ((QuarkusExtension) comp).getGuide() + "\">Click to open guide</a>");
QuarkusExtension extension = (QuarkusExtension) comp;
StringBuilder builder = new StringBuilder("<html><body>").append(extension.getDescription()).append(".");
if (StringUtils.isNotBlank(extension.getGuide())) {
builder.append(" <a href=\"").append(extension.getGuide()).append("\">Click to open guide</a>");
}
builder.append("</body></html>");
extensionDetailTextPane.setText(builder.toString());
Expand All @@ -243,6 +223,117 @@ public void valueChanged(TreeSelectionEvent e) {
return outerPanel;
}

private KeyListener onSelectedExtensionsKeyPressed(List<QuarkusCategory> categories, CheckboxTree extensionsTree, JList<QuarkusExtension> 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<QuarkusCategory> categories, CheckboxTree extensionsTree, JBList<QuarkusExtension> 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<QuarkusCategory> categories, JBList<QuarkusExtension> 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<QuarkusCategory> 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<QuarkusCategory> 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);
Expand Down Expand Up @@ -274,6 +365,34 @@ private CheckedTreeNode getModel(List<QuarkusCategory> 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<CheckedTreeNode> findTreeNodesForExtension(@NotNull CheckboxTree extensionsTree, @NotNull QuarkusExtension extension) {
DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode) extensionsTree.getModel().getRoot();
Enumeration<TreeNode> enumeration = rootNode.depthFirstEnumeration();
Set<CheckedTreeNode> 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
*
Expand Down

0 comments on commit 1c9f7f7

Please sign in to comment.