ui: create simpler alternative for BeanConfigurable

Drop support for reflective field access, improve type safety.
Prohibit overriding 'onApply', 'createComponent' and others.
Implement UiDslConfigurable to fix inconsistent gaps between components.

GitOrigin-RevId: 0efd98e0d3fc5fa6bf61a5119b2374ea46c5e957
This commit is contained in:
Aleksey Pivovarov
2020-01-24 20:05:59 +03:00
committed by intellij-monorepo-bot
parent 2e475a418e
commit ff0b5b7f6b
5 changed files with 283 additions and 12 deletions

View File

@@ -10,8 +10,8 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.ex.EditorSettingsExternalizable;
import com.intellij.openapi.extensions.BaseExtensionPointName;
import com.intellij.openapi.options.BeanConfigurable;
import com.intellij.openapi.options.CompositeConfigurable;
import com.intellij.openapi.options.ConfigurableBuilder;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.ex.ConfigurableWrapper;
import com.intellij.openapi.project.Project;
@@ -112,12 +112,8 @@ public class CodeFoldingConfigurable extends CompositeConfigurable<CodeFoldingOp
@NotNull
private static String sortByTitle(@NotNull CodeFoldingOptionsProvider p) {
if (p instanceof BeanConfigurable) {
String title = ((BeanConfigurable)p).getTitle();
if (title != null) {
return ApplicationBundle.message("title.general").equals(title) ? "" : title;
}
}
return "z";
String title = ConfigurableBuilder.getConfigurableTitle(p);
if (ApplicationBundle.message("title.general").equals(title)) return "";
return title != null ? title : "z";
}
}

View File

@@ -4,7 +4,7 @@ package com.intellij.application.options.editor;
import com.intellij.ide.ui.OptionsSearchTopHitProvider;
import com.intellij.ide.ui.search.OptionDescription;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.options.BeanConfigurable;
import com.intellij.openapi.options.ConfigurableBuilder;
import com.intellij.openapi.options.ConfigurableWithOptionDescriptors;
import com.intellij.openapi.options.UnnamedConfigurable;
import com.intellij.openapi.options.ex.ConfigurableWrapper;
@@ -35,7 +35,7 @@ final class CodeFoldingOptionsTopHitProvider implements OptionsSearchTopHitProvi
return;
}
String title = configurable instanceof BeanConfigurable ? ((BeanConfigurable<?>)configurable).getTitle() : null;
String title = ConfigurableBuilder.getConfigurableTitle(configurable);
String prefix = title == null ? byDefault + " " : StringUtil.trimEnd(byDefault, ':') + " in " + title + ": ";
result.addAll(((ConfigurableWithOptionDescriptors)configurable).getOptionDescriptors(CodeFoldingConfigurable.ID, s -> prefix + s));
});

View File

