From 2b2b55465dd71ecaa237c2283cf99d28755d43d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=A6=E5=A2=83=E8=BF=B7=E7=A6=BB?= Date: Wed, 24 Sep 2025 22:55:38 +0800 Subject: [PATCH 1/2] support IDEA 253.x and scrolling modules --- .github/workflows/ScalaCI.yml | 16 +- build.sbt | 5 +- .../jbexternal/DependencyAnalyzerManager.kt | 3 +- .../jbexternal/DependencyAnalyzerViewImpl.kt | 13 +- .../DependencyAnalyzerVirtualFile.kt | 3 +- .../sbt/analyzer/jbexternal/package-info.kt | 4 +- .../jbexternal/util/DependencyUiUtil.kt | 45 +++- .../jbexternal/util/ExternalProjectUiUtil.kt | 151 ++++++----- .../analyzer/jbexternal/util/ScopeUiUtil.kt | 235 ++++++++++++------ .../sbt/analyzer/jbexternal/util/UiUtils.kt | 77 ++---- src/main/resources/META-INF/plugin.xml | 9 +- .../SbtDependencyAnalyzerContributor.scala | 9 +- .../AddDependencyPreviewWizard.scala | 2 - 13 files changed, 336 insertions(+), 236 deletions(-) diff --git a/.github/workflows/ScalaCI.yml b/.github/workflows/ScalaCI.yml index afaa1b7..2c41222 100644 --- a/.github/workflows/ScalaCI.yml +++ b/.github/workflows/ScalaCI.yml @@ -14,11 +14,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 21 cache: sbt - uses: sbt/setup-sbt@v1 - uses: coursier/cache-action@v6 @@ -29,11 +29,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 21 cache: sbt - uses: sbt/setup-sbt@v1 - uses: coursier/cache-action@v6 @@ -44,11 +44,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 21 cache: sbt - uses: sbt/setup-sbt@v1 - uses: coursier/cache-action@v6 @@ -59,11 +59,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: 21 cache: sbt - uses: sbt/setup-sbt@v1 - uses: coursier/cache-action@v6 diff --git a/build.sbt b/build.sbt index 3e05fe8..d7c2cc8 100644 --- a/build.sbt +++ b/build.sbt @@ -11,9 +11,8 @@ lazy val ktVersion = "2.1.0" lazy val jbAnnotVersion = "26.0.2" // https://youtrack.jetbrains.com/articles/IDEA-A-2100661679/IntelliJ-IDEA-2023.3-Latest-Builds -// NOTE: Latest-Builds 233 -lazy val intellijVersion = "252.25557.131" -lazy val pluginVersion = s"0.8.2-$intellijVersion" +lazy val intellijVersion = "253.20558.43" +lazy val pluginVersion = s"0.9.0-RC1" ThisBuild / version := pluginVersion diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerManager.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerManager.kt index 205d039..c24357b 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerManager.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerManager.kt @@ -1,8 +1,6 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package bitlap.sbt.analyzer.jbexternal -import bitlap.sbt.analyzer.jbexternal.util.whenDisposed - import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerExtension @@ -10,6 +8,7 @@ import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyze import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project +import com.intellij.openapi.observable.util.whenDisposed @Service(Service.Level.PROJECT) class DependencyAnalyzerManager(private val project: Project) { diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerViewImpl.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerViewImpl.kt index 85cbad5..b823f8d 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerViewImpl.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerViewImpl.kt @@ -35,6 +35,7 @@ import com.intellij.openapi.observable.operation.core.getOperationInProgressProp import com.intellij.openapi.observable.operation.core.isOperationInProgress import com.intellij.openapi.observable.operation.core.withCompletedOperation import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.observable.util.* import com.intellij.openapi.progress.util.BackgroundTaskUtil import com.intellij.openapi.project.Project @@ -45,11 +46,10 @@ import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.SearchTextField import com.intellij.ui.components.JBLoadingPanel import com.intellij.util.concurrency.AppExecutorUtil -import com.intellij.util.ui.JBUI import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency as Dependency /** - * https://github.com/JetBrains/intellij-community/blob/idea/233.11799.300/platform/external-system-impl/src/com/intellij/openapi/externalSystem/dependency/analyzer/DependencyAnalyzerViewImpl.kt + * https://github.com/JetBrains/intellij-community/blob/idea/253.20558.43/platform/external-system-impl/src/com/intellij/openapi/externalSystem/dependency/analyzer/DependencyAnalyzerViewImpl.kt */ class DependencyAnalyzerViewImpl( private val project: Project, private val systemId: ProjectSystemId, private val parentDisposable: Disposable @@ -324,8 +324,9 @@ class DependencyAnalyzerViewImpl( val externalProjectSelector = ExternalProjectSelector( externalProjectProperty, externalProjects, iconsProvider ).bindEnabled(!dependencyLoadingProperty) - val dataFilterField = SearchTextField(SEARCH_HISTORY_PROPERTY).apply { setPreferredWidth(JBUI.scale(240)) } - .apply { textEditor.bind(dependencyDataFilterProperty) }.bindEnabled(!dependencyLoadingProperty) + val dataFilterField = + SearchTextField(SEARCH_HISTORY_PROPERTY).apply { textEditor.bind(dependencyDataFilterProperty) } + .bindEnabled(!dependencyLoadingProperty) val scopeFilterSelector = SearchScopeSelector(dependencyScopeFilterProperty).bindEnabled(!dependencyLoadingProperty) val dependencyInspectionFilterButton = toggleAction(showDependencyWarningsProperty).apply { @@ -367,11 +368,11 @@ class DependencyAnalyzerViewImpl( .bindEnabled(!dependencyLoadingProperty) val dependencyPanel = cardPanel { ScrollPaneFactory.createScrollPane(if (it) dependencyTree else dependencyList, true) - }.bind(showDependencyTreeProperty) + }.bindSelected(showDependencyTreeProperty) val dependencyLoadingPanel = JBLoadingPanel(BorderLayout(), parentDisposable).apply { add(dependencyPanel, BorderLayout.CENTER) } .apply { setLoadingText(ExternalSystemBundle.message("external.system.dependency.analyzer.dependency.loading")) } - .bind(dependencyLoadingProperty) + .bindLoading(dependencyLoadingProperty) val showDependencyTreeButton = toggleAction(showDependencyTreeProperty).apply { templatePresentation.text = ExternalSystemBundle.message("external.system.dependency.analyzer.resolved.tree.show") diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerVirtualFile.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerVirtualFile.kt index 46ca7b3..02b38d0 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerVirtualFile.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/DependencyAnalyzerVirtualFile.kt @@ -5,7 +5,6 @@ import com.intellij.icons.AllIcons import com.intellij.ide.actions.SplitAction import com.intellij.ide.plugins.UIComponentFileEditor import com.intellij.ide.plugins.UIComponentVirtualFile -import com.intellij.ide.plugins.UIComponentVirtualFile.Content import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerView import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.util.ExternalSystemBundle @@ -13,7 +12,7 @@ import com.intellij.openapi.project.Project import com.intellij.util.containers.DisposableWrapperList /** - * https://github.com/JetBrains/intellij-community/blob/idea/233.11799.300/platform/external-system-impl/src/com/intellij/openapi/externalSystem/dependency/analyzer/DependencyAnalyzerVirtualFile.kt + * https://github.com/JetBrains/intellij-community/blob/idea/253.20558.43/platform/external-system-impl/src/com/intellij/openapi/externalSystem/dependency/analyzer/DependencyAnalyzerVirtualFile.kt */ internal class DependencyAnalyzerVirtualFile(private val project: Project, private val systemId: ProjectSystemId) : UIComponentVirtualFile( diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/package-info.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/package-info.kt index 635fb20..c538a49 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/package-info.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/package-info.kt @@ -1,6 +1,6 @@ package bitlap.sbt.analyzer.jbexternal /** - * NOTE: Kotlin code can only be referenced and cannot refer to code in src/main/scala - * The code for this package is almost copied from https://github.com/JetBrains/intellij-community + * NOTE: Kotlin code can only be referenced and cannot refer to code in src/main/scala. + * The code in this package is almost entirely copied from https://github.com/JetBrains/intellij-community. */ diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/DependencyUiUtil.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/DependencyUiUtil.kt index 4369d26..8e91653 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/DependencyUiUtil.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/DependencyUiUtil.kt @@ -1,14 +1,8 @@ // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package bitlap.sbt.analyzer.jbexternal.util -import javax.swing.JList -import javax.swing.JTree -import javax.swing.ListModel -import javax.swing.tree.DefaultMutableTreeNode -import javax.swing.tree.TreeModel import bitlap.sbt.analyzer.jbexternal.SbtDAArtifact - import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.DataSink import com.intellij.openapi.actionSystem.UiDataProvider @@ -28,6 +22,12 @@ import com.intellij.ui.components.JBList import com.intellij.ui.treeStructure.SimpleTree import com.intellij.util.ui.ListUiUtil import com.intellij.util.ui.tree.TreeUtil +import org.jetbrains.annotations.Nls +import javax.swing.JList +import javax.swing.JTree +import javax.swing.ListModel +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.TreeModel import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency as Dependency @@ -51,10 +51,8 @@ private fun SimpleColoredComponent.customizeCellRenderer( } val dataText = group.data.getDisplayText(showGroupId) append(dataText, if (group.isOmitted) GRAYED_ATTRIBUTES else REGULAR_ATTRIBUTES) - val scopes = group.variances.map { it.scope.name }.toSet() - val scopesText = scopes.singleOrNull() ?: ExternalSystemBundle.message( - "external.system.dependency.analyzer.scope.n", scopes.size - ) + val nScopesText = ExternalSystemBundle.message("external.system.dependency.analyzer.scope.n", group.scopes.size) + val scopesText = group.scopes.map { it.name }.singleOrNull() ?: nScopesText append(" ($scopesText)", GRAYED_ATTRIBUTES) if (showSize) { @@ -84,6 +82,30 @@ private fun SimpleColoredComponent.customizeCellRenderer( else -> return } } + + toolTipText = buildList { + val dataText = when (group.data) { + is Dependency.Data.Module -> ExternalSystemBundle.message("external.system.dependency.analyzer.tooltip.module") + is Dependency.Data.Artifact -> ExternalSystemBundle.message("external.system.dependency.analyzer.tooltip.artifact") + } + add(htmlParagraph(dataText + "\n" + htmlList(listOf(group.data.getDisplayText(true))))) + + val scopesText = ExternalSystemBundle.message("external.system.dependency.analyzer.tooltip.scopes") + add(htmlParagraph(scopesText + "\n" + htmlList(group.scopes.map { it.title }.toSet()))) + + if (group.status.isNotEmpty()) { + val statusText = ExternalSystemBundle.message("external.system.dependency.analyzer.tooltip.status") + add(htmlParagraph(statusText + "\n" + htmlList(group.status.map { it.title }.toSet()))) + } + }.joinToString("\n") +} + +private fun htmlList(elements: Iterable<@Nls String>): @NlsSafe String { + return "" +} + +private fun htmlParagraph(text: @Nls String): @NlsSafe String { + return "

