mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-20 19:51:27 +00:00
improved password field: added caps lock indicator (see #458) and a warning + tooltip if control characters are found in the password (fixes #841)
This commit is contained in:
@@ -9,11 +9,21 @@
|
||||
package org.cryptomator.ui.controls;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.Dragboard;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
|
||||
import java.awt.Toolkit;
|
||||
import java.nio.CharBuffer;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
@@ -31,12 +41,32 @@ public class SecPasswordField extends PasswordField {
|
||||
private static final int GROW_BUFFER_SIZE = 50;
|
||||
private static final String PLACEHOLDER = "*";
|
||||
|
||||
private final Tooltip tooltip = new Tooltip();
|
||||
private final Label indicator = new Label();
|
||||
private final StringProperty nonPrintableCharsWarning = new SimpleStringProperty();
|
||||
private final StringProperty capslockWarning = new SimpleStringProperty();
|
||||
|
||||
private char[] content = new char[INITIAL_BUFFER_SIZE];
|
||||
private int length = 0;
|
||||
|
||||
public SecPasswordField() {
|
||||
this.onDragOverProperty().set(this::handleDragOver);
|
||||
this.onDragDroppedProperty().set(this::handleDragDropped);
|
||||
indicator.setAlignment(Pos.CENTER_RIGHT);
|
||||
indicator.setMouseTransparent(true);
|
||||
indicator.setTextOverrun(OverrunStyle.CLIP);
|
||||
this.getChildren().add(indicator);
|
||||
this.setTooltip(tooltip);
|
||||
this.addEventHandler(DragEvent.DRAG_OVER, this::handleDragOver);
|
||||
this.addEventHandler(DragEvent.DRAG_DROPPED, this::handleDragDropped);
|
||||
this.addEventHandler(KeyEvent.ANY, this::handleKeyEvent);
|
||||
this.focusedProperty().addListener(this::focusedChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
indicator.resize(50.0, getHeight());
|
||||
indicator.relocate(getWidth() - indicator.getWidth(), 0);
|
||||
indicator.layout();
|
||||
}
|
||||
|
||||
private void handleDragOver(DragEvent event) {
|
||||
@@ -55,6 +85,50 @@ public class SecPasswordField extends PasswordField {
|
||||
event.consume();
|
||||
}
|
||||
|
||||
private void handleKeyEvent(KeyEvent e) {
|
||||
if (e.getCode() == KeyCode.CAPS) {
|
||||
updateVisualHints(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void focusedChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
updateVisualHints(isFocused());
|
||||
}
|
||||
|
||||
private void updateVisualHints(boolean focused) {
|
||||
StringBuilder tooltipSb = new StringBuilder();
|
||||
StringBuilder indicatorSb = new StringBuilder();
|
||||
if (containsNonPrintableCharacters()) {
|
||||
indicatorSb.append('⚠');
|
||||
tooltipSb.append(nonPrintableCharsWarning.get()).append('\n');
|
||||
}
|
||||
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
|
||||
if (focused && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)) {
|
||||
indicatorSb.append('⇪');
|
||||
tooltipSb.append(capslockWarning.get()).append('\n');
|
||||
}
|
||||
indicator.setText(indicatorSb.toString());
|
||||
tooltip.setText(tooltipSb.toString());
|
||||
if (tooltip.getText().isEmpty()) {
|
||||
setTooltip(null);
|
||||
} else {
|
||||
setTooltip(tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if any {@link Character#isISOControl(char) control character} is present in the current value of this password field.
|
||||
* @implNote runs in O(n)
|
||||
*/
|
||||
boolean containsNonPrintableCharacters() {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (Character.isISOControl(content[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces a range of characters with the given text.
|
||||
* The text will be normalized to <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a>.
|
||||
@@ -86,6 +160,8 @@ public class SecPasswordField extends PasswordField {
|
||||
// copy new text to content buffer
|
||||
normalizedText.getChars(0, normalizedText.length(), content, start);
|
||||
|
||||
// trigger visual hints
|
||||
updateVisualHints(true);
|
||||
String placeholderString = Strings.repeat(PLACEHOLDER, normalizedText.length());
|
||||
super.replaceText(start, end, placeholderString);
|
||||
}
|
||||
@@ -156,4 +232,22 @@ public class SecPasswordField extends PasswordField {
|
||||
Arrays.fill(buffer, SWIPE_CHAR);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public void setNonPrintableCharsWarning(String value) {
|
||||
nonPrintableCharsWarning.set(value);
|
||||
}
|
||||
|
||||
public String getNonPrintableCharsWarning() {
|
||||
return nonPrintableCharsWarning.get();
|
||||
}
|
||||
|
||||
public void setCapslockWarning(String value) {
|
||||
capslockWarning.set(value);
|
||||
}
|
||||
|
||||
public String getCapslockWarning() {
|
||||
return capslockWarning.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,15 +38,15 @@
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label text="%changePassword.label.oldPassword" GridPane.rowIndex="0" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="oldPasswordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="oldPasswordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label text="%changePassword.label.newPassword" GridPane.rowIndex="1" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="newPasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="newPasswordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label text="%changePassword.label.retypePassword" GridPane.rowIndex="2" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="retypePasswordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<VBox GridPane.columnIndex="1" GridPane.rowIndex="3" spacing="6.0">
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.password" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="0" GridPane.columnIndex="1" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="0" GridPane.columnIndex="1" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.retypePassword" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="retypePasswordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="1" GridPane.columnIndex="1" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<VBox GridPane.columnIndex="1" GridPane.rowIndex="2" spacing="6.0">
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label text="%unlock.label.password" GridPane.rowIndex="0" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<HBox GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" spacing="12.0" alignment="CENTER_RIGHT" cacheShape="true" cache="true">
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
app.name=Cryptomator
|
||||
|
||||
ctrl.secPasswordField.nonPrintableChars=Password contains control characters. Recommendation: Remove them to ensure compatibility with other clients.
|
||||
ctrl.secPasswordField.capsLocked=Caps Lock is activated.
|
||||
|
||||
# main.fxml
|
||||
main.emptyListInstructions=Click here to add a vault
|
||||
main.directoryList.contextMenu.remove=Remove from List
|
||||
|
||||
@@ -152,7 +152,7 @@ class SecPasswordFieldTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("test swipe char[]")
|
||||
public void swipe() {
|
||||
public void testSwipe() {
|
||||
pwField.appendText("topSecret");
|
||||
|
||||
CharSequence result1 = pwField.getCharacters();
|
||||
@@ -163,4 +163,17 @@ class SecPasswordFieldTest {
|
||||
Assertions.assertEquals("", result2.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("test control characters")
|
||||
public void testControlCharacters() {
|
||||
pwField.appendText("normal");
|
||||
Assertions.assertFalse(pwField.containsNonPrintableCharacters());
|
||||
|
||||
pwField.appendText("\00\01\02");
|
||||
Assertions.assertTrue(pwField.containsNonPrintableCharacters());
|
||||
|
||||
CharSequence result1 = pwField.getCharacters();
|
||||
Assertions.assertEquals("normal\00\01\02", result1.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user