001/**
002 *   Please feel free to use any fragment of the code in this file that you need
003 *   in your own work. As far as I am concerned, it's in the public domain. No
004 *   permission is necessary or required. Credit is always appreciated if you
005 *   use a large chunk or base a significant product on one of my examples,
006 *   but that's not required either.
007 *
008 *   This code is distributed in the hope that it will be useful,
009 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
010 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
011 *
012 *      --- Joseph A. Huwaldt
013 */
014package jahuwaldt.swing;
015
016import java.awt.Toolkit;
017import java.text.DecimalFormat;
018import java.text.DecimalFormatSymbols;
019import java.text.Format;
020import java.text.ParsePosition;
021import javax.swing.text.AttributeSet;
022import javax.swing.text.BadLocationException;
023import javax.swing.text.PlainDocument;
024
025/**
026 * A formatted document that uses a user supplied Format object to control the format of
027 * the text in the document. For example, this document will ignore anything that isn't a
028 * number if a NumberFormat is supplied.
029 *
030 * <p> Modified by: Joseph A. Huwaldt </p>
031 *
032 * @author Joseph A. Huwaldt, Date: February 24, 2000
033 * @version February 23, 2025
034 */
035@SuppressWarnings("serial")
036public class FormattedDocument extends PlainDocument {
037
038    private final Format format;
039    private final ParsePosition pos = new ParsePosition(0);
040    private String decimalSymbol = null;
041    private String minusSignSymbol = null;
042
043    /**
044     * Construct a formatted document that uses the supplied Format object.
045     *
046     * @param f The format to use for this document.
047     */
048    public FormattedDocument(Format f) {
049        format = f;
050        if (format instanceof DecimalFormat) {
051            DecimalFormatSymbols symbols = ((DecimalFormat)format).getDecimalFormatSymbols();
052            decimalSymbol = String.valueOf(symbols.getDecimalSeparator());
053            minusSignSymbol = String.valueOf(symbols.getMinusSign());
054        }
055    }
056
057    /**
058     * Method that returns the Format used by this document.
059     *
060     * @return The format used by this document.
061     */
062    public Format getFormat() {
063        return format;
064    }
065
066    /**
067     * Inserts some content into the document using the documents Format to validate what
068     * has be inserted. Inserting content causes a write lock to be held while the actual
069     * changes are taking place, followed by notification to the observers on the thread
070     * that grabbed the write lock.
071     *
072     * @param offs the starting offset &ge; 0
073     * @param str  the string to insert; does nothing with null/empty strings
074     * @param a    the attributes for the inserted content
075     * @throws javax.swing.text.BadLocationException the given insert position is not 
076     * a valid position within the document.
077     */
078    @Override
079    public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
080
081        if (str == null || str.length() == 0)
082            return;
083
084        String currentText = getText(0, getLength());
085        String beforeOffset = currentText.substring(0, offs);
086        String afterOffset = currentText.substring(offs, currentText.length());
087        String proposedResult = beforeOffset + str + afterOffset;
088
089        //  Work around a couple of "bugs" in DecimalFormat.parseObject().
090        //  Allow leading decimal places when typing in a number.
091        if (format instanceof DecimalFormat
092                && (proposedResult.equals(decimalSymbol) || proposedResult.equals(minusSignSymbol + decimalSymbol)))
093            super.insertString(offs, str, a);
094
095        else {
096            pos.setIndex(0);
097            format.parseObject(proposedResult, pos);
098            if (pos.getIndex() == proposedResult.length())
099                super.insertString(offs, str, a);
100
101            else {
102                System.err.println("FormattedDocument.insertString() error.");
103                Toolkit.getDefaultToolkit().beep();
104            }
105        }
106    }
107
108    /**
109     * Removes some content from the document. Removing content causes a write lock to be
110     * held while the actual changes are taking place. Observers are notified of the
111     * change on the thread that called this method.
112     *
113     * @param offs the starting offset &ge; 0
114     * @param len  the number of characters to remove &ge; 0
115     * @throws javax.swing.text.BadLocationException the given remove position is
116     * not a valid position within the document
117     */
118    @Override
119    public void remove(int offs, int len) throws BadLocationException {
120        String currentText = getText(0, getLength());
121        String beforeOffset = currentText.substring(0, offs);
122        String afterOffset = currentText.substring(len + offs, currentText.length());
123        String proposedResult = beforeOffset + afterOffset;
124
125        if (proposedResult.length() != 0) {
126            pos.setIndex(0);
127            format.parseObject(proposedResult, pos);
128            if (pos.getIndex() != proposedResult.length()) {
129                System.out.println("FormattedDocument.remove() error.");
130                Toolkit.getDefaultToolkit().beep();
131                return;
132            }
133        }
134
135        super.remove(offs, len);
136    }
137}