| /* |
| * Copyright 2000-2012 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ui; |
| |
| import com.intellij.ui.treeStructure.Tree; |
| import com.intellij.util.ui.UIUtil; |
| import com.intellij.util.ui.tree.TreeUtil; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.tree.*; |
| import java.awt.*; |
| import java.awt.event.KeyAdapter; |
| import java.awt.event.KeyEvent; |
| import java.awt.event.MouseEvent; |
| import java.lang.reflect.Array; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| |
| public class CheckboxTreeBase extends Tree { |
| private final CheckPolicy myCheckPolicy; |
| private static final CheckPolicy DEFAULT_POLICY = new CheckPolicy(true, true, false, true); |
| |
| public CheckboxTreeBase() { |
| this(new CheckboxTreeCellRendererBase(), null); |
| } |
| |
| public CheckboxTreeBase(final CheckboxTreeCellRendererBase cellRenderer, CheckedTreeNode root) { |
| this(cellRenderer, root, DEFAULT_POLICY); |
| } |
| |
| public CheckboxTreeBase(CheckboxTreeCellRendererBase cellRenderer, @Nullable CheckedTreeNode root, CheckPolicy checkPolicy) { |
| myCheckPolicy = checkPolicy; |
| |
| setRootVisible(false); |
| setShowsRootHandles(true); |
| setLineStyleAngled(); |
| TreeUtil.installActions(this); |
| |
| installRenderer(cellRenderer); |
| |
| addKeyListener(new KeyAdapter() { |
| public void keyPressed(KeyEvent e) { |
| if (isToggleEvent(e)) { |
| TreePath treePath = getLeadSelectionPath(); |
| if (treePath == null) return; |
| final Object o = treePath.getLastPathComponent(); |
| if (!(o instanceof CheckedTreeNode)) return; |
| CheckedTreeNode firstNode = (CheckedTreeNode)o; |
| boolean checked = toggleNode(firstNode); |
| |
| TreePath[] selectionPaths = getSelectionPaths(); |
| for (int i = 0; selectionPaths != null && i < selectionPaths.length; i++) { |
| final TreePath selectionPath = selectionPaths[i]; |
| final Object o1 = selectionPath.getLastPathComponent(); |
| if (!(o1 instanceof CheckedTreeNode)) continue; |
| CheckedTreeNode node = (CheckedTreeNode)o1; |
| checkNode(node, checked); |
| ((DefaultTreeModel)getModel()).nodeChanged(node); |
| } |
| |
| e.consume(); |
| } |
| } |
| }); |
| |
| setSelectionRow(0); |
| if (root != null) { |
| setModel(new DefaultTreeModel(root)); |
| } |
| } |
| |
| public void installRenderer(final CheckboxTreeCellRendererBase cellRenderer) { |
| setCellRenderer(cellRenderer); |
| new ClickListener() { |
| @Override |
| public boolean onClick(MouseEvent e, int clickCount) { |
| int row = getRowForLocation(e.getX(), e.getY()); |
| if (row < 0) return false; |
| final Object o = getPathForRow(row).getLastPathComponent(); |
| if (!(o instanceof CheckedTreeNode)) return false; |
| Rectangle rowBounds = getRowBounds(row); |
| cellRenderer.setBounds(rowBounds); |
| Rectangle checkBounds = cellRenderer.myCheckbox.getBounds(); |
| checkBounds.setLocation(rowBounds.getLocation()); |
| |
| if (checkBounds.height == 0) checkBounds.height = checkBounds.width = rowBounds.height; |
| |
| final CheckedTreeNode node = (CheckedTreeNode)o; |
| if (checkBounds.contains(e.getPoint())) { |
| if (node.isEnabled()) { |
| toggleNode(node); |
| setSelectionRow(row); |
| return true; |
| } |
| } |
| else if (clickCount > 1) { |
| onDoubleClick(node); |
| return true; |
| } |
| return false; |
| } |
| }.installOn(this); |
| } |
| |
| protected void onDoubleClick(final CheckedTreeNode node) { |
| } |
| |
| protected boolean isToggleEvent(KeyEvent e) { |
| return e.getKeyCode() == KeyEvent.VK_SPACE; |
| } |
| |
| protected boolean toggleNode(CheckedTreeNode node) { |
| boolean checked = !node.isChecked(); |
| checkNode(node, checked); |
| |
| // notify model listeners about model change |
| final TreeModel model = getModel(); |
| model.valueForPathChanged(new TreePath(node.getPath()), node.getUserObject()); |
| |
| return checked; |
| } |
| |
| /** |
| * Collect checked leaf nodes of the type {@code nodeType} and that are accepted by |
| * {@code filter} |
| * |
| * @param nodeType the type of userobject to consider |
| * @param filter the filter (if null all nodes are accepted) |
| * @param <T> the type of the node |
| * @return an array of collected nodes |
| */ |
| @SuppressWarnings("unchecked") |
| public <T> T[] getCheckedNodes(final Class<T> nodeType, @Nullable final NodeFilter<T> filter) { |
| final ArrayList<T> nodes = new ArrayList<T>(); |
| final Object root = getModel().getRoot(); |
| if (!(root instanceof CheckedTreeNode)) { |
| throw new IllegalStateException( |
| "The root must be instance of the " + CheckedTreeNode.class.getName() + ": " + root.getClass().getName()); |
| } |
| new Object() { |
| @SuppressWarnings("unchecked") |
| public void collect(CheckedTreeNode node) { |
| if (node.isLeaf()) { |
| Object userObject = node.getUserObject(); |
| if (node.isChecked() && userObject != null && nodeType.isAssignableFrom(userObject.getClass())) { |
| final T value = (T)userObject; |
| if (filter != null && !filter.accept(value)) return; |
| nodes.add(value); |
| } |
| } |
| else { |
| for (int i = 0; i < node.getChildCount(); i++) { |
| final TreeNode child = node.getChildAt(i); |
| if (child instanceof CheckedTreeNode) { |
| collect((CheckedTreeNode)child); |
| } |
| } |
| } |
| } |
| }.collect((CheckedTreeNode)root); |
| T[] result = (T[])Array.newInstance(nodeType, nodes.size()); |
| nodes.toArray(result); |
| return result; |
| } |
| |
| |
| public int getToggleClickCount() { |
| // to prevent node expanding/collapsing on checkbox toggling |
| return -1; |
| } |
| |
| protected void checkNode(CheckedTreeNode node, boolean checked) { |
| adjustParentsAndChildren(node, checked); |
| repaint(); |
| } |
| |
| protected void onNodeStateChanged(CheckedTreeNode node) { |
| |
| } |
| |
| protected void nodeStateWillChange(CheckedTreeNode node) { |
| |
| } |
| |
| protected void adjustParentsAndChildren(final CheckedTreeNode node, final boolean checked) { |
| changeNodeState(node, checked); |
| if (!checked) { |
| if (myCheckPolicy.uncheckParentWithUncheckedChild) { |
| TreeNode parent = node.getParent(); |
| while (parent != null) { |
| if (parent instanceof CheckedTreeNode) { |
| changeNodeState((CheckedTreeNode)parent, false); |
| } |
| parent = parent.getParent(); |
| } |
| } |
| if (myCheckPolicy.uncheckChildrenWithUncheckedParent) { |
| uncheckChildren(node); |
| } |
| |
| } |
| else { |
| if (myCheckPolicy.checkChildrenWithCheckedParent) { |
| checkChildren(node); |
| } |
| |
| if (myCheckPolicy.checkParentWithCheckedChild) { |
| TreeNode parent = node.getParent(); |
| while (parent != null) { |
| if (parent instanceof CheckedTreeNode) { |
| changeNodeState((CheckedTreeNode)parent, true); |
| } |
| parent = parent.getParent(); |
| } |
| } |
| |
| } |
| repaint(); |
| } |
| |
| private void changeNodeState(final CheckedTreeNode node, final boolean checked) { |
| if (node.isChecked() != checked) { |
| nodeStateWillChange(node); |
| node.setChecked(checked); |
| onNodeStateChanged(node); |
| } |
| } |
| |
| private void uncheckChildren(final CheckedTreeNode node) { |
| final Enumeration children = node.children(); |
| while (children.hasMoreElements()) { |
| final Object o = children.nextElement(); |
| if (!(o instanceof CheckedTreeNode)) continue; |
| CheckedTreeNode child = (CheckedTreeNode)o; |
| changeNodeState(child, false); |
| uncheckChildren(child); |
| } |
| } |
| |
| private void checkChildren(final CheckedTreeNode node) { |
| final Enumeration children = node.children(); |
| while (children.hasMoreElements()) { |
| final Object o = children.nextElement(); |
| if (!(o instanceof CheckedTreeNode)) continue; |
| CheckedTreeNode child = (CheckedTreeNode)o; |
| changeNodeState(child, true); |
| checkChildren(child); |
| } |
| } |
| |
| protected void adjustParents(final CheckedTreeNode node, final boolean checked) { |
| TreeNode parentNode = node.getParent(); |
| if (!(parentNode instanceof CheckedTreeNode)) return; |
| CheckedTreeNode parent = (CheckedTreeNode)parentNode; |
| |
| if (!checked && isAllChildrenUnchecked(parent)) { |
| changeNodeState(parent, false); |
| adjustParents(parent, false); |
| } |
| else if (checked && isAllChildrenChecked(parent)) { |
| changeNodeState(parent, true); |
| adjustParents(parent, true); |
| } |
| } |
| |
| private static boolean isAllChildrenUnchecked(final CheckedTreeNode node) { |
| for (int i = 0; i < node.getChildCount(); i++) { |
| final TreeNode o = node.getChildAt(i); |
| if ((o instanceof CheckedTreeNode) && ((CheckedTreeNode)o).isChecked()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private static boolean isAllChildrenChecked(final CheckedTreeNode node) { |
| for (int i = 0; i < node.getChildCount(); i++) { |
| final TreeNode o = node.getChildAt(i); |
| if ((o instanceof CheckedTreeNode) && !((CheckedTreeNode)o).isChecked()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public static class CheckboxTreeCellRendererBase extends JPanel implements TreeCellRenderer { |
| private final ColoredTreeCellRenderer myTextRenderer; |
| public final JCheckBox myCheckbox; |
| private final boolean myUsePartialStatusForParentNodes; |
| |
| public CheckboxTreeCellRendererBase(boolean opaque) { |
| this(opaque, true); |
| } |
| |
| public CheckboxTreeCellRendererBase(boolean opaque, final boolean usePartialStatusForParentNodes) { |
| super(new BorderLayout()); |
| myUsePartialStatusForParentNodes = usePartialStatusForParentNodes; |
| myCheckbox = new JCheckBox(); |
| myTextRenderer = new ColoredTreeCellRenderer() { |
| public void customizeCellRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { } |
| }; |
| myTextRenderer.setOpaque(opaque); |
| add(myCheckbox, BorderLayout.WEST); |
| add(myTextRenderer, BorderLayout.CENTER); |
| } |
| |
| public CheckboxTreeCellRendererBase() { |
| this(true); |
| } |
| |
| public final Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { |
| invalidate(); |
| if (value instanceof CheckedTreeNode) { |
| CheckedTreeNode node = (CheckedTreeNode)value; |
| |
| NodeState state = getNodeStatus(node); |
| myCheckbox.setVisible(true); |
| myCheckbox.setSelected(state != NodeState.CLEAR); |
| myCheckbox.setEnabled(node.isEnabled() && state != NodeState.PARTIAL); |
| myCheckbox.setOpaque(false); |
| myCheckbox.setBackground(null); |
| setBackground(null); |
| } |
| else { |
| myCheckbox.setVisible(false); |
| } |
| myTextRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); |
| |
| if (UIUtil.isUnderGTKLookAndFeel()) { |
| final Color background = selected ? UIUtil.getTreeSelectionBackground() : UIUtil.getTreeTextBackground(); |
| UIUtil.changeBackGround(this, background); |
| } |
| else if (UIUtil.isUnderNimbusLookAndFeel()) { |
| UIUtil.changeBackGround(this, UIUtil.TRANSPARENT_COLOR); |
| } |
| customizeRenderer(tree, value, selected, expanded, leaf, row, hasFocus); |
| revalidate(); |
| |
| return this; |
| } |
| |
| private NodeState getNodeStatus(final CheckedTreeNode node) { |
| final boolean checked = node.isChecked(); |
| if (node.getChildCount() == 0 || !myUsePartialStatusForParentNodes) return checked ? NodeState.FULL : NodeState.CLEAR; |
| |
| NodeState result = null; |
| |
| for (int i = 0; i < node.getChildCount(); i++) { |
| TreeNode child = node.getChildAt(i); |
| NodeState childStatus = child instanceof CheckedTreeNode? getNodeStatus((CheckedTreeNode)child) : |
| checked? NodeState.FULL : NodeState.CLEAR; |
| if (childStatus == NodeState.PARTIAL) return NodeState.PARTIAL; |
| if (result == null) { |
| result = childStatus; |
| } |
| else if (result != childStatus) { |
| return NodeState.PARTIAL; |
| } |
| } |
| |
| return result == null ? NodeState.CLEAR : result; |
| } |
| |
| /** |
| * Should be implemented by concrete implementations. |
| * This method is invoked only for customization of component. |
| * All component attributes are cleared when this method is being invoked. |
| * Note that in general case <code>value</code> is not an instance of CheckedTreeNode. |
| */ |
| public void customizeRenderer(JTree tree, |
| Object value, |
| boolean selected, |
| boolean expanded, |
| boolean leaf, |
| int row, |
| boolean hasFocus) { |
| if (value instanceof CheckedTreeNode) { |
| customizeCellRenderer(tree, value, selected, expanded, leaf, row, hasFocus); |
| } |
| } |
| |
| /** |
| * @deprecated |
| * @see CheckboxTreeCellRendererBase#customizeRenderer(javax.swing.JTree, Object, boolean, boolean, boolean, int, boolean) |
| */ |
| @Deprecated |
| public void customizeCellRenderer(JTree tree, |
| Object value, |
| boolean selected, |
| boolean expanded, |
| boolean leaf, |
| int row, |
| boolean hasFocus) { |
| } |
| |
| public ColoredTreeCellRenderer getTextRenderer() { |
| return myTextRenderer; |
| } |
| |
| public JCheckBox getCheckbox() { |
| return myCheckbox; |
| } |
| } |
| |
| |
| public enum NodeState { |
| FULL, CLEAR, PARTIAL |
| } |
| |
| public static class CheckPolicy { |
| final boolean checkChildrenWithCheckedParent; |
| final boolean uncheckChildrenWithUncheckedParent; |
| final boolean checkParentWithCheckedChild; |
| final boolean uncheckParentWithUncheckedChild; |
| |
| public CheckPolicy(final boolean checkChildrenWithCheckedParent, |
| final boolean uncheckChildrenWithUncheckedParent, |
| final boolean checkParentWithCheckedChild, |
| final boolean uncheckParentWithUncheckedChild) { |
| this.checkChildrenWithCheckedParent = checkChildrenWithCheckedParent; |
| this.uncheckChildrenWithUncheckedParent = uncheckChildrenWithUncheckedParent; |
| this.checkParentWithCheckedChild = checkParentWithCheckedChild; |
| this.uncheckParentWithUncheckedChild = uncheckParentWithUncheckedChild; |
| } |
| } |
| } |