\n$text\n

" } internal abstract class AbstractDependencyList( @@ -227,6 +249,7 @@ internal class DependencyGroup(val variances: List) { val data by lazy { dependency.data } val scopes by lazy { variances.map { it.scope }.toSet() } val parents by lazy { variances.map { it.parent }.toSet() } + val status by lazy { variances.flatMap { it.status } } val warnings by lazy { variances.flatMap { it.warnings } } val isOmitted by lazy { variances.all { it.isOmitted } } val hasWarnings by lazy { variances.any { it.hasWarnings } } @@ -243,4 +266,4 @@ internal class DependencyGroup(val variances: List) { internal val Dependency.hasWarnings: Boolean get() = warnings.isNotEmpty() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ExternalProjectUiUtil.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ExternalProjectUiUtil.kt index a833cde..65274f7 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ExternalProjectUiUtil.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ExternalProjectUiUtil.kt @@ -1,109 +1,130 @@ // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package bitlap.sbt.analyzer.jbexternal.util -import java.awt.Component -import javax.swing.* import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerProject import com.intellij.openapi.externalSystem.ui.ExternalSystemIconProvider import com.intellij.openapi.externalSystem.util.ExternalSystemBundle import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.util.bind -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.observable.util.whenItemSelected import com.intellij.openapi.observable.util.whenMousePressed +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.text.NaturalComparator import com.intellij.ui.ListUtil +import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.DropDownLink import com.intellij.ui.components.JBList -import com.intellij.util.ui.EmptyIcon +import com.intellij.ui.components.panels.ListLayout +import com.intellij.ui.speedSearch.ListWithFilter import com.intellij.util.ui.JBUI +import java.awt.Component +import javax.swing.* -@Suppress("DEPRECATION") internal class ExternalProjectSelector( property: ObservableMutableProperty, externalProjects: List, - iconProvider: ExternalSystemIconProvider + iconProvider: ExternalSystemIconProvider, ) : JPanel() { - private val projectIcon = if (iconProvider.projectIcon is EmptyIcon) { - PROJECT_ICON - } else { - iconProvider.projectIcon - } - init { - val dropDownLink = ExternalProjectDropDownLink(property, externalProjects).apply { - border = JBUI.Borders.empty(BORDER, ICON_TEXT_GAP / 2, BORDER, BORDER) - } - val label = JLabel(projectIcon).apply { border = JBUI.Borders.empty(BORDER, BORDER, BORDER, ICON_TEXT_GAP / 2) } - .apply { labelFor = dropDownLink } + val dropDownLink = ExternalProjectDropDownLink(property, externalProjects, iconProvider).apply { + border = JBUI.Borders.empty(BORDER, ICON_TEXT_GAP / 2, BORDER, BORDER) + } + val label = JLabel(iconProvider.projectIcon).apply { + border = JBUI.Borders.empty(BORDER, BORDER, BORDER, ICON_TEXT_GAP / 2) + }.apply { labelFor = dropDownLink } - layout = com.intellij.ide.plugins.newui.HorizontalLayout(0) + layout = ListLayout.horizontal(0) border = JBUI.Borders.empty() add(label) add(dropDownLink) } +} - private fun createPopup( - externalProjects: List, onChange: (DependencyAnalyzerProject) -> Unit - ): JBPopup { - val content = - ExternalProjectPopupContent(externalProjects).apply { whenMousePressed { onChange(selectedValue) } } - return JBPopupFactory.getInstance().createComponentPopupBuilder(content, null).createPopup() - .apply { content.whenMousePressed(listener = ::closeOk) } - } +private class ExternalProjectPopupContent( + externalProjects: List, + iconProvider: ExternalSystemIconProvider, +) { + + val component: JComponent + + val list: JBList - private inner class ExternalProjectPopupContent(externalProject: List) : - JBList() { - init { - model = createDefaultListModel(externalProject) + init { + val elements = externalProjects.sortedWith(Comparator.comparing({ it.title }, NaturalComparator.INSTANCE)) + list = JBList(elements).apply { border = emptyListBorder() - cellRenderer = ExternalProjectRenderer() + cellRenderer = ExternalProjectRenderer(iconProvider) selectionMode = ListSelectionModel.SINGLE_SELECTION ListUtil.installAutoSelectOnMouseMove(this) - setupListPopupPreferredWidth(this) } + component = ListWithFilter.wrap( + list, ScrollPaneFactory.createScrollPane(list), { it.title }, false, false, true + ) } - private inner class ExternalProjectRenderer : ListCellRenderer { - override fun getListCellRendererComponent( - list: JList, - value: DependencyAnalyzerProject?, - index: Int, - isSelected: Boolean, - cellHasFocus: Boolean - ): Component { - return JLabel().apply { if (value != null) icon = projectIcon } - .apply { if (value != null) text = value.title }.apply { border = emptyListCellBorder(list, index) } - .apply { iconTextGap = JBUI.scale(ICON_TEXT_GAP) } - .apply { background = if (isSelected) list.selectionBackground else list.background } - .apply { foreground = if (isSelected) list.selectionForeground else list.foreground } - .apply { isOpaque = true }.apply { isEnabled = list.isEnabled }.apply { font = list.font } + fun afterChange(listener: (DependencyAnalyzerProject) -> Unit) { + list.whenMousePressed { + listener(list.selectedValue) } } +} - private inner class ExternalProjectDropDownLink( - property: ObservableMutableProperty, - externalProjects: List, - ) : DropDownLink(property.get(), - { createPopup(externalProjects, it::selectedItem.setter) }) { - override fun popupPoint() = super.popupPoint().apply { x += insets.left }.apply { x -= JBUI.scale(BORDER) } - .apply { x -= projectIcon.iconWidth }.apply { x -= JBUI.scale(ICON_TEXT_GAP) } - - override fun itemToString(item: DependencyAnalyzerProject?): String = when (item) { - null -> ExternalSystemBundle.message("external.system.dependency.analyzer.projects.empty") - else -> item.title - } +private class ExternalProjectRenderer( + private val iconProvider: ExternalSystemIconProvider, +) : ListCellRenderer { - init { - autoHideOnDisable = false - foreground = JBUI.CurrentTheme.Label.foreground() - whenItemSelected { text = itemToString(selectedItem) } - bind(property) - } + override fun getListCellRendererComponent( + list: JList, + value: DependencyAnalyzerProject?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean, + ): Component { + return JLabel().apply { if (value != null) icon = iconProvider.projectIcon } + .apply { if (value != null) text = value.title }.apply { border = emptyListCellBorder(list, index) } + .apply { iconTextGap = JBUI.scale(ICON_TEXT_GAP) } + .apply { background = if (isSelected) list.selectionBackground else list.background } + .apply { foreground = if (isSelected) list.selectionForeground else list.foreground } + .apply { isOpaque = true }.apply { isEnabled = list.isEnabled }.apply { font = list.font } } } +private class ExternalProjectDropDownLink( + property: ObservableMutableProperty, + externalProjects: List, + private val iconProvider: ExternalSystemIconProvider, +) : DropDownLink( + property.get(), { createPopup(externalProjects, iconProvider, it::selectedItem.setter) }) { + override fun popupPoint() = super.popupPoint().apply { x += insets.left }.apply { x -= JBUI.scale(BORDER) } + .apply { x -= iconProvider.projectIcon.iconWidth }.apply { x -= JBUI.scale(ICON_TEXT_GAP) } + + override fun itemToString(item: DependencyAnalyzerProject?): String = when (item) { + null -> ExternalSystemBundle.message("external.system.dependency.analyzer.projects.empty") + else -> item.title + } + init { + autoHideOnDisable = false + foreground = JBUI.CurrentTheme.Label.foreground() + whenItemSelected { text = itemToString(selectedItem) } + bind(property) + } + + companion object { + + fun createPopup( + externalProjects: List, + iconProvider: ExternalSystemIconProvider, + onChange: (DependencyAnalyzerProject) -> Unit + ): JBPopup { + val content = ExternalProjectPopupContent(externalProjects, iconProvider) + content.afterChange(onChange) + return JBPopupFactory.getInstance().createComponentPopupBuilder(content.component, null).setResizable(true) + .setRequestFocus(true).createPopup().apply { content.list.whenMousePressed(listener = ::closeOk) } + } + } +} diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ScopeUiUtil.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ScopeUiUtil.kt index 4afe821..a245ec0 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ScopeUiUtil.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/ScopeUiUtil.kt @@ -1,13 +1,6 @@ // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package bitlap.sbt.analyzer.jbexternal.util -import java.awt.Component -import javax.swing.JCheckBox -import javax.swing.JLabel -import javax.swing.JList -import javax.swing.JPanel -import javax.swing.ListCellRenderer -import javax.swing.ListSelectionModel import com.intellij.ide.nls.NlsMessages import com.intellij.openapi.externalSystem.dependency.analyzer.DependencyAnalyzerDependency.Scope @@ -21,14 +14,23 @@ import com.intellij.openapi.observable.util.whenMousePressed import com.intellij.openapi.ui.popup.JBPopup import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.util.text.NaturalComparator +import com.intellij.openapi.util.text.StringUtil import com.intellij.ui.ListUtil +import com.intellij.ui.ScrollPaneFactory import com.intellij.ui.components.DropDownLink import com.intellij.ui.components.JBList +import com.intellij.ui.components.panels.ListLayout +import com.intellij.ui.speedSearch.ListWithFilter +import com.intellij.util.containers.ContainerUtil import com.intellij.util.ui.JBUI import com.intellij.util.ui.ThreeStateCheckBox +import org.jetbrains.annotations.Nls +import java.awt.Component +import javax.swing.* -@Suppress("DEPRECATION") internal class SearchScopeSelector(property: ObservableMutableProperty>) : JPanel() { + init { val dropDownLink = SearchScopeDropDownLink(property).apply { border = JBUI.Borders.empty(BORDER, ICON_TEXT_GAP / 2, BORDER, BORDER) @@ -37,94 +39,149 @@ internal class SearchScopeSelector(property: ObservableMutableProperty) : JBList() { +private class SearchScopePopupContent(scopes: List) { - private val propertyGraph = PropertyGraph(isBlockPropagation = false) - private val anyScopeProperty = propertyGraph.lazyProperty(::suggestAnyScopeState) - private val scopeProperties = - scopes.map { ScopeProperty.Just(it.scope, propertyGraph.lazyProperty { it.isSelected }) } + val component: JComponent - private fun suggestAnyScopeState(): ThreeStateCheckBox.State { - return when { - scopeProperties.all { it.property.get() } -> ThreeStateCheckBox.State.SELECTED - !scopeProperties.any { it.property.get() } -> ThreeStateCheckBox.State.NOT_SELECTED - else -> ThreeStateCheckBox.State.DONT_CARE + private val allScopes: List + + private val standardGroup: SearchScopeItem.Group + private val standardScopes: List + + private val customGroup: SearchScopeItem.Group + private val customScopes: List + + init { + val propertyGraph = PropertyGraph(isBlockPropagation = false) + + allScopes = + scopes.sortedWith(Comparator.comparing({ it.scope.title }, NaturalComparator.INSTANCE)).map { scope -> + SearchScopeItem.Element( + scope.scope, propertyGraph.property(scope.isSelected) + ) + } + standardScopes = allScopes.filter { it.scope.type == Scope.Type.STANDARD } + customScopes = allScopes.filter { it.scope.type == Scope.Type.CUSTOM } + + standardGroup = SearchScopeItem.Group( + when (standardScopes.size == allScopes.size) { + true -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any") + else -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.standard") + }, propertyGraph.lazyProperty { suggestGroupState(standardScopes) }) + customGroup = SearchScopeItem.Group( + when (customScopes.size == allScopes.size) { + true -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any") + else -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.custom") + }, propertyGraph.lazyProperty { suggestGroupState(customScopes) }) + + propertyGraph.afterPropagation { + component.repaint() } + initProperties(standardGroup, standardScopes) + initProperties(customGroup, customScopes) } - private fun suggestScopeState(currentState: Boolean): Boolean { - return when (anyScopeProperty.get()) { - ThreeStateCheckBox.State.SELECTED -> true - ThreeStateCheckBox.State.NOT_SELECTED -> false - ThreeStateCheckBox.State.DONT_CARE -> currentState + init { + val items = ContainerUtil.concat( + getItems(standardGroup, standardScopes), getItems(customGroup, customScopes) + ) + val list = JBList(items).apply { + border = emptyListBorder() + cellRenderer = SearchScopeRenderer() + selectionMode = ListSelectionModel.SINGLE_SELECTION + ListUtil.installAutoSelectOnMouseMove(this) + } + list.whenMousePressed { + when (val scope = list.selectedValue) { + is SearchScopeItem.Group -> scope.property.set( + ThreeStateCheckBox.nextState( + scope.property.get(), false + ) + ) + + is SearchScopeItem.Element -> scope.property.set(!scope.property.get()) + } } + component = ListWithFilter.wrap( + list, ScrollPaneFactory.createScrollPane(list), { it.title }, false, false, true + ) } fun afterChange(listener: (List) -> Unit) { - for (scope in scopeProperties) { + for (scope in allScopes) { scope.property.afterChange { - listener(scopeProperties.map { ScopeItem(it.scope, it.property.get()) }) + listener(allScopes.map { ScopeItem(it.scope, it.property.get()) }) } } } - init { - val anyScope = ScopeProperty.Any(anyScopeProperty) - model = createDefaultListModel(listOf(anyScope) + scopeProperties) - border = emptyListBorder() - cellRenderer = SearchScopePropertyRenderer() - selectionMode = ListSelectionModel.SINGLE_SELECTION - ListUtil.installAutoSelectOnMouseMove(this) - setupListPopupPreferredWidth(this) - whenMousePressed { - when (val scope = selectedValue) { - is ScopeProperty.Any -> scope.property.set(ThreeStateCheckBox.nextState(scope.property.get(), false)) - is ScopeProperty.Just -> scope.property.set(!scope.property.get()) + companion object { + + private fun suggestGroupState(scopes: List): ThreeStateCheckBox.State { + return when { + scopes.all { it.property.get() } -> ThreeStateCheckBox.State.SELECTED + !scopes.any { it.property.get() } -> ThreeStateCheckBox.State.NOT_SELECTED + else -> ThreeStateCheckBox.State.DONT_CARE } } - propertyGraph.afterPropagation { - repaint() - } - for (scope in scopeProperties) { - anyScopeProperty.dependsOn(scope.property) { - suggestAnyScopeState() + private fun suggestScopeState(group: SearchScopeItem.Group, scope: SearchScopeItem.Element): Boolean { + return when (group.property.get()) { + ThreeStateCheckBox.State.SELECTED -> true + ThreeStateCheckBox.State.NOT_SELECTED -> false + ThreeStateCheckBox.State.DONT_CARE -> scope.property.get() } - scope.property.dependsOn(anyScopeProperty) { - suggestScopeState(scope.property.get()) + } + + private fun initProperties(group: SearchScopeItem.Group, scopes: List) { + for (scope in scopes) { + group.property.dependsOn(scope.property) { + suggestGroupState(scopes) + } + scope.property.dependsOn(group.property) { + suggestScopeState(group, scope) + } } } - } - companion object { - fun createPopup(scopes: List, onChange: (List) -> Unit): JBPopup { - val content = SearchScopePopupContent(scopes) - content.afterChange(onChange) - return JBPopupFactory.getInstance().createComponentPopupBuilder(content, null).createPopup() + private fun getItems( + group: SearchScopeItem.Group, scopes: List + ): List { + if (scopes.isEmpty()) { + return emptyList() + } + return ContainerUtil.concat(listOf(group), scopes) } } } -private class SearchScopePropertyRenderer : ListCellRenderer { +private class SearchScopeRenderer : ListCellRenderer { + override fun getListCellRendererComponent( - list: JList, value: ScopeProperty, index: Int, isSelected: Boolean, cellHasFocus: Boolean + list: JList, + value: SearchScopeItem, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean, ): Component { val checkBox = when (value) { - is ScopeProperty.Any -> ThreeStateCheckBox(ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any")).apply { - isThirdStateEnabled = false - }.apply { state = value.property.get() }.bind(value.property) - - is ScopeProperty.Just -> JCheckBox(value.scope.title).apply { this@apply.isSelected = value.property.get() } + is SearchScopeItem.Group -> ThreeStateCheckBox(value.title).apply { isThirdStateEnabled = false } .bind(value.property) + + is SearchScopeItem.Element -> JCheckBox(value.title).bind(value.property) } - return checkBox.apply { border = emptyListCellBorder(list, index, if (index > 0) 1 else 0) } + val indent = when (value) { + is SearchScopeItem.Group -> 0 + is SearchScopeItem.Element -> 1 + } + return checkBox.apply { border = emptyListCellBorder(list, index, indent) } .apply { background = if (isSelected) list.selectionBackground else list.background } .apply { foreground = if (isSelected) list.selectionForeground else list.foreground } .apply { isOpaque = true }.apply { isEnabled = list.isEnabled }.apply { font = list.font } @@ -134,18 +191,32 @@ private class SearchScopePropertyRenderer : ListCellRenderer { private class SearchScopeDropDownLink( property: ObservableMutableProperty> ) : DropDownLink>( - property.get(), - { SearchScopePopupContent.createPopup(property.get(), it::selectedItem.setter) }) { + property.get(), { createPopup(property.get(), it::selectedItem.setter) }) { + override fun popupPoint() = super.popupPoint().apply { x += insets.left } override fun itemToString(item: List): @NlsSafe String { + val selectedScopes = item.filter { it.isSelected } + val standardScopes = item.filter { it.scope.type == Scope.Type.STANDARD } + val selectedStandardScopes = standardScopes.filter { it.isSelected } + val customScopes = item.filter { it.scope.type == Scope.Type.CUSTOM } + val selectedCustomScopes = customScopes.filter { it.isSelected } return when { - item.all { it.isSelected } -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any") - !item.any { it.isSelected } -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.none") - else -> { - val scopes = item.filter { it.isSelected }.map { it.scope.title } - abbreviate(NlsMessages.formatNarrowAndList(scopes), 30) - } + selectedScopes.isEmpty() -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.none") + + selectedScopes.size == item.size -> ExternalSystemBundle.message("external.system.dependency.analyzer.scope.any") + + selectedScopes.size == standardScopes.size && selectedScopes.size == selectedStandardScopes.size -> ExternalSystemBundle.message( + "external.system.dependency.analyzer.scope.standard" + ) + + selectedScopes.size == customScopes.size && selectedScopes.size == selectedCustomScopes.size -> ExternalSystemBundle.message( + "external.system.dependency.analyzer.scope.custom" + ) + + else -> StringUtil.shortenPathWithEllipsis( + NlsMessages.formatNarrowAndList(selectedScopes.map { it.scope.title }), 30, true + ) } } @@ -155,6 +226,16 @@ private class SearchScopeDropDownLink( whenItemSelected { text = itemToString(selectedItem) } bind(property) } + + companion object { + + fun createPopup(scopes: List, onChange: (List) -> Unit): JBPopup { + val content = SearchScopePopupContent(scopes) + content.afterChange(onChange) + return JBPopupFactory.getInstance().createComponentPopupBuilder(content.component, null).setResizable(true) + .setRequestFocus(true).createPopup() + } + } } internal class ScopeItem( @@ -163,7 +244,19 @@ internal class ScopeItem( override fun toString() = "$isSelected: $scope" } -private sealed interface ScopeProperty { - class Any(val property: GraphProperty) : ScopeProperty - class Just(val scope: Scope, val property: GraphProperty) : ScopeProperty +private sealed interface SearchScopeItem { + + val title: @Nls(capitalization = Nls.Capitalization.Title) String + + class Group( + override val title: @Nls(capitalization = Nls.Capitalization.Title) String, + val property: GraphProperty, + ) : SearchScopeItem + + class Element( + val scope: Scope, + val property: GraphProperty, + ) : SearchScopeItem { + override val title: @Nls(capitalization = Nls.Capitalization.Title) String by scope::title + } } \ No newline at end of file diff --git a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/UiUtils.kt b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/UiUtils.kt index 474a377..c069d90 100644 --- a/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/UiUtils.kt +++ b/src/main/kotlin/bitlap/sbt/analyzer/jbexternal/util/UiUtils.kt @@ -1,54 +1,34 @@ // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package bitlap.sbt.analyzer.jbexternal.util -import java.awt.BorderLayout -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.JList -import javax.swing.JPanel -import javax.swing.JTree -import javax.swing.Icon -import javax.swing.border.Border - -import bitlap.sbt.analyzer.jbexternal.DependencyAnalyzerManager import com.intellij.icons.AllIcons -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.ActionToolbar -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.actionSystem.ToggleAction +import com.intellij.openapi.actionSystem.* import com.intellij.openapi.actionSystem.impl.ActionButton import com.intellij.openapi.externalSystem.util.ExternalSystemBundle -import com.intellij.openapi.observable.properties.ObservableBooleanProperty import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableProperty import com.intellij.openapi.observable.util.bind import com.intellij.openapi.project.DumbAware import com.intellij.openapi.ui.SimpleToolWindowPanel -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.IconLoader import com.intellij.ui.CardLayoutPanel import com.intellij.ui.OnePixelSplitter -import com.intellij.ui.components.JBLoadingPanel +import com.intellij.ui.components.panels.ListLayout import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import com.intellij.util.ui.tree.TreeUtil +import org.jetbrains.annotations.Nls +import org.jetbrains.annotations.NonNls +import java.awt.BorderLayout +import javax.swing.* +import javax.swing.border.Border + -internal val PROJECT_ICON: Icon = - IconLoader.getIcon("/icons/sbt_dependency_analyzer.svg", DependencyAnalyzerManager::class.java) internal const val BORDER = 6 internal const val INDENT = 16 internal const val ICON_TEXT_GAP = 4 internal const val ACTION_BORDER = 2 -// import com.intellij.openapi.observable.util.whenDisposed -fun Disposable.whenDisposed(listener: () -> Unit) { - Disposer.register(this, Disposable { listener() }) -} - internal fun emptyListBorder(): Border { return JBUI.Borders.empty() } @@ -61,53 +41,30 @@ internal fun emptyListCellBorder(list: JList<*>, index: Int, indent: Int = 0): B return JBUI.Borders.empty(topGap, leftGap, bottomGap, rightGap) } -internal fun setupListPopupPreferredWidth(list: JList<*>) { - list.setPreferredWidth(maxOf(JBUI.scale(164), list.preferredSize.width)) -} +internal fun label(text: @Nls String) = JLabel(text).apply { border = JBUI.Borders.empty(BORDER) } -internal fun JComponent.setPreferredWidth(width: Int) { - preferredSize = preferredSize.also { it.width = width } -} - -internal fun label(text: String) = JLabel(text).apply { border = JBUI.Borders.empty(BORDER) } - -internal fun label(property: ObservableProperty) = label(property.get()).bind(property) +internal fun label(property: ObservableProperty<@Nls String>) = label(property.get()).bind(property) internal fun toolWindowPanel(configure: SimpleToolWindowPanel.() -> Unit) = SimpleToolWindowPanel(true, true).apply { configure() } internal fun toolbarPanel(configure: BorderLayoutPanel.() -> Unit) = BorderLayoutPanel().apply { layout = BorderLayout() }.apply { border = JBUI.Borders.empty(1, 2) } - .apply { withMinimumHeight(JBUI.scale(30)) }.apply { withPreferredHeight(JBUI.scale(30)) }.apply { configure() } + .apply { configure() } -@Suppress("DEPRECATION") internal fun horizontalPanel(vararg components: JComponent) = - JPanel().apply { layout = com.intellij.ide.plugins.newui.HorizontalLayout(0) } - .apply { border = JBUI.Borders.empty() }.apply { components.forEach(::add) } + JPanel().apply { layout = ListLayout.horizontal(0) }.apply { border = JBUI.Borders.empty() } + .apply { components.forEach(::add) } -internal fun horizontalSplitPanel(proportionKey: String, proportion: Float, configure: OnePixelSplitter.() -> Unit) = - OnePixelSplitter(false, proportionKey, proportion).apply { configure() } +internal fun horizontalSplitPanel( + proportionKey: @NonNls String, proportion: Float, configure: OnePixelSplitter.() -> Unit +) = OnePixelSplitter(false, proportionKey, proportion).apply { configure() } internal fun cardPanel(createPanel: (T) -> JComponent) = object : CardLayoutPanel() { override fun prepare(key: T) = key override fun create(ui: T) = createPanel(ui) } -internal fun > C.bind(property: ObservableProperty): C = apply { - select(property.get(), true) - property.afterChange { select(it, true) } -} - -internal fun C.bind(property: ObservableBooleanProperty): C = apply { - if (property.get()) { - startLoading() - } else { - stopLoading() - } - property.afterSet { startLoading() } - property.afterReset { stopLoading() } -} - internal fun toggleAction(property: ObservableMutableProperty): ToggleAction = object : ToggleAction(), DumbAware { override fun isSelected(e: AnActionEvent) = property.get() @@ -136,4 +93,4 @@ internal fun expandTreeAction(tree: JTree) = action { TreeUtil.expandAll(tree) } internal fun collapseTreeAction(tree: JTree) = action { TreeUtil.collapseAll(tree, 0) }.apply { templatePresentation.text = ExternalSystemBundle.message("external.system.dependency.analyzer.resolved.tree.collapse") -}.apply { templatePresentation.icon = AllIcons.Actions.Collapseall } +}.apply { templatePresentation.icon = AllIcons.Actions.Collapseall } \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 7bdef6d..92231eb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -3,12 +3,12 @@ org.bitlap.sbtDependencyAnalyzer Sbt Dependency Analyzer - 0.8.0-252.25557.131 + 0.9.0-RC1 Bitlap - + com.intellij.modules.platform com.intellij.modules.lang @@ -138,6 +138,11 @@ 0.9.0-RC1 +
    +
  • Support IDEA 253.x (build version 253.20558.43) .
  • +
  • Support scrolling modules .
  • +

0.8.2-252.25557.131

  • Support sbt 1.11+ .
  • diff --git a/src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerContributor.scala b/src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerContributor.scala index 2d6fbac..9263005 100644 --- a/src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerContributor.scala +++ b/src/main/scala/bitlap/sbt/analyzer/SbtDependencyAnalyzerContributor.scala @@ -332,8 +332,9 @@ object SbtDependencyAnalyzerContributor def getStatus(usage: Dependency, data: Dependency.Data): JList[Dependency.Status] = val status = ListBuffer[Dependency.Status]() if (node.getResolutionState == ResolutionState.UNRESOLVED) { + val title = ExternalSystemBundle.message("external.system.dependency.analyzer.warning.unresolved.title") val message = ExternalSystemBundle.message("external.system.dependency.analyzer.warning.unresolved") - status.append(DAWarning(message)) + status.append(DAWarning(title, message)) } val selectionReason = node.getSelectionReason data match @@ -347,11 +348,15 @@ object SbtDependencyAnalyzerContributor case _ => null if (conflictedVersion != null) { + val title = ExternalSystemBundle.message( + "external.system.dependency.analyzer.warning.version.conflict.title", + conflictedVersion + ) val message = ExternalSystemBundle.message( "external.system.dependency.analyzer.warning.version.conflict", conflictedVersion ) - status.append(DAWarning(message)) + status.append(DAWarning(title, message)) } case _ => status.asJava diff --git a/src/main/scala/bitlap/sbt/analyzer/util/packagesearch/AddDependencyPreviewWizard.scala b/src/main/scala/bitlap/sbt/analyzer/util/packagesearch/AddDependencyPreviewWizard.scala index dee7b1c..0a88113 100644 --- a/src/main/scala/bitlap/sbt/analyzer/util/packagesearch/AddDependencyPreviewWizard.scala +++ b/src/main/scala/bitlap/sbt/analyzer/util/packagesearch/AddDependencyPreviewWizard.scala @@ -26,8 +26,6 @@ class AddDependencyPreviewWizard( val elementToAdd: Any = elem var resultFileLine: Option[DependencyOrRepositoryPlaceInfo] = None - override def getHelpID: String = null - def search(): Option[DependencyOrRepositoryPlaceInfo] = { if (!showAndGet()) { return None From 775c02cb96d81de34a147a1c6ef0a6545718e6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=A6=E5=A2=83=E8=BF=B7=E7=A6=BB?= Date: Wed, 24 Sep 2025 23:02:11 +0800 Subject: [PATCH 2/2] fix --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d7c2cc8..4e54b83 100644 --- a/build.sbt +++ b/build.sbt @@ -73,7 +73,8 @@ lazy val `sbt-dependency-analyzer` = (project in file(".")) xmx = 2048, xms = 256, defaultOptions = intellijVMOptions.value.defaultOptions ++ Seq( - "--add-exports=java.management/sun.management=ALL-UNNAMED" + "--add-exports=java.management/sun.management=ALL-UNNAMED", + "--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED" ) ), Compile / unmanagedResourceDirectories += baseDirectory.value / "src" / "main" / "resources",