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 + } +}