From ff0b5b7f6b7ea6308232dce252d12faa7df334c9 Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Fri, 24 Jan 2020 20:05:59 +0300 Subject: [PATCH] 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 --- .../editor/CodeFoldingConfigurable.java | 12 +- .../CodeFoldingOptionsTopHitProvider.java | 4 +- .../openapi/options/BeanConfigurable.java | 4 +- .../openapi/options/ConfigurableBuilder.java | 244 ++++++++++++++++++ .../options/ConfigurableBuilderHelper.kt | 31 +++ 5 files changed, 283 insertions(+), 12 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilder.java create mode 100644 platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilderHelper.kt diff --git a/platform/lang-impl/src/com/intellij/application/options/editor/CodeFoldingConfigurable.java b/platform/lang-impl/src/com/intellij/application/options/editor/CodeFoldingConfigurable.java index 9c21a2147608..b209111be431 100644 --- a/platform/lang-impl/src/com/intellij/application/options/editor/CodeFoldingConfigurable.java +++ b/platform/lang-impl/src/com/intellij/application/options/editor/CodeFoldingConfigurable.java @@ -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)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)); }); diff --git a/platform/platform-impl/src/com/intellij/openapi/options/BeanConfigurable.java b/platform/platform-impl/src/com/intellij/openapi/options/BeanConfigurable.java index 755da72d23dc..9a65b9dfe55c 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/BeanConfigurable.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/BeanConfigurable.java @@ -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 implements UnnamedConfigurable, ConfigurableWithOptionDescriptors { private final T myInstance; @@ -293,7 +293,7 @@ public abstract class BeanConfigurable implements UnnamedConfigurable, Config @Override public List getOptionDescriptors(@NotNull String configurableId, @NotNull Function nameConverter) { - List boxes = JBIterable.from(myFields).filter(CheckboxField.class).toList(); + List boxes = JBIterable.from(myFields).filter(CheckboxField.class).toList(); Object instance = getInstance(); return ContainerUtil.map(boxes, box -> new BooleanOptionDescription(nameConverter.apply(box.getTitle()), configurableId) { @Override diff --git a/platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilder.java b/platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilder.java new file mode 100644 index 000000000000..50796aa466e5 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilder.java @@ -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 getValue(); + + void setValue(@NotNull T value); + } + + private static class CallbackAccessor implements PropertyAccessor { + private final Getter myGetter; + private final Setter mySetter; + + private CallbackAccessor(Getter getter, Setter 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 implements PropertyAccessor { + private final KMutableProperty0 myProperty; + + private KPropertyAccessor(KMutableProperty0 property) { + myProperty = property; + } + + @Override + public T getValue() { + return myProperty.get(); + } + + @Override + public void setValue(@NotNull T value) { + myProperty.set(value); + } + } + + abstract static class BeanField { + protected final PropertyAccessor myAccessor; + private C myComponent; + + private BeanField(@NotNull PropertyAccessor 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 { + private final String myTitle; + + private CheckboxField(PropertyAccessor 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> 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 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 void component(@NotNull JComponent component, + @NotNull Getter beanGetter, + @NotNull Setter beanSetter, + @NotNull Getter componentGetter, + @NotNull Setter componentSetter) { + BeanField field = new BeanField(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 getOptionDescriptors(@NotNull String configurableId, + @NotNull Function nameConverter) { + List 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; + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilderHelper.kt b/platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilderHelper.kt new file mode 100644 index 000000000000..588089e5ea1c --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/options/ConfigurableBuilderHelper.kt @@ -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>) { + if (title != null) { + titledRow(title) { + appendFields(fields) + } + } + else { + appendFields(fields) + } + } + + private fun RowBuilder.appendFields(fields: List>) { + for (field in fields) { + row { + component(field.component) + .onApply { field.apply() } + .onIsModified { field.isModified() } + .onReset { field.reset() } + } + } + } + } +} \ No newline at end of file