JTree verschwindet nach Hinzufügen eines Knotens

Wie hier vorbereitet bitte ich Euch um Hilfe bei der Lösung eines Problems mit meinem TreeModel.

Nach dem Hinzufügen eines neuen Knotens verschwindet der Inhalt des JTree. Ich finde leider den Fehler nicht und hoffe, dass Ihr mir bei der Lösung des Problems helfen könnt.

Wie immer anbei ein KSKB:

Die Main-Class:

package category;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.tree.TreePath;

/**
 *
 * @author l-ectron-x
 */
public class Main {

    private CategoryTreeModel treeModel;
    private JTree tree;

    Main() {
        JFrame f = new JFrame("JTree-Demo");
        f.add(createActionBar(), BorderLayout.NORTH);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Category root = new Category();
        treeModel = new CategoryTreeModel(root);
        tree = new JTree(treeModel);
        tree.setEditable(true);
        tree.setRowHeight(20);
        tree.setCellRenderer(new CategoryTreeCellRenderer());
        tree.setSelectionRow(0);
        tree.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        f.add(new JScrollPane(tree));
        f.setSize(300, 600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);

    }

    public static void main(String[] args) {
        Main main = new Main();
    }

    private JToolBar createActionBar() {
        JToolBar tb = new JToolBar();
        tb.add(new AbstractAction("Add") {
            @Override
            public void actionPerformed(ActionEvent e) {
                Object o = tree.getLastSelectedPathComponent();
                treeModel.addNode(o);
                tree.expandPath(tree.getSelectionPath());
            }
        });

        tb.add(new AbstractAction("Edit") {
            @Override
            public void actionPerformed(ActionEvent e) {
                TreePath path = tree.getSelectionPath();
                tree.startEditingAtPath(path);
            }
        });

        tb.add(new AbstractAction("Remove") {
            @Override
            public void actionPerformed(ActionEvent e) {
                Object node = tree.getLastSelectedPathComponent();
                treeModel.removeNode(node);
            }
        });

        return tb;
    }
}

Das TreeModel:

package category;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

/**
 *
 * @author l-ectron-x
 */
class CategoryTreeModel implements TreeModel {

    private Set<TreeModelListener> eventListenerSet = new HashSet<>();
    private Category root;

    CategoryTreeModel(Category root) {
        this.root = root;
    }

    public void addNode(Object parent) {
        int childIndex = this.getChildCount(parent);
        Category child = null;
        Category parentCategory = (Category) parent;
        Type parentType = parentCategory.getType();
        switch (parentType) {
            case PROJECT:
                child = new Category(root, "Server_" + (childIndex + 1), parentType);
                root.add(child);
                break;
            case SERVER:
                child = new Category(parentCategory, "Part_" + (childIndex + 1), parentType);
                parentCategory.add(child);
                break;
            case PART:
                child = new Category(parentCategory, "Page_" + (childIndex + 1), parentType);
                parentCategory.add(child);
                break;
            default:
                break;
        }

        System.out.println("fireTreeNodesInserted(" + parent + ", " + this.getTreePath(parent) + ", new int[]{" + childIndex + "}, new Object[]{" + child + "})");
        fireTreeNodesInserted(parent, this.getTreePath(parent), new int[]{childIndex}, new Object[]{child});
    }

    public void removeNode(Object node) {
        Category child = (Category) node;
        Category parent = child.getParent();
        int childIndex = this.getIndexOfChild(parent, child);
        if (childIndex < 0) {
            return;
        }
        parent.remove(child);

        fireTreeNodesRemoved(parent, this.getTreePath(parent), new int[]{childIndex}, new Object[]{node});
    }

    @Override
    public Object getRoot() {
        return root;
    }

    @Override
    public Object getChild(Object parent, int index) {
        Category parentCategory = (Category) parent;
        return parentCategory.get(index);
    }

    @Override
    public int getChildCount(Object parent) {
        Category parentCategory = (Category) parent;
        return parentCategory.size();
    }

    @Override
    public boolean isLeaf(Object node) {
        return this.getChildCount(node) == 0;
    }

    @Override
    public void valueForPathChanged(TreePath path, Object newValue) {
        TreePath oldValue = path;
        Category node = (Category) path.getLastPathComponent();
        node.setName(newValue.toString());

        fireTreeNodesChanged(oldValue, path, null, null);
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        Category parentCategory = (Category) parent;
        if (parent == null || parentCategory.isEmpty() || child == null) {
            return -1;
        }
        return parentCategory.indexOf(child);
    }

    @Override
    public void addTreeModelListener(TreeModelListener l) {
        eventListenerSet.add(l);
    }

    @Override
    public void removeTreeModelListener(TreeModelListener l) {
        eventListenerSet.remove(l);
    }

    /**
     * Erzeugt aus dem übergebenen Element ein TreePath-Objekt.
     *
     * @param node das Element, von dem aus ein Pfad bis zum Wurzelknoten
     * erzeugt werden soll
     * @return das erzeugte TreePath-Objekt
     */
    public TreePath getTreePath(Object node) {
        List<Object> nodes = new ArrayList<>();
        Category category = (Category) node;

        do {
            nodes.add(category);
        } while ((category = category.getParent()) != null);

        Collections.reverse(nodes);

        return nodes.isEmpty() ? null : new TreePath(nodes.toArray());
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren.
     *
     * @param source der Elternknoten, dem neue Kindknoten hinzugefügt wurden
     * @param path der Pfad zum Wurzelknoten
     * @param childIndices die Indizes der neuen Kindknoten
     * @param children die neuen Kindknoten-Objekte
     */
    protected void fireTreeNodesInserted(Object source, TreePath path, int[] childIndices, Object[] children) {
        TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeNodesInserted(event);
        }
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren.
     *
     * @param source der Elternknoten, der für die Erzeugung des Ereignisses
     * verantwortlich ist
     * @param path der Pfad zum Wurzelknoten
     * @param childIndices die Indizes der geänderten Kindknoten
     * @param children die geänderten Kindknoten-Objekte
     */
    protected void fireTreeNodesChanged(Object source, TreePath path, int[] childIndices, Object[] children) {
        TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeNodesChanged(event);
        }
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren. The event instance is lazily created using the parameters
     * passed into the fire method.
     *
     * @param source der Elternknoten, aus dem Kindknoten entfernt wurden
     * @param path der Pfad zum Wurzelknoten
     * @param childIndices die Indizes der entfernten Kindknoten
     * @param children die entfernen Kindknoten-Objekte
     */
    protected void fireTreeNodesRemoved(Object source, TreePath path, int[] childIndices, Object[] children) {
        TreeModelEvent event = new TreeModelEvent(source, path, childIndices, children);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeNodesRemoved(event);
        }
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren. The event instance is lazily created using the parameters
     * passed into the fire method.
     *
     * @param source der Knoten, an dem sich das Baummodell geändert hat
     * @param path der Pfad zum Wurzelknoten
     */
    protected void fireTreeStructureChanged(Object source, TreePath path) {
        TreeModelEvent event = new TreeModelEvent(source, path);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeStructureChanged(event);
        }
    }

}

Der TreeCellRenderer:

package category;

import java.awt.Component;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeCellRenderer;

/**
 *
 * @author l-ectron-x
 */
class CategoryTreeCellRenderer extends DefaultTreeCellRenderer {

    @Override
    public Component getTreeCellRendererComponent(
            JTree tree,
            Object value,
            boolean selected,
            boolean expanded,
            boolean leaf,
            int row,
            boolean hasFocus) {

        super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        
        Category category = (Category) value;
        setText(category.getName());

        return this;
    }
}

Ein Hauptknoten

package category;

import static category.Type.PROJECT;
import java.util.ArrayList;

/**
 *
 * @author l-ectron-x
 */
