--- old/modules/javafx.controls/src/main/java/javafx/scene/control/TextInputControl.java 2017-10-04 16:46:40.458612704 +0530 +++ new/modules/javafx.controls/src/main/java/javafx/scene/control/TextInputControl.java 2017-10-04 16:46:40.254510704 +0530 @@ -65,6 +65,7 @@ import java.text.BreakIterator; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import com.sun.javafx.util.Utils; @@ -570,14 +571,34 @@ return; } - // If you select some stuff and type anything, then we need to - // create an undo record. If the range is a single character and - // is right next to the index of the last undo record end index, then - // we don't need to create a new undo record. In all other cases - // we do. + /* + * A new undo record is created, if + * 1. createNewUndoRecord is true, currently it is set to true for paste operation + * 2. Text is selected and a character is typed + * 3. This is the first operation to be added to undo record + * 4. forceNewUndoRecord is true, currently it is set to true if there is no text present + * 5. Space character is typed + * 6. 2500 milliseconds are elapsed since the undo record was created + * 7. Cursor position is changed and a character is typed + * 8. A range of text is replaced programmatically using replaceText() + * Otherwise, the last undo record is updated or discarded. + */ + int endOfUndoChange = undoChange == undoChangeHead ? -1 : undoChange.start + undoChange.newText.length(); + boolean isNewSpaceChar = false; + if (newText.equals(" ")) { + if (UndoRedoChange.isSpaceCharSequence()) { + isNewSpaceChar = false; + } else { + isNewSpaceChar = true; + UndoRedoChange.setSpaceCharSequence(true); + } + } else { + UndoRedoChange.setSpaceCharSequence(false); + } if (createNewUndoRecord || nonEmptySelection || endOfUndoChange == -1 || forceNewUndoRecord || - (endOfUndoChange != change.start && endOfUndoChange != change.end) || change.end - change.start > 1) { + isNewSpaceChar || UndoRedoChange.ifChangeDurationElapsed() || + (endOfUndoChange != change.start && endOfUndoChange != change.end) || change.end - change.start > 0) { undoChange = undoChange.add(change.start, oldText, newText); } else if (change.start != change.end && change.text.isEmpty()) { // I know I am deleting, and am located at the end of the range of the current undo record @@ -1467,6 +1488,9 @@ * behavior as necessary. */ static class UndoRedoChange { + static long prevRecordTime; + static final long CHANGE_DURATION = 2500; // milliseconds + static boolean spaceCharSequence = false; int start; String oldText; String newText; @@ -1482,9 +1506,21 @@ c.newText = newText; c.prev = this; next = c; + prevRecordTime = (new Date()).getTime(); return c; } + static boolean ifChangeDurationElapsed() { + return ((new Date()).getTime() - prevRecordTime > CHANGE_DURATION) ; + } + + static void setSpaceCharSequence(boolean value) { + spaceCharSequence = value; + } + static boolean isSpaceCharSequence() { + return spaceCharSequence; + } + public UndoRedoChange discard() { prev.next = next; return prev; --- old/modules/javafx.controls/src/test/java/test/javafx/scene/control/TextInputControlTest.java 2017-10-04 16:46:41.038902703 +0530 +++ new/modules/javafx.controls/src/test/java/test/javafx/scene/control/TextInputControlTest.java 2017-10-04 16:46:40.858812704 +0530 @@ -46,6 +46,7 @@ import javafx.stage.Stage; import java.util.Arrays; import java.util.Collection; +import java.util.Date; import javafx.scene.control.IndexRange; import javafx.scene.control.PasswordField; import javafx.scene.control.TextArea; @@ -1852,6 +1853,196 @@ textInput.replaceText(0, 4, ""); assertEquals("", textInput.getText()); + + textInput.undo(); + assertEquals("abcd", textInput.getText()); + + textInput.undo(); + assertEquals("abcefg", textInput.getText()); + + textInput.undo(); + assertEquals("abcd", textInput.getText()); + + textInput.undo(); + assertEquals("", textInput.getText()); + } + + // Test for JDK-8178418 + @Test public void UndoRedoSpaceSequence() { + Toolkit tk = (StubToolkit)Toolkit.getToolkit(); + StackPane root = new StackPane(); + Scene scene = new Scene(root); + Stage stage = new Stage(); + String text = "123456789"; + String tempText = ""; + + textInput.setText(text); + stage.setScene(scene); + root.getChildren().removeAll(); + root.getChildren().add(textInput); + stage.show(); + tk.firePulse(); + + KeyEventFirer keyboard = new KeyEventFirer(textInput); + + // Test sequence of spaces + keyboard.doKeyPress(KeyCode.HOME); + tk.firePulse(); + for(int i = 0; i < 10; ++i) { + keyboard.doKeyTyped(KeyCode.SPACE); + tk.firePulse(); + tempText += " "; + } + assertTrue(textInput.getText().equals(tempText + text)); + + textInput.undo(); + assertTrue(textInput.getText().equals(text)); + + textInput.redo(); + assertTrue(textInput.getText().equals(tempText + text)); + + root.getChildren().removeAll(); + stage.hide(); + tk.firePulse(); + } + + // Test for JDK-8178418 + @Test public void UndoRedoReverseSpaceSequence() { + Toolkit tk = (StubToolkit)Toolkit.getToolkit(); + StackPane root = new StackPane(); + Scene scene = new Scene(root); + Stage stage = new Stage(); + String text = "123456789"; + String tempText = ""; + + textInput.setText(text); + stage.setScene(scene); + root.getChildren().removeAll(); + root.getChildren().add(textInput); + stage.show(); + tk.firePulse(); + + KeyEventFirer keyboard = new KeyEventFirer(textInput); + // Test reverse sequence of spaces + keyboard.doKeyPress(KeyCode.HOME); + tk.firePulse(); + for(int i = 0; i < 10; ++i) { + keyboard.doKeyTyped(KeyCode.SPACE); + keyboard.doKeyPress(KeyCode.LEFT); + tk.firePulse(); + tempText += " "; + assertTrue(textInput.getText().equals(tempText + text)); + } + + for(int i = 0; i < 10; ++i) { + textInput.undo(); + tk.firePulse(); + } + assertTrue(textInput.getText().equals(text)); + + tempText = ""; + for(int i = 0; i < 10; ++i) { + textInput.redo(); + tk.firePulse(); + tempText += " "; + assertTrue(textInput.getText().equals(tempText + text)); + } + + root.getChildren().removeAll(); + stage.hide(); + tk.firePulse(); + } + + // Test for JDK-8178418 + @Test public void UndoRedoWords() { + Toolkit tk = (StubToolkit)Toolkit.getToolkit(); + StackPane root = new StackPane(); + Scene scene = new Scene(root); + Stage stage = new Stage(); + String text = "123456789"; + String tempText = ""; + + textInput.setText(text); + stage.setScene(scene); + root.getChildren().removeAll(); + root.getChildren().add(textInput); + stage.show(); + tk.firePulse(); + + KeyEventFirer keyboard = new KeyEventFirer(textInput); + + // Test words separated by space + keyboard.doKeyPress(KeyCode.HOME); + tk.firePulse(); + for(int i = 0; i < 10; ++i) { + keyboard.doKeyTyped(KeyCode.SPACE); + keyboard.doKeyTyped(KeyCode.A); + keyboard.doKeyTyped(KeyCode.B); + tk.firePulse(); + tempText += " AB"; + assertTrue(textInput.getText().equals(tempText + text)); + } + + for(int i = 0; i < 10; ++i) { + textInput.undo(); + tk.firePulse(); + } + assertTrue(textInput.getText().equals(text)); + + tempText = ""; + for(int i = 0; i < 10; ++i) { + textInput.redo(); + tk.firePulse(); + tempText += " AB"; + assertTrue(textInput.getText().equals(tempText + text)); + } + + root.getChildren().removeAll(); + stage.hide(); + tk.firePulse(); + } + + // Test for JDK-8178418 + @Test public void UndoRedoTimestampBased() { + Toolkit tk = (StubToolkit)Toolkit.getToolkit(); + StackPane root = new StackPane(); + Scene scene = new Scene(root); + Stage stage = new Stage(); + String text = "123456789"; + String tempText = ""; + + textInput.setText(text); + stage.setScene(scene); + root.getChildren().removeAll(); + root.getChildren().add(textInput); + stage.show(); + tk.firePulse(); + + KeyEventFirer keyboard = new KeyEventFirer(textInput); + + // Test continuos sequence of characters + // in this case an undo-redo record is added after 2500 mili seconds. + keyboard.doKeyPress(KeyCode.HOME); + tk.firePulse(); + + long startTime = (new Date()).getTime(); + while(((new Date()).getTime() - startTime < 4000)) { + + keyboard.doKeyTyped(KeyCode.A); + tk.firePulse(); + tempText += "A"; + assertTrue(textInput.getText().equals(tempText + text)); + } + + textInput.undo(); + assertFalse(textInput.getText().equals(text)); + textInput.undo(); + tk.firePulse(); + assertTrue(textInput.getText().equals(text)); + + root.getChildren().removeAll(); + stage.hide(); + tk.firePulse(); } // TODO tests for Content firing event notification properly