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 93e7c27..4d6bede 100644 Binary files a/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png and b/src/main/resources/nl/andrewlalis/crystalkeep/ui/images/cluster_node_icon.png differ