The Source for Java Technology Collaboration
User: Password:



Scott Violet's Blog

December 2005 Archives


JPasswordField with an empty echo character: the fix

Posted by zixle on December 19, 2005 at 03:38 PM | Permalink | Comments (5)

In my last blog I explained how to customize the visual feedback provided by JPasswordField. Unfortunately the first option I detailed, specifying a character that takes no space, had a bug in it. This blog discusses the bug and how to fix it. If you want to cut to the chase and run the demo it can be found here.

Specifying you want JPasswordField to render a character with no space is as easy as:

passwordField.setEchoCharacter('\u200b');
As my last demo illustrated, this will produce an ArithmeticException when you click on the password field (second one in the demo). As you could probably guess the exception occurs because PasswordView is dividing the width of the character without checking it, resulting in the exception. The fix is to create a subclass of PasswordView that overrides viewToModel without dividing by the character width. Here's the code:
  public int viewToModel(float fx, float fy, Shape boundsAsShape, Position.Bias[] bias) {
    boundsAsShape = adjustAllocation(boundsAsShape);
    Rectangle bounds = (boundsAsShape instanceof Rectangle) ?
                (Rectangle)boundsAsShape : boundsAsShape.getBounds();
    if (fx < bounds.x) {
      return getStartOffset();
    }
    return getEndOffset();
  }
With a character that takes up no space there's no way for the user to click anywhere but the beginning or end of the text. As such, viewToModel will return one of these.

To install this View you need to do something similar to that of the last blog (refer to it for more details). Create a custom JPasswordField, with a custom UI that returns the new View:

  new JPasswordField() {
    public void updateUI() {
      setUI(new BasicPasswordFieldUI() {
        public View create(Element elem) {
          return new EmptySpacePasswordView(elem);
        }
      });
    }
  };
And here's the demo.

That's enough for passwordfield and text, next time around something new.

Variations of JPasswordField

Posted by zixle on December 05, 2005 at 03:37 PM | Permalink | Comments (2)

We've been gathering input from customers on various features they would like to see us implement. At such a meeting one customer requested a variation of JPasswordField. I figured it would be interesting to blog on how you could modify JPasswordField to vary the level of feedback it gives you. I'll cover various pieces of Swing's text architecture in implementing one variation, as well as a demo. If you want to cut to the chase, and have 1.5 installed, here's the demo.

Variation 1: no feedback

JPasswordField allows you to change the character it uses as the echo character. That is, regardless of what character the user types you'll see the echo character in it's place (1). A simple variation on JPasswordField that makes it harder for folks to guess at what you're typing is to change the echo character to a character that takes up no space. You can use '\u200b' for this. This means that someone watching you type at a password field won't be able to tell how many characters are in your password. Of course they can still watch you type at the keyboard, and the user gets very little feedback that they're actually typing. Here's the code for this:
  passwordField.setEchoCharacter('\u200b');

Variation 2: variable amount of space

For those that remember NeXT's login dialog this'll be familiar. Rather than using a single echo character a random amount of white space will be used for each character. This has the benefit over the previous approach in that you can't readily determine how many characters are in the password, but you get some level of visual feedback as you type your password. Apple's latest login dialogs don't use this scheme, which would seem to indicate many folks didn't like it.

All of Swing's text components, be it JPasswordField, JTextField, JEditorPane share the same architecture. What applies to one equally applies to the others. The text component use an instance of View to do the rendering of text. In some ways a View can be thought of as a lightweight Component, for example, View has methods like getPreferredSpan, getMinimumSpan, paint... Just as Containers are typically nested Views are usually nested as well. Views are responsible doing the following:

  • Layout of either text or child Views. This includes calculating a size for the View.
  • Painting.
  • Provide a mapping between model and view, or view and model. For example, what position on screen is the character at location 5 in the model.
  • Updating as the model changes.
To implement the variable width password field we'll provide a custom View implementation, VariableWidthPasswordView. VariableWidthPasswordView will keep a List of integers that correspond to the width of each characters. An instance of Random will be used to determine the amount of white space to use for a particular character:
    private static final int MIN_WIDTH = 2;
    private static final int MAX_WIDTH = 15;

    private List<Integer> widths;
    private Random widthGenerator;

    private final int generateCharacterSize() {
        return widthGenerator.nextInt(MAX_WIDTH - MIN_WIDTH) + MIN_WIDTH;
    }
