/******************************************************************************* * Copyright (c) 2000, 2017 IBM Corporation 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 * *******************************************************************************/ package org.eclipse.dltk.ui.preferences; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.dltk.internal.corext.util.Messages; import org.eclipse.dltk.internal.ui.dialogs.StatusUtil; import org.eclipse.dltk.internal.ui.preferences.ScrolledPageContent; import org.eclipse.dltk.ui.dialogs.StatusInfo; import org.eclipse.dltk.ui.util.PixelConverter; import org.eclipse.dltk.ui.util.SWTFactory; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontMetrics; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.events.ExpansionAdapter; import org.eclipse.ui.forms.events.ExpansionEvent; import org.eclipse.ui.forms.widgets.ExpandableComposite; /** * Configures preferences. * */ public abstract class AbstractConfigurationBlock implements IPreferenceConfigurationBlock { protected static class FilePathValidator implements IInputValidator { @Override public String isValid(String newText) { IPath path = Path.fromOSString(newText); File file = path.toFile(); String error = null; if ("".equals(newText)) { //$NON-NLS-1$ error = PreferencesMessages.AbstractConfigurationBlock_emptyPath; } else if (!file.exists()) { error = PreferencesMessages.AbstractConfigurationBlock_fileDoesntExist; } else if (!file.isFile()) { error = PreferencesMessages.AbstractConfigurationBlock_pathIsntAFile; } return error; } } /** * Use as follows: * * <pre> * SectionManager manager= new SectionManager(); * Composite composite= manager.createSectionComposite(parent); * * Composite xSection= manager.createSection("section X")); * xSection.setLayout(new FillLayout()); * new Button(xSection, SWT.PUSH); // add controls to section.. * * [...] * * return composite; // return main composite * </pre> */ protected final class SectionManager { /** The preference setting for keeping no section open. */ private static final String __NONE = "__none"; //$NON-NLS-1$ private Set<ExpandableComposite> fSections = new HashSet<>(); private boolean fIsBeingManaged = false; private ExpansionAdapter fListener = new ExpansionAdapter() { @Override public void expansionStateChanged(ExpansionEvent e) { ExpandableComposite source = (ExpandableComposite) e .getSource(); updateSectionStyle(source); if (fIsBeingManaged) return; if (e.getState()) { try { fIsBeingManaged = true; for (Iterator<ExpandableComposite> iter = fSections .iterator(); iter.hasNext();) { ExpandableComposite composite = iter.next(); if (composite != source) composite.setExpanded(false); } } finally { fIsBeingManaged = false; } if (fLastOpenKey != null && fDialogSettingsStore != null) fDialogSettingsStore.setValue(fLastOpenKey, source.getText()); } else { if (!fIsBeingManaged && fLastOpenKey != null && fDialogSettingsStore != null) fDialogSettingsStore.setValue(fLastOpenKey, __NONE); } ExpandableComposite exComp = getParentExpandableComposite( source); if (exComp != null) exComp.layout(true, true); ScrolledPageContent parentScrolledComposite = getParentScrolledComposite( source); if (parentScrolledComposite != null) { parentScrolledComposite.reflow(true); } } }; private Composite fBody; private final String fLastOpenKey; private final IPreferenceStore fDialogSettingsStore; private ExpandableComposite fFirstChild = null; /** * Creates a new section manager. */ public SectionManager() { this(null, null); } /** * Creates a new section manager. */ public SectionManager(IPreferenceStore dialogSettingsStore, String lastOpenKey) { fDialogSettingsStore = dialogSettingsStore; fLastOpenKey = lastOpenKey; } private void manage(ExpandableComposite section) { if (section == null) throw new NullPointerException(); if (fSections.add(section)) section.addExpansionListener(fListener); makeScrollableCompositeAware(section); } /** * Creates a new composite that can contain a set of expandable * sections. A <code>ScrolledPageComposite</code> is created and a new * composite within that, to ensure that expanding the sections will * always have enough space, unless there already is a * <code>ScrolledComposite</code> along the parent chain of * <code>parent</code>, in which case a normal <code>Composite</code> is * created. * <p> * The receiver keeps a reference to the inner body composite, so that * new sections can be added via <code>createSection</code>. * </p> * * @param parent * the parent composite * @return the newly created composite */ public Composite createSectionComposite(Composite parent) { Assert.isTrue(fBody == null); boolean isNested = isNestedInScrolledComposite(parent); Composite composite; if (isNested) { composite = new Composite(parent, SWT.NONE); fBody = composite; } else { composite = new ScrolledPageContent(parent); fBody = ((ScrolledPageContent) composite).getBody(); } fBody.setLayout(new GridLayout()); return composite; } /** * Creates an expandable section within the parent created previously by * calling <code>createSectionComposite</code>. Controls can be added * directly to the returned composite, which has no layout initially. * * @param label * the display name of the section * @return a composite within the expandable section */ public Composite createSection(String label) { Assert.isNotNull(fBody); final ExpandableComposite excomposite = new ExpandableComposite( fBody, SWT.NONE, ExpandableComposite.TWISTIE | ExpandableComposite.CLIENT_INDENT | ExpandableComposite.COMPACT); if (fFirstChild == null) fFirstChild = excomposite; excomposite.setText(label); String last = null; if (fLastOpenKey != null && fDialogSettingsStore != null) last = fDialogSettingsStore.getString(fLastOpenKey); if (fFirstChild == excomposite && !__NONE.equals(last) || label.equals(last)) { excomposite.setExpanded(true); if (fFirstChild != excomposite) fFirstChild.setExpanded(false); } else { excomposite.setExpanded(false); } excomposite.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false)); updateSectionStyle(excomposite); manage(excomposite); Composite contents = new Composite(excomposite, SWT.NONE); excomposite.setClient(contents); return contents; } } protected static final int INDENT = 20; private OverlayPreferenceStore fStore; private Map<Button, String> fCheckBoxes = new HashMap<>(); private ArrayList fRadioButtons = new ArrayList(); private Map fComboBoxes = new HashMap(); private SelectionListener fCheckBoxListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { Button button = (Button) e.widget; fStore.setValue(fCheckBoxes.get(button), button.getSelection()); } }; private SelectionListener fComboBoxListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { Combo combo = (Combo) e.widget; Map data = (Map) fComboBoxes.get(combo); String key = (String) combo.getData(); String value = (String) data.get(combo.getText()); fStore.setValue(key, value); } }; private SelectionListener fRadioButtonListener = new SelectionListener() { @Override public void widgetDefaultSelected(SelectionEvent e) { } @Override public void widgetSelected(SelectionEvent e) { // Button button= (Button) e.widget; for (int i = 0; i < fRadioButtons.size(); i++) { Button button = (Button) fRadioButtons.get(i); if (button.getSelection()) { String[] info = (String[]) button.getData(); fStore.setValue(info[0], Integer.parseInt(info[1])); } } } }; private Map<Text, String> fTextFields = new HashMap<>(); private ModifyListener fTextFieldListener = e -> { Text text = (Text) e.widget; fStore.setValue(fTextFields.get(text), text.getText()); }; private ArrayList fNumberFields = new ArrayList(); private ModifyListener fNumberFieldListener = e -> numberFieldChanged( (Text) e.widget); /** * List of master/slave listeners when there's a dependency. * * @see #createDependency(Button, Control) * */ private ArrayList fMasterSlaveListeners = new ArrayList(); private org.eclipse.dltk.ui.dialogs.StatusInfo fStatus; private final PreferencePage fMainPage; protected Shell getShell() { return fMainPage.getShell(); } public AbstractConfigurationBlock(OverlayPreferenceStore store) { Assert.isNotNull(store); fStore = store; fMainPage = null; } public AbstractConfigurationBlock(OverlayPreferenceStore store, PreferencePage mainPreferencePage) { Assert.isNotNull(store); Assert.isNotNull(mainPreferencePage); fStore = store; fMainPage = mainPreferencePage; } protected final ScrolledPageContent getParentScrolledComposite( Control control) { Control parent = control.getParent(); while (!(parent instanceof ScrolledPageContent) && parent != null) { parent = parent.getParent(); } if (parent instanceof ScrolledPageContent) { return (ScrolledPageContent) parent; } return null; } private final ExpandableComposite getParentExpandableComposite( Control control) { Control parent = control.getParent(); while (!(parent instanceof ExpandableComposite) && parent != null) { parent = parent.getParent(); } if (parent instanceof ExpandableComposite) { return (ExpandableComposite) parent; } return null; } protected void updateSectionStyle(ExpandableComposite excomposite) { excomposite.setFont(JFaceResources.getFontRegistry() .getBold(JFaceResources.DIALOG_FONT)); } private void makeScrollableCompositeAware(Control control) { ScrolledPageContent parentScrolledComposite = getParentScrolledComposite( control); if (parentScrolledComposite != null) { parentScrolledComposite.adaptChild(control); } } private boolean isNestedInScrolledComposite(Composite parent) { return getParentScrolledComposite(parent) != null; } protected Composite createComposite(Composite parent, Font font, int columns, int hspan, int fill, int marginwidth, int marginheight) { return SWTFactory.createComposite(parent, font, columns, hspan, fill, marginwidth, marginheight); } protected Group createGroup(Composite parent, String text, int columns, int hspan, int fill) { return SWTFactory.createGroup(parent, text, columns, hspan, fill); } protected Label createLabel(Composite parent, String text, int hspan) { return SWTFactory.createLabel(parent, text, hspan); } protected Button addCheckBox(Composite parent, String label, String key, int indentation) { Button checkBox = new Button(parent, SWT.CHECK); checkBox.setText(label); GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); gd.horizontalIndent = indentation; gd.horizontalSpan = 2; checkBox.setLayoutData(gd); checkBox.addSelectionListener(fCheckBoxListener); makeScrollableCompositeAware(checkBox); fCheckBoxes.put(checkBox, key); return checkBox; } protected Combo addComboBox(Composite parent, String label, String key, String[] items, String[] values) { if (values == null || items == null || label == null || items.length != values.length) throw new IllegalArgumentException( PreferencesMessages.AbstractConfigurationBlock_valuesItemsAndLabelMustNotBeNull); GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); Label labelControl = new Label(parent, SWT.NONE); labelControl.setText(label); labelControl.setLayoutData(gd); Combo combo = new Combo(parent, SWT.SINGLE | SWT.READ_ONLY); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); combo.setFont(parent.getFont()); combo.setItems(items); combo.setLayoutData(gd); combo.setData(key); combo.addSelectionListener(fComboBoxListener); makeScrollableCompositeAware(combo); Map data = new HashMap(); for (int i = 0; i < items.length; i++) { data.put(items[i], values[i]); } fComboBoxes.put(combo, data); return combo; } protected Button addRadioButton(Composite parent, String label, String key, int value) { GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_FILL); Button button = new Button(parent, SWT.RADIO); button.setText(label); button.setData(new String[] { key, String.valueOf(value) }); button.addSelectionListener(fRadioButtonListener); button.setLayoutData(gd); button.setSelection(value == getPreferenceStore().getInt(key)); fRadioButtons.add(button); return button; } /** * Returns an array of size 2: - first element is of type <code>Label</code> * - second element is of type <code>Text</code> Use * <code>getLabelControl</code> and <code>getTextControl</code> to get the 2 * controls. * * @param composite * the parent composite * @param label * the text field's label * @param key * the preference key * @param textLimit * the text limit * @param indentation * the field's indentation * @param isNumber * <code>true</code> iff this text field is used to edit a number * @return the controls added */ protected Control[] addLabelledTextField(Composite composite, String label, String key, int textLimit, int indentation, boolean isNumber, IInputValidator validator) { PixelConverter pixelConverter = new PixelConverter(composite); Label labelControl = new Label(composite, SWT.NONE); labelControl.setText(label); GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); gd.horizontalIndent = indentation; labelControl.setLayoutData(gd); Text textControl = new Text(composite, SWT.BORDER | SWT.SINGLE); gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); gd.widthHint = pixelConverter .convertWidthInCharsToPixels(textLimit + 2); textControl.setLayoutData(gd); textControl.setTextLimit(textLimit); if (validator != null) textControl.setData(validator); fTextFields.put(textControl, key); if (isNumber) { fNumberFields.add(textControl); textControl.addModifyListener(fNumberFieldListener); } else { textControl.addModifyListener(fTextFieldListener); } return new Control[] { labelControl, textControl }; } protected Control[] addLabelledTextField(Composite composite, String label, String key, int textLimit, int indentation, boolean isNumber) { return addLabelledTextField(composite, label, key, textLimit, indentation, isNumber, null); } protected void createDependency(final Button master, final Control slave) { createDependency(master, new Control[] { slave }); } protected void createDependency(final Button master, final Control[] slaves) { Assert.isTrue(slaves.length > 0); indent(slaves[0]); SelectionListener listener = new SelectionListener() { @Override public void widgetSelected(SelectionEvent e) { boolean state = master.getSelection(); for (int i = 0; i < slaves.length; i++) { slaves[i].setEnabled(state); } } @Override public void widgetDefaultSelected(SelectionEvent e) { } }; master.addSelectionListener(listener); fMasterSlaveListeners.add(listener); } protected static void indent(Control control) { ((GridData) control.getLayoutData()).horizontalIndent += INDENT; } @Override public void initialize() { initializeFields(); } protected void initializeFields() { Iterator iter = fCheckBoxes.keySet().iterator(); while (iter.hasNext()) { Button b = (Button) iter.next(); String key = fCheckBoxes.get(b); b.setSelection(fStore.getBoolean(key)); } for (int i = 0; i < fRadioButtons.size(); i++) { Button button = (Button) fRadioButtons.get(i); String[] info = (String[]) button.getData(); int dflt = fStore.getInt(info[0]); String val = info[1]; button.setSelection(dflt == Integer.parseInt(val)); } Iterator iter2 = fComboBoxes.keySet().iterator(); while (iter2.hasNext()) { Combo b = (Combo) iter2.next(); String value = fStore.getString((String) b.getData()); Map data = (Map) fComboBoxes.get(b); for (Iterator iterator = data.keySet().iterator(); iterator .hasNext();) { String title = (String) iterator.next(); if (data.get(title).equals(value)) { b.setText(title); break; } } } iter = fTextFields.keySet().iterator(); while (iter.hasNext()) { Text t = (Text) iter.next(); String key = fTextFields.get(t); t.setText(fStore.getString(key)); } // Update slaves iter = fMasterSlaveListeners.iterator(); while (iter.hasNext()) { SelectionListener listener = (SelectionListener) iter.next(); listener.widgetSelected(null); } updateStatus(new StatusInfo()); } @Override public void performOk() { } @Override public void performDefaults() { initializeFields(); } IStatus getStatus() { if (fStatus == null) fStatus = new StatusInfo(); return fStatus; } @Override public void dispose() { } private void numberFieldChanged(Text textControl) { String number = textControl.getText(); IInputValidator validator = (IInputValidator) textControl.getData(); if (validator == null) { IStatus status = validatePositiveNumber(number); if (!status.matches(IStatus.ERROR)) fStore.setValue(fTextFields.get(textControl), number); updateStatus(status); } else { StatusInfo status = new StatusInfo(); String res = validator.isValid(number); if (res != null) { status.setError(res); } else fStore.setValue(fTextFields.get(textControl), number); updateStatus(status); } } private IStatus validatePositiveNumber(String number) { StatusInfo status = new StatusInfo(); if (number.length() == 0) { status.setError( PreferencesMessages.DLTKEditorPreferencePage_empty_input); } else { try { int value = Integer.parseInt(number); if (value < 0) status.setError(Messages.format( PreferencesMessages.DLTKEditorPreferencePage_invalid_input, number)); } catch (NumberFormatException e) { status.setError(Messages.format( PreferencesMessages.DLTKEditorPreferencePage_invalid_input, number)); } } return status; } protected void updateStatus(IStatus status) { if (fMainPage == null) return; fMainPage.setValid(status.isOK()); StatusUtil.applyToStatusLine(fMainPage, status); } protected final OverlayPreferenceStore getPreferenceStore() { return fStore; } protected Composite createSubsection(Composite parent, SectionManager manager, String label) { if (manager != null) { return manager.createSection(label); } else { Group group = new Group(parent, SWT.SHADOW_NONE); group.setText(label); GridData data = new GridData(SWT.FILL, SWT.CENTER, true, false); group.setLayoutData(data); return group; } } private FontMetrics fFontMetrics; /** * Initializes the computation of horizontal and vertical dialog units based * on the size of current font. * <p> * This method must be called before any of the dialog unit based conversion * methods are called. * </p> * * @param testControl * a control from which to obtain the current font */ protected void initializeDialogUnits(Control testControl) { // Compute and store a font metric GC gc = new GC(testControl); gc.setFont(JFaceResources.getDialogFont()); fFontMetrics = gc.getFontMetrics(); gc.dispose(); } /** * Returns the number of pixels corresponding to the width of the given * number of characters. * <p> * This method may only be called after <code>initializeDialogUnits</code> * has been called. * </p> * <p> * Clients may call this framework method, but should not override it. * </p> * * @param chars * the number of characters * @return the number of pixels */ protected int convertWidthInCharsToPixels(int chars) { // test for failure to initialize for backward compatibility if (fFontMetrics == null) return 0; return Dialog.convertWidthInCharsToPixels(fFontMetrics, chars); } /** * Returns the number of pixels corresponding to the height of the given * number of characters. * <p> * This method may only be called after <code>initializeDialogUnits</code> * has been called. * </p> * <p> * Clients may call this framework method, but should not override it. * </p> * * @param chars * the number of characters * @return the number of pixels */ protected int convertHeightInCharsToPixels(int chars) { // test for failure to initialize for backward compatibility if (fFontMetrics == null) return 0; return Dialog.convertHeightInCharsToPixels(fFontMetrics, chars); } }