/*******************************************************************************
* 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 java.util.Arrays;
import java.util.List;
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.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
/**
* Points to note:
* - Preferences service instead of a preference store
* - Preferences have a level
* - Four fixed levels
* - Named by strings because ...
* - Makes some provision for loading, etc. into fields
* on pages that are *not* associated with a level (just in case)
* - Preferences are displayed on a tab on a page
* - Preference values may be inherited
*
* @author sutton
*
*/
public abstract class FieldEditor extends org.eclipse.jface.preference.FieldEditor
{
protected static final List<String> PREFS_LEVELS_AS_LIST= Arrays.asList(IPreferencesService.levels);
/**
* The preferences page on which the tab that contains this
* field is displayed
*/
// Relationship to dialogPage in FieldEditor?
protected PreferencePage prefPage = null;
/**
* The preferences tab on which the field is displayed
*/
protected PreferencesTab prefTab = null;
/**
* The preferences service in which the values for this
* field are stored
*/
protected IPreferencesService preferencesService = null;
/**
* The preferences level with which this field is associated
*/
protected String preferencesLevel = null;
/**
* The Composite control that contains the field
*/
protected Composite parent = null;
/**
* Whether the value shown for this field is stored on
* the level for this field or inherited from a higher level
*/
protected boolean isInherited = false;
/**
* Whether the value shown for this field was previously stored on
* the level for this field or inherited from a higher level
*/
protected boolean wasInherited = false;
/**
* Whether the value stored for this field (if any) can be
* removed. (Removal of a value on a lower level will
* generally result in the inheritance of a value from a
* higher level. The effects of removal of values at the
* top ("default") are not defined and this should generally
* not be allowed.)
*/
protected boolean isRemovable = false;
/**
* Flags whether the field has an associated "special"
* (distinguished) value (the purpose of which is
* generally field specific)
*/
protected boolean hasSpecialValue = false;
/**
* A general placeholder for a special (distinguished) value
* that may be associated with this field (the purpose of
* which is generally field specific)
*/
// Not sure whether such a general representation will be
// useful, but it conveys the idea
protected Object specialValue = null;
/**
* Name of the preferences level from which the value
* displayed by this field was loaded. If non-null then
* will generally be one of the four standard level names.
*/
protected String levelFromWhichLoaded = null;
/**
* Whether the value held by the field is the same as the
* value stored in the corresponding preferences node (that
* is, the node from which it was most recently loaded or
* the node into which it was most recently stored)
*/
protected boolean fieldModified = false;
protected Object previousValue = null;
protected String toolTipText = null;
//
// Fields in FieldEditor:
//
// private String preferenceName has public get and set methods
// private *** preferenceStore is deprecated (along with any associated methods)
// private boolean field isDefaultPresented has public read and write methods
// private String labelText has public (or protected) get and set methods;
// also settable through init (called by constructors but protected and not
// apparently limited to use at construction time)
// private Label label has public and protected get methods; public
// one creates when first called; no other set methods (makes sense)
// private field IPropertyChangeListener propertyChangeListener has a public
// set method but no get method (needed here?)
// private field DialogPage page has public get and set methods
// Note: there is no parent field
/**
* Parameterless constructor that mimics the one in FieldEditor
*/
protected FieldEditor() {
super();
}
/**
* Creates a field editor, taking the information that is
* specific to IMP field editors but not that used by
* field editors in general. Calls the empty constructor for
* a field editor.
*
* @param page The preferences page on which the tab is shown
* @param tab The tab on which field editor is shown
* @param service The preferences service in which the preference
* values are stored
* @param level The level at which this preference is assigned
*/
// Able to do anything useful without a parent?
public FieldEditor(
PreferencePage page, PreferencesTab tab,
IPreferencesService service, String level)
{
super();
preferencesService = service;
preferencesLevel = level;
prefPage = page;
setPage(prefPage);
prefTab = tab;
}
/**
* Creates a field editor, taking all of the relevant information
* (IMP specific and field-editor generic). Calls the non-empty
* constructor for a field editor.
*
* @param page The preferences page on which the tab is shown
* @param tab The tab on which field editor is shown
* @param service The preferences service in which the preference
* values are stored
* @param level The level at which this preference is assigned
* @param name The name of this preference
* @param labelText The text used to label the field on the page
* @param parent The composite control that contains this editor
*/
public FieldEditor(
PreferencePage page, PreferencesTab tab,
IPreferencesService service, String level,
String name, String labelText, Composite parent)
{
//super(name, labelText, parent);
super();
init(name, labelText);
preferencesService = service;
preferencesLevel = level;
this.parent = parent;
prefPage = page;
setPage(prefPage);
prefTab = tab;
}
/*
* Methods to get and set the preferences level from which the value
* of this field was loaded (or set)
*/
/**
* Get the preferences level from which the value of this field
* was set. Should be the name of a preferences level if the
* value was set from a preferences node, or null if the value
* was set directly into the field.
*
* @return The preferencesl level from which the current value
* for this field was loaded, or null if the current value
* was set directly
*/
public String getLevelFromWhichLoaded() {
return levelFromWhichLoaded;
}
/*
* Methods to get and set the isInherited field for this editor
*/
/**
* @return Whether the value displayed for this field is
* stored at the level of this field or inherited from
* a higher level
*/
public boolean isInherited() { return isInherited; }
/**
* Sets the field that indicates whether the value displayed for
* this field is stored on the level for this field or inherited from
* a higher level.
*
* @param inherited Whether the value is stored or inherited
*/
protected void setInherited(boolean inherited) {
wasInherited = isInherited;
isInherited = inherited;
}
/**
* @return Whether the inheritance state of this field has changed
* from its previous value
*/
protected boolean inheritanceChanged() {
return wasInherited != isInherited;
}
// Empty is a sting-related concept, not for fields in general
//
// public boolean isEmptyValueAllowed() {
// return isEmptyStringAllowed();
// }
//
// public void setEmptyValueAllowed(boolean allowed) {
// setEmptyStringAllowed(allowed);
// }
//
//
// public String getEmptyValue() {
// if (isEmptyStringAllowed())
// return emptyValue;
// throw new IllegalStateException("StringFieldEditor.getEmptyValue: called when field does not allow an empty value");
// }
/*
* Methods relating to the removal of field values
*/
/**
* @return Whether the value stored for this field (if any)
* can be removed
*/
public boolean isRemovable() { return isRemovable; }
/**
* Sets whether the value stored for this field (if any)
* can be removed
*
* @param isRemovable Whether ...
*/
public void setRemovable(boolean isRemovable) {
this.isRemovable = isRemovable;
}
////////////////////////////////////////////////////////////////////////////
/*
* Methods related to loading a value for the field. "Loading" a value for
* a field means setting the value that the field is to display.
* Because of multiple preference levels and inheritance of preference values
* this is a more varied and complicated concern than with FieldEditor.
*/
/**
* Initializes this field editor with a preference value from
* the preference service.
*
* If the field is associated with a specific preference level (the
* usual case) then the value from that level, if any, is loaded.
*
* If the field is not associated with a specific preference level,
* then a value obtained from some applicable level may be loaded.
* (This is a feature that supports preferences pages that are not
* strictly aligned with preferences levels, unlike the "usual"
* approach assumed here.)
*
* Because it cannot be guaranteed here that the value that is loaded
* is not inherited, the flag isDefaultPresented cannot be set to
* false here (as it can in FieldEditor).
*/
public void load() {
if (preferencesService != null) {
//isDefaultPresented = false;
doLoad();
refreshValidState();
}
}
/**
* Do the work of loading
*/
abstract protected void doLoad();
// // This is an example implementation from the IMP String field editor
// // with level-specific and level-independent branches:
// {
// if (getTextControl() != null) {
// String value = null;
// if (preferencesLevel != null) {
// // The "normal" case, in which field corresponds to a preferences level
// value = preferencesService.getStringPreference(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.getStringPreference(getPreferenceName());
// levelFromWhichLoaded = preferencesService.getApplicableLevel(getPreferenceName(), preferencesLevel);
// setInherited(true);
// }
// if (IPreferencesService.DEFAULT_LEVEL.equals(levelFromWhichLoaded))
// setPresentsDefaultValue(true);
// previousValue = value;
// setStringValue(value);
// }
// }
/**
* Initializes this field editor with the value associated with the
* default level of the preference store.
*/
public void loadDefault() {
if (preferencesService != null) {
setPresentsDefaultValue(true);
doLoadDefault();
refreshValidState();
}
}
/**
* Do the work of loading the default value into the field.
* Generally depends on the type of the field.
*/
abstract protected void doLoadDefault();
// // This is an example implementation from the IMP String field editor:
// {
// if (getTextControl() != null) {
// String value = preferencesService.getStringPreference(IPreferencesService.DEFAULT_LEVEL, getPreferenceName());
// setStringValue(value);
// }
// // empty in FieldEditor:
// refreshValidState();
// // Comments on valueChanged() says it is not called when
// // a value is initialized or restored from default so
// // don't call that here
// }
/**
* Set this field with the preference value associated with
* the given level. Sets presentsDefaultValue to true if the
* given level is "default".
*/
public void loadLevel(String level) {
if (preferencesService != null &&
preferencesService.isaPreferencesLevel(level))
{
doLoadLevel(level);
if (IPreferencesService.DEFAULT_LEVEL.equals(level))
setPresentsDefaultValue(true);
// SMS 25 Nov 2006
// still need to signal valueChanged on default level?
// else
// SMS 15 Nov 2006: try this if not default level
valueChanged();
refreshValidState();
}
}
/**
* Do the work of loading the value for the given level into the field.
* Generally depends on the type of the field.
*/
abstract protected void doLoadLevel(String level);
// // This is an example implementation from the IMP String field editor:
// {
// if (getTextControl() != null) {
// String value = null;
// if (preferencesLevel != null) {
// value = preferencesService.getStringPreference(level, getPreferenceName());
// } else {
// value = preferencesService.getStringPreference(getPreferenceName());
// }
// setStringValue(value);
// }
// //valueChanged();
// }
/**
* Set this field with the currently applicable preference value,
* 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.)
*
* @return The level from which the value was loaded
*/
public String loadWithInheritance() {
if (preferencesService != null) {
levelFromWhichLoaded = doLoadWithInheritance();
if (IPreferencesService.DEFAULT_LEVEL.equals(levelFromWhichLoaded))
setPresentsDefaultValue(true);
refreshValidState();
}
return levelFromWhichLoaded;
}
/**
* 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.
*/
abstract protected String doLoadWithInheritance();
// // This is an example implementation from the IMP String field editor:
// {
// 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 just 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.getStringPreference(levels[level], getPreferenceName());
// if (value == null) continue;
// if (value.equals("") && !isEmptyStringAllowed()) continue;
// levelAtWhichFound = level;
// levelLoaded = levels[levelAtWhichFound];
// break;
// }
//
// // Set the field to the value we found
// setStringValue(value);
//
// // We loaded it at this level or inherited it from some other level
// setInherited(fieldLevelIndex != levelAtWhichFound);
//
// // Since we just loaded some new text, it won't be modified yet
// fieldModified = false;
//
// // TODO: Check on use of previous value
// previousValue = value;
//
// // Set the background color of the field according to where found
// Text text = getTextControl();
// if (isInherited())
// text.setBackground(PreferencesUtilities.colorBluish);
// else
// text.setBackground(PreferencesUtilities.colorWhite);
//
// //System.out.println("doLoadWithInheritance: preferencesName = " + getPreferenceName() + "; preferenceLevel = " + preferencesLevel + "; levelLoaded = " + levelLoaded);
//
// return levelLoaded;
// }
/*
* Methods relating to storing the value of a field.
*/
/**
* Store the value in this field, using the preferences service,
* at the level associated with this field.
*
* Checks many preconditions. Throws an exception if the preferences
* service or preferences level is null or if the associated level is
* the project level but no project is set. Returns without effect if
* the value is inherited, if the value is the default value, or if the
* field is not modified.
*
* Why not store inherited values? Inherited values are shown for a
* field when the field has no value of its own. They are shown because
* they apply on the field's level but they are not actually set (or stored)
* on that level. Since there is no value actually set for the field,
* there is no value to store for it. Note that separate controls on
* a preferene page may make it possible to adopt an inherited value for
* a field (that is, to copy it into the field), after which it can be stored.
*
* Also sets fieldModified to false (since it was just stored and hasn't
* been changed yet) and levelFromWhichLoaded to the level associated with
* the current field.
*
* @throws IllegalStateException
* if the preferences service is null, if the preferences
* level is null, or if the associated level is the project
* level but no project is set
*
*/
public void store() {
// Don't store if the preferences service is null, since that may
// represent an illegal state and anyway we need to refer to it below
if (preferencesService == null) {
throw new IllegalStateException("FieldEditor.store(): attempt to store when preferences service is null");
}
// Can't store the value If there is no valid level (not having a preferences level
// isn't necessarily an error, but it does prevent storing)
//if (preferencesLevel == null) return;
if (preferencesService == null) {
throw new IllegalStateException("FieldEditor.store(): attempt to store when preferences level is null");
}
// Don't store a value that comes from some other level
if (isInherited) return;
//if (presentsDefaultValue()) return;
// Don't bother storing if the field hasn't been modified
if (!fieldModified) return;
// Don't store the value if the field's level is the project level
// but no project is selected
if (IPreferencesService.PROJECT_LEVEL.equals(preferencesLevel) &&
preferencesService.getProject() == null)
{
throw new IllegalStateException("FieldEditor.store(): attempt to store project preference when project is not set");
}
// If the level is the default level, go ahead and store it even
// though preferences on the default level aren't persistent:
// the preference still needs to be stored into the default preference
// node (since that is needed to put the new value into effect, and
// why provide a new value if you don't want it to go into effect?)
// and the flushing of that node doesn't have any effect in any case.
// In other words, do not return if the level is the default level
// Store the value
doStore();
// If we've just stored the field, we've addressed any modifications
//System.out.println("STFE.store: setting fieldModified to FALSE");
fieldModified = false;
levelFromWhichLoaded = preferencesLevel;
}
/**
* Do the work of actually storing the value.
* Generally depends on the specific type of the field.
* May also need to check field-specific conditions (e.g., whether
* empty values are allowed).
* If necessary also adjust appearance of field on the preference
* page to reflect the non-inherited state.
*/
abstract protected void doStore();
// // This is an example implementation from the IMP String field editor:
// {
// String value = getTextControl().getText();
// boolean isEmpty = value.equals(emptyValue); // Want empty value, but can't call method to retrieve it
// // with fields where empty is not allowed
// // 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 && !isEmptyStringAllowed()) {
// // 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;
// }
// // Shouldn't need this check here now
// if (isInherited() && !fieldModified) {
// // We have a value but it's inherited
// // (left over from after the last time we cleared the field)
// // so don't need (or want) to store it
// 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);
//
// // If we've just stored the field, we've addressed any modifications
// // Shouldn't need these here now
// fieldModified = false;
// levelFromWhichLoaded = preferencesLevel;
// // 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().setBackground(PreferencesUtilities.colorWhite);
//
// IEclipsePreferences node = preferencesService.getNodeForLevel(preferencesLevel);
// try {
// if (node != null) node.flush();
// } catch (BackingStoreException e) {
// System.err.println("StringFieldEditor. (): BackingStoreException flushing node; node may not have been flushed:" +
// "\n\tnode path = " + node.absolutePath() + ", preferences level = " + preferencesLevel);
// }
// }
/*
* Methods relating the preferences level associated with this field
*/
/**
* Set the preference level associated with this field to the given level.
*
* @param The string name of a preference level
* @throws IllegalArgumentException
* if the given value does not denote a preference level
*/
public void setPreferencesLevel(String level) {
if (!preferencesService.isaPreferencesLevel(level)) {
throw new IllegalArgumentException("FieldEditor.setPreferencesLevel(): given level = " + level + " is invalid");
}
preferencesLevel = level;
}
/**
* @return The string name of the preference level associated with
* this field
*/
public String getPreferencesLevel() {
return preferencesLevel;
}
/*
* Subtypes will require methods to get and set the value of the field; these
* will depend on the type of the value.
*
* The method to set the value should set previousValue to the value
* in effect at the time of the call, set fieldModified to true, set
* levelFromWhichLoaded to the level associated with the field, and
* call valueChanged(). It should also set presentsDefaultValue
* according to whether the value set is the same as the value that
* is set on the "default" level (regardless of whether the value has
* been set from that level).
*/
/*
* Subtypes will also require methods to get the applicable UI Control.
* These methods will be dependent on the type of the field, which will
* determine the type of control (e.g., TextControl, ChangeControl).
*
* The typical pattern for such methods is to check whether the control
* exists and, if not, to create it, adding a ModifyListener and a
* DisposeListener.
*
* If the parent control (a Composite) is needed for obtaining the field's
* control, then it is also advisable to check whether the paretn is disposed
* (as attempts to get controls from a disposed parent typically fail).
*
* A null value can be returned if no real control is found.
*/
/**
* @return The parent control of this field
*/
public Composite getParent() {
return parent;
}
/**
* @return the "holder" of this field, which can be enabled/disabled as a group
* (usually different from the parent)
*/
public abstract Composite getHolder();
/**
* Should call the supertype method fireValueChanged() to inform 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.
*
* This hook is <em>not</em> called when the text is initialized
* (or reset to the default value) from the preference store.
*
* The means of obtaining the current value (for comparison with the
* old) will probably depend on the type of the field and its control.
*
* This method should probably not adjust any of the fields associated
* with the field editor, as those should have been set appropriately
* at the point where valueChanged() was called.
*/
abstract protected boolean valueChanged();
/*
* FieldEditor contains a method refreshValidState() that has an
* empty implementation. That method can be overridden in subtypes
* to update the validity status of a value.
*/
/**
* A utility method to trigger the reevaluation of the
* validity state of the preferences tab.
* (Started out doing more; may not be as useful now as
* it was once expected to be.)
* Need to promote notification of preference page
* from field to tab.
*/
protected boolean notifyState(boolean state)
{
// if (prefPage != null)
// prefPage.setValid(state);
if (prefTab != null)
prefTab.setValid(state);
return state;
}
/*
* Methods related to marking modified fields
*/
public void setModifiedMarkOnLabel() {
// SMS 27 Nov 2006: needed here? should be up to caller
// if (isInherited) return;
Label label = getLabelControl(parent);
if (label != null) {
// String labelText = label.getText();
// if (!labelText.startsWith(Markings.MODIFIED_MARK)) {
// labelText = Markings.MODIFIED_MARK + labelText;
// label.setText(labelText);
// }
// replace changed mark by color to eliminate text-box overflow bug
label.setForeground(PreferencesUtilities.colorRed);
}
}
public void clearModifiedMarkOnLabel() {
Label label = getLabelControl(parent);
if (label != null) {
// String labelText = label.getText();
// if (labelText.startsWith(Markings.MODIFIED_MARK))
// labelText = labelText.substring(Markings.MODIFIED_MARK.length());
// label.setText(labelText);
label.setForeground(PreferencesUtilities.colorBlack);
}
}
/*
* Methods related to error messages
*/
protected void clearErrorMessage() {
prefTab.clearErrorMessages(this);
}
protected void setErrorMessage(String msg) {
prefTab.setErrorMessage(this, msg);
}
public boolean hasErrorMessage() {
return prefTab.errorMessages.containsKey(this);
}
public String getFieldMessagePrefix() {
return /*prefTab.getTabItem().getText() + " Tab: " +*/ getLabelText() + ": ";
}
protected abstract void doSetToolTip();
public void setToolTipText(String toolTipText) {
this.toolTipText= toolTipText;
doSetToolTip();
}
public String getToolTipText() {
return toolTipText;
}
}