@@ -27,7 +27,7 @@ import java.util.List;
import java.util.function.Function;
/**
* @author yole
* See {@link ConfigurableBuilder} for {@link UiDslConfigurable} alternative.
*/
public abstract class BeanConfigurable<T> implements UnnamedConfigurable, ConfigurableWithOptionDescriptors {
private final T myInstance;
@@ -293,7 +293,7 @@ public abstract class BeanConfigurable<T> implements UnnamedConfigurable, Config
@Override
public List<OptionDescription> getOptionDescriptors(@NotNull String configurableId,
@NotNull Function<? super String, String> nameConverter) {
List<BeanConfigurable.CheckboxField> boxes = JBIterable.from(myFields).filter(CheckboxField.class).toList();
List<CheckboxField> boxes = JBIterable.from(myFields).filter(CheckboxField.class).toList();
Object instance = getInstance();
return ContainerUtil.map(boxes, box -> new BooleanOptionDescription(nameConverter.apply(box.getTitle()), configurableId) {
@Override

View File

@@ -0,0 +1,244 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.options;
import com.intellij.ide.ui.search.BooleanOptionDescription;
import com.intellij.ide.ui.search.OptionDescription;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Getter;
import com.intellij.openapi.util.Setter;
import com.intellij.ui.layout.RowBuilder;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import kotlin.reflect.KMutableProperty0;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
/**
* See also {@link UiDslConfigurable.Simple} for more flexible alternative.
*/
public abstract class ConfigurableBuilder extends UiDslConfigurable.Simple
implements UiDslConfigurable, ConfigurableWithOptionDescriptors {
private String myTitle;
private interface PropertyAccessor<T> {
T getValue();
void setValue(@NotNull T value);
}
private static class CallbackAccessor<T> implements PropertyAccessor<T> {
private final Getter<? extends T> myGetter;
private final Setter<? super T> mySetter;
private CallbackAccessor(Getter<? extends T> getter, Setter<? super T> setter) {
myGetter = getter;
mySetter = setter;
}
@Override
public T getValue() {
return myGetter.get();
}
@Override
public void setValue(@NotNull T value) {
mySetter.set(value);
}
}
private static class KPropertyAccessor<T> implements PropertyAccessor<T> {
private final KMutableProperty0<T> myProperty;
private KPropertyAccessor(KMutableProperty0<T> property) {
myProperty = property;
}
@Override
public T getValue() {
return myProperty.get();
}
@Override
public void setValue(@NotNull T value) {
myProperty.set(value);
}
}
abstract static class BeanField<C extends JComponent, T> {
protected final PropertyAccessor<T> myAccessor;
private C myComponent;
private BeanField(@NotNull PropertyAccessor<T> accessor) {
myAccessor = accessor;
}
@NotNull
C getComponent() {
if (myComponent == null) {
myComponent = createComponent();
}
return myComponent;
}
@NotNull
protected abstract C createComponent();
boolean isModified() {
final Object componentValue = getComponentValue();
final Object beanValue = myAccessor.getValue();
return !Comparing.equal(componentValue, beanValue);
}
void apply() {
myAccessor.setValue(getComponentValue());
}
void reset() {
setComponentValue(myAccessor.getValue());
}
protected abstract T getComponentValue();
protected abstract void setComponentValue(T value);
}
private static class CheckboxField extends BeanField<JCheckBox, @NotNull Boolean> {
private final String myTitle;
private CheckboxField(PropertyAccessor<Boolean> accessor, @NotNull String title) {
super(accessor);
myTitle = title;
}
@NotNull
private String getTitle() {
return myTitle;
}
private void setAccessorValue(boolean value) {
myAccessor.setValue(value);
}
private boolean getAccessorValue() {
return myAccessor.getValue();
}
@NotNull
@Override
protected JCheckBox createComponent() {
return new JCheckBox(myTitle);
}
@Override
protected Boolean getComponentValue() {
return getComponent().isSelected();
}
@Override
protected void setComponentValue(@NotNull Boolean value) {
getComponent().setSelected(value.booleanValue());
}
}
private final List<BeanField<?, ?>> myFields = new ArrayList<>();
protected ConfigurableBuilder() {
}
protected ConfigurableBuilder(@Nullable String title) {
setTitle(title);
}
@Nullable
public String getTitle() {
return myTitle;
}
protected void setTitle(@Nullable String title) {
myTitle = title;
}
/**
* Adds check box with given {@code title}.
* Initial checkbox value is obtained from {@code getter}.
* After the apply, the value from the check box is written back to model via {@code setter}.
*/
protected void checkBox(@NotNull String title, @NotNull Getter<@NotNull Boolean> getter, @NotNull Setter<? super Boolean> setter) {
myFields.add(new CheckboxField(new CallbackAccessor<>(getter, setter), title));
}
protected void checkBox(@NotNull String title, @NotNull KMutableProperty0<@NotNull Boolean> prop) {
myFields.add(new CheckboxField(new KPropertyAccessor<>(prop), title));
}
/**
* Adds custom component (e.g. edit box).
* Initial value is obtained from {@code beanGetter} and applied to the component via {@code componentSetter}.
* E.g. text is read from the model and set to the edit box.
* After the apply, the value from the component is queried via {@code componentGetter} and written back to model via {@code beanSetter}.
* E.g. text from the edit box is queried and saved back to model bean.
*/
protected <V> void component(@NotNull JComponent component,
@NotNull Getter<? extends V> beanGetter,
@NotNull Setter<? super V> beanSetter,
@NotNull Getter<? extends V> componentGetter,
@NotNull Setter<? super V> componentSetter) {
BeanField<JComponent, V> field = new BeanField<JComponent, V>(new CallbackAccessor<>(beanGetter, beanSetter)) {
@NotNull
@Override
protected JComponent createComponent() {
return component;
}
@Override
protected V getComponentValue() {
return componentGetter.get();
}
@Override
protected void setComponentValue(V value) {
componentSetter.set(value);
}
};
myFields.add(field);
}
@NotNull
@Override
public List<OptionDescription> getOptionDescriptors(@NotNull String configurableId,
@NotNull Function<? super String, String> nameConverter) {
List<ConfigurableBuilder.CheckboxField> boxes = JBIterable.from(myFields).filter(CheckboxField.class).toList();
return ContainerUtil.map(boxes, box -> new BooleanOptionDescription(nameConverter.apply(box.getTitle()), configurableId) {
@Override
public boolean isOptionEnabled() {
return box.getAccessorValue();
}
@Override
public void setOptionState(boolean enabled) {
box.setAccessorValue(enabled);
}
});
}
@Override
public void createComponentRow(@NotNull RowBuilder builder) {
ConfigurableBuilderHelper.buildFieldsPanel$intellij_platform_ide_impl(builder, myTitle, myFields);
}
@Nullable
public static String getConfigurableTitle(@NotNull UnnamedConfigurable configurable) {
if (configurable instanceof BeanConfigurable<?>) {
return ((BeanConfigurable<?>)configurable).getTitle();
}
if (configurable instanceof ConfigurableBuilder) {
return ((ConfigurableBuilder)configurable).getTitle();
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.options
import com.intellij.ui.layout.*
class ConfigurableBuilderHelper {
companion object {
@JvmStatic
internal fun RowBuilder.buildFieldsPanel(title: String?, fields: List<ConfigurableBuilder.BeanField<*, *>>) {
if (title != null) {
titledRow(title) {
appendFields(fields)
}
}
else {
appendFields(fields)
}
}
private fun RowBuilder.appendFields(fields: List<ConfigurableBuilder.BeanField<*, *>>) {
for (field in fields) {
row {
component(field.component)
.onApply { field.apply() }
.onIsModified { field.isModified() }
.onReset { field.reset() }
}
}
}
}
}