Class PasswordField


public final class PasswordField extends TextField
TextField that renders every character as a mask dot, with an optional reveal toggle. While masked, copy/cut are blocked (the clipboard never sees the secret); paste and all editing behave like a TextField.

The dot is drawn, not typeset; see DOT_DIAMETER. Everything else (geometry, padding, the animated border) is inherited from TextField: this class declares no extent of its own beyond the dot's two ratios.

The content is never shaped. shapeDisplay(java.lang.String, limn.graphics.Font) builds the masked line from ShapedText.uniform(java.lang.String, limn.graphics.Font, float, limn.graphics.TextMetrics, long), which is one multiplication and no glyphs, so the secret never reaches a shaper nor the memo a shaper keeps — and the caret, the click mapping, the selection band and the painted dots then all come out of that one piece of arithmetic instead of two that have to be kept in agreement.

What an assistive technology is told follows the same toggle. Masked, the node is a password field carrying the mask — one bullet per painted dot, with the caret and the selection in the mask's own offsets — and revealed it is a plain text field carrying the value under the model's. The role and the masked state move together, so no platform ever sees a password role over cleartext; see onAccessibility(limn.accessibility.Accessibility).

  • Constructor Details

    • PasswordField

      public PasswordField()
  • Method Details

    • isRevealed

      public boolean isRevealed()
      Whether the characters are shown instead of the mask.
    • setRevealed

      public PasswordField setRevealed(boolean newRevealed)
      Shows/hides the real text (the optional "reveal" toggle).
    • shapeDisplay

      protected ShapedText shapeDisplay(String text, Font font)
      The masked line: one dot cell per grapheme cluster, built by arithmetic and not by a shaper.

      ShapedText.uniform(java.lang.String, limn.graphics.Font, float, limn.graphics.TextMetrics, long) is what makes the whole class two overrides instead of a pair of parallel measurements. It gives a line whose index space is the model's, so the caret, the selection range and a click need no translation from source offsets to mask offsets — the count-preserving substitution the old mask string had to guarantee is not a thing that can go wrong any more, because there is no second string. And the content does not reach shape, which is the reason this override exists: a shaper resolves faces, memoizes what it was asked, and would be a place a secret comes to rest.

      Cells are grapheme clusters, so one dot stands for one user-perceived character and the caret can never land at a fractional dot: an astral character is one dot, not two, which also keeps the mask from leaking that a character was astral.

      Overrides:
      shapeDisplay in class TextField
      Parameters:
      text - the model's text, which is the index space of the result
      font - the font the line is drawn in
      Returns:
      the shaped display line; never null
    • paintDisplayText

      protected void paintDisplayText(Canvas canvas, ShapedText display, float x, float baseline, TextMetrics metrics, SizeTokens t, Color ink)
      Paints the mask as circles on the ink box's centre line.

      The centre is baseline - ascent + height/2, the middle of the same band the selection fill and the caret span, so the dots stay centred against any face the field's own text would use, at any step, with no per-font vertical fudge. (A dot has no baseline of its own to sit on: this is why the vertical anchor is the band and not the baseline.)

      Overrides:
      paintDisplayText in class TextField
      Parameters:
      canvas - where to draw
      display - the shaped display line, as TextField.shapeDisplay(java.lang.String, limn.graphics.Font) produced it
      x - left edge of the line, in this widget's coordinates
      baseline - the text baseline, in this widget's coordinates
      metrics - the line's vertical band
      t - the size row resolved for this pass
      ink - the colour to draw in
    • allowClipboardCopy

      protected boolean allowClipboardCopy()
      Description copied from class: TextField
      Whether copy/cut may export the content (password fields say no).
      Overrides:
      allowClipboardCopy in class TextField
    • allowsWordJumps

      protected boolean allowsWordJumps()
      While masked, word-wise caret jumps and deletes would let an observer count the words and their lengths inside the secret; modifier+arrows degrade to per-character moves until revealed.
      Overrides:
      allowsWordJumps in class TextField
    • acceptsTextInput

      protected boolean acceptsTextInput()
      Secure text entry keeps the platform IME off (the convention of NSSecureTextField/ES_PASSWORD): composition would echo the secret in cleartext between the mask characters, surface it in the OS candidate window, and feed it to the IME's learning dictionary. The scene also drops preedit events for widgets that refuse text input, so no composition can reach this field even mid-teardown. Direct key/char input (including paste) is unaffected.
      Overrides:
      acceptsTextInput in class TextField
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      The same node TextField publishes, said to be masked and carrying the mask in place of the contents — one bullet per painted dot, with the caret and the selection in the mask's own offsets.

      The role and the PASSWORD state follow the toggle, and they move together. Stating both flat and conditioning only the facet on the flag would contradict itself: State.PASSWORD says in its own words that the text is masked and must never be spoken, which is not a thing to publish beside the cleartext the facet carries when the field is revealed. Which of the two follows the flag is then the real question, and it is the role as well, because a platform's refusal to speak is keyed on the role and not on the state — AT-SPI's password role, AppKit's secure-field class, Role.PASSWORD_FIELD's own definition as "a text field whose content is masked". A revealed field left under that role would be the one control on screen a blind user cannot read after deliberately asking to, which is the whole purpose of the toggle. Moving the two together is what keeps any platform from ever seeing a password role over cleartext, or a plain field over a mask.

      The role is written after the super call and cannot move before it, for the reason SearchField records: a role is a plain store into the node's slot, the base writes TEXT_FIELD into it, and it lands on this widget's node rather than on the trailing button's only because the base closes every synthetic child it opened.

      Everything else is inherited and deliberately not restated: the placeholder names the node, EDITABLE and the context menu are the base's, and a trailing button — the natural place to hang a reveal control — is the base's synthetic child with the base's box. READ_ONLY is never set: a masked field is editable, and masking is not read-only. There is no node for the reveal toggle itself, because the toggle is a public setter an application drives from a control of its own and this widget draws nothing for it, and a synthetic child has to be something its owner draws.

      The facet is republished here in both branches, over whatever the base wrote, because the two branches have to share one witness; publishedText carries the whole of that reasoning. The base's write while revealed is a dead set of stores this hook overwrites in full, which costs nothing.

      Overrides:
      onAccessibility in class TextField
      Parameters:
      a - the node being described
    • onAccessibilityAction

      protected boolean onAccessibilityAction(Accessible.Action action, Accessible.Argument arg)
      Translates a caret or a selection back out of the mask's offsets before the field places it, and refuses one the mask has no stop for.

      This is the half the forward mapping leaves out, and it is where the bug is. Publishing maps a model offset forward to a mask ordinal for the facet and stops there. The verbs come back the other way: a client holds the published string, so it asks in mask ordinals, and the inherited handler consumes model char offsets. Left alone, a reader asking for the caret after the second dot of a value whose first character is astral lands inside the surrogate pair, silently — the shorter buffer's own bounds check passes, which is the same shape of failure the base refuses an offset-carrying verb over during an IME composition.

      An ordinal outside the mask is refused and never clamped, because ShapedText.caretIndex(int) clamps: without the check a client asking for dot forty of a three-dot field would be quietly answered with the end of the value, which is the mistake the base states its own reason for refusing rather than hiding.

      Everything else goes to super and must. The context menu is still the only route a reader has to Cut, Copy, Paste and Select All, and the two the mask must not let out are already greyed by allowClipboardCopy() rather than by suppressing the menu. The whole-value set carries no offsets at all, needs no translation, and is how a password manager fills the field.

      Overrides:
      onAccessibilityAction in class TextField
      Parameters:
      action - what is being asked
      arg - the text for a whole-value set, and the mask's own range for the other two
      Returns:
      whether this widget did it
    • setMask

      public TextField setMask(@Nullable String pattern)
      Refused: a password is not formatted. A pattern's literals and sections would be drawn as dots like everything else, and its "masked" would mean two things on one field, so a password field takes no input mask. null, which takes a mask away, is accepted and does nothing. TextField.unmaskedText() is therefore the password as typed.
      Overrides:
      setMask in class TextField
      Parameters:
      pattern - null
      Returns:
      this field
      Throws:
      UnsupportedOperationException - for any pattern