/*******************************************************************************
* 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 org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.conversion.IConverter;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.swt.SWT;
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.Color;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Text;
import org.eclipse.riena.core.util.RienaConfiguration;
import org.eclipse.riena.ui.ridgets.IMarkableRidget;
import org.eclipse.riena.ui.ridgets.IRidget;
import org.eclipse.riena.ui.ridgets.ITextRidget;
import org.eclipse.riena.ui.ridgets.swt.AbstractEditableRidget;
import org.eclipse.riena.ui.ridgets.swt.MarkerSupport;
import org.eclipse.riena.ui.ridgets.validation.ValidationRuleStatus;
import org.eclipse.riena.ui.swt.facades.SWTFacade;
import org.eclipse.riena.ui.swt.utils.SwtUtilities;
/**
* Ridget for an SWT <code>Text</code> widget.
*/
public class TextRidget extends AbstractEditableRidget implements ITextRidget {
/**
* This property is used by the databinding to sync ridget and model. It is always fired before its sibling {@link ITextRidget#PROPERTY_TEXT} to ensure that
* the model is updated before any listeners try accessing it.
* <p>
* This property is not API. Do not use in client code.
*/
private static final String PROPERTY_TEXT_INTERNAL = "textInternal"; //$NON-NLS-1$
private static final String EMPTY_STRING = ""; //$NON-NLS-1$
protected final FocusListener focusListener;
protected final KeyListener crKeyListener;
protected final ModifyListener modifyListener;
protected final ValidationListener verifyListener;
private String textValue = EMPTY_STRING;
private boolean isDirectWriting;
private IConverter inputConverter;
private boolean multilineIgnoreEnterKey;
private final static boolean DEFAULT_DIRECTWRITING = getDefaultTextRidgetDirectWritingEnabled();
/**
* This system property controls {@code RienaStatus.getDefaultTextRidgetDirectWritingEnabled}
*/
private static final String RIENA_TEXT_RIDGET_DIRECTWRITING_PROPERTY = "riena.textridget.directwriting"; //$NON-NLS-1$
private static final String DIRECTWRITING_DEFAULT = "false"; //$NON-NLS-1$
/**
* Checks if the systemproperty <code>riena.textridget.directwriting</code> was given, that indicates that every TextRidget has per default directwriting
* enabled.
*
* @return <code>true</code> if per default directwriting is enabled in TextRidgets, otherwise <code>false</code>
*/
private static boolean getDefaultTextRidgetDirectWritingEnabled() {
return Boolean.parseBoolean(System.getProperty(RIENA_TEXT_RIDGET_DIRECTWRITING_PROPERTY, DIRECTWRITING_DEFAULT));
}
public TextRidget() {
crKeyListener = new CRKeyListener();
focusListener = new FocusManager();
modifyListener = new SyncModifyListener();
verifyListener = new ValidationListener();
isDirectWriting = DEFAULT_DIRECTWRITING;
addPropertyChangeListener(IRidget.PROPERTY_ENABLED, new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
forceTextToControl(textValue);
}
});
addPropertyChangeListener(IMarkableRidget.PROPERTY_OUTPUT_ONLY, new PropertyChangeListener() {
public void propertyChange(final PropertyChangeEvent evt) {
updateEditable();
forceTextToControl(textValue);
}
});
multilineIgnoreEnterKey = Boolean.valueOf(RienaConfiguration.getInstance().getProperty(RienaConfiguration.MULTILINE_TEXT_IGNORE_ENTER_KEY));
}
protected TextRidget(final String initialValue) {
this();
Assert.isNotNull(initialValue);
textValue = initialValue;
}
@Override
protected IObservableValue getRidgetObservable() {
return BeansObservables.observeValue(this, PROPERTY_TEXT_INTERNAL);
}
@Override
protected void checkUIControl(final Object uiControl) {
checkType(uiControl, Text.class);
}
/**
* Returns true, if the input to this method is considered 'non empty', false otherwise.
* <p>
* Subclasses may override, to provide their own notion what is considered to be an 'non empty' String value.
*
* @param input
* a String; never null
* @return false if the input is considered 'empty', true otherwise
*/
protected boolean isNotEmpty(final String input) {
return input.length() > 0;
}
@Override
protected final synchronized void bindUIControl() {
final Text control = getTextWidget();
if (control != null) {
setUIText(textValue);
updateEditable();
addListeners(control);
}
}
@Override
protected final synchronized void unbindUIControl() {
super.unbindUIControl();
final Text control = getTextWidget();
if (control != null) {
removeListeners(control);
}
}
/**
* Template method for adding listeners to the control. Will be called automatically. Children can override but must call super.
*
* @param control
* a Text instance (never null)
*/
protected synchronized void addListeners(final Text control) {
control.addKeyListener(crKeyListener);
control.addFocusListener(focusListener);
control.addModifyListener(modifyListener);
control.addVerifyListener(verifyListener);
}
/**
* Template method for removing listeners from the control. Will be called automatically. Children can override but must call super.
*
* @param control
* a Text instance (never null)
*/
protected synchronized void removeListeners(final Text control) {
control.removeKeyListener(crKeyListener);
control.removeFocusListener(focusListener);
control.removeModifyListener(modifyListener);
control.removeVerifyListener(verifyListener);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.riena.ui.ridgets.ITextRidget#setMultilineIgnoreEnterKey(boolean)
*/
public void setMultilineIgnoreEnterKey(final boolean multilineIgnoreEnterKey) {
this.multilineIgnoreEnterKey = multilineIgnoreEnterKey;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.riena.ui.ridgets.ITextRidget#isMultilineIgnoreEnterKey()
*/
public boolean isMultilineIgnoreEnterKey() {
return multilineIgnoreEnterKey;
}
// helping methods
// ////////////////
/**
* Given an input {@value} , compute an output value for the UI control, based on the current marker state. The method is called when any of the following
* markers changes state: output, read-only.
* <p>
* Subclasses may override, but should call super.
*
* @since 2.0
*/
protected String getTextBasedOnMarkerState(final String value) {
final boolean hideValue = !isEnabled() && MarkerSupport.isHideDisabledRidgetContent();
return hideValue ? EMPTY_STRING : value;
}
/**
* Returns the underlying Text control.
* <p>
* Ridgets that wrap a Text widget into other UI elements, may override this method to provide access to the text widget.
*
* @return the Text control to be used by this ridget; may be null
* @since 2.0
*/
protected Text getTextWidget() {
return (Text) getUIControl();
}
protected String getUIText() {
final Text control = getTextWidget();
Assert.isNotNull(control);
return control.getText();
}
protected void updateEditable() {
final Text control = getTextWidget();
if (control != null && !control.isDisposed()) {
final boolean isEditable = isOutputOnly() ? false : true;
if (isEditable != control.getEditable()) {
final Color bgColor = control.getBackground();
control.setEditable(isEditable);
// workaround for Bug 315689 / 315691
control.setBackground(bgColor);
}
}
}
protected void setUIText(final String text) {
final Text control = getTextWidget();
if (control != null) {
control.setText(getTextBasedOnMarkerState(text));
control.setSelection(0, 0);
}
}
protected void selectAll() {
final Text text = getTextWidget();
// if not multi line text field
if (text != null && (text.getStyle() & SWT.MULTI) == 0) {
text.selectAll();
}
}
public synchronized String getText() {
return textValue;
}
/**
* This method is not API. Do not use in client code.
*
* @noreference This method is not intended to be referenced by clients.
*/
public final synchronized String getTextInternal() {
return getText();
}
public void setInputToUIControlConverter(final IConverter converter) {
if (converter != null) {
Assert.isLegal(converter.getFromType() == String.class, "Invalid from-type. Need a String-to-String converter"); //$NON-NLS-1$
Assert.isLegal(converter.getToType() == String.class, "Invalid to-type. Need a String-to-String converter"); //$NON-NLS-1$
}
this.inputConverter = converter;
}
/**
* {@inheritDoc}
* <p>
* Invoking this method will copy the given text into the ridget and the widget regardless of the validation outcome. If the text does not pass validation
* the error marker will be set and the text will <b>not</b> be copied into the model. If validation passes the text will be copied into the model as well.
* <p>
* Passing a null value is equivalent to {@code setText("")}.
*/
public synchronized void setText(final String text) {
final String oldValue = textValue;
textValue = text != null ? text : EMPTY_STRING;
forceTextToControl(textValue);
disableMandatoryMarkers(isNotEmpty(textValue));
final IStatus onEdit = checkOnEditRules(textValue, new ValidationCallback(false));
if (onEdit.isOK()) {
firePropertyChange(PROPERTY_TEXT_INTERNAL, oldValue, textValue);
firePropertyChange(ITextRidget.PROPERTY_TEXT, oldValue, textValue);
}
}
/**
* This method is not API. Do not use in client code.
*
* @noreference This method is not intended to be referenced by clients.
*/
public final synchronized void setTextInternal(final String text) {
setText(text);
}
public synchronized boolean revalidate() {
if (getUIControl() != null) {
textValue = getUIText();
}
forceTextToControl(textValue);
disableMandatoryMarkers(isNotEmpty(textValue));
final IStatus status = checkAllRules(textValue, new ValidationCallback(false));
if (status.isOK()) {
getValueBindingSupport().updateFromTarget();
}
return !isErrorMarked();
}
/**
* {@inheritDoc}
* <p>
* Invoking this method will copy the model value into the ridget and the widget regardless of the validation outcome. If the model value does not pass
* validation, the error marker will be set.
*/
@Override
public synchronized void updateFromModel() {
super.updateFromModel();
// As per Bug 319938 - we use getText() instead of textValue for this
// check, to have it done on the String that the databinging sees. Some
// subclasses such as NumericTextRidget override getText() and return a
// value different from textInternal. In retrospect getText() should have
// been final.
checkAllRules(getText(), new ValidationCallback(false));
}
public synchronized boolean isDirectWriting() {
return isDirectWriting;
}
public synchronized void setDirectWriting(final boolean directWriting) {
if (this.isDirectWriting != directWriting) {
this.isDirectWriting = directWriting;
}
}
@Override
public final boolean isDisableMandatoryMarker() {
return isNotEmpty(textValue);
}
// helping methods
// ////////////////
synchronized void forceTextToControl(final String newValue) {
final Text control = getTextWidget();
if (!SwtUtilities.isDisposed(control)) {
final SWTFacade facade = SWTFacade.getDefault();
final Object[] vListeners = facade.removeListeners(control, SWT.Verify);
final Object[] mListeners = facade.removeListeners(control, SWT.Modify);
TextRidget.this.setUIText(newValue);
facade.addListeners(control, SWT.Modify, mListeners);
facade.addListeners(control, SWT.Verify, vListeners);
}
}
private synchronized void updateTextValue() {
if (isOutputOnly()) {
return;
}
final String oldValue = textValue;
final String newValue = getUIText();
if (!oldValue.equals(newValue)) {
textValue = newValue;
if (checkOnEditRules(newValue, null).isOK()) {
firePropertyChange(PROPERTY_TEXT_INTERNAL, oldValue, newValue);
if (isExternalValueChange(oldValue, newValue)) {
firePropertyChange(ITextRidget.PROPERTY_TEXT, oldValue, newValue);
}
}
}
}
/**
* Answers true if the the {@link TextRidget} should fire a {@link PropertyChangeEvent} for property {@link ITextRidget#PROPERTY_TEXT} given the transition
* from oldValue to newValue.
*/
protected boolean isExternalValueChange(final String oldValue, final String newValue) {
return true;
}
protected void enterKeyReleased() {
if (multilineIgnoreEnterKey && isMultiline()) {
return;
}
updateTextValue();
}
/**
* @return <code>true</code> if the ridget is bound to a multiline text field (style is {@link SWT#MULTI})
*
*/
private boolean isMultiline() {
final Control textField = getUIControl();
return textField != null && (textField.getStyle() & SWT.MULTI) != 0;
}
// helping classes
// ////////////////
/**
* Update text value in ridget when ENTER is pressed
*/
private final class CRKeyListener extends KeyAdapter implements KeyListener {
@Override
public void keyReleased(final KeyEvent e) {
if (e.character == '\r') {
enterKeyReleased();
}
}
}
/**
* Manages activities trigger by focus changed:
* <ol>
* <li>select single line text fields, when focus is gained by keyboard</li>
* <li>update text value in ridget, when focus is lost</li>
* <ol>
*/
private final class FocusManager implements FocusListener {
public void focusGained(final FocusEvent e) {
if (isFocusable() && !isOutputOnly()) {
selectAll();
}
}
public void focusLost(final FocusEvent e) {
updateTextValue();
}
}
/**
* Updates the text value in the ridget, if direct writing is enabled.
*/
private final class SyncModifyListener implements ModifyListener {
public void modifyText(final ModifyEvent e) {
if (isDirectWriting) {
updateTextValue();
}
final String text = getUIText();
disableMandatoryMarkers(isNotEmpty(text));
}
}
/**
* Validation listener that checks 'on edit' validation rules when the text widget's contents are modified by the user. If the new text value does not pass
* the test and outcome is ERROR_BLOCK_WITH_FLASH, the change will be rejected. If the new text passed the test, or fails the test without blocking, the
* value is copied into the ridget. This will fire a proprty change event (see {@link TextRidget#setText(String)}) causing the 'on update' validation rules
* to be checked and will copy the value into the model if it passes those checks.
*/
private final class ValidationListener implements VerifyListener {
public synchronized void verifyText(final VerifyEvent e) {
if (!e.doit) {
return;
}
if (inputConverter != null) {
e.text = (String) inputConverter.convert(e.text);
}
final String oldText = getUIText();
final String newText = getText(oldText, e);
final IStatus status = checkOnEditRules(newText, new ValidationCallback(true));
final boolean doit = !(status.getCode() == ValidationRuleStatus.ERROR_BLOCK_WITH_FLASH);
if (!doit) {
// we preserve the old text so we also have to restore the validation status
// see Bug 374184
checkOnEditRules(oldText, new ValidationCallback(false));
}
e.doit = doit;
}
private String getText(final String oldText, final VerifyEvent e) {
String newText;
// deletion
if (e.keyCode == 127 || e.keyCode == 8) {
newText = oldText.substring(0, e.start) + oldText.substring(e.end);
} else { // addition / replace
newText = oldText.substring(0, e.start) + e.text + oldText.substring(e.end);
}
return newText;
}
}
}