FormattedTextFieldExample.java Source code

Java tutorial

Introduction

Here is the source code for FormattedTextFieldExample.java

Source

/*
Core SWING Advanced Programming 
By Kim Topley
ISBN: 0 13 083292 8       
Publisher: Prentice Hall  
*/

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.metal.MetalTextFieldUI;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.FieldView;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import javax.swing.text.Position;
import javax.swing.text.Segment;
import javax.swing.text.Utilities;
import javax.swing.text.View;
import javax.swing.text.ViewFactory;

public class FormattedTextFieldExample {
    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
        } catch (Exception evt) {
        }

        JFrame f = new JFrame("Formatted Text Field");
        final FormattedTextField tf = new FormattedTextField();

        FormattedTextField.FormatSpec formatSpec = new FormattedTextField.FormatSpec("(...)-...-....",
                "(***)-***-****");
        tf.setFormatSpec(formatSpec);
        f.getContentPane().add(tf, "Center");
        f.getContentPane().add(new JLabel("Phone Number: ", JLabel.RIGHT), "West");

        tf.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent evt) {
                System.out.println("Field content is <" + tf.getText() + ">");
            }
        });

        f.pack();
        f.setVisible(true);
    }
}

class FormattedTextField extends JTextField {
    public FormattedTextField() {
        this(null, null, 0, null);
    }

    public FormattedTextField(String text, FormatSpec spec) {
        this(null, text, 0, spec);
    }

    public FormattedTextField(int columns, FormatSpec spec) {
        this(null, null, columns, spec);
    }

    public FormattedTextField(String text, int columns, FormatSpec spec) {
        this(null, text, columns, spec);
    }

    public FormattedTextField(Document doc, String text, int columns, FormatSpec spec) {
        super(doc, text, columns);
        setFont(new Font("monospaced", Font.PLAIN, 14));
        if (spec != null) {
            setFormatSpec(spec);
        }
    }

    public void updateUI() {
        setUI(new FormattedTextFieldUI());
    }

    public FormatSpec getFormatSpec() {
        return formatSpec;
    }

    public void setFormatSpec(FormattedTextField.FormatSpec formatSpec) {
        FormatSpec oldFormatSpec = this.formatSpec;

        // Do nothing if no change to the format specification
        if (formatSpec.equals(oldFormatSpec) == false) {
            this.formatSpec = formatSpec;

            // Limit the input to the number of markers.
            Document doc = getDocument();
            if (doc instanceof BoundedPlainDocument) {
                ((BoundedPlainDocument) doc).setMaxLength(formatSpec.getMarkerCount());
            }

            // Notify a change in the format spec
            firePropertyChange(FORMAT_PROPERTY, oldFormatSpec, formatSpec);
        }
    }

    // Use a model that bounds the input length
    protected Document createDefaultModel() {
        BoundedPlainDocument doc = new BoundedPlainDocument();

        doc.addInsertErrorListener(new BoundedPlainDocument.InsertErrorListener() {
            public void insertFailed(BoundedPlainDocument doc, int offset, String str, AttributeSet a) {
                // Beep when the field is full
                Toolkit.getDefaultToolkit().beep();
            }
        });
        return doc;
    }

    public static class FormatSpec {
        public FormatSpec(String format, String mask) {
            this.format = format;
            this.mask = mask;
            this.formatSize = format.length();
            if (formatSize != mask.length()) {
                throw new IllegalArgumentException("Format and mask must be the same size");
            }

            for (int i = 0; i < formatSize; i++) {
                if (mask.charAt(i) == MARKER_CHAR) {
                    markerCount++;
                }
            }
        }

        public String getFormat() {
            return format;
        }

        public String getMask() {
            return mask;
        }

        public int getFormatSize() {
            return formatSize;
        }

        public int getMarkerCount() {
            return markerCount;
        }

        public boolean equals(Object fmt) {
            return fmt != null && (fmt instanceof FormatSpec) && ((FormatSpec) fmt).getFormat().equals(format)
                    && ((FormatSpec) fmt).getMask().equals(mask);
        }

        public String toString() {
            return "FormatSpec with format <" + format + ">, mask <" + mask + ">";
        }

        private String format;

        private String mask;

        private int formatSize;

        private int markerCount;

        public static final char MARKER_CHAR = '*';
    }

    protected FormatSpec formatSpec;

    public static final String FORMAT_PROPERTY = "format";
}

class BoundedPlainDocument extends PlainDocument {
    public BoundedPlainDocument() {
        // Default constructor - must use setMaxLength later
        this.maxLength = 0;
    }

    public BoundedPlainDocument(int maxLength) {
        this.maxLength = maxLength;
    }

    public BoundedPlainDocument(AbstractDocument.Content content, int maxLength) {
        super(content);
        if (content.length() > maxLength) {
            throw new IllegalArgumentException("Initial content larger than maximum size");
        }
        this.maxLength = maxLength;
    }

    public void setMaxLength(int maxLength) {
        if (getLength() > maxLength) {
            throw new IllegalArgumentException("Current content larger than new maximum size");
        }

        this.maxLength = maxLength;
    }

    public int getMaxLength() {
        return maxLength;
    }

    public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
        if (str == null) {
            return;
        }

        // Note: be careful here - the content always has a
        // trailing newline, which should not be counted!
        int capacity = maxLength + 1 - getContent().length();
        if (capacity >= str.length()) {
            // It all fits
            super.insertString(offset, str, a);
        } else {
            // It doesn't all fit. Add as much as we can.
            if (capacity > 0) {
                super.insertString(offset, str.substring(0, capacity), a);
            }

            // Finally, signal an error.
            if (errorListener != null) {
                errorListener.insertFailed(this, offset, str, a);
            }
        }
    }

    public void addInsertErrorListener(InsertErrorListener l) {
        if (errorListener == null) {
            errorListener = l;
            return;
        }
        throw new IllegalArgumentException("InsertErrorListener already registered");
    }

    public void removeInsertErrorListener(InsertErrorListener l) {
        if (errorListener == l) {
            errorListener = null;
        }
    }

    public interface InsertErrorListener {
        public abstract void insertFailed(BoundedPlainDocument doc, int offset, String str, AttributeSet a);
    }

    protected InsertErrorListener errorListener; // Unicast listener

    protected int maxLength;
}

class FormattedTextFieldUI extends MetalTextFieldUI implements PropertyChangeListener {
    public static ComponentUI createUI(JComponent c) {
        return new FormattedTextFieldUI();
    }

    public FormattedTextFieldUI() {
        super();
    }

    public void installUI(JComponent c) {
        super.installUI(c);

        if (c instanceof FormattedTextField) {
            c.addPropertyChangeListener(this);
            editor = (FormattedTextField) c;
            formatSpec = editor.getFormatSpec();
        }
    }

    public void uninstallUI(JComponent c) {
        super.uninstallUI(c);
        c.removePropertyChangeListener(this);
    }

    public void propertyChange(PropertyChangeEvent evt) {
        if (evt.getPropertyName().equals(FormattedTextField.FORMAT_PROPERTY)) {
            // Install the new format specification
            formatSpec = editor.getFormatSpec();

            // Recreate the View hierarchy
            modelChanged();
        }
    }

    // ViewFactory method - creates a view
    public View create(Element elem) {
        return new FormattedFieldView(elem, formatSpec);
    }

    protected FormattedTextField.FormatSpec formatSpec;

    protected FormattedTextField editor;
}

class FormattedFieldView extends FieldView {
    public FormattedFieldView(Element elem, FormattedTextField.FormatSpec formatSpec) {
        super(elem);

        this.contentBuff = new Segment();
        this.measureBuff = new Segment();
        this.workBuff = new Segment();
        this.element = elem;

        buildMapping(formatSpec); // Build the model -> view map
        createContent(); // Update content string
    }

    // View methods start here
    public float getPreferredSpan(int axis) {
        int widthFormat;
        int widthContent;

        if (formatSize == 0 || axis == View.Y_AXIS) {
            return super.getPreferredSpan(axis);
        }

        widthFormat = Utilities.getTabbedTextWidth(measureBuff, getFontMetrics(), 0, this, 0);
        widthContent = Utilities.getTabbedTextWidth(contentBuff, getFontMetrics(), 0, this, 0);

        return Math.max(widthFormat, widthContent);
    }

    public Shape modelToView(int pos, Shape a, Position.Bias b) throws BadLocationException {
        a = adjustAllocation(a);
        Rectangle r = new Rectangle(a.getBounds());
        FontMetrics fm = getFontMetrics();
        r.height = fm.getHeight();

        int oldCount = contentBuff.count;

        if (pos < offsets.length) {
            contentBuff.count = offsets[pos];
        } else {
            // Beyond the end: point to the location
            // after the last model position.
            contentBuff.count = offsets[offsets.length - 1] + 1;
        }

        int offset = Utilities.getTabbedTextWidth(contentBuff, metrics, 0, this, element.getStartOffset());
        contentBuff.count = oldCount;

        r.x += offset;
        r.width = 1;

        return r;
    }

    public int viewToModel(float fx, float fy, Shape a, Position.Bias[] bias) {
        a = adjustAllocation(a);
        bias[0] = Position.Bias.Forward;

        int x = (int) fx;
        int y = (int) fy;
        Rectangle r = a.getBounds();
        int startOffset = element.getStartOffset();
        int endOffset = element.getEndOffset();

        if (y < r.y || x < r.x) {
            return startOffset;
        } else if (y > r.y + r.height || x > r.x + r.width) {
            return endOffset - 1;
        }

        // The given position is within the bounds of the view.
        int offset = Utilities.getTabbedTextOffset(contentBuff, getFontMetrics(), r.x, x, this, startOffset);
        // The offset includes characters not in the model,
        // so get rid of them to return a true model offset.
        for (int i = 0; i < offsets.length; i++) {
            if (offset <= offsets[i]) {
                offset = i;
                break;
            }
        }

        // Don't return an offset beyond the data
        // actually in the model.
        if (offset > endOffset - 1) {
            offset = endOffset - 1;
        }
        return offset;
    }

    public void insertUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
        super.insertUpdate(changes, adjustAllocation(a), f);
        createContent(); // Update content string
    }

    public void removeUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
        super.removeUpdate(changes, adjustAllocation(a), f);
        createContent(); // Update content string
    }

    // End of View methods

    // View drawing methods: overridden from PlainView
    protected void drawLine(int line, Graphics g, int x, int y) {
        // Set the colors
        JTextComponent host = (JTextComponent) getContainer();
        unselected = (host.isEnabled()) ? host.getForeground() : host.getDisabledTextColor();
        Caret c = host.getCaret();
        selected = c.isSelectionVisible() ? host.getSelectedTextColor() : unselected;

        int p0 = element.getStartOffset();
        int p1 = element.getEndOffset() - 1;
        int sel0 = ((JTextComponent) getContainer()).getSelectionStart();
        int sel1 = ((JTextComponent) getContainer()).getSelectionEnd();

        try {
            // If the element is empty or there is no selection
            // in this view, just draw the whole thing in one go.
            if (p0 == p1 || sel0 == sel1 || inView(p0, p1, sel0, sel1) == false) {
                drawUnselectedText(g, x, y, 0, contentBuff.count);
                return;
            }

            // There is a selection in this view. Draw up to three regions:
            //   (a) The unselected region before the selection.
            //   (b) The selected region.
            //   (c) The unselected region after the selection.
            // First, map the selected region offsets to be relative
            // to the start of the region and then map them to view
            // offsets so that they take into account characters not
            // present in the model.
            int mappedSel0 = mapOffset(Math.max(sel0 - p0, 0));
            int mappedSel1 = mapOffset(Math.min(sel1 - p0, p1 - p0));

            if (mappedSel0 > 0) {
                // Draw an initial unselected region
                x = drawUnselectedText(g, x, y, 0, mappedSel0);
            }
            x = drawSelectedText(g, x, y, mappedSel0, mappedSel1);

            if (mappedSel1 < contentBuff.count) {
                drawUnselectedText(g, x, y, mappedSel1, contentBuff.count);
            }
        } catch (BadLocationException e) {
            // Should not happen!
        }
    }

    protected int drawUnselectedText(Graphics g, int x, int y, int p0, int p1) throws BadLocationException {
        g.setColor(unselected);
        workBuff.array = contentBuff.array;
        workBuff.offset = p0;
        workBuff.count = p1 - p0;
        return Utilities.drawTabbedText(workBuff, x, y, g, this, p0);
    }

    protected int drawSelectedText(Graphics g, int x, int y, int p0, int p1) throws BadLocationException {
        workBuff.array = contentBuff.array;
        workBuff.offset = p0;
        workBuff.count = p1 - p0;
        g.setColor(selected);
        return Utilities.drawTabbedText(workBuff, x, y, g, this, p0);
    }

    // End of View drawing methods

    // Build the model-to-view mapping
    protected void buildMapping(FormattedTextField.FormatSpec formatSpec) {
        formatSize = formatSpec != null ? formatSpec.getFormatSize() : 0;

        if (formatSize != 0) {
            // Save the format string as a character array
            formatChars = formatSpec.getFormat().toCharArray();

            // Allocate a buffer to store the formatted string
            formattedContent = new char[formatSize];
            contentBuff.offset = 0;
            contentBuff.count = formatSize;
            contentBuff.array = formattedContent;

            // Keep the mask for computing
            // the preferred horizontal span, but use
            // a wide character for measurement
            char[] maskChars = formatSpec.getMask().toCharArray();
            measureBuff.offset = 0;
            measureBuff.array = maskChars;
            measureBuff.count = formatSize;

            // Get the number of markers
            markerCount = formatSpec.getMarkerCount();

            // Allocate an array to hold the offsets
            offsets = new int[markerCount];

            // Create the offset array
            markerCount = 0;
            for (int i = 0; i < formatSize; i++) {
                if (maskChars[i] == FormattedTextField.FormatSpec.MARKER_CHAR) {
                    offsets[markerCount++] = i;

                    // Replace marker with a wide character
                    // in the array used for measurement.
                    maskChars[i] = WIDE_CHARACTER;
                }
            }
        }
    }

    // Use the document content and the format
    // string to build the display content
    protected void createContent() {
        try {
            Document doc = getDocument();
            int startOffset = element.getStartOffset();
            int endOffset = element.getEndOffset();
            int length = endOffset - startOffset - 1;

            // If there is no format, use the raw data.
            if (formatSize != 0) {
                // Get the document content
                doc.getText(startOffset, length, workBuff);

                // Initialize the output buffer with the
                // format string.
                System.arraycopy(formatChars, 0, formattedContent, 0, formatSize);

                // Insert the model content into
                // the target string.
                int count = Math.min(length, markerCount);
                int firstOffset = workBuff.offset;

                // Place the model data into the output array
                for (int i = 0; i < count; i++) {
                    formattedContent[offsets[i]] = workBuff.array[i + firstOffset];
                }
            } else {
                doc.getText(startOffset, length, contentBuff);
            }
        } catch (BadLocationException bl) {
            contentBuff.count = 0;
        }
    }

    // Map a document offset to a view offset.
    protected int mapOffset(int pos) {
        pos -= element.getStartOffset();
        if (pos >= offsets.length) {
            return contentBuff.count;
        } else {
            return offsets[pos];
        }
    }

    // Determines whether the selection intersects
    // a given range of model offsets.
    protected boolean inView(int p0, int p1, int sel0, int sel1) {
        if (sel0 >= p0 && sel0 < p1) {
            return true;
        }

        if (sel0 < p0 && sel1 >= p0) {
            return true;
        }

        return false;
    }

    protected char[] formattedContent; // The formatted content for display

    protected char[] formatChars; // The format string as characters

    protected Segment contentBuff; // Segment pointing to formatted content

    protected Segment measureBuff; // Segment pointing to mask string

    protected Segment workBuff; // Segment used for scratch purposes

    protected Element element; // The mapped element

    protected int[] offsets; // Model-to-view offsets

    protected Color selected; // Selected text color

    protected Color unselected; // Unselected text color

    protected int formatSize; // Length of the formatting string

    protected int markerCount; // Number of markers in the format

    protected static final char WIDE_CHARACTER = 'm';
}