PY-72994 Packages(refactor, feat): Rename Delete package andd Update package

GitOrigin-RevId: 56ddaade343cb5eeb3240fd1090f68b72e42ed86
This commit is contained in:
Nikita.Ashihmin
2024-08-20 15:41:29 +04:00
committed by intellij-monorepo-bot
parent c15235e357
commit 134376878d
23 changed files with 787 additions and 537 deletions

View File

@@ -742,7 +742,7 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
dynamic="true"/> dynamic="true"/>
<extensionPoint qualifiedName="Pythonid.PythonPackagingToolwindowActionProvider" <extensionPoint qualifiedName="Pythonid.PythonPackagingToolwindowActionProvider"
interface="com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider" interface="com.jetbrains.python.packaging.toolwindow.actions.PythonPackagingToolwindowActionProvider"
dynamic="true"/> dynamic="true"/>
<extensionPoint qualifiedName="Pythonid.pythonPackageManagerProvider" <extensionPoint qualifiedName="Pythonid.pythonPackageManagerProvider"

View File

@@ -1356,8 +1356,9 @@ python.toolwindow.packages.custom.repo.invalid={0} (Authorization failed)
python.toolwindow.packages.documentation.link=Documentation python.toolwindow.packages.documentation.link=Documentation
python.toolwindow.packages.no.interpreter.text=Select an interpreter to see the installed packages python.toolwindow.packages.no.interpreter.text=Select an interpreter to see the installed packages
python.toolwindow.packages.latest.version.label=latest python.toolwindow.packages.latest.version.label=latest
python.toolwindow.packages.delete.package=Delete Package python.toolwindow.packages.delete.package=Uninstall
python.toolwindow.packages.update.package=Update Package python.toolwindow.packages.update.package=Change Version
python.toolwindow.packages.update.package.version=Update {0} -> {1}
python.toolwindow.packages.install.link=Install python.toolwindow.packages.install.link=Install
python.toolwindow.packages.search.text.placeholder=Search for more packages python.toolwindow.packages.search.text.placeholder=Search for more packages
python.toolwindow.packages.description.panel.placeholder=Select a package to view documentation python.toolwindow.packages.description.panel.placeholder=Select a package to view documentation

View File

@@ -4,9 +4,9 @@ package com.jetbrains.python.packaging.conda
import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.common.PythonPackageDetails import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider import com.jetbrains.python.packaging.toolwindow.actions.PythonPackageInstallAction
import com.jetbrains.python.packaging.toolwindow.PythonPackageInstallAction import com.jetbrains.python.packaging.toolwindow.actions.PythonPackagingToolwindowActionProvider
import com.jetbrains.python.packaging.toolwindow.SimplePythonPackageInstallAction import com.jetbrains.python.packaging.toolwindow.actions.SimplePythonPackageInstallAction
import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental @ApiStatus.Experimental

View File

@@ -5,9 +5,9 @@ import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.common.PythonPackageDetails import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonSimplePackageDetails import com.jetbrains.python.packaging.common.PythonSimplePackageDetails
import com.jetbrains.python.packaging.management.PythonPackageManager import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.toolwindow.PythonPackageInstallAction import com.jetbrains.python.packaging.toolwindow.actions.PythonPackageInstallAction
import com.jetbrains.python.packaging.toolwindow.PythonPackagingToolwindowActionProvider import com.jetbrains.python.packaging.toolwindow.actions.PythonPackagingToolwindowActionProvider
import com.jetbrains.python.packaging.toolwindow.SimplePythonPackageInstallAction import com.jetbrains.python.packaging.toolwindow.actions.SimplePythonPackageInstallAction
import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.ApiStatus
@ApiStatus.Experimental @ApiStatus.Experimental

View File

