Added ability to render the clusters in a tree and have per-item context menus.

This commit is contained in:
Andrew Lalis 2021-05-29 10:43:35 +02:00
parent 2e78b95588
commit 4d40431bcd
13 changed files with 199 additions and 27 deletions

View File

@ -29,7 +29,6 @@ public class CrystalKeep extends Application {
URL url = CrystalKeep.class.getClassLoader().getResource("ui/crystalkeep.fxml");
FXMLLoader loader = new FXMLLoader(url);
Model model = new Model();
loader.setController(new MainViewController(model));
var scene = new Scene(loader.load());
stage.setScene(scene);
stage.setTitle("CrystalKeep");
@ -43,14 +42,20 @@ public class CrystalKeep extends Application {
} catch (IOException e) {
rootCluster = new Cluster("Root");
rootCluster.addShard(new TextShard(rootCluster, "Example Shard", LocalDateTime.now(), "Hello world!"));
rootCluster.addShard(new LoginCredentialsShard(rootCluster, "Netflix", LocalDateTime.now(), "user", "secret password"));
for (int i = 0; i < 100; i++) {
rootCluster.addShard(new TextShard(rootCluster, "test " + i, LocalDateTime.now(), "value: " + i));
}
clusterLoader.saveDefault(rootCluster);
System.out.println("Saved root cluster on first load.");
}
model.setActiveCluster(rootCluster);
System.out.println(rootCluster);
stage.show();
MainViewController controller = loader.getController();
controller.init(model);
System.out.println(rootCluster);
}
public static void test() throws IOException {

View File

@ -2,16 +2,48 @@ package nl.andrewlalis.crystalkeep.control;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import nl.andrewlalis.crystalkeep.model.Model;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.VBox;
import nl.andrewlalis.crystalkeep.model.*;
import nl.andrewlalis.crystalkeep.model.serialization.ClusterLoader;
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
import nl.andrewlalis.crystalkeep.model.shards.TextShard;
import nl.andrewlalis.crystalkeep.view.ClusterTreeItem;
import nl.andrewlalis.crystalkeep.view.CrystalItemTreeCell;
import nl.andrewlalis.crystalkeep.view.ShardTreeItem;
import java.io.IOException;
public class MainViewController {
private final Model model;
public class MainViewController implements ModelListener {
private Model model;
public MainViewController(Model model) {
@FXML
public TreeView<CrystalItem> clusterTreeView;
@FXML
public VBox shardDetailContainer;
public void init(Model model) {
this.model = model;
this.model.addListener(this);
this.activeClusterUpdated();
assert(this.clusterTreeView != null);
this.clusterTreeView.setCellFactory(param -> new CrystalItemTreeCell());
this.clusterTreeView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
shardDetailContainer.getChildren().clear();
if (newValue instanceof ShardTreeItem) {
var node = (ShardTreeItem) newValue;
System.out.println(node.getShard());
if (node.getShard() instanceof TextShard) {
shardDetailContainer.getChildren().add(new TextArea(((TextShard) node.getShard()).getText()));
} else if (node.getShard() instanceof LoginCredentialsShard) {
shardDetailContainer.getChildren().add(new Label("Username: " + ((LoginCredentialsShard) node.getShard()).getUsername()));
shardDetailContainer.getChildren().add(new Label("Password: " + ((LoginCredentialsShard) node.getShard()).getPassword()));
}
}
});
}
@FXML
@ -23,4 +55,23 @@ public class MainViewController {
e.printStackTrace();
}
}
@Override
public void activeClusterUpdated() {
if (this.model.getActiveCluster() == null) return;
TreeItem<CrystalItem> root = this.createNode(this.model.getActiveCluster());
this.clusterTreeView.setRoot(root);
this.clusterTreeView.setShowRoot(true);
}
private TreeItem<CrystalItem> createNode(Cluster cluster) {
ClusterTreeItem node = new ClusterTreeItem(cluster);
for (Cluster child : cluster.getClustersOrdered()) {
node.getChildren().add(this.createNode(child));
}
for (Shard shard : cluster.getShardsOrdered()) {
node.getChildren().add(new ShardTreeItem(shard));
}
return node;
}
}

View File

@ -1,4 +0,0 @@
package nl.andrewlalis.crystalkeep.control;
public class ShardDetailController {
}

View File

