mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-19 21:11:28 +07:00
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:
committed by
intellij-monorepo-bot
parent
32710b000c
commit
ee35416f38
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user