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 ));
+ }
+}