/******************************************************************************* * 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.util.Date; import java.util.regex.Pattern; import com.ibm.icu.text.SimpleDateFormat; import org.eclipse.core.databinding.BindingException; 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.VerifyEvent; import org.eclipse.swt.events.VerifyListener; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Text; import org.eclipse.riena.core.util.RAPDetector; import org.eclipse.riena.ui.core.marker.ValidationTime; import org.eclipse.riena.ui.ridgets.IDateTextRidget; import org.eclipse.riena.ui.ridgets.IDecimalTextRidget; import org.eclipse.riena.ui.ridgets.databinding.DateToStringConverter; import org.eclipse.riena.ui.ridgets.databinding.StringToDateConverter; import org.eclipse.riena.ui.ridgets.validation.ValidDate; import org.eclipse.riena.ui.ridgets.validation.ValidIntermediateDate; import org.eclipse.riena.ui.swt.DatePickerComposite; import org.eclipse.riena.ui.swt.DatePickerComposite.IDateConverterStrategy; import org.eclipse.riena.ui.swt.utils.UIControlsFactory; /** * Ridget for a 'date/time/date time' SWT <code>Text</code> widget. The desired date/time/dat time pattern can be set via {@link #setFormat(String)}. See * {@link IDecimalTextRidget} for supported patterns. * * @see UIControlsFactory#createTextDate(org.eclipse.swt.widgets.Composite) */ public class DateTextRidget extends TextRidget implements IDateTextRidget { /** * Controls the expansion that happens when a two-digit year is entered where a four-digit year is expected. If the entered value is below * * * * {@value} * , it will be prefixed with '20'. Otherwise it will be prefixed with '19'. */ private static final int Y2K_CUTOFF = 30; private final DateVerifyListener verifyListener; private final KeyListener keyListener; private final FocusListener focusListener; // private final PaintListener paintListener; private String pattern; private ValidDate validDateRule; private ValidIntermediateDate validIntermediateDateRule; private StringToDateConverter uiControlToModelconverter; private DateToStringConverter modelToUIControlConverter; public DateTextRidget() { verifyListener = new DateVerifyListener(); keyListener = new DateKeyListener(); focusListener = new DateFocusListener(); // paintListener = new DatePaintListener(); setFormat(IDateTextRidget.FORMAT_DDMMYYYY); } @Override protected void checkUIControl(final Object uiControl) { if (null == uiControl) { return; } final boolean isValidDatePicker = isValidType(uiControl, DatePickerComposite.class); if (isValidDatePicker) { final DatePickerComposite datePicker = (DatePickerComposite) uiControl; datePicker.setDateConverterStrategy(new RidgetAwareDateConverterStrategy()); } if ((!isValidType(uiControl, Text.class) && !isValidDatePicker)) { throw new BindingException(String.format("uiControl must be a '%s' or a '%s' but was a %s ", //$NON-NLS-1$ Text.class.getSimpleName(), DatePickerComposite.class.getSimpleName(), uiControl.getClass().getSimpleName())); } } @Override protected final synchronized void addListeners(final Text control) { control.addVerifyListener(verifyListener); control.addKeyListener(keyListener); control.addFocusListener(focusListener); // control.addPaintListener(paintListener); super.addListeners(control); } /** * {@inheritDoc} * <p> * The 'empty' value will be replaced with the empty string, if the ridget is in output only mode. Otherwise same behavior as super. */ @Override protected final String getTextBasedOnMarkerState(final String value) { if (isOutputOnly() && !isNotEmpty(value)) { return ""; //$NON-NLS-1$ } return super.getTextBasedOnMarkerState(value); } /** * This ridget is bound to Text or DatePickerComposite controls. In the second case this method will return the Text component of the DatePickerComposite. * * @return a Text widget or null */ @Override protected Text getTextWidget() { Text result; final Control control = super.getUIControl(); if (control instanceof DatePickerComposite) { result = ((DatePickerComposite) control).getTextfield(); } else { result = (Text) control; } return result; } @Override protected boolean isNotEmpty(final String input) { boolean result = false; if (pattern != null) { final String emptyString = new SegmentedString(pattern).toString(); result = !emptyString.equals(input); } return result; } @Override protected final synchronized void removeListeners(final Text control) { // control.removePaintListener(paintListener); control.removeFocusListener(focusListener); control.removeKeyListener(keyListener); control.removeVerifyListener(verifyListener); super.removeListeners(control); } @Override protected void updateEditable() { super.updateEditable(); final Control control = getUIControl(); if (control instanceof DatePickerComposite) { ((DatePickerComposite) control).updateButtonEnablement(); } } public final void setFormat(final String datePattern) { removeValidationRule(validDateRule); removeValidationRule(validIntermediateDateRule); pattern = datePattern; validDateRule = new ValidDate(pattern); validIntermediateDateRule = new ValidIntermediateDate(pattern); uiControlToModelconverter = new StringToDateConverter(pattern); modelToUIControlConverter = new DateToStringConverter(pattern); addValidationRule(validDateRule, ValidationTime.ON_UPDATE_TO_MODEL); addValidationRule(validIntermediateDateRule, ValidationTime.ON_UI_CONTROL_EDIT); setUIControlToModelConverter(uiControlToModelconverter); setModelToUIControlConverter(modelToUIControlConverter); getValueBindingSupport().rebindToModel(); final boolean isBound = getValueBindingSupport().getModelObservable() != null; if (isBound && getValueBindingSupport().getModelObservable().getValueType() == Date.class) { updateFromModel(); } else { setText(new SegmentedString(pattern).toString()); // clear } } /** * {@inheritDoc} * <p> * * @throws RuntimeException * if {code text} does not (partially) match the specified format pattern. A partial match is any string that has digits and separators in the * expected places - as defined by the format pattern - regardless of limits for a certain group (i.e. month <= 12 etc.). For example, assuming * the format pattern is 'dd.MM.yyyy', all of the following values are valid: "", "12", "12.10", "47.11", "12.10.20", "12.10.2008", * " . . ", " .10". Invalid values would be: null, "abc", "12.ab", "12122008", "12/12/2008" */ @Override public synchronized void setText(final String text) { final String newText = checkAndFormatValue(text); super.setText(newText); } // helping methods ////////////////// private boolean isValidType(final Object uiControl, final Class<?> type) { return ((uiControl != null) && (type.isAssignableFrom(uiControl.getClass()))); } private String checkAndFormatValue(final String text) { final SegmentedString ss = new SegmentedString(pattern); if (text != null) { if (!ss.isValidPartialMatch(text)) { final String msg = String.format("'%s' is no partial match for '%s'", text, pattern); //$NON-NLS-1$ throw new IllegalArgumentException(msg); } ss.insert(0, text); } return ss.toString(); } private String computeAutoFill(final String input) { String result = null; final int index = pattern.lastIndexOf("yyyy"); //$NON-NLS-1$ if (index != -1 && pattern.length() == input.length()) { String yyyy = input.substring(index, index + 4); if (Pattern.matches(" \\d\\d", yyyy)) { //$NON-NLS-1$ final String yy = yyyy.substring(2); if (Integer.parseInt(yy) < Y2K_CUTOFF) { yyyy = "20" + yy; //$NON-NLS-1$ } else { yyyy = "19" + yy; //$NON-NLS-1$ } result = input.substring(0, index) + yyyy + input.substring(index + 4); } } return result; } private synchronized void forceTextToControl(final Text control, final String text) { verifyListener.setEnabled(false); control.setText(text); verifyListener.setEnabled(true); } // helping classes ////////////////// /** * @param control */ private void transferTextToBean(final Text control) { final String autoFill = computeAutoFill(control.getText()); if (autoFill != null) { forceTextToControl(control, autoFill); } } /** * A {@link IDateConverterStrategy} that uses the current modelToUIControlConverter and uiControlToModelconverter for conversion between {@link String} and * {@link Date} * * @noinstantiate This class is not intended to be instantiated by clients. */ public final class RidgetAwareDateConverterStrategy implements IDateConverterStrategy { /* * The dates given are in the local timezone, so that why we use the SimpleDateFormat instead of the DateToStringConverter / StringToDateConverter */ public void setDateToTextField(final Date date) { final SimpleDateFormat format = new SimpleDateFormat(pattern); final String text = format.format(date); setText(text); } public Date getDateFromTextField(final String dateString) { Date result; try { final SimpleDateFormat format = new SimpleDateFormat(pattern); result = format.parse(dateString); } catch (final Exception exc) { result = null; // dateString not parseable, fallback: null } return result; } } /** * 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. Unsupported modifications will be cancelled. */ private final class DateVerifyListener implements VerifyListener { private volatile boolean isEnabled = true; public void setEnabled(final boolean isEnabled) { this.isEnabled = isEnabled; } public void verifyText(final VerifyEvent e) { if (!e.doit || !isEnabled) { return; } final Text control = (Text) e.widget; final String oldText = control.getText(); 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 int pos = sel.x; if (pos >= e.text.length()) { pos = e.text.length() - 1; } character = e.text.charAt(pos); start = sel.x; end = sel.y; } } final int oldPos = control.getCaretPosition(); int newPos = -1; final SegmentedString ss = new SegmentedString(pattern, oldText); if (character == '\b' || e.keyCode == 127) {// backspace, del newPos = ss.delete(start, end - 1); if (newPos == -1) { newPos = character == '\b' ? start : end; } } else if (SegmentedString.isDigit(character)) { if (end - start > 0) { newPos = ss.replace(start, end - 1, e.text); } else { newPos = ss.insert(start, String.valueOf(character)); } } else if (SegmentedString.isSeparator(character)) { if (end - start > 0) { newPos = ss.replace(start, end - 1, String.valueOf(character)); } else { newPos = ss.insert(start, String.valueOf(character)); } } e.doit = false; if (newPos != -1) { forceTextToControl(control, ss.toString()); control.setSelection(newPos); // System.out.println("newPos: " + newPos); if (newPos == oldPos && oldText.equals(ss.toString())) { flash(); } } else { flash(); } } 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; } } /** * 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> * <ol> * <li>Left & Right arrow - will jump over separators and spaces</li> * <li> * Delete / Backspace at a single separator - will jump to the next valid location in the same direction</li> * <li>Shift - disables jumping over grouping separators when pressed down</li> * </ol> */ private final class DateKeyListener 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; if (131072 == e.keyCode) { shiftDown = true; } else { final String text = control.getText(); final int caret = control.getCaretPosition(); final int selectionCount = control.getSelectionCount(); if (16777219 == e.keyCode && selectionCount == 0 && !shiftDown) {// left arrow e.doit = false; final SegmentedString ss = new SegmentedString(pattern, text); final int index = ss.findNewCursorPosition(caret, -1); control.setSelection(index); } else if (16777220 == e.keyCode && selectionCount == 0 && !shiftDown) { //right arrow e.doit = false; final SegmentedString ss = new SegmentedString(pattern, text); final int index = ss.findNewCursorPosition(caret, 1); control.setSelection(index); } else if (127 == e.keyCode && selectionCount == 0 && !shiftDown) { // delete on the left of a separator; no selection if (caret < text.length() && SegmentedString.isSeparator(text.charAt(caret))) { e.doit = false; final SegmentedString ss = new SegmentedString(pattern, text); int index = ss.findNewCursorPosition(caret, 1); // if index (right) is a digit, delete if (index < text.length() && SegmentedString.isDigit(text.charAt(index))) { ss.delete(caret, index); forceTextToControl(control, ss.toString()); index++; } control.setSelection(index); } } else if ('\b' == e.character && selectionCount == 0 && !shiftDown) { // backspace on the right of a separator; no selection if (caret > 0 && SegmentedString.isSeparator(text.charAt(caret - 1))) { e.doit = false; final SegmentedString ss = new SegmentedString(pattern, text); final int index = ss.findNewCursorPosition(caret, -1); // if index (left) is a digit, delete if (index > 0 && SegmentedString.isDigit(text.charAt(index - 1))) { ss.delete(index - 1, caret - 1); forceTextToControl(control, ss.toString()); } control.setSelection(index); } } else if ('\r' == e.character) { transferTextToBean((Text) e.widget); } } } } /** * For date ridgets with a four-digit-year segment, this focus listener will try to expand two-digit-years into four-digit years. */ private final class DateFocusListener extends FocusAdapter { @Override public void focusLost(final FocusEvent e) { transferTextToBean((Text) e.widget); } } // disabled for 1.0 - fixes bug 261291 // /** // * For proportional fonts (example: Arial, Helvetice, Verdana), this paint // * listener will try to align separators by adding whitespace as necessary. // * The goal to make ' . . ' appears to have the same width as '88.88.8888' // * even with a proportional font. // */ // private static final class DatePaintListener implements PaintListener { // // private Font font; // private int widthEight; // private int widthSpace; // // public void paintControl(PaintEvent e) { // Text control = (Text) e.widget; // if (control.isFocusControl() || control.isDisposed()) { // return; // } // GC gc = e.gc; // updateWidths(gc); // if (widthEight == widthSpace) { // no padding necessary // return; // } // Point size = control.getSize(); // Rectangle clientArea = control.getClientArea(); // String text = getPaddedText(control.getText()); // Point textExt = gc.stringExtent(text); // int delta = Math.max(0, 2 * (size.x - clientArea.width)); // int x = size.x - delta - textExt.x; // gc.fillRectangle(0, 0, size.x, size.y); // gc.drawString(text, x, 1); // } // // // helping methods // ////////////////// // // /** // * Update the stored widths of the '8' and ' ' characters, if the font // * has changed. // */ // private void updateWidths(GC gc) { // Font font = gc.getFont(); // if (this.font != font) { // this.font = font; // widthEight = gc.getAdvanceWidth('8'); // widthSpace = gc.getAdvanceWidth(' '); // // System.out.println("8= " + widthEight + " sp= " + widthSpace); // } // } // // /** // * Replace single space characters with several spaces in order to match // * the width of a regular character. // */ // private String getPaddedText(String input) { // StringBuilder result = new StringBuilder(input.length()); // for (int i = 0; i < input.length(); i++) { // char ch = input.charAt(i); // if (ch == ' ') { // int lookAhead = computeLookAhead(input, i); // int numChars = 1 + lookAhead; // int width = widthEight * numChars; // while (width >= widthSpace) { // width = width - widthSpace; // result.append(' '); // } // i = i + lookAhead; // } else { // result.append(ch); // } // } // return result.toString(); // } // // /** // * Returns the number of space ' ' characters after the start position. // */ // private int computeLookAhead(String text, int start) { // int result = 0; // for (int i = start + 1; i < text.length(); i++) { // if (text.charAt(i) == ' ') { // result++; // } else { // return result; // } // } // return result; // } // } }