@ -3,12 +3,10 @@ package nl.andrewlalis.crystalkeep.model;
import lombok.Getter;
import lombok.Setter;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.*;
@Getter
public class Cluster implements Comparable<Cluster> {
public class Cluster implements Comparable<Cluster>, CrystalItem {
@Setter
private String name;
private final Set<Shard> shards;
@ -37,6 +35,18 @@ public class Cluster implements Comparable<Cluster> {
cluster.setParent(this);
}
public List<Cluster> getClustersOrdered() {
List<Cluster> clusters = new ArrayList<>(this.getClusters());
clusters.sort(Comparator.naturalOrder());
return clusters;
}
public List<Shard> getShardsOrdered() {
List<Shard> shards = new ArrayList<>(this.getShards());
shards.sort(Comparator.naturalOrder());
return shards;
}
@Override
public int compareTo(Cluster o) {
return this.getName().compareTo(o.getName());
@ -68,4 +78,9 @@ public class Cluster implements Comparable<Cluster> {
}
return sb.toString();
}
@Override
public String getIconPath() {
return "ui/images/cluster_node_icon.png";
}
}

View File

@ -0,0 +1,10 @@
package nl.andrewlalis.crystalkeep.model;
/**
* Unifying interface for both clusters and shards and any other possible
* components in the hierarchy.
*/
public interface CrystalItem {
String getName();
String getIconPath();
}

View File

@ -2,11 +2,21 @@ package nl.andrewlalis.crystalkeep.model;
import lombok.Getter;
import java.util.HashSet;
import java.util.Set;
@Getter
public class Model {
private Cluster activeCluster;
private final Set<ModelListener> listeners = new HashSet<>();
public void addListener(ModelListener listener) {
this.listeners.add(listener);
}
public void setActiveCluster(Cluster activeCluster) {
this.activeCluster = activeCluster;
this.listeners.forEach(ModelListener::activeClusterUpdated);
}
}

View File

@ -0,0 +1,5 @@
package nl.andrewlalis.crystalkeep.model;
public interface ModelListener {
void activeClusterUpdated();
}

View File

@ -18,7 +18,7 @@ import java.util.Objects;
* </p>
*/
@Getter
public abstract class Shard implements Comparable<Shard> {
public abstract class Shard implements Comparable<Shard>, CrystalItem {
@Setter
private Cluster cluster;
@Setter
@ -57,4 +57,9 @@ public abstract class Shard implements Comparable<Shard> {
public String toString() {
return "Shard: name=\"" + this.name + "\", type=" + this.type + ", createdAt=" + this.createdAt;
}
@Override
public String getIconPath() {
return "ui/images/shard_node_icon.png";
}
}

View File

@ -0,0 +1,21 @@
package nl.andrewlalis.crystalkeep.view;
import javafx.scene.control.TreeItem;
import javafx.scene.image.ImageView;
import lombok.Getter;
import nl.andrewlalis.crystalkeep.model.Cluster;
import nl.andrewlalis.crystalkeep.model.CrystalItem;
public class ClusterTreeItem extends TreeItem<CrystalItem> {
@Getter
private final Cluster cluster;
public ClusterTreeItem(Cluster cluster) {
super(cluster);
this.cluster = cluster;
ImageView clusterIcon = new ImageView("ui/images/cluster_node_icon.png");
clusterIcon.setFitHeight(16);
clusterIcon.setPreserveRatio(true);
super.setGraphic(clusterIcon);
}
}

View File

@ -0,0 +1,37 @@
package nl.andrewlalis.crystalkeep.view;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.image.ImageView;
import nl.andrewlalis.crystalkeep.model.CrystalItem;
import nl.andrewlalis.crystalkeep.model.Shard;
public class CrystalItemTreeCell extends TreeCell<CrystalItem> {
private final ContextMenu contextMenu;
public CrystalItemTreeCell() {
MenuItem item = new MenuItem("Delete");
this.contextMenu = new ContextMenu(item);
}
@Override
protected void updateItem(CrystalItem item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
this.setText(item.getName());
ImageView icon = new ImageView(item.getIconPath());
icon.setFitHeight(16);
icon.setPreserveRatio(true);
this.setGraphic(icon);
if (item instanceof Shard) {
this.setContextMenu(this.contextMenu);
}
} else {
this.setText(null);
this.setGraphic(null);
this.setContextMenu(null);
}
}
}

View File

@ -0,0 +1,16 @@
package nl.andrewlalis.crystalkeep.view;
import javafx.scene.control.TreeItem;
import lombok.Getter;
import nl.andrewlalis.crystalkeep.model.CrystalItem;
import nl.andrewlalis.crystalkeep.model.Shard;
public class ShardTreeItem extends TreeItem<CrystalItem> {
@Getter
private final Shard shard;
public ShardTreeItem(Shard shard) {
super(shard);
this.shard = shard;
}
}

View File

@ -7,6 +7,9 @@
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.layout.Pane?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="nl.andrewlalis.crystalkeep.control.MainViewController">
<MenuBar>
<Menu text="File">
@ -15,10 +18,17 @@
</MenuBar>
<BorderPane>
<left>
<fx:include source="clusters_view.fxml" />
<BorderPane prefWidth="200.0">
<top>
<Label text="Clusters" BorderPane.alignment="TOP_CENTER"/>
</top>
<center>
<TreeView fx:id="clusterTreeView" prefWidth="200.0"/>
</center>
</BorderPane>
</left>
<center>
<fx:include source="shard_detail.fxml" />
<VBox fx:id="shardDetailContainer" minWidth="600" />
</center>
</BorderPane>
</VBox>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="nl.andrewlalis.crystalkeep.control.ShardDetailController"
prefHeight="400.0" prefWidth="600.0">
</AnchorPane>