PY-64402: WIP: Make an "Other" group collapsed.

Such groups are rendered as plain text until a user clicks on them. After that, they are substituted with children.

Since there's been no such API in the platform, I've introduced `com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup` which works along with `ActionGroupPanelWrapper` for project types in "New project wizard".

It is now only used by Python, see `PycharmNewProjectStep`

GitOrigin-RevId: f38a0643ce6bc65e5e2f6485ef255ee20ba2e7a5
This commit is contained in:
Ilya.Kazakevich
2024-03-08 23:25:45 +01:00
committed by intellij-monorepo-bot
parent 32710b000c
commit ee35416f38
7 changed files with 214 additions and 13 deletions

View File

@@ -12,6 +12,9 @@ import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.CollapsedActionGroup;
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.CollapsedButtonKt;
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.ListListenerCollapsedActionGroupExpander;
import com.intellij.ui.*;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.panels.NonOpaquePanel;
@@ -46,6 +49,7 @@ public final class ActionGroupPanelWrapper {
java.util.List<AnAction> groups = flattenActionGroups(action);
DefaultListModel<AnAction> model = JBList.createDefaultListModel(groups);
JBList<AnAction> list = new JBList<>(model);
ListListenerCollapsedActionGroupExpander.expandCollapsableGroupsOnSelection(list, model, parentDisposable);
for (AnAction group : groups) {
if (group instanceof Disposable) {
Disposer.register(parentDisposable, (Disposable)group);
@@ -94,8 +98,31 @@ public final class ActionGroupPanelWrapper {
return getProjectsBackground();
}
@Override
public Component getListCellRendererComponent(JList<? extends AnAction> list,
AnAction value,
int index,
boolean isSelected,
boolean cellHasFocus) {
var component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
// Collapsable group should be rendered as collapsable button
if (value instanceof CollapsedActionGroup actionGroup) {
return CollapsedButtonKt.createCollapsedButton(actionGroup, childAction -> {
// To get an action width we set to the component, render it, and see component width
// This approach obeys component spacing, font's size e.t.c
setLabelByAction(childAction);
return component.getPreferredSize().width;
});
}
return component;
}
@Override
protected void customizeComponent(JList<? extends AnAction> list, AnAction value, boolean isSelected) {
setLabelByAction(value);
}
private void setLabelByAction(@NotNull AnAction value) {
if (myTextLabel != null) {
myTextLabel.setText(value.getTemplateText());
myTextLabel.setIcon(value.getTemplatePresentation().getIcon());
@@ -249,24 +276,41 @@ public final class ActionGroupPanelWrapper {
private static List<AnAction> flattenActionGroups(final @NotNull ActionGroup action) {
final ArrayList<AnAction> groups = new ArrayList<>();
String groupName;
for (AnAction anAction : action.getChildren(null)) {
if (anAction instanceof ActionGroup) {
groupName = anAction.getTemplateText();
for (AnAction childAction : ((ActionGroup)anAction).getChildren(null)) {
if (groupName != null) {
setParentGroupName(groupName, childAction);
}
groups.add(childAction);
if (anAction instanceof ActionGroup actionGroup) {
var elementsToAdd = getActionChildrenToAddInsteadOfAction(actionGroup);
if (elementsToAdd != null) {
// Some GroupActions shouldn't be added directly, but children must be added instead
groups.addAll(Arrays.asList(elementsToAdd));
continue;
}
}
else {
groups.add(anAction);
}
// Collapse groups and regular actions
groups.add(anAction);
}
return groups;
}
/**
* Action children must have parent name to display group separator.
* {@link CollapsedActionGroup} will be substituted with children later, when clicked.
* Regular {@link ActionGroup} must be replaced now
*/
@NotNull
private static AnAction @Nullable [] getActionChildrenToAddInsteadOfAction(@NotNull ActionGroup actionGroup) {
String groupName;
AnAction[] children = actionGroup.getChildren(null);
groupName = actionGroup.getTemplateText();
for (AnAction childAction : children) {
if (groupName != null) {
setParentGroupName(groupName, childAction);
}
}
return actionGroup instanceof CollapsedActionGroup
? null
: children;
}
private static @NlsContexts.Separator String getParentGroupName(final @NotNull AnAction value) {
return (String)value.getTemplatePresentation().getClientProperty(ACTION_GROUP_KEY);
}

View File

@@ -0,0 +1,13 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.DefaultActionGroup
import org.jetbrains.annotations.Nls
/**
* As [com.intellij.openapi.actionSystem.ActionGroup] it might contain children [AnAction], but children
* aren't displayed until user clicks on it.
* this logic is part [com.intellij.openapi.wm.impl.welcomeScreen.ActionGroupPanelWrapper]
*/
class CollapsedActionGroup(name: @Nls String, actions: List<AnAction>) : DefaultActionGroup(name, actions)

View File

@@ -0,0 +1,31 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.util.NlsContexts
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Dimension
import javax.swing.JComponent
/**
* Crates [JComponent] that renders as collapsed button with text from [actionGroup] and width from the longest child action.
*/
@RequiresEdt
fun createCollapsedButton(actionGroup: CollapsedActionGroup, getActionWidth: (childAction: AnAction) -> Int): JComponent {
val button = collapsedButton(actionGroup.templateText)
val maxChildWidth = actionGroup.getChildren(null).maxOfOrNull { getActionWidth(it) }
val preferredSize = button.preferredSize
if (maxChildWidth != null && maxChildWidth > preferredSize.width) {
button.preferredSize = Dimension(maxChildWidth, button.preferredSize.height)
}
return button
}
@RequiresEdt
private fun collapsedButton(@NlsContexts.BorderTitle text: String) = panel {
collapsibleGroup(text) {
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.util.Disposer
import com.intellij.util.concurrency.annotations.RequiresEdt
import javax.swing.DefaultListModel
import javax.swing.JList
import javax.swing.event.ListSelectionEvent
import javax.swing.event.ListSelectionListener
/**
* Listens for [JList] selection events, and when [CollapsedActionGroup] selected -- substitutes it with children
* See [expandCollapsableGroupsOnSelection]
*/
class ListListenerCollapsedActionGroupExpander private constructor(
private val list: JList<AnAction>,
private val model: DefaultListModel<AnAction>) : ListSelectionListener {
companion object {
@JvmStatic
@RequiresEdt
fun expandCollapsableGroupsOnSelection(list: JList<AnAction>, model: DefaultListModel<AnAction>, parentDisposable: Disposable) {
val instance = ListListenerCollapsedActionGroupExpander(list, model)
list.addListSelectionListener(instance)
Disposer.register(parentDisposable, Disposable { list.removeListSelectionListener(instance) })
}
}
override fun valueChanged(e: ListSelectionEvent) {
// Replace collapsable action with children
val selectedIndex = list.selectedIndex
val group = list.selectedValue as? CollapsedActionGroup ?: return
model.remove(selectedIndex)
model.addAll(selectedIndex, group.getChildren(null).asList())
list.removeListSelectionListener(this)
}
}

View File

@@ -0,0 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
/**
* {@link com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.CollapsedActionGroup} is
* used by {@link com.intellij.openapi.wm.impl.welcomeScreen.ActionGroupPanelWrapper}.
* Such groups rendered as items which are substituted by children on click.
*/
package com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup;

View File

@@ -0,0 +1,67 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.ui
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.CollapsedActionGroup
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.ListListenerCollapsedActionGroupExpander
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.createCollapsedButton
import com.intellij.testFramework.junit5.RunInEdt
import com.intellij.testFramework.junit5.TestApplication
import com.intellij.testFramework.junit5.TestDisposable
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.jupiter.api.Test
import javax.swing.DefaultListModel
import javax.swing.JList
@TestApplication
@RunInEdt
class CollapsedActionGroupTest {
@TestDisposable
private lateinit var disposable: Disposable
private val actionsBeforeCollapseGroup = (1..5).map { MyAction(it) }.toList()
private val subActions = (6..8).map { MyAction(it) }.toList()
private val actionsAfterCollapseGroup = (9..12).map { MyAction(it) }.toList()
private val collapseGroup = CollapsedActionGroup("MyGroup", subActions)
@Test
fun listenerOpensActions() {
val initialModelList = actionsBeforeCollapseGroup + listOf(collapseGroup) + actionsAfterCollapseGroup
val model = DefaultListModel<AnAction>().apply {
addAll(initialModelList)
}
val list = JList(model)
ListListenerCollapsedActionGroupExpander.expandCollapsableGroupsOnSelection(list, model, disposable)
list.selectedIndex = 1
MatcherAssert.assertThat("Model broken after selection", model.elements().toList(), Matchers.equalTo(initialModelList))
list.selectedIndex = initialModelList.size - 1
MatcherAssert.assertThat("Model broken after selection", model.elements().toList(), Matchers.equalTo(initialModelList))
list.selectedIndex = actionsBeforeCollapseGroup.size // Click on collapse grop
MatcherAssert.assertThat("Model hasn't been expanded after clicking on collapse button", model.elements().toList(), Matchers.equalTo(
actionsBeforeCollapseGroup + subActions + actionsAfterCollapseGroup
))
}
@Test
fun collapsedButtonWidth() {
val componentWidth = createCollapsedButton(collapseGroup) {
(it as MyAction).preferedWidth
}.preferredSize.width
val maxActionWidth = subActions.maxOfOrNull { it.preferedWidth }!!
MatcherAssert.assertThat(
"Component size should be at least as wide as longest action not to blink when actions appear",
componentWidth, Matchers.greaterThanOrEqualTo(maxActionWidth))
}
}
private class MyAction(id: Int) : AnAction("Action$id") {
override fun actionPerformed(e: AnActionEvent) = Unit
val preferedWidth = id * 100
}

View File

@@ -6,6 +6,7 @@ import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.CollapsedActionGroup;
import com.intellij.platform.DirectoryProjectGenerator;
import com.intellij.util.ObjectUtils;
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle;
@@ -79,8 +80,8 @@ public final class PyCharmNewProjectStep extends AbstractNewProjectStep<PyNewPro
var python = new DefaultActionGroup(PyCharmCommunityCustomizationBundle.message("new.project.python.group.name"),
map.get(true).stream().flatMap(pair -> Arrays.stream(pair.second)).toList());
var other = new DefaultActionGroup(PyCharmCommunityCustomizationBundle.message("new.project.other.group.name"),
map.get(false).stream().flatMap(pair -> Arrays.stream(pair.second)).toList());
var other = new CollapsedActionGroup(PyCharmCommunityCustomizationBundle.message("new.project.other.group.name"),
map.get(false).stream().flatMap(pair -> Arrays.stream(pair.second)).toList());
return new AnAction[] { python, other };
}