@@ -6,6 +6,11 @@ import com.intellij.openapi.project.Project
import com.intellij.ui.JBColor import com.intellij.ui.JBColor
import com.jetbrains.python.PyBundle.message import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.packaging.repository.PyPackageRepository 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 java.awt.Rectangle
import javax.swing.JLabel import javax.swing.JLabel
import javax.swing.JPanel import javax.swing.JPanel
@@ -99,7 +104,7 @@ class PyPackagingTablesView(private val project: Project,
foreground = JBColor.RED foreground = JBColor.RED
icon = AllIcons.General.Error icon = AllIcons.General.Error
} }
it to headerPanel(label, null) it to PyPackagesUiComponents.headerPanel(label, null)
} }
.forEach { .forEach {
invalidRepositories[it.first] = it.second invalidRepositories[it.first] = it.second

View File

@@ -46,10 +46,19 @@ import com.jetbrains.python.packaging.PyPackageUtil
import com.jetbrains.python.packaging.common.PythonLocalPackageSpecification import com.jetbrains.python.packaging.common.PythonLocalPackageSpecification
import com.jetbrains.python.packaging.common.PythonPackageDetails import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.common.PythonVcsPackageSpecification 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 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.BorderLayout
import java.awt.Component
import java.awt.Dimension import java.awt.Dimension
import java.awt.FlowLayout import java.awt.FlowLayout
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
@@ -60,7 +69,7 @@ import javax.swing.event.DocumentEvent
import javax.swing.event.ListSelectionListener import javax.swing.event.ListSelectionListener
class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolWindow) : SimpleToolWindowPanel(false, true), Disposable { 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 selectedPackage: DisplayablePackage? = null
private var selectedPackageDetails: PythonPackageDetails? = null private var selectedPackageDetails: PythonPackageDetails? = null
@@ -172,20 +181,20 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
tablesView = PyPackagingTablesView(project, packageListPanel, this) tablesView = PyPackagingTablesView(project, packageListPanel, this)
leftPanel = createLeftPanel(service) leftPanel = createLeftPanel(service)
rightPanel = borderPanel { rightPanel = PyPackagesUiComponents.borderPanel {
add(borderPanel { add(PyPackagesUiComponents.borderPanel {
border = SideBorder(JBColor.GRAY, SideBorder.BOTTOM) border = SideBorder(JBColor.GRAY, SideBorder.BOTTOM)
preferredSize = Dimension(preferredSize.width, 50) preferredSize = Dimension(preferredSize.width, 50)
minimumSize = Dimension(minimumSize.width, 50) minimumSize = Dimension(minimumSize.width, 50)
maximumSize = Dimension(maximumSize.width, 50) maximumSize = Dimension(maximumSize.width, 50)
add(boxPanel { add(PyPackagesUiComponents.boxPanel {
add(Box.createHorizontalStrut(10)) add(Box.createHorizontalStrut(10))
add(packageNameLabel) add(packageNameLabel)
add(Box.createHorizontalStrut(10)) add(Box.createHorizontalStrut(10))
add(documentationLink) add(documentationLink)
}, BorderLayout.WEST) }, BorderLayout.WEST)
add(boxPanel { add(PyPackagesUiComponents.boxPanel {
alignmentX = Component.RIGHT_ALIGNMENT alignmentX = RIGHT_ALIGNMENT
add(progressBar) add(progressBar)
add(versionSelector) add(versionSelector)
add(versionLabel) add(versionLabel)
@@ -266,8 +275,8 @@ class PyPackagingToolWindowPanel(private val project: Project, toolWindow: ToolW
} }
} }
mainPanel = borderPanel { mainPanel = PyPackagesUiComponents.borderPanel {
val topToolbar = boxPanel { val topToolbar = PyPackagesUiComponents.boxPanel {
border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM) border = SideBorder(NamedColorUtil.getBoundsColor(), SideBorder.BOTTOM)
preferredSize = Dimension(preferredSize.width, 30) preferredSize = Dimension(preferredSize.width, 30)
minimumSize = Dimension(minimumSize.width, 30) minimumSize = Dimension(minimumSize.width, 30)

View File

@@ -36,6 +36,7 @@ import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.packaging.management.packagesByRepository import com.jetbrains.python.packaging.management.packagesByRepository
import com.jetbrains.python.packaging.repository.* import com.jetbrains.python.packaging.repository.*
import com.jetbrains.python.packaging.statistics.PythonPackagesToolwindowStatisticsCollector 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.PythonInterpreterTargetEnvironmentFactory
import com.jetbrains.python.run.applyHelperPackageToPythonPath import com.jetbrains.python.run.applyHelperPackageToPythonPath
import com.jetbrains.python.run.buildTargetedCommandLine import com.jetbrains.python.run.buildTargetedCommandLine
@@ -197,29 +198,27 @@ class PyPackagingToolWindowService(val project: Project, val serviceScope: Corou
} }
private suspend fun invokeUpdateLatestVersion() { private suspend fun invokeUpdateLatestVersion() {
serviceScope.launch(Dispatchers.Default) { val proccessPackages = installedPackages
val proccessPackages = installedPackages val updatedPackages = proccessPackages.map { (name, pyPackage: InstalledPackage) ->
val updatedPackages = proccessPackages.map { (name, pyPackage: InstalledPackage) -> val specification = pyPackage.repository.createPackageSpecification(pyPackage.name)
val specification = pyPackage.repository.createPackageSpecification(pyPackage.name) val latestVersion = manager.repositoryManager.getLatestVersion(specification)
val latestVersion = manager.repositoryManager.getLatestVersion(specification) val currentVersion = PyPackageVersionNormalizer.normalize(pyPackage.instance.version)
val currentVersion = PyPackageVersionNormalizer.normalize(pyPackage.instance.version)
val upgradeTo = if (latestVersion != null && currentVersion != null && val upgradeTo = if (latestVersion != null && currentVersion != null &&
PyPackageVersionComparator.compare(latestVersion, currentVersion) > 0) { PyPackageVersionComparator.compare(latestVersion, currentVersion) > 0) {
latestVersion latestVersion
} }
else { else {
null null
} }
name to upgradeTo name to upgradeTo
}.toMap() }.toMap()
installedPackages = installedPackages.map { installedPackages = installedPackages.map {
val newVersion = updatedPackages[it.key] val newVersion = updatedPackages[it.key]
it.key to it.value.withNextVersion(newVersion) it.key to it.value.withNextVersion(newVersion)
}.toMap() }.toMap()
handleSearch(currentQuery) handleSearch(currentQuery)
}
} }
private suspend fun showPackagingNotification(text: @Nls String) { private suspend fun showPackagingNotification(text: @Nls String) {

View File

@@ -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<PythonPackageInstallAction>?
companion object {
val EP_NAME = ExtensionPointName.create<PythonPackagingToolwindowActionProvider>("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<PyPackagingToolWindowService>().installPackage(specification)
}
}

View File

@@ -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<PyPackagingToolWindowService>()
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
}

View File

@@ -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<PyPackagingToolWindowService>()
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
}

View File

@@ -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<PyPackagingToolWindowService>()
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
}

View File

@@ -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)
}

View File

@@ -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<PythonPackageInstallAction>?
companion object {
val EP_NAME = ExtensionPointName.create<PythonPackagingToolwindowActionProvider>("Pythonid.PythonPackagingToolwindowActionProvider")
}
}

View File

@@ -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<PyPackagingToolWindowService>().installPackage(specification)
}
}

View File

@@ -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<PyPackagingToolWindowService>()
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
}

View File

@@ -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. // 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 package com.jetbrains.python.packaging.toolwindow.model
import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.packaging.PyPackageVersion 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) 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) { class InstalledPackage(val instance: PythonPackage, repository: PyPackageRepository, val nextVersion: PyPackageVersion? = null) : DisplayablePackage(instance.name, repository) {
val currentVersion = PyPackageVersionNormalizer.normalize(instance.version)
val canBeUpdated: Boolean val canBeUpdated: Boolean
get() { get() {
val currentVersion = PyPackageVersionNormalizer.normalize(instance.version) ?: return false currentVersion ?: return false
return nextVersion != null && PyPackageVersionComparator.compare(nextVersion, currentVersion) > 0 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 InstallablePackage(name: String, repository: PyPackageRepository) : DisplayablePackage(name, repository)
class ExpandResultNode(var more: Int, repository: PyPackageRepository) : DisplayablePackage("", repository) class ExpandResultNode(var more: Int, repository: PyPackageRepository) : DisplayablePackage("", repository)
open class PyPackagesViewData(@NlsSafe val repository: PyPackageRepository, val packages: List<DisplayablePackage>, val exactMatch: Int = -1, val moreItems: Int = 0) open class PyPackagesViewData(@NlsSafe val repository: PyPackageRepository, val packages: List<DisplayablePackage>, val exactMatch: Int = -1, val moreItems: Int = 0)
class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList()) class PyInvalidRepositoryViewData(repository: PyPackageRepository) : PyPackagesViewData(repository, emptyList())

View File

@@ -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<T : DisplayablePackage>(
project: Project,
model: ListTableModel<T>,
tablesView: PyPackagingTablesView,
val controller: PyPackagingToolWindowPanel,
) : JBTable(model) {
private var lastSelectedRow = -1
internal var hoveredColumn = -1
@Suppress("UNCHECKED_CAST")
private val listModel: ListTableModel<T>
get() = model as ListTableModel<T>
var items: List<T>
get() = listModel.items
set(value) {
listModel.items = value.toMutableList()
}
init {
val service = project.service<PyPackagingToolWindowService>()
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<PyPackagingToolWindowService>().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<T>)
if (result.moreItems > 0) {
node.more = result.moreItems
items = items + listOf(node) as List<T>
}
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"
}
}

View File

@@ -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<T : DisplayablePackage> : ListTableModel<T>() {
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]
}

View File

@@ -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<String>(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<PyPackagingToolWindowService>().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
}
})
}
}
}
}