class Category extends ArrayList<Category> {

    private Type type;
    private String name;
    private Category parent;

    Category() {
        this(null, "Neues Projekt", PROJECT);
    }

    Category(Category parent, String name, Type type) {
        this.parent = parent;
        this.type = type;
        this.name = name;
    }

    public Type getType() {
        return type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Category getParent() {
        return parent;
    }
    
    public boolean isRoot() {
        return parent == null;
    }

    @Override
    public String toString() {
        return name;
    }

}

Ein Spezialknoten:

package category;


/**
 *
 * @author l-ectron-x
 */
class Page<Image> extends Category {

    private String imagePath;

    Page() {

    }

    Page(Category parent, String name, Type type) {
        super(parent, name, type);
    }

    public void setImagePath(String path) {
        this.imagePath = path;
    }

    public String getImagePath() {
        return imagePath;
    }
}

Der Typ, der vom Spezialknoten gesammelt wird:

package category;


import java.util.Date;

/**
 *
 * @author l-ectron-x
 */
class Image {

    private String name;

    Image() {
        this.name = "Image_" + String.format("%1$tH:%1$tM:%1$tS", new Date());
    }

    public String getName() {
        return name;
    }
}

Eine Aufzählung von Knotentypen, erspart die unnötige Vererbung

package category;

/**
 *
 * @author l-ectron-x
 */
public enum Type {
    PROJECT, SERVER, PART, PAGE, IMAGE
}

OK.

:face_with_raised_eyebrow:

OK, OK, OK.

Eigentlich bin ich ja Swing-Fan. Aber um JTrees bzw. das TreeModel habe ich bisher in gewissem Sinne einen Bogen gemacht, weil ich irgendwann mal gemerkt habe: Die sind ein Krampf. Seitdem habe ich mich da mit DefaultTreeModel und DefaultMutableTreeNode über Wasser gehalten.

Jetzt habe ich gedacht: Joa, kann nicht so schwer sein. Mal schauen. Aber nachdem ich ca. 1.5 Stunden verbrannt habe, stehen „Frust“ und „Nutzen“ in einem so ungünstigen Verhältnis, dass ich erstmal nicht weitermache. (Hire me :wink: ).

Hier ist der letzte Stand. Gaaanz unten stehen Notizen. Nein, ich weiß nicht, warum der Baum verschwunden ist, und nein, ich weiß nicht, warum er jetzt den verkackten ersten Knoten nicht anzeigt (siehe Debugausgabe - da ist irgendwas auf sehr elementarer Ebene im Argen)

package bytewelt.treenodeproblem;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

/**
 *
 * @author l-ectron-x
 */
public class Main {

    private CategoryTreeModel treeModel;
    private JTree tree;

    Main() {
        JFrame f = new JFrame("JTree-Demo");
        f.add(createActionBar(), BorderLayout.NORTH);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Category root = new Category();
        treeModel = new CategoryTreeModel(root);

        treeModel.addTreeModelListener(new TreeModelListener()
        {
            
            @Override
            public void treeStructureChanged(TreeModelEvent e)
            {
                System.out.println("treeStructureChanged "+e);
            }
            
            @Override
            public void treeNodesRemoved(TreeModelEvent e)
            {
                System.out.println("treeNodesRemoved "+e);
            }
            
            @Override
            public void treeNodesInserted(TreeModelEvent e)
            {
                System.out.println("treeNodesInserted "+e);
            }
            
            @Override
            public void treeNodesChanged(TreeModelEvent e)
            {
                System.out.println("treeNodesChanged "+e);
            }
        });
        treeModel.addNode(root);
        treeModel.addNode(root);
        
        tree = new JTree(treeModel);
        System.out.println("Path 0 "+tree.getPathForRow(0));
        System.out.println("Path 1 "+tree.getPathForRow(1));
        System.out.println("Path 2 "+tree.getPathForRow(2));
        System.out.println("Bounds 0 "+tree.getPathBounds(tree.getPathForRow(0)));
        System.out.println("Bounds 1 "+tree.getPathBounds(tree.getPathForRow(1)));
        System.out.println("Bounds 2 "+tree.getPathBounds(tree.getPathForRow(2)));
        tree.updateUI();

        //tree.setEditable(true);
        //tree.setRowHeight(20);
        //tree.setCellRenderer(new CategoryTreeCellRenderer());
        tree.setSelectionRow(0);
        tree.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        f.add(new JScrollPane(tree));
        f.setSize(300, 600);
        f.setLocationRelativeTo(null);
        f.setVisible(true);

    }

