From d9735a4a455c4b0fcfbe9eae7b0dcbfb2f03519d Mon Sep 17 00:00:00 2001 From: Artem Aleksyuk Date: Tue, 1 Jul 2025 18:18:38 +0200 Subject: [PATCH] GO-18846: Display suggestions when there are no services in the Services tool window (cherry picked from commit 33f10e2419123d8e68c937323ef7ef147f3bc4f8) IJ-CR-168429 GitOrigin-RevId: 4817144fc4c9e73e8a9efe6e44ba50fcdb751d38 --- .../src/AddServiceEmptyTreeSuggestion.kt | 59 +++++++++++++++ .../src/ServiceTreeView.java | 71 ++++++++----------- .../messages/ExecutionBundle.properties | 1 + .../services/ServiceViewContributor.java | 8 +++ .../ServiceViewEmptyTreeSuggestion.kt | 30 ++++++++ 5 files changed, 127 insertions(+), 42 deletions(-) create mode 100644 platform/execution.serviceView/src/AddServiceEmptyTreeSuggestion.kt create mode 100644 platform/lang-api/src/com/intellij/execution/services/ServiceViewEmptyTreeSuggestion.kt diff --git a/platform/execution.serviceView/src/AddServiceEmptyTreeSuggestion.kt b/platform/execution.serviceView/src/AddServiceEmptyTreeSuggestion.kt new file mode 100644 index 000000000000..fdd096b10464 --- /dev/null +++ b/platform/execution.serviceView/src/AddServiceEmptyTreeSuggestion.kt @@ -0,0 +1,59 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.execution.serviceView + +import com.intellij.execution.ExecutionBundle +import com.intellij.execution.services.ServiceViewEmptyTreeSuggestion +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.actionSystem.impl.ActionButtonUtil +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent +import java.awt.event.InputEvent +import javax.swing.Icon + +class AddServiceEmptyTreeSuggestion : ServiceViewEmptyTreeSuggestion { + private val ADD_SERVICE_ACTION_ID: String = "ServiceView.AddService" + + // Always the last + override val weight: Int = -10 + + override val icon: Icon? = AllIcons.General.Add + + override val text: String = ExecutionBundle.message("service.view.empty.tree.suggestion.add.service") + + override val shortcutText: String? + get() { + val addAction = ActionManager.getInstance().getAction(ADD_SERVICE_ACTION_ID) + val shortcutSet = addAction?.shortcutSet + val shortcut = shortcutSet?.getShortcuts()?.firstOrNull() ?: return null + + return KeymapUtil.getShortcutText(shortcut) + } + + override fun onActivate(dataContext: DataContext, inputEvent: InputEvent?) { + val selectedView = ServiceViewActionProvider.getSelectedView(dataContext) ?: return + val action = ActionManager.getInstance().getAction(ADD_SERVICE_ACTION_ID) + val actionGroup = action as? ActionGroup ?: return + + val popup = JBPopupFactory.getInstance().createActionGroupPopup( + "", + actionGroup, + dataContext, + JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, + false, + ActionPlaces.getActionGroupPopupPlace(ADD_SERVICE_ACTION_ID)) + val button = ActionButtonUtil.findActionButtonById(selectedView, ADD_SERVICE_ACTION_ID) ?: return + popup.addListener(object : JBPopupListener { + override fun beforeShown(event: LightweightWindowEvent) { + Toggleable.setSelected(button, true) + } + + override fun onClosed(event: LightweightWindowEvent) { + Toggleable.setSelected(button, null) + } + }) + popup.showUnderneathOf(button) + } +} \ No newline at end of file diff --git a/platform/execution.serviceView/src/ServiceTreeView.java b/platform/execution.serviceView/src/ServiceTreeView.java index 47b53e3ab5d5..c4f18625bd46 100644 --- a/platform/execution.serviceView/src/ServiceTreeView.java +++ b/platform/execution.serviceView/src/ServiceTreeView.java @@ -2,19 +2,14 @@ package com.intellij.platform.execution.serviceView; import com.intellij.execution.ExecutionBundle; -import com.intellij.execution.services.ServiceEventListener; -import com.intellij.execution.services.ServiceViewContributor; -import com.intellij.execution.services.ServiceViewDescriptor; -import com.intellij.execution.services.ServiceViewManager; +import com.intellij.execution.services.*; import com.intellij.ide.DataManager; import com.intellij.ide.dnd.DnDManager; import com.intellij.ide.util.treeView.TreeState; import com.intellij.openapi.Disposable; -import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.application.AppUIExecutor; -import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; @@ -24,7 +19,6 @@ import com.intellij.platform.execution.serviceView.ServiceViewNavBarService.Serv import com.intellij.platform.navbar.frontend.vm.NavBarVm; import com.intellij.ui.AutoScrollToSourceHandler; import com.intellij.ui.SimpleTextAttributes; -import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.tree.AsyncTreeModel; import com.intellij.ui.tree.RestoreSelectionListener; import com.intellij.ui.tree.TreeVisitor; @@ -47,8 +41,7 @@ import javax.swing.*; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.awt.event.InputEvent; import java.lang.ref.WeakReference; import java.util.*; import java.util.List; @@ -58,8 +51,6 @@ import java.util.concurrent.CancellationException; import static com.intellij.platform.execution.serviceView.ServiceViewDragHelper.getTheOnlyRootContributor; final class ServiceTreeView extends ServiceView { - private static final String ADD_SERVICE_ACTION_ID = "ServiceView.AddService"; - private final ServiceViewTree myTree; private final ServiceViewTreeModel myTreeModel; private final ServiceViewModel.ServiceViewModelListener myListener; @@ -459,41 +450,37 @@ final class ServiceTreeView extends ServiceView { } private static void setEmptyText(JComponent component, StatusText emptyText) { + emptyText.withUnscaledGapAfter(5); emptyText.setText(ExecutionBundle.message("service.view.empty.tree.text")); - emptyText.appendSecondaryText(ExecutionBundle.message("service.view.add.service.action.name"), - SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES, - new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ActionGroup addActionGroup = ObjectUtils.tryCast( - ActionManager.getInstance().getAction(ADD_SERVICE_ACTION_ID), ActionGroup.class); - if (addActionGroup == null) return; - Point position = component.getMousePosition(); - if (position == null) { - Rectangle componentBounds = component.getBounds(); - Rectangle textBounds = emptyText.getComponent().getBounds(); - position = new Point(componentBounds.width / 2, - componentBounds.height / (emptyText.isShowAboveCenter() ? 3 : 2) + - textBounds.height / 4); - - } - DataContext dataContext = DataManager.getInstance().getDataContext(component); - JBPopupFactory.getInstance().createActionGroupPopup( - addActionGroup.getTemplatePresentation().getText(), addActionGroup, dataContext, - JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, - false, null, -1, null, ActionPlaces.getActionGroupPopupPlace(ADD_SERVICE_ACTION_ID)) - .show(new RelativePoint(component, position)); - } - }); - AnAction addAction = ActionManager.getInstance().getAction(ADD_SERVICE_ACTION_ID); - ShortcutSet shortcutSet = addAction == null ? null : addAction.getShortcutSet(); - Shortcut shortcut = shortcutSet == null ? null : ArrayUtil.getFirstElement(shortcutSet.getShortcuts()); - if (shortcut != null) { - emptyText.appendSecondaryText(" (" + KeymapUtil.getShortcutText(shortcut) + ")", StatusText.DEFAULT_ATTRIBUTES, null); + var sortedSuggestions = getEmptyTreeSuggestions(); + for (int i = 0; i < sortedSuggestions.size(); i++) { + ServiceViewEmptyTreeSuggestion suggestion = sortedSuggestions.get(i); + String suggestionText = suggestion.getText(); + Icon icon = suggestion.getIcon(); + emptyText.appendText(0, i + 1, icon, suggestionText, SimpleTextAttributes.LINK_PLAIN_ATTRIBUTES, e -> { + InputEvent inputEvent = e.getSource() instanceof InputEvent ie ? ie : null; + DataContext dataContext = DataManager.getInstance().getDataContext(component); + suggestion.onActivate(dataContext, inputEvent); + }); + String shortcutText = suggestion.getShortcutText(); + if (shortcutText != null) { + String paddedText = " " + shortcutText; + emptyText.appendText(0, i + 1, icon, paddedText, SimpleTextAttributes.GRAYED_ATTRIBUTES, null); + } } } + @NotNull + private static List getEmptyTreeSuggestions() { + List externalSuggestions = + ContainerUtil.mapNotNull(ServiceViewContributor.CONTRIBUTOR_EP_NAME.getExtensionList(), + ServiceViewContributor::getEmptyTreeSuggestion); + var allSuggestions = ContainerUtil.append(externalSuggestions, new AddServiceEmptyTreeSuggestion()); + var highWeightFirst = Comparator.comparingInt(ServiceViewEmptyTreeSuggestion::getWeight).reversed(); + return ContainerUtil.sorted(allSuggestions, highWeightFirst); + } + private static List adjustPaths(List paths, Collection roots, Object treeRoot) { List result = new SmartList<>(); for (TreePath path : paths) { diff --git a/platform/execution/resources/messages/ExecutionBundle.properties b/platform/execution/resources/messages/ExecutionBundle.properties index eab284f546b0..b7665b74ff3a 100644 --- a/platform/execution/resources/messages/ExecutionBundle.properties +++ b/platform/execution/resources/messages/ExecutionBundle.properties @@ -370,6 +370,7 @@ run.dashboard.apply.to.all.types=Apply to all types service.view.open.in.new.tab.ad.text=Drag node onto tool window header to open a new tab service.view.empty.tree.text=No services configured. +service.view.empty.tree.suggestion.add.service=Add a service service.view.empty.tab.text=No content available service.view.empty.selection.text=Select service to view details service.view.add.service.action.name=Add service diff --git a/platform/lang-api/src/com/intellij/execution/services/ServiceViewContributor.java b/platform/lang-api/src/com/intellij/execution/services/ServiceViewContributor.java index 9d30c678677c..5a207210e66c 100644 --- a/platform/lang-api/src/com/intellij/execution/services/ServiceViewContributor.java +++ b/platform/lang-api/src/com/intellij/execution/services/ServiceViewContributor.java @@ -3,6 +3,7 @@ package com.intellij.execution.services; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; @@ -37,4 +38,11 @@ public interface ServiceViewContributor { /// @return a [ServiceViewDescriptor] for the child node [T] @NotNull ServiceViewDescriptor getServiceDescriptor(@NotNull Project project, @NotNull T service); + + /// @see ServiceViewEmptyTreeSuggestion + @ApiStatus.Internal + @Nullable + default ServiceViewEmptyTreeSuggestion getEmptyTreeSuggestion() { + return null; + } } diff --git a/platform/lang-api/src/com/intellij/execution/services/ServiceViewEmptyTreeSuggestion.kt b/platform/lang-api/src/com/intellij/execution/services/ServiceViewEmptyTreeSuggestion.kt new file mode 100644 index 000000000000..cc28475f4209 --- /dev/null +++ b/platform/lang-api/src/com/intellij/execution/services/ServiceViewEmptyTreeSuggestion.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.execution.services + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.util.NlsSafe +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls +import java.awt.event.InputEvent +import javax.swing.Icon + +/** + * Represents a suggestion displayed when there are no services in the Services tool window. + * Enhances the UX for users opening the Services tool window for the first time. + * @see ServiceViewContributor + */ +@ApiStatus.Internal +interface ServiceViewEmptyTreeSuggestion { + val weight: Int + + val icon: Icon? + + @get:Nls(capitalization = Nls.Capitalization.Sentence) + val text: String + + @get:NlsSafe + val shortcutText: String? + get() = null + + fun onActivate(dataContext: DataContext, inputEvent: InputEvent?) +} \ No newline at end of file