This discussion is archived
1 Reply Latest reply: Jan 13, 2013 7:25 AM by James_D RSS

Format Number in Textfield with 1000 separator

editOr Newbie
Currently Being Moderated
Hi,

I'm entering numeric values (Doubles) into a textfield. Since those numbers can be pretty large I'd like to have a 1000-separator for orientation purposes. This separator should update every time I modify the textfield.
I tried to format the number in the changeListener using the class DecimalFormat. But when adding separators this leads to 2 problems:
1. The Cursor position does not stay at the place you whould expect it to be (since separators are added)
2. When editing (using the "del"-key every time this key stroke deletes a separator nothing happens. This seems clear since the code does the following: Delete separator -> Change -> format number (which n fact is not changed when deleting the separator) --> separator is added again (which brings me to the beginning again).
This leads to the result, that the number is only deleted until the next separator when holding the "delete"-key, which is pretty uncomfortable.

Unfortunately I couldn't find a solution that works for me. I thought this might be a common problem and can be solved a more elegant way, maybe completely different way?

Any help much appreciated
Michael

Edited by: editOr on 12.01.2013 13:01
  • 1. Re: Format Number in Textfield with 1000 separator
    James_D Guru
    Currently Being Moderated
    I would try to subclass TextField and override the replaceText method. You can override other text modification methods to refer to that one.

    It appears that the default behavior is to position the carat after the handling of the event that makes changes to the text is complete. This means that any carat positioning you do in these methods will subsequently be replaced by the default carat positioning. A workaround is to set the carat position later on the FX Application Thread, using Platform.runLater(...), though this feels like a hack.

    As far as I can see, to correctly position the carat, you'll need to track the actual changes the formatting makes to the changes to the text in relation to the position of the insertion or deletion. So you may as well do the formatting yourself, rather than use the DecimalFormat class. This is the basic idea: but you will need to experiment a bit to make sure this works. (Also, it doesn't support exponential notation such as 1.0E5.)
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.scene.Scene;
    import javafx.scene.control.IndexRange;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class DecimalFormatTextFieldTest extends Application {
    
      @Override
      public void start(Stage primaryStage) {
        TextField textField = new FormattedDoubleTextField();
    
        BorderPane root = new BorderPane();
        root.setTop(textField);
        Scene scene = new Scene(root, 200, 200);
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      public static void main(String[] args) {
        launch(args);
      }
    
      private class FormattedDoubleTextField extends TextField {
        @Override
        public void replaceText(int start, int end, String text) {
          // Really crude attempt at parsing. Probably better ways to do this.
    
          // Mock up the result of inserting the text "as is"
          StringBuilder mockupText = new StringBuilder(getText());
          mockupText.replace(start, end, text);
    
          // Strip the commas out, they will need to move anyway
          int commasRemovedBeforeInsert = 0;
          for (int commaIndex = mockupText.lastIndexOf(","); commaIndex >= 0; commaIndex = mockupText
              .lastIndexOf(",")) {
            mockupText.replace(commaIndex, commaIndex + 1, "");
            if (commaIndex < start) {
              commasRemovedBeforeInsert++;
            }
          }
    
          // Check if the inserted text is ok (still forms a number)
          boolean ok = true;
          int decimalPointCount = 0;
          for (int i = 0; i < mockupText.length() && ok; i++) {
            char c = mockupText.charAt(i);
            if (c == '-') {
              ok = i == 0;
            } else if (c == '.') {
              ok = decimalPointCount == 0;
            } else {
              ok = Character.isDigit(c);
            }
          }
    
          // if it's ok, insert the commas in the correct place, update the text,
          // and position the carat:
          if (ok) {
            int commasInsertedBeforeInsert = 0;
            int startNonFractional = 0;
            if (mockupText.length() > 0 && mockupText.charAt(0) == '-') {
              startNonFractional = 1;
            }
            int endNonFractional = mockupText.indexOf(".");
            if (endNonFractional == -1) {
              endNonFractional = mockupText.length();
            }
            for (int commaInsertIndex = endNonFractional - 3; commaInsertIndex > startNonFractional; commaInsertIndex -= 3) {
              mockupText.insert(commaInsertIndex, ",");
              if (commaInsertIndex < start - commasRemovedBeforeInsert
                  + text.length()) {
                commasInsertedBeforeInsert++;
              }
            }
    
            final int caratPos = start - commasRemovedBeforeInsert
                + commasInsertedBeforeInsert + text.length();
    
            // System.out.printf("Original text: %s. Replaced text: %s. start: %d. end: %d. commasInsertedBeforeInsert: %d. commasRemovedBeforeInsert: %d. caratPos: %d.%n",
            // getText(), mockupText, start, end, commasInsertedBeforeInsert,
            // commasRemovedBeforeInsert, caratPos);
    
            // update the text:
            this.setText(mockupText.toString());
    
            // move the carat:
            // Needs to be scheduled to the fx application thread after the current
            // event has finished processing to override
            // default behavior
            // This seems like a bit of a hack...
            Platform.runLater(new Runnable() {
              @Override
              public void run() {
                positionCaret(caratPos);
              }
            });
          }
        }
    
        @Override
        public void replaceText(IndexRange range, String text) {
          this.replaceText(range.getStart(), range.getEnd(), text);
        }
    
        @Override
        public void insertText(int index, String text) {
          this.replaceText(index, index, text);
        }
    
        @Override
        public void deleteText(int start, int end) {
    
          // special case where user deletes a comma:
          if (start >= 1 && end - start == 1 && getText().charAt(start) == ',') {
            // move cursor back
            this.selectRange(getAnchor() - 1, getAnchor() - 1);
          } else {
            this.replaceText(start, end, "");
          }
        }
    
        @Override
        public void deleteText(IndexRange range) {
          this.deleteText(range.getStart(), range.getEnd());
        }
    
        @Override
        public void replaceSelection(String replacement) {
          this.replaceText(getSelection(), replacement);
        }
      }
    
    }
    Edited by: James_D on Jan 12, 2013 8:05 PM

    Edited by: James_D on Jan 13, 2013 7:17 AM

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points