Swing's text components use Document for modeling the text. As you type at a text component the text is inserted into the Document. As the Document is mutated the Views are notified (as long as the document is associated with a JTextComponent). This allows the Views to stay in sync with the model. VariableWidthPasswordView will override the two View methods insertUpdate and removeUpdate, which are invoked as text is inserted or removed. The implementation of these two methods is roughly the same, they will update the cache of widths and trigger a repaint. Here's the implementation of insertUpdate:
    // Invoked when text has been inserted. Updates the width List appropriately
    public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
        for (int i = 0; i < changes.getLength(); i++) {
            // Notice the autoboxing of int to Integer here.
            widths.add(i + changes.getOffset(), generateCharacterSize());
        }
        // Invoke super to update the visibility appropriately.
        super.insertUpdate(changes, a, f);
    }
Invoking insertUpdate on super is done to update JPasswordFields visibility model. It will also signal the preferred size of the View has changed and repaint.

VariableWidthPasswordView doesn't need to paint, so it no-ops the appropriate paint methods. The primary View method for painting is paint. VariableWidthPasswordView is a subclass of FieldView which implements paint to invoke drawSelectedText and drawUnselectedText. VariableWidthPasswordView will override drawSelectedText and drawUnselectedText to do nothing. This is done rather than overriding paint as paint is also responsible for invoking the appropriate methods to render the selection. By overriding drawUnselectedText and drawSelectedText to do nothing the selection is still rendered appropriately by FieldView.

To map from a position of text in the model to that of the view VariableWidthPasswordView overrides modelToView. modelToView uses the List of widths to determine the position. Here's the implementation:

    public Shape modelToView(int pos, Shape a, Position.Bias b)
            throws BadLocationException {
        // JPasswordField and JTextField implement their own virtual
        // coordinate system. This does the necessary mapping.
        Rectangle bounds = adjustAllocation(a).getBounds();
        bounds.x += modelToView(pos);
        bounds.width = 1;
        return bounds;
    }
    // Returns the view x coordinate in the views space for the specified
    // model coordinate.
    private final int modelToView(int p1) {
        int offset = 0;
        int end = Math.min(widths.size(), p1);
        for (int i = 0; i < end; i++) {
            // Notice the auto unboxing here.
            offset += widths.get(i);
        }
        return offset;
    }
viewToModel does the inverse mapping:
    public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
        a = adjustAllocation(a);
        Rectangle bounds = (a instanceof Rectangle) ? (Rectangle)a : a.getBounds();
        int x = (int)fx - bounds.x;
        for (int i = 0; i < widths.size(); i++) {
            // More auto unboxing.
            x -= widths.get(i);
            if (x <= 0) {
                return i;
            }
        }
        return getEndOffset();
    }
The last thing to mention is calculating the preferred size for the View. That's done in terms of the widths field as well:
    public float getPreferredSpan(int axis) {
        if (axis == View.X_AXIS) {
            int size = 0;
            for (Integer width : widths) {
                size += width;
            }
            return size;
        }
        return super.getPreferredSpan(axis);
    }
Now that we have the View implementation we need to modify JPasswordField to use it. Views are created by a ViewFactory, which is obtained from the EditorKit. It's up to the TextUI implementation to provide the EditorKit and ViewFactory. Unfortunately this means creating a custom UI to plug-in VariableWidthPasswordView. BasicPasswordFieldUI is itself a ViewFactory, so that by subclassing BasicPasswordFieldUI we can plug-in VariableWidthPasswordView. Here's the code for it:
   // We generally discourage using UIs from one look and feel
   // with another look and feel. When using metal this will be fine,
   // that may not be true when using other look and feels.
   JPasswordField myPasswordField = new JPasswordField(10) {
        public void updateUI() {
            setUI(new BasicPasswordFieldUI() {
                public View create(Element elem) {
                    return new VariableWidthPasswordView(elem);
                }
            });
        }
    };
The complete source for VariableWidthPasswordView can be found here. And the demo showing the various takes on password field can be found here. For more information on Swing's text component take a look at the Swing tutorial section on Using Text Components or Tim's article on Using the Swing Text Package.

1: As this demo shows it's possible to write a UI that ignores the echo character. All of Sun's UIs honor the echo character, but third party look and feels may not.



Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds