Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX TreeView of multiple object types? (and more)

I currently have the following object data structure:

Item

  • String name
  • ArrayList of information

Character

  • String name
  • Collection of Item

Account

  • String name
  • Collection of Character (up to 8 max)

I want to make a TreeView that looks like the following:

Root(invisible)
======Jake(Account)
============JakesChar(Character)
==================Amazing Sword(Item)
==================Broken Bow(Item)
==================Junk Metal(Item)
======Mark(Account)
============myChar(Character)
==================Godly Axe(Item)
======FreshAcc(Account)
======MarksAltAcc(Account)
============IllLvlThisIPromise(Character)
======Jeffrey(Account)
============Jeff(Character)
==================Super Gun(Item)
==================Better Super Gun(Item)
==================Super Gun Scope(Item)

I made all those names up and such, obviously the real implementation would be a lot more complex. How can this be done? The TreeItem requires each TreeItem to be the same type as its' parent.

The ONLY solution I have is to do the following:

public class ObjectPointer
{
    Object pointer;
    String name;
}

My TreeView would be of type ObjectPointer and on each row I would cast the ObjectPointer to Account, Character, or Item. This is AWFUL but I think it would work.

Sub Questions:

  • How do I get TreeItem(s) to detect setOnMouseHover events?

  • How do I get TreeItem(s) to not use the toString method of their type and instead a custom way of displaying the String property that they need?

  • How do I get the TreeItem(s) to display colored text in the GUI instead of plain text?

Thank you!

like image 493
Hatefiend Avatar asked Jan 26 '16 08:01

Hatefiend


1 Answers

If you look at your model and think generically, all the classes have a degree of similarity, which you could factor out into a superclass:

package model;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public abstract class GameObject<T extends GameObject<?>> {


    public GameObject(String name) {
        setName(name);
    }

    private final StringProperty name = new SimpleStringProperty();

    public final StringProperty nameProperty() {
        return this.name;
    }


    public final String getName() {
        return this.nameProperty().get();
    }


    public final void setName(final String name) {
        this.nameProperty().set(name);
    }

    private final ObservableList<T> items = FXCollections.observableArrayList();

    public ObservableList<T> getItems() {
        return items ;
    }

    public abstract void createAndAddChild(String name);
}

The type parameter T here represents the type of "child" objects. So your Account class (whose child type is GameCharacter - don't name classes the same as anything in java.lang, btw...) looks like

package model;

public class Account extends GameObject<GameCharacter> {

    public Account(String name) {
        super(name);
    }

    @Override
    public void createAndAddChild(String name) {
        getItems().add(new GameCharacter(name));
    }

}

and similarly all the way down the hierarchy. I'd define an Information class (even though it just has a name) to make everything fit the structure, so:

package model;

public class Item extends GameObject<Information> {

    public Item(String name) {
        super(name);
    }

    @Override
    public void createAndAddChild(String name) {
        getItems().add(new Information(name));
    }

}

and, since Information has no children, its child list is just an empty list:

package model;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class Information extends GameObject<GameObject<?>> {

    public Information(String name) {
        super(name);
    }

    @Override
    public ObservableList<GameObject<?>> getItems() {
        return FXCollections.emptyObservableList();
    }

    @Override
    public void createAndAddChild(String name) {
        throw new IllegalStateException("Information has no child items");
    }

}

Now every item in your tree is a GameObject<?>, so you can basically build a TreeView<GameObject<?>>. The tricky part is that your tree items need to reflect the structure already built in the model. Since you have observable lists there, you can do this with listeners on the lists.

You can use a cell factory on the tree to customize the appearance of the cells displaying the TreeItems. If you want a different appearance for each type of item, I'd recommend defining the styles in an external CSS class, and setting a CSS PseudoClass on the cell corresponding to the type of item. If you use some naming convention (I have that the pseudo-class name is the lower case version of the class name), it can be quite slick to do that. Here's a fairly simple example:

package ui;

import static java.util.stream.Collectors.toList;

import java.util.Arrays;
import java.util.List;

import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item; 

public class Tree {

    private final TreeView<GameObject<?>> treeView ;

    private final List<Class<? extends GameObject<?>>> itemTypes = Arrays.asList(
         Account.class, GameCharacter.class, Item.class, Information.class
    );

    public Tree(ObservableList<Account> accounts) {
        treeView = new TreeView<>();

        GameObject<?> root = new GameObject<Account>("") {

            @Override
            public ObservableList<Account> getItems() {
                return accounts ;
            }

            @Override
            public void createAndAddChild(String name) {
                getItems().add(new Account(name));
            }

        };

        TreeItem<GameObject<?>> treeRoot = createItem(root);

        treeView.setRoot(treeRoot);
        treeView.setShowRoot(false);

        treeView.setCellFactory(tv -> {

            TreeCell<GameObject<?>> cell = new TreeCell<GameObject<?>>() {

                @Override
                protected void updateItem(GameObject<?> item, boolean empty) {
                    super.updateItem(item, empty);
                    textProperty().unbind();
                    if (empty) {
                        setText(null);
                        itemTypes.stream().map(Tree.this::asPseudoClass)
                            .forEach(pc -> pseudoClassStateChanged(pc, false));
                    } else {
                        textProperty().bind(item.nameProperty());
                        PseudoClass itemPC = asPseudoClass(item.getClass());
                        itemTypes.stream().map(Tree.this::asPseudoClass)
                            .forEach(pc -> pseudoClassStateChanged(pc, itemPC.equals(pc)));
                    }
                }
            };

            cell.hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
                if (isNowHovered && (! cell.isEmpty())) {
                    System.out.println("Mouse hover on "+cell.getItem().getName());
                }
            });

            return cell ;
        }
    }

    public TreeView<GameObject<?>> getTreeView() {
        return treeView ;
    }

    private TreeItem<GameObject<?>> createItem(GameObject<?> object) {

        // create tree item with children from game object's list:

        TreeItem<GameObject<?>> item = new TreeItem<>(object);
        item.setExpanded(true);
        item.getChildren().addAll(object.getItems().stream().map(this::createItem).collect(toList()));

        // update tree item's children list if game object's list changes:

        object.getItems().addListener((Change<? extends GameObject<?>> c) -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    item.getChildren().addAll(c.getAddedSubList().stream().map(this::createItem).collect(toList()));
                }
                if (c.wasRemoved()) {
                    item.getChildren().removeIf(treeItem -> c.getRemoved().contains(treeItem.getValue()));
                }
            }
        });

        return item ;
    }

    private PseudoClass asPseudoClass(Class<?> clz) {
        return PseudoClass.getPseudoClass(clz.getSimpleName().toLowerCase());
    }

}

Quick test, which works, but note you probably need to test more of the functionality:

package application;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import model.Account;
import model.GameCharacter;
import model.GameObject;
import model.Information;
import model.Item;
import ui.Tree;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        Tree tree = new Tree(createAccounts());
        TreeView<GameObject<?>> treeView = tree.getTreeView();

        TextField addField = new TextField();
        Button addButton = new Button("Add");
        EventHandler<ActionEvent> addHandler = e -> {
            TreeItem<GameObject<?>> selected = treeView
                .getSelectionModel()
                .getSelectedItem();
            if (selected != null) {
                selected.getValue().createAndAddChild(addField.getText());
                addField.clear();
            }
        };
        addField.setOnAction(addHandler);
        addButton.setOnAction(addHandler);
        addButton.disableProperty().bind(Bindings.createBooleanBinding(() -> {
            TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
            return selected == null || selected.getValue() instanceof Information ;
        }, treeView.getSelectionModel().selectedItemProperty()));

        Button deleteButton = new Button("Delete");
        deleteButton.setOnAction(e -> {
            TreeItem<GameObject<?>> selected = treeView.getSelectionModel().getSelectedItem() ;
            TreeItem<GameObject<?>> parent = selected.getParent() ;
            parent.getValue().getItems().remove(selected.getValue());
        });
        deleteButton.disableProperty().bind(treeView.getSelectionModel().selectedItemProperty().isNull());

        HBox controls = new HBox(5, addField, addButton, deleteButton);
        controls.setPadding(new Insets(5));
        controls.setAlignment(Pos.CENTER);

        BorderPane root = new BorderPane(treeView);
        root.setBottom(controls);

        Scene scene = new Scene(root, 600, 600);
        scene.getStylesheets().add(getClass().getResource("/ui/style/style.css").toExternalForm());
        primaryStage.setScene(scene);

        primaryStage.show();
    }   

    public static void main(String[] args) {
        launch(args);
    }

    private ObservableList<Account> createAccounts() {
        Account jake = new Account("Jake");
        Account mark = new Account("Mark");
        Account freshAcc = new Account("Fresh Account");
        Account marksAltAcc = new Account("Mark's alternative account");
        Account jeffrey = new Account("Jeffrey");

        GameCharacter jakesChar = new GameCharacter("Jakes character");
        Item amazingSword = new Item("Amazing Sword");
        Item brokenBow = new Item("Broken Bow");
        Item junkMetal = new Item("Junk Metal");

        GameCharacter myChar = new GameCharacter("Me");
        Item godlyAxe = new Item("Godly Axe");

        GameCharacter level = new GameCharacter("I'll level this I promise");

        GameCharacter jeff = new GameCharacter("Jeff");
        Item superGun = new Item("Super Gun");
        Item superGunScope = new Item("Super Gun Scope");

        jake.getItems().add(jakesChar);
        mark.getItems().add(myChar);
        marksAltAcc.getItems().add(level);
        jeffrey.getItems().add(jeff);

        jakesChar.getItems().addAll(amazingSword, brokenBow, junkMetal);
        myChar.getItems().add(godlyAxe);
        jeff.getItems().addAll(superGun, superGunScope);

        return FXCollections.observableArrayList(jake, mark, freshAcc, marksAltAcc, jeffrey);

    }

}

and the CSS as an example:

.tree-cell, .tree-cell:hover:empty {
    -fx-background-color: -fx-background ;
    -fx-background: -fx-control-inner-background ;
}

.tree-cell:hover {
    -fx-background-color: crimson, -fx-background ;
    -fx-background-insets: 0, 1;
}

.tree-cell:account {
    -fx-background: lightsalmon ;
}
.tree-cell:gamecharacter {
    -fx-background: bisque ;
}
.tree-cell:item {
    -fx-background: antiquewhite ;
}

.tree-cell:selected {
    -fx-background: crimson ;
}
like image 199
James_D Avatar answered Nov 03 '22 12:11

James_D