JTree mit Checkboxen abändern

Hallo zusammen,
Ich habe mir vor einiger Zeit ein einen JTree gesucht, der statt der normlen Anzeige Checkboxen verwendet. Bei meiner Suche bin ich dabei auf folgendes gestoßen:

http://www.jroller.com/santhosh/entry/jtree_with_checkboxes

Die dort präsentierte Änderung eines java2s-Beispiels funktioniert auch genau wie beschrieben. Für meine Zwecke würde ich aber gerne ein Detail ändern:

Wenn eine Node angewählt wird, sollen nicht automatisch alle Subnodes angewählt werden.

Hintergrund ist, dass ich den Tree als „übersichtliche Auswahl“ von Kriterien verwende: Ich kann ein Kriterium verwenden, muss aber nicht zwangsläufig die Subkriterien verwenden (Details wären an dieser Stelle zu lang, aber der Sinn dahinter ist gewährleistet).

Leider finde ich die entsprechende Stelle im Code leider nicht.
Ich bin der Meinung, dass etwas im CheckTreeSelectionModel geändert werden muss, wahrscheinlich in „addSelectionPath“. Wenn jemand ne Alternative kennt oder weiß was zu tun ist wäre ich sehr dankbar :). Hier habe ich den notwendigen Code in einer Klasse zusammengestellt:
[SPOILER]

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.*;
import javax.swing.tree.*;

public class CheckTreeExample {
	
	public CheckTreeExample(JTree tree) {
		CheckTreeManager treeManager = new CheckTreeManager(tree);
	}
	
	public static void main(String args[]) {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		
		//Add tree
		JTree tree = new JTree();
		new CheckTreeExample(tree);
		
		frame.add(tree);
		
		//Finish Frame
		frame.pack();
		frame.setSize(300, 500);		
		frame.setVisible(true);
	}
	
	
	public class CheckTreeManager extends MouseAdapter implements TreeSelectionListener{ 
	    private CheckTreeSelectionModel selectionModel; 
	    private JTree tree = new JTree(); 
	    int hotspot = new JCheckBox().getPreferredSize().width; 
	 
	    public CheckTreeManager(JTree tree){ 
	        this.tree = tree; 
	        selectionModel = new CheckTreeSelectionModel(tree.getModel()); 
	        tree.setCellRenderer(new CheckTreeCellRenderer(tree.getCellRenderer(), selectionModel)); 
	        tree.addMouseListener(this); 
	        selectionModel.addTreeSelectionListener(this); 
	    } 
	 
	    public void mouseClicked(MouseEvent me){ 
	        TreePath path = tree.getPathForLocation(me.getX(), me.getY()); 
	        if(path==null) 
	            return; 
	        if(me.getX()>tree.getPathBounds(path).x+hotspot) 
	            return; 
	 
	        boolean selected = selectionModel.isPathSelected(path, true); 
	        selectionModel.removeTreeSelectionListener(this); 
	 
	        try{ 
	            if(selected) 
	                selectionModel.removeSelectionPath(path); 
	            else 
	                selectionModel.addSelectionPath(path); 
	        } finally{ 
	            selectionModel.addTreeSelectionListener(this); 
	            tree.treeDidChange(); 
	        } 
	    } 
	 
	    public CheckTreeSelectionModel getSelectionModel(){ 
	        return selectionModel; 
	    } 
	 
	    public void valueChanged(TreeSelectionEvent e){ 
	        tree.treeDidChange(); 
	    } 
	} 
	
	
	public class CheckTreeSelectionModel extends DefaultTreeSelectionModel{ 
	    private TreeModel model; 
	 
	    public CheckTreeSelectionModel(TreeModel model){ 
	        this.model = model; 
	        setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); 
	    } 
	 
