| /******************************************************************************* |
| * Copyright (c) 2006 The Pampered Chef and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * The Pampered Chef - initial API and implementation |
| ******************************************************************************/ |
| |
| package org.eclipse.jface.examples.databinding.mask; |
| |
| import java.beans.PropertyChangeListener; |
| import java.beans.PropertyChangeSupport; |
| |
| import org.eclipse.jface.examples.databinding.mask.internal.EditMaskParser; |
| import org.eclipse.jface.examples.databinding.mask.internal.SWTUtil; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.events.FocusAdapter; |
| import org.eclipse.swt.events.FocusEvent; |
| import org.eclipse.swt.events.FocusListener; |
| import org.eclipse.swt.events.VerifyEvent; |
| import org.eclipse.swt.events.VerifyListener; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Text; |
| |
| /** |
| * Ensures text widget has the format specified by the edit mask. Edit masks |
| * are currently defined as follows: |
| * |
| * The following characters are reserved words that match specific kinds of |
| * characters: |
| * |
| * # - digits 0-9 |
| * A - uppercase A-Z |
| * a - upper or lowercase a-z, A-Z |
| * n - alphanumeric 0-9, a-z, A-Z |
| * |
| * All other characters are literals. The above characters may be turned into |
| * literals by preceeding them with a backslash. Use two backslashes to |
| * denote a backslash. |
| * |
| * Examples: |
| * |
| * (###) ###-#### U.S. phone number |
| * ###-##-#### U.S. Social Security number |
| * \\\### A literal backslash followed by a literal pound symbol followed by two digits |
| * |
| * Ideas for future expansion: |
| * |
| * Quantifiers as postfix modifiers to a token. ie: |
| * |
| * #{1, 2}/#{1,2}/#### MM/DD/YYYY date format allowing 1 or 2 digit month or day |
| * |
| * Literals may only be quantified as {0,1} which means that they only appear |
| * if placeholders on both sides of the literal have data. This will be used |
| * along with: |
| * |
| * Right-to-left support for numeric entry. When digits are being entered and |
| * a decimal point is present in the mask, digits to the left of the decimal |
| * are entered right-to-left but digits to the right of the decimal left-to-right. |
| * This might need to be a separate type of edit mask. (NumericMask, maybe?) |
| * |
| * Example: |
| * |
| * $#{0,3},{0,1}#{0,3}.#{0,2} ie: $123,456.12 or $12.12 or $1,234.12 or $123.12 |
| * |
| * |
| * Here's the basic idea of how the current implementation works (the actual |
| * implementation is slightly more abstracted and complicated than this): |
| * |
| * We always let the verify event pass if the user typed a new character or selected/deleted anything. |
| * During the verify event, we post an async runnable. |
| * Inside that async runnable, we: |
| * - Remember the selection position |
| * - getText(), then |
| * - Strip out all literal characters from text |
| * - Truncate the resulting string to raw length of edit mask without literals |
| * - Insert literal characters back in the correct positions |
| * - setText() the resulting string |
| * - reset the selection to the correct location |
| * |
| * @since 3.3 |
| */ |
| public class EditMask { |
| |
| public static final String FIELD_TEXT = "text"; |
| public static final String FIELD_RAW_TEXT = "rawText"; |
| public static final String FIELD_COMPLETE = "complete"; |
| protected Text text; |
| protected EditMaskParser editMaskParser; |
| private boolean fireChangeOnKeystroke = true; |
| private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this); |
| |
| protected String oldValidRawText = ""; |
| protected String oldValidText = ""; |
| |
| /** |
| * Creates an instance that wraps around a text widget and manages its<br> |
| * formatting. |
| * |
| * @param text |
| * @param editMask |
| */ |
| public EditMask(Text text) { |
| this.text = text; |
| } |
| |
| /** |
| * @return the underlying Text control used by EditMask |
| */ |
| public Text getControl() { |
| return this.text; |
| } |
| |
| /** |
| * Set the edit mask string on the edit mask control. |
| * |
| * @param editMask The edit mask string |
| */ |
| public void setMask(String editMask) { |
| editMaskParser = new EditMaskParser(editMask); |
| text.addVerifyListener(verifyListener); |
| text.addFocusListener(focusListener); |
| text.addDisposeListener(disposeListener); |
| updateTextField.run(); |
| oldValidText = text.getText(); |
| oldValidRawText = editMaskParser.getRawResult(); |
| } |
| |
| /** |
| * @param string Sets the text string in the receiver |
| */ |
| public void setText(String string) { |
| String oldValue = text.getText(); |
| if (editMaskParser != null) { |
| editMaskParser.setInput(string); |
| String formattedResult = editMaskParser.getFormattedResult(); |
| text.setText(formattedResult); |
| firePropertyChange(FIELD_TEXT, oldValue, formattedResult); |
| } else { |
| text.setText(string); |
| firePropertyChange(FIELD_TEXT, oldValue, string); |
| } |
| oldValidText = text.getText(); |
| oldValidRawText = editMaskParser.getRawResult(); |
| } |
| |
| /** |
| * @return the actual (formatted) text |
| */ |
| public String getText() { |
| if (editMaskParser != null) { |
| return editMaskParser.getFormattedResult(); |
| } |
| return text.getText(); |
| } |
| |
| /** |
| * setRawText takes raw text as a parameter but formats it before |
| * setting the text in the Text control. |
| * |
| * @param string the raw (unformatted) text |
| */ |
| public void setRawText(String string) { |
| if (string == null) { |
| string = ""; |
| } |
| if (editMaskParser != null) { |
| String oldValue = editMaskParser.getRawResult(); |
| editMaskParser.setInput(string); |
| text.setText(editMaskParser.getFormattedResult()); |
| firePropertyChange(FIELD_RAW_TEXT, oldValue, string); |
| } else { |
| String oldValue = text.getText(); |
| text.setText(string); |
| firePropertyChange(FIELD_RAW_TEXT, oldValue, string); |
| } |
| oldValidText = text.getText(); |
| oldValidRawText = editMaskParser.getRawResult(); |
| } |
| |
| /** |
| * @return The input text with literals removed |
| */ |
| public String getRawText() { |
| if (editMaskParser != null) { |
| return editMaskParser.getRawResult(); |
| } |
| return text.getText(); |
| } |
| |
| /** |
| * @return true if the field is complete according to the mask; false otherwise |
| */ |
| public boolean isComplete() { |
| if (editMaskParser == null) { |
| return true; |
| } |
| return editMaskParser.isComplete(); |
| } |
| |
| /** |
| * Returns the placeholder character. The placeholder |
| * character must be a different character than any character that is |
| * allowed as input anywhere in the edit mask. For example, if the edit |
| * mask permits spaces to be used as input anywhere, the placeholder |
| * character must be something other than a space character. |
| * <p> |
| * The space character is the default placeholder character. |
| * |
| * @return the placeholder character |
| */ |
| public char getPlaceholder() { |
| if (editMaskParser == null) { |
| throw new IllegalArgumentException("Have to set an edit mask first"); |
| } |
| return editMaskParser.getPlaceholder(); |
| } |
| |
| /** |
| * Sets the placeholder character for the edit mask. The placeholder |
| * character must be a different character than any character that is |
| * allowed as input anywhere in the edit mask. For example, if the edit |
| * mask permits spaces to be used as input anywhere, the placeholder |
| * character must be something other than a space character. |
| * <p> |
| * The space character is the default placeholder character. |
| * |
| * @param placeholder The character to use as a placeholder |
| */ |
| public void setPlaceholder(char placeholder) { |
| if (editMaskParser == null) { |
| throw new IllegalArgumentException("Have to set an edit mask first"); |
| } |
| editMaskParser.setPlaceholder(placeholder); |
| updateTextField.run(); |
| oldValidText = text.getText(); |
| } |
| |
| /** |
| * Indicates if change notifications will be fired after every keystroke |
| * that affects the value of the rawText or only when the value is either |
| * complete or empty. |
| * |
| * @return true if every change (including changes from one invalid state to |
| * another) triggers a change event; false if only empty or valid |
| * values trigger a change event. Defaults to false. |
| */ |
| public boolean isFireChangeOnKeystroke() { |
| return fireChangeOnKeystroke; |
| } |
| |
| /** |
| * Sets if change notifications will be fired after every keystroke that |
| * affects the value of the rawText or only when the value is either |
| * complete or empty. |
| * |
| * @param fireChangeOnKeystroke |
| * true if every change (including changes from one invalid state |
| * to another) triggers a change event; false if only empty or |
| * valid values trigger a change event. Defaults to false. |
| */ |
| public void setFireChangeOnKeystroke(boolean fireChangeOnKeystroke) { |
| this.fireChangeOnKeystroke = fireChangeOnKeystroke; |
| } |
| |
| /** |
| * JavaBeans boilerplate code... |
| * |
| * @param listener |
| */ |
| public void addPropertyChangeListener(PropertyChangeListener listener) { |
| propertyChangeSupport.addPropertyChangeListener(listener); |
| } |
| |
| /** |
| * JavaBeans boilerplate code... |
| * |
| * @param propertyName |
| * @param listener |
| */ |
| public void addPropertyChangeListener(String propertyName, |
| PropertyChangeListener listener) { |
| propertyChangeSupport.addPropertyChangeListener(propertyName, listener); |
| } |
| |
| /** |
| * JavaBeans boilerplate code... |
| * |
| * @param listener |
| */ |
| public void removePropertyChangeListener(PropertyChangeListener listener) { |
| propertyChangeSupport.removePropertyChangeListener(listener); |
| } |
| |
| /** |
| * JavaBeans boilerplate code... |
| * |
| * @param propertyName |
| * @param listener |
| */ |
| public void removePropertyChangeListener(String propertyName, |
| PropertyChangeListener listener) { |
| propertyChangeSupport.removePropertyChangeListener(propertyName, |
| listener); |
| } |
| |
| private boolean isEitherValueNotNull(Object oldValue, Object newValue) { |
| return oldValue != null || newValue != null; |
| } |
| |
| private void firePropertyChange(String propertyName, Object oldValue, |
| Object newValue) { |
| if (isEitherValueNotNull(oldValue, newValue)) { |
| propertyChangeSupport.firePropertyChange(propertyName, |
| oldValue, newValue); |
| } |
| } |
| |
| protected boolean updating = false; |
| |
| protected int oldSelection = 0; |
| protected int selection = 0; |
| protected String oldRawText = ""; |
| protected boolean replacedSelectedText = false; |
| |
| private VerifyListener verifyListener = new VerifyListener() { |
| public void verifyText(VerifyEvent e) { |
| // If the edit mask is already full, don't let the user type |
| // any new characters |
| if (editMaskParser.isComplete() && // should eventually be .isFull() to account for optional characters |
| e.start == e.end && |
| e.text.length() > 0) |
| { |
| e.doit=false; |
| return; |
| } |
| |
| oldSelection = selection; |
| Point selectionRange = text.getSelection(); |
| selection = selectionRange.x; |
| |
| if (!updating) { |
| replacedSelectedText = false; |
| if (selectionRange.y - selectionRange.x > 0 && e.text.length() > 0) { |
| replacedSelectedText = true; |
| } |
| // If the machine is loaded down (ie: spyware, malware), we might |
| // get another keystroke before asyncExec can process, so we use |
| // greedyExec instead. |
| SWTUtil.greedyExec(Display.getCurrent(), updateTextField); |
| // Display.getCurrent().asyncExec(updateTextField); |
| } |
| } |
| }; |
| |
| private Runnable updateTextField = new Runnable() { |
| public void run() { |
| updating = true; |
| try { |
| if (!text.isDisposed()) { |
| Boolean oldIsComplete = new Boolean(editMaskParser.isComplete()); |
| |
| editMaskParser.setInput(text.getText()); |
| text.setText(editMaskParser.getFormattedResult()); |
| String newRawText = editMaskParser.getRawResult(); |
| |
| updateSelectionPosition(newRawText); |
| firePropertyChangeEvents(oldIsComplete, newRawText); |
| } |
| } finally { |
| updating = false; |
| } |
| } |
| |
| private void updateSelectionPosition(String newRawText) { |
| |
| // Adjust the selection |
| if (isInsertingNewCharacter(newRawText) || replacedSelectedText) { |
| // Find the position after where the new character was actually inserted |
| int selectionDelta = |
| editMaskParser.getNextInputPosition(oldSelection) |
| - oldSelection; |
| if (selectionDelta == 0) { |
| selectionDelta = editMaskParser.getNextInputPosition(selection) |
| - selection; |
| } |
| selection += selectionDelta; |
| } |
| |
| // Did we just type something that was accepted by the mask? |
| if (!newRawText.equals(oldRawText)) { // yep |
| |
| // If the user hits <end>, bounce them back to the end of their actual input |
| int firstIncompletePosition = editMaskParser.getFirstIncompleteInputPosition(); |
| if (firstIncompletePosition > 0 && selection > firstIncompletePosition) |
| selection = firstIncompletePosition; |
| text.setSelection(new Point(selection, selection)); |
| |
| } else { // nothing was accepted by the mask |
| |
| // Either we backspaced over a literal or we typed an illegal character |
| if (selection > oldSelection) { // typed an illegal character; backup |
| text.setSelection(new Point(selection-1, selection-1)); |
| } else { // backspaced over a literal; don't interfere with selection position |
| text.setSelection(new Point(selection, selection)); |
| } |
| } |
| oldRawText = newRawText; |
| } |
| |
| private boolean isInsertingNewCharacter(String newRawText) { |
| return newRawText.length() > oldRawText.length(); |
| } |
| |
| private void firePropertyChangeEvents(Boolean oldIsComplete, String newRawText) { |
| Boolean newIsComplete = new Boolean(editMaskParser.isComplete()); |
| if (!oldIsComplete.equals(newIsComplete)) { |
| firePropertyChange(FIELD_COMPLETE, oldIsComplete, newIsComplete); |
| } |
| if (!newRawText.equals(oldValidRawText)) { |
| if ( newIsComplete.booleanValue() || "".equals(newRawText) || fireChangeOnKeystroke) { |
| firePropertyChange(FIELD_RAW_TEXT, oldValidRawText, newRawText); |
| firePropertyChange(FIELD_TEXT, oldValidText, text.getText()); |
| oldValidText = text.getText(); |
| oldValidRawText = newRawText; |
| } |
| } |
| } |
| }; |
| |
| private FocusListener focusListener = new FocusAdapter() { |
| public void focusGained(FocusEvent e) { |
| selection = editMaskParser.getFirstIncompleteInputPosition(); |
| text.setSelection(selection, selection); |
| } |
| }; |
| |
| private DisposeListener disposeListener = new DisposeListener() { |
| public void widgetDisposed(DisposeEvent e) { |
| text.removeVerifyListener(verifyListener); |
| text.removeFocusListener(focusListener); |
| text.removeDisposeListener(disposeListener); |
| } |
| }; |
| |
| } |