diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 797dab6..c469dfc 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,6 +2,7 @@ module crystalkeep { requires javafx.fxml; requires javafx.controls; + opens nl.andrewlalis.crystalkeep.control; opens nl.andrewlalis.crystalkeep; exports nl.andrewlalis.crystalkeep.control to javafx.fxml; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java index fdcb416..0e8e845 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/CrystalKeep.java @@ -3,6 +3,7 @@ package nl.andrewlalis.crystalkeep; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; +import javafx.scene.control.Alert; import javafx.stage.Stage; import nl.andrewlalis.crystalkeep.control.MainViewController; import nl.andrewlalis.crystalkeep.model.Cluster; @@ -13,10 +14,12 @@ import nl.andrewlalis.crystalkeep.model.shards.TextShard; import java.io.IOException; import java.net.URL; +import java.security.GeneralSecurityException; import java.time.LocalDateTime; +import java.util.Optional; public class CrystalKeep extends Application { - public static void main(String[] args) { + public static void main(String[] args) throws GeneralSecurityException { launch(args); } @@ -29,7 +32,7 @@ public class CrystalKeep extends Application { stage.setScene(scene); stage.setTitle("CrystalKeep"); stage.sizeToScene(); - model.setActiveCluster(this.loadRootCluster()); + model.setActiveCluster(null); stage.show(); @@ -37,22 +40,33 @@ public class CrystalKeep extends Application { controller.init(model); } - private Cluster loadRootCluster() throws IOException { - ClusterLoader clusterLoader = new ClusterLoader(); - Cluster rootCluster; - try { - rootCluster = clusterLoader.loadDefault(); - System.out.println("Loaded existing root cluster."); - } catch (IOException e) { - rootCluster = new Cluster("Root"); - rootCluster.addShard(new TextShard("Example Shard", LocalDateTime.now(), "Hello world!")); - rootCluster.addShard(new LoginCredentialsShard("Netflix", LocalDateTime.now(), "user", "secret password")); - for (int i = 0; i < 100; i++) { - rootCluster.addShard(new TextShard("test " + i, LocalDateTime.now(), "value: " + i)); - } - clusterLoader.saveDefault(rootCluster); - System.out.println("Saved root cluster on first load."); - } - return rootCluster; - } +// private Cluster loadRootCluster() throws IOException, GeneralSecurityException { +// ClusterLoader clusterLoader = new ClusterLoader(); +// Cluster rootCluster; +// +//// Optional oc = clusterLoader.load(ClusterLoader.DEFAULT_CLUSTER); +//// if (oc.isEmpty()) { +//// new Alert(Alert.AlertType.ERROR, "Could not load cluster.").show(); +//// System.exit(1); +//// } +//// return oc.get(); +// +//// try { +//// long start = System.currentTimeMillis(); +//// rootCluster = clusterLoader.load(ClusterLoader.DEFAULT_CLUSTER); +//// long dur = System.currentTimeMillis() - start; +//// System.out.println("Loaded existing root cluster in " + dur + " ms."); +//// } catch (Exception e) { +//// e.printStackTrace(); +//// rootCluster = new Cluster("Root"); +//// rootCluster.addShard(new TextShard("Example Shard", LocalDateTime.now(), "Hello world!")); +//// rootCluster.addShard(new LoginCredentialsShard("Netflix", LocalDateTime.now(), "user", "secret password")); +//// for (int i = 0; i < 100; i++) { +//// rootCluster.addShard(new TextShard("test " + i, LocalDateTime.now(), "value: " + i)); +//// } +//// clusterLoader.save(rootCluster, ClusterLoader.DEFAULT_CLUSTER, "test"); +//// System.out.println("Saved root cluster on first load."); +//// } +//// return rootCluster; +// } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/AddClusterHandler.java b/src/main/java/nl/andrewlalis/crystalkeep/control/AddClusterHandler.java new file mode 100644 index 0000000..f481c81 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/AddClusterHandler.java @@ -0,0 +1,31 @@ +package nl.andrewlalis.crystalkeep.control; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Dialog; +import javafx.scene.control.TextInputDialog; +import nl.andrewlalis.crystalkeep.model.Cluster; +import nl.andrewlalis.crystalkeep.model.Model; + +public class AddClusterHandler implements EventHandler { + private final Cluster cluster; + private final Model model; + + public AddClusterHandler(Cluster cluster, Model model) { + this.cluster = cluster; + this.model = model; + } + + @Override + public void handle(ActionEvent event) { + Dialog d = new TextInputDialog(); + d.setContentText("Enter the name of the new cluster."); + d.setHeaderText(null); + d.setTitle("Add Cluster"); + d.setGraphic(null); + d.showAndWait().ifPresent(s -> { + cluster.addCluster(new Cluster(s.trim())); + model.notifyListeners(); + }); + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java b/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java new file mode 100644 index 0000000..a1f0164 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/AddShardHandler.java @@ -0,0 +1,56 @@ +package nl.andrewlalis.crystalkeep.control; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.ChoiceDialog; +import javafx.scene.control.Dialog; +import javafx.scene.control.TextInputDialog; +import nl.andrewlalis.crystalkeep.model.Cluster; +import nl.andrewlalis.crystalkeep.model.Model; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.ShardType; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class AddShardHandler implements EventHandler { + private final Cluster cluster; + private final Model model; + + public AddShardHandler(Cluster cluster, Model model) { + this.cluster = cluster; + this.model = model; + } + + @Override + public void handle(ActionEvent event) { + Dialog d = new TextInputDialog(); + d.setContentText("Enter the name of the new shard."); + d.showAndWait().ifPresent(s -> { + List choices = Arrays.stream(ShardType.values()) + .map(Enum::name) + .collect(Collectors.toList()); + Dialog d1 = new ChoiceDialog<>("TEXT", choices); + d1.setContentText("Choose the type of shard to create."); + d1.showAndWait().ifPresent(typeName -> { + ShardType type = ShardType.valueOf(typeName.toUpperCase()); + Shard shard; + switch (type) { + case TEXT: + shard = new TextShard(s); + break; + case LOGIN_CREDENTIALS: + shard = new LoginCredentialsShard(s); + break; + default: + throw new IllegalStateException("Invalid shard type selected."); + } + cluster.addShard(shard); + model.notifyListeners(); + }); + }); + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java new file mode 100644 index 0000000..505c8b3 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java @@ -0,0 +1,46 @@ +package nl.andrewlalis.crystalkeep.control; + +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.TreeItem; +import javafx.scene.layout.VBox; +import nl.andrewlalis.crystalkeep.model.CrystalItem; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; +import nl.andrewlalis.crystalkeep.view.ShardTreeItem; +import nl.andrewlalis.crystalkeep.view.shard_details.LoginCredentialsPane; +import nl.andrewlalis.crystalkeep.view.shard_details.ShardPane; +import nl.andrewlalis.crystalkeep.view.shard_details.TextShardPane; + +import java.util.HashMap; +import java.util.Map; + +public class ClusterTreeViewItemSelectionListener implements ChangeListener> { + private static final Map, Class>> shardPanesMap = new HashMap<>(); + static { + shardPanesMap.put(TextShard.class, TextShardPane.class); + shardPanesMap.put(LoginCredentialsShard.class, LoginCredentialsPane.class); + } + + private final VBox shardDetailContainer; + + public ClusterTreeViewItemSelectionListener(VBox shardDetailContainer) { + this.shardDetailContainer = shardDetailContainer; + } + + @Override + public void changed(ObservableValue> observable, TreeItem oldValue, TreeItem newValue) { + shardDetailContainer.getChildren().clear(); + if (newValue instanceof ShardTreeItem) { + var node = (ShardTreeItem) newValue; + var paneClass = shardPanesMap.get(node.getShard().getClass()); + try { + var pane = paneClass.getDeclaredConstructor(node.getShard().getClass()).newInstance(node.getShard()); + shardDetailContainer.getChildren().add(pane); + } catch (Exception e) { + e.printStackTrace(); + } + } + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/DeleteItemHandler.java b/src/main/java/nl/andrewlalis/crystalkeep/control/DeleteItemHandler.java new file mode 100644 index 0000000..942dba9 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/DeleteItemHandler.java @@ -0,0 +1,41 @@ +package nl.andrewlalis.crystalkeep.control; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TreeItem; +import nl.andrewlalis.crystalkeep.model.Cluster; +import nl.andrewlalis.crystalkeep.model.CrystalItem; +import nl.andrewlalis.crystalkeep.model.Model; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.view.ClusterTreeItem; + +import java.util.Optional; + +public class DeleteItemHandler implements EventHandler { + private final TreeItem treeItem; + private final Model model; + + public DeleteItemHandler(TreeItem treeItem, Model model) { + this.treeItem = treeItem; + this.model = model; + } + + @Override + public void handle(ActionEvent event) { + if (this.treeItem.getParent() != null && this.treeItem.getParent() instanceof ClusterTreeItem) { + Optional result = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete this item?").showAndWait(); + if (result.isPresent() && result.get() == ButtonType.OK) { + var cluster = ((ClusterTreeItem) this.treeItem.getParent()).getCluster(); + var item = this.treeItem.getValue(); + if (item instanceof Shard) { + cluster.removeShard((Shard) item); + } else if (item instanceof Cluster) { + cluster.removeCluster((Cluster) item); + } + this.model.notifyListeners(); + } + } + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java index 542d27c..c2ac4f0 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java @@ -1,32 +1,24 @@ package nl.andrewlalis.crystalkeep.control; -import javafx.event.ActionEvent; +import javafx.application.Platform; import javafx.fxml.FXML; +import javafx.scene.control.Alert; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; 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 nl.andrewlalis.crystalkeep.view.shard_details.LoginCredentialsPane; -import nl.andrewlalis.crystalkeep.view.shard_details.ShardPane; -import nl.andrewlalis.crystalkeep.view.shard_details.TextShardPane; +import java.io.File; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.nio.file.Path; +import java.security.GeneralSecurityException; public class MainViewController implements ModelListener { - private static final Map, Class>> shardPanesMap = new HashMap<>(); - static { - shardPanesMap.put(TextShard.class, TextShardPane.class); - shardPanesMap.put(LoginCredentialsShard.class, LoginCredentialsPane.class); - } - private Model model; @FXML @@ -40,29 +32,8 @@ public class MainViewController implements ModelListener { this.activeClusterUpdated(); assert(this.clusterTreeView != null); this.clusterTreeView.setCellFactory(param -> new CrystalItemTreeCell(this.model)); - this.clusterTreeView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - shardDetailContainer.getChildren().clear(); - if (newValue instanceof ShardTreeItem) { - var node = (ShardTreeItem) newValue; - var paneClass = shardPanesMap.get(node.getShard().getClass()); - try { - var pane = paneClass.getDeclaredConstructor(node.getShard().getClass()).newInstance(node.getShard()); - shardDetailContainer.getChildren().add(pane); - } catch (Exception e) { - e.printStackTrace(); - } - } - }); - } - - @FXML - public void exit(ActionEvent event) { - System.out.println("Exiting..."); - try { - new ClusterLoader().saveDefault(model.getActiveCluster()); - } catch (IOException e) { - e.printStackTrace(); - } + this.clusterTreeView.getSelectionModel().selectedItemProperty() + .addListener(new ClusterTreeViewItemSelectionListener(this.shardDetailContainer)); } @Override @@ -84,4 +55,62 @@ public class MainViewController implements ModelListener { } return node; } + + @FXML + public void exit() { + Platform.exit(); + } + + @FXML + public void load() { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Load a Cluster"); + chooser.setInitialDirectory(ClusterLoader.CLUSTER_PATH.toFile()); + chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts")); + File file = chooser.showOpenDialog(this.clusterTreeView.getScene().getWindow()); + if (file == null) return; + ClusterLoader loader = new ClusterLoader(); + var password = loader.promptPassword(); + if (password.isEmpty() || password.get().isEmpty()) return; + try { + var cluster = loader.load(file.toPath(), password.get()); + model.setActiveCluster(cluster); + model.setActiveClusterPath(file.toPath()); + } catch (Exception e) { + e.printStackTrace(); + new Alert(Alert.AlertType.WARNING, "Could not load cluster.").showAndWait(); + } + } + + @FXML + public void save() { + if (model.getActiveCluster() == null) return; + ClusterLoader loader = new ClusterLoader(); + Path path = model.getActiveClusterPath(); + if (path == null) { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Save Cluster"); + chooser.setInitialDirectory(ClusterLoader.CLUSTER_PATH.toFile()); + chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts")); + File file = chooser.showSaveDialog(this.clusterTreeView.getScene().getWindow()); + if (file == null) return; + path = file.toPath(); + } + var password = loader.promptPassword(); + if (password.isEmpty() || password.get().isEmpty()) return; + try { + new ClusterLoader().save(model.getActiveCluster(), path, password.get()); + } catch (IOException | GeneralSecurityException e) { + e.printStackTrace(); + var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster."); + alert.showAndWait(); + } + } + + @FXML + public void newCluster() { + Cluster c = new Cluster("Root"); + model.setActiveCluster(c); + model.setActiveClusterPath(null); + } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java index ec755c2..83f9169 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Model.java @@ -1,11 +1,13 @@ package nl.andrewlalis.crystalkeep.model; +import java.nio.file.Path; import java.util.HashSet; import java.util.Set; public class Model { private Cluster activeCluster; + private Path activeClusterPath; private final Set listeners = new HashSet<>(); @@ -13,6 +15,10 @@ public class Model { return activeCluster; } + public Path getActiveClusterPath() { + return activeClusterPath; + } + public void addListener(ModelListener listener) { this.listeners.add(listener); } @@ -22,6 +28,10 @@ public class Model { this.notifyListeners(); } + public void setActiveClusterPath(Path activeClusterPath) { + this.activeClusterPath = activeClusterPath; + } + public void notifyListeners() { this.listeners.forEach(ModelListener::activeClusterUpdated); } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java index 71e47da..329c0a9 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java @@ -1,27 +1,62 @@ package nl.andrewlalis.crystalkeep.model.serialization; +import javafx.scene.control.Dialog; +import javafx.scene.control.TextInputDialog; import nl.andrewlalis.crystalkeep.model.Cluster; -import java.io.FileInputStream; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Optional; public class ClusterLoader { - private static final Path CLUSTER_PATH = Path.of("clusters"); - private static final Path DEFAULT_CLUSTER = CLUSTER_PATH.resolve("default.cts"); + public static final Path CLUSTER_PATH = Path.of("clusters"); + public static final Path DEFAULT_CLUSTER = CLUSTER_PATH.resolve("default.cts"); + private static final byte[] SALT = "zf9i78vy".getBytes(StandardCharsets.UTF_8); + private static final byte[] IV = "Fafioje;a324fsde".getBytes(StandardCharsets.UTF_8); - public Cluster loadDefault() throws IOException { - InputStream is = new FileInputStream(DEFAULT_CLUSTER.toFile()); - return ClusterSerializer.readCluster(is); + public Optional promptPassword() { + Dialog d = new TextInputDialog(); + d.setContentText("Enter the password"); + d.setGraphic(null); + d.setHeaderText(null); + d.setTitle("Enter Password"); + return d.showAndWait(); } - public void saveDefault(Cluster cluster) throws IOException { + public Cluster load(Path path, String password) throws Exception { + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.getSecretKey(password), new IvParameterSpec(IV)); + byte[] raw = Files.readAllBytes(path); + return ClusterSerializer.readCluster(new ByteArrayInputStream(cipher.doFinal(raw))); + } + + public void save(Cluster cluster, Path path, String password) throws IOException, GeneralSecurityException { Files.createDirectories(CLUSTER_PATH); - OutputStream os = Files.newOutputStream(DEFAULT_CLUSTER); - ClusterSerializer.writeCluster(cluster, os); - os.close(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ClusterSerializer.writeCluster(cluster, bos); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, this.getSecretKey(password), new IvParameterSpec(IV)); + Files.write(path, cipher.doFinal(bos.toByteArray())); + bos.close(); + } + + private SecretKey getSecretKey(String password) throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + KeySpec spec = new PBEKeySpec(password.toCharArray(), SALT, 65536, 256); + return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java index 55ea9f9..5189dba 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -19,6 +19,10 @@ public class LoginCredentialsShard extends Shard { this.password = password; } + public LoginCredentialsShard(String name) { + this(name, LocalDateTime.now(), "", ""); + } + public String getUsername() { return username; } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java index ee6d75d..0d42427 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -17,6 +17,10 @@ public class TextShard extends Shard { this.text = text; } + public TextShard(String name) { + this(name, LocalDateTime.now(), ""); + } + public String getText() { return text; } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java index e256e35..dd083df 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java @@ -5,10 +5,12 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.TreeCell; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import nl.andrewlalis.crystalkeep.control.AddClusterHandler; +import nl.andrewlalis.crystalkeep.control.AddShardHandler; +import nl.andrewlalis.crystalkeep.control.DeleteItemHandler; import nl.andrewlalis.crystalkeep.model.Cluster; import nl.andrewlalis.crystalkeep.model.CrystalItem; import nl.andrewlalis.crystalkeep.model.Model; -import nl.andrewlalis.crystalkeep.model.Shard; import java.io.InputStream; @@ -27,28 +29,23 @@ public class CrystalItemTreeCell extends TreeCell { if (!empty) { ContextMenu menu = new ContextMenu(); if (item instanceof Cluster) { + var cluster = (Cluster) item; var addShardItem = new MenuItem("Add Shard"); + addShardItem.setOnAction(new AddShardHandler(cluster, model)); var addClusterItem = new MenuItem("Add Cluster"); + addClusterItem.setOnAction(new AddClusterHandler(cluster, model)); menu.getItems().addAll(addShardItem, addClusterItem); } - var deleteItem = new MenuItem("Delete"); - deleteItem.setOnAction(event -> { - if (this.getTreeItem().getParent() != null && this.getTreeItem().getParent() instanceof ClusterTreeItem) { - var cluster = ((ClusterTreeItem) this.getTreeItem().getParent()).getCluster(); - if (item instanceof Shard) { - cluster.removeShard((Shard) item); - } else if (item instanceof Cluster) { - cluster.removeCluster((Cluster) item); - } - this.model.notifyListeners(); - } - }); - menu.getItems().add(deleteItem); + if (this.getTreeItem().getParent() != null && this.getTreeItem().getParent() instanceof ClusterTreeItem) { + var deleteItem = new MenuItem("Delete"); + deleteItem.setOnAction(new DeleteItemHandler(this.getTreeItem(), this.model)); + menu.getItems().add(deleteItem); + } this.setText(item.getName()); InputStream is = getClass().getResourceAsStream(item.getIconPath()); if (is != null) { ImageView icon = new ImageView(new Image(is)); - icon.setFitHeight(24); + icon.setFitHeight(16); icon.setPreserveRatio(true); this.setGraphic(icon); } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java index c164dc1..a6ebfcc 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/TextShardPane.java @@ -12,6 +12,7 @@ public class TextShardPane extends ShardPane { @Override protected Node getContent(TextShard shard) { var textArea = new TextArea(shard.getText()); + textArea.setWrapText(true); textArea.textProperty().addListener((observable, oldValue, newValue) -> { shard.setText(newValue); }); diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml b/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml index 5855dda..b915bd3 100644 --- a/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml +++ b/src/main/resources/nl/andrewlalis/crystalkeep/ui/crystalkeep.fxml @@ -13,6 +13,9 @@ + + +