diff --git a/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeCellRenderer.java b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeCellRenderer.java new file mode 100644 index 0000000..9e11063 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeCellRenderer.java @@ -0,0 +1,91 @@ +package net.vhati.modmanager.ui.tree; + +import java.awt.BorderLayout; +import java.awt.Component; +import javax.swing.JPanel; +import javax.swing.JTree; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreePath; + +import net.vhati.modmanager.ui.tree.ChecklistTreePathFilter; +import net.vhati.modmanager.ui.tree.ChecklistTreeSelectionModel; +import net.vhati.modmanager.ui.tree.TristateCheckBox; +import net.vhati.modmanager.ui.tree.TristateButtonModel.TristateState; + + +/** + * A cell renderer that augments an existing renderer with a checkbox. + */ +public class ChecklistTreeCellRenderer extends JPanel implements TreeCellRenderer { + + protected ChecklistTreeSelectionModel selectionModel; + protected ChecklistTreePathFilter checklistFilter; + protected TreeCellRenderer delegate; + protected TristateCheckBox checkbox = new TristateCheckBox(); + protected int checkMaxX = 0; + + + /** + * Constructor. + * + * @param delegate a traditional TreeCellRenderer + * @param selectionModel a model to query for checkbox states + * @param checklistFilter a TreePath filter, or null to always show a checkbox + */ + public ChecklistTreeCellRenderer( TreeCellRenderer delegate, ChecklistTreeSelectionModel selectionModel, ChecklistTreePathFilter checklistFilter ) { + super(); + this.delegate = delegate; + this.selectionModel = selectionModel; + this.checklistFilter = checklistFilter; + + this.setLayout( new BorderLayout() ); + this.setOpaque( false ); + checkbox.setOpaque( false ); + + checkMaxX = checkbox.getPreferredSize().width; + } + + + @Override + public Component getTreeCellRendererComponent( JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus ) { + this.removeAll(); + checkbox.setState( TristateState.DESELECTED ); + + Component delegateComp = delegate.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus ); + + TreePath path = tree.getPathForRow( row ); + if ( path != null ) { + if ( selectionModel.isPathSelected( path, selectionModel.isDigged() ) ) { + checkbox.setState( TristateState.SELECTED ); + } else { + checkbox.setState( ( selectionModel.isDigged() && selectionModel.isPartiallySelected( path ) ) ? TristateState.INDETERMINATE : TristateState.DESELECTED ); + } + } + checkbox.setVisible( path == null || checklistFilter == null || checklistFilter.isSelectable( path ) ); + checkbox.setEnabled( tree.isEnabled() ); + + this.add( checkbox, BorderLayout.WEST ); + this.add( delegateComp, BorderLayout.CENTER ); + return this; + } + + + public void setDelegate( TreeCellRenderer delegate ) { + this.delegate = delegate; + } + + public TreeCellRenderer getDelegate() { + return delegate; + } + + + /** + * Returns the checkbox's right edge (in the renderer component's coordinate space). + * + * Values less than that can be interpreted as within the checkbox's bounds. + * X=0 is the renderer component's left edge. + */ + public int getCheckboxMaxX() { + return checkMaxX; + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeManager.java b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeManager.java new file mode 100644 index 0000000..84f83f6 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeManager.java @@ -0,0 +1,129 @@ +/** + * Based on CheckTreeManager (rev 120, 2007-07-20) + * By Santhosh Kumar T + * https://java.net/projects/myswing + * + * https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/check/CheckTreeManager.java?rev=120 + */ + +/** + * MySwing: Advanced Swing Utilites + * Copyright (C) 2005 Santhosh Kumar T + *

+ * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + *

+ * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ + +package net.vhati.modmanager.ui.tree; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JTree; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; + +import net.vhati.modmanager.ui.tree.ChecklistTreePathFilter; +import net.vhati.modmanager.ui.tree.ChecklistTreeSelectionModel; + + +public class ChecklistTreeManager extends MouseAdapter implements TreeSelectionListener { + + private ChecklistTreeSelectionModel selectionModel; + private ChecklistTreePathFilter checklistFilter; + protected JTree tree = new JTree(); + protected int checkMaxX = 0; + + + /** + * Constructor. + * + * Modifies a given tree to add checkboxes. + * - The tree's existing cell renderer will be wrapped with a ChecklistTreeCellRenderer. + * - A MouseListener will be added to the tree to detect clicks, which will toggle checkboxes. + * + * A secondary ChecklistTreeSelectionModel will track checkboxes' states (independent of row + * highlighting). + * + * @param tree a tree to modify + * @param dig true show that a node is partially selected by scanning its descendents, false otherwise + * @checklistFilter a filter to decide which TreePaths need checkboxes, or null + */ + public ChecklistTreeManager( JTree tree, boolean dig, ChecklistTreePathFilter checklistFilter ) { + this.tree = tree; + this.checklistFilter = checklistFilter; + + // Note: If largemodel is not set then treenodes are getting truncated. + // Need to debug further to find the problem. + if ( checklistFilter != null ) tree.setLargeModel( true ); + + selectionModel = new ChecklistTreeSelectionModel( tree.getModel(), dig ); + + ChecklistTreeCellRenderer checklistRenderer = new ChecklistTreeCellRenderer( tree.getCellRenderer(), selectionModel, checklistFilter ); + setCheckboxMaxX( checklistRenderer.getCheckboxMaxX() ); + tree.setCellRenderer( checklistRenderer ); + + selectionModel.addTreeSelectionListener( this ); + tree.addMouseListener( this ); + } + + + /** + * Sets the checkbox's right edge (in the TreeCellRenderer component's coordinate space). + * + * Values less than that will be interpreted as within the checkbox's bounds. + * X=0 is the renderer component's left edge. + */ + public void setCheckboxMaxX( int x ) { + checkMaxX = x; + } + + + public ChecklistTreePathFilter getChecklistFilter() { + return checklistFilter; + } + + + public ChecklistTreeSelectionModel getSelectionModel() { + return selectionModel; + } + + + @Override + public void mouseClicked( MouseEvent e ) { + TreePath path = tree.getPathForLocation( e.getX(), e.getY() ); + if ( path == null ) return; + + if ( e.getX() > tree.getPathBounds(path).x + checkMaxX ) return; + + if ( checklistFilter != null && !checklistFilter.isSelectable(path) ) return; + + boolean selected = selectionModel.isPathSelected( path, selectionModel.isDigged() ); + selectionModel.removeTreeSelectionListener( this ); + + try { + if ( selected ) { + selectionModel.removeSelectionPath( path ); + } else { + selectionModel.addSelectionPath( path ); + } + } + finally { + selectionModel.addTreeSelectionListener( this ); + tree.treeDidChange(); + } + } + + + @Override + public void valueChanged( TreeSelectionEvent e ) { + tree.treeDidChange(); + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreePanel.java b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreePanel.java new file mode 100644 index 0000000..37ec53a --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreePanel.java @@ -0,0 +1,196 @@ +package net.vhati.modmanager.ui.tree; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import javax.swing.DropMode; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.vhati.modmanager.ui.tree.ChecklistTreeManager; +import net.vhati.modmanager.ui.tree.ChecklistTreeSelectionModel; +import net.vhati.modmanager.ui.tree.GroupTreeCellRenderer; +import net.vhati.modmanager.ui.tree.TreeTransferHandler; + + +public class ChecklistTreePanel extends JPanel { + + private DefaultTreeModel treeModel = null; + private JTree tree = null; + private ChecklistTreeManager checklistManager = null; + + + public ChecklistTreePanel() { + super( new BorderLayout() ); + + DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode( "Root" ); + treeModel = new DefaultTreeModel( rootNode ); + tree = new JTree( treeModel ); + tree.setCellRenderer( new GroupTreeCellRenderer() ); + tree.setRootVisible( false ); + tree.getSelectionModel().setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); + checklistManager = new ChecklistTreeManager( tree, true, null ); + + JScrollPane scrollPane = new JScrollPane( tree, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); + this.add( scrollPane, BorderLayout.CENTER ); + + tree.setTransferHandler( new TreeTransferHandler( tree ) ); + tree.setDropMode( DropMode.ON_OR_INSERT ); // Drop between rows, or onto groups. + tree.setDragEnabled( true ); + } + + + /** + * Returns all userObjects of nodes with ticked checkboxes (except root itself). + */ + public List getSelectedUserObjects() { + ChecklistTreeSelectionModel checklistSelectionModel = checklistManager.getSelectionModel(); + List results = new ArrayList(); + + for ( Enumeration enumer = checklistSelectionModel.getAllSelectedPaths(); enumer.hasMoreElements(); ) { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement(); + if ( !childNode.isRoot() && childNode.getUserObject() != null ) { + results.add( childNode.getUserObject() ); + } + } + + return results; + } + + /** + * Returns all userObjects of all nodes (except root itself). + */ + public List getAllUserObjects() { + List results = new ArrayList(); + + DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot(); + getAllUserObjects( rootNode, results ); + + return results; + } + + private void getAllUserObjects( DefaultMutableTreeNode currentNode, List results ) { + if ( !currentNode.isRoot() && currentNode.getUserObject() != null ) { + results.add( currentNode.getUserObject() ); + } + + for ( Enumeration enumer = currentNode.children(); enumer.hasMoreElements(); ) { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement(); + getAllUserObjects( currentNode, results ); + } + } + + + public void clear() { + DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot(); + rootNode.removeAllChildren(); + treeModel.reload(); + } + + + /** + * Adds a group to consolidate mods. + * + * TODO: Trigger a rename. + */ + public void addGroup() { + DefaultMutableTreeNode rootNode = (DefaultMutableTreeNode)treeModel.getRoot(); + GroupAwareTreeNode groupNode = new GroupAwareTreeNode( "New Group", true ); + rootNode.add( groupNode ); + treeModel.nodesWereInserted( rootNode, new int[]{rootNode.getIndex( groupNode )} ); + } + + /** + * Disband selected groups. + * + * TODO + */ + public void removeSelectedGroups() { + } + + /** + * Rename last selected group. + * + * TODO + */ + public void renameSelectedGroup() { + } + + + /** + * Cycles through ticking all checkboxes and clearing them. + */ + public void toggleAllNodeSelection() { + } + + /** + * Cycles through expanding all nodes and collapsing them. + */ + public void toggleAllNodeExpansion() { + boolean canExpand = false; + boolean canCollapse = false; + + for ( int i = tree.getRowCount()-1; i >= 0; i-- ) { + if ( tree.isCollapsed( i ) ) { + canExpand = true; + } + else if ( tree.isExpanded( i ) ) { + canCollapse = true; + } + } + + if ( canExpand ) { + expandAllNodes( tree.getRowCount() ); + } + else if ( canCollapse ) { + collapseAllNodes( new TreePath( treeModel.getRoot() ) ); + } + } + + /** + * Expands all nodes by repeatedly expanding until the row count stops + * growing. + */ + public void expandAllNodes( int prevRowCount ) { + for ( int i=0; i < prevRowCount; i++ ) { + tree.expandRow( i ); + } + if ( tree.getRowCount() != prevRowCount ) { + expandAllNodes( tree.getRowCount() ); + } + } + + /** + * Collapses all nodes by walking the TreeModel. + */ + public void collapseAllNodes( TreePath currentPath ) { + Object currentNode = currentPath.getLastPathComponent(); + for ( int i = treeModel.getChildCount( currentNode )-1; i >= 0; i-- ) { + Object childNode = treeModel.getChild( currentNode, i ); + TreePath childPath = currentPath.pathByAddingChild( childNode ); + collapseAllNodes( childPath ); + } + if ( currentNode != treeModel.getRoot() ) tree.collapsePath( currentPath ); + } + + + public JTree getTree() { + return tree; + } + + public DefaultTreeModel getTreeModel() { + return treeModel; + } + + public ChecklistTreeManager getChecklistManager() { + return checklistManager; + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreePathFilter.java b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreePathFilter.java new file mode 100644 index 0000000..5bb5969 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreePathFilter.java @@ -0,0 +1,13 @@ +package net.vhati.modmanager.ui.tree; + +import javax.swing.tree.TreePath; + + +/** + * Decides whether a given TreePath should have a checkbox. + */ +public interface ChecklistTreePathFilter { + + public boolean isSelectable( TreePath path ); + +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeSelectionModel.java b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeSelectionModel.java new file mode 100644 index 0000000..09d82aa --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/ChecklistTreeSelectionModel.java @@ -0,0 +1,258 @@ +/* + * Based on CheckTreeSelectionModel (rev 120, 2007-07-20) + * By Santhosh Kumar T + * https://java.net/projects/myswing + * + * https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/check/CheckTreeSelectionModel.java?rev=120 + */ + +/** + * MySwing: Advanced Swing Utilites + * Copyright (C) 2005 Santhosh Kumar T + *

+ * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + *

+ * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ + +package net.vhati.modmanager.ui.tree; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Stack; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.vhati.modmanager.ui.tree.PreorderEnumeration; + + +public class ChecklistTreeSelectionModel extends DefaultTreeSelectionModel { + + private TreeModel model; + private boolean dig = true; + + + public ChecklistTreeSelectionModel( TreeModel model, boolean dig ) { + this.model = model; + this.dig = dig; + this.setSelectionMode( TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION ); + } + + public boolean isDigged() { + return dig; + } + + + /** + * Returns true if path1 is a descendant of path2. + */ + private boolean isDescendant( TreePath path1, TreePath path2 ) { + Object obj1[] = path1.getPath(); + Object obj2[] = path2.getPath(); + for ( int i=0; i < obj2.length; i++ ) { + if ( obj1[i] != obj2[i] ) return false; + } + return true; + } + + + /** + * Returns true a selected node exists in the subtree of a given unselected path. + * Returns false if the given path is itself selected. + */ + public boolean isPartiallySelected( TreePath path ) { + if ( isPathSelected( path, true ) ) return false; + + TreePath[] selectionPaths = getSelectionPaths(); + if( selectionPaths == null ) return false; + + for ( int j=0; j < selectionPaths.length; j++ ) { + if ( isDescendant( selectionPaths[j], path ) ) { + return true; + } + } + return false; + } + + /** + * Returns true if a given path is selected. + * + * If dig is true, then the path is assumed to be selected, if + * one of its ancestors is selected. + */ + public boolean isPathSelected( TreePath path, boolean dig ) { + if ( !dig ) return super.isPathSelected( path ); + + while ( path != null && !super.isPathSelected( path ) ) { + path = path.getParentPath(); + } + return ( path != null ); + } + + + @Override + public void setSelectionPaths( TreePath[] paths ) { + if ( dig ) { + throw new UnsupportedOperationException(); + } else { + super.setSelectionPaths( paths ); + } + } + + @Override + public void addSelectionPaths( TreePath[] paths ) { + if ( !dig ) { + super.addSelectionPaths( paths ); + return; + } + + // Unselect all descendants of paths[]. + for( int i=0; i < paths.length; i++ ) { + TreePath path = paths[i]; + TreePath[] selectionPaths = getSelectionPaths(); + if ( selectionPaths == null ) break; + + ArrayList toBeRemoved = new ArrayList(); + for ( int j=0; j < selectionPaths.length; j++ ) { + if ( isDescendant( selectionPaths[j], path ) ) { + toBeRemoved.add( selectionPaths[j] ); + } + } + super.removeSelectionPaths( (TreePath[])toBeRemoved.toArray( new TreePath[0] ) ); + } + + // If all siblings are selected then unselect them and select parent recursively + // otherwize just select that path. + for ( int i=0; i < paths.length; i++ ) { + TreePath path = paths[i]; + TreePath temp = null; + while ( areSiblingsSelected(path) ) { + temp = path; + if ( path.getParentPath() == null ) break; + path = path.getParentPath(); + } + if ( temp != null ) { + if ( temp.getParentPath() != null ) { + addSelectionPath( temp.getParentPath() ); + } + else { + if ( !isSelectionEmpty() ) { + removeSelectionPaths(getSelectionPaths()); + } + super.addSelectionPaths( new TreePath[]{temp} ); + } + } + else { + super.addSelectionPaths( new TreePath[]{path} ); + } + } + } + + @Override + public void removeSelectionPaths( TreePath[] paths ) { + if( !dig ) { + super.removeSelectionPaths( paths ); + return; + } + + for ( int i=0; i < paths.length; i++ ) { + TreePath path = paths[i]; + if ( path.getPathCount() == 1 ) { + super.removeSelectionPaths( new TreePath[]{path} ); + } else { + toggleRemoveSelection(path); + } + } + } + + + /** + * Returns true if all siblings of given path are selected. + */ + private boolean areSiblingsSelected( TreePath path ) { + TreePath parent = path.getParentPath(); + if ( parent == null ) return true; + + Object node = path.getLastPathComponent(); + Object parentNode = parent.getLastPathComponent(); + + int childCount = model.getChildCount( parentNode ); + for ( int i=0; i < childCount; i++ ) { + Object childNode = model.getChild( parentNode, i ); + if ( childNode == node ) continue; + + if ( !isPathSelected( parent.pathByAddingChild( childNode ) ) ) { + return false; + } + } + return true; + } + + + /** + * Unselects a given path, toggling ancestors if they were entirely selected. + * + * If any ancestor node of the given path is selected, it will be unselected, + * and all its descendants - except any within the given path - will be selected. + * The ancestor will have gone from fully selected to partially selected. + * + * Otherwise, the given path will be unselected, and nothing else will change. + */ + private void toggleRemoveSelection( TreePath path ) { + Stack stack = new Stack(); + TreePath parent = path.getParentPath(); + + while ( parent != null && !isPathSelected( parent ) ) { + stack.push( parent ); + parent = parent.getParentPath(); + } + + if ( parent != null ) { + stack.push( parent ); + } + else { + super.removeSelectionPaths( new TreePath[]{path} ); + return; + } + + while ( !stack.isEmpty() ) { + TreePath temp = (TreePath)stack.pop(); + TreePath peekPath = ( stack.isEmpty() ? path : (TreePath)stack.peek() ); + Object node = temp.getLastPathComponent(); + Object peekNode = peekPath.getLastPathComponent(); + + int childCount = model.getChildCount( node ); + for ( int i=0; i < childCount; i++ ) { + Object childNode = model.getChild( node, i ); + if ( childNode != peekNode ) { + super.addSelectionPaths( new TreePath[]{temp.pathByAddingChild( childNode )} ); + } + } + } + super.removeSelectionPaths( new TreePath[]{parent} ); + } + + + public Enumeration getAllSelectedPaths() { + TreePath[] treePaths = getSelectionPaths(); + if ( treePaths == null ) { + return Collections.enumeration( Collections.EMPTY_LIST ); + } + + Enumeration enumer = Collections.enumeration( Arrays.asList( treePaths ) ); + if ( dig ) { + enumer = new PreorderEnumeration( enumer, model ); + } + return enumer; + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/ChildrenEnumeration.java b/src/main/java/net/vhati/modmanager/ui/tree/ChildrenEnumeration.java new file mode 100644 index 0000000..0d132ef --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/ChildrenEnumeration.java @@ -0,0 +1,57 @@ +/** + * Based on ChildrenEnumeration (rev 120, 2007-07-20) + * By Santhosh Kumar T + * https://java.net/projects/myswing + * + * https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/ChildrenEnumeration.java?rev=120 + */ + +/** + * MySwing: Advanced Swing Utilites + * Copyright (C) 2005 Santhosh Kumar T + *

+ * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + *

+ * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ + +package net.vhati.modmanager.ui.tree; + +import java.util.Enumeration; +import java.util.NoSuchElementException; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + + +public class ChildrenEnumeration implements Enumeration { + + private TreePath path; + private TreeModel model; + private int position = 0; + private int childCount; + + + public ChildrenEnumeration( TreePath path, TreeModel model ) { + this.path = path; + this.model = model; + childCount = model.getChildCount( path.getLastPathComponent() ); + } + + @Override + public boolean hasMoreElements() { + return position < childCount; + } + + @Override + public Object nextElement() { + if( !hasMoreElements() ) throw new NoSuchElementException(); + + return path.pathByAddingChild( model.getChild( path.getLastPathComponent(), position++ ) ); + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/GroupAwareTreeNode.java b/src/main/java/net/vhati/modmanager/ui/tree/GroupAwareTreeNode.java new file mode 100644 index 0000000..63e0120 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/GroupAwareTreeNode.java @@ -0,0 +1,23 @@ +package net.vhati.modmanager.ui.tree; + +import javax.swing.tree.DefaultMutableTreeNode; + + +/** + * A TreeNode that remembers whether it allows children when cloned. + */ +public class GroupAwareTreeNode extends DefaultMutableTreeNode { + + public GroupAwareTreeNode( Object userObject, boolean allowsChildren ) { + super( userObject, allowsChildren ); + } + + + @Override + public Object clone() { + GroupAwareTreeNode newNode = (GroupAwareTreeNode)super.clone(); + newNode.setAllowsChildren( this.getAllowsChildren() ); + + return newNode; + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/GroupTreeCellRenderer.java b/src/main/java/net/vhati/modmanager/ui/tree/GroupTreeCellRenderer.java new file mode 100644 index 0000000..c969e5c --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/GroupTreeCellRenderer.java @@ -0,0 +1,39 @@ +package net.vhati.modmanager.ui.tree; + +import java.awt.Component; +import javax.swing.JTree; +import javax.swing.UIManager; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; + + +/** + * A renderer that sets icons based on whether children are allowed. + * + * A group with no children will still have a group icon. + */ +public class GroupTreeCellRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent( JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus ) { + super.getTreeCellRendererComponent( tree, value, sel, expanded, leaf, row, hasFocus ); + + if ( value instanceof DefaultMutableTreeNode ) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode)value; + if ( node.getAllowsChildren() ) { + if ( expanded ) { + this.setIcon( this.getDefaultOpenIcon() ); + this.setDisabledIcon( this.getDefaultOpenIcon() ); + } else { + this.setIcon( this.getDefaultClosedIcon() ); + this.setDisabledIcon( this.getDefaultClosedIcon() ); + } + } else { + this.setIcon( this.getDefaultLeafIcon() ); + this.setDisabledIcon( this.getDefaultLeafIcon() ); + } + } + + return this; + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/PreorderEnumeration.java b/src/main/java/net/vhati/modmanager/ui/tree/PreorderEnumeration.java new file mode 100644 index 0000000..7ebf789 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/PreorderEnumeration.java @@ -0,0 +1,67 @@ +/** + * Based on PreorderEnumeration (rev 120, 2007-07-20) + * By Santhosh Kumar T + * https://java.net/projects/myswing + * + * https://java.net/projects/myswing/sources/svn/content/trunk/src/skt/swing/tree/PreorderEnumeration.java?rev=120 + */ + +/** + * MySwing: Advanced Swing Utilites + * Copyright (C) 2005 Santhosh Kumar T + *

+ * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + *

+ * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ + +package net.vhati.modmanager.ui.tree; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Stack; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + + +public class PreorderEnumeration implements Enumeration { + + private TreeModel model; + protected Stack stack = new Stack(); + + + public PreorderEnumeration( TreePath path, TreeModel model ) { + this( Collections.enumeration( Collections.singletonList( path ) ), model ); + } + + public PreorderEnumeration( Enumeration enumer, TreeModel model ){ + this.model = model; + stack.push( enumer ); + } + + + @Override + public boolean hasMoreElements() { + return ( !stack.empty() && stack.peek().hasMoreElements() ); + } + + @Override + public Object nextElement() { + Enumeration enumer = stack.peek(); + TreePath path = (TreePath)enumer.nextElement(); + + if ( !enumer.hasMoreElements() ) stack.pop(); + + if ( model.getChildCount( path.getLastPathComponent() ) > 0 ) { + stack.push( new ChildrenEnumeration( path, model ) ); + } + + return path; + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/TreeTransferHandler.java b/src/main/java/net/vhati/modmanager/ui/tree/TreeTransferHandler.java new file mode 100644 index 0000000..3ec1edd --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/TreeTransferHandler.java @@ -0,0 +1,257 @@ +package net.vhati.modmanager.ui.tree; + +import java.awt.Cursor; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.dnd.DragSource; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.TransferHandler; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + + +/** + * A handler to enable drag-and-drop within a JTree. + * + * When dropped, copies of highlighted nodes will be made via clone() and + * inserted at the drop location, then the originals will be removed. + * + * Dragging onto a space between nodes will insert at that location. + * Dragging onto a node that allows children will insert into it. + * Dragging onto a node that doesn't allow children will insert after it. + * + * All nodes must be instances of DefaultMutableTreeNode (or subclasses). + * Set the Jtree's DropMode to ON_OR_INSERT. + * The root node must be hidden, to prevent it from being dragged. + * The tree's selection model may be set to single or multiple. + */ +public class TreeTransferHandler extends TransferHandler { + + private DataFlavor localTreePathFlavor = null; + private JTree tree = null; + + + public TreeTransferHandler( JTree tree ) { + super(); + this.tree = tree; + + try { + localTreePathFlavor = new DataFlavor( DataFlavor.javaJVMLocalObjectMimeType + ";class=\""+ TreePath[].class.getName() +"\"" ); + } + catch ( ClassNotFoundException e ) { + //log.error( e ); + } + } + + @Override + protected Transferable createTransferable( JComponent c ) { + assert ( c == tree ); + TreePath[] highlightedPaths = tree.getSelectionPaths(); + + Map> pathsByLengthMap = new TreeMap>(); + for ( TreePath path : highlightedPaths ) { + if ( path.getPath().length == 1 ) continue; // Omit root node (shouldn't drag it anyway). + + Integer pathLength = new Integer( path.getPath().length ); + if ( !pathsByLengthMap.containsKey( pathLength ) ) { + pathsByLengthMap.put( pathLength, new ArrayList() ); + } + pathsByLengthMap.get( pathLength ).add( path ); + } + // For each length (shortest-first), iterate its paths. + // For each of those paths, search longer lengths' lists, + // removing any paths that are descendants of those short ancestor nodes. + List lengthsList = new ArrayList( pathsByLengthMap.keySet() ); + for ( int i=0; i < lengthsList.size(); i++ ) { + for ( TreePath ancestorPath : pathsByLengthMap.get( lengthsList.get( i ) ) ) { + for ( int j=i+1; j < lengthsList.size(); j++ ) { + + List childPaths = pathsByLengthMap.get( lengthsList.get( j ) ); + for ( Iterator childIt = childPaths.iterator(); childIt.hasNext(); ) { + TreePath childPath = childIt.next(); + if ( ancestorPath.isDescendant( childPath ) ) { + childIt.remove(); + } + } + + } + } + } + List uniquePathList = new ArrayList(); + for ( List paths : pathsByLengthMap.values() ) { + uniquePathList.addAll( paths ); + } + TreePath[] uniquePathsArray = uniquePathList.toArray( new TreePath[uniquePathList.size()] ); + + return new TreePathTransferrable( uniquePathsArray ); + } + + @Override + public boolean canImport( TransferHandler.TransferSupport ts ) { + boolean b = ( ts.getComponent() == tree && ts.isDrop() && ts.isDataFlavorSupported(localTreePathFlavor) ); + tree.setCursor( b ? DragSource.DefaultMoveDrop : DragSource.DefaultMoveNoDrop ); + return b; + } + + @Override + public int getSourceActions( JComponent comp ) { + return TransferHandler.MOVE; + } + + @Override + @SuppressWarnings("Unchecked") + public boolean importData( TransferHandler.TransferSupport ts ) { + if ( !canImport(ts) ) return false; + + JTree dstTree = (JTree)ts.getComponent(); + DefaultTreeModel dstTreeModel = (DefaultTreeModel)dstTree.getModel(); + JTree.DropLocation dl = (JTree.DropLocation)ts.getDropLocation(); + TreePath dropPath = dl.getPath(); // Dest parent node, or null. + int dropIndex = dl.getChildIndex(); // Insertion child index in the dest parent node, + // or -1 if dropped onto a group. + + dstTree.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) ); + if ( dropPath == null ) return false; + DefaultMutableTreeNode dropParentNode = (DefaultMutableTreeNode)dropPath.getLastPathComponent(); + + // When dropping onto a non-group node, insert into the position after it instead. + if ( !dropParentNode.getAllowsChildren() ) { + DefaultMutableTreeNode prevParentNode = dropParentNode; + dropPath = dropPath.getParentPath(); + dropParentNode = (DefaultMutableTreeNode)dropPath.getLastPathComponent(); + dropIndex = dropParentNode.getIndex( prevParentNode ) + 1; + } + + try { + TreePath[] draggedPaths = (TreePath[])ts.getTransferable().getTransferData( localTreePathFlavor ); + + // Bail if the dropPath was among those dragged. + boolean badDrop = false; + for ( TreePath path : draggedPaths ) { + if ( path.equals( dropPath ) ) { + badDrop = true; + break; + } + } + + if ( !badDrop && dropParentNode.getAllowsChildren() ) { + for ( TreePath path : draggedPaths ) { + // Copy the dragged node and any children. + DefaultMutableTreeNode srcNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + DefaultMutableTreeNode newNode = (DefaultMutableTreeNode)cloneNodes( srcNode ); + + if ( dropIndex != -1 ) { + // Insert. + dropParentNode.insert( newNode, dropIndex ); + dstTreeModel.nodesWereInserted( dropParentNode, new int[]{dropIndex} ); + dropIndex++; // Next insertion will be after this node. + } + else { + // Add to the end. + dropParentNode.add( newNode ); + dstTreeModel.nodesWereInserted( dropParentNode, new int[]{dropParentNode.getChildCount()-1} ); + if ( !dstTree.isExpanded( dropPath ) ) dstTree.expandPath( dropPath ); + } + } + return true; + } + } + catch ( Exception e ) { + // UnsupportedFlavorException: if Transferable.getTransferData() fails. + // IOException: if Transferable.getTransferData() fails. + // IllegalStateException: if insert/add fails because dropPath's node doesn't allow children. + //log.error( e ); + } + return false; + } + + @Override + protected void exportDone( JComponent source, Transferable data, int action ) { + if ( action == TransferHandler.MOVE || action == TransferHandler.NONE ) { + tree.setCursor( Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR) ); + } + + JTree srcTree = (JTree)source; + DefaultTreeModel srcTreeModel = (DefaultTreeModel)srcTree.getModel(); + + if ( action == TransferHandler.MOVE ) { + // Remove original dragged rows now that the move completed. + // Scan the tree checking equality is fine (DefaultMutableTreeNode's equals() does ==). + + try { + TreePath[] draggedPaths = (TreePath[])data.getTransferData( localTreePathFlavor ); + for ( TreePath path : draggedPaths ) { + DefaultMutableTreeNode doomedNode = (DefaultMutableTreeNode)path.getLastPathComponent(); + TreeNode parentNode = doomedNode.getParent(); + int doomedIndex = parentNode.getIndex( doomedNode ); + doomedNode.removeFromParent(); + srcTreeModel.nodesWereRemoved( parentNode, new int[]{doomedIndex}, new Object[]{doomedNode} ); + } + } + catch ( Exception e ) { + //log.error( e ); + } + } + } + + + /** + * Recursively clones a node and its descendants. + * + * The clone() methods will reuse userObjects, but the nodes themselves will be new. + */ + @SuppressWarnings("Unchecked") + private DefaultMutableTreeNode cloneNodes( DefaultMutableTreeNode srcNode ) { + DefaultMutableTreeNode resultNode = (DefaultMutableTreeNode)srcNode.clone(); + + Enumeration enumer = srcNode.children(); + while ( enumer.hasMoreElements() ) { + DefaultMutableTreeNode childNode = (DefaultMutableTreeNode)enumer.nextElement(); + resultNode.add( cloneNodes( (DefaultMutableTreeNode)childNode ) ); + } + + return resultNode; + } + + + /** + * Drag and drop TreePath data, constructed with a raw object + * from a drag source, to be transformed into a flavor + * suitable for the drop target. + */ + private class TreePathTransferrable implements Transferable { + private TreePath[] data; + + public TreePathTransferrable( TreePath[] data ) { + this.data = data; + } + + @Override + public Object getTransferData( DataFlavor flavor ) { + if ( flavor.equals( localTreePathFlavor ) ) { + return data; + } + return null; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] {localTreePathFlavor}; + } + + @Override + public boolean isDataFlavorSupported( DataFlavor flavor ) { + return flavor.equals( localTreePathFlavor ); + } + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/TristateButtonModel.java b/src/main/java/net/vhati/modmanager/ui/tree/TristateButtonModel.java new file mode 100644 index 0000000..4817243 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/TristateButtonModel.java @@ -0,0 +1,105 @@ +/** + * Copied from "TristateCheckBox Revisited" (2007-05-25) + * By Dr. Heinz M. Kabutz + * http://www.javaspecialists.co.za/archive/Issue145.html + */ + +package net.vhati.modmanager.ui.tree; + +import java.awt.event.ItemEvent; +import javax.swing.JToggleButton.ToggleButtonModel; + + +public class TristateButtonModel extends ToggleButtonModel { + + private TristateState state = TristateState.DESELECTED; + + + public TristateButtonModel( TristateState state ) { + setState( state ); + } + + public TristateButtonModel() { + this( TristateState.DESELECTED ); + } + + + public void setIndeterminate() { + setState( TristateState.INDETERMINATE ); + } + + public boolean isIndeterminate() { + return ( state == TristateState.INDETERMINATE ); + } + + + @Override + public void setEnabled( boolean enabled ) { + super.setEnabled(enabled); + // Restore state display. + displayState(); + } + + @Override + public void setSelected( boolean selected ) { + setState( selected ? TristateState.SELECTED : TristateState.DESELECTED ); + } + + @Override + public void setArmed( boolean b ) { + } + + @Override + public void setPressed( boolean b ) { + } + + + public void iterateState() { + setState( state.next() ); + } + + public void setState( TristateState state ) { + this.state = state; + displayState(); + if ( state == TristateState.INDETERMINATE && isEnabled() ) { + // Send ChangeEvent. + fireStateChanged(); + + // Send ItemEvent. + int indeterminate = 3; + fireItemStateChanged(new ItemEvent( this, ItemEvent.ITEM_STATE_CHANGED, this, indeterminate )); + } + } + + private void displayState() { + super.setSelected( state != TristateState.DESELECTED ); + super.setArmed( state == TristateState.INDETERMINATE ); + super.setPressed( state == TristateState.INDETERMINATE ); + } + + public TristateState getState() { + return state; + } + + + + public static enum TristateState { + SELECTED { + public TristateState next() { + return INDETERMINATE; + } + }, + INDETERMINATE { + public TristateState next() { + return DESELECTED; + } + }, + DESELECTED { + public TristateState next() { + return SELECTED; + } + }; + + public abstract TristateState next(); + } +} diff --git a/src/main/java/net/vhati/modmanager/ui/tree/TristateCheckBox.java b/src/main/java/net/vhati/modmanager/ui/tree/TristateCheckBox.java new file mode 100644 index 0000000..95a4395 --- /dev/null +++ b/src/main/java/net/vhati/modmanager/ui/tree/TristateCheckBox.java @@ -0,0 +1,142 @@ +/* + * Based on "TristateCheckBox Revisited" (2007-05-25) + * By Dr. Heinz M. Kabutz + * http://www.javaspecialists.co.za/archive/Issue145.html + */ + +package net.vhati.modmanager.ui.tree; + +import java.awt.AWTEvent; +import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.ButtonModel; +import javax.swing.Icon; +import javax.swing.JCheckBox; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.plaf.ActionMapUIResource; + +import net.vhati.modmanager.ui.tree.TristateButtonModel; +import net.vhati.modmanager.ui.tree.TristateButtonModel.TristateState; + + +public class TristateCheckBox extends JCheckBox { + + private final ChangeListener enableListener; + + + public TristateCheckBox( String text, Icon icon, TristateState initial ) { + super( text, icon ); + + setModel( new TristateButtonModel( initial ) ); + + enableListener = new ChangeListener() { + @Override + public void stateChanged( ChangeEvent e ) { + TristateCheckBox.this.setFocusable( TristateCheckBox.this.getModel().isEnabled() ); + } + }; + + // Add a listener for when the mouse is pressed. + super.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed( MouseEvent e ) { + TristateCheckBox.this.iterateState(); + } + }); + + // Reset the keyboard action map. + ActionMap map = new ActionMapUIResource(); + map.put( "pressed", new AbstractAction() { + @Override + public void actionPerformed( ActionEvent e ) { + TristateCheckBox.this.iterateState(); + } + }); + map.put( "released", null ); + SwingUtilities.replaceUIActionMap( this, map ); + } + + public TristateCheckBox( String text, TristateState initial ) { + this( text, null, initial ); + } + + public TristateCheckBox( String text ) { + this( text, null ); + } + + public TristateCheckBox() { + this( null ); + } + + + public void setIndeterminate() { + getTristateModel().setIndeterminate(); + } + + public boolean isIndeterminate() { + return getTristateModel().isIndeterminate(); + } + + + public void setState( TristateState state ) { + getTristateModel().setState( state ); + } + + public TristateState getState() { + return getTristateModel().getState(); + } + + + @Override + public void setModel( ButtonModel newModel ) { + super.setModel( newModel ); + + // Listen for enable changes. + if ( model instanceof TristateButtonModel ) { + model.addChangeListener( enableListener ); + } + } + + @SuppressWarnings("unchecked") + public TristateButtonModel getTristateModel() { + return (TristateButtonModel)super.getModel(); + } + + + /** + * No one may add mouse listeners, not even Swing! + */ + @Override + public void addMouseListener( MouseListener l ) { + } + + + private void iterateState() { + // Maybe do nothing at all? + if ( !super.getModel().isEnabled() ) return; + + this.grabFocus(); + + // Iterate state. + getTristateModel().iterateState(); + + // Fire ActionEvent. + int modifiers = 0; + AWTEvent currentEvent = EventQueue.getCurrentEvent(); + if ( currentEvent instanceof InputEvent ) { + modifiers = ((InputEvent)currentEvent).getModifiers(); + } + else if ( currentEvent instanceof ActionEvent ) { + modifiers = ((ActionEvent)currentEvent).getModifiers(); + } + fireActionPerformed(new ActionEvent( this, ActionEvent.ACTION_PERFORMED, this.getText(), System.currentTimeMillis(), modifiers )); + } +}