SdkListModelBuilder - refactor actions processing, avoid side-effects

GitOrigin-RevId: 75dccb4e9d39fb78657d57f1f1d6f5a2cd8e948d
This commit is contained in:
Eugene Petrenko
2019-12-18 22:37:16 +00:00
committed by intellij-monorepo-bot
parent 98b1e28cc4
commit eb51bdbcea
8 changed files with 151 additions and 126 deletions
@@ -108,12 +108,6 @@ public class JdkComboBox extends SdkComboBoxBase<JdkComboBoxItem> {
reloadModel();
}
@Override
protected void onNewSdkAdded(@NotNull Sdk sdk) {
setSelectedJdk(sdk);
myOnNewSdkAdded.consume(sdk);
}
@Override
protected void onModelUpdated(@NotNull SdkListModel model) {
Object previousSelection = getSelectedItem();
@@ -260,7 +254,7 @@ public class JdkComboBox extends SdkComboBoxBase<JdkComboBoxItem> {
}
private void resolveSuggestionsIfNeeded() {
myModel.reloadActions(this, getSelectedJdk());
myModel.reloadActions();
DialogWrapper dialogWrapper = DialogWrapper.findInstance(this);
if (dialogWrapper == null) {
@@ -293,10 +287,12 @@ public class JdkComboBox extends SdkComboBoxBase<JdkComboBoxItem> {
if (anObject instanceof InnerComboBoxItem) {
SdkListItem item = ((InnerComboBoxItem)anObject).getItem();
if (item instanceof SdkListItem.ActionableItem) {
((SdkListItem.ActionableItem)item).executeAction();
return;
}
if (myModel.executeAction(this, item, newItem -> {
setSelectedItem(newItem);
if (newItem instanceof SdkItem) {
myOnNewSdkAdded.consume(((SdkItem)newItem).getSdk());
}
})) return;
}
if (anObject instanceof SelectableComboBoxItem) {
@@ -2,7 +2,6 @@
package com.intellij.openapi.roots.ui.configuration;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
@@ -22,11 +21,6 @@ public abstract class SdkComboBoxBase<T> extends ComboBox<T> {
public void syncModel(@NotNull SdkListModel model) {
SdkComboBoxBase.this.onModelUpdated(model);
}
@Override
public void onNewSdkAdded(@NotNull Sdk sdk) {
SdkComboBoxBase.this.onNewSdkAdded(sdk);
}
});
UIUtil.putClientProperty(this, ANIMATION_IN_RENDERER_ALLOWED, true);
@@ -38,8 +32,6 @@ public abstract class SdkComboBoxBase<T> extends ComboBox<T> {
protected abstract void onModelUpdated(@NotNull SdkListModel model);
protected abstract void onNewSdkAdded(@NotNull Sdk sdk);
public void setInvalidJdk(String name) {
setSelectedItem(myModel.setInvalidSdk(name));
}
@@ -16,10 +16,6 @@ import java.util.Objects;
public abstract class SdkListItem {
private SdkListItem() {}
public interface ActionableItem {
void executeAction();
}
public static abstract class SdkItem extends SdkListItem {
private final Sdk mySdk;
@@ -93,7 +89,7 @@ public abstract class SdkListItem {
}
}
public static abstract class SuggestedItem extends SdkListItem implements ActionableItem {
public static final class SuggestedItem extends SdkListItem {
private final SdkType mySdkType;
private final String myHomePath;
private final String myVersion;
@@ -118,20 +114,16 @@ public abstract class SdkListItem {
public String getVersion() {
return myVersion;
}
@Override
public abstract void executeAction();
}
enum ActionRole {
DOWNLOAD, ADD
}
public static abstract class ActionItem extends SdkListItem implements ActionableItem {
@Nullable
final GroupItem myGroup;
final ActionRole myRole;
final NewSdkAction myAction;
public static final class ActionItem extends SdkListItem {
@Nullable final GroupItem myGroup;
@NotNull final ActionRole myRole;
@NotNull final NewSdkAction myAction;
ActionItem(@NotNull ActionRole role, @NotNull NewSdkAction action, @Nullable GroupItem group) {
myRole = role;
@@ -141,17 +133,8 @@ public abstract class SdkListItem {
@NotNull
ActionItem withGroup(@NotNull GroupItem group) {
ActionItem that = this;
return new ActionItem(myRole, myAction, group) {
@Override
public void executeAction() {
that.executeAction();
}
};
return new ActionItem(myRole, myAction, group);
}
@Override
public abstract void executeAction();
}
public static final class GroupItem extends SdkListItem {
@@ -3,10 +3,9 @@ package com.intellij.openapi.roots.ui.configuration;
import com.google.common.collect.ImmutableList;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkType;
@@ -30,12 +29,13 @@ import java.util.Map;
import java.util.Objects;
public class SdkListModelBuilder {
private static final Logger LOG = Logger.getInstance(SdkListModelBuilder.class);
@Nullable private final Project myProject;
@NotNull private final ProjectSdksModel mySdkModel;
@NotNull private final Condition<? super Sdk> mySdkFilter;
@NotNull private final Condition<? super SdkTypeId> mySdkTypeFilter;
@NotNull private final Condition<? super SdkTypeId> mySdkTypeCreationFilter;
@NotNull private final Consumer<Sdk> myOnNewSdkAdded;
@NotNull private final EventDispatcher<ModelListener> myModelListener = EventDispatcher.create(ModelListener.class);
@@ -68,9 +68,6 @@ public class SdkListModelBuilder {
mySdkFilter = sdk -> sdk != null
&& mySdkTypeFilter.value(sdk.getSdkType())
&& (sdkFilter == null || sdkFilter.value(sdk));
myOnNewSdkAdded = sdk -> {
if (sdk != null) myModelListener.getMulticaster().onNewSdkAdded(sdk);
};
}
/**
@@ -84,11 +81,6 @@ public class SdkListModelBuilder {
* into a specific model and apply it for the control
*/
default void syncModel(@NotNull SdkListModel model) {}
/**
* A callback executed when a new Sdk was created and added
*/
default void onNewSdkAdded(@NotNull Sdk sdk) {}
}
public void addModelListener(@NotNull ModelListener listener) {
@@ -99,8 +91,11 @@ public class SdkListModelBuilder {
myModelListener.removeListener(listener);
}
private void syncModel() {
myModelListener.getMulticaster().syncModel(buildModel());
@NotNull
private SdkListModel syncModel() {
SdkListModel model = buildModel();
myModelListener.getMulticaster().syncModel(model);
return model;
}
@NotNull
@@ -183,24 +178,70 @@ public class SdkListModelBuilder {
for (Sdk sdk : sortSdks(mySdkModel.getSdks())) {
if (!mySdkFilter.value(sdk)) continue;
newHead.add(new SdkItem(sdk) {
@Override
boolean hasSameSdk(@NotNull Sdk value) {
return Objects.equals(getSdk(), value) || Objects.equals(mySdkModel.findSdk(getSdk()), value);
}
});
newHead.add(newSdkItem(sdk));
}
myHead = newHead.build();
syncModel();
}
public void reloadActions(@NotNull JComponent parent, @Nullable Sdk selectedSdk) {
Map<SdkType, NewSdkAction> downloadActions = mySdkModel.createDownloadActions(parent, selectedSdk, myOnNewSdkAdded, mySdkTypeCreationFilter);
Map<SdkType, NewSdkAction> addActions = mySdkModel.createAddActions(parent, selectedSdk, myOnNewSdkAdded, mySdkTypeCreationFilter);
@NotNull
private SdkItem newSdkItem(@NotNull Sdk sdk) {
return new SdkItem(sdk) {
@Override
boolean hasSameSdk(@NotNull Sdk value) {
return Objects.equals(getSdk(), value) || Objects.equals(mySdkModel.findSdk(getSdk()), value);
}
};
}
myDownloadActions = createActions(parent, ActionRole.DOWNLOAD, downloadActions);
myAddActions = createActions(parent, ActionRole.ADD, addActions);
/**
* Executes an action that is associated with the given {@param item}.
* <br/>
* If there are no actions associated, method returns {@code false},
* the {@param afterExecution} is NOT executed
* <br/>
* If there is action associated, it is scheduled for execution. The
* {@param afterExecution} callback is ONLY if the action execution
* ended up successfully and a new item was added to the model. In that
* case the callback is executed after the model is updated and the
* {@link #syncModel()} is invoked. The implementation may not
* execute the callback or and model update for any internal and
* non-selectable items
*
* @return {@code true} if action was started and the {@param afterExecution}
* callback could happen later, {@code false} otherwise
*/
public boolean executeAction(@NotNull JComponent parent,
@NotNull SdkListItem item,
@NotNull Consumer<? super SdkListItem> afterExecution) {
Consumer<Sdk> onNewSdkAdded = sdk -> {
reloadSdks();
SdkItem sdkItem = newSdkItem(sdk);
afterExecution.consume(sdkItem);
};
if (item instanceof ActionItem) {
NewSdkAction action = ((ActionItem)item).myAction;
action.actionPerformed(null, parent, onNewSdkAdded);
return true;
}
if (item instanceof SuggestedItem) {
SuggestedItem suggestedItem = (SuggestedItem)item;
mySdkModel.addSdk(suggestedItem.getSdkType(), suggestedItem.getHomePath(), onNewSdkAdded);
return true;
}
return false;
}
public void reloadActions() {
Map<SdkType, NewSdkAction> downloadActions = mySdkModel.createDownloadActions(mySdkTypeCreationFilter);
Map<SdkType, NewSdkAction> addActions = mySdkModel.createAddActions(mySdkTypeCreationFilter);
myDownloadActions = createActions(ActionRole.DOWNLOAD, downloadActions);
myAddActions = createActions(ActionRole.ADD, addActions);
syncModel();
}
@@ -218,12 +259,7 @@ public class SdkListModelBuilder {
@Override
public void onSdkDetected(@NotNull SdkType type, @NotNull String version, @NotNull String home) {
SuggestedItem item = new SuggestedItem(type, version, home) {
@Override
public void executeAction() {
mySdkModel.addSdk(getSdkType(), getHomePath(), myOnNewSdkAdded);
}
};
SuggestedItem item = new SuggestedItem(type, version, home);
mySuggestions = ImmutableList.<SuggestedItem>builder()
.addAll(mySuggestions)
@@ -242,24 +278,11 @@ public class SdkListModelBuilder {
}
@NotNull
private static ImmutableList<ActionItem> createActions(@NotNull JComponent parent,
@NotNull ActionRole role,
private static ImmutableList<ActionItem> createActions(@NotNull ActionRole role,
@NotNull Map<SdkType, NewSdkAction> actions) {
ImmutableList.Builder<ActionItem> builder = ImmutableList.builder();
for (NewSdkAction action : actions.values()) {
builder.add(new ActionItem(role, action, null) {
@Override
public void executeAction() {
DataContext dataContext = DataManager.getInstance().getDataContext(parent);
AnActionEvent event = new AnActionEvent(null,
dataContext,
ActionPlaces.UNKNOWN,
new Presentation(""),
ActionManager.getInstance(),
0);
myAction.actionPerformed(event);
}
});
builder.add(new ActionItem(role, action, null));
}
return builder.build();
}
@@ -17,7 +17,6 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.Collections;
import java.util.function.Consumer;
public class SdkPopupFactory {
private final SdkListModelBuilder myModelBuilder;
@@ -35,11 +34,10 @@ public class SdkPopupFactory {
@NotNull
public SdkPopup createPopup(@NotNull SdkPopupListener listener) {
SdkListItemContext context = new SdkListItemContext();
SdkPopupImpl popup = new SdkPopupImpl(context);
SdkPopupImpl popup = new SdkPopupImpl(context, value -> {
if (value instanceof SdkListItem.ActionableItem) {
((SdkListItem.ActionableItem)value).executeAction();
//TODO: handle the outcome of the action execution here
popup.addItemSelectedListener(value -> {
if (myModelBuilder.executeAction(popup.getList(), value, listener::onNewItemAdded)) {
return;
}
listener.onExistingItemSelected(value);
@@ -58,7 +56,7 @@ public class SdkPopupFactory {
popup.addListener(new JBPopupListener() {
@Override
public void beforeShown(@NotNull LightweightWindowEvent event) {
myModelBuilder.reloadActions(popup.getList(), null);
myModelBuilder.reloadActions();
myModelBuilder.detectItems(popup.getList(), popup);
}
@@ -73,8 +71,8 @@ public class SdkPopupFactory {
}
private class SdkPopupImpl extends ComboBoxPopup<SdkListItem> implements SdkPopup {
SdkPopupImpl(SdkListItemContext context, Consumer<SdkListItem> onItemSelected) {
super(context, null, onItemSelected);
SdkPopupImpl(SdkListItemContext context) {
super(context, null);
}
@Override
@@ -1,7 +1,6 @@
// Copyright 2000-2018 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.roots.ui.configuration.projectRoot;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.application.ApplicationManager;
@@ -246,17 +245,17 @@ public class ProjectSdksModel implements SdkModel {
@NotNull final Consumer<? super Sdk> updateTree,
@Nullable Condition<? super SdkTypeId> filter) {
Map<SdkType, NewSdkAction> downloadActions = createDownloadActions(parent, selectedSdk, updateTree, filter);
Map<SdkType, NewSdkAction> defaultAddActions = createAddActions(parent, selectedSdk, updateTree, filter);
Map<SdkType, NewSdkAction> downloadActions = createDownloadActions(filter);
Map<SdkType, NewSdkAction> defaultAddActions = createAddActions(filter);
for (SdkType type : getAddableSdkTypes(filter)) {
AnAction downloadAction = downloadActions.get(type);
NewSdkAction downloadAction = downloadActions.get(type);
if (downloadAction != null) {
group.add(downloadAction);
group.add(downloadAction.setOverrides(selectedSdk, parent, updateTree));
}
AnAction defaultAction = defaultAddActions.get(type);
NewSdkAction defaultAction = defaultAddActions.get(type);
if (defaultAction != null) {
group.add(defaultAction);
group.add(defaultAction.setOverrides(selectedSdk, parent, updateTree));
}
}
}
@@ -264,6 +263,10 @@ public class ProjectSdksModel implements SdkModel {
public static abstract class NewSdkAction extends DumbAwareAction {
private final SdkType mySdkType;
private Sdk mySelectedSdkOverride;
private JComponent myParentOverride;
private Consumer<? super Sdk> myCallbackOverride;
private NewSdkAction(@NotNull SdkType sdkType,
@Nls(capitalization = Nls.Capitalization.Title) @Nullable String text,
@Nullable Icon icon) {
@@ -272,16 +275,37 @@ public class ProjectSdksModel implements SdkModel {
}
@NotNull
public SdkType getSdkType() {
NewSdkAction setOverrides(@Nullable Sdk selectedSdkFallback,
@NotNull JComponent parentFallback,
@NotNull Consumer<? super Sdk> callbackFallback) {
mySelectedSdkOverride = selectedSdkFallback;
myParentOverride = parentFallback;
myCallbackOverride = callbackFallback;
return this;
}
@NotNull
public final SdkType getSdkType() {
return mySdkType;
}
@Override
public final void actionPerformed(@NotNull AnActionEvent e) {
Sdk selectedSdk = mySelectedSdkOverride;
JComponent parent = myParentOverride;
Consumer<? super Sdk> callback = myCallbackOverride;
if (callback == null || parent == null) return;
actionPerformed(selectedSdk, parent, callback);
}
public abstract void actionPerformed(@Nullable Sdk selectedSdk,
@NotNull JComponent parent,
@NotNull Consumer<? super Sdk> callback);
}
@NotNull
public Map<SdkType, NewSdkAction> createDownloadActions(@NotNull final JComponent parent,
@Nullable final Sdk selectedSdk,
@NotNull final Consumer<? super Sdk> updateTree,
@Nullable Condition<? super SdkTypeId> filter) {
public Map<SdkType, NewSdkAction> createDownloadActions(@Nullable Condition<? super SdkTypeId> filter) {
Map<SdkType, NewSdkAction> result = new LinkedHashMap<>();
for (final SdkType type : getAddableSdkTypes(filter)) {
SdkDownload downloadExtension = SdkDownload.EP_NAME.findFirstSafe(it -> it.supportsDownload(type));
@@ -290,8 +314,10 @@ public class ProjectSdksModel implements SdkModel {
String downloadText = ProjectBundle.message("sdk.configure.download.action", type.getPresentableName());
NewSdkAction downloadAction = new NewSdkAction(type, downloadText, downloadExtension.getIconForDownloadAction(type)) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
doDownload(downloadExtension, parent, selectedSdk, type, updateTree);
public void actionPerformed(@Nullable Sdk selectedSdk,
@NotNull JComponent parent,
@NotNull Consumer<? super Sdk> callback) {
doDownload(downloadExtension, parent, selectedSdk, type, callback);
}
};
@@ -301,21 +327,20 @@ public class ProjectSdksModel implements SdkModel {
}
@NotNull
public Map<SdkType, NewSdkAction> createAddActions(@NotNull final JComponent parent,
@Nullable final Sdk selectedSdk,
@NotNull final Consumer<? super Sdk> updateTree,
@Nullable Condition<? super SdkTypeId> filter) {
public Map<SdkType, NewSdkAction> createAddActions(@Nullable Condition<? super SdkTypeId> filter) {
Map<SdkType, NewSdkAction> result = new LinkedHashMap<>();
for (final SdkType type : getAddableSdkTypes(filter)) {
String addOnDiskText = ProjectBundle.message("sdk.configure.add.default.action", type.getPresentableName());
NewSdkAction addAction = new NewSdkAction(type, addOnDiskText, type.getIconForAddAction()) {
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
public void actionPerformed(@Nullable Sdk selectedSdk,
@NotNull JComponent parent,
@NotNull Consumer<? super Sdk> callback) {
if (type.supportsCustomCreateUI()) {
type.showCustomCreateUI(ProjectSdksModel.this, parent, selectedSdk, sdk -> setupSdk(sdk, updateTree));
type.showCustomCreateUI(ProjectSdksModel.this, parent, selectedSdk, sdk -> setupSdk(sdk, callback));
} else {
SdkConfigurationUtil.selectSdkHome(type, home -> addSdk(type, home, updateTree));
SdkConfigurationUtil.selectSdkHome(type, home -> addSdk(type, home, callback));
}
}
};
@@ -3,8 +3,6 @@ package com.intellij.ide.ui.laf.darcula.ui;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupListener;
import com.intellij.openapi.ui.popup.LightweightWindowEvent;
@@ -87,7 +85,7 @@ public class DarculaJBPopupComboPopup<T> implements ComboPopup, ComboBoxPopup.Co
//noinspection unchecked
T selectedItem = (T)myComboBox.getSelectedItem();
myPopup = new ComboBoxPopup<T>(this, selectedItem, value -> myComboBox.setSelectedItem(value)) {
myPopup = new ComboBoxPopup<T>(this, selectedItem) {
@Override
public void cancel(InputEvent e) {
if (e instanceof MouseEvent) {
@@ -100,6 +98,7 @@ public class DarculaJBPopupComboPopup<T> implements ComboPopup, ComboBoxPopup.Co
super.cancel(e);
}
};
myPopup.addItemSelectedListener(value -> myComboBox.setSelectedItem(value));
myPopup.addListener(new JBPopupListener() {
@Override
public void beforeShown(@NotNull LightweightWindowEvent event) {
@@ -10,6 +10,7 @@ import com.intellij.ui.SimpleColoredComponent;
import com.intellij.ui.TitledSeparator;
import com.intellij.ui.components.JBList;
import com.intellij.ui.popup.WizardPopup;
import java.util.function.Consumer;
import com.intellij.util.ui.JBUI;
import org.jetbrains.annotations.NotNull;
@@ -27,9 +28,8 @@ public class ComboBoxPopup<T> extends ListPopupImpl {
private final Context<T> myContext;
public ComboBoxPopup(@NotNull Context<T> context,
@Nullable T selectedItem,
@NotNull Consumer<T> onItemSelected) {
this(context, null, popupStateFromContext(context, onItemSelected, selectedItem), null);
@Nullable T selectedItem) {
this(context, null, popupStateFromContext(context, selectedItem), null);
}
private ComboBoxPopup(@NotNull Context<T> context,
@@ -41,11 +41,15 @@ public class ComboBoxPopup<T> extends ListPopupImpl {
configurePopup();
}
public void addItemSelectedListener(@NotNull Consumer<T> callback) {
//noinspection unchecked
((MyBasePopupState<T>)getStep()).addItemSelectedListener(callback);
}
@NotNull
private static <T> MyBasePopupState<T> popupStateFromContext(@NotNull Context<T> context,
@NotNull Consumer<T> onItemSelected,
@Nullable Object selectedItem) {
MyBasePopupState<T> step = new MyBasePopupState<T>(onItemSelected,
MyBasePopupState<T> step = new MyBasePopupState<T>(t -> {},
() -> context.getModel(),
() -> context.getRenderer()) {
@Override
@@ -173,7 +177,7 @@ public class ComboBoxPopup<T> extends ListPopupImpl {
private static class MyBasePopupState<T> extends BaseListPopupStep<T> {
private final JBList<T> myProxyList = new JBList<>();
private final Consumer<T> myOnItemSelected;
private Consumer<T> myOnItemSelected;
private final Supplier<ListModel<T>> myGetComboboxModel;
private final Supplier<ListCellRenderer<? super T>> myGetRenderer;
@@ -186,6 +190,10 @@ public class ComboBoxPopup<T> extends ListPopupImpl {
myGetRenderer = getRenderer;
}
private void addItemSelectedListener(@NotNull Consumer<T> callback) {
myOnItemSelected = myOnItemSelected.andThen(callback);
}
@Nullable
@Override
@SuppressWarnings("rawtypes")
@@ -195,7 +203,8 @@ public class ComboBoxPopup<T> extends ListPopupImpl {
//noinspection unchecked
ListModel<T> nextModel = ((ComboBoxPopupState<T>)model).onChosen(selectedValue);
if (nextModel != null) {
return new MyBasePopupState<>(myOnItemSelected, () -> nextModel, myGetRenderer);
return new MyBasePopupState<>(t -> myOnItemSelected.accept(t) /* mutable callback! */,
() -> nextModel, myGetRenderer);
}
}