View File

@@ -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<T : DisplayablePackage>(val repository: PyPackageRepository, val table: PyPackagesTable<T>) {
@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<T>
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()
}
}

View File

@@ -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<TextAttribute, Any>
attributes[TextAttribute.UNDERLINE] = underline
attributes[TextAttribute.LIGATURES] = TextAttribute.LIGATURES_ON
font = font.deriveFont(attributes)
}
}

View File

@@ -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<T : DisplayablePackage>(project: Project,
model: ListTableModel<T>,
tablesView: PyPackagingTablesView,
controller: PyPackagingToolWindowPanel) : JBTable(model) {
private var lastSelectedRow = -1
internal var hoveredColumn = -1
init {
val service = project.service<PyPackagingToolWindowService>()
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<PyPackagingToolWindowService>().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<String>(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<PyPackagingToolWindowService>().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<T>)
if (result.moreItems > 0) {
node.more = result.moreItems
items = items + listOf(node) as List<T>
}
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<T>) = 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<T>
get() = model as ListTableModel<T>
var items: List<T>
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<T : DisplayablePackage> : ListTableModel<T>() {
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<TextAttribute, Any>
attributes[TextAttribute.UNDERLINE] = underline
attributes[TextAttribute.LIGATURES] = TextAttribute.LIGATURES_ON
font = font.deriveFont(attributes)
}
}
internal class PyPackagingTableGroup<T: DisplayablePackage>(val repository: PyPackageRepository, val table: PyPackagesTable<T>) {
@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<T>
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()
}
}

View File

@@ -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<PyPackageCoroutine>().coroutineScope.launch(context, block = body)
fun getIoScope(project: Project): CoroutineScope = project.service<PyPackageCoroutine>().ioScope
fun getScope(project: Project): CoroutineScope = project.service<PyPackageCoroutine>().coroutineScope
}
}