blob: 498b330c9e0692e60bfa4f4f5dd97c049f81c122 [file] [log] [blame]
/*=============================================================================#
# 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;
}
}