[services] IDEA-320784 use new nav bar in Services tool window

GitOrigin-RevId: d43651031472aa4cf301bad233241dbb4d4d9279
This commit is contained in:
Konstantin Aleev
2023-07-18 12:05:23 +00:00
committed by intellij-monorepo-bot
parent a409d288ed
commit b47a759d98
8 changed files with 296 additions and 216 deletions
@@ -3,9 +3,10 @@ package com.intellij.execution.services;
import com.intellij.execution.ExecutionBundle;
import com.intellij.execution.services.ServiceModel.ServiceViewItem;
import com.intellij.execution.services.ServiceViewNavBarService.ServiceViewNavBarSelector;
import com.intellij.ide.DataManager;
import com.intellij.ide.dnd.DnDManager;
import com.intellij.ide.navigationToolbar.NavBarModel;
import com.intellij.ide.navbar.vm.NavBarVm;
import com.intellij.ide.util.treeView.TreeState;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
@@ -32,6 +33,7 @@ import com.intellij.util.containers.JBTreeTraverser;
import com.intellij.util.ui.StatusText;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.AsyncPromise;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;
@@ -70,9 +72,6 @@ final class ServiceTreeView extends ServiceView {
myTreeModel = new ServiceViewTreeModel(model);
myTree = new ServiceViewTree(myTreeModel, this);
myListener = new ServiceViewTreeModelListener();
model.addModelListener(myListener);
ServiceViewActionProvider actionProvider = ServiceViewActionProvider.getInstance();
ui.setServiceToolbar(actionProvider);
ui.setMasterComponent(myTree, actionProvider);
@@ -86,18 +85,30 @@ final class ServiceTreeView extends ServiceView {
myTree.addTreeSelectionListener(new RestoreSelectionListener());
myTree.addTreeSelectionListener(e -> onSelectionChanged());
Consumer<ServiceViewItem> selector = item ->
select(item.getValue(), item.getRootContributor().getClass())
.onSuccess(result -> AppUIExecutor.onUiThread().expireWith(this).submit(() -> {
JComponent component = getUi().getDetailsComponent();
if (component != null) {
IdeFocusManager.getInstance(getProject()).requestFocus(component, false);
}
}));
myNavBarPanel = new ServiceViewNavBarPanel(getProject(), true, getModel(), selector);
myNavBarPanel.getModel().updateModel((Object)null);
ServiceViewNavBarSelector selector = new ServiceViewNavBarSelector() {
@Override
public void select(@NotNull ServiceModel.ServiceViewItem item) {
ServiceTreeView.this.select(item.getValue(), item.getRootContributor().getClass())
.onSuccess(result -> AppUIExecutor.onUiThread().expireWith(ServiceTreeView.this).submit(() -> {
JComponent component = getUi().getDetailsComponent();
if (component != null) {
IdeFocusManager.getInstance(getProject()).requestFocus(component, false);
}
}));
}
@Nullable
@Override
public ServiceViewItem getSelectedItem() {
return myLastSelection;
}
};
myNavBarPanel = ServiceViewNavBarService.getInstance(project).createNavBarPanel(this, selector);
myUi.setNavBar(myNavBarPanel);
myListener = new ServiceViewTreeModelListener();
model.addModelListener(myListener);
if (model instanceof ServiceViewModel.AllServicesModel) {
setEmptyText(myTree, myTree.getEmptyText());
}
@@ -241,7 +252,11 @@ final class ServiceTreeView extends ServiceView {
IdeFocusManager.getInstance(getProject()).requestFocus(myTree, false);
}
else {
myNavBarPanel.rebuildAndSelectTail(true);
NavBarVm vm = myNavBarPanel.getModel();
if (vm != null) {
myNavBarPanel.updateModel();
vm.selectTail();
}
}
}
@@ -271,7 +286,7 @@ final class ServiceTreeView extends ServiceView {
}
myLastSelection = newSelection;
myNavBarPanel.getModel().updateModel(newSelection);
myNavBarPanel.updateModel();
if (!mySelected) return;
@@ -325,40 +340,6 @@ final class ServiceTreeView extends ServiceView {
});
}
private void updateNavBar() {
AppUIExecutor.onUiThread().expireWith(this).submit(() -> {
ServiceViewItem item = getNavBarItem();
if (item == null) return;
WeakReference<ServiceViewItem> itemRef = new WeakReference<>(item);
getModel().getInvoker().invoke(() -> {
ServiceViewItem viewItem = itemRef.get();
if (viewItem == null) return;
ServiceViewItem updatedItem = getModel().findItemSafe(viewItem);
if (updatedItem != null) {
WeakReference<ServiceViewItem> updatedRef = new WeakReference<>(updatedItem);
AppUIExecutor.onUiThread().expireWith(this).submit(() -> {
ServiceViewItem updatedViewItem = updatedRef.get();
if (updatedViewItem == null) return;
ServiceViewItem navBarItem = getNavBarItem();
if (updatedViewItem.equals(navBarItem) && !updatedViewItem.isRemoved()) {
myNavBarPanel.getModel().updateModel(updatedItem);
}
});
}
});
});
}
private ServiceViewItem getNavBarItem() {
NavBarModel navBarModel = myNavBarPanel.getModel();
if (navBarModel.isEmpty()) return null;
return ObjectUtils.tryCast(navBarModel.getElement(navBarModel.size() - 1), ServiceViewItem.class);
}
private void updateSelectionPaths() {
AppUIExecutor.onUiThread().expireWith(this).submit(() -> {
TreePath[] currentPaths = myTree.getSelectionPaths();
@@ -558,11 +539,7 @@ final class ServiceTreeView extends ServiceView {
}
private void updateNavBar() {
AppUIExecutor.onUiThread().expireWith(myNavBarPanel).submit(() -> {
myNavBarPanel.hidePopup();
myNavBarPanel.getModel().updateModel((Object)null);
myNavBarPanel.getUpdateQueue().rebuildUi();
});
myNavBarPanel.updateModel();
}
}
@@ -2,14 +2,19 @@
package com.intellij.execution.services;
import com.intellij.execution.services.ServiceModel.ServiceViewItem;
import com.intellij.execution.services.ServiceViewNavBarPanel.ServiceViewNavBarRoot;
import com.intellij.icons.AllIcons;
import com.intellij.ide.navbar.impl.DefaultNavBarItem;
import com.intellij.ide.navigationToolbar.AbstractNavBarModelExtension;
import com.intellij.ide.util.treeView.WeighedItem;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.List;
public class ServiceViewNavBarExtension extends AbstractNavBarModelExtension {
@Nullable
@@ -19,11 +24,36 @@ public class ServiceViewNavBarExtension extends AbstractNavBarModelExtension {
return serviceView == null ? null : ServiceViewActionProvider.SERVICE_VIEW_ITEM_POPUP;
}
@Override
public @Nullable Object getData(@NotNull String dataId, @NotNull DataProvider provider) {
if (PlatformCoreDataKeys.SELECTED_ITEM.is(dataId)) {
return unwrapNavBarItem(PlatformCoreDataKeys.SELECTED_ITEM.getData(provider));
}
if (PlatformCoreDataKeys.SELECTED_ITEMS.is(dataId)) {
Object[] items = PlatformCoreDataKeys.SELECTED_ITEMS.getData(provider);
if (items != null) {
return ContainerUtil.map2Array(items, ServiceViewNavBarExtension::unwrapNavBarItem);
}
return null;
}
return null;
}
private static Object unwrapNavBarItem(Object item) {
if (item instanceof DefaultNavBarItem<?> defaultNavBarItem) {
Object data = defaultNavBarItem.getData();
if (data instanceof ServiceViewNavBarItem serviceViewNavBarItem) {
return serviceViewNavBarItem.getItem().getValue();
}
}
return item;
}
@Nullable
@Override
public String getPresentableText(Object object) {
if (object instanceof ServiceViewItem) {
return ServiceViewDragHelper.getDisplayName(((ServiceViewItem)object).getViewDescriptor().getPresentation());
if (object instanceof ServiceViewNavBarItem item) {
return ServiceViewDragHelper.getDisplayName(item.getItem().getViewDescriptor().getPresentation());
}
if (object instanceof ServiceViewNavBarRoot) {
return "";
@@ -34,12 +64,81 @@ public class ServiceViewNavBarExtension extends AbstractNavBarModelExtension {
@Nullable
@Override
public Icon getIcon(Object object) {
if (object instanceof ServiceViewItem) {
return ((ServiceViewItem)object).getViewDescriptor().getPresentation().getIcon(false);
if (object instanceof ServiceViewNavBarItem item) {
return item.getItem().getViewDescriptor().getPresentation().getIcon(false);
}
if (object instanceof ServiceViewNavBarRoot) {
return AllIcons.Nodes.Services;
}
return null;
}
@Override
public boolean processChildren(Object object, Object rootElement, Processor<Object> processor) {
if (object instanceof ServiceViewNavBarRoot root) {
ServiceViewModel viewModel = root.getViewModel();
List<? extends ServiceViewItem> roots = viewModel.getVisibleRoots();
for (int i = 0; i < roots.size(); i++) {
if (!processor.process(new ServiceViewNavBarItem(roots.get(i), viewModel, -i))) return false;
}
return true;
}
if (object instanceof ServiceViewNavBarItem navBarItem) {
ServiceViewModel viewModel = navBarItem.getViewModel();
ServiceViewItem item = navBarItem.getItem();
if (item instanceof ServiceModel.ServiceNode service) {
if (service.getProvidingContributor() != null && !service.isChildrenInitialized()) {
viewModel.getInvoker().invoke(() -> {
service.getChildren(); // initialize children on background thread
});
return true;
}
}
List<? extends ServiceViewItem> children = viewModel.getChildren(item);
for (int i = 0; i < children.size(); i++) {
if (!processor.process(new ServiceViewNavBarItem(children.get(i), viewModel, -i))) return false;
}
return true;
}
return true;
}
static final class ServiceViewNavBarRoot {
private final ServiceViewModel myViewModel;
ServiceViewNavBarRoot(@NotNull ServiceViewModel viewModel) {
myViewModel = viewModel;
}
@NotNull ServiceViewModel getViewModel() {
return myViewModel;
}
}
static final class ServiceViewNavBarItem implements WeighedItem {
private final ServiceViewItem myItem;
private final ServiceViewModel myViewModel;
private final int myWeight;
ServiceViewNavBarItem(@NotNull ServiceViewItem item, @NotNull ServiceViewModel viewModel, int weight) {
myItem = item;
myViewModel = viewModel;
myWeight = weight;
}
@Override
public int getWeight() {
return myWeight;
}
@NotNull ServiceViewItem getItem() {
return myItem;
}
@NotNull ServiceViewModel getViewModel() {
return myViewModel;
}
}
}
@@ -1,153 +0,0 @@
// 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.execution.services;
import com.intellij.execution.services.ServiceModel.ServiceNode;
import com.intellij.execution.services.ServiceModel.ServiceViewItem;
import com.intellij.ide.navigationToolbar.NavBarModel;
import com.intellij.ide.navigationToolbar.NavBarModelExtension;
import com.intellij.ide.navigationToolbar.NavBarModelListener;
import com.intellij.ide.navigationToolbar.NavBarPanel;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
final class ServiceViewNavBarPanel extends NavBarPanel {
private final @NotNull Consumer<? super ServiceViewItem> mySelector;
private boolean myRebuildNeeded = true;
ServiceViewNavBarPanel(@NotNull Project project, boolean docked, @NotNull ServiceViewModel viewModel,
@NotNull Consumer<? super ServiceViewItem> selector) {
super(project, docked);
mySelector = selector;
Disposer.register(viewModel, this);
((ServiceViewNavBarModel)getModel()).setViewModel(viewModel);
}
@Override
protected boolean isDisposeOnRemove() {
return false;
}
@Override
protected NavBarModel createModel() {
return new ServiceViewNavBarModel(myProject, new NavBarModelListener() {
@Override
public void modelChanged() {
myRebuildNeeded = true;
getUpdateQueue().queueRebuildUi();
}
@Override
public void selectionChanged() {
updateItems();
scrollSelectionToVisible(true);
}
});
}
@Override
public boolean isRebuildUiNeeded() {
if (myRebuildNeeded) {
myRebuildNeeded = false;
return true;
}
return super.isRebuildUiNeeded();
}
@Override
protected void doubleClick(Object object) {
if (object instanceof ServiceViewItem) {
mySelector.accept((ServiceViewItem)object);
}
hideHint(true);
}
@Override
public @Nullable Object getData(@NotNull String dataId) {
if (NAV_BAR_ACTION_HANDLER.is(dataId)) {
return this;
}
return super.getData(dataId);
}
void hidePopup() {
hideHint(false);
}
private static final class ServiceViewNavBarModel extends NavBarModel {
private ServiceViewModel myViewModel;
private final ServiceViewNavBarRoot myRoot = new ServiceViewNavBarRoot();
ServiceViewNavBarModel(@NotNull Project project, @NotNull NavBarModelListener notificator) {
super(project, notificator, null);
}
void setViewModel(@NotNull ServiceViewModel viewModel) {
myViewModel = viewModel;
}
@Override
public void updateModel(DataContext dataContext) {
}
@Override
public void updateModelAsync(@NotNull DataContext dataContext, @Nullable Runnable callback) {
}
@Override
protected void updateModel(PsiElement psiElement, NavBarModelExtension ownerExtension) {
}
@Override
public void updateModel(Object object) {
List<Object> path = new ArrayList<>();
if (object instanceof ServiceViewItem item) {
List<? extends ServiceViewItem> roots = myViewModel.getVisibleRoots();
do {
path.add(item);
item = roots.contains(item) ? null : item.getParent();
}
while (item != null);
}
path.add(myRoot);
Collections.reverse(path);
setModel(path, true);
}
@Override
protected boolean hasChildren(Object object) {
return !getChildren(object).isEmpty();
}
@Override
public List<Object> getChildren(Object object) {
if (object == myRoot) {
return new ArrayList<>(myViewModel.getVisibleRoots());
}
if (object instanceof ServiceViewItem) {
if (object instanceof ServiceNode service) {
if (service.getProvidingContributor() != null && !service.isChildrenInitialized()) {
myViewModel.getInvoker().invoke(() -> {
service.getChildren(); // initialize children on background thread
});
return Collections.emptyList();
}
}
return new ArrayList<>(myViewModel.getChildren((ServiceViewItem)object));
}
return Collections.emptyList();
}
}
static final class ServiceViewNavBarRoot {
}
}
@@ -0,0 +1,99 @@
// Copyright 2000-2023 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.execution.services.ServiceViewNavBarExtension.ServiceViewNavBarItem
import com.intellij.execution.services.ServiceViewNavBarService.ServiceViewNavBarSelector
import com.intellij.ide.navbar.NavBarItem
import com.intellij.ide.navbar.ide.NavBarVmItem
import com.intellij.ide.navbar.ide.toVmItems
import com.intellij.ide.navbar.impl.DefaultNavBarItem
import com.intellij.ide.navbar.ui.StaticNavBarPanel
import com.intellij.openapi.application.readAction
import com.intellij.openapi.project.Project
import com.intellij.util.ui.update.Activatable
import com.intellij.util.ui.update.UiNotifyConnector
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.awt.Window
import java.util.function.Supplier
internal class ServiceViewNavBarPanel(project: Project,
cs: CoroutineScope,
private val updateRequests: MutableSharedFlow<Unit>,
private val viewModel: ServiceViewModel,
private val selector: ServiceViewNavBarSelector,
requestNavigation: (NavBarVmItem) -> Unit) :
StaticNavBarPanel(project, cs, updateRequests, requestNavigation) {
private val root = ServiceViewNavBarExtension.ServiceViewNavBarRoot(viewModel)
@Volatile
private var visible = true
private val listeners = ArrayList<Supplier<Unit>>()
init {
UiNotifyConnector.installOn(this, object : Activatable {
override fun showNotify() {
visible = true
}
override fun hideNotify() {
visible = false
notifyListeners()
}
}, false)
cs.launch {
channelFlow {
listeners.add {
trySend(Unit)
}
awaitClose()
}
.buffer(Channel.CONFLATED)
.collect(updateRequests)
}
}
fun updateModel() {
if (visible) {
notifyListeners()
}
}
private fun notifyListeners() {
for (listener in listeners) {
listener.get()
}
}
override suspend fun getDefaultModel(project: Project): List<NavBarVmItem> {
return readAction { getItems().toVmItems() }
}
override fun contextItems(window: Window): Flow<List<NavBarVmItem>> {
@OptIn(ExperimentalCoroutinesApi::class)
return updateRequests.transformLatest {
val path = getItems()
emit(readAction { path.toVmItems() })
}
}
private fun getItems(): List<NavBarItem> {
val path = ArrayList<NavBarItem>()
var item = if (visible) selector.getSelectedItem() else null
if (item != null) {
val roots = viewModel.visibleRoots
do {
path.add(DefaultNavBarItem(ServiceViewNavBarItem(item!!, viewModel, 0)))
item = if (roots.contains(item)) null else item.parent
}
while (item != null)
}
path.add(DefaultNavBarItem(root))
path.reverse()
return path
}
}
@@ -0,0 +1,49 @@
// Copyright 2000-2023 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.execution.services.ServiceModel.ServiceViewItem
import com.intellij.execution.services.ServiceViewNavBarExtension.ServiceViewNavBarItem
import com.intellij.ide.navbar.impl.DefaultNavBarItem
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.util.childScope
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@Service(Service.Level.PROJECT)
internal class ServiceViewNavBarService(val project: Project, val cs: CoroutineScope) {
companion object {
@JvmStatic
fun getInstance(project: Project): ServiceViewNavBarService = project.service()
}
fun createNavBarPanel(serviceView: ServiceView,
selector: ServiceViewNavBarSelector): ServiceViewNavBarPanel {
@OptIn(ExperimentalCoroutinesApi::class)
val childScope = cs.childScope(Dispatchers.Default.limitedParallelism(1))
Disposer.register(serviceView) {
childScope.cancel()
}
val updateRequests: MutableSharedFlow<Unit> = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
return ServiceViewNavBarPanel(project, childScope, updateRequests, serviceView.model, selector) {
childScope.launch {
val item = ((it.pointer.dereference() as? DefaultNavBarItem<*>)?.data as? ServiceViewNavBarItem)?.item ?: return@launch
withContext(Dispatchers.EDT) {
selector.select(item)
}
}
}
}
internal interface ServiceViewNavBarSelector {
fun select(item: ServiceViewItem)
fun getSelectedItem(): ServiceViewItem?
}
}
@@ -7,6 +7,7 @@ import com.intellij.ide.navbar.NavBarItemPresentation
import com.intellij.ide.navigationToolbar.NavBarModelExtension
import com.intellij.ide.projectView.ProjectView
import com.intellij.ide.projectView.impl.ProjectRootsUtil
import com.intellij.ide.util.treeView.WeighedItem
import com.intellij.model.Pointer
import com.intellij.model.Pointer.hardPointer
import com.intellij.openapi.editor.colors.CodeInsightColors.ERRORS_ATTRIBUTES
@@ -213,6 +214,10 @@ internal class OrderEntryNavBarItem(data: OrderEntry) : DefaultNavBarItem<OrderE
}
}
internal class WeightedNavBarItem(data: WeighedItem): DefaultNavBarItem<WeighedItem>(data) {
override fun weight(): Int = data.weight
}
@Internal
val navBarErrorAttributes: SimpleTextAttributes =
EditorColorsManager.getInstance()
@@ -5,6 +5,7 @@ import com.intellij.diagnostic.PluginException
import com.intellij.ide.navbar.NavBarItem
import com.intellij.ide.navbar.NavBarItemProvider
import com.intellij.ide.navigationToolbar.NavBarModelExtension
import com.intellij.ide.util.treeView.WeighedItem
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
@@ -143,6 +144,7 @@ class DefaultNavBarItemProvider : NavBarItemProvider {
}
}
is OrderEntry -> OrderEntryNavBarItem(child)
is WeighedItem -> WeightedNavBarItem(child)
else -> DefaultNavBarItem(child)
}
}
@@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.transformLatest
import java.awt.BorderLayout
import java.awt.Window
internal class StaticNavBarPanel(
internal open class StaticNavBarPanel(
private val project: Project,
private val cs: CoroutineScope,
private val updateRequests: Flow<Any>,
@@ -70,7 +70,7 @@ internal class StaticNavBarPanel(
private suspend fun handleWindow(window: Window): Nothing = supervisorScope {
val vm = NavBarVmImpl(
this@supervisorScope,
initialItems = defaultModel(project),
initialItems = getDefaultModel(project),
contextItems = contextItems(window),
requestNavigation,
)
@@ -83,7 +83,9 @@ internal class StaticNavBarPanel(
}
}
private fun contextItems(window: Window): Flow<List<NavBarVmItem>> {
protected open suspend fun getDefaultModel(project: Project): List<NavBarVmItem> = defaultModel(project)
protected open fun contextItems(window: Window): Flow<List<NavBarVmItem>> {
@OptIn(ExperimentalCoroutinesApi::class)
return updateRequests.transformLatest {
dataContext(window, panel = this@StaticNavBarPanel)?.let {