From f7b54dc1820b079d64927840443e3c14cfd13534 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 31 May 2021 13:24:32 +0200 Subject: [PATCH] Cleaned up view models, tried to get pasting to work. --- designs/cluster_node_icon.svg | 48 ++++++++-------- pom.xml | 5 ++ src/main/java/module-info.java | 1 + .../ClusterTreeViewItemSelectionListener.java | 24 +------- .../crystalkeep/model/Cluster.java | 5 -- .../crystalkeep/model/CrystalItem.java | 1 - .../andrewlalis/crystalkeep/model/Shard.java | 5 -- .../model/shards/LoginCredentialsShard.java | 5 -- .../crystalkeep/model/shards/TextShard.java | 5 -- .../crystalkeep/util/ImageCache.java | 54 ++++++++++++++++++ .../crystalkeep/view/CrystalItemTreeCell.java | 24 +++++--- .../LoginCredentialsViewModel.java | 22 +++++-- .../view/shard_details/ShardViewModel.java | 10 ++++ .../view/shard_details/ViewModels.java | 34 +++++++++++ .../ui/images/cluster_node_icon.png | Bin 803 -> 7348 bytes 15 files changed, 164 insertions(+), 79 deletions(-) create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/util/ImageCache.java create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java diff --git a/designs/cluster_node_icon.svg b/designs/cluster_node_icon.svg index 6dbe761..c07d77b 100644 --- a/designs/cluster_node_icon.svg +++ b/designs/cluster_node_icon.svg @@ -16,9 +16,9 @@ id="svg8" inkscape:version="0.92.4 (5da689c313, 2019-01-14)" sodipodi:docname="cluster_node_icon.svg" - inkscape:export-filename="A:\Programming\GitHub-andrewlalis\CrystalKeep\src\main\resources\ui\images\cluster_node_icon.png" - inkscape:export-xdpi="192" - inkscape:export-ydpi="192"> + inkscape:export-filename="A:\Programming\GitHub-andrewlalis\CrystalKeep\src\main\resources\nl\andrewlalis\crystalkeep\ui\images\cluster_node_icon.png" + inkscape:export-xdpi="1536" + inkscape:export-ydpi="1536"> + units="px" + showguides="true" + inkscape:guide-bbox="true" /> @@ -48,7 +50,7 @@ image/svg+xml - + @@ -58,21 +60,19 @@ id="layer1" transform="translate(0,-292.76667)"> + style="fill:#cf00f8;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 0.99910255,294.74397 1.11756415,-1.93568 1.1175641,1.93568 z" + id="path824" + inkscape:connector-curvature="0" /> + + diff --git a/pom.xml b/pom.xml index 75c9717..f376f07 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,11 @@ javafx-fxml ${javafx.version} + + com.1stleg + jnativehook + 2.1.0 + org.junit.jupiter diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c469dfc..10f6a58 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,7 @@ module crystalkeep { requires javafx.fxml; requires javafx.controls; + requires jnativehook; opens nl.andrewlalis.crystalkeep.control; opens nl.andrewlalis.crystalkeep; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java index fdcc1d0..318716a 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/ClusterTreeViewItemSelectionListener.java @@ -5,24 +5,10 @@ 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.LoginCredentialsViewModel; -import nl.andrewlalis.crystalkeep.view.shard_details.ShardViewModel; -import nl.andrewlalis.crystalkeep.view.shard_details.TextShardViewModel; - -import java.util.HashMap; -import java.util.Map; +import nl.andrewlalis.crystalkeep.view.shard_details.ViewModels; public class ClusterTreeViewItemSelectionListener implements ChangeListener> { - private static final Map, Class>> shardPanesMap = new HashMap<>(); - static { - shardPanesMap.put(TextShard.class, TextShardViewModel.class); - shardPanesMap.put(LoginCredentialsShard.class, LoginCredentialsViewModel.class); - } - private final VBox shardDetailContainer; public ClusterTreeViewItemSelectionListener(VBox shardDetailContainer) { @@ -34,13 +20,7 @@ public class ClusterTreeViewItemSelectionListener implements ChangeListener shardDetailContainer.getChildren().add(vm.getContentPane())); } } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java index 02b721c..b994b57 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Cluster.java @@ -93,9 +93,4 @@ public class Cluster implements Comparable, CrystalItem { } return sb.toString(); } - - @Override - public String getIconPath() { - return "/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png"; - } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/CrystalItem.java b/src/main/java/nl/andrewlalis/crystalkeep/model/CrystalItem.java index 3fcaaaa..6f4c213 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/CrystalItem.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/CrystalItem.java @@ -6,5 +6,4 @@ package nl.andrewlalis.crystalkeep.model; */ public interface CrystalItem { String getName(); - String getIconPath(); } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java index da4d004..3149a22 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/Shard.java @@ -64,9 +64,4 @@ public abstract class Shard implements Comparable, CrystalItem { public String toString() { return "Shard: name=\"" + this.name + "\", type=" + this.type + ", createdAt=" + this.createdAt; } - - @Override - public String getIconPath() { - return "/nl/andrewlalis/crystalkeep/ui/images/shard_node_icon.png"; - } } 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 cc4ce07..7d7ba75 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -44,11 +44,6 @@ public class LoginCredentialsShard extends Shard { return super.toString() + ", username=\"" + this.username + "\", password=\"" + this.password + "\""; } - @Override - public String getIconPath() { - return "/nl/andrewlalis/crystalkeep/ui/images/login_credentials_shard_node_icon.png"; - } - public static class Serializer implements ShardSerializer { @Override 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 729d94c..328d132 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -34,11 +34,6 @@ public class TextShard extends Shard { return super.toString() + ", text=\"" + this.text + "\""; } - @Override - public String getIconPath() { - return "/nl/andrewlalis/crystalkeep/ui/images/text_shard_node_icon.png"; - } - public static class Serializer implements ShardSerializer { @Override public byte[] serialize(TextShard shard) throws IOException { diff --git a/src/main/java/nl/andrewlalis/crystalkeep/util/ImageCache.java b/src/main/java/nl/andrewlalis/crystalkeep/util/ImageCache.java new file mode 100644 index 0000000..fbe64ec --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/util/ImageCache.java @@ -0,0 +1,54 @@ +package nl.andrewlalis.crystalkeep.util; + +import javafx.scene.image.Image; + +import java.io.InputStream; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This cache stores pre-loaded images that are used in the application, so that + * they only need to be loaded once. + */ +public class ImageCache { + private static final Map images = new ConcurrentHashMap<>(); + + public static Optional get(String path, double width, double height) { + ImageSpec spec = new ImageSpec(path, width, height); + Image img = images.get(spec); + if (img == null) { + InputStream is = ImageCache.class.getResourceAsStream(path); + if (is == null) return Optional.empty(); + img = new Image(is, spec.width, spec.height, true, true); + images.put(spec, img); + } + return Optional.of(img); + } + + private static class ImageSpec { + private final String path; + private final double width; + private final double height; + + public ImageSpec(String path, double width, double height) { + this.path = path; + this.width = width; + this.height = height; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImageSpec imageSpec = (ImageSpec) o; + return Double.compare(imageSpec.width, width) == 0 && Double.compare(imageSpec.height, height) == 0 && path.equals(imageSpec.path); + } + + @Override + public int hashCode() { + return Objects.hash(path, width, height); + } + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java index dd083df..32bad78 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/CrystalItemTreeCell.java @@ -3,7 +3,6 @@ package nl.andrewlalis.crystalkeep.view; import javafx.scene.control.ContextMenu; 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; @@ -11,10 +10,13 @@ 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 java.io.InputStream; +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.util.ImageCache; +import nl.andrewlalis.crystalkeep.view.shard_details.ViewModels; public class CrystalItemTreeCell extends TreeCell { + private static final String CLUSTER_ICON = "/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png"; + private final Model model; public CrystalItemTreeCell(Model model) { @@ -36,19 +38,27 @@ public class CrystalItemTreeCell extends TreeCell { addClusterItem.setOnAction(new AddClusterHandler(cluster, model)); menu.getItems().addAll(addShardItem, addClusterItem); } + 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)); + + String iconPath = CLUSTER_ICON; + if (item instanceof Shard) { + var vm = ViewModels.get((Shard) item); + if (vm.isPresent()) { + iconPath = vm.get().getIconPath(); + } + } + ImageCache.get(iconPath, 16, 16).ifPresent(img -> { + ImageView icon = new ImageView(img); icon.setFitHeight(16); icon.setPreserveRatio(true); this.setGraphic(icon); - } + }); this.setContextMenu(menu); } else { this.setText(null); diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java index 887c0d4..8a0a97f 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java @@ -9,6 +9,9 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import org.jnativehook.GlobalScreen; +import org.jnativehook.keyboard.NativeKeyAdapter; +import org.jnativehook.keyboard.NativeKeyEvent; public class LoginCredentialsViewModel extends ShardViewModel { public LoginCredentialsViewModel(LoginCredentialsShard shard) { @@ -61,12 +64,21 @@ public class LoginCredentialsViewModel extends ShardViewModel { - System.out.println("Not yet implemented."); + + var copyBothButton = new Button("Copy Username and Password"); + copyBothButton.setOnAction(event -> { + ClipboardContent content = new ClipboardContent(); + content.putString(shard.getUsername()); + final Clipboard c = Clipboard.getSystemClipboard(); + c.setContent(content); + var t = new Thread(() -> { + while (c.getString().equals(shard.getUsername())) { + System.out.println("User hasn't pasted yet"); + } + }); }); - typePasswordButton.setDisable(true); - passwordActionsPane.getChildren().addAll(showPasswordCheckbox, copyPasswordButton, typePasswordButton); + + passwordActionsPane.getChildren().addAll(showPasswordCheckbox, copyPasswordButton); gp.add(passwordActionsPane, 1, 2); return gp; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java index 99a85e5..7c7e194 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ShardViewModel.java @@ -12,6 +12,12 @@ import nl.andrewlalis.crystalkeep.model.Shard; import java.time.format.DateTimeFormatter; +/** + * The view model for a type of shard. A shard type's view model defines how the + * shard is displayed in the application, including the actual UI contents, and + * icon. + * @param The type of shard. + */ public abstract class ShardViewModel { protected final T shard; @@ -42,4 +48,8 @@ public abstract class ShardViewModel { } protected abstract Node getContent(T shard); + + public String getIconPath() { + return "/nl/andrewlalis/crystalkeep/ui/images/shard_node_icon.png"; + } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java new file mode 100644 index 0000000..09f4581 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/ViewModels.java @@ -0,0 +1,34 @@ +package nl.andrewlalis.crystalkeep.view.shard_details; + +import nl.andrewlalis.crystalkeep.model.Shard; +import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; +import nl.andrewlalis.crystalkeep.model.shards.TextShard; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * Utility class that provides the ability to look up and obtain a new view + * model for a particular type of shard. + */ +public class ViewModels { + private static final Map, Class> shardViewModels = new HashMap<>(); + static { + shardViewModels.put(TextShard.class, TextShardViewModel.class); + shardViewModels.put(LoginCredentialsShard.class, LoginCredentialsViewModel.class); + } + + public static Optional> get(Shard shard) { + try { + Class viewModelClass = shardViewModels.get(shard.getClass()); + if (viewModelClass != null) { + var viewModel = (ShardViewModel) viewModelClass.getDeclaredConstructor(shard.getClass()).newInstance(shard); + return Optional.of(viewModel); + } + } catch (Exception e) { + e.printStackTrace(); + } + return Optional.empty(); + } +} diff --git a/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png index 93e7c270fbb168ce43ca0ff6ac1399d5b53ea59c..4d6bede940973a6e159152322dc5e3c1386ed5ea 100644 GIT binary patch literal 7348 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 mUEC9L0DD*uhY_=na{LASdDu_prygto0000