blob: 63bce7cafd237e104d85eadf80073ceb7d7c14eb [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 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.rj.eclient.graphics;
import java.util.ArrayList;
import java.util.List;
import org.osgi.service.prefs.BackingStoreException;
import org.eclipse.core.databinding.AggregateValidationStatus;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.IValueChangeListener;
import org.eclipse.core.databinding.observable.value.ValueChangeEvent;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FontDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.ecommons.databinding.core.validation.DecimalValidator;
import org.eclipse.statet.ecommons.ui.components.StatusInfo;
import org.eclipse.statet.ecommons.ui.dialogs.DialogUtils;
import org.eclipse.statet.ecommons.ui.util.LayoutUtils;
/**
* Preference page with options to configure R graphic options:
* <ul>
* <li>Default font families ('serif', 'sans', 'mono')</li>
* </ul>
*
* The page is not registered by this plug-in.
*/
public class RGraphicsPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
public static double[] parseDPI(final String prefValue) {
if (prefValue != null) {
final String[] strings= prefValue.split(",");
if (strings.length == 2) {
try {
return new double[] {
Double.parseDouble(strings[0]),
Double.parseDouble(strings[1]),
};
}
catch (final Exception e) {}
}
}
return null;
}
private static class FontPref {
final String prefKey;
String defaultName;
String currentName;
Font currentFont;
Label valueLabel;
public FontPref(final String key) {
this.prefKey= key;
}
@Override
public int hashCode() {
return this.prefKey.hashCode();
}
@Override
public boolean equals(final @Nullable Object obj) {
return (this == obj
|| (obj instanceof FontPref
&& this.prefKey.equals(((FontPref) obj).prefKey) ));
}
}
private static class Encoding {
final String label;
final String prefValue;
public Encoding(final String label, final String prefValue) {
this.label= label;
this.prefValue= prefValue;
}
@Override
public int hashCode() {
return this.prefValue.hashCode();
}
@Override
public boolean equals(final @Nullable Object obj) {
return (this == obj
|| (obj instanceof Encoding
&& this.prefValue.equals(((Encoding) obj).prefValue) ));
}
@Override
public String toString() {
return this.label.toString();
}
}
private static final Encoding[] SYMBOL_ENCODINGS= new Encoding[] {
new Encoding("Unicode", "Unicode"),
new Encoding("Adobe Symbol", "AdobeSymbol"),
};
private static final Encoding SYMBOL_ENCODING_DEFAULT= SYMBOL_ENCODINGS[1];
private DataBindingContext dbc;
private FontPref serifPref;
private FontPref sansPref;
private FontPref monoPref;
private FontPref symbolFontPref;
private FontPref[] fontPrefs;
private Button symbolUseControl;
private ComboViewer symbolEncodingControl;
private Control[] symbolChildControls;
private final int size= 10;
private Button customDpiControl;
private Composite customDpiComposite;
private Text customHDpiControl;
private Text customVDpiControl;
private boolean customEnabled;
private IObservableValue<Double> customHDpiVisibleValue;
private IObservableValue<Double> customVDpiVisibleValue;
private double customHDpiUserValue;
private double customVDpiUserValue;
/**
* Created via extension point
*/
public RGraphicsPreferencePage() {
}
@Override
public void init(final IWorkbench workbench) {
this.serifPref= new FontPref(RGraphics.PREF_FONTS_SERIF_FONTNAME_KEY);
this.sansPref= new FontPref(RGraphics.PREF_FONTS_SANS_FONTNAME_KEY);
this.monoPref= new FontPref(RGraphics.PREF_FONTS_MONO_FONTNAME_KEY);
this.symbolFontPref= new FontPref(RGraphics.PREF_FONTS_SYMBOL_FONTNAME_KEY);
this.fontPrefs= new FontPref[] { this.serifPref, this.sansPref, this.monoPref, this.symbolFontPref };
final IEclipsePreferences node= DefaultScope.INSTANCE.getNode(RGraphics.FONTS_PREF_QUALIFIER);
for (final FontPref pref : this.fontPrefs) {
pref.defaultName= (node != null) ? node.get(pref.prefKey, "") : "";
}
}
@Override
protected Control createContents(final Composite parent) {
final Composite pageComposite= new Composite(parent, SWT.NONE);
pageComposite.setLayout(LayoutUtils.newCompositeGrid(1));
final Group displayGroup= createDisplayGroup(pageComposite);
displayGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
final Group fontGroup= createFontGroup(pageComposite);
fontGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
initBindings();
loadDisplayOptions();
loadFontOptions();
applyDialogFont(pageComposite);
return pageComposite;
}
protected Group createDisplayGroup(final Composite parent) {
final Group group= new Group(parent, SWT.NONE);
group.setText("Display:");
group.setLayout(LayoutUtils.newGroupGrid(2));
this.customDpiControl= new Button(group, SWT.CHECK);
this.customDpiControl.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
this.customDpiControl.setText("Use custom DPI (instead of system setting):");
this.customDpiComposite= new Composite(group, SWT.NONE);
{ final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, false);
gd.horizontalIndent= LayoutUtils.defaultIndent();
this.customDpiComposite.setLayoutData(gd);
this.customDpiComposite.setLayout(LayoutUtils.newCompositeGrid(2));
}
{ final Label label= new Label(this.customDpiComposite, SWT.LEFT);
label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
label.setText("&Horizontal (x):");
}
{ final Text text= new Text(this.customDpiComposite, SWT.BORDER | SWT.RIGHT);
final GridData gd= new GridData(SWT.LEFT, SWT.CENTER, false, false);
gd.widthHint= LayoutUtils.hintWidth(text, 8);
text.setLayoutData(gd);
this.customHDpiControl= text;
}
{ final Label label= new Label(this.customDpiComposite, SWT.LEFT);
label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
label.setText("&Vertical (y):");
}
{ final Text text= new Text(this.customDpiComposite, SWT.BORDER | SWT.RIGHT);
final GridData gd= new GridData(SWT.LEFT, SWT.CENTER, false, false);
gd.widthHint= LayoutUtils.hintWidth(text, 8);
text.setLayoutData(gd);
this.customVDpiControl= text;
}
this.customDpiControl.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
updateDisplayGroup();
}
});
return group;
}
private void updateDisplayGroup() {
this.customEnabled= this.customDpiControl.getSelection();
DialogUtils.setEnabled(this.customDpiComposite, null, this.customEnabled);
final double hvalue;
final double vvalue;
if (!this.customEnabled || this.customHDpiUserValue <= 0 || this.customVDpiUserValue <= 0) {
final Point dpi= Display.getDefault().getDPI();
hvalue= dpi.x;
vvalue= dpi.y;
if (this.customEnabled) {
this.customHDpiUserValue= hvalue;
this.customVDpiUserValue= vvalue;
}
}
else {
hvalue= this.customHDpiUserValue;
vvalue= this.customVDpiUserValue;
}
this.customHDpiVisibleValue.setValue(hvalue);
this.customVDpiVisibleValue.setValue(vvalue);
}
protected Group createFontGroup(final Composite parent) {
final Group group= new Group(parent, SWT.NONE);
group.setText("Fonts:");
group.setLayout(LayoutUtils.newGroupGrid(3));
addFont(group, this.serifPref, "Default &Serif Font ('serif'):");
addFont(group, this.sansPref, "Default S&ansserif Font ('sans'):");
addFont(group, this.monoPref, "Default &Monospace Font ('mono'):");
LayoutUtils.addSmallFiller(group, false);
this.symbolUseControl= new Button(group, SWT.CHECK);
this.symbolUseControl.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 3, 1));
this.symbolUseControl.setText("Use special S&ymbol Font");
final List<Control> symbolControls= new ArrayList<>();
addFont(group, this.symbolFontPref, "Symbol &Font:",
LayoutUtils.defaultIndent(), symbolControls );
{ final Label label= new Label(group, SWT.NONE);
final GridData gd= new GridData(SWT.FILL, SWT.CENTER, false, false);
gd.horizontalIndent= LayoutUtils.defaultIndent();
label.setLayoutData(gd);
label.setText("Encoding:");
symbolControls.add(label);
}
{ this.symbolEncodingControl= new ComboViewer(group, SWT.DROP_DOWN | SWT.READ_ONLY);
final GridData gd= new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1);
gd.widthHint= LayoutUtils.hintWidth(this.symbolEncodingControl.getCombo(), 15);
this.symbolEncodingControl.getControl().setLayoutData(gd);
this.symbolEncodingControl.setContentProvider(new ArrayContentProvider());
this.symbolEncodingControl.setInput(SYMBOL_ENCODINGS);
symbolControls.add(this.symbolEncodingControl.getControl());
}
this.symbolChildControls= symbolControls.toArray(new Control[symbolControls.size()]);
this.symbolUseControl.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent e) {
updateSymbolControls();
}
});
return group;
}
private void addFont(final Group group, final FontPref pref, final String text) {
addFont(group, pref, text, 0, null);
}
private void addFont(final Group group, final FontPref pref, final String text,
final int indent, final List<Control> controls) {
final Label label= new Label(group, SWT.NONE);
final GridData gd= new GridData(SWT.FILL, SWT.CENTER, false, false);
gd.horizontalIndent= indent;
label.setLayoutData(gd);
label.setText(text);
pref.valueLabel= new Label(group, SWT.BORDER);
pref.valueLabel.setBackground(label.getDisplay().getSystemColor(SWT.COLOR_WHITE));
pref.valueLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
final Button button= new Button(group, SWT.PUSH);
button.setText("Edit...");
button.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
button.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(final SelectionEvent e) {
final FontDialog dialog= new FontDialog(button.getShell(), SWT.NONE);
dialog.setFontList((pref.currentFont != null) ? pref.currentFont.getFontData() : null);
final FontData result= dialog.open();
if (result != null) {
set(pref, result.getName());
}
}
@Override
public void widgetDefaultSelected(final SelectionEvent e) {
}
});
if (controls != null) {
controls.add(label);
controls.add(pref.valueLabel);
controls.add(button);
}
}
protected void initBindings() {
final Realm realm= Realm.getDefault();
this.dbc= new DataBindingContext(realm);
addBindings(this.dbc, realm);
final AggregateValidationStatus validationStatus= new AggregateValidationStatus(this.dbc,
AggregateValidationStatus.MAX_SEVERITY );
validationStatus.addValueChangeListener(new IValueChangeListener<IStatus>() {
@Override
public void handleValueChange(final ValueChangeEvent<? extends IStatus> event) {
final IStatus currentStatus= event.diff.getNewValue();
updateStatus(currentStatus);
}
});
}
protected void addBindings(final DataBindingContext dbc, final Realm realm) {
this.customHDpiVisibleValue= new WritableValue<>(realm, 96.0, Double.class);
this.customVDpiVisibleValue= new WritableValue<>(realm, 96.0, Double.class);
dbc.bindValue(
WidgetProperties.text(SWT.Modify)
.observe(this.customHDpiControl),
this.customHDpiVisibleValue,
new UpdateValueStrategy<String, Double>()
.setAfterGetValidator(new DecimalValidator(10.0, 10000.0,
"The value for Horizontal (x) DPI is invalid (10-10000)." )),
null );
dbc.bindValue(
WidgetProperties.text(SWT.Modify)
.observe(this.customVDpiControl),
this.customVDpiVisibleValue,
new UpdateValueStrategy<String, Double>()
.setAfterGetValidator(new DecimalValidator(10.0, 10000.0,
"The value for Vertical (x) DPI is invalid (10-10000)." )),
null );
this.customHDpiVisibleValue.addValueChangeListener(new IValueChangeListener<Double>() {
@Override
public void handleValueChange(final ValueChangeEvent<? extends Double> event) {
if (RGraphicsPreferencePage.this.customEnabled) {
RGraphicsPreferencePage.this.customHDpiUserValue= RGraphicsPreferencePage.this.customHDpiVisibleValue.getValue();
}
}
});
this.customVDpiVisibleValue.addValueChangeListener(new IValueChangeListener<Double>() {
@Override
public void handleValueChange(final ValueChangeEvent<? extends Double> event) {
if (RGraphicsPreferencePage.this.customEnabled) {
RGraphicsPreferencePage.this.customVDpiUserValue= RGraphicsPreferencePage.this.customVDpiVisibleValue.getValue();
}
}
});
}
protected void updateStatus(final IStatus status) {
setValid(!status.matches(IStatus.ERROR));
StatusInfo.applyToStatusLine(this, status);
}
protected void setCustomDpi(final String prefValue) {
final double[] dpi= parseDPI(prefValue);
if (dpi != null) {
this.customHDpiUserValue= dpi[0];
this.customVDpiUserValue= dpi[1];
this.customDpiControl.setSelection(true);
}
else {
this.customDpiControl.setSelection(false);
}
updateDisplayGroup();
}
protected void updateSymbolControls() {
DialogUtils.setEnabled(this.symbolChildControls, null, this.symbolUseControl.getSelection());
}
protected void set(final FontPref pref, final String value) {
if (value.equals(pref.currentName)) {
return;
}
pref.valueLabel.setText("");
Font font;
try {
font= new Font(pref.valueLabel.getDisplay(), value, this.size, SWT.NONE);
if (pref != this.symbolFontPref) {
pref.valueLabel.setFont(font);
}
pref.valueLabel.setText(value);
}
catch (final SWTError e) {
font= JFaceResources.getDialogFont();
pref.valueLabel.setFont(font);
pref.valueLabel.setText(value + " (not available)");
}
if (pref.currentFont != null && !pref.currentFont.isDisposed()) {
pref.currentFont.dispose();
}
pref.currentName= value;
pref.currentFont= font;
}
@Override
protected void performDefaults() {
setCustomDpi(null);
this.symbolUseControl.setSelection(true);
this.symbolEncodingControl.setSelection(new StructuredSelection(SYMBOL_ENCODING_DEFAULT));
for (final FontPref pref : this.fontPrefs) {
set(pref, pref.defaultName);
}
updateSymbolControls();
super.performDefaults();
}
@Override
protected void performApply() {
saveDisplayOptions(true);
saveFontOptions(true);
}
@Override
public boolean performOk() {
saveDisplayOptions(false);
saveFontOptions(false);
return true;
}
protected IScopeContext getScope() {
return InstanceScope.INSTANCE;
}
protected void loadDisplayOptions() {
final IEclipsePreferences node= getScope().getNode(RGraphics.PREF_DISPLAY_QUALIFIER);
setCustomDpi(node.get(RGraphics.PREF_DISPLAY_CUSTOM_DPI_KEY, null));
}
protected void saveDisplayOptions(final boolean flush) {
final IEclipsePreferences node= getScope().getNode(RGraphics.PREF_DISPLAY_QUALIFIER);
if (this.customEnabled) {
node.put(RGraphics.PREF_DISPLAY_CUSTOM_DPI_KEY,
Double.toString(this.customHDpiUserValue) + "," + Double.toString(this.customVDpiUserValue));
}
else {
node.remove(RGraphics.PREF_DISPLAY_CUSTOM_DPI_KEY);
}
if (flush) {
try {
node.flush();
}
catch (final BackingStoreException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, RGraphics.BUNDLE_ID, -1,
"An error occurred when storing R graphics display preferences.", e));
}
}
}
protected void loadFontOptions() {
if (this.fontPrefs != null) {
final IEclipsePreferences node= getScope().getNode(RGraphics.FONTS_PREF_QUALIFIER);
this.symbolUseControl.setSelection(node.getBoolean(RGraphics.PREF_FONTS_SYMBOL_USE_KEY, true));
final String s= node.get(RGraphics.PREF_FONTS_SYMBOL_ENCODING_KEY, (String) null);
this.symbolEncodingControl.setSelection(new StructuredSelection((s != null) ?
new Encoding(null, s) : SYMBOL_ENCODING_DEFAULT ));
for (final FontPref pref : this.fontPrefs) {
final String value= node.get(pref.prefKey, ""); //$NON-NLS-1$
set(pref, (value.length() > 0) ? value : pref.defaultName);
}
updateSymbolControls();
}
}
protected void saveFontOptions(final boolean flush) {
if (this.fontPrefs != null) {
final IEclipsePreferences node= getScope().getNode(RGraphics.FONTS_PREF_QUALIFIER);
node.putBoolean(RGraphics.PREF_FONTS_SYMBOL_USE_KEY, this.symbolUseControl.getSelection());
final IStructuredSelection selection= (IStructuredSelection) this.symbolEncodingControl.getSelection();
if (selection.getFirstElement() instanceof Encoding
&& !SYMBOL_ENCODING_DEFAULT.equals(selection.getFirstElement())) {
node.put(RGraphics.PREF_FONTS_SYMBOL_ENCODING_KEY,
((Encoding) selection.getFirstElement()).prefValue );
}
else {
node.remove(RGraphics.PREF_FONTS_SYMBOL_ENCODING_KEY);
}
for (final FontPref pref : this.fontPrefs) {
if (pref.currentName == null || pref.currentName.equals(pref.defaultName)) {
node.remove(pref.prefKey);
}
else {
node.put(pref.prefKey, pref.currentName);
}
}
updateSymbolControls();
if (flush) {
try {
node.flush();
}
catch (final BackingStoreException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, RGraphics.BUNDLE_ID, -1,
"An error occurred when storing R graphics font preferences.", e));
}
}
}
}
@Override
public void dispose() {
if (this.dbc != null) {
this.dbc.dispose();
this.dbc= null;
}
if (this.fontPrefs != null) {
for (final FontPref pref : this.fontPrefs) {
if (pref.currentFont != null && !pref.currentFont.isDisposed()) {
pref.currentFont.dispose();
pref.currentFont= null;
}
}
}
super.dispose();
}
}