	    // tests whether there is any unselected node in the subtree of given path 
	    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; 
	    } 
	 
	    // tells whether given path is selected. 
	    // if dig is true, then a path is assumed to be selected, if 
	    // one of its ancestor 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; 
	    } 
	 
	    // is path1 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**!=obj2**) 
	                return false; 
	        } 
	        return true; 
	    } 
	 
	    public void setSelectionPaths(TreePath[] pPaths){ 
	        throw new UnsupportedOperationException("not implemented yet!!!"); 
	    } 
	 
	    public void addSelectionPaths(TreePath[] paths) {
	        // unselect all descendants of paths[] 
	        for(int i = 0; i<paths.length; i++){ 
	            TreePath path = paths**; 
	            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**; 
	            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}); 
	        } 
	    } 
	 
	    // tells whether 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; 
	    } 
	 
	    public void removeSelectionPaths(TreePath[] paths){ 
	        for(int i = 0; i<paths.length; i++){ 
	            TreePath path = paths**; 
	            if(path.getPathCount()==1) 
	                super.removeSelectionPaths(new TreePath[]{ path}); 
	            else 
	                toggleRemoveSelection(path); 
	        } 
	    } 
	 
	    // if any ancestor node of given path is selected then unselect it 
	    //  and selection all its descendants except given path and descendants. 
	    // otherwise just unselect the given path 
	    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 class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer{ 
	    private CheckTreeSelectionModel selectionModel; 
	    private TreeCellRenderer delegate; 
	    private TristateCheckBox checkBox = new TristateCheckBox(); 
	 
	    public CheckTreeCellRenderer(TreeCellRenderer delegate, CheckTreeSelectionModel selectionModel){ 
	        this.delegate = delegate; 
	        this.selectionModel = selectionModel; 
	        setLayout(new BorderLayout()); 
	        setOpaque(false); 
	        checkBox.setOpaque(false); 
	    } 
	 
	 
	    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus){ 
	        Component renderer = delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); 
	 
	        TreePath path = tree.getPathForRow(row); 
	        if(path!=null){ 
	            if(selectionModel.isPathSelected(path, true)) 
	                checkBox.setState(Boolean.TRUE); 
	            else 
	                checkBox.setState(selectionModel.isPartiallySelected(path) ? null : Boolean.FALSE); 
	        } 
	        removeAll(); 
	        add(checkBox, BorderLayout.WEST); 
	        add(renderer, BorderLayout.CENTER); 
	        return this; 
	    } 
	}  
	
	
	public class TristateCheckBox extends JCheckBox{
		private final TristateDecorator model;
		public TristateCheckBox(String text, Icon icon, Boolean initial){ super(text, icon);
		
		// Add a listener for when the mouse is pressed
		super.addMouseListener(new MouseAdapter(){
			public void mousePressed(MouseEvent e){
				grabFocus(); model.nextState(); 
				}
			}); 
		
		// Reset the keyboard action map
		ActionMap map = new ActionMapUIResource();
		map.put("pressed", new AbstractAction(){
			//NOI18N
			public void actionPerformed(ActionEvent e){
				grabFocus(); model.nextState();
				}
			}); 
		map.put("released", null);
		
		//NOI18N
		SwingUtilities.replaceUIActionMap(this, map);
		
		// set the model to the adapted model
		model = new TristateDecorator(getModel());
		setModel(model); setState(initial);
		} 
		
		
		public TristateCheckBox(String text, Boolean initial){
			this(text, null, initial);
		} 
		
		
		public TristateCheckBox(String text){
			this(text, null);
		} 
		
		
		public TristateCheckBox(){
			this(null); 
		} 
		
		/** No one may add mouse listeners, not even Swing! */ 
		public void addMouseListener(MouseListener l){} 
		
		
		/**  Set the new state to either SELECTED, NOT_SELECTED or
		 * 	 DONT_CARE. If state == null, it is treated as DONT_CARE. 
		 */ 	
		public void setState(Boolean state){
			model.setState(state);
		} 
		
		
		/** Return the current state, which is determined by the
		 *  * selection status of the model.
		 *   */
		public Boolean getState(){
			return model.getState();
			}
		
		
		/** * Exactly which Design Pattern is this? Is it an Adapter,
		 *  * a Proxy or a Decorator? In this case, my vote lies with the
		 *   * Decorator, because we are extending functionality and
		 *    * "decorating" the original model with a more powerful model.
		 *     */ 
		
		
		private class TristateDecorator implements ButtonModel{
			private final ButtonModel other;
			private TristateDecorator(ButtonModel other){
				this.other = other;
			} 
			
			private void setState(Boolean state){
				if(state==Boolean.FALSE){
					other.setArmed(false);
					setPressed(false);
					setSelected(false);
				} else if(state==Boolean.TRUE){
					other.setArmed(false);
					setPressed(false);
					setSelected(true);
				} else{
					other.setArmed(true);
					setPressed(true);
					setSelected(true);
				}
			} 
			
			/** * The current state is embedded in the selection / armed
			 *  * state of the model. 
			 *  * * We return the SELECTED state when the checkbox is selected
			 *   * but not armed, DONT_CARE state when the checkbox is
			 *    * selected and armed (grey) and NOT_SELECTED when the
			 *     * checkbox is deselected. 
			 *     */
			private Boolean getState(){
				if(isSelected() && !isArmed()){
					// normal black tick
					return Boolean.TRUE;
				} else if(isSelected() && isArmed()){
					// don't care grey tick
					return null;
				} else{
					// normal deselected
					return Boolean.FALSE;
				} 
			} 
			
			/** We rotate between NOT_SELECTED, SELECTED and DONT_CARE.
			 */ 
			private void nextState(){
				Boolean current = getState();
				if(current == Boolean.FALSE){
					setState(Boolean.TRUE);
				} else if(current == Boolean.TRUE){
					setState(null);
				} else if(current == null){
					setState(Boolean.FALSE);
				} 
			}
			
			/** Filter: No one may change the armed status except us. */
			public void setArmed(boolean b){
				}
			
			public boolean isFocusTraversable() {
				return isEnabled(); 
			} 
			
			/** We disable focusing on the component when it is not * enabled. */
			public void setEnabled(boolean b){
				// setFocusable(b);
				other.setEnabled(b);
			} 
			
			/** All these methods simply delegate to the "other" model 
			 * * that is being decorated. 
			 * */ 
			public boolean isArmed(){
				return other.isArmed();
			}
			
			public boolean isSelected(){
				return other.isSelected();
			}
			
			public boolean isEnabled(){
				return other.isEnabled();
			}
			
			public boolean isPressed(){
				return other.isPressed();
			}
			
			public boolean isRollover(){
				return other.isRollover();
			}
			
			public void setSelected(boolean b){
				other.setSelected(b);
			}
			
			public void setPressed(boolean b){
				other.setPressed(b); 
			}
			
			public void setRollover(boolean b){
				other.setRollover(b);
			}
			
			public void setMnemonic(int key){
				other.setMnemonic(key); 
			}
			
			public int getMnemonic(){
				return other.getMnemonic();
			}
			
			public void setActionCommand(String s){
				other.setActionCommand(s);
			}
			
			public String getActionCommand(){
				return other.getActionCommand();
			}
			
			public void setGroup(ButtonGroup group){
				other.setGroup(group);
			}
			
			public void addActionListener(ActionListener l){
				other.addActionListener(l);
			}
			
			public void removeActionListener(ActionListener l){
				other.removeActionListener(l);
			}
			
			public void addItemListener(ItemListener l){
				other.addItemListener(l);
			}
			
			public void removeItemListener(ItemListener l){
				other.removeItemListener(l);
			}
			
			public void addChangeListener(ChangeListener l){
				other.addChangeListener(l);
			}
			
			public void removeChangeListener(ChangeListener l){
				other.removeChangeListener(l);
			}
			
			public Object[] getSelectedObjects(){
				return other.getSelectedObjects();
			}
		}
	}

}

[/SPOILER]

*** Edit ***

Achja: Wird eine SubNode angewählt, so sollen natürlich die Parents davon angewählt werden. D.h. heißt umgekehrt, dass beim deselektieren sehr wohl auch alle SubNodes deaktiviert werden müssen. Leider habe ich es bis jetzt noch nicht geschafft die Funktion auch nur abzustellen :frowning:

Sicher “sagen, was zu tun ist”, kann ich noch nicht. (Wenn ich vor der Wahl stünde, würde ich mal websuchen, ob’s nicht schon eine Implementierung gibt, die das macht, was ich will - das sollte ja nicht so ungewöhnlich sein… (ich dachte, schonmal was in der Art gemacht zu haben, aber entweder ich täusche mich, oder finde es gerade nicht…)).

Das Problem ist hier aber: Die Knoten werden nicht ausgewählt! Sie werden nur als “ausgewählt” angezeigt, durch die etwas merkwürdige Semantik mit der “isPartiallySelected”-Methode oder so…

Jedenfalls: Wenn du mal

public void addSelectionPaths(TreePath[] paths) {
    ....
    System.out.println("Selected "+Arrays.toString(getSelectionPaths()));
}

(ganz am Ende) einfügst, und dann auf “Sports” klickst, werden zwar alle Kindknoten als “Selected” angezeigt, aber die Selektion enthält nur “JTree, sports”. Erst wenn man dann einen der Kindknoten explizit ABwählt, werden alle anderen als explizit ANgewählt in der Selection angezeigt.

Alles ziemlich wirr :verzweifel:

Der pragmatische Ansatz, im CellRenderer
if (selectionModel.isPathSelected(path, true)) auf
if (selectionModel.isPathSelected(path, false))
zu ändern, reicht erstmal nicht, dadurch wird das Verhalten irgendwie wirr, was wohl mit dem

// if all siblings are selected then unselect them and select parent recursively

zusammenhängt (man könnte sagen: Das “ungwöhnliche” Verhalten, das oben beschrieben ist, ist irgendwie “eingeplant” und wird vorausgesetzt).

Vielleicht hilft das zumindest als Schubser, bin gerade übermüdet und habe im Moment nicht den Nerv da weiter zu schauen, aber nochmal: Ich denke, das sollte eigentlich etwas einfacher sein, und … wenn’s nicht klappt, schau ich vielleicht später nochmal, ob man das nicht auch einfacher hinkriegen kann…

Ich habe es nun (zumindest vorerst ausreichend) gefunden. Ich habe weiter gesucht und bin dann auf einen anderen CheckBoxTree gestoßen, nämlich den:

http://www.jidesoft.com/javadoc/com/jidesoft/swing/CheckBoxTree.html

Ihn zu erstellen funktioniert (fast) genauso wie der vorherige, er ist aber schneller und reagiert sofort auf TreeSelectionEvents. Mittels

checkboxtree.getCheckBoxTreeSelectionModel().addTreeSelectionListener(new SelectionListener());

wird ein CheckBoxTreeSelectionModel erstellt und diesem ein Listener hinzugefügt. Dieser funktioniert dann wie ein normaler TreeSelectionListener, über das Model kann ich abgreifen was angewählt ist. Außerdem kann ich hier über

einstellen, dass mit Auswahl eines Parents die Children nicht auch angewählt werden. Das einzige was dann leider fehlt ist, dass ich Children anwählen kann ohne die Parents mitauszuwählen. Aber vorerst reicht das :slight_smile: