PY-49929 Packages(fix): Allow selecting multiple packages simultaneously for a quick uninstall \ upgrade

GitOrigin-RevId: df8b02713955006061411b92cb89f40dc25046be
This commit is contained in:
Nikita.Ashihmin
2024-08-24 20:36:33 +04:00
committed by intellij-monorepo-bot
parent 46871c21ac
commit e66c52bd22
16 changed files with 190 additions and 123 deletions

View File

@@ -865,9 +865,10 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<group id="PyPackageToolwindowContext"> <group id="PyPackageToolwindowContext">
<action id="PyInstallPackage" class="com.jetbrains.python.packaging.toolwindow.actions.InstallPackageAction"/> <action id="PyInstallPackage" class="com.jetbrains.python.packaging.toolwindow.actions.InstallPackageAction"/>
<action id="PyInstallWithOptionPackage" class="com.jetbrains.python.packaging.toolwindow.actions.InstallWithOptionsPackageAction"/> <action id="PyInstallWithOptionPackage" class="com.jetbrains.python.packaging.toolwindow.actions.InstallWithOptionsPackageAction"/>
<action id="PyDeletePackage" class="com.jetbrains.python.packaging.toolwindow.actions.DeletePackageAction"/>
<action id="PyChangeVersionPackage" class="com.jetbrains.python.packaging.toolwindow.actions.ChangeVersionPackageAction"/> <action id="PyChangeVersionPackage" class="com.jetbrains.python.packaging.toolwindow.actions.ChangeVersionPackageAction"/>
<action id="PyUpdateToLatestPackage" class="com.jetbrains.python.packaging.toolwindow.actions.UpdatePackageToLatestAction"/> <action id="PyUpdateToLatestPackage" class="com.jetbrains.python.packaging.toolwindow.actions.UpdatePackageToLatestAction"/>
<separator/>
<action id="PyDeletePackage" class="com.jetbrains.python.packaging.toolwindow.actions.DeletePackageAction"/>
</group> </group>
</actions> </actions>

View File

@@ -1529,7 +1529,7 @@ advertiser.code.cells.supported.by.pro=Code cells are supported by PyCharm Profe
action.PyInstallPackage.text=Install action.PyInstallPackage.text=Install
action.PyDeletePackage.text=Uninstall action.PyDeletePackage.text=Uninstall
action.PyUpdateToLatestPackage.text=Update to Latest action.PyUpdateToLatestPackage.text=Update to Latest
action.PyChangeVersionPackage.text=Install Specific Version action.PyChangeVersionPackage.text=Change Version
action.PyInstallWithOptionPackage.text=Install With Options action.PyInstallWithOptionPackage.text=Install With Options
python.toolwindow.packages.deleting.text=Uninstalling\u2026 python.toolwindow.packages.deleting.text=Uninstalling\u2026
progress.text.installing=Installing\u2026 progress.text.installing=Installing\u2026

View File

@@ -0,0 +1,6 @@
// 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.repository
import com.jetbrains.python.PyBundle
class InstalledPyPackagedRepository : PyPackageRepository(PyBundle.message("python.toolwindow.packages.installed.label"), "", "") {}

View File

@@ -5,12 +5,12 @@ import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project 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.InstalledPyPackagedRepository
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.model.*
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTable import com.jetbrains.python.packaging.toolwindow.packages.PyPackagingTableGroup
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTableModel import com.jetbrains.python.packaging.toolwindow.packages.table.PyPackagesTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents 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
@@ -19,11 +19,12 @@ import javax.swing.JTable
class PyPackagingTablesView(private val project: Project, class PyPackagingTablesView(private val project: Project,
private val container: JPanel, private val container: JPanel,
private val controller: PyPackagingToolWindowPanel) { private val controller: PyPackagingToolWindowPanel) {
private val repositories: MutableList<PyPackagingTableGroup<DisplayablePackage>> = mutableListOf() private val repositories: MutableList<PyPackagingTableGroup> = mutableListOf()
private val installedPackages = PyPackagingTableGroup( private val installedPackages = PyPackagingTableGroup(
object : PyPackageRepository(message("python.toolwindow.packages.installed.label"), "", "") {}, InstalledPyPackagedRepository(),
PyPackagesTable(project, PyPackagesTableModel(), this, controller)) PyPackagesTable(project, this, controller))
private val invalidRepositories: MutableMap<String, JPanel> = mutableMapOf() private val invalidRepositories: MutableMap<String, JPanel> = mutableMapOf()
init { init {
installedPackages.addTo(container) installedPackages.addTo(container)
installedPackages.expand() installedPackages.expand()
@@ -31,8 +32,10 @@ class PyPackagingTablesView(private val project: Project,
fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) { fun showSearchResult(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
updatePackages(installed, repoData) updatePackages(installed, repoData)
installedPackages.expand() installedPackages.expand()
installedPackages.updateHeaderText(installed.size) installedPackages.updateHeaderText(installed.size)
val tableToData = repositories.map { repo -> repo to repoData.find { it.repository.name == repo.name }!! } val tableToData = repositories.map { repo -> repo to repoData.find { it.repository.name == repo.name }!! }
tableToData.forEach { (table, data) -> tableToData.forEach { (table, data) ->
table.updateHeaderText(data.packages.size + data.moreItems) table.updateHeaderText(data.packages.size + data.moreItems)
@@ -47,17 +50,21 @@ class PyPackagingTablesView(private val project: Project,
fun resetSearch(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) { fun resetSearch(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
updatePackages(installed, repoData) updatePackages(installed, repoData)
installedPackages.expand() installedPackages.expand()
installedPackages.updateHeaderText(null) installedPackages.updateHeaderText(null)
repositories.forEach { repositories.forEach {
it.collapse() it.collapse()
it.updateHeaderText(null) it.updateHeaderText(null)
} }
container.scrollRectToVisible(Rectangle(0, 0)) container.scrollRectToVisible(Rectangle(0, 0))
} }
private fun updatePackages(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) { private fun updatePackages(installed: List<InstalledPackage>, repoData: List<PyPackagesViewData>) {
installedPackages.table.items = installed installedPackages.table.items = installed
val (validRepoData, invalid) = repoData.partition { it !is PyInvalidRepositoryViewData } val (validRepoData, invalid) = repoData.partition { it !is PyInvalidRepositoryViewData }
for (data in validRepoData) { for (data in validRepoData) {
@@ -73,7 +80,7 @@ class PyPackagingTablesView(private val project: Project,
selectedItem?.let { existingRepo.table.selectPackage(it) } selectedItem?.let { existingRepo.table.selectPackage(it) }
} }
else { else {
val newTable = PyPackagesTable(project, PyPackagesTableModel(), this, controller) val newTable = PyPackagesTable(project, this, controller)
newTable.items = withExpander newTable.items = withExpander
val newTableGroup = PyPackagingTableGroup(data.repository, newTable) val newTableGroup = PyPackagingTableGroup(data.repository, newTable)
@@ -129,7 +136,7 @@ class PyPackagingTablesView(private val project: Project,
tableWithMatch.setRowSelectionInterval(exactMatch, exactMatch) tableWithMatch.setRowSelectionInterval(exactMatch, exactMatch)
} }
fun requestSelection(table: JTable) { fun removeSelectionNotFormTable(table: JTable) {
if (table != installedPackages.table) installedPackages.table.clearSelection() if (table != installedPackages.table) installedPackages.table.clearSelection()
repositories.asSequence() repositories.asSequence()
.filter { it.table != table } .filter { it.table != table }
@@ -178,4 +185,9 @@ class PyPackagingTablesView(private val project: Project,
} }
} }
} }
fun getSelectedPackages(): List<DisplayablePackage> {
val repos = listOf(installedPackages) + repositories
return repos.flatMap { it.table.selectedItems() }
}
} }

View File

@@ -137,6 +137,7 @@ class PyPackagingToolWindowPanel(private val project: Project) : SimpleToolWindo
override fun uiDataSnapshot(sink: DataSink) { override fun uiDataSnapshot(sink: DataSink) {
sink[PyPackagesUiComponents.SELECTED_PACKAGE_DATA_CONTEXT] = descriptionController.selectedPackage.get() sink[PyPackagesUiComponents.SELECTED_PACKAGE_DATA_CONTEXT] = descriptionController.selectedPackage.get()
sink[PyPackagesUiComponents.SELECTED_PACKAGES_DATA_CONTEXT] = this.packageListController.getSelectedPackages()
super.uiDataSnapshot(sink) super.uiDataSnapshot(sink)
} }

View File

@@ -10,6 +10,7 @@ import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackages
import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -31,8 +32,7 @@ internal class ChangeVersionPackageAction : DumbAwareAction() {
} }
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
val pkg = e.selectedPackage as? InstalledPackage e.presentation.isEnabledAndVisible = e.selectedPackage as? InstalledPackage != null && e.selectedPackages.size == 1
e.presentation.isEnabledAndVisible = pkg != null
} }
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT

View File

@@ -8,12 +8,23 @@ import com.intellij.openapi.project.DumbAwareAction
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackages
import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
internal class DeletePackageAction : DumbAwareAction() { internal class DeletePackageAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) { override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return val project = e.project ?: return
val selectedPackages = e.selectedPackages.filterIsInstance<InstalledPackage>()
if (selectedPackages.size > 1) {
PyPackageCoroutine.launch(project, Dispatchers.IO) {
selectedPackages.forEach { pkg ->
project.service<PyPackagingToolWindowService>().deletePackage(pkg)
}
}
return
}
val pkg = e.selectedPackage as? InstalledPackage ?: return val pkg = e.selectedPackage as? InstalledPackage ?: return
val service = project.service<PyPackagingToolWindowService>() val service = project.service<PyPackagingToolWindowService>()
@@ -23,7 +34,7 @@ internal class DeletePackageAction : DumbAwareAction() {
} }
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.selectedPackage is InstalledPackage e.presentation.isEnabledAndVisible = e.selectedPackages.all { it is InstalledPackage }
} }
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT

View File

@@ -3,12 +3,14 @@ package com.jetbrains.python.packaging.toolwindow.actions
import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.service
import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.DumbAwareAction
import com.intellij.ui.awt.RelativePoint import com.intellij.ui.awt.RelativePoint
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackages
import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -17,9 +19,20 @@ import java.awt.event.MouseEvent
internal class InstallPackageAction : DumbAwareAction() { internal class InstallPackageAction : DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) { override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return val project = e.project ?: return
val selectedPackages = e.selectedPackages.filterIsInstance<InstallablePackage>()
if (selectedPackages.size > 1) {
PyPackageCoroutine.launch(project, Dispatchers.IO) {
selectedPackages.forEach { pkg ->
val specification = pkg.repository.createPackageSpecification(pkg.name, null)
project.service<PyPackagingToolWindowService>().installPackage(specification)
}
}
return
}
val pkg = e.selectedPackage as? InstallablePackage ?: return val pkg = e.selectedPackage as? InstallablePackage ?: return
PyPackageCoroutine.launch(project, Dispatchers.IO) { PyPackageCoroutine.launch(project, Dispatchers.IO) {
val service = PyPackagingToolWindowService.getInstance(project) val service = PyPackagingToolWindowService.getInstance(project)
val details = service.detailsForPackage(pkg) val details = service.detailsForPackage(pkg)
@@ -32,7 +45,7 @@ internal class InstallPackageAction : DumbAwareAction() {
} }
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.selectedPackage is InstallablePackage e.presentation.isEnabledAndVisible = e.selectedPackages.all { it is InstallablePackage }
} }
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT

View File

@@ -13,6 +13,7 @@ import com.jetbrains.python.packaging.common.PythonPackageDetails
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackage
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents.selectedPackages
import com.jetbrains.python.packaging.utils.PyPackageCoroutine import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -32,7 +33,7 @@ internal class InstallWithOptionsPackageAction : DumbAwareAction() {
} }
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = e.selectedPackage is InstallablePackage e.presentation.isEnabledAndVisible = e.selectedPackage as? InstallablePackage != null && e.selectedPackages.size == 1
} }
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT

View File

@@ -8,6 +8,7 @@ import com.intellij.ui.ScrollPaneFactory
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.model.PyPackagesViewData import com.jetbrains.python.packaging.toolwindow.model.PyPackagesViewData
import javax.swing.BoxLayout import javax.swing.BoxLayout
@@ -37,4 +38,8 @@ class PyPackagesListController(val project: Project, val controller: PyPackaging
fun selectPackage(name: String) { fun selectPackage(name: String) {
tablesView.selectPackage(name) tablesView.selectPackage(name)
} }
fun getSelectedPackages(): List<DisplayablePackage> {
return tablesView.getSelectedPackages()
}
} }

View File

@@ -1,15 +1,17 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. // 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 package com.jetbrains.python.packaging.toolwindow.packages
import com.intellij.icons.AllIcons import com.intellij.icons.AllIcons
import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.repository.PyPackageRepository import com.jetbrains.python.packaging.repository.PyPackageRepository
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.packages.table.PyPackagesTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import javax.swing.JLabel import javax.swing.JLabel
import javax.swing.JPanel import javax.swing.JPanel
internal class PyPackagingTableGroup<T : DisplayablePackage>(val repository: PyPackageRepository, val table: PyPackagesTable<T>) { internal class PyPackagingTableGroup(val repository: PyPackageRepository, val table: PyPackagesTable) {
@NlsSafe @NlsSafe
val name: String = repository.name!! val name: String = repository.name!!
@@ -19,7 +21,7 @@ internal class PyPackagingTableGroup<T : DisplayablePackage>(val repository: PyP
private var itemsCount: Int? = null private var itemsCount: Int? = null
internal var items: List<T> internal var items: List<DisplayablePackage>
get() = table.items get() = table.items
set(value) { set(value) {
table.items = value table.items = value
@@ -47,11 +49,6 @@ internal class PyPackagingTableGroup<T : DisplayablePackage>(val repository: PyP
panel.add(table) panel.add(table)
} }
fun replace(row: Int, pkg: T) {
table.removeRow(row)
table.insertRow(row, pkg)
}
fun removeFrom(panel: JPanel) { fun removeFrom(panel: JPanel) {
panel.remove(header) panel.remove(header)
panel.remove(table) panel.remove(table)

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. // 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 package com.jetbrains.python.packaging.toolwindow.packages
import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.hover.TableHoverListener import com.intellij.ui.hover.TableHoverListener
@@ -10,6 +10,8 @@ import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import com.jetbrains.python.packaging.toolwindow.packages.table.PyPackagesTable
import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesUiComponents
import java.awt.Component import java.awt.Component
import java.awt.font.TextAttribute import java.awt.font.TextAttribute
import javax.swing.BoxLayout import javax.swing.BoxLayout
@@ -20,7 +22,9 @@ import javax.swing.table.DefaultTableCellRenderer
internal class PyPaginationAwareRenderer : DefaultTableCellRenderer() { internal class PyPaginationAwareRenderer : DefaultTableCellRenderer() {
private val nameLabel = JLabel().apply { border = JBUI.Borders.empty(0, 12) } private val nameLabel = JLabel().apply { border = JBUI.Borders.empty(0, 12) }
private val versionLabel = JLabel().apply { border = JBUI.Borders.emptyRight(12) } private val versionLabel = JLabel().apply { border = JBUI.Borders.emptyRight(12) }
private val linkLabel = JLabel(PyBundle.message("action.python.packages.install.text")).apply { private val linkLabel = JLabel(PyBundle.message("action.python.packages.install.text")).apply {
border = JBUI.Borders.emptyRight(12) border = JBUI.Borders.emptyRight(12)
foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED foreground = JBUI.CurrentTheme.Link.Foreground.ENABLED
@@ -45,7 +49,7 @@ internal class PyPaginationAwareRenderer : DefaultTableCellRenderer() {
row: Int, row: Int,
column: Int, column: Int,
): Component { ): Component {
val rowSelected = table.selectedRow == row val rowSelected = row in table.selectedRows
val tableFocused = table.hasFocus() val tableFocused = table.hasFocus()
if (value is ExpandResultNode) { if (value is ExpandResultNode) {
@@ -101,7 +105,7 @@ internal class PyPaginationAwareRenderer : DefaultTableCellRenderer() {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun JLabel.updateUnderline(table: JTable, currentRow: Int) { private fun JLabel.updateUnderline(table: JTable, currentRow: Int) {
val hoveredRow = TableHoverListener.getHoveredRow(table) val hoveredRow = TableHoverListener.getHoveredRow(table)
val hoveredColumn = (table as PyPackagesTable<*>).hoveredColumn val hoveredColumn = (table as PyPackagesTable).hoveredColumn
val underline = if (hoveredRow == currentRow && hoveredColumn == 1) TextAttribute.UNDERLINE_ON else -1 val underline = if (hoveredRow == currentRow && hoveredColumn == 1) TextAttribute.UNDERLINE_ON else -1
val attributes = font.attributes as MutableMap<TextAttribute, Any> val attributes = font.attributes as MutableMap<TextAttribute, Any>

View File

@@ -0,0 +1,45 @@
// 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.packages.table
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.hover.TableHoverListener
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.PyPackagesUiComponents
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
internal class PyPackageTableMouseAdapter(private val table: PyPackagesTable) : MouseAdapter() {
val project: Project = table.project
val service
get() = project.service<PyPackagingToolWindowService>()
override fun mouseClicked(e: MouseEvent) {
if (e.clickCount != 1 || table.columnAtPoint(e.point) != 1) return // double click or click on package name column, nothing to be done
val hoveredRow = TableHoverListener.getHoveredRow(table)
val selectedPackage = table.items[hoveredRow]
if (selectedPackage is InstallablePackage) {
PyPackageCoroutine.launch(project, Dispatchers.IO) {
val details = service.detailsForPackage(selectedPackage)
withContext(Dispatchers.Main) {
PyPackagesUiComponents.createAvailableVersionsPopup(selectedPackage, details, project).show(RelativePoint(e))
}
}
}
else if (selectedPackage is InstalledPackage && selectedPackage.canBeUpdated) {
PyPackageCoroutine.launch(project, Dispatchers.IO) {
val nextVersion = selectedPackage.nextVersion ?: return@launch
val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name, nextVersion.presentableText)
project.service<PyPackagingToolWindowService>().updatePackage(specification)
}
}
}
}

View File

@@ -0,0 +1,25 @@
// 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.packages.table
import com.intellij.ui.hover.TableHoverListener
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage
import java.awt.Cursor
import javax.swing.JTable
internal class PyPackagesHoverListener(private val pyPackageTable: PyPackagesTable) : TableHoverListener() {
override fun onHover(table: JTable, row: Int, column: Int) {
pyPackageTable.hoveredColumn = column
if (column == 1) {
table.repaint(table.getCellRect(row, column, true))
val currentPackage = pyPackageTable.items[row]
if (currentPackage is InstallablePackage
|| (currentPackage is InstalledPackage && currentPackage.canBeUpdated)) {
table.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
return
}
}
table.cursor = Cursor.getDefaultCursor()
}
}

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. // 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 package com.jetbrains.python.packaging.toolwindow.packages.table
import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
@@ -8,58 +8,43 @@ import com.intellij.openapi.project.Project
import com.intellij.ui.DoubleClickListener import com.intellij.ui.DoubleClickListener
import com.intellij.ui.PopupHandler import com.intellij.ui.PopupHandler
import com.intellij.ui.SideBorder 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.ui.table.JBTable
import com.intellij.util.ui.ListTableModel
import com.intellij.util.ui.NamedColorUtil import com.intellij.util.ui.NamedColorUtil
import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView import com.jetbrains.python.packaging.toolwindow.PyPackagingTablesView
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowPanel
import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService import com.jetbrains.python.packaging.toolwindow.PyPackagingToolWindowService
import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage import com.jetbrains.python.packaging.toolwindow.model.DisplayablePackage
import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode import com.jetbrains.python.packaging.toolwindow.model.ExpandResultNode
import com.jetbrains.python.packaging.toolwindow.model.InstallablePackage import com.jetbrains.python.packaging.toolwindow.packages.PyPaginationAwareRenderer
import com.jetbrains.python.packaging.toolwindow.model.InstalledPackage import com.jetbrains.python.packaging.toolwindow.ui.PyPackagesTableModel
import com.jetbrains.python.packaging.utils.PyPackageCoroutine
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.ActionEvent
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import javax.swing.AbstractAction import javax.swing.AbstractAction
import javax.swing.JTable
import javax.swing.KeyStroke import javax.swing.KeyStroke
import javax.swing.ListSelectionModel import javax.swing.ListSelectionModel
import javax.swing.table.TableCellRenderer
internal class PyPackagesTable<T : DisplayablePackage>( internal class PyPackagesTable(
project: Project, val project: Project,
model: ListTableModel<T>,
tablesView: PyPackagingTablesView, tablesView: PyPackagingTablesView,
val controller: PyPackagingToolWindowPanel, val controller: PyPackagingToolWindowPanel,
) : JBTable(model) { ) : JBTable(PyPackagesTableModel<DisplayablePackage>()) {
private val scope = PyPackageCoroutine.getIoScope(project)
private var lastSelectedElement: DisplayablePackage? = null
internal var hoveredColumn = -1 internal var hoveredColumn = -1
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private val listModel: ListTableModel<T> val model: PyPackagesTableModel<DisplayablePackage> = getModel() as PyPackagesTableModel<DisplayablePackage>
get() = model as ListTableModel<T>
var items: List<T> var items: List<DisplayablePackage>
get() = listModel.items get() = model.items
set(value) { set(value) {
listModel.items = value.toMutableList() model.items = value.toMutableList()
} }
init { init {
val service = project.service<PyPackagingToolWindowService>() val service = project.service<PyPackagingToolWindowService>()
setShowGrid(false) setShowGrid(false)
setSelectionMode(ListSelectionModel.SINGLE_SELECTION) setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
val column = columnModel.getColumn(1) val column = columnModel.getColumn(1)
column.minWidth = 130 column.minWidth = 130
column.maxWidth = 130 column.maxWidth = 130
@@ -69,51 +54,14 @@ internal class PyPackagesTable<T : DisplayablePackage>(
initCrossNavigation(service, tablesView) initCrossNavigation(service, tablesView)
val hoverListener = object : TableHoverListener() { val hoverListener = PyPackagesHoverListener(this)
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) hoverListener.addTo(this)
addMouseListener(object : MouseAdapter() { addMouseListener(PyPackageTableMouseAdapter(this))
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) {
scope.launch(Dispatchers.IO) {
val details = service.detailsForPackage(selectedPackage)
withContext(Dispatchers.Main) {
PyPackagesUiComponents.createAvailableVersionsPopup(selectedPackage, details, project).show(RelativePoint(e))
}
}
}
else if (selectedPackage is InstalledPackage && selectedPackage.canBeUpdated) {
scope.launch(Dispatchers.IO) {
val specification = selectedPackage.repository.createPackageSpecification(selectedPackage.name,
selectedPackage.nextVersion!!.presentableText)
project.service<PyPackagingToolWindowService>().updatePackage(specification)
}
}
}
})
selectionModel.addListSelectionListener { selectionModel.addListSelectionListener {
tablesView.requestSelection(this) tablesView.removeSelectionNotFormTable(this)
val pkg = model.items.getOrNull(selectedRow) val pkg = selectedItem()
lastSelectedElement = pkg
if (pkg != null && pkg !is ExpandResultNode) { if (pkg != null && pkg !is ExpandResultNode) {
controller.packageSelected(pkg) controller.packageSelected(pkg)
} }
@@ -124,8 +72,9 @@ internal class PyPackagesTable<T : DisplayablePackage>(
object : DoubleClickListener() { object : DoubleClickListener() {
override fun onDoubleClick(event: MouseEvent): Boolean { override fun onDoubleClick(event: MouseEvent): Boolean {
val pkg = model.items[selectedRow] val pkg = selectedItem() ?: return true
if (pkg is ExpandResultNode) loadMoreItems(service, pkg) if (pkg is ExpandResultNode)
loadMoreItems(service, pkg)
return true return true
} }
}.installOn(this) }.installOn(this)
@@ -134,8 +83,20 @@ internal class PyPackagesTable<T : DisplayablePackage>(
PopupHandler.installPopupMenu(this, packageActionGroup, "PackagePopup") PopupHandler.installPopupMenu(this, packageActionGroup, "PackagePopup")
} }
override fun getCellRenderer(row: Int, column: Int) = PyPaginationAwareRenderer()
fun selectedItem(): T? = items.getOrNull(selectedRow) fun selectedItem(): DisplayablePackage? = items.getOrNull(selectedRow)
fun selectedItems(): Sequence<DisplayablePackage> {
return selectedRows.asSequence().mapNotNull { items.getOrNull(it) }
}
fun selectPackage(pkg: DisplayablePackage) {
val index = items.indexOf(pkg)
if (index != -1) {
setRowSelectionInterval(index, index)
}
}
private fun initCrossNavigation(service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) { private fun initCrossNavigation(service: PyPackagingToolWindowService, tablesView: PyPackagingTablesView) {
getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ENTER_ACTION) getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), ENTER_ACTION)
@@ -181,43 +142,20 @@ internal class PyPackagesTable<T : DisplayablePackage>(
}) })
} }
@Suppress("UNCHECKED_CAST")
private fun loadMoreItems(service: PyPackagingToolWindowService, node: ExpandResultNode) { private fun loadMoreItems(service: PyPackagingToolWindowService, node: ExpandResultNode) {
val result = service.getMoreResultsForRepo(node.repository, items.size - 1) val result = service.getMoreResultsForRepo(node.repository, items.size - 1)
items = items.dropLast(1) + (result.packages as List<T>) items = items.dropLast(1) + result.packages
if (result.moreItems > 0) { if (result.moreItems > 0) {
node.more = result.moreItems node.more = result.moreItems
items = items + listOf(node) as List<T> items = items + listOf(node)
} }
this@PyPackagesTable.revalidate() this@PyPackagesTable.revalidate()
this@PyPackagesTable.repaint() this@PyPackagesTable.repaint()
} }
override fun getCellRenderer(row: Int, column: Int): TableCellRenderer {
return PyPaginationAwareRenderer()
}
fun selectPackage(pkg: DisplayablePackage) {
val index = items.indexOf(pkg)
if (index != -1) {
setRowSelectionInterval(index, index)
}
}
override fun clearSelection() {
lastSelectedElement = null
super.clearSelection()
}
internal fun removeRow(index: Int) = listModel.removeRow(index)
internal fun insertRow(index: Int, pkg: T) = listModel.insertRow(index, pkg)
companion object { companion object {
private const val NEXT_ROW_ACTION = "selectNextRow" private const val NEXT_ROW_ACTION = "selectNextRow"
private const val PREVIOUS_ROW_ACTION = "selectPreviousRow" private const val PREVIOUS_ROW_ACTION = "selectPreviousRow"
private const val ENTER_ACTION = "ENTER" private const val ENTER_ACTION = "ENTER"
} }
} }

View File

@@ -29,6 +29,7 @@ import javax.swing.*
object PyPackagesUiComponents { object PyPackagesUiComponents {
val SELECTED_PACKAGE_DATA_CONTEXT = DataKey.create<DisplayablePackage>("SELECTED_PACKAGE_DATA_CONTEXT") val SELECTED_PACKAGE_DATA_CONTEXT = DataKey.create<DisplayablePackage>("SELECTED_PACKAGE_DATA_CONTEXT")
val SELECTED_PACKAGES_DATA_CONTEXT = DataKey.create<List<DisplayablePackage>>("SELECTED_PACKAGES_DATA_CONTEXT")
private val DataContext.selectedPackage: DisplayablePackage? private val DataContext.selectedPackage: DisplayablePackage?
get() = getData(SELECTED_PACKAGE_DATA_CONTEXT) get() = getData(SELECTED_PACKAGE_DATA_CONTEXT)
@@ -36,6 +37,13 @@ object PyPackagesUiComponents {
internal val AnActionEvent.selectedPackage: DisplayablePackage? internal val AnActionEvent.selectedPackage: DisplayablePackage?
get() = dataContext.selectedPackage get() = dataContext.selectedPackage
private val DataContext.selectedPackages: List<DisplayablePackage>
get() = getData(SELECTED_PACKAGES_DATA_CONTEXT) ?: emptyList()
internal val AnActionEvent.selectedPackages: List<DisplayablePackage>
get() = dataContext.selectedPackages
fun createAvailableVersionsPopup(selectedPackage: DisplayablePackage, details: PythonPackageDetails, project: Project): ListPopup { fun createAvailableVersionsPopup(selectedPackage: DisplayablePackage, details: PythonPackageDetails, project: Project): ListPopup {
return JBPopupFactory.getInstance().createListPopup(object : BaseListPopupStep<String>(null, details.availableVersions) { return JBPopupFactory.getInstance().createListPopup(object : BaseListPopupStep<String>(null, details.availableVersions) {
override fun onChosen(selectedValue: String?, finalChoice: Boolean): PopupStep<*>? { override fun onChosen(selectedValue: String?, finalChoice: Boolean): PopupStep<*>? {