diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index a1ef2c93..786fed6f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -332,7 +332,7 @@ public class DevicePane extends TitledDescriptionPane { VBox vBox = new VBox(); vBox.setMaxHeight(120); vBox.setSpacing(42); - pinField = (CustomPasswordField)TextFields.createClearablePasswordField(); + pinField = new ViewPasswordField(); Platform.runLater(() -> pinField.requestFocus()); enterPinButton = new Button("Enter PIN"); enterPinButton.setDefaultButton(true); @@ -372,7 +372,7 @@ public class DevicePane extends TitledDescriptionPane { } private Node getPassphraseEntry() { - CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField(); + CustomPasswordField passphraseField = new ViewPasswordField(); passphrase.bind(passphraseField.textProperty()); HBox.setHgrow(passphraseField, Priority.ALWAYS); passphraseField.setOnAction(event -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java index 6d2919c0..e7d6f121 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileImportPane.java @@ -190,7 +190,7 @@ public abstract class FileImportPane extends TitledDescriptionPane { protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException; private Node getPasswordEntry(File file) { - CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField(); + CustomPasswordField passwordField = new ViewPasswordField(); passwordField.setPromptText("Wallet password"); password.bind(passwordField.textProperty()); HBox.setHgrow(passwordField, Priority.ALWAYS); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/KeystorePassphraseDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/KeystorePassphraseDialog.java index 507086f3..4f918d2f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/KeystorePassphraseDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/KeystorePassphraseDialog.java @@ -25,7 +25,7 @@ public class KeystorePassphraseDialog extends Dialog { } public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) { - this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField(); + this.passphrase = new ViewPasswordField(); final DialogPane dialogPane = getDialogPane(); setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : "")); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/ViewPasswordField.java b/src/main/java/com/sparrowwallet/sparrow/control/ViewPasswordField.java new file mode 100644 index 00000000..ca0c2456 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/ViewPasswordField.java @@ -0,0 +1,28 @@ +package com.sparrowwallet.sparrow.control; + +import javafx.beans.property.ObjectProperty; +import javafx.scene.Node; +import javafx.scene.control.Skin; +import org.controlsfx.control.textfield.CustomPasswordField; + +public class ViewPasswordField extends CustomPasswordField { + public ViewPasswordField() { + super(); + getStyleClass().add("view-password-text-field"); + } + + @Override + protected Skin createDefaultSkin() { + return new ViewPasswordFieldSkin(this) { + @Override + public ObjectProperty leftProperty() { + return ViewPasswordField.this.leftProperty(); + } + + @Override + public ObjectProperty rightProperty() { + return ViewPasswordField.this.rightProperty(); + } + }; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/ViewPasswordFieldSkin.java b/src/main/java/com/sparrowwallet/sparrow/control/ViewPasswordFieldSkin.java new file mode 100644 index 00000000..4694a5bb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/ViewPasswordFieldSkin.java @@ -0,0 +1,83 @@ +package com.sparrowwallet.sparrow.control; + +import impl.org.controlsfx.skin.CustomTextFieldSkin; +import javafx.animation.FadeTransition; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.scene.Cursor; +import javafx.scene.control.PasswordField; +import javafx.scene.layout.*; +import javafx.util.Duration; +import org.controlsfx.control.textfield.CustomPasswordField; + +public abstract class ViewPasswordFieldSkin extends CustomTextFieldSkin { + private static final Duration FADE_DURATION = Duration.millis(350); + public static final char BULLET = '\u25CF'; + + private boolean mask = true; + + public ViewPasswordFieldSkin(CustomPasswordField textField) { + super(textField); + + Region viewPasswordButton = new Region(); + viewPasswordButton.getStyleClass().addAll("graphic"); + StackPane viewPasswordButtonPane = new StackPane(viewPasswordButton); + viewPasswordButtonPane.getStyleClass().addAll("view-password-button"); + viewPasswordButtonPane.setOpacity(0.0); + viewPasswordButtonPane.setCursor(Cursor.DEFAULT); + + viewPasswordButtonPane.setOnMouseReleased(e -> { + if(mask) { + viewPasswordButtonPane.getStyleClass().remove("view-password-button"); + viewPasswordButtonPane.getStyleClass().addAll("hide-password-button"); + mask = false; + } else { + viewPasswordButtonPane.getStyleClass().remove("hide-password-button"); + viewPasswordButtonPane.getStyleClass().addAll("view-password-button"); + mask = true; + } + textField.setText(textField.getText()); + textField.end(); + }); + + textField.rightProperty().set(viewPasswordButtonPane); + + final FadeTransition fader = new FadeTransition(FADE_DURATION, viewPasswordButtonPane); + fader.setCycleCount(1); + + textField.textProperty().addListener(new InvalidationListener() { + @Override + public void invalidated(Observable arg0) { + String text = textField.getText(); + boolean isTextEmpty = text == null || text.isEmpty(); + boolean isButtonVisible = fader.getNode().getOpacity() > 0; + + if (isTextEmpty && isButtonVisible) { + setButtonVisible(false); + } else if (!isTextEmpty && !isButtonVisible) { + setButtonVisible(true); + } + } + + private void setButtonVisible( boolean visible ) { + fader.setFromValue(visible? 0.0: 1.0); + fader.setToValue(visible? 1.0: 0.0); + fader.play(); + } + }); + } + + @Override + protected String maskText(String txt) { + if(getSkinnable() instanceof PasswordField && mask) { + int n = txt.length(); + StringBuilder passwordBuilder = new StringBuilder(n); + for (int i = 0; i < n; i++) { + passwordBuilder.append(BULLET); + } + return passwordBuilder.toString(); + } else { + return txt; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletPasswordDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletPasswordDialog.java index 88d02f68..66cc56b9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletPasswordDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletPasswordDialog.java @@ -8,7 +8,6 @@ import javafx.beans.binding.BooleanBinding; import javafx.scene.control.*; import javafx.scene.layout.VBox; import org.controlsfx.control.textfield.CustomPasswordField; -import org.controlsfx.control.textfield.TextFields; import org.controlsfx.glyphfont.FontAwesome; import org.controlsfx.glyphfont.Glyph; import org.controlsfx.validation.ValidationResult; @@ -31,8 +30,8 @@ public class WalletPasswordDialog extends Dialog { public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) { this.requirement = requirement; - this.password = (CustomPasswordField)TextFields.createClearablePasswordField(); - this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField(); + this.password = new ViewPasswordField(); + this.passwordConfirm = new ViewPasswordField(); this.backupExisting = new CheckBox("Backup existing wallet first"); this.changePassword = new CheckBox("Change password"); this.deleteBackups = new CheckBox("Delete any backups"); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index 6e3d6ad3..6e24956f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.control.ViewPasswordField; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Storage; import javafx.application.Platform; @@ -147,7 +148,7 @@ public class WalletController extends WalletFormController implements Initializa Label label = new Label("Enter password to unlock:"); label.managedProperty().bind(label.visibleProperty()); label.visibleProperty().bind(walletEncryptedProperty); - CustomPasswordField passwordField = (CustomPasswordField)TextFields.createClearablePasswordField(); + CustomPasswordField passwordField = new ViewPasswordField(); passwordField.setMaxWidth(300); passwordField.managedProperty().bind(passwordField.visibleProperty()); passwordField.visibleProperty().bind(walletEncryptedProperty); diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index fe2e9755..218cc7f4 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -156,6 +156,28 @@ -fx-background-color: #116a8d; } +.view-password-text-field .view-password-button > .graphic { + -fx-background-color: #949494; + -fx-scale-shape: false; + -fx-padding: 0 25 0 0; + -fx-shape: "M10.4617+4.5C6.16092+4.5+2.48807+7.02125+1+10.5802C2.48807+14.1391+6.16092+16.6604+10.4617+16.6604C14.7625+16.6604+18.4353+14.1391+19.9234+10.5802C18.4353+7.02125+14.7625+4.5+10.4617+4.5ZM10.4617+14.6336C8.08767+14.6336+6.16092+12.8177+6.16092+10.5802C6.16092+8.34268+8.08767+6.52673+10.4617+6.52673C12.8357+6.52673+14.7625+8.34268+14.7625+10.5802C14.7625+12.8177+12.8357+14.6336+10.4617+14.6336ZM10.4617+8.14811C9.03384+8.14811+7.88123+9.23444+7.88123+10.5802C7.88123+11.9259+9.03384+13.0123+10.4617+13.0123C11.8896+13.0123+13.0422+11.9259+13.0422+10.5802C13.0422+9.23444+11.8896+8.14811+10.4617+8.14811Z"; +} + +.view-password-text-field .hide-password-button > .graphic { + -fx-background-color: #949494; + -fx-scale-shape: false; + -fx-padding: 0 25 0 0; + -fx-shape: "M10.6168+6.26674C13.9302+6.26674+16.8852+7.95776+18.3277+10.6332C17.8119+11.6018+17.0863+12.4354+16.2208+13.1102L17.4535+14.2296C18.6687+13.2531+19.6304+12.0305+20.2336+10.6332C18.7212+7.14798+14.9881+4.67893+10.6168+4.67893C9.50651+4.67893+8.43991+4.83772+7.43452+5.13146L8.87704+6.4414C9.44531+6.3382+10.0223+6.26674+10.6168+6.26674ZM9.68136+7.1718L11.4911+8.81518C11.9894+9.01366+12.3915+9.37885+12.6101+9.83138L14.4198+11.4748C14.4898+11.2048+14.5422+10.919+14.5422+10.6253C14.551+8.6564+12.785+7.06065+10.6168+7.06065C10.2933+7.06065+9.98735+7.10034+9.68136+7.1718ZM1.883+4.57573L4.226+6.70339C2.80097+7.71959+1.67318+9.06923+1+10.6332C2.51246+14.1185+6.24553+16.5875+10.6168+16.5875C11.9457+16.5875+13.2221+16.3573+14.3936+15.9365L17.3835+18.6517L18.6162+17.5323L3.1157+3.44838L1.883+4.57573ZM8.43991+10.53L10.7217+12.6021C10.6868+12.61+10.6518+12.618+10.6168+12.618C9.41034+12.618+8.43117+11.7288+8.43117+10.6332C8.43117+10.5935+8.43991+10.5697+8.43991+10.53ZM5.46745+7.83074L6.99739+9.22007C6.79631+9.65672+6.68266+10.1331+6.68266+10.6332C6.68266+12.6021+8.44866+14.2058+10.6168+14.2058C11.1676+14.2058+11.6921+14.1026+12.1642+13.92L13.021+14.698C12.2517+14.8886+11.4474+14.9997+10.6168+14.9997C7.30338+14.9997+4.3484+13.3087+2.90588+10.6332C3.51786+9.49794+4.4096+8.56113+5.46745+7.83074Z"; +} + +.view-password-text-field .view-password-button:hover > .graphic, .view-password-text-field .hide-password-button:hover > .graphic { + -fx-background-color: #0184bc; +} + +.view-password-text-field .view-password-button:pressed > .graphic, .view-password-text-field .hide-password-button:pressed > .graphic { + -fx-background-color: #116a8d; +} + .readonly.text-input { -fx-text-fill: derive(-fx-text-inner-color, 40%); }