diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml
index a54e831d421a..130c611bccc4 100644
--- a/python/pluginCore/resources/META-INF/plugin.xml
+++ b/python/pluginCore/resources/META-INF/plugin.xml
@@ -742,7 +742,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
dynamic="true"/>
{1}
python.toolwindow.packages.install.link=Install
python.toolwindow.packages.search.text.placeholder=Search for more packages
python.toolwindow.packages.description.panel.placeholder=Select a package to view documentation
diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
index 4649f05352b1..f5c6400646aa 100644
--- a/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
+++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackagingToolwindowActionProvider.kt
@@ -4,9 +4,9 @@ package com.jetbrains.python.packaging.conda
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider
-import com.jetbrains.python.packaging.toolwindow.PythonPackageInstallAction
-import com.jetbrains.python.packaging.toolwindow.SimplePythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.actions.PythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.actions.PythonPackagingToolwindowActionProvider
+import com.jetbrains.python.packaging.toolwindow.actions.SimplePythonPackageInstallAction
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
diff --git a/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt b/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt
index 0ada465443eb..59f47e555db0 100644
--- a/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt
+++ b/python/src/com/jetbrains/python/packaging/pip/PipPackagingToolwindowActionProvider.kt
@@ -5,9 +5,9 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import com.jetbrains.python.packaging.management.PythonPackageManager
-import com.jetbrains.python.packaging.toolwindow.PythonPackageInstallAction
-import com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider
-import com.jetbrains.python.packaging.toolwindow.SimplePythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.actions.PythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.actions.PythonPackagingToolwindowActionProvider
+import com.jetbrains.python.packaging.toolwindow.actions.SimplePythonPackageInstallAction
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt
index acc37be34ec8..cd66de9bfaa9 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingTablesView.kt
@@ -6,6 +6,11 @@ import com.intellij.openapi.project.Project
import com.intellij.ui.JBColor
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.packaging.toolwindow.model.*
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTable
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTableModel
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagingTableGroup
import java.awt.Rectangle
import javax.swing.JLabel
import javax.swing.JPanel
@@ -99,7 +104,7 @@ class PyPackagingTablesView(private val project: Project,
foreground = JBColor.RED
icon = AllIcons.General.Error
}
- it to headerPanel(label, null)
+ it to PyPackagesUiComponents.headerPanel(label, null)
}
.forEach {
invalidRepositories[it.first] = it.second
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt
index c239882dd00a..fd18a33e323e 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowPanel.kt
@@ -46,10 +46,19 @@ import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.common.PythonLocalPackageSpecification
import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonVcsPackageSpecification
+import com.jetbrains.python.packaging.toolwindow.actions.PythonPackageInstallAction
+import com.jetbrains.python.packaging.toolwindow.actions.PythonPackagingToolwindowActionProvider
+import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
+import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
+import com.jetbrains.python.packaging.toolwindow.model.PyPackagesViewData
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import com.jetbrains.python.sdk.pythonSdk
-import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.awt.BorderLayout
-import java.awt.Component
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.event.ActionEvent
@@ -60,7 +69,7 @@ import javax.swing.event.DocumentEvent
import javax.swing.event.ListSelectionListener
class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolWindow) : SimpleToolWindowPanel(false, true), Disposable {
- internal val packagingScope = CoroutineScope(Dispatchers.IO)
+ internal val packagingScope = PyPackageCoroutine.getIoScope(project)
private var selectedPackage: DisplayablePackage? = null
private var selectedPackageDetails: PythonPackageDetails? = null
@@ -172,20 +181,20 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
tablesView = PyPackagingTablesView(project, packageListPanel, this)
leftPanel = createLeftPanel(service)
- rightPanel = borderPanel {
- add(borderPanel {
+ rightPanel = PyPackagesUiComponents.borderPanel {
+ add(PyPackagesUiComponents.borderPanel {
border = SideBorder(JBColor.GRAY, SideBorder.BOTTOM)
preferredSize = Dimension(preferredSize.width, 50)
minimumSize = Dimension(minimumSize.width, 50)
maximumSize = Dimension(maximumSize.width, 50)
- add(boxPanel {
+ add(PyPackagesUiComponents.boxPanel {
add(Box.createHorizontalStrut(10))
add(packageNameLabel)
add(Box.createHorizontalStrut(10))
add(documentationLink)
}, BorderLayout.WEST)
- add(boxPanel {
- alignmentX = Component.RIGHT_ALIGNMENT
+ add(PyPackagesUiComponents.boxPanel {
+ alignmentX = RIGHT_ALIGNMENT
add(progressBar)
add(versionSelector)
add(versionLabel)
@@ -266,8 +275,8 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
}
}
- mainPanel = borderPanel {
- val topToolbar = boxPanel {
+ mainPanel = PyPackagesUiComponents.borderPanel {
+ val topToolbar = PyPackagesUiComponents.boxPanel {
border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM)
preferredSize = Dimension(preferredSize.width, 30)
minimumSize = Dimension(minimumSize.width, 30)
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
index 7e6524b98836..1ca45e6e346e 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/PyPackagingToolWindowService.kt
@@ -36,6 +36,7 @@ import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.repository.*
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector
+import com.jetbrains.python.packaging.toolwindow.model.*
import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.applyHelperPackageToPythonPath
import com.jetbrains.python.run.buildTargetedCommandLine
@@ -197,29 +198,27 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
}
private suspend fun invokeUpdateLatestVersion() {
- serviceScope.launch(Dispatchers.Default) {
- val proccessPackages = installedPackages
- val updatedPackages = proccessPackages.map { (name, pyPackage: InstalledPackage) ->
- val specification = pyPackage.repository.createPackageSpecification(pyPackage.name)
- val latestVersion = manager.repositoryManager.getLatestVersion(specification)
- val currentVersion = PyPackageVersionNormalizer.normalize(pyPackage.instance.version)
+ val proccessPackages = installedPackages
+ val updatedPackages = proccessPackages.map { (name, pyPackage: InstalledPackage) ->
+ val specification = pyPackage.repository.createPackageSpecification(pyPackage.name)
+ val latestVersion = manager.repositoryManager.getLatestVersion(specification)
+ val currentVersion = PyPackageVersionNormalizer.normalize(pyPackage.instance.version)
- val upgradeTo = if (latestVersion != null && currentVersion != null &&
- PyPackageVersionComparator.compare(latestVersion, currentVersion) > 0) {
- latestVersion
- }
- else {
- null
- }
- name to upgradeTo
- }.toMap()
+ val upgradeTo = if (latestVersion != null && currentVersion != null &&
+ PyPackageVersionComparator.compare(latestVersion, currentVersion) > 0) {
+ latestVersion
+ }
+ else {
+ null
+ }
+ name to upgradeTo
+ }.toMap()
- installedPackages = installedPackages.map {
- val newVersion = updatedPackages[it.key]
- it.key to it.value.withNextVersion(newVersion)
- }.toMap()
- handleSearch(currentQuery)
- }
+ installedPackages = installedPackages.map {
+ val newVersion = updatedPackages[it.key]
+ it.key to it.value.withNextVersion(newVersion)
+ }.toMap()
+ handleSearch(currentQuery)
}
private suspend fun showPackagingNotification(text: @Nls String) {
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions.kt
deleted file mode 100644
index f9c124a1da20..000000000000
--- a/python/src/com/jetbrains/python/packaging/toolwindow/actions.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
-package com.jetbrains.python.packaging.toolwindow
-
-import com.intellij.openapi.components.service
-import com.intellij.openapi.extensions.ExtensionPointName
-import com.intellij.openapi.project.Project
-import com.jetbrains.python.packaging.common.PythonPackageDetails
-import com.jetbrains.python.packaging.common.PythonPackageSpecification
-import com.jetbrains.python.packaging.management.PythonPackageManager
-import org.jetbrains.annotations.Nls
-
-interface PythonPackagingToolwindowActionProvider {
- fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List?
-
- companion object {
- val EP_NAME = ExtensionPointName.create("Pythonid.PythonPackagingToolwindowActionProvider")
- }
-}
-
-abstract class PythonPackageInstallAction(internal val text: @Nls String,
- internal val project: Project) {
-
- abstract suspend fun installPackage(specification: PythonPackageSpecification)
-}
-
-class SimplePythonPackageInstallAction(text: @Nls String,
- project: Project) : PythonPackageInstallAction(text, project) {
- override suspend fun installPackage(specification: PythonPackageSpecification) {
- project.service().installPackage(specification)
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt
new file mode 100644
index 000000000000..add9d01a9546
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/ChangeVersionPackageAction.kt
@@ -0,0 +1,42 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.ui.awt.RelativePoint
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTable
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.awt.event.MouseEvent
+
+internal class ChangeVersionPackageAction(val table: PyPackagesTable<*>) : DumbAwareAction(PyBundle.message("python.toolwindow.packages.update.package")) {
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ val pkg = table.selectedItem() as? InstalledPackage ?: return
+
+ PyPackageCoroutine.getIoScope(project).launch {
+ val service = project.service()
+ val details = service.detailsForPackage(pkg)
+ withContext(Dispatchers.EDT) {
+ PyPackagesUiComponents.createAvailableVersionsPopup(pkg, details, project, table.controller).show(
+ RelativePoint(e.inputEvent as MouseEvent))
+ }
+ }
+ }
+
+ override fun update(e: AnActionEvent) {
+ val pkg = table.selectedItem() as? InstalledPackage
+ e.presentation.isEnabledAndVisible = pkg != null
+ }
+
+ override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt
new file mode 100644
index 000000000000..6d30ce0432d2
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/DeletePackageAction.kt
@@ -0,0 +1,32 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.DumbAwareAction
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTable
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
+import kotlinx.coroutines.Dispatchers
+
+internal class DeletePackageAction(val table: PyPackagesTable<*>) : DumbAwareAction(PyBundle.message("python.toolwindow.packages.delete.package")) {
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ val pkg = table.selectedItem() as? InstalledPackage ?: return
+
+ val service = project.service()
+
+ PyPackageCoroutine.launch(project, Dispatchers.IO) {
+ service.deletePackage(pkg)
+ }
+ }
+
+ override fun update(e: AnActionEvent) {
+ e.presentation.isEnabledAndVisible = table.selectedItem() is InstalledPackage
+ }
+
+ override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt
new file mode 100644
index 000000000000..29a299fbe898
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/InstallPackageAction.kt
@@ -0,0 +1,39 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.ui.awt.RelativePoint
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTable
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.awt.event.MouseEvent
+
+internal class InstallPackageAction(val table: PyPackagesTable<*>) : DumbAwareAction(PyBundle.message("python.toolwindow.packages.install.link")) {
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ val service = project.service()
+ val pkg = table.selectedItem() as? InstallablePackage ?: return
+
+ PyPackageCoroutine.launch(project, Dispatchers.IO) {
+ val details = service.detailsForPackage(pkg)
+ withContext(Dispatchers.Main) {
+ val popup = PyPackagesUiComponents.createAvailableVersionsPopup(pkg, details, project, table.controller)
+ popup.show(RelativePoint(e.inputEvent as MouseEvent))
+ }
+ }
+ }
+
+ override fun update(e: AnActionEvent) {
+ e.presentation.isEnabledAndVisible = table.selectedItem() is InstallablePackage
+ }
+
+ override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/PythonPackageInstallAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PythonPackageInstallAction.kt
new file mode 100644
index 000000000000..793c28274b78
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PythonPackageInstallAction.kt
@@ -0,0 +1,14 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.project.Project
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import org.jetbrains.annotations.Nls
+
+abstract class PythonPackageInstallAction(
+ internal val text: @Nls String,
+ internal val project: Project,
+) {
+
+ abstract suspend fun installPackage(specification: PythonPackageSpecification)
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/PythonPackagingToolwindowActionProvider.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PythonPackagingToolwindowActionProvider.kt
new file mode 100644
index 000000000000..b96686bfd745
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/PythonPackagingToolwindowActionProvider.kt
@@ -0,0 +1,14 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.management.PythonPackageManager
+
+interface PythonPackagingToolwindowActionProvider {
+ fun getInstallActions(details: PythonPackageDetails, packageManager: PythonPackageManager): List?
+
+ companion object {
+ val EP_NAME = ExtensionPointName.create("Pythonid.PythonPackagingToolwindowActionProvider")
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/SimplePythonPackageInstallAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/SimplePythonPackageInstallAction.kt
new file mode 100644
index 000000000000..9c56eb395497
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/SimplePythonPackageInstallAction.kt
@@ -0,0 +1,15 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.jetbrains.python.packaging.common.PythonPackageSpecification
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import org.jetbrains.annotations.Nls
+
+class SimplePythonPackageInstallAction(text: @Nls String,
+ project: Project) : PythonPackageInstallAction(text, project) {
+ override suspend fun installPackage(specification: PythonPackageSpecification) {
+ project.service().installPackage(specification)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt b/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt
new file mode 100644
index 000000000000..9eee348b1223
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/actions/UpdatePackageToLatestAction.kt
@@ -0,0 +1,61 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.actions
+
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.EDT
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.DumbAwareAction
+import com.intellij.ui.awt.RelativePoint
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
+import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTable
+import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
+import com.jetbrains.python.packaging.utils.PyPackageCoroutine
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.awt.event.MouseEvent
+
+internal class UpdatePackageToLatestAction(val table: PyPackagesTable<*>) : DumbAwareAction(PyBundle.message("python.toolwindow.packages.update.package")) {
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ val pkg = table.selectedItem()
+
+ val service = project.service()
+ if (pkg is InstalledPackage && pkg.canBeUpdated) {
+ PyPackageCoroutine.getIoScope(project).launch {
+ val specification = pkg.repository.createPackageSpecification(pkg.name, pkg.nextVersion!!.presentableText)
+ service.updatePackage(specification)
+ }
+ }
+ else if (pkg is InstallablePackage) {
+ PyPackageCoroutine.getIoScope(project).launch {
+ val details = service.detailsForPackage(pkg)
+ withContext(Dispatchers.EDT) {
+ PyPackagesUiComponents.createAvailableVersionsPopup(pkg, details, project, table.controller).show(
+ RelativePoint(e.inputEvent as MouseEvent))
+ }
+ }
+ }
+ }
+
+ override fun update(e: AnActionEvent) {
+ val pkg = table.selectedItem() as? InstalledPackage
+
+ val currentVersion = pkg?.currentVersion?.presentableText
+ val nextVersion = pkg?.nextVersion?.presentableText
+ if (currentVersion != null && nextVersion != null) {
+ e.presentation.isEnabledAndVisible = true
+ e.presentation.text = PyBundle.message("python.toolwindow.packages.update.package.version", currentVersion, nextVersion)
+ }
+ else {
+ e.presentation.isEnabledAndVisible = false
+ }
+
+ }
+
+ override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt
similarity index 82%
rename from python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt
rename to python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt
index d20e1781ca6d..51a230bd4b7d 100644
--- a/python/src/com/jetbrains/python/packaging/toolwindow/modelComponents.kt
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/model/modelComponents.kt
@@ -1,5 +1,5 @@
-// Copyright 2000-2021 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.jetbrains.python.packaging.toolwindow
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.model
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.packaging.PyPackageVersion
@@ -11,9 +11,11 @@ import com.jetbrains.python.packaging.repository.PyPackageRepository
sealed class DisplayablePackage(@NlsSafe val name: String, val repository: PyPackageRepository)
class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository, val nextVersion: PyPackageVersion? = null) : DisplayablePackage(instance.name, repository) {
+ val currentVersion = PyPackageVersionNormalizer.normalize(instance.version)
+
val canBeUpdated: Boolean
get() {
- val currentVersion = PyPackageVersionNormalizer.normalize(instance.version) ?: return false
+ currentVersion ?: return false
return nextVersion != null && PyPackageVersionComparator.compare(nextVersion, currentVersion) > 0
}
@@ -24,6 +26,9 @@ class InstalledPackage(val instance: PythonPackage, repository: PyPackageReposit
class InstallablePackage(name: String, repository: PyPackageRepository) : DisplayablePackage(name, repository)
+
class ExpandResultNode(var more: Int, repository: PyPackageRepository) : DisplayablePackage("", repository)
+
open class PyPackagesViewData(@NlsSafe val repository: PyPackageRepository, val packages: List, val exactMatch: Int = -1, val moreItems: Int = 0)
+
class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList())
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesTable.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesTable.kt
new file mode 100644
index 000000000000..e18ca5d01e70
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesTable.kt
@@ -0,0 +1,218 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.ui
+
+import com.intellij.openapi.actionSystem.DefaultActionGroup
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.ui.DoubleClickListener
+import com.intellij.ui.PopupHandler
+import com.intellij.ui.SideBorder
+import com.intellij.ui.awt.RelativePoint
+import com.intellij.ui.hover.TableHoverListener
+import com.intellij.ui.table.JBTable
+import com.intellij.util.ui.ListTableModel
+import com.intellij.util.ui.NamedColorUtil
+import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import com.jetbrains.python.packaging.toolwindow.actions.ChangeVersionPackageAction
+import com.jetbrains.python.packaging.toolwindow.actions.DeletePackageAction
+import com.jetbrains.python.packaging.toolwindow.actions.InstallPackageAction
+import com.jetbrains.python.packaging.toolwindow.actions.UpdatePackageToLatestAction
+import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
+import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
+import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
+import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.awt.Cursor
+import java.awt.event.ActionEvent
+import java.awt.event.KeyEvent
+import java.awt.event.MouseAdapter
+import java.awt.event.MouseEvent
+import javax.swing.AbstractAction
+import javax.swing.JTable
+import javax.swing.KeyStroke
+import javax.swing.ListSelectionModel
+import javax.swing.table.TableCellRenderer
+
+internal class PyPackagesTable(
+ project: Project,
+ model: ListTableModel,
+ tablesView: PyPackagingTablesView,
+ val controller: PyPackagingToolWindowPanel,
+) : JBTable(model) {
+ private var lastSelectedRow = -1
+ internal var hoveredColumn = -1
+
+ @Suppress("UNCHECKED_CAST")
+ private val listModel: ListTableModel
+ get() = model as ListTableModel
+
+ var items: List
+ get() = listModel.items
+ set(value) {
+ listModel.items = value.toMutableList()
+ }
+
+ init {
+ val service = project.service()
+ setShowGrid(false)
+ setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
+ val column = columnModel.getColumn(1)
+ column.minWidth = 130
+ column.maxWidth = 130
+ column.resizable = false
+ border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM)
+ rowHeight = 20
+
+ initCrossNavigation(service, tablesView)
+
+ val hoverListener = object : TableHoverListener() {
+ override fun onHover(table: JTable, row: Int, column: Int) {
+ hoveredColumn = column
+ if (column == 1) {
+ table.repaint(table.getCellRect(row, column, true))
+ val currentPackage = items[row]
+ if (currentPackage is InstallablePackage
+ || (currentPackage is InstalledPackage && currentPackage.canBeUpdated)) {
+ cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
+ return
+ }
+ }
+ cursor = Cursor.getDefaultCursor()
+ }
+ }
+ hoverListener.addTo(this)
+
+ addMouseListener(object : MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent) {
+ if (e.clickCount != 1 || columnAtPoint(e.point) != 1) return // double click or click on package name column, nothing to be done
+ val hoveredRow = TableHoverListener.getHoveredRow(this@PyPackagesTable)
+ val selectedPackage = this@PyPackagesTable.items[hoveredRow]
+
+ if (selectedPackage is InstallablePackage) {
+ controller.packagingScope.launch(Dispatchers.IO) {
+ val details = service.detailsForPackage(selectedPackage)
+ withContext(Dispatchers.Main) {
+ PyPackagesUiComponents.createAvailableVersionsPopup(selectedPackage, details, project, controller).show(RelativePoint(e))
+ }
+ }
+ }
+ else if (selectedPackage is InstalledPackage && selectedPackage.canBeUpdated) {
+ controller.packagingScope.launch(Dispatchers.IO) {
+ val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name,
+ selectedPackage.nextVersion!!.presentableText)
+ project.service().updatePackage(specification)
+ }
+ }
+ }
+ })
+
+ selectionModel.addListSelectionListener {
+ if (selectedRow != -1 && selectedRow != lastSelectedRow) {
+ lastSelectedRow = selectedRow
+ tablesView.requestSelection(this)
+ val pkg = model.items[selectedRow]
+ if (pkg !is ExpandResultNode) controller.packageSelected(pkg)
+ }
+ }
+
+ object : DoubleClickListener() {
+ override fun onDoubleClick(event: MouseEvent): Boolean {
+ val pkg = model.items[selectedRow]
+ if (pkg is ExpandResultNode) loadMoreItems(service, pkg)
+ return true
+ }
+ }.installOn(this)
+
+ val packageActionGroup = DefaultActionGroup(
+ DeletePackageAction(this),
+ InstallPackageAction(this),
+ UpdatePackageToLatestAction(this),
+ ChangeVersionPackageAction(this),
+ )
+ PopupHandler.installPopupMenu(this, packageActionGroup, "PackagePopup")
+ }
+
+
+ fun selectedItem(): T? = items.getOrNull(selectedRow)
+
+ private fun initCrossNavigation(service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) {
+ getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ENTER_ACTION)
+ actionMap.put(ENTER_ACTION, object : AbstractAction() {
+ override fun actionPerformed(e: ActionEvent?) {
+ if (selectedRow == -1) return
+ val index = selectedRow
+ val selectedItem = selectedItem() ?: return
+
+ if (selectedItem is ExpandResultNode) {
+ loadMoreItems(service, selectedItem)
+ }
+ setRowSelectionInterval(index, index)
+ }
+ })
+
+ val nextRowAction = actionMap[NEXT_ROW_ACTION]
+ actionMap.put(NEXT_ROW_ACTION, object : AbstractAction() {
+ override fun actionPerformed(e: ActionEvent?) {
+ if (selectedRow == -1) return
+
+ if (selectedRow + 1 == items.size) {
+ tablesView.selectNextFrom(this@PyPackagesTable)
+ }
+ else {
+ nextRowAction.actionPerformed(e)
+ }
+ }
+ })
+
+ val prevRowAction = actionMap[PREVIOUS_ROW_ACTION]
+ actionMap.put(PREVIOUS_ROW_ACTION, object : AbstractAction() {
+ override fun actionPerformed(e: ActionEvent?) {
+ if (selectedRow == -1) return
+
+ if (selectedRow == 0) {
+ tablesView.selectPreviousOf(this@PyPackagesTable)
+ }
+ else {
+ prevRowAction.actionPerformed(e)
+ }
+ }
+ })
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun loadMoreItems(service: PyPackagingToolWindowService, node: ExpandResultNode) {
+ val result = service.getMoreResultsForRepo(node.repository, items.size - 1)
+ items = items.dropLast(1) + (result.packages as List)
+ if (result.moreItems > 0) {
+ node.more = result.moreItems
+ items = items + listOf(node) as List
+ }
+ this@PyPackagesTable.revalidate()
+ this@PyPackagesTable.repaint()
+ }
+
+ override fun getCellRenderer(row: Int, column: Int): TableCellRenderer {
+ return PyPaginationAwareRenderer()
+ }
+
+ override fun clearSelection() {
+ lastSelectedRow = -1
+ super.clearSelection()
+ }
+
+ internal fun removeRow(index: Int) = listModel.removeRow(index)
+ internal fun insertRow(index: Int, pkg: T) = listModel.insertRow(index, pkg)
+
+
+ companion object {
+ private const val NEXT_ROW_ACTION = "selectNextRow"
+ private const val PREVIOUS_ROW_ACTION = "selectPreviousRow"
+ private const val ENTER_ACTION = "ENTER"
+ }
+}
+
+
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesTableModel.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesTableModel.kt
new file mode 100644
index 000000000000..226ea6dbb487
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesTableModel.kt
@@ -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.jetbrains.python.packaging.toolwindow.ui
+
+import com.intellij.util.ui.ListTableModel
+import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
+
+internal class PyPackagesTableModel : ListTableModel() {
+ override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean = false
+ override fun getColumnCount(): Int = 2
+ override fun getColumnName(column: Int): String = column.toString()
+ override fun getColumnClass(columnIndex: Int): Class<*> = DisplayablePackage::class.java
+ override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? = items[rowIndex]
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt
new file mode 100644
index 000000000000..c479813ba9fb
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagesUiComponents.kt
@@ -0,0 +1,77 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.ui
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.popup.JBPopupFactory
+import com.intellij.openapi.ui.popup.ListPopup
+import com.intellij.openapi.ui.popup.PopupStep
+import com.intellij.openapi.ui.popup.util.BaseListPopupStep
+import com.intellij.ui.SideBorder
+import com.intellij.util.ui.JBUI
+import com.intellij.util.ui.NamedColorUtil
+import com.intellij.util.ui.UIUtil
+import com.jetbrains.python.packaging.common.PythonPackageDetails
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
+import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
+import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.awt.BorderLayout
+import java.awt.Dimension
+import java.awt.event.MouseAdapter
+import java.awt.event.MouseEvent
+import javax.swing.*
+
+object PyPackagesUiComponents {
+ fun createAvailableVersionsPopup(selectedPackage: DisplayablePackage, details: PythonPackageDetails, project: Project, controller: PyPackagingToolWindowPanel): ListPopup {
+ return JBPopupFactory.getInstance().createListPopup(object : BaseListPopupStep(null, details.availableVersions) {
+ override fun onChosen(selectedValue: String?, finalChoice: Boolean): PopupStep<*>? {
+ return doFinalStep {
+ val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name, selectedValue)
+ controller.packagingScope.launch(Dispatchers.IO) {
+ project.service().installPackage(specification)
+ }
+ }
+ }
+ }, 8)
+ }
+
+
+ fun boxPanel(init: JPanel.() -> Unit) = object : JPanel() {
+ init {
+ layout = BoxLayout(this, BoxLayout.X_AXIS)
+ alignmentX = LEFT_ALIGNMENT
+ init()
+ }
+ }
+
+ fun borderPanel(init: JPanel.() -> Unit) = object : JPanel() {
+ init {
+ layout = BorderLayout(0, 0)
+ init()
+ }
+ }
+
+ fun headerPanel(label: JLabel, component: JComponent?) = object : JPanel() {
+ init {
+ background = UIUtil.getLabelBackground()
+ layout = BorderLayout()
+ border = BorderFactory.createCompoundBorder(SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM), JBUI.Borders.empty(0, 5))
+ preferredSize = Dimension(preferredSize.width, 25)
+ minimumSize = Dimension(minimumSize.width, 25)
+ maximumSize = Dimension(maximumSize.width, 25)
+
+ add(label, BorderLayout.WEST)
+ if (component != null) {
+ addMouseListener(object : MouseAdapter() {
+ override fun mouseClicked(e: MouseEvent?) {
+ component.isVisible = !component.isVisible
+ label.icon = if (component.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
+ }
+ })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagingTableGroup.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagingTableGroup.kt
new file mode 100644
index 000000000000..efd9cabe9fd4
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPackagingTableGroup.kt
@@ -0,0 +1,64 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.ui
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.util.NlsSafe
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.repository.PyPackageRepository
+import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
+import javax.swing.JLabel
+import javax.swing.JPanel
+
+internal class PyPackagingTableGroup(val repository: PyPackageRepository, val table: PyPackagesTable) {
+ @NlsSafe
+ val name: String = repository.name!!
+
+ private var expanded = false
+ private val label = JLabel(name).apply { icon = AllIcons.General.ArrowDown }
+ private val header: JPanel = PyPackagesUiComponents.headerPanel(label, table)
+ private var itemsCount: Int? = null
+
+
+ internal var items: List
+ get() = table.items
+ set(value) {
+ table.items = value
+ }
+
+ fun collapse() {
+ expanded = false
+ table.isVisible = false
+ label.icon = if (table.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
+ }
+
+ fun expand() {
+ expanded = true
+ table.isVisible = true
+ label.icon = if (table.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
+ }
+
+ fun updateHeaderText(newItemCount: Int?) {
+ itemsCount = newItemCount
+ label.text = if (itemsCount == null) name else PyBundle.message("python.toolwindow.packages.custom.repo.searched", name, itemsCount)
+ }
+
+ fun addTo(panel: JPanel) {
+ panel.add(header)
+ panel.add(table)
+ }
+
+ fun replace(row: Int, pkg: T) {
+ table.removeRow(row)
+ table.insertRow(row, pkg)
+ }
+
+ fun removeFrom(panel: JPanel) {
+ panel.remove(header)
+ panel.remove(table)
+ }
+
+ fun repaint() {
+ table.invalidate()
+ table.repaint()
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPaginationAwareRenderer.kt b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPaginationAwareRenderer.kt
new file mode 100644
index 000000000000..efb4bc4492e4
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/toolwindow/ui/PyPaginationAwareRenderer.kt
@@ -0,0 +1,112 @@
+// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.jetbrains.python.packaging.toolwindow.ui
+
+import com.intellij.openapi.util.NlsSafe
+import com.intellij.ui.hover.TableHoverListener
+import com.intellij.util.ui.JBUI
+import com.intellij.util.ui.UIUtil
+import com.jetbrains.python.PyBundle
+import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
+import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
+import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
+import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
+import java.awt.Component
+import java.awt.font.TextAttribute
+import javax.swing.BoxLayout
+import javax.swing.JLabel
+import javax.swing.JPanel
+import javax.swing.JTable
+import javax.swing.table.DefaultTableCellRenderer
+
+internal class PyPaginationAwareRenderer : DefaultTableCellRenderer() {
+ private val nameLabel = JLabel().apply { border = JBUI.Borders.empty(0, 12) }
+ private val versionLabel = JLabel().apply { border = JBUI.Borders.emptyRight(12) }
+ private val linkLabel = JLabel(PyBundle.message("python.toolwindow.packages.install.link")).apply {
+ border = JBUI.Borders.emptyRight(12)
+ foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED
+ }
+
+ private val namePanel = JPanel().apply {
+ layout = BoxLayout(this, BoxLayout.X_AXIS)
+ border = JBUI.Borders.empty()
+ add(nameLabel)
+ }
+
+ private val versionPanel = PyPackagesUiComponents.boxPanel {
+ border = JBUI.Borders.emptyRight(12)
+ add(versionLabel)
+ }
+
+ override fun getTableCellRendererComponent(
+ table: JTable,
+ value: Any?,
+ isSelected: Boolean,
+ hasFocus: Boolean,
+ row: Int,
+ column: Int,
+ ): Component {
+ val rowSelected = table.selectedRow == row
+ val tableFocused = table.hasFocus()
+
+ if (value is ExpandResultNode) {
+ if (column == 1) {
+ versionPanel.removeAll()
+ return versionPanel
+ }
+ else {
+ nameLabel.text = PyBundle.message("python.toolwindow.packages.load.more", value.more)
+ nameLabel.foreground = UIUtil.getContextHelpForeground()
+ return namePanel
+ }
+ }
+
+ // version column
+ if (column == 1) {
+ versionPanel.background = JBUI.CurrentTheme.Table.background(rowSelected, tableFocused)
+ versionPanel.foreground = JBUI.CurrentTheme.Table.foreground(rowSelected, tableFocused)
+ versionPanel.removeAll()
+
+ if (value is InstallablePackage) {
+ linkLabel.text = PyBundle.message("python.toolwindow.packages.install.link")
+ linkLabel.updateUnderline(table, row)
+ if (rowSelected || TableHoverListener.getHoveredRow(table) == row) {
+ versionPanel.add(linkLabel)
+ }
+ }
+ else if (value is InstalledPackage && value.nextVersion != null && value.canBeUpdated) {
+ @NlsSafe val updateLink = value.instance.version + " -> " + value.nextVersion.presentableText
+ linkLabel.text = updateLink
+ linkLabel.updateUnderline(table, row)
+ versionPanel.add(linkLabel)
+ }
+ else {
+ @NlsSafe val version = (value as InstalledPackage).instance.version
+ versionLabel.text = version
+ versionPanel.add(versionLabel)
+ }
+ return versionPanel
+ }
+
+ // package name column
+ val currentPackage = value as DisplayablePackage
+
+ namePanel.background = JBUI.CurrentTheme.Table.background(rowSelected, tableFocused)
+ namePanel.foreground = JBUI.CurrentTheme.Table.foreground(rowSelected, tableFocused)
+
+ nameLabel.text = currentPackage.name
+ nameLabel.foreground = JBUI.CurrentTheme.Label.foreground()
+ return namePanel
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun JLabel.updateUnderline(table: JTable, currentRow: Int) {
+ val hoveredRow = TableHoverListener.getHoveredRow(table)
+ val hoveredColumn = (table as PyPackagesTable<*>).hoveredColumn
+ val underline = if (hoveredRow == currentRow && hoveredColumn == 1) TextAttribute.UNDERLINE_ON else -1
+
+ val attributes = font.attributes as MutableMap
+ attributes[TextAttribute.UNDERLINE] = underline
+ attributes[TextAttribute.LIGATURES] = TextAttribute.LIGATURES_ON
+ font = font.deriveFont(attributes)
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt b/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt
deleted file mode 100644
index 35676c6d4664..000000000000
--- a/python/src/com/jetbrains/python/packaging/toolwindow/uiComponents.kt
+++ /dev/null
@@ -1,462 +0,0 @@
-// Copyright 2000-2021 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.jetbrains.python.packaging.toolwindow
-
-import com.intellij.icons.AllIcons
-import com.intellij.openapi.actionSystem.AnActionEvent
-import com.intellij.openapi.actionSystem.DefaultActionGroup
-import com.intellij.openapi.components.service
-import com.intellij.openapi.project.DumbAwareAction
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.ui.popup.JBPopupFactory
-import com.intellij.openapi.ui.popup.ListPopup
-import com.intellij.openapi.ui.popup.PopupStep
-import com.intellij.openapi.ui.popup.util.BaseListPopupStep
-import com.intellij.openapi.util.NlsSafe
-import com.intellij.ui.DoubleClickListener
-import com.intellij.ui.PopupHandler
-import com.intellij.ui.SideBorder
-import com.intellij.ui.awt.RelativePoint
-import com.intellij.ui.hover.TableHoverListener
-import com.intellij.ui.table.JBTable
-import com.intellij.util.ui.*
-import com.intellij.util.ui.JBUI
-import com.jetbrains.python.PyBundle.message
-import com.jetbrains.python.packaging.common.PythonPackageDetails
-import com.jetbrains.python.packaging.repository.PyPackageRepository
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.awt.*
-import java.awt.event.ActionEvent
-import java.awt.event.KeyEvent
-import java.awt.event.MouseAdapter
-import java.awt.event.MouseEvent
-import java.awt.font.TextAttribute
-import javax.swing.*
-import javax.swing.table.DefaultTableCellRenderer
-import javax.swing.table.TableCellRenderer
-
-
-internal class PyPackagesTable(project: Project,
- model: ListTableModel,
- tablesView: PyPackagingTablesView,
- controller: PyPackagingToolWindowPanel) : JBTable(model) {
- private var lastSelectedRow = -1
- internal var hoveredColumn = -1
- init {
- val service = project.service()
- setShowGrid(false)
- setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
- val column = columnModel.getColumn(1)
- column.minWidth = 130
- column.maxWidth = 130
- column.resizable = false
- border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM)
- rowHeight = 20
-
- initCrossNavigation(service, tablesView)
-
- val hoverListener = object : TableHoverListener() {
- override fun onHover(table: JTable, row: Int, column: Int) {
- hoveredColumn = column
- if (column == 1) {
- table.repaint(table.getCellRect(row, column, true))
- val currentPackage = items[row]
- if (currentPackage is InstallablePackage
- || (currentPackage is InstalledPackage && currentPackage.canBeUpdated)) {
- cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
- return
- }
- }
- cursor = Cursor.getDefaultCursor()
- }
- }
- hoverListener.addTo(this)
-
- addMouseListener(object : MouseAdapter() {
- override fun mouseClicked(e: MouseEvent) {
- if (e.clickCount != 1 || columnAtPoint(e.point) != 1) return // double click or click on package name column, nothing to be done
- val hoveredRow = TableHoverListener.getHoveredRow(this@PyPackagesTable)
- val selectedPackage = this@PyPackagesTable.items[hoveredRow]
-
- if (selectedPackage is InstallablePackage) {
- controller.packagingScope.launch(Dispatchers.IO) {
- val details = service.detailsForPackage(selectedPackage)
- withContext(Dispatchers.Main) {
- createAvailableVersionsPopup(selectedPackage, details, project, controller).show(RelativePoint(e))
- }
- }
- }
- else if (selectedPackage is InstalledPackage && selectedPackage.canBeUpdated) {
- controller.packagingScope.launch(Dispatchers.IO) {
- val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name,
- selectedPackage.nextVersion!!.presentableText)
- project.service().updatePackage(specification)
- }
- }
- }
- })
-
- selectionModel.addListSelectionListener {
- if (selectedRow != -1 && selectedRow != lastSelectedRow) {
- lastSelectedRow = selectedRow
- tablesView.requestSelection(this)
- val pkg = model.items[selectedRow]
- if (pkg !is ExpandResultNode) controller.packageSelected(pkg)
- }
- }
-
- object : DoubleClickListener() {
- override fun onDoubleClick(event: MouseEvent): Boolean {
- val pkg = model.items[selectedRow]
- if (pkg is ExpandResultNode) loadMoreItems(service, pkg)
- return true
- }
- }.installOn(this)
-
- PopupHandler.installPopupMenu(this, DefaultActionGroup(object : DumbAwareAction({
- val pkg = if (selectedRow >= 0) model.items[selectedRow] else null
- if (pkg is InstalledPackage) {
- message("python.toolwindow.packages.delete.package")
- }
- else {
- message("python.toolwindow.packages.install.link")
- }
- }) {
- override fun actionPerformed(e: AnActionEvent) {
- controller.packagingScope.launch(Dispatchers.Main) {
- if (selectedRow == -1) return@launch
- val pkg = model.items[selectedRow]
- if (pkg is InstalledPackage) {
- withContext(Dispatchers.IO) {
- service.deletePackage(pkg)
- }
- }
- else if (pkg is InstallablePackage) {
- controller.packagingScope.launch(Dispatchers.IO) {
- val details = service.detailsForPackage(pkg)
- withContext(Dispatchers.Main) {
- createAvailableVersionsPopup(pkg as InstallablePackage, details, project, controller).show(RelativePoint(e.inputEvent as MouseEvent))
- }
- }
- }
- }
- }
- }, object : DumbAwareAction({
- val pkg = if (selectedRow >= 0) model.items[selectedRow] else null
- if (pkg is InstalledPackage && pkg.canBeUpdated) {
- message("python.toolwindow.packages.update.package")
- }
- else {
- ""
- }
- }) {
- override fun actionPerformed(e: AnActionEvent) {
- controller.packagingScope.launch(Dispatchers.Main) {
- if (selectedRow == -1) return@launch
- val pkg = model.items[selectedRow]
- if (pkg is InstalledPackage && pkg.canBeUpdated) {
- controller.packagingScope.launch(Dispatchers.IO) {
- val specification = pkg.repository.createPackageSpecification(pkg.name, pkg.nextVersion!!.presentableText)
- service.updatePackage(specification)
- }
- }
- else if (pkg is InstallablePackage) {
- controller.packagingScope.launch(Dispatchers.IO) {
- val details = service.detailsForPackage(pkg)
- withContext(Dispatchers.Main) {
- createAvailableVersionsPopup(pkg as InstallablePackage, details, project, controller).show(RelativePoint(e.inputEvent as MouseEvent))
- }
- }
- }
- }
- }
- }), "PackagePopup")
- }
-
- private fun createAvailableVersionsPopup(selectedPackage: InstallablePackage, details: PythonPackageDetails, project: Project, controller: PyPackagingToolWindowPanel): ListPopup {
- return JBPopupFactory.getInstance().createListPopup(object : BaseListPopupStep(null, details.availableVersions) {
- override fun onChosen(selectedValue: String?, finalChoice: Boolean): PopupStep<*>? {
- return doFinalStep {
- val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name, selectedValue)
- controller.packagingScope.launch(Dispatchers.IO) {
- project.service().installPackage(specification)
- }
- }
- }
- }, 8)
- }
-
- private fun initCrossNavigation(service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) {
- getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ENTER_ACTION)
- actionMap.put(ENTER_ACTION, object : AbstractAction() {
- override fun actionPerformed(e: ActionEvent?) {
- if (selectedRow == -1) return
- val index = selectedRow
- val selectedItem = items[selectedRow]
-
- if (selectedItem is ExpandResultNode) {
- loadMoreItems(service, selectedItem)
- }
- setRowSelectionInterval(index, index)
- }
- })
-
- val nextRowAction = actionMap[NEXT_ROW_ACTION]
- actionMap.put(NEXT_ROW_ACTION, object : AbstractAction() {
- override fun actionPerformed(e: ActionEvent?) {
- if (selectedRow == -1) return
-
- if (selectedRow + 1 == items.size) {
- tablesView.selectNextFrom(this@PyPackagesTable)
- }
- else {
- nextRowAction.actionPerformed(e)
- }
- }
- })
-
- val prevRowAction = actionMap[PREVIOUS_ROW_ACTION]
- actionMap.put(PREVIOUS_ROW_ACTION, object : AbstractAction() {
- override fun actionPerformed(e: ActionEvent?) {
- if (selectedRow == -1) return
-
- if (selectedRow == 0) {
- tablesView.selectPreviousOf(this@PyPackagesTable)
- }
- else {
- prevRowAction.actionPerformed(e)
- }
- }
- })
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun loadMoreItems(service: PyPackagingToolWindowService, node: ExpandResultNode) {
- val result = service.getMoreResultsForRepo(node.repository, items.size - 1)
- items = items.dropLast(1) + (result.packages as List)
- if (result.moreItems > 0) {
- node.more = result.moreItems
- items = items + listOf(node) as List
- }
- this@PyPackagesTable.revalidate()
- this@PyPackagesTable.repaint()
- }
-
- override fun getCellRenderer(row: Int, column: Int): TableCellRenderer {
- return PyPaginationAwareRenderer()
- }
-
- override fun clearSelection() {
- lastSelectedRow = -1
- super.clearSelection()
- }
-
- internal fun addRows(rows: List) = listModel.addRows(rows)
- internal fun removeRow(index: Int) = listModel.removeRow(index)
- internal fun insertRow(index: Int, pkg: T) = listModel.insertRow(index, pkg)
-
- @Suppress("UNCHECKED_CAST")
- private val listModel: ListTableModel
- get() = model as ListTableModel
-
- var items: List
- get() = listModel.items
- set(value) { listModel.items = value.toMutableList() }
-
- companion object {
- private const val NEXT_ROW_ACTION = "selectNextRow"
- private const val PREVIOUS_ROW_ACTION = "selectPreviousRow"
- private const val ENTER_ACTION = "ENTER"
- }
-}
-
-internal class PyPackagesTableModel : ListTableModel() {
- override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean = false
- override fun getColumnCount(): Int = 2
- override fun getColumnName(column: Int): String = column.toString()
- override fun getColumnClass(columnIndex: Int): Class<*> = DisplayablePackage::class.java
-
- override fun getValueAt(rowIndex: Int, columnIndex: Int): Any? = items[rowIndex]
-}
-
-fun boxPanel(init: JPanel.() -> Unit) = object : JPanel() {
- init {
- layout = BoxLayout(this, BoxLayout.X_AXIS)
- alignmentX = LEFT_ALIGNMENT
- init()
- }
-}
-
-fun borderPanel(init: JPanel.() -> Unit) = object : JPanel() {
- init {
- layout = BorderLayout(0, 0)
- init()
- }
-}
-
-fun headerPanel(label: JLabel, component: JComponent?) = object : JPanel() {
- init {
- background = UIUtil.getLabelBackground()
- layout = BorderLayout()
- border = BorderFactory.createCompoundBorder(SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM), JBUI.Borders.empty(0, 5))
- preferredSize = Dimension(preferredSize.width, 25)
- minimumSize = Dimension(minimumSize.width, 25)
- maximumSize = Dimension(maximumSize.width, 25)
-
- add(label, BorderLayout.WEST)
- if (component != null) {
- addMouseListener(object : MouseAdapter() {
- override fun mouseClicked(e: MouseEvent?) {
- component.isVisible = !component.isVisible
- label.icon = if (component.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
- }
- })
- }
- }
-}
-
-private class PyPaginationAwareRenderer : DefaultTableCellRenderer() {
- private val nameLabel = JLabel().apply { border = JBUI.Borders.empty(0, 12) }
- private val versionLabel = JLabel().apply { border = JBUI.Borders.emptyRight(12) }
- private val linkLabel = JLabel(message("python.toolwindow.packages.install.link")).apply {
- border = JBUI.Borders.emptyRight(12)
- foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED
- }
-
- private val namePanel = JPanel().apply {
- layout = BoxLayout(this, BoxLayout.X_AXIS)
- border = JBUI.Borders.empty()
- add(nameLabel)
- }
-
- private val versionPanel = boxPanel {
- border = JBUI.Borders.emptyRight(12)
- add(versionLabel)
- }
-
- override fun getTableCellRendererComponent(table: JTable,
- value: Any?,
- isSelected: Boolean,
- hasFocus: Boolean,
- row: Int,
- column: Int): Component {
- val rowSelected = table.selectedRow == row
- val tableFocused = table.hasFocus()
-
- if (value is ExpandResultNode) {
- if (column == 1) {
- versionPanel.removeAll()
- return versionPanel
- }
- else {
- nameLabel.text = message("python.toolwindow.packages.load.more", value.more)
- nameLabel.foreground = UIUtil.getContextHelpForeground()
- return namePanel
- }
- }
-
- // version column
- if (column == 1) {
- versionPanel.background = JBUI.CurrentTheme.Table.background(rowSelected, tableFocused)
- versionPanel.foreground = JBUI.CurrentTheme.Table.foreground(rowSelected, tableFocused)
- versionPanel.removeAll()
-
- if (value is InstallablePackage) {
- linkLabel.text = message("python.toolwindow.packages.install.link")
- linkLabel.updateUnderline(table, row)
- if (rowSelected || TableHoverListener.getHoveredRow(table) == row) {
- versionPanel.add(linkLabel)
- }
- }
- else if (value is InstalledPackage && value.nextVersion != null && value.canBeUpdated) {
- @NlsSafe val updateLink = value.instance.version + " -> " + value.nextVersion.presentableText
- linkLabel.text = updateLink
- linkLabel.updateUnderline(table, row)
- versionPanel.add(linkLabel)
- }
- else {
- @NlsSafe val version = (value as InstalledPackage).instance.version
- versionLabel.text = version
- versionPanel.add(versionLabel)
- }
- return versionPanel
- }
-
- // package name column
- val currentPackage = value as DisplayablePackage
-
- namePanel.background = JBUI.CurrentTheme.Table.background(rowSelected, tableFocused)
- namePanel.foreground = JBUI.CurrentTheme.Table.foreground(rowSelected, tableFocused)
-
- nameLabel.text = currentPackage.name
- nameLabel.foreground = JBUI.CurrentTheme.Label.foreground()
- return namePanel
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun JLabel.updateUnderline(table: JTable, currentRow: Int) {
- val hoveredRow = TableHoverListener.getHoveredRow(table)
- val hoveredColumn = (table as PyPackagesTable<*>).hoveredColumn
- val underline = if (hoveredRow == currentRow && hoveredColumn == 1) TextAttribute.UNDERLINE_ON else -1
-
- val attributes = font.attributes as MutableMap
- attributes[TextAttribute.UNDERLINE] = underline
- attributes[TextAttribute.LIGATURES] = TextAttribute.LIGATURES_ON
- font = font.deriveFont(attributes)
- }
-}
-
-
-internal class PyPackagingTableGroup(val repository: PyPackageRepository, val table: PyPackagesTable) {
- @NlsSafe val name: String = repository.name!!
-
- private var expanded = false
- private val label = JLabel(name).apply { icon = AllIcons.General.ArrowDown }
- private val header: JPanel = headerPanel(label, table)
- private var itemsCount: Int? = null
-
-
- internal var items: List
- get() = table.items
- set(value) {
- table.items = value
- }
-
- fun collapse() {
- expanded = false
- table.isVisible = false
- label.icon = if (table.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
- }
-
- fun expand() {
- expanded = true
- table.isVisible = true
- label.icon = if (table.isVisible) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
- }
-
- fun updateHeaderText(newItemCount: Int?) {
- itemsCount = newItemCount
- label.text = if (itemsCount == null) name else message("python.toolwindow.packages.custom.repo.searched", name, itemsCount)
- }
-
- fun addTo(panel: JPanel) {
- panel.add(header)
- panel.add(table)
- }
-
- fun replace(row: Int, pkg: T) {
- table.removeRow(row)
- table.insertRow(row, pkg)
- }
-
- fun removeFrom(panel: JPanel) {
- panel.remove(header)
- panel.remove(table)
- }
-
- fun repaint() {
- table.invalidate()
- table.repaint()
- }
-}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/packaging/utils/PyPackageCoroutine.kt b/python/src/com/jetbrains/python/packaging/utils/PyPackageCoroutine.kt
new file mode 100644
index 000000000000..0f9e81b88702
--- /dev/null
+++ b/python/src/com/jetbrains/python/packaging/utils/PyPackageCoroutine.kt
@@ -0,0 +1,23 @@
+package com.jetbrains.python.packaging.utils
+
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.platform.util.coroutines.childScope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlin.coroutines.CoroutineContext
+
+@Service(Service.Level.PROJECT)
+class PyPackageCoroutine(val project: Project, val coroutineScope: CoroutineScope) {
+ val ioScope = coroutineScope.childScope("Jupyter IO scope", context = Dispatchers.IO)
+
+ companion object {
+ fun launch(project: Project, context: CoroutineContext = Dispatchers.Main, body: suspend CoroutineScope.() -> Unit) =
+ project.service().coroutineScope.launch(context, block = body)
+
+ fun getIoScope(project: Project): CoroutineScope = project.service().ioScope
+ fun getScope(project: Project): CoroutineScope = project.service().coroutineScope
+ }
+}