    public static void main(String[] args) {
        
        
        Main main = new Main();
        
        
    }

    private JToolBar createActionBar() {
        JToolBar tb = new JToolBar();
        tb.add(new AbstractAction("Add") {
            @Override
            public void actionPerformed(ActionEvent e) {
                Object o = tree.getLastSelectedPathComponent();
                treeModel.addNode(o);
                tree.expandPath(tree.getSelectionPath());
            }
        });

        tb.add(new AbstractAction("Edit") {
            @Override
            public void actionPerformed(ActionEvent e) {
                TreePath path = tree.getSelectionPath();
                tree.startEditingAtPath(path);
            }
        });

        tb.add(new AbstractAction("Remove") {
            @Override
            public void actionPerformed(ActionEvent e) {
                Object node = tree.getLastSelectedPathComponent();
                treeModel.removeNode(node);
            }
        });

        return tb;
    }
}


/**
 *
 * @author l-ectron-x
 */


class CategoryTreeModel implements TreeModel {

    private Set<TreeModelListener> eventListenerSet = new HashSet<>();
    private Category root;

    CategoryTreeModel(Category root) {
        this.root = root;
    }

    public void addNode(Object parent) {
        int childIndex = this.getChildCount(parent);
        Category child = null;
        Category parentCategory = (Category) parent;
        Type parentType = parentCategory.getType();
        switch (parentType) {
            case PROJECT:
                child = new Category(root, "Server_" + (childIndex + 1), Type.SERVER);
                root.add(child);
                break;
            case SERVER:
                child = new Category(parentCategory, "Part_" + (childIndex + 1), Type.PART);
                parentCategory.add(child);
                break;
            case PART:
                child = new Category(parentCategory, "Page_" + (childIndex + 1), Type.IMAGE);
                parentCategory.add(child);
                break;
            default:
                System.out.println("--------------- Invalid parent");
                break;
        }

        //System.out.println("fireTreeNodesInserted(" + this.getTreePath(parent) + ", new int[]{" + childIndex + "}, new Object[]{" + child + "})");
        //fireTreeNodesInserted(this.getTreePath(parent), new int[]{childIndex}, new Object[]{child});

        fireTreeStructureChanged(getTreePath(root));
        
        Categories.print(root);
    }

    public void removeNode(Object node) {
        Category child = (Category) node;
        Category parent = child.getParent();
        int childIndex = this.getIndexOfChild(parent, child);
        if (childIndex < 0) {
            return;
        }
        parent.remove(child);

        fireTreeNodesRemoved(this.getTreePath(parent), new int[]{childIndex}, new Object[]{node});
    }

    @Override
    public Object getRoot() {
        //System.out.println("Return root "+root);
        return root;
    }

    @Override
    public Object getChild(Object parent, int index) {
        Category parentCategory = (Category) parent;
        Category child = parentCategory.get(index);
        System.out.println("Child "+index+" of "+parent+" is "+child);
        return child;
    }

    @Override
    public int getChildCount(Object parent) {
        Category parentCategory = (Category) parent;
        System.out.print("Get child count of "+parent);
        System.out.println(" is "+parentCategory.size());
        return parentCategory.size();
    }

    @Override
    public boolean isLeaf(Object node) {
        return this.getChildCount(node) == 0;
    }

