| /*=============================================================================# |
| # Copyright (c) 2005, 2021 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ecommons.preferences.ui; |
| |
| import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import org.osgi.service.prefs.BackingStoreException; |
| |
| import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory; |
| import org.eclipse.core.databinding.observable.value.AbstractObservableValue; |
| import org.eclipse.core.databinding.observable.value.IObservableValue; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.ProjectScope; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.core.runtime.preferences.DefaultScope; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; |
| import org.eclipse.core.runtime.preferences.IScopeContext; |
| import org.eclipse.core.runtime.preferences.InstanceScope; |
| import org.eclipse.jface.dialogs.IDialogConstants; |
| import org.eclipse.jface.dialogs.MessageDialog; |
| import org.eclipse.jface.preference.IPreferenceStore; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer; |
| import org.eclipse.ui.preferences.IWorkingCopyManager; |
| |
| import org.eclipse.statet.jcommons.collections.ImCollections; |
| import org.eclipse.statet.jcommons.collections.ImList; |
| import org.eclipse.statet.jcommons.collections.ImSet; |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| |
| import org.eclipse.statet.ecommons.databinding.jface.DataBindingSupport; |
| import org.eclipse.statet.ecommons.preferences.SettingsChangeNotifier; |
| import org.eclipse.statet.ecommons.preferences.core.Preference; |
| import org.eclipse.statet.ecommons.preferences.core.PreferenceAccess; |
| import org.eclipse.statet.ecommons.preferences.core.PreferenceSetService; |
| import org.eclipse.statet.ecommons.resources.core.BuildUtils; |
| import org.eclipse.statet.ecommons.runtime.core.StatusChangeListener; |
| |
| |
| /** |
| * Allows load, save, restore of managed preferences, including: |
| * <p><ul> |
| * <li>Connected databinding context:<ul> |
| * <li>use {@link #initBindings()} to create dbc</li> |
| * <li>use {@link #createObservable(Object)} to create observables for model</li> |
| * <li>override {@link #addBindings(DataBindingSupport)}) to register bindings</li> |
| * </ul></li> |
| * <li>optional project scope</li> |
| * <li>change settings groups ({@link SettingsChangeNotifier})</li> |
| * </ul> |
| * Instead of data binding, it is possible to overwrite |
| * {@link #updatePreferences()} and {@link #updateControls()} |
| * to map the preferences to UI. |
| */ |
| @NonNullByDefault |
| public abstract class ManagedConfigurationBlock extends ConfigurationBlock |
| implements PreferenceAccess, IObservableFactory { |
| |
| |
| private static final String REBUILD_QUALIFIER= "org.eclipse.jdt.ui"; //$NON-NLS-1$ |
| private static final String REBUILD_KEY= "preferences_build_requested"; //$NON-NLS-1$ |
| |
| |
| protected class PreferenceManager { |
| |
| private final ImList<IScopeContext> lookupOrder; |
| private final @Nullable IScopeContext inheritScope; |
| private final Map<Preference<?>, @Nullable String> preferences; |
| |
| /** Manager for a working copy of the preferences */ |
| private final IWorkingCopyManager manager; |
| /** Map saving the project settings, if disabled */ |
| private @Nullable Map<Preference<?>, @Nullable Object> disabledProjectSettings; |
| |
| private int rebuildCounter; |
| |
| |
| PreferenceManager(final Map<Preference<?>, @Nullable String> prefs) { |
| this.manager= getContainer().getWorkingCopyManager(); |
| this.preferences= prefs; |
| |
| ManagedConfigurationBlock.this.preferenceManager= this; |
| |
| if (ManagedConfigurationBlock.this.project != null) { |
| this.lookupOrder= ImCollections.newList( |
| new ProjectScope(ManagedConfigurationBlock.this.project), |
| InstanceScope.INSTANCE, |
| DefaultScope.INSTANCE ); |
| this.inheritScope= null; |
| } |
| else { |
| this.lookupOrder= ImCollections.newList( |
| InstanceScope.INSTANCE, |
| DefaultScope.INSTANCE ); |
| this.inheritScope= this.lookupOrder.get(1); |
| } |
| |
| // testIfOptionsComplete(); |
| |
| // init disabled settings, if required |
| if (ManagedConfigurationBlock.this.project == null |
| || hasProjectSpecificSettings(ManagedConfigurationBlock.this.project) ) { |
| this.disabledProjectSettings= null; |
| } |
| else { |
| saveDisabledProjectSettings(); |
| } |
| |
| checkRebuild(); |
| } |
| |
| |
| /* Managing methods ***********************************************************/ |
| |
| /** |
| * Checks, if project specific options exists |
| * |
| * @param project to look up |
| * @return |
| */ |
| boolean hasProjectSpecificSettings(final IProject project) { |
| final IScopeContext projectContext= new ProjectScope(project); |
| for (final Preference<?> pref : this.preferences.keySet()) { |
| if (getInternalValue(pref, projectContext, true) != null) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void setUseProjectSpecificSettings(final boolean enable) { |
| final boolean hasProjectSpecificOption= (this.disabledProjectSettings == null); |
| if (enable != hasProjectSpecificOption) { |
| if (enable) { |
| loadDisabledProjectSettings(); |
| } else { |
| saveDisabledProjectSettings(); |
| } |
| } |
| } |
| |
| private void saveDisabledProjectSettings() { |
| final Map<Preference<?>, @Nullable Object> projectSettings= new IdentityHashMap<>(); |
| for (final Preference<?> pref : this.preferences.keySet()) { |
| projectSettings.put(pref, getValue(pref)); |
| setInternalValue(pref, null); // clear project settings |
| } |
| this.disabledProjectSettings= projectSettings; |
| } |
| |
| private void loadDisabledProjectSettings() { |
| final Map<Preference<?>, @Nullable Object> projectSettings= nonNullAssert(this.disabledProjectSettings); |
| for (final Preference<?> key : this.preferences.keySet()) { |
| // Copy values from saved disabled settings to working store |
| setValue((Preference) key, projectSettings.get(key)); |
| } |
| this.disabledProjectSettings= null; |
| } |
| |
| boolean processChanges(final boolean saveStore) { |
| final List<Preference<?>> changedPrefs= new ArrayList<>(); |
| final boolean needsBuild= getChanges(changedPrefs); |
| if (changedPrefs.isEmpty()) { |
| return true; |
| } |
| |
| boolean doBuild= false; |
| if (needsBuild) { |
| if (checkRebuild()) { |
| final String[] strings= getFullBuildDialogStrings(getProject() == null); |
| if (strings != null) { |
| final MessageDialog dialog= new MessageDialog(getShell(), |
| strings[0], null, strings[1], |
| MessageDialog.QUESTION, new String[] { |
| IDialogConstants.YES_LABEL, |
| IDialogConstants.NO_LABEL, |
| IDialogConstants.CANCEL_LABEL }, |
| 2 ); |
| final int res= dialog.open(); |
| if (res == 0) { |
| doBuild= true; |
| } |
| else if (res != 1) { |
| return false; // cancel pressed |
| } |
| } |
| } |
| } |
| if (saveStore) { |
| try { |
| this.manager.applyChanges(); |
| } |
| catch (final BackingStoreException e) { |
| logSaveError(e); |
| return false; |
| } |
| if (doBuild) { |
| createRebuild().schedule(); |
| } |
| } |
| else { |
| if (doBuild) { |
| getContainer().registerUpdateJob(createRebuild()); |
| } |
| } |
| final Set<String> groupIds= new HashSet<>(); |
| for (final Preference<?> pref : changedPrefs) { |
| final String groupId= this.preferences.get(pref); |
| if (groupId != null) { |
| groupIds.add(groupId); |
| } |
| } |
| scheduleChangeNotification(groupIds, saveStore); |
| return true; |
| } |
| |
| /** |
| * |
| * @param currContext |
| * @param changedSettings |
| * @return true, if rebuild is required. |
| */ |
| private boolean getChanges(final List<Preference<?>> changedSettings) { |
| final IScopeContext currContext= this.lookupOrder.get(0); |
| boolean needsBuild= false; |
| for (final Preference<?> key : this.preferences.keySet()) { |
| final String oldValue= getInternalValue(key, currContext, false); |
| final String value= getInternalValue(key, currContext, true); |
| if (value == null) { |
| if (oldValue != null) { |
| changedSettings.add(key); |
| needsBuild |= !oldValue.equals(getInternalValue(key, true)); |
| } |
| } |
| else if (!value.equals(oldValue)) { |
| changedSettings.add(key); |
| needsBuild |= (oldValue != null || !value.equals(getInternalValue(key, true))); |
| |
| if (this.inheritScope != null |
| && value.equals(getInternalValue(key, this.inheritScope, false) )) { |
| final IEclipsePreferences node= getNode(currContext, key.getQualifier(), true); |
| node.remove(key.getKey()); |
| } |
| } |
| } |
| return needsBuild; |
| } |
| |
| |
| void loadDefaults() { |
| final IScopeContext defaultScope= DefaultScope.INSTANCE; |
| for (final Preference<?> key : this.preferences.keySet()) { |
| final String defValue= getInternalValue(key, defaultScope, false); |
| setInternalValue(key, defValue); |
| } |
| } |
| |
| // DEBUG |
| private void testIfOptionsComplete() { |
| for (final Preference<?> key : this.preferences.keySet()) { |
| if (getInternalValue(key, false) == null) { |
| System.out.println("preference option missing: " + key + " (" + this.getClass().getName() +')'); //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| } |
| } |
| |
| private IEclipsePreferences getNode(final IScopeContext context, final String qualifier, final boolean useWorkingCopy) { |
| final IEclipsePreferences node= context.getNode(qualifier); |
| if (useWorkingCopy && context != DefaultScope.INSTANCE) { |
| return this.manager.getWorkingCopy(node); |
| } |
| return node; |
| } |
| |
| private @Nullable String getInternalValue(final Preference<?> pref, final IScopeContext context, final boolean useWorkingCopy) { |
| final IEclipsePreferences node= getNode(context, pref.getQualifier(), useWorkingCopy); |
| return node.get(pref.getKey(), null); |
| } |
| |
| private @Nullable String getInternalValue(final Preference<?> pref, final boolean ignoreTopScope) { |
| for (int i= ignoreTopScope ? 1 : 0; i < this.lookupOrder.size(); i++) { |
| final String value= getInternalValue(pref, this.lookupOrder.get(i), true); |
| if (value != null) { |
| return value; |
| } |
| } |
| return null; |
| } |
| |
| private <T> void setInternalValue(final Preference<T> pref, final @Nullable String value) { |
| final IEclipsePreferences node= getNode(this.lookupOrder.get(0), pref.getQualifier(), true); |
| if (value != null) { |
| node.put(pref.getKey(), value); |
| } |
| else { |
| node.remove(pref.getKey()); |
| } |
| } |
| |
| |
| private <T> void setValue(final Preference<T> pref, final @Nullable T value) { |
| final IEclipsePreferences node= getNode(this.lookupOrder.get(0), pref.getQualifier(), true); |
| final String storeValue; |
| if (value == null |
| || (storeValue= pref.usage2Store(value)) == null) { |
| node.remove(pref.getKey()); |
| return; |
| } |
| node.put(pref.getKey(), storeValue); |
| } |
| |
| private <T> T getValue(final Preference<T> pref) { |
| IEclipsePreferences node= null; |
| for (int i= 0; i < this.lookupOrder.size(); i++) { |
| final IEclipsePreferences nodeToCheck= getNode(this.lookupOrder.get(i), pref.getQualifier(), true); |
| if (nodeToCheck.get(pref.getKey(), null) != null) { |
| node= nodeToCheck; |
| break; |
| } |
| } |
| |
| final String storeValue= (node != null) ? node.get(pref.getKey(), null) : null; |
| return pref.store2Usage(storeValue); |
| } |
| |
| |
| private boolean checkRebuild() { |
| final int counter= this.manager.getWorkingCopy(DefaultScope.INSTANCE.getNode(REBUILD_QUALIFIER)) |
| .getInt(REBUILD_KEY, 0); |
| if (counter > this.rebuildCounter) { |
| this.rebuildCounter= counter; |
| return false; |
| } |
| return true; |
| } |
| |
| private Job createRebuild() { |
| this.rebuildCounter++; |
| this.manager.getWorkingCopy(DefaultScope.INSTANCE.getNode(REBUILD_QUALIFIER)) |
| .putInt(REBUILD_KEY, this.rebuildCounter); |
| return BuildUtils.getBuildJob(getProject()); |
| } |
| |
| } |
| |
| |
| private final @Nullable IProject project; |
| private @Nullable PreferenceManager preferenceManager; |
| |
| private DataBindingSupport dataBinding; |
| private @Nullable StatusChangeListener statusListener; |
| |
| private Composite pageComposite; |
| |
| |
| protected ManagedConfigurationBlock(final @Nullable IProject project) { |
| this(project, null, null); |
| } |
| |
| protected ManagedConfigurationBlock(final @Nullable IProject project, |
| final @Nullable StatusChangeListener statusListener) { |
| this(project, null, statusListener); |
| } |
| |
| protected ManagedConfigurationBlock(final @Nullable IProject project, |
| final String title, |
| final @Nullable StatusChangeListener statusListener) { |
| super(title); |
| this.project= project; |
| this.statusListener= statusListener; |
| } |
| |
| |
| protected void setStatusListener(final StatusChangeListener listener) { |
| this.statusListener= listener; |
| } |
| |
| |
| public final @Nullable IProject getProject() { |
| return this.project; |
| } |
| |
| @Override |
| public void createContents(final Composite pageComposite, |
| final IWorkbenchPreferenceContainer container, final IPreferenceStore preferenceStore) { |
| this.pageComposite= pageComposite; |
| super.createContents(pageComposite, container, preferenceStore); |
| } |
| |
| /** |
| * initialize preference management |
| * |
| * @param container |
| * @param prefs map with preference objects as key and their settings group id as optional value |
| */ |
| protected void setupPreferenceManager(final Map<Preference<?>, @Nullable String> prefs) { |
| new PreferenceManager(prefs); |
| } |
| |
| protected void initBindings() { |
| this.dataBinding= new DataBindingSupport(this.pageComposite); |
| addBindings(this.dataBinding); |
| |
| this.dataBinding.installStatusListener(this.statusListener); |
| } |
| |
| protected DataBindingSupport getDataBinding() { |
| return this.dataBinding; |
| } |
| |
| protected void addBindings(final DataBindingSupport db) { |
| } |
| |
| /** |
| * Point to hook, before the managed preference values are saved to store. |
| * E.g. you can set some additional (or all) values. |
| */ |
| protected void updatePreferences() { |
| } |
| |
| @Override |
| public boolean performOk(final int flags) { |
| final PreferenceManager preferenceManager= this.preferenceManager; |
| if (preferenceManager != null) { |
| updatePreferences(); |
| return preferenceManager.processChanges((flags & SAVE_STORE) != 0); |
| } |
| return true; |
| } |
| |
| @Override |
| public void performDefaults() { |
| final PreferenceManager preferenceManager= this.preferenceManager; |
| if (preferenceManager != null) { |
| preferenceManager.loadDefaults(); |
| updateControls(); |
| } |
| } |
| |
| |
| /* */ |
| |
| /** |
| * Checks, if project specific options exists |
| * |
| * @param project to look up |
| * @return |
| */ |
| public boolean hasProjectSpecificOptions(final IProject project) { |
| final PreferenceManager preferenceManager= this.preferenceManager; |
| if (project != null && preferenceManager != null) { |
| return preferenceManager.hasProjectSpecificSettings(project); |
| } |
| return false; |
| } |
| |
| @Override |
| public void setUseProjectSpecificSettings(final boolean enable) { |
| super.setUseProjectSpecificSettings(enable); |
| final PreferenceManager preferenceManager= this.preferenceManager; |
| if (this.project != null && preferenceManager != null) { |
| preferenceManager.setUseProjectSpecificSettings(enable); |
| } |
| } |
| |
| protected void updateControls() { |
| if (this.dataBinding != null) { |
| this.dataBinding.getContext().updateTargets(); |
| } |
| } |
| |
| |
| /* Access preference values ***************************************************/ |
| |
| @Override |
| public ImList<IScopeContext> getPreferenceContexts() { |
| final PreferenceManager preferenceManager= nonNullAssert(this.preferenceManager); |
| |
| return preferenceManager.lookupOrder; |
| } |
| |
| /** |
| * Returns the value for the specified preference. |
| * |
| * @param pref preference key |
| * @return value of the preference |
| */ |
| @Override |
| @SuppressWarnings("unchecked") |
| public <T> T getPreferenceValue(final Preference<T> pref) { |
| assert (pref != null); |
| final PreferenceManager preferenceManager= nonNullAssert(this.preferenceManager); |
| |
| final Map<Preference<?>, @Nullable Object> projectSettings= preferenceManager.disabledProjectSettings; |
| if (projectSettings != null) { |
| return (T)projectSettings.get(pref); |
| } |
| return preferenceManager.getValue(pref); |
| } |
| |
| /** |
| * Sets a preference value in the default store. |
| * |
| * @param key preference key |
| * @param value new value |
| * @return old value |
| */ |
| @SuppressWarnings("unchecked") |
| public <T> T setPrefValue(final Preference<T> key, final T value) { |
| final PreferenceManager preferenceManager= nonNullAssert(this.preferenceManager); |
| |
| final Map<Preference<?>, @Nullable Object> projectSettings= preferenceManager.disabledProjectSettings; |
| if (projectSettings != null) { |
| return (T) projectSettings.put(key, value); |
| } |
| final T oldValue= getPreferenceValue(key); |
| preferenceManager.setValue(key, value); |
| return oldValue; |
| } |
| |
| public void setPrefValues(final Map<Preference<?>, Object> map) { |
| for (final Entry<Preference<?>, Object> entry : map.entrySet()) { |
| setPrefValue((Preference)entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| /** |
| * Not (yet) supported |
| */ |
| @Override |
| public void addPreferenceNodeListener(final String nodeQualifier, final IPreferenceChangeListener listener) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Not (yet) supported |
| */ |
| @Override |
| public void removePreferenceNodeListener(final String nodeQualifier, final IPreferenceChangeListener listener) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Not (yet) supported |
| */ |
| @Override |
| public void addPreferenceSetListener(final PreferenceSetService.ChangeListener listener, |
| final ImSet<String> qualifiers) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Not (yet) supported |
| */ |
| @Override |
| public void removePreferenceSetListener(final PreferenceSetService.ChangeListener listener) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| |
| @Override |
| public IObservableValue createObservable(final Object target) { |
| return createObservable((Preference<?>) target); |
| } |
| |
| public <T> IObservableValue<T> createObservable(final Preference<T> pref) { |
| return new AbstractObservableValue<T>() { |
| @Override |
| public Object getValueType() { |
| return pref.getUsageType(); |
| } |
| @Override |
| protected void doSetValue(final T value) { |
| setPrefValue(pref, value); |
| } |
| @Override |
| protected T doGetValue() { |
| return getPreferenceValue(pref); |
| } |
| @Override |
| public synchronized void dispose() { |
| super.dispose(); |
| } |
| }; |
| } |
| |
| /** |
| * Changes requires full build, this method should be overwritten |
| * and return the Strings for the dialog. |
| * |
| * @param workspaceSettings true, if settings for workspace; false, if settings for project. |
| * @return |
| */ |
| protected String @Nullable [] getFullBuildDialogStrings(final boolean workspaceSettings) { |
| return null; |
| } |
| |
| } |