/******************************************************************************* * 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.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Group; import org.osgi.service.prefs.BackingStoreException; /** * A field editor for an enumeration type preference. * The choices are presented as a list of radio buttons. */ public class RadioGroupFieldEditor extends FieldEditor { /** * List of radio button entries of the form [label,value]. */ protected String[] values; protected String[] labels; /** * Number of columns into which to arrange the radio buttons. */ protected int numColumns; /** * Indent used for the first column of the radio button matrix. */ protected int indent = HORIZONTAL_GAP; /** * The current value, or <code>null</code> if none. */ protected String value; /** * The box of radio buttons, or <code>null</code> if none * (before creation and after disposal). */ protected Composite radioBox; /** * The radio buttons, or <code>null</code> if none * (before creation and after disposal). */ protected Button[] radioButtons; /** * Whether to use a Group control. */ protected boolean useGroup; /** * Creates a new radio group field editor */ protected RadioGroupFieldEditor() { } // Public constructors all adapted from RadioGroupFieldEditor. /** * Creates a radio group field editor. * This constructor does not use a <code>Group</code> to contain the radio buttons. * It is equivalent to using the following constructor with <code>false</code> * for the <code>useGroup</code> argument. * <p> * Example usage: * <pre> * RadioGroupFieldEditor editor= new RadioGroupFieldEditor( * "GeneralPage.DoubleClick", resName, 1, * new String[][] { * {"Open Browser", "open"}, * {"Expand Tree", "expand"} * }, * parent); * </pre> * </p> * * @param name the name of the preference this field editor works on * @param labelText the label text of the field editor * @param numColumns the number of columns for the radio button presentation * @param labelAndValues list of radio button [label, value] entries; * the value is returned when the radio button is selected * @param parent the parent of the field editor's control */ public RadioGroupFieldEditor( PreferencePage page, PreferencesTab tab, IPreferencesService service, String level, String name, String labelText, int numColumns, String[] values, String[] labels, Composite parent, boolean isEnabled, boolean isRemovable) { this(page, tab, service, level, name, labelText, numColumns, values, labels, parent, false); } /** * Creates a radio group field editor. * <p> * Example usage: * <pre> * RadioGroupFieldEditor editor= new RadioGroupFieldEditor( * "GeneralPage.DoubleClick", resName, 1, * new String[][] { * {"Open Browser", "open"}, * {"Expand Tree", "expand"} * }, * parent, * true); * </pre> * </p> * * @param name the name of the preference this field editor works on * @param labelText the label text of the field editor * @param numColumns the number of columns for the radio button presentation * @param labelAndValues list of radio button [label, value] entries; * the value is returned when the radio button is selected * @param parent the parent of the field editor's control * @param useGroup whether to use a Group control to contain the radio buttons */ public RadioGroupFieldEditor( PreferencePage page, PreferencesTab tab, IPreferencesService service, String level, String name, String labelText, int numColumns, String[] values, String[] labels, Composite parent, boolean useGroup) { super(page, tab, service, level, name, labelText, parent); // Adapted from RadioGroupFieldEditor Assert.isTrue(values.length == labels.length); this.values = values; this.numColumns = numColumns; this.useGroup = useGroup; this.labels= labels; createControl(parent); } /* (non-Javadoc) * Method declared on FieldEditor. */ protected void adjustForNumColumns(int numColumns) { Control control = getLabelControl(); if (control != null) { // should be true if !useGroup, and !useGroup should be true, // so should not occur ((GridData) control.getLayoutData()).horizontalSpan = numColumns; } ((GridData) radioBox.getLayoutData()).horizontalSpan = numColumns; } /* (non-Javadoc) * Method declared on FieldEditor. */ protected void doFillIntoGrid(Composite parent, int numColumns) { if (useGroup) { Control control = getRadioBoxControl(parent); GridData gd = new GridData(GridData.FILL_HORIZONTAL); control.setLayoutData(gd); } else { Control control = getLabelControl(parent); GridData gd = new GridData(); gd.horizontalSpan = numColumns; control.setLayoutData(gd); control = getRadioBoxControl(parent); gd = new GridData(); gd.horizontalSpan = numColumns; gd.horizontalIndent = indent; control.setLayoutData(gd); } } @Override protected void doSetToolTip() { if (toolTipText != null) { getRadioBoxControl().setToolTipText(toolTipText); } } /** * * Override of method from RadioGroupFieldEditor. * * Method declared on FieldEditor. */ protected void doLoad() { //updateValue(getPreferenceStore().getString(getPreferenceName())); //if (getTextControl() != null) { if (radioButtons != 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); setPreviousStringValue(value); updateValue(value); } } /** * Method declared on FieldEditor. * * SMS 23 Dec 2006: This method is probably inapropriate and should probably * be removed. The effect as programmed is to update the field without going * through inheritance and without updating the model, and what that might mean * hasn't been well defined. Also, there hasn't been a demonstrated need for * this method. */ protected void doLoadDefault() { //updateValue(getPreferenceStore().getDefaultString(getPreferenceName())); if (radioButtons != null) { String value = preferencesService.getStringPreference(IPreferencesService.DEFAULT_LEVEL, getPreferenceName()); updateValue(value); // calls valueChanged() } } /** * Override of method from RadioGroupFieldEditor. * * SMS 23 Dec 2006: Should probably remove this method for reasons similar * to those given for loadDefault * * Method declared on FieldEditor. */ protected void doLoadLevel(String level) { if (radioButtons != null) { String value = null; if (preferencesLevel != null) { value = preferencesService.getStringPreference(level, getPreferenceName()); } else { // TODO: Check whether this is right value = preferencesService.getStringPreference(getPreferenceName()); } updateValue(value); // calls valueChanged() } } /** * Load into the button field the value for this preference that is either * the value defined on this preferences level, if any, or the value inherited * from the next applicable level, if any. Return the level at which the * value loaded was found. Load nothing and return null if no value is 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 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; // Search up levels starting from the level of this field for (int level = fieldLevelIndex; level < levels.length; level++) { value = preferencesService.getStringPreference(levels[level], getPreferenceName()); if (value == null) continue; levelAtWhichFound = level; levelLoaded = levels[levelAtWhichFound]; break; } // We loaded it at this level or inherited it from some other level // (need to set before calling updateValue(..)) setInherited(fieldLevelIndex != levelAtWhichFound); // Set the field to the value we found updateValue(value); // Since we just loaded some new value, it won't be modified yet fieldModified = false; setPreviousStringValue(value); // Set the background color of the field according to where found setFieldColors(); //System.out.println("doLoadWithInheritance: preferencesName = " + getPreferenceName() + "; preferenceLevel = " + preferencesLevel + "; levelLoaded = " + levelLoaded); return levelLoaded; } protected void setFieldColors() { Control buttonBox = getRadioBoxControl(parent); Color color = isInherited() ? PreferencesUtilities.colorBluish : PreferencesUtilities.colorWhite; buttonBox.setBackground(PreferencesUtilities.colorWhite); if (radioButtons != null) { for (int i = 0; i < radioButtons.length; i++) { ((Button) radioButtons[i]).setBackground(color); } } } /** * Overrides method implemented in RadioGroupFieldEditor. * * Abstract method declared on FieldEditor. */ protected void doStore() { String value = getStringValue(); boolean isEmpty = value.equals(""); // Want empty value, but can't call method to retrieve it // with fields where empty is not allowed // isEmpty shouldn't really occur with a radio group field, // but address that case just in case if (isEmpty) { // 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; } if (isInherited() && !fieldModified) { // If inherited, why do we care whether it's modified? // We have a value but it's inherited // shouldn't want to store in any case, should we? // (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 // and levelFromWhichLoaded is equivalent to levelOnWhichStored // (which also means that the field isn't inherited) fieldModified = false; levelFromWhichLoaded = preferencesLevel; setInherited(false); setPresentsDefaultValue( value.equals(preferencesService.getStringPreference(IPreferencesService.DEFAULT_LEVEL, getPreferenceName()))); // If we've stored the field then it's not inherited, so be sure it's // color indicates that. setFieldColors(); IEclipsePreferences node = preferencesService.getNodeForLevel(preferencesLevel); try { if (node != null) node.flush(); } catch (BackingStoreException e) { System.err.println("RadioGroupFieldEditor.doStore(): BackingStoreException flushing node; node may not have been flushed:" + "\n\tnode path = " + node.absolutePath() + ", preferences level = " + preferencesLevel); } } /* * For radio group fields we override the following two methods because * the means of accessing the text to be modified is different. * * The representation of RadioGroups used here uses a Group to hold the buttons. * The text associated with this group is used to label the field, so it is this * text on which "modified" marks are set and cleared. The implementations of * the methods that set and clear the marks assume the use of a group to hold * the buttons. If groups are not used for this purpose, then the methods to * set and clear the marks will need to be adapted accordingly. * * @see org.eclipse.imp.preferences.fields.FieldEditor#setModifiedMarkOnLabel() * @see org.eclipse.imp.preferences.fields.FieldEditor#clearModifiedMarkOnLabel() */ public void setModifiedMarkOnLabel() { if (radioBox != null && useGroup) { Group radioGroup = (Group) radioBox; radioGroup.setForeground(PreferencesUtilities.colorRed); } } public void clearModifiedMarkOnLabel() { if (radioBox != null && useGroup) { Group radioGroup = (Group) radioBox; radioGroup.setForeground(PreferencesUtilities.colorBlack); } } /** * Returns this field editor's radio group control, using the * locally cached reference to parent to obtain it. * * @return the radio group control */ public Composite getRadioBoxControl() { Composite radioBox = getRadioBoxControl(parent); return radioBox; } /** * Returns this field editor's radio group control. * @param parent The parent to create the radioBox in * @return the radio group control */ public Composite getRadioBoxControl(Composite parent) { if (radioBox == null) { Font font = parent.getFont(); if (useGroup) { Group group = new Group(parent, SWT.NONE); group.setFont(font); String text = getLabelText(); // This puts the label in the border around the button group // (but is not responsible for the field label outside the box) if (text != null) group.setText(text); radioBox = group; GridLayout layout = new GridLayout(); layout.horizontalSpacing = HORIZONTAL_GAP; layout.numColumns = numColumns; radioBox.setLayout(layout); } else { radioBox = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(); layout.marginWidth = 0; layout.marginHeight = 0; layout.horizontalSpacing = HORIZONTAL_GAP; layout.numColumns = numColumns; radioBox.setLayout(layout); radioBox.setFont(font); } radioButtons = new Button[values.length]; for (int i = 0; i < values.length; i++) { Button radio = new Button(radioBox, SWT.RADIO | SWT.LEFT); radioButtons[i] = radio; radio.setText(labels[i]); radio.setData(values[i]); radio.setFont(font); radio.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent event) { // String oldValue = value; value = (String) event.widget.getData(); setPresentsDefaultValue(false); // SMS 12 Dec 2006 setInherited(false); // boolean valueChanged = valueChanged(true); //fireValueChanged(VALUE, oldValue, value); // Added: //fieldModified = true; } }); } radioBox.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent event) { radioBox = null; radioButtons = null; } }); } else { checkParent(radioBox, parent); } return radioBox; } @Override public Composite getHolder() { return getRadioBoxControl().getParent(); } /** * 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; updateValue(newValue); } /** * Select the radio button that conforms to the given value. * * Note: This is (or should be) called whenever the value is * updated through the API (including but not limited to the * loading of the field from the preferences model). It provides * a common place to address certain concerns for all of those * cases (of which there are several). However, the field value * can also be changed through the GUI, in which case these * concerns may need to be addressed somewhere appropriate to * that case. (The concerns include, for example, setting the * modified mark on the field label and signaling that the * value has changed.) * * Overrides the method from RadioGroupFieldEditor. * @param selectedValue the selected value */ protected void updateValue(String selectedValue) { value = selectedValue; if (radioButtons == null) return; setPreviousStringValue(getStringValue()); // Check each button for a match to selectedValue // and set its selection accordingly. Also note // whether a button matching selectedValue was found. boolean found = false; if (value != null) { for (int i = 0; i < radioButtons.length; i++) { Button radio = radioButtons[i]; boolean selection = false; if (((String) radio.getData()).equals(value)) { selection = true; found = true; } // Whether selection is true or false radio.setSelection(selection); } } // If we weren't able to find the value, select the first // radio button as a default if (!found && radioButtons.length > 0) { radioButtons[0].setSelection(true); value = (String) radioButtons[0].getData(); found = true; // in effect } // Should *always* have "found" some selection at this point if (found) { //fieldModified = true; //setModifiedMarkOnLabel(); valueChanged(); } return; } /** * 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. Not concerned with validity of values, * here, on the assumption that if no valid value was entered then * some valid value will be selected by default. * * 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. Sets certain attributes of * the field that do not depend on the context of the call. Does * not set other attributes that do depend on the context (so those * must be set separately). */ protected boolean valueChanged() { return valueChanged(false); } protected boolean valueChanged(boolean assumeChanged) { // Check for change in value boolean valueChanged = assumeChanged || inheritanceChanged(); String newValue = getStringValue(); //getText(); if (!valueChanged) { //String newValue = getStringValue(); //getText(); if (!newValue.equals(getPreviousStringValue())) { valueChanged = true; // Inform the listener //fireValueChanged(VALUE, getPreviousStringValue(), newValue); } } if (valueChanged) { // Update the following attributes of the field, // which can be done without regard to context // Inform the listener fireValueChanged(VALUE, getPreviousStringValue(), newValue); fieldModified = true; // SMS 23 Dec 2006: Don't do these here because they may // (or should) have been done before valueChanged was called. setPreviousStringValue(newValue); setModifiedMarkOnLabel(); } return valueChanged; } public Button[] getRadioButtons() { return radioButtons; } protected String getPreviousStringValue() { return (String) previousValue; } protected void setPreviousStringValue(String value) { previousValue = value; } /** * Gets the current String value of the field, corresponding * to the currently pressed button. * * Note: This method, and the value of the current selection, * are not available from RadioGroupFieldEditor. * * @return The current String value of the field, corresponding * to the currently pressed button. */ public String getStringValue() { return value; } /* * Method declared on FieldEditor. * Copied from RadioGroupFieldEditor. */ public int getNumberOfControls() { return 1; } /* * @see FieldEditor.setEnabled(boolean,Composite). */ public void setEnabled(boolean enabled, Composite parent) { if (!useGroup) super.setEnabled(enabled, parent); for (int i = 0; i < radioButtons.length; i++) { radioButtons[i].setEnabled(enabled); } } }