    @Override
    public void valueForPathChanged(TreePath path, Object newValue) {
        Category node = (Category) path.getLastPathComponent();
        node.setName(newValue.toString());
        fireTreeNodesChanged(path, null, null);
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        Category parentCategory = (Category) parent;
        if (parent == null || parentCategory.isEmpty() || child == null) {
            return -1;
        }
        int result = parentCategory.indexOf(child);
        System.out.println("Index of "+child+" in "+parent+" is "+result);
        return result;
    }

    @Override
    public void addTreeModelListener(TreeModelListener l) {
        eventListenerSet.add(l);
    }

    @Override
    public void removeTreeModelListener(TreeModelListener l) {
        eventListenerSet.remove(l);
    }

    /**
     * Erzeugt aus dem übergebenen Element ein TreePath-Objekt.
     *
     * @param node das Element, von dem aus ein Pfad bis zum Wurzelknoten
     * erzeugt werden soll
     * @return das erzeugte TreePath-Objekt
     */
    public TreePath getTreePath(Object node) {
        List<Object> nodes = new ArrayList<>();
        Category category = (Category) node;

        do {
            nodes.add(category);
        } while ((category = category.getParent()) != null);

        Collections.reverse(nodes);

        return nodes.isEmpty() ? null : new TreePath(nodes.toArray());
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren.
     *
     * @param source der Elternknoten, dem neue Kindknoten hinzugefügt wurden
     * @param path der Pfad zum Wurzelknoten
     * @param childIndices die Indizes der neuen Kindknoten
     * @param children die neuen Kindknoten-Objekte
     */
    protected void fireTreeNodesInserted(TreePath path, int[] childIndices, Object[] children) {
        TreeModelEvent event = new TreeModelEvent(this, path, childIndices, children);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeNodesInserted(event);
        }
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren.
     *
     * @param source der Elternknoten, der für die Erzeugung des Ereignisses
     * verantwortlich ist
     * @param path der Pfad zum Wurzelknoten
     * @param childIndices die Indizes der geänderten Kindknoten
     * @param children die geänderten Kindknoten-Objekte
     */
    protected void fireTreeNodesChanged(TreePath path, int[] childIndices, Object[] children) {
        TreeModelEvent event = new TreeModelEvent(this, path, childIndices, children);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeNodesChanged(event);
        }
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren. The event instance is lazily created using the parameters
     * passed into the fire method.
     *
     * @param source der Elternknoten, aus dem Kindknoten entfernt wurden
     * @param path der Pfad zum Wurzelknoten
     * @param childIndices die Indizes der entfernten Kindknoten
     * @param children die entfernen Kindknoten-Objekte
     */
    protected void fireTreeNodesRemoved(TreePath path, int[] childIndices, Object[] children) {
        TreeModelEvent event = new TreeModelEvent(this, path, childIndices, children);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeNodesRemoved(event);
        }
    }

    /**
     * Informiert alle registrierten Listener, die sich für diesen Ereignistyp
     * interessieren. The event instance is lazily created using the parameters
     * passed into the fire method.
     *
     * @param source der Knoten, an dem sich das Baummodell geändert hat
     * @param path der Pfad zum Wurzelknoten
     */
    protected void fireTreeStructureChanged(TreePath path) {
        TreeModelEvent event = new TreeModelEvent(this, path);
        Iterator<TreeModelListener> it = eventListenerSet.iterator();
        while (it.hasNext()) {
            it.next().treeStructureChanged(event);
        }
    }

}



/**
 *
 * @author l-ectron-x
 */
class CategoryTreeCellRenderer extends DefaultTreeCellRenderer {

    @Override
    public Component getTreeCellRendererComponent(
            JTree tree,
            Object value,
            boolean selected,
            boolean expanded,
            boolean leaf,
            int row,
            boolean hasFocus) {

        super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        
        Category category = (Category) value;
        setText(category.getName());

        return this;
    }
}



/**
 *
 * @author l-ectron-x
 */
class Category extends ArrayList<Category> {

