/******************************************************************************* * Copyright (c) 2007 IBM Corporation. * 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: * Robert Fuhrer (rfuhrer@watson.ibm.com) - initial API and implementation *******************************************************************************/ package org.eclipse.imp.preferences.fields; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.imp.preferences.IPreferencesService; import org.eclipse.imp.preferences.PreferencesTab; import org.eclipse.imp.preferences.PreferencesUtilities; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.SWT; 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.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Text; import org.osgi.service.prefs.BackingStoreException; /** * @author sutton */ public class StringFieldEditor extends FieldEditor { public interface Validator { /** * @param value * @return a non-null error message if the value fails validation */ String validate(String value); } /** * Cached valid state. */ private boolean isValid; /** * Old text value. */ //private String oldValue; protected String previousValue; /** * The text field, or <code>null</code> if none. */ protected Text textField; /** * Width of text field in characters; initially unlimited. */ protected int widthInChars = org.eclipse.jface.preference.StringFieldEditor.UNLIMITED; /** * Text limit of text field in characters; initially unlimited. */ protected int textLimit = org.eclipse.jface.preference.StringFieldEditor.UNLIMITED; /** * The error message, or <code>null</code> if none. */ protected String errorMessage; /** * Indicates whether the empty string is legal; * <code>true</code> by default. */ protected boolean emptyStringAllowed = true; /** * The empty string (in case you needed it) */ protected final String emptyValue = ""; /** * The validation strategy; * <code>VALIDATE_ON_KEY_STROKE</code> by default. */ protected int validateStrategy = org.eclipse.jface.preference.StringFieldEditor.VALIDATE_ON_KEY_STROKE; protected Validator validator; /** * Creates a string field editor. * Use the method <code>setTextLimit</code> to limit the text. * * @param name the name of the preference this field editor works on * @param labelText the label text of the field editor * @param width the width of the text input field in characters, * or <code>UNLIMITED</code> for no limit * @param strategy either <code>VALIDATE_ON_KEY_STROKE</code> to perform * on the fly checking (the default), or <code>VALIDATE_ON_FOCUS_LOST</code> to * perform validation only after the text has been typed in * @param parent the parent of the field editor's control * @since 2.0 */ public StringFieldEditor( PreferencePage page, PreferencesTab tab, IPreferencesService service, String level, String name, String labelText, int width, int strategy, Composite parent) { super(page, tab, service, level, name, labelText, parent); // Relating to StrinfFieldEditor things init(name, labelText); widthInChars = width; setValidateStrategy(strategy); isValid = false; // Why set this in a local field rather than in the page? errorMessage = JFaceResources .getString("StringFieldEditor.errorMessage");//$NON-NLS-1$ createControl(parent); } /** * Creates a string field editor. * Use the method <code>setTextLimit</code> to limit the text. * * @param name the name of the preference this field editor works on * @param labelText the label text of the field editor * @param width the width of the text input field in characters, * or <code>UNLIMITED</code> for no limit * @param parent the parent of the field editor's control */ public StringFieldEditor( PreferencePage page, PreferencesTab tab, IPreferencesService service, String level, String name, String labelText, int width, Composite parent) { this(page, tab, service, level, name, labelText, (width == org.eclipse.jface.preference.StringFieldEditor.UNLIMITED ? 40 : width), org.eclipse.jface.preference.StringFieldEditor.VALIDATE_ON_KEY_STROKE, parent); } /** * Creates a string field editor of unlimited width. * Use the method <code>setTextLimit</code> to limit the text. * * @param name the name of the preference this field editor works on * @param labelText the label text of the field editor * @param parent the parent of the field editor's control */ public StringFieldEditor( PreferencePage page, PreferencesTab tab, IPreferencesService service, String level, String name, String labelText, Composite parent) { this(page, tab, service, level, name, labelText, 40, parent); } @Override public Composite getHolder() { return getTextControl().getParent(); } /** * Sets the strategy for validating the text. * <p> * Calling this method has no effect after <code>createPartControl</code> * is called. Thus this method is really only useful for subclasses to call * in their constructor. However, it has public visibility for backward * compatibility. * </p> * * @param value either <code>VALIDATE_ON_KEY_STROKE</code> to perform * on the fly checking (the default), or <code>VALIDATE_ON_FOCUS_LOST</code> to * perform validation only after the text has been typed in * * Copied from StringFieldEditor and adapted to this location. */ public void setValidateStrategy(int value) { Assert.isTrue(value == org.eclipse.jface.preference.StringFieldEditor.VALIDATE_ON_FOCUS_LOST || value == org.eclipse.jface.preference.StringFieldEditor.VALIDATE_ON_KEY_STROKE); validateStrategy = value; } public void setValidator(Validator validator) { this.validator = validator; } /* * Methods related to a special value for this field, treated * as a string (as you would expect) */ /** * @return The special value associated with this field, if there * is such a value and it is a string * @throws IllegalStateException * If the field does not have a special value or * if the special value is not a string */ public String getSpecialStringValue() { if (!hasSpecialValue) { throw new IllegalStateException("StringField.getSpecialValue(): field does not have a special value"); } else if (!(specialValue instanceof String)) { throw new IllegalStateException("StringField.getSpecialValue(): special value is not a String"); } else { return (String) specialValue; } } /** * Set the special value associated with this field to be the given string. * Overrides the method in the supertype to check that the given value is * a String. * * @param specialValue The special value to associate with this field * @throws IllegalStateException * If the given value is not a String */ public void setSpecialValue(String specialValue) { if (!(specialValue instanceof String)) { throw new IllegalStateException("StringField.setSpecialValue(): given value is not a String"); } hasSpecialValue = true; this.specialValue = specialValue; } /* * Methods related to whether the empty string is allowed * for this field. * * These methods mimic coresponding instance methods defined * on the superclass. */ /** * @return Whether the empty string is allowed as a value * for this field. */ public boolean isEmptyValueAllowed() { return emptyStringAllowed; } /** * Sets whether the empty string is allowed as a value * for this field. * * @param allowed Whether ... */ public void setEmptyValueAllowed(boolean allowed) { emptyStringAllowed = allowed; } /** * @return The empty value for fields of this type, * i.e., the empty string, regardless of whether * that value is allowed for a particular field */ public String getEmptyValue() { return emptyValue; } /* (non-Javadoc) * Method declared on FieldEditor. */ public boolean isValid() { return isValid; } /* (non-Javadoc) * Method declared on FieldEditor. */ protected void refreshValidState() { isValid = checkState(); } /* * Methods related to loading values from the preferences service * into the preferences store. * * All of the "doLoad..." methods should * - Set isInherited, presentsDefaultValue, and levelFromWhichLoaded * since these are know directly here and vary from load method to * load method * - Call setStringValue(..), which will set previousValue and * fieldModified (which can be set generally given the old and * new values), and which will also call valueChanged(), which */ /** * Load the string value for this field. Load it according to * the preference level associated with this field, if there is one, * or load it according to an applicable level, if not. */ protected void doLoad() { if (getTextControl(parent) != null) { String value = null; if (preferencesLevel != null) { // The "normal" case, in which field corresponds to a preferences level value = preferencesService.getRawStringPreference(preferencesLevel, getPreferenceName()); levelFromWhichLoaded = preferencesLevel; setInherited(false); } else { // Not normal, exactly, but possible if loading is being done into a // field that is not associated with a specific level value = preferencesService.getRawStringPreference(getPreferenceName()); levelFromWhichLoaded = preferencesService.getApplicableLevel(getPreferenceName(), preferencesLevel); setInherited(!levelFromWhichLoaded.equals(preferencesLevel)); } setPresentsDefaultValue(IPreferencesService.DEFAULT_LEVEL.equals(levelFromWhichLoaded)); setStringValue(value); } } /** * Load the default value associated with this field. That is, load * the value set at the default level for this field, regardless of * the actual level of this field. */ protected void doLoadDefault() { if (getTextControl(parent) != null) { String value = preferencesService.getRawStringPreference(IPreferencesService.DEFAULT_LEVEL, getPreferenceName()); levelFromWhichLoaded = IPreferencesService.DEFAULT_LEVEL; setInherited(false); // We're putting the default value here directly, not inheriting it setPresentsDefaultValue(true); // Need this really? setStringValue(value); // calls valueChanged(); // SMS 28 Nov 2006 added here // Value is default but is not inherited Text text = getTextControl(parent); text.setBackground(PreferencesUtilities.colorWhite); } } /** * Do the work of loading the value for the given level into the field. */ protected void doLoadLevel(String level) { if (getTextControl(parent) != null) { String value = null; if (preferencesLevel != null) { value = preferencesService.getRawStringPreference(level, getPreferenceName()); } else { // TODO: Check whether this is right value = preferencesService.getRawStringPreference(getPreferenceName()); } // We're putting the level's value here directly, not inheriting it, so ... levelFromWhichLoaded = level; setInherited(false); setPresentsDefaultValue(IPreferencesService.DEFAULT_LEVEL.equals(level)); setStringValue(value); // calls valueChanged(); } } /** * Do the work of setting the currently applicable value for this field, * inheriting the value from a higher level if the value is not stored * on the level associated with the field. (The "default" level should * always have a value.) Load nothing and return null if no value is found. * * Should set varous fields such as levelFromWhichLoaded, previousValue, * isInherited, and fieldModified. Should also adjust the appearance of * the field on the preferences page to reflect inherited state. * * @return The level from which the applicable value was loaded or * null if no value found. */ protected String doLoadWithInheritance() { String levelLoaded = null; String[] levels = IPreferencesService.levels; int fieldLevelIndex = 0; // If we're loading with inheritance for some field that is // not attached to a preferences level (such as the "applicable" // field, which inherits values from all of the real fields) // then assume that we should search from the bottom up String tmpPreferencesLevel = (preferencesLevel == null)? levels[0]: preferencesLevel; // Find the index of the level to which this field belongs for (int i = 0; i < levels.length; i++) { if (tmpPreferencesLevel.equals(levels[i])) { fieldLevelIndex = i; break; } } String value = null; int levelAtWhichFound = -1; for (int level = fieldLevelIndex; level < levels.length; level++) { value = preferencesService.getRawStringPreference(levels[level], getPreferenceName()); if (value == null) continue; if (value.equals("") && !isEmptyValueAllowed()) continue; levelAtWhichFound = level; levelLoaded = levels[levelAtWhichFound]; break; } levelFromWhichLoaded = levelLoaded; setInherited(fieldLevelIndex != levelAtWhichFound); setPresentsDefaultValue(IPreferencesService.DEFAULT_LEVEL.equals(levelFromWhichLoaded)); setStringValue(value); // sets fieldModified and previousValue // Set the background color of the field according to where found Text text = getTextControl(parent); if (isInherited()) text.setBackground(PreferencesUtilities.colorBluish); else text.setBackground(PreferencesUtilities.colorWhite); return levelLoaded; } /* * Method related to storing String values for this field */ protected void doStore() { String value = getTextControl(parent).getText(); boolean isEmpty = value.equals(emptyValue); // getText() will return an empty string if the field is empty, // and empty strings can be stored in the preferences service, // but an empty string is recognized by the preferences service // as a valid value--when usually it is not. Once it is recognized // as a valid value, it precludes the searching of subsequent // levels that might contain a non-empty (and actually valid) value. // We would like to be able to store a null value with the preferences // service so as to not short-circuit the search process, but we can't // do that. So, if the field value is empty, we have to eliminate the // preference entirely. (Will that work in general???) if (isEmpty && !isEmptyValueAllowed()) { // We have an empty value where that isn't allowed, so clear the // preference. Expect that clearing the preferences at a level will // trigger a loading with inheritance at that level preferencesService.clearPreferenceAtLevel(preferencesLevel, getPreferenceName()); // If the preference value was previously empty (e.g., if previously inherited) // then clearing the preference node now doesn't cause a change event, so // doesn't trigger reloading with inheritance. So we should just load the // field again to make sure any inheritance occurs if needed loadWithInheritance(); return; } // We have a value (possibly empty, if that is allowed) that has changed // from the previous value, so store it preferencesService.setStringPreference(preferencesLevel, getPreferenceName(), value); fieldModified = false; levelFromWhichLoaded = preferencesLevel; isInherited = false; setPresentsDefaultValue( value.equals(preferencesService.getRawStringPreference(IPreferencesService.DEFAULT_LEVEL, getPreferenceName()))); // If we've stored the field then it's not inherited, so be sure it's // color indicates that. // For text fields, the background color is the backgroud color within // the field, so don't have to worry about matching anything else getTextControl(parent).setBackground(PreferencesUtilities.colorWhite); IEclipsePreferences node = preferencesService.getNodeForLevel(preferencesLevel); try { if (node != null) node.flush(); } catch (BackingStoreException e) { System.err.println("StringFieldEditor.doStore(): BackingStoreException flushing node; node may not have been flushed:" + "\n\tnode path = " + node.absolutePath() + ", preferences level = " + preferencesLevel); } } /* * Methods related to getting and setting the String value for this field */ /** * @return the previous value held by this field */ protected String getPreviousStringValue() { return (String) previousValue; } /** * Sets the previous value held by this field */ protected void setPreviousStringValue(String value) { previousValue = value; } /** * Set the value of this field directly, from outside of * the field, without loading a value from the preferences * service. * * Intended for use by external clients of the field. * * In addition to setting the value of the field this method * also sets several attributes to appropriately characterize * a field that has been set in this way. * * @param newValue */ public void setFieldValueFromOutside(String newValue) { setPreviousStringValue(getStringValue()); setInherited(false); setPresentsDefaultValue(false); levelFromWhichLoaded = null; setStringValue(newValue); } /** * Returns the field editor's current value. * * @return the current value */ public String getStringValue() { String value = getTextControl(parent).getText(); return value; } /** * Set this field editor's value. * Likewise set previousValue and fieldModified flags, * and call valueChanged() to signal that the value has * changed. * * * @param value the new value, or <code>null</code> meaning the empty string */ protected void setStringValue(String value) { if (getTextControl(parent) != null) { if (value == null) value = "";//$NON-NLS-1$ setPreviousStringValue(getTextControl(parent).getText()); getTextControl(parent).setText(value); // Set fieldModified here because we know we just set it // and we consider it modified even if the old and new // values are the same fieldModified = true; // Set on this level, and so not inherited // SMS 17 Nov 2006: level and inherited should be set in doLoad methods // Or wherever else setStringValue is called from (e.g., listeners) //levelFromWhichLoaded = preferencesLevel; //isInherited = levelFromWhichLoaded == preferencesLevel; valueChanged(); // valueChanged() takes care of setting the previous value // and presents default value // } // setPresentsDefaultValue( // value.equals(preferencesService.getStringPreference(IPreferencesService.DEFAULT_LEVEL, getPreferenceName()))); } } /** * Informs this field editor's listener, if it has one, about a change * to the value (<code>VALUE</code> property) provided that the old and * new values are different. Also informs the listener (if there is one) * of a change in the validity of the field (<code>IS_VALID</code> property). * * This hook is <em>not</em> called when the text is initialized * (or reset to the default value) from the preference store. * (That comment is taken from the original implementation of this * method. I've tried to follow it consistently for IMP preferences, * but I'm not sure if the original intention translates into the * multi-level model. Still, so far there seems to be no problem * with it. SMS 16 Nov 2006) * * Copied from StringFieldEditor and adapted to use in IMP. * Added return of a boolean value. Not intended to set any attributes * of the field editor, just to signal changes to listeners. */ protected boolean valueChanged() { return valueChanged(false); } protected boolean valueChanged(boolean assertChanged) { // Check for change in value boolean valueChanged = assertChanged || inheritanceChanged(); String newValue = textField.getText(); if (!valueChanged) { valueChanged = newValue.equals(getPreviousStringValue()); } if (valueChanged) { // Check for change in validity fireValueChanged(VALUE, getPreviousStringValue(), newValue); boolean wasValid = isValid; refreshValidState(); if (isValid != wasValid) fireStateChanged(IS_VALID, wasValid, isValid); fieldModified = true; setPreviousStringValue(newValue); setModifiedMarkOnLabel(); // SMS 28 Nov 2006 added here // Set the background color of the field according to where found Text text = getTextControl(parent); if (isInherited()) text.setBackground(PreferencesUtilities.colorBluish); else text.setBackground(PreferencesUtilities.colorWhite); } return valueChanged; } /** * Checks whether the text input field contains a valid value or not. * * @return <code>true</code> if the field value is valid, * and <code>false</code> if invalid */ protected boolean checkState() { boolean result = true; if (!emptyStringAllowed && getStringValue().length() == 0) { setErrorMessage(getFieldMessagePrefix() + "String is empty when empty string is not allowed"); result = false; } if (result && getTextControl(parent) == null) { setErrorMessage(getFieldMessagePrefix() + "Text control is null; no valid value represented"); result = false; } if (result && getTextControl(parent) != null) { String txt = getStringValue(); result = (txt.trim().length() > 0) || emptyStringAllowed; if (!result) { setErrorMessage(getFieldMessagePrefix() + "String is blank when empty string is not allowed"); } } if (result) result = result && doCheckState(); return notifyState(result); } /** * A hook for subclasses to implement more specialized checks * * @return Whether the state is okay based on additional checks */ protected boolean doCheckState() { if (validator != null) { String msg= validator.validate(getStringValue()); if (msg != null) { setErrorMessage(msg); return false; } } clearErrorMessage(); return true; } protected boolean textNull = true; public Text getTextControl() { return getTextControl(parent); } /** * Returns this field editor's text control. * * The control is created if it does not yet exist * * Copied from StringFieldEditor and adapted to this location. * * @param parent the parent * @return the text control */ public Text getTextControl(Composite parent) { if (textField == null) { textField = new Text(parent, SWT.SINGLE | SWT.BORDER); textField.setFont(parent.getFont()); String toolTipText = getToolTipText(); if (toolTipText != null) { textField.setToolTipText(toolTipText); } switch (validateStrategy) { case org.eclipse.jface.preference.StringFieldEditor.VALIDATE_ON_KEY_STROKE: textField.addKeyListener(new KeyAdapter() { /* (non-Javadoc) * @see org.eclipse.swt.events.KeyAdapter#keyReleased(org.eclipse.swt.events.KeyEvent) */ public void keyReleased(KeyEvent e) { // Should call setInherited(..) before calling valueChanged() because // valueChanged() will mark the field as modified, but only if isInherited // is false, which it now should be refreshValidState(); setInherited(false); boolean changed = valueChanged(true); fieldModified = changed; } }); break; case org.eclipse.jface.preference.StringFieldEditor.VALIDATE_ON_FOCUS_LOST: textField.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { clearErrorMessage(); } }); textField.addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent e) { refreshValidState(); } public void focusLost(FocusEvent e) { // Assume no change in inheritance with this event valueChanged(); clearErrorMessage(); } }); break; default: Assert.isTrue(false, "Unknown validate strategy");//$NON-NLS-1$ } // SMS 16 Nov 2006 // The original StringFieldEditor just had a text-field modify listener, // as copied below. But that would be redundant with the listeners copied // from StringFieldEditor above. The use of more than one listener for the // same event is problematic if they all check for changes in the text value, // because only the first will see a change, and other will set fieldModified // to false, in effect cancelling processing that should occur following the // triggering change // textField.addModifyListener( // new ModifyListener() { // public void modifyText(ModifyEvent e) { // //System.out.println("STFE.text modify listener (from getTextControl): textModified set to true"); // valueChanged(); // fieldModified = true; // setInherited(false); // } // } // ); textField.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent event) { textField = null; } }); if (textLimit > 0) {//Only set limits above 0 - see SWT spec textField.setTextLimit(textLimit); } } else { checkParent(textField, parent); } return textField; } /* * UI-related methods copied from StringFieldEditor */ /** * Does something with the columns * (but the IMP templates adjust columns separately) */ protected void adjustForNumColumns(int numColumns) { GridData gd = (GridData) textField.getLayoutData(); gd.horizontalSpan = numColumns - 1; // We only grab excess space if we have to // If another field editor has more columns then // we assume it is setting the width. gd.grabExcessHorizontalSpace = gd.horizontalSpan == 1; } /** * Fills this field editor's basic controls into the given parent. * <p> * The string field implementation of this <code>FieldEditor</code> * framework method contributes the text field. Subclasses may override * but must call <code>super.doFillIntoGrid</code>. * </p> */ protected void doFillIntoGrid(Composite parent, int numColumns) { getLabelControl(parent); textField = getTextControl(parent); GridData gd = new GridData(); gd.horizontalSpan = numColumns - 1; if (widthInChars != org.eclipse.jface.preference.StringFieldEditor.UNLIMITED) { GC gc = new GC(textField); try { Point extent = gc.textExtent("X");//$NON-NLS-1$ gd.widthHint = widthInChars * extent.x; } finally { gc.dispose(); } } else { gd.horizontalAlignment = GridData.FILL; gd.grabExcessHorizontalSpace = true; } textField.setLayoutData(gd); } @Override protected void doSetToolTip() { if (toolTipText != null) { getTextControl().setToolTipText(toolTipText); } } /** * Returns the number of controls in this editor. * These are two: the text control and the label control. * * @return The number of controls in this editor */ public int getNumberOfControls() { return 2; } }