alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*******************************************************************************
 * 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);
		}
	};

}
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.