    private Type type;
    private String name;
    private Category parent;

    Category() {
        this(null, "Neues Projekt", Type.PROJECT);
    }

    Category(Category parent, String name, Type type) {
        this.parent = parent;
        this.type = type;
        this.name = name;
    }

    public Type getType() {
        return type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Category getParent() {
        return parent;
    }
    
    public boolean isRoot() {
        return parent == null;
    }

    @Override
    public String toString() {
        return name;
    }

}


/**
*
* @author l-ectron-x
*/
class Page<Image> extends Category {

   private String imagePath;

   Page() {

   }

   Page(Category parent, String name, Type type) {
       super(parent, name, type);
   }

   public void setImagePath(String path) {
       this.imagePath = path;
   }

   public String getImagePath() {
       return imagePath;
   }
}


/**
 *
 * @author l-ectron-x
 */
class Image {

    private String name;

    Image() {
        this.name = "Image_" + String.format("%1$tH:%1$tM:%1$tS", new Date());
    }

    public String getName() {
        return name;
    }
}

/**
*
* @author l-ectron-x
*/
enum Type {
   PROJECT, SERVER, PART, PAGE, IMAGE
}



















// Notes:
// The first argument of events and fire* methods should be the model
// The treePath that is passed to the event should in some cases be the path 
//   from the node to the root (not from the parent to the root). See javadoc.
// The class "Page<Image>" certainly does not make sense 
// In the lines 
//   child = new Category(root, "Server_" + (childIndex + 1), parentType);
// the last arg should probably the CHILD type!?
// In addNode, just fire fireTreeStructureChanged(getTreePath(parent))
//  (but the expansion and selection will cause headaches...)
class Categories
{
    public static void print(Object object)
    {
        System.out.println(createString(object));
    }

    private static String createString(Object object)
    {
        return createString(object, "");
    }
    
    private static String createString(Object object, String indent)
    {
        if (!(object instanceof Category))
        {
            return indent + "Not a category: " + object;
        }
        Category category = (Category)object;
        
        StringBuilder sb = new StringBuilder();
        sb.append(indent + "Category: " + category + ", " + category.getType() + "\n");
        for (Category child : category)
        {
            sb.append(createString(child, indent+"  "));
        }
        return sb.toString();
    }
}

Nebenbei: Von ArrayList zu erben halte ich für keine gute Idee. Alternativen wären

  • Die List als field, und per Collections.unmodifiableList(children) rausgeben
  • Die wenigen (für das TreeModel) benötigten Methoden (im wesentlichen ja getNumChildren, getChild, addChild, removeChild) direkt anbieten und intern mit der Liste implementieren.

Ansonsten werde ich wohl irgendwann mal ein UI für GitHub - javagl/Category: A simple model for hierarchical categories bauen, und zusehen, dass ich (im Zuge dessen) ein AbstractTreeModel implementiere, das dann vielleicht in der nähe von CommonUI/JTrees.java at master · javagl/CommonUI · GitHub landet, und bei dem die Sache mit den TreeModelEvents einmal sauber so geregelt ist, dass man sich nicht immer wieder selbst darum kümmern muss.

Dem möchte ich mich anschließen, das war auch nur Faulheit zwecks Demonstration von mir. Korrekter wäre es ein weiteres Interface ElementCollection extends Collection zu kreieren und darin die entsprechenden Methoden auszuführen, die man benötigt. Als was man die List dann konkret implementiert, ist damit offen, das Problem aber, dass nur eines von beiden Collection, Iterable u.Ä. beerben darf, besteht aber weiter.

Beim Adden holst du dir btw. die Node, in welche geaddet werden soll und addest dann genau diese und keine neue.

treeModel.addNode(new Category(o, name, type));

müsste es heißen.

Ne, da hast du dir den Code nicht genau angesehen. Bei addNode wird einiges gemacht und daher ist der Aufruf so wie er ist richtig.

Was mir gerade aufgefallen ist, wenn du nicht von ArrayList erbst sondern in Category eine Liste der Kinder führst und dann alle fehlenden Methoden implementierst funktioniert alles.

Also mit dieser Category funktioniert es wie erwartet:

package category;

import static category.Type.*;

import java.util.ArrayList;

/**
 *
 * @author l-ectron-x
 */
class Category{

    private Type type;
    private String name;
    private Category parent;
    
    private  ArrayList<Category> children= new ArrayList<Category>(); 

    Category() {
        this(null, "Neues Project", PROJECT);
    }

    Category(Category parent, String name, Type type) {
        this.parent = parent;
        this.type = type;
        this.name = name;
    }

    public Type getType() {
        return type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Category getParent() {
        return parent;
    }
    
    public boolean isRoot() {
        return parent == null;
    }

    @Override
    public String toString() {
        return name;
    }

	public boolean add(Category category) {
		return children.add(category);		
	}

	public void remove(Category child) {
		children.remove(child);		
	}

	public int size() {		
		return children.size();
	}

	public Category get(int index) {
		
		return children.get(index);
	}

	public int indexOf(Object child) {
		return children.indexOf(child);		
	}

public boolean isEmpty() {
		return children.isEmpty();
	}

}

Ich hab keine Idee woran es liegt, aber das kann nur an den JTRee interna liegen

Edit:
Add Node hat aber glaub ich auch noch Fehler:

public void addNode(Object parent) {
        int childIndex = this.getChildCount(parent);
        Category child = null;
        Category parentCategory = (Category) parent;
        Type parentType = parentCategory.getType();
        switch (parentType) {
            case PROJECT:
                child = new Category(root, "Server_" + (childIndex + 1), parentType); //Und auch hier muss root durch parentCategory ersetzt werden
                root.add(child);  // <- enweder hier nicht direkt zu root hinzufuegen sondern zu parentCategory, oder der Type in Zeile darueber muss auf Server geaendert werden.
                break;
            case SERVER:
                child = new Category(parentCategory, "Part_" + (childIndex + 1), parentType);
                parentCategory.add(child);
                break;
            case PART:
                child = new Category(parentCategory, "Page_" + (childIndex + 1), parentType);
                parentCategory.add(child);
                break;
            default:
                break;
        }

        System.out.println("fireTreeNodesInserted(" + parent + ", " + this.getTreePath(parent) + ", new int[]{" + childIndex + "}, new Object[]{" + child + "})");
        fireTreeNodesInserted(parent, this.getTreePath(parent), new int[]{childIndex}, new Object[]{child});
    }
1 „Gefällt mir“

Aaaaahhh …

Ich hatte zwischendrin die Vermutung, dass durch das extends ArrayList auf einmal irgendwelche Methoden vorhanden sind, die nicht vorhanden sein sollten … ein Bauchgefühl, dass irgendwo in den Tiefen von Swing etwas stehen könnte wie

void doSomethingWith(Object treeNode) {
    if (treeNode instanceof Iterable) doSomethingSpecial();
    else doTheRightThing();
}

Aber … es ist (mit an Sicherheit grenzender Wahrscheintlichkeit) viiiel einfacher:

public class BasicTreeUI extends TreeUI {

    /** Object responsible for handling sizing and expanded issues. */
    protected AbstractLayoutCache  treeState;
    ....
    /** Used for minimizing the drawing of vertical lines. */
    protected Hashtable<TreePath,Boolean> drawingCache;

    ...
}    

public class FixedHeightLayoutCache extends AbstractLayoutCache {
...
    /**
     * Maps from TreePath to a FHTreeStateNode.
     */
    private Hashtable<TreePath, FHTreeStateNode> treePathMapping;
}

Und das in Kombination mit

public class TreePath extends Object implements Serializable {
    ....
    public int hashCode() {
        return getLastPathComponent().hashCode();
    }
}

ist sicher eine der Hauptursachen des Problems: In den Moment, wo in diese ArrayList ein neues Element eingefügt wird, ändert sich ihr hashCode, und damit auch der hashCode von jedem TreePath, wo sie drin ist, und damit haut’s alles komplett raus.

Manchmal ist man blind. Aber vielleicht hat das zur Folge, dass man, wenn man in Zukunft wieder mal ein mulmiges Gefühl hat, wenn man sieht, dass jemand von sowas wie ArrayList erbt, einen driftigen Grund nennen kann, warum das eine schlechte Idee ist.

1 „Gefällt mir“

es ist nicht nur der geänderte HashCode, sondern zwei leere Listen sind auch equal. D.h sobald ein node zwei Kinder hat, sind beide Kinder equal.

Also ein weiterer Grund nicht von ArrazList zu erben. oder zumindest hashCode und equal ueberschreiben.

@Override
	public int hashCode() {
		final int prime = 31;
		int result =  name.hashCode();
		result = prime * result + ((parent == null) ? 0 : parent.hashCode());
		result = prime * result + ((type == null) ? 0 : type.hashCode());
		return result;
	}



	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (!super.equals(obj))
			return false;
		if (getClass() != obj.getClass())
			return false;
		Category other = (Category) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (parent == null) {
			if (other.parent != null)
				return false;
		} else if (!parent.equals(other.parent))
			return false;
		if (type != other.type)
			return false;
		return true;
	}

Die Fehler in der addNode, die ich oben erwähnt habe, müssen aber auch gefixt werden.

Hihi, du hast anscheinend das falsche Kezboard-Lazout :clown_face:

Sicher, equals und hashCode gehen Hand in Hand, darum ist der Hinweis auf hashCode nur ein Teilaspekt. Zu versuchen, das durch eigene Implementierungen zu umgehen, wäre aber kein Workaround sondern ein Würgaround. Das eigentliche Problem ist schon das Erben von ArrayList.

Leeuute! Vielen Dank, dass Ihr Euch die Zeit genommen und Euch das Problem mal angesehen habt! Ihr habt mich vor dem Wahnsinn gerettet. :sweat_smile:
Ich bastle seit 2 Wochen in meiner Freizeit an dem Kram (will immer gerne alle alleine machen), hatte 4 verschiedene Varianten im Editor und war nun mit meinem Latain am Ende.
Schön auch Eure Ausführungen zu lesen, habe dabei einiges gelernt. Toll!

@AmunRa, muss ich das auch ändern, wenn ich eigentlich nur ein Projekt im JTree haben möchte? Warum ist es, wie ich das gemacht habe, ein Fehler?

Edit: Habe es eben mitbekommen, Stimmt, du hast in beiden Punkten recht. Kümmere ich mich drum. Danke!

Sieht nun so aus und funktioniert tadellos.

    public void addNode(Object parent) {
        int childIndex = this.getChildCount(parent);
        Category child = null;
        Category parentCategory = (Category) parent;
        Type parentType = parentCategory.getType();
        switch (parentType) {
            case PROJECT:
                child = new Category(parentCategory, "Server_" + (childIndex + 1), Type.SERVER);
                parentCategory.add(child);
                break;
            case SERVER:
                child = new Category(parentCategory, "Part_" + (childIndex + 1), Type.PART);
                parentCategory.add(child);
                break;
            case PART:
                child = new Category(parentCategory, "Page_" + (childIndex + 1), Type.PAGE);
                parentCategory.add(child);
                break;
            default:
                break;
        }

        System.out.println("fireTreeNodesInserted(" + parent + ", " + this.getTreePath(parent) + ", new int[]{" + childIndex + "}, new Object[]{" + child + "})");
        fireTreeNodesInserted(parent, this.getTreePath(parent), new int[]{childIndex}, new Object[]{child});
    }

Oh, okay, das habe ich übersehen.