/******************************************************************************* * Copyright (c) 2007, 2014 compeople AG 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: * compeople AG - initial API and implementation *******************************************************************************/ package org.eclipse.riena.internal.ui.ridgets.swt; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.math.BigDecimal; import java.math.BigInteger; import java.text.DecimalFormatSymbols; import java.util.Arrays; import java.util.regex.Pattern; import org.eclipse.core.databinding.BindingException; import org.eclipse.core.databinding.conversion.IConverter; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.runtime.Assert; import org.eclipse.swt.SWT; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Text; import org.eclipse.riena.core.util.RAPDetector; import org.eclipse.riena.core.util.StringUtils; import org.eclipse.riena.ui.core.marker.NegativeMarker; import org.eclipse.riena.ui.ridgets.INumericTextRidget; import org.eclipse.riena.ui.ridgets.ITextRidget; import org.eclipse.riena.ui.ridgets.swt.ToStringConverterFactory; import org.eclipse.riena.ui.swt.facades.SWTFacade; import org.eclipse.riena.ui.swt.utils.UIControlsFactory; /** * Ridget for a 'numeric' SWT <code>Text</code> widget. * * @see UIControlsFactory#createTextNumeric(org.eclipse.swt.widgets.Composite) */ public class NumericTextRidget extends TextRidget implements INumericTextRidget { /** * This is not API and should not be called by clients. Public for testing only. */ public static String group(final String input, final boolean isGrouping, final boolean isDecimal) { String result = input; final int decIndex = input.indexOf(DECIMAL_SEPARATOR); if (isGrouping) { final String left = decIndex == -1 ? input : input.substring(0, decIndex); final String right = decIndex == -1 ? "" : input.substring(decIndex); //$NON-NLS-1$ result = NumericTextRidget.group(left) + right; } if (decIndex == -1 && isDecimal) { result += DECIMAL_SEPARATOR; } return result; } /** * This is not API and should not be called by clients. Public for testing only. */ public static String ungroup(final String input) { final StringBuilder result = new StringBuilder(input.length()); for (int i = 0; i < input.length(); i++) { final char ch = input.charAt(i); if (ch != GROUPING_SEPARATOR) { result.append(ch); } } // System.out.println("ungroup: " + input + " >> " + result.toString()); return result.toString(); } /** * This is not API and should not be called by clients. Public for testing only. */ public static String removeLeadingCruft(final String input) { if (Pattern.matches("0+.", input) && input.endsWith(String.valueOf(DECIMAL_SEPARATOR))) { //$NON-NLS-1$ return ZERO_DEC; } if (MINUS_DEC.matches(input)) { return MINUS_DEC; } if (Pattern.matches(MINUS_SIGN + "0+.", input) && input.endsWith(String.valueOf(DECIMAL_SEPARATOR))) { //$NON-NLS-1$ return ZERO_DEC; } if (String.valueOf(MINUS_SIGN).equals(input) || MINUS_ZERO.equals(input)) { return String.valueOf(MINUS_SIGN); } if (String.valueOf(ZERO).equals(input)) { return String.valueOf(ZERO); } final StringBuilder result = new StringBuilder(input.length()); int start = 0; if (input.indexOf(MINUS_SIGN) == 0) { result.append(MINUS_SIGN); start++; } if (start < input.length() && ZERO == input.charAt(start)) { int newStart = start + 1; while (newStart < input.length() && ZERO == input.charAt(newStart)) { newStart++; } if (newStart == input.length()) { result.append(ZERO); if (MINUS_ZERO.equals(result.toString())) { result.delete(0, 1); } } else { result.append(input.substring(newStart)); } } else { result.append(input.substring(start)); } return result.toString(); } /** * This is not API. */ protected static String removeTrailingPadding(final String text) { String result = text; final int decSep = text.indexOf(DECIMAL_SEPARATOR); if (decSep != -1) { int index = text.length() - 1; char ch = result.charAt(index); while (index >= decSep && (ch == DECIMAL_SEPARATOR || ch == '0')) { result = result.substring(0, index); index--; if (index >= decSep) { ch = result.charAt(index); } } } return result; } private static String group(final String input) { final int numLength = input.length(); final boolean isNegative = input.indexOf(MINUS_SIGN) == 0; final int groupSize = 3; final int delta = isNegative ? -2 : -1; final int groupCount = (numLength + delta) / groupSize; final char[] result = new char[numLength + groupCount]; Arrays.fill(result, '#'); int availableChars = groupSize + 1; for (int i = result.length - 1; i > 0; i--) { availableChars--; if (availableChars == 0) { result[i] = GROUPING_SEPARATOR; availableChars = groupSize + 1; } } for (int i = 0, j = 0; i < result.length; i++) { if ('#' == result[i]) { result[i] = input.charAt(j); j++; } } // System.out.println("group: " + input + " >> " + String.valueOf(result)); return String.valueOf(result); } protected static final char DECIMAL_SEPARATOR = new DecimalFormatSymbols().getDecimalSeparator(); protected static final char GROUPING_SEPARATOR = new DecimalFormatSymbols().getGroupingSeparator(); protected static final char MINUS_SIGN = new DecimalFormatSymbols().getMinusSign(); protected static final char ZERO = '0'; protected static final String ZERO_DEC = String.valueOf('0') + DECIMAL_SEPARATOR; private static final String MINUS_ZERO = String.valueOf(MINUS_SIGN) + ZERO; private static final String MINUS_DEC = String.valueOf(MINUS_SIGN) + DECIMAL_SEPARATOR; private final NumericVerifyListener verifyListener; private final NumericModifyListener modifyListener; private final KeyListener keyListener; private final FocusListener focusListener; private boolean isConvertEmpty; private boolean isGrouping; private boolean isMarkNegative; private boolean isSigned; private NegativeMarker negativeMarker; private int maxLength; private int precision; /** * True, if this ridget has a custom model-to-control converter */ private boolean hasCustomConverter; public NumericTextRidget() { super("0"); //$NON-NLS-1$ verifyListener = new NumericVerifyListener(); modifyListener = new NumericModifyListener(); keyListener = new NumericKeyListener(); focusListener = new NumericFocusListener(); isSigned = true; isGrouping = true; isMarkNegative = true; maxLength = INumericTextRidget.MAX_LENGTH_UNBOUNDED; precision = -1; addPropertyChangeListener(ITextRidget.PROPERTY_TEXT, new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent evt) { // System.out.println("updateMarkNeg: " + evt.getNewValue()); updateMarkNegative(); } }); } /** * Checks that given String value is a valid number. * <p> * Subclasses should override and adjust to their needs without calling super(). * * @param number * a String value; "" is used for the empty value */ protected void checkNumber(final String number) { if (!"".equals(number)) { //$NON-NLS-1$ final BigInteger bigInteger = checkIsNumber(number); checkSigned(bigInteger); checkMaxLength(number); } } @Override protected void checkUIControl(final Object uiControl) { super.checkUIControl(uiControl); checkType(uiControl, Text.class); if (uiControl != null) { final int style = ((Text) uiControl).getStyle(); if ((style & SWT.SINGLE) == 0) { throw new BindingException("Text widget must be SWT.SINGLE"); //$NON-NLS-1$ } } } @Override protected final synchronized void addListeners(final Text control) { control.addModifyListener(modifyListener); control.addVerifyListener(verifyListener); control.addKeyListener(keyListener); control.addFocusListener(focusListener); super.addListeners(control); } protected IConverter getConverter(final Class<?> type, final int precision) { if (hasCustomConverter) { return getValueBindingSupport().getModelToUIControlConverter(); } else { return ToStringConverterFactory.createNumberConverter(type, precision); } } protected synchronized int getPrecision() { return precision; } /** * Converts text to BigInteger and checks if it is less than zero. * <p> * Subclasses may override and adjust to their needs without calling super(). * * @param text * a String representing a numeric value; never null * @return true if text is negative, false otherwise. */ protected boolean isNegative(final String text) { final BigInteger value = new BigInteger(text); return (value.compareTo(BigInteger.ZERO) < 0); } @Override protected final synchronized void removeListeners(final Text control) { control.removeModifyListener(modifyListener); control.removeVerifyListener(verifyListener); control.removeFocusListener(focusListener); control.removeKeyListener(keyListener); super.removeListeners(control); } protected synchronized void setPrecision(final int precision) { this.precision = precision; final String oldText = getText(); final String newText = formatFraction(oldText); setText(newText); } @Override protected void setUIText(final String text) { super.setUIText(beautifyText(text)); // if (isDecimal()) { // super.setUIText(beautifyText(text)); // } else { // if (isConvertEmptyToZero() && text.length() == 0) { // super.setUIText(createZero()); // } else { // super.setUIText(text); // } // } } // public methods ///////////////// public final synchronized int getMaxLength() { return maxLength; } @Override public synchronized String getText() { String result = super.getText(); if (isDecimal()) { result = removeTrailingPadding(result); } return result; } public synchronized boolean isConvertEmptyToZero() { return isConvertEmpty; } public synchronized boolean isGrouping() { return isGrouping; } public synchronized boolean isMarkNegative() { return isMarkNegative; } public synchronized boolean isSigned() { return isSigned; } public synchronized void setConvertEmptyToZero(final boolean convertEmpty) { if (isConvertEmpty != convertEmpty) { isConvertEmpty = convertEmpty; setText(getText()); } } public synchronized void setGrouping(final boolean useGrouping) { if (isGrouping != useGrouping) { isGrouping = useGrouping; updateGrouping(); } } public synchronized void setMarkNegative(final boolean mustBeMarked) { if (isMarkNegative != mustBeMarked) { isMarkNegative = mustBeMarked; updateMarkNegative(); } } public final synchronized void setMaxLength(final int maxLength) { Assert.isLegal(INumericTextRidget.MAX_LENGTH_UNBOUNDED == maxLength || maxLength > 0, "maxLength must be greater than zero or -1 (INumericTextRidget.MAX_LENGTH_UNBOUNDED): " + maxLength); //$NON-NLS-1$ final int oldValue = this.maxLength; if (oldValue != maxLength) { this.maxLength = maxLength; firePropertyChange(INumericTextRidget.PROPERTY_MAXLENGTH, oldValue, maxLength); } } @Override public void setModelToUIControlConverter(final IConverter converter) { hasCustomConverter = converter != null; super.setModelToUIControlConverter(converter); } public final synchronized void setSigned(final boolean signed) { if (isSigned != signed) { final boolean oldValue = isSigned; isSigned = signed; firePropertyChange(PROPERTY_SIGNED, oldValue, isSigned); } } /** * {@inheritDoc} * <p> * If decimal and/or grouping separators are contained in the given {@code text} value, they must follow the convention of the current locale. * <p> * Examples: * <ul> * <li>DE - valid text: "1.234,56" or "1234,56"</li> * <li>US - valid text: "1,234.56" or "1234.56"</li> * </ul> * <p> * Passing a null value is equivalent to {@code setText("")}. * * @see DecimalFormatSymbols#getDecimalSeparator() */ @Override public final synchronized void setText(final String text) { final String value = text != null ? text : ""; //$NON-NLS-1$ checkNumber(value); super.setText(treatDecimalSeparator(group(ungroup(value), isGrouping, isDecimal()))); } /** * {@inheritDoc} * <p> * * @throws RuntimeException * if the value obtain from model exceeds the specified maximum length or precision. It is responsibility of application to handle this. */ @Override public synchronized void updateFromModel() { checkValue(); super.updateFromModel(); } // helping methods ////////////////// private void beautifyText(final Text control) { if (control != null) { final String oldText = control.getText(); final String newText = beautifyText(oldText); if (!newText.equals(oldText)) { final SWTFacade facade = SWTFacade.getDefault(); final Object[] verifyListeners = facade.removeListeners(control, SWT.Verify); stopModifyListener(); control.setText(newText); control.setSelection(newText.length()); startModifyListener(); facade.addListeners(control, SWT.Verify, verifyListeners); } } } private String beautifyText(final String text) { if (text.length() == 0 || String.valueOf(DECIMAL_SEPARATOR).equals(text)) { return isConvertEmptyToZero() ? createZero() : text; } if (MINUS_DEC.equals(text)) { return isConvertEmptyToZero() ? createZero() : String.valueOf(DECIMAL_SEPARATOR); } String newText = formatFraction(text); if (newText.length() > 1 && newText.charAt(0) == DECIMAL_SEPARATOR) { newText = "0" + newText; //$NON-NLS-1$ } else if (newText.startsWith(MINUS_DEC)) { boolean hasValue = false; for (int i = 0; !hasValue && i < newText.length(); i++) { final char ch = newText.charAt(i); hasValue = Character.isDigit(ch) && ch != '0'; } if (hasValue) { newText = newText.substring(0, 1) + "0" + newText.substring(1); //$NON-NLS-1$ } else { newText = "0" + newText.substring(1); //$NON-NLS-1$ } } newText = treatDecimalSeparator(group(ungroup(newText), isGrouping, isDecimal())); return newText; } private BigInteger checkIsNumber(final String number) { try { return new BigInteger(ungroup(number)); } catch (final NumberFormatException nfe) { throw new NumberFormatException("Not a valid number: " + number); //$NON-NLS-1$ } } private void checkMaxLength(final String number) { if (INumericTextRidget.MAX_LENGTH_UNBOUNDED == maxLength) { return; } int length = number.length() - StringUtils.count(number, GROUPING_SEPARATOR); if (number.length() > 0 && number.charAt(0) == MINUS_SIGN) { length -= 1; } if (maxLength < length) { final String msg = String.format("Length (%d) exceeded: %s", maxLength, number); //$NON-NLS-1$ throw new NumberFormatException(msg); } } private void checkSigned(final BigInteger value) { if (!isSigned() && value.compareTo(BigInteger.ZERO) == -1) { throw new NumberFormatException("Negative numbers not allowed: " + value); //$NON-NLS-1$ } } private void checkValue() { final IObservableValue modelObservable = getValueBindingSupport().getModelObservable(); if (modelObservable == null) { return; } final Object value = modelObservable.getValue(); final Class<?> type = (Class<?>) modelObservable.getValueType(); if (type == null) { return; } final IConverter converter = getConverter(type, Integer.MAX_VALUE); if (converter != null) { checkNumber((String) converter.convert(value)); } } @Override protected boolean isExternalValueChange(final String oldValue, final String newValue) { return isValueChanged(oldValue, newValue); } private boolean isValueChanged(final String oldValue, final String newValue) { if (oldValue.equals(newValue)) { return false; } // compare the numeric value ignoring format try { final BigDecimal oldExtrValue = converttValue(oldValue); final BigDecimal newExtrValue = converttValue(newValue); if (oldExtrValue == null && newExtrValue == null) { return false; } if (oldExtrValue == null || newExtrValue == null) { return true; } return oldExtrValue.compareTo(newExtrValue) != 0; } catch (final NumberFormatException nfe) { return true; } } /* * convert the given String to a numeric value */ private BigDecimal converttValue(String value) { value = value.replaceAll(String.valueOf("\\" + GROUPING_SEPARATOR), ""); //$NON-NLS-1$ //$NON-NLS-2$ value = value.replaceAll(String.valueOf("\\" + DECIMAL_SEPARATOR), String.valueOf(GROUPING_SEPARATOR)); //$NON-NLS-1$ if (String.valueOf(GROUPING_SEPARATOR).equals(value)) { return null; } return new BigDecimal(value); } private String createZero() { String result; if (isDecimal()) { final int decimalDigits = Math.max(0, getPrecision()); final char[] zero = new char[2 + decimalDigits]; Arrays.fill(zero, ZERO); zero[1] = DECIMAL_SEPARATOR; result = String.valueOf(zero); } else { result = String.valueOf(ZERO); } return result; } private void startModifyListener() { modifyListener.setEnabled(true); } private void startVerifyListener() { verifyListener.setEnabled(true); } private synchronized String createPattern(final String input) { String result; // -1 => no length limit final int length = getMaxLength() == INumericTextRidget.MAX_LENGTH_UNBOUNDED ? input.length() : getMaxLength(); if (isDecimal() && getPrecision() > 0) { final String decSep = DECIMAL_SEPARATOR == '.' ? "\\." : String.valueOf(DECIMAL_SEPARATOR); //$NON-NLS-1$ result = String.format("\\d{0,%d}%s\\d{0,%d}", length, decSep, getPrecision()); //$NON-NLS-1$ if (isSigned) { result = String.format("%c?", MINUS_SIGN) + result; //$NON-NLS-1$ } } else { if (isSigned) { result = String.format("%c?\\d{0,%d}", MINUS_SIGN, length); //$NON-NLS-1$ } else { result = String.format("\\d{0,%d}", length); //$NON-NLS-1$ } } // System.out.println("pattern: " + result); return result; } private String formatFraction(final String text) { String result = text; final int decSep = text.indexOf(DECIMAL_SEPARATOR); if (decSep != -1) { final int fractionDigits = text.substring(decSep).length() - 1; final int prec = getPrecision(); if (fractionDigits < prec) { final int pad = Math.max(0, getPrecision() - fractionDigits); if (pad > 0) { final char[] zeroes = new char[pad]; Arrays.fill(zeroes, '0'); result = text + String.valueOf(zeroes); } } else if (fractionDigits > prec) { final int diff = fractionDigits - prec; result = text.substring(0, text.length() - diff); } result = treatDecimalSeparator(result); } return result; } /* * No decimal separator, if precision is 0 (i.e. no faction to show). If precision > 0 and no decimal separator present, append decimal separator to text. * * @param text the original text string * * @return the modified text string */ private String treatDecimalSeparator(final String text) { if (isDecimal()) { final int decimalSeparatorIndex = text.indexOf(DECIMAL_SEPARATOR); if (getPrecision() == 0 && decimalSeparatorIndex != -1) { return text.substring(0, decimalSeparatorIndex); } if (decimalSeparatorIndex == -1 && getPrecision() > 0) { return text + DECIMAL_SEPARATOR; } } return text; } private boolean isDecimal() { return getPrecision() != -1; } private void stopModifyListener() { modifyListener.setEnabled(false); } private void stopVerifyListener() { verifyListener.setEnabled(false); } private void updateGrouping() { final String text = treatDecimalSeparator(group(ungroup(getText()), isGrouping, isDecimal())); if (isValueChanged(text, getText())) { setText(getText()); } else { forceTextToControl(text); } } private void updateMarkNegative() { final String text = ungroup(getText()); boolean needMarker = false; if (isMarkNegative()) { try { needMarker = isNegative(text); } catch (final NumberFormatException nfe) { needMarker = false; } } if (needMarker) { if (negativeMarker == null) { negativeMarker = new NegativeMarker(); } addMarker(negativeMarker); } else { if (negativeMarker != null) { removeMarker(negativeMarker); } } } // helping classes ////////////////// /** * This listener handles addition, deletion and replacement of text in the Text control. When the text in the control is modified, it will compute the new * value and match it against a pattern. The pattern is computed based on the maxLength, precision and signed settings of this ridget. If the new value does * not match against the pattern, e.doit is set to false and the modification is canceled. */ private final class NumericVerifyListener implements VerifyListener { private boolean isEnabled = true; public synchronized void setEnabled(final boolean isEnabled) { this.isEnabled = isEnabled; } public synchronized void verifyText(final VerifyEvent e) { if (!e.doit || !isEnabled) { return; } final Text control = (Text) e.widget; final String oldText = control.getText(); String newText = null; int newCursorPos = -1; boolean applyText = false; int start = e.start; int end = e.end; char character = e.character; // this is a workaround for RAP bug #327439 if (character == 0 && RAPDetector.isRAPavailable()) { // try to get the char from the selection and update start/end positions final Point sel = control.getSelection(); if (oldText.length() > e.text.length()) { // delete character = '\b'; start = findChangePos(oldText, e.text); end = start + Math.max(sel.y - sel.x, 1); } else { // insert / replace character = e.text.charAt(sel.x); start = sel.x; end = sel.y; } } if (Character.isDigit(character) || MINUS_SIGN == character) { // insert / replace final boolean preserveDecSep = oldText.substring(start, end).indexOf(DECIMAL_SEPARATOR) != -1; if (preserveDecSep) { if (oldText.charAt(start) == DECIMAL_SEPARATOR && oldText.length() > end - start) { // #(.#)# -> #.C# or (.#)# -> .C# newText = oldText.substring(0, start) + DECIMAL_SEPARATOR + character + oldText.substring(end); } else { // (#.#) -> C. or #(#.#)# -> #C.# or #(#.)# -> #C.# newText = oldText.substring(0, start) + character + DECIMAL_SEPARATOR + oldText.substring(end); } applyText = true; } else { newText = oldText.substring(0, start) + character + oldText.substring(end); } } else if ('\b' == character || 127 == e.keyCode) { // delete final NumericString ns = new NumericString(oldText, isGrouping()); newCursorPos = ns.delete(start, end, character); newText = ns.toString(); applyText = true; } boolean doFlash = true; if (newText != null) { final String newTextNoGroup = ungroup(newText); final String regex = createPattern(newTextNoGroup); e.doit = Pattern.matches(regex, newTextNoGroup); doFlash = !e.doit; if (e.doit && applyText) { e.doit = false; stopVerifyListener(); setTextAndCursor(control, newTextNoGroup, newCursorPos, e, end); startVerifyListener(); } } else { e.doit = false; } if (doFlash) { flash(); } } /** * @param oldText * @param text * @return */ private int findChangePos(final String oldText, final String newText) { final int length = newText.length(); for (int i = 0; i < length; i++) { if (oldText.charAt(i) != newText.charAt(i)) { return i; } } return length; } private void setTextAndCursor(final Text control, final String text, final int cursorPos, final VerifyEvent e, final int end) { final String oldText = control.getText(); final int oldCursorPos = control.getCaretPosition(); control.setText(text); if (text.length() > 0 && text.charAt(0) == DECIMAL_SEPARATOR && control.getText().startsWith(String.valueOf(ZERO))) { control.setSelection(1); } else if (oldCursorPos != 0 && text.length() == 1 && text.charAt(0) == DECIMAL_SEPARATOR) { control.setSelection(0); } else if (text.length() == 2 && text.charAt(1) == DECIMAL_SEPARATOR) { control.setSelection(1); } else if (cursorPos > -1) { control.setSelection(cursorPos); } else { final int posFromRight = oldText.length() - end; control.setSelection(control.getText().length() - posFromRight); } } } /** * When the user types a key in the text control that modifies it's content this listener is invoked. It will replace the string in the text control, by the * a formatted equivalent, according to the settings in this ridget (precision, maxlength, etc.). */ private final class NumericModifyListener implements ModifyListener { private boolean isEnabled = true; public synchronized void setEnabled(final boolean isEnabled) { this.isEnabled = isEnabled; } public synchronized void modifyText(final ModifyEvent e) { if (!isEnabled || !isEnabled()) { return; } // System.out.println(e); final Text control = (Text) e.widget; final String oldText = control.getText(); final boolean isDecimal = isDecimal(); String newText = treatDecimalSeparator(group(removeLeadingCruft(ungroup(oldText)), isGrouping(), isDecimal)); if (isOutputOnly() && newText.equals(String.valueOf(DECIMAL_SEPARATOR))) { newText = ""; //$NON-NLS-1$ } if (isDecimal && newText.startsWith(String.valueOf(DECIMAL_SEPARATOR)) && newText.length() > 1) { newText = ZERO + newText; } if (!oldText.equals(newText)) { stopVerifyListener(); final int posFromRight = oldText.length() - control.getCaretPosition(); control.setText(newText); final int caretPos = newText.length() - posFromRight; control.setSelection(caretPos); // System.out.println("newText= " + newText + " @ " + caretPos); startVerifyListener(); } } } /** * This listener controls which key strokes are allowed by the text control. Additionally some keystrokes replaced with special behavior. Currently those * key strokes are: * <ol> * <li>Left & Right arrow - will jump over grouping separators</li> * <li> * Shift - ddisables jumping over grouping separators when pressed down</li> * <li>Decimal separator - will cause the cursor to jump over the decimal separator if directly to the right of it. Otherwise ignored</li> * <li> * minus ('-') - for signed widgets, it adds the '-' character to the left of the widget. Otherwise ignored</li> * <li>CR ('\r') - for decimal ridgets, it will pad the fractional digits by adding '0's until the maximum number of fractional digits is reached (as * specified by the ridgets precision value) * </ol> */ private final class NumericKeyListener extends KeyAdapter { private boolean shiftDown = false; @Override public void keyReleased(final KeyEvent e) { if (131072 == e.keyCode) { shiftDown = false; } } @Override public void keyPressed(final KeyEvent e) { final Text control = (Text) e.widget; final String text = control.getText(); if (131072 == e.keyCode) { shiftDown = true; } else if (16777219 == e.keyCode && control.getSelectionCount() == 0) {// left arrow final int index = control.getCaretPosition() - 1; if (index > 1 && GROUPING_SEPARATOR == text.charAt(index) && !shiftDown) { e.doit = false; control.setSelection(index - 1); } } else if (16777220 == e.keyCode && control.getSelectionCount() == 0) { //right arrow final int index = control.getCaretPosition() + 1; if (index < text.length() - 1 && GROUPING_SEPARATOR == text.charAt(index) && !shiftDown) { e.doit = false; control.setSelection(index + 1); } } else if (DECIMAL_SEPARATOR == e.character) { e.doit = false; final int index = control.getCaretPosition(); if (index < text.length() && text.charAt(index) == DECIMAL_SEPARATOR) { control.setSelection(index + 1); } else { flash(); } } else if (MINUS_SIGN == e.character) { e.doit = false; if (isSigned()) { final Event event = new Event(); event.type = SWT.Verify; event.character = MINUS_SIGN; event.start = 0; event.end = 0; event.widget = control; event.text = String.valueOf(MINUS_SIGN); control.notifyListeners(SWT.Verify, event); if (event.doit) { final boolean selectedAll = control.getSelection().x == 0 && control.getSelection().y == control.getText().length(); final int caret = selectedAll ? 1 : control.getCaretPosition() + 1; stopVerifyListener(); control.setText(MINUS_SIGN + text); control.setSelection(caret); startVerifyListener(); } else { flash(); } } else { flash(); } } else if ('\r' == e.character) { if (isDecimal()) { final String newText = formatFraction(text); if (!newText.equals(text)) { stopVerifyListener(); control.setText(newText); control.setSelection(newText.length()); startVerifyListener(); } } } } } /** * For decimal ridgets: this focus listener will pad the fractional digits by adding '0's until the maximum number of fractional digits is reached (as * specified by the ridgets precision value) */ private final class NumericFocusListener extends FocusAdapter { @Override public void focusLost(final FocusEvent e) { final Text control = (Text) e.widget; beautifyText(control); } } }