From 5ce446dd3d93916ee735a65070979758e3eade6f Mon Sep 17 00:00:00 2001 From: Fabrizio Scarponi <36624359+fscarponi@users.noreply.github.com> Date: Wed, 10 Jan 2024 14:05:34 +0100 Subject: [PATCH] Introduce InfoPanelContent attributes and update UI in HeaderAttributesTab (#9) Refactor platformListMock to getter in HeaderAttributesTab scrollToAttribute has CoroutineScope as receiver rename InfoPanelContentEvent Attributes Declared into FromVariant and Search into FromSearch rename Attributes Declared into FromVariant and Search into FromSearch This commit adds InfoPanelContent Attributes for "Declared" type to the asPanelContent function, including the tab's title, variant's title, and attributes. A click event handler for the selected packages is also added to refresh the side panel content. A new event for package selection has been placed in 'PackageListItemEvent'. Furthermore, the attribute type name description and content title in the package search UI have been updated for clarity. The style of the 'Search' content in HeaderAttributesTab has also been modified to reduce redundancy and clarify display conditions. Now, by clicking on an attribute badge, it's possible to scroll to individual attributes with a smooth animation. (cherry picked from commit 259a0d297dbc7f11f61e42a23b625f7297461781) --- packagesearch.versions.toml | 1 + .../plugin/ui/PackageSearchColors.kt | 17 ++ .../plugin/ui/PackageSearchPackagePanel.kt | 1 - .../plugin/ui/bridge/Components.kt | 47 ++- .../packagesearch/plugin/ui/bridge/Utils.kt | 1 - .../packagesearch/plugin/ui/model/Utils.kt | 2 +- .../ui/model/infopanel/InfoPanelContent.kt | 18 ++ .../model/infopanel/InfoPanelContentEvent.kt | 19 +- .../ui/model/infopanel/InfoPanelViewModel.kt | 35 +++ .../plugin/ui/model/infopanel/Utils.kt | 4 +- .../ui/model/infopanel/asPanelContent.kt | 26 ++ .../packageslist/PackageListItemEvent.kt | 22 +- .../packageslist/PackageListViewModel.kt | 50 ++- .../packages/PackageSearchPackageList.kt | 7 +- .../packages/items/PackageGroupHeader.kt | 43 ++- .../ui/panels/side/HeaderAttributesTab.kt | 286 ++++++++++++++++++ .../ui/panels/side/PackageSearchInfoPanel.kt | 8 + .../panels/tree/PackageSearchModulesTree.kt | 5 +- .../messages/packageSearchBundle.properties | 7 +- 19 files changed, 565 insertions(+), 34 deletions(-) create mode 100644 plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt create mode 100644 plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt diff --git a/packagesearch.versions.toml b/packagesearch.versions.toml index 5a21e761..64c1e6cd 100644 --- a/packagesearch.versions.toml +++ b/packagesearch.versions.toml @@ -37,6 +37,7 @@ ij-platform-ide-core = { module = "com.jetbrains.intellij.platform:ide-core", ve ij-platform-ide-impl = { module = "com.jetbrains.intellij.platform:ide-impl", version.ref = "idea" } jewel-ui = { module = "org.jetbrains.jewel:jewel-ui", version.ref = "jewel" } jewel-foundation = { module = "org.jetbrains.jewel:jewel-foundation", version.ref = "jewel" } +jewel-standalone = { module= "org.jetbrains.jewel:jewel-int-ui-standalone", version.ref = "jewel" } jewel-bridge-ij232 = { module = "org.jetbrains.jewel:jewel-ide-laf-bridge", version.ref = "jewel-bridge-232" } jewel-bridge-ij233 = { module = "org.jetbrains.jewel:jewel-ide-laf-bridge", version.ref = "jewel-bridge-233" } compose-desktop-components-splitpane = { module = "org.jetbrains.compose.components:components-splitpane", version.ref = "composeDesktop" } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt new file mode 100644 index 00000000..01beec23 --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchColors.kt @@ -0,0 +1,17 @@ +package com.jetbrains.packagesearch.plugin.ui + +import androidx.compose.ui.graphics.Color +import com.jetbrains.packagesearch.plugin.ui.bridge.pickComposeColorFromLaf + +object PackageSearchColors { + object Backgrounds { + fun packageItemHeader(): Color = + pickComposeColorFromLaf("ToolWindow.HeaderTab.selectedInactiveBackground") + + + fun attributeBadge() = packageItemHeader() + + } + + +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt index 86143fb4..ef36d5ec 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/PackageSearchPackagePanel.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.intellij.ui.JBColor import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule import com.jetbrains.packagesearch.plugin.ui.bridge.packageSearchSplitter diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt index 92198ed7..abe57e57 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Components.kt @@ -4,7 +4,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.onClick +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -23,9 +26,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import com.intellij.icons.AllIcons +import com.jetbrains.packagesearch.plugin.ui.PackageSearchColors import com.jetbrains.packagesearch.plugin.ui.PackageSearchMetrics import java.awt.Cursor -import org.jetbrains.compose.splitpane.SplitPaneScope import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.LocalTextStyle import org.jetbrains.jewel.ui.component.DropdownLink @@ -34,6 +37,7 @@ import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.MenuScope import org.jetbrains.jewel.ui.component.PopupMenu import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.compose.splitpane.SplitPaneScope @Composable fun LabelInfo( @@ -141,7 +145,6 @@ internal fun PackageActionPopup( } - internal fun SplitPaneScope.packageSearchSplitter( splitterColor: Color, cursor: PointerIcon = PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR)), @@ -171,3 +174,43 @@ internal fun SplitPaneScope.packageSearchSplitter( } } } + + +@Composable +internal fun AttributeBadge(text: String, onClick: () -> Unit) { + val isDark = JewelTheme.isDark + val background = remember(isDark) { + PackageSearchColors.Backgrounds.attributeBadge() + } + + Box( + modifier = Modifier + .background(color = background, shape = RoundedCornerShape(12.dp)) + .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))) + .onClick { onClick() }, + ) { + Text( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), text = text, + ) + } + +} + +//@Preview +//@Composable +//internal fun AttributeBadgePreview() { +// Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { +// IntUiTheme { +// Box(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// AttributeBadge(text = "Android") {} +// } +// } +// IntUiTheme(true) { +// Box(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// AttributeBadge(text = "Android") {} +// } +// } +// } +// +// +//} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt index 772738c9..a81e88f3 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/bridge/Utils.kt @@ -41,4 +41,3 @@ fun isLightTheme(): Boolean { private fun Color.getBrightness() = (red * 299 + green * 587 + blue * 114) / 1000 -fun Modifier.pointerChangeToHandModifier() = this.pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))) diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt index a72338df..8fc1f628 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/Utils.kt @@ -32,4 +32,4 @@ internal val Project.isProjectImportingFlow trySend(false) } } - }.withInitialValue(false) \ No newline at end of file + }.withInitialValue(false) diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt index b95b719b..d4004664 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContent.kt @@ -2,6 +2,7 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.core.data.IconProvider import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem sealed interface InfoPanelContent { @@ -140,4 +141,21 @@ sealed interface InfoPanelContent { } } + sealed interface Attributes : InfoPanelContent{ + val attributes: List + + data class FromVariant( + override val tabTitle: String, + val variantName: String, + override val attributes: List, + ) : Attributes + + data class FromSearch( + override val tabTitle: String, + override val attributes: List, + val defaultSourceSet: String, + val additionalSourceSets: List, + ) : Attributes + } + } \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt index 79f5a506..28da25d2 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelContentEvent.kt @@ -2,14 +2,14 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.core.data.PackageSearchDeclaredPackage import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import org.jetbrains.packagesearch.api.v3.ApiPackage sealed interface InfoPanelContentEvent { - val module: PackageSearchModule - sealed interface Package : InfoPanelContentEvent { + val module: PackageSearchModule val packageListId: PackageListItem.Package.Id @@ -51,5 +51,20 @@ sealed interface InfoPanelContentEvent { } } + sealed interface Attributes : InfoPanelContentEvent { + val attributes: List + + data class FromVariant( + val variantName: String, + override val attributes: List, + ) : Attributes + + data class FromSearch( + val defaultVariant: String, + val additionalVariants: List, + override val attributes: List, + ) : Attributes + + } } \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt index bbb34cbf..c008b068 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/InfoPanelViewModel.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.jetbrains.packagesearch.plugin.core.data.PackageSearchDeclaredPackage import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModule +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListViewModel import com.jetbrains.packagesearch.plugin.utils.PackageSearchProjectService @@ -62,6 +63,14 @@ class InfoPanelViewModel( } } } + + is InfoPanelContentEvent.Attributes.FromVariant -> { + event.asPanelContent() + } + + is InfoPanelContentEvent.Attributes.FromSearch -> { + event.asPanelContent() + } } } .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -134,6 +143,32 @@ class InfoPanelViewModel( ) } + fun setDeclaredHeaderAttributes( + variantName: String, + attributes: List, + ) { + setDataEventChannel.trySend( + InfoPanelContentEvent.Attributes.FromVariant( + variantName = variantName, + attributes = attributes + ) + ) + } + + fun setSearchHeaderAttributes( + defaultVariant: String, + additionalVariants: List, + attributes: List, + ) { + setDataEventChannel.trySend( + InfoPanelContentEvent.Attributes.FromSearch( + defaultVariant = defaultVariant, + additionalVariants = additionalVariants, + attributes = attributes + ) + ) + } + } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt index 3d237a05..b62d6d07 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/Utils.kt @@ -1,6 +1,7 @@ package com.jetbrains.packagesearch.plugin.ui.model.infopanel import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message +import org.jetbrains.jewel.foundation.lazy.tree.buildTree import org.jetbrains.packagesearch.api.v3.ApiGitHub import org.jetbrains.packagesearch.api.v3.ApiScm import org.jetbrains.packagesearch.api.v3.LicenseFile @@ -24,4 +25,5 @@ internal fun Licenses<*>.asInfoPanelLicenseList() = buildList { internal fun LicenseFile.toInfoPanelLicense(): InfoPanelContent.PackageInfo.License? { val name = name ?: url ?: return null return InfoPanelContent.PackageInfo.License(name, url) -} \ No newline at end of file +} + diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt index f2dc380a..61b0f4e5 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/infopanel/asPanelContent.kt @@ -126,6 +126,11 @@ internal fun InfoPanelContentEvent.Package.Declared.WithVariant.asPanelContent( declaredVariant = variantName, allowMissingScope = !module.dependencyMustHaveAScope, variantTerminology = module.variantTerminology + ), + InfoPanelContent.Attributes.FromVariant( + variantName = variantName, + tabTitle = message("packagesearch.ui.toolwindow.sidepanel.platforms"), + attributes = module.variants.getValue(variantName).attributes ) ) @@ -152,6 +157,11 @@ internal fun InfoPanelContentEvent.Package.Remote.WithVariants.asPanelContent( isLoading = isLoading, isInstalledInPrimaryVariant = module.variants.getValue(primaryVariantName).declaredDependencies .any { it.id == apiPackage.id } + ), + InfoPanelContent.Attributes.FromVariant( + tabTitle = message("packagesearch.ui.toolwindow.sidepanel.platforms"), + variantName = primaryVariantName, + attributes = module.variants.getValue(primaryVariantName).attributes ) ) @@ -175,4 +185,20 @@ internal fun InfoPanelContentEvent.Package.Remote.Base.asPanelContent( repositories = apiPackage.repositories(), isLoading = isLoading ) +) + +internal fun InfoPanelContentEvent.Attributes.FromVariant.asPanelContent() = listOf( + InfoPanelContent.Attributes.FromVariant( + variantName = variantName, + tabTitle = message("packagesearch.ui.toolwindow.sidepanel.platforms"), + attributes = attributes, + ) +) +internal fun InfoPanelContentEvent.Attributes.FromSearch.asPanelContent() = listOf( + InfoPanelContent.Attributes.FromSearch( + tabTitle = message("packagesearch.ui.toolwindow.packages.details.info.attributes"), + defaultSourceSet = defaultVariant, + additionalSourceSets = additionalVariants, + attributes = attributes, + ) ) \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt index 1a0f27f7..62233de8 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListItemEvent.kt @@ -21,7 +21,19 @@ sealed interface PackageListItemEvent { sealed interface InfoPanelEvent : PackageListItemEvent { @Serializable - data class OnHeaderAttributesClick(override val eventId: PackageListItem.Header.Id) : InfoPanelEvent + sealed interface OnHeaderAttributesClick : InfoPanelEvent { + @Serializable + data class DeclaredHeaderAttributesClick( + override val eventId: PackageListItem.Header.Id.Declared, + val variantName: String, + ) : OnHeaderAttributesClick + @Serializable + data class SearchHeaderAttributesClick( + override val eventId: PackageListItem.Header.Id.Remote, + val attributesNames: List + ) : OnHeaderAttributesClick + + } @Serializable data class OnHeaderVariantsClick(override val eventId: PackageListItem.Header.Id) : InfoPanelEvent @@ -31,6 +43,11 @@ sealed interface PackageListItemEvent { @Serializable data class OnPackageDoubleClick(override val eventId: PackageListItem.Id) : InfoPanelEvent + + @Serializable + data class OnSelectedPackageClick(override val eventId: PackageListItem.Id) : InfoPanelEvent + + } @Serializable @@ -87,9 +104,6 @@ sealed interface PackageListItemEvent { } - - - @Serializable data class Update(override val eventId: PackageListItem.Package.Declared.Id) : OnPackageAction diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt index 5b6fd371..5a56b3b9 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/model/packageslist/PackageListViewModel.kt @@ -310,6 +310,7 @@ class PackageListViewModel( is PackageListItemEvent.InfoPanelEvent.OnHeaderVariantsClick -> handle(event) is PackageListItemEvent.InfoPanelEvent.OnPackageSelected -> handle(event) is PackageListItemEvent.InfoPanelEvent.OnPackageDoubleClick -> handle(event) + is PackageListItemEvent.InfoPanelEvent.OnSelectedPackageClick -> handle(event) is PackageListItemEvent.OnPackageAction.GoToSource -> handle(event) is PackageListItemEvent.OnPackageAction.Install.Base -> handle(event) is PackageListItemEvent.OnPackageAction.Install.WithVariant -> handle(event) @@ -331,6 +332,12 @@ class PackageListViewModel( logInfoPanelOpened() } + private fun handle(event: PackageListItemEvent.InfoPanelEvent.OnSelectedPackageClick) { + if (event.eventId is PackageListItem.Package.Id) handle( + PackageListItemEvent.InfoPanelEvent.OnPackageSelected(event.eventId) + ) + } + private fun handle(event: PackageListItemEvent.InfoPanelEvent.OnPackageSelected) { val infoPanelViewModel = project.service() logPackageSelected(event.eventId is PackageListItem.Package.Declared.Id) @@ -591,13 +598,46 @@ class PackageListViewModel( } } - private fun handle(event: PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick) { - logTODO() - logHeaderAttributesClick( - isSearchHeader = event.eventId is PackageListItem.Header.Id.Remote - ) + private suspend fun handle(event: PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick) { + logHeaderAttributesClick(isSearchHeader = event.eventId is PackageListItem.Header.Id.Remote) + val infoPanelViewModel = project.service() + + val module = event.eventId.getModule() as? PackageSearchModule.WithVariants ?: return + + when (event) { + is PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.DeclaredHeaderAttributesClick -> { + val attributes = module.variants[event.variantName]?.attributes ?: return + infoPanelViewModel.setDeclaredHeaderAttributes(event.variantName, attributes = attributes) + } + + is PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.SearchHeaderAttributesClick -> { + val attributes = module + .variants + .map { it.value.attributes } + .flatten() + .filter { it.value in event.attributesNames } + .distinct() + + val variants = module.variants.values.map { it.name } - module.mainVariantName + + infoPanelViewModel.setSearchHeaderAttributes( + defaultVariant = module.mainVariantName, + additionalVariants = variants, + attributes = attributes + ) + } + } + + + project.service().isInfoPanelOpen.let { openStateFlow -> + if (!openStateFlow.value) { + openStateFlow.emit(true) + } + } + } + private suspend fun handle(event: PackageListItemEvent.EditPackageEvent.SetVariant) { val module = event.eventId .getModule() as? PackageSearchModule.WithVariants diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt index 85be9f20..2a745c06 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/PackageSearchPackageList.kt @@ -131,7 +131,12 @@ internal fun SelectableLazyItemScope.PackageListItem( .onClick( interactionSource = remember { MutableInteractionSource() }, onDoubleClick = { onPackageListItemEvent(OnPackageDoubleClick(content.id)) }, - onClick = { } + onClick = { + //this event should be handled when you click on a selected package to refresh side panel content + if (isSelected ) onPackageListItemEvent( + PackageListItemEvent.InfoPanelEvent.OnSelectedPackageClick(content.id) + ) + } ), ) { Row( diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt index e13e1981..3151cb0f 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/packages/items/PackageGroupHeader.kt @@ -1,7 +1,6 @@ package com.jetbrains.packagesearch.plugin.ui.panels.packages.items import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.SpaceBetween import androidx.compose.foundation.layout.Box @@ -11,6 +10,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.onClick import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -20,17 +23,19 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.intellij.icons.AllIcons import com.jetbrains.packagesearch.plugin.PackageSearchBundle.message +import com.jetbrains.packagesearch.plugin.ui.PackageSearchColors import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo -import com.jetbrains.packagesearch.plugin.ui.bridge.pickComposeColorFromLaf import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItem.Header.State import com.jetbrains.packagesearch.plugin.ui.model.packageslist.PackageListItemEvent import java.awt.Cursor +import org.jetbrains.jewel.foundation.modifier.onHover import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.CircularProgressIndicator import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.Link import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.theme.linkStyle @Composable fun PackageListHeader( @@ -38,12 +43,10 @@ fun PackageListHeader( content: PackageListItem.Header, onEvent: (PackageListItemEvent) -> Unit, ) { - val backgroundColor = - if (JewelTheme.isDark) { - pickComposeColorFromLaf("ToolWindow.HeaderTab.selectedInactiveBackground") - } else { - pickComposeColorFromLaf("Tree.selectionInactiveBackground") - } + val isDarkTheme = JewelTheme.isDark + val backgroundColor = remember(isDarkTheme) { + PackageSearchColors.Backgrounds.packageItemHeader() + } Row( modifier = Modifier @@ -88,22 +91,40 @@ fun PackageListHeader( } Text( - fontWeight = FontWeight(600), + fontWeight = FontWeight.ExtraBold, text = content.title, maxLines = 1 ) } if (content.attributes.isNotEmpty()) { + var attributeTextColor by remember { mutableStateOf(Color.Unspecified) } + val linkTextColor = JewelTheme.linkStyle.colors.content Box( modifier = Modifier .onClick { - onEvent(PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick(content.id)) + val event = + when (content.id) { + is PackageListItem.Header.Id.Declared.Base -> return@onClick + is PackageListItem.Header.Id.Remote -> PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.SearchHeaderAttributesClick( + eventId = content.id, + attributesNames = content.attributes + ) + is PackageListItem.Header.Id.Declared.WithVariant -> PackageListItemEvent.InfoPanelEvent.OnHeaderAttributesClick.DeclaredHeaderAttributesClick( + eventId = content.id, + variantName = content.title, + ) + } + onEvent(event) + } + .onHover { + attributeTextColor = if (it) linkTextColor else Color.Unspecified } .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))), contentAlignment = Alignment.Center, ) { - LabelInfo( + Text( text = content.attributes.joinToString(" "), + color = attributeTextColor, maxLines = 1 ) } diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt new file mode 100644 index 00000000..2715203a --- /dev/null +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/HeaderAttributesTab.kt @@ -0,0 +1,286 @@ +package com.jetbrains.packagesearch.plugin.ui.panels.side + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.jetbrains.packagesearch.plugin.PackageSearchBundle +import com.jetbrains.packagesearch.plugin.core.data.PackageSearchModuleVariant +import com.jetbrains.packagesearch.plugin.ui.bridge.AttributeBadge +import com.jetbrains.packagesearch.plugin.ui.bridge.LabelInfo +import com.jetbrains.packagesearch.plugin.ui.model.infopanel.InfoPanelContent +import kotlin.math.roundToInt +import kotlin.random.Random +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.jetbrains.jewel.ui.component.Text + +@Composable +fun HeaderAttributesTab( + content: InfoPanelContent.Attributes, + scrollState: ScrollState, +) { + HeaderAttributesTabImpl( + content = content, + scrollState = scrollState, + contentTitle = PackageSearchBundle.message("packagesearch.ui.toolwindow.sidepanel.searchResultSupport"), + attributeTypeName = PackageSearchBundle.message("packagesearch.ui.toolwindow.sidepanel.platformsList"), + sourceSetString = PackageSearchBundle.message("packagesearch.ui.toolwindow.sidepanel.searchResultSupport.sourceSets"), + ) +} + +@Composable +private fun HeaderAttributesTabImpl( + content: InfoPanelContent.Attributes, + contentTitle: String, + scrollState: ScrollState, + attributeTypeName: String, + sourceSetString: String, +) { + val scope = rememberCoroutineScope() + val attributes = content.attributes + // Global positions of attributes will be used to store the y offset used on scrollToItem + val attributeGlobalPositionMap = remember { mutableMapOf() } + + Column(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.padding(12.dp)) { + if (content is InfoPanelContent.Attributes.FromSearch) { + Text( + text = contentTitle, + fontSize = 14.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.padding(top = 4.dp) + ) + } + + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + attributes.forEachIndexed { index, attribute -> + AttributeBadge(text = attribute.value) { + scope.scrollToAttribute(scrollState, attributeGlobalPositionMap, index) + } + } + } + + if (content is InfoPanelContent.Attributes.FromSearch) { + SourceSetsList(content, sourceSetString) + } + + AttributeItems(attributeTypeName = attributeTypeName, attributes, attributeGlobalPositionMap) + } +} + + +@Composable +private fun ColumnScope.SourceSetsList( + content: InfoPanelContent.Attributes.FromSearch, + title: String, +) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = title, + fontSize = 14.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.padding(top = 4.dp) + ) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + Text(text = content.defaultSourceSet) + LabelInfo(PackageSearchBundle.message("packagesearch.ui.toolwindow.variant.default.text")) + } + Text(text = content.additionalSourceSets.joinToString(", ")) + + } +} + +@Composable +private fun AttributeItems( + attributeTypeName: String, + attributesName: List, + attributeGlobalPosition: MutableMap, +) { + Text( + text = attributeTypeName, + fontSize = 14.sp, + fontWeight = FontWeight.ExtraBold, + modifier = Modifier.padding(top = 4.dp) + ) + + attributesName.forEachIndexed { index, attribute -> + AttributeItem( + modifier = Modifier.onGloballyPositioned { + attributeGlobalPosition[index] = it.positionInParent().y.roundToInt() + }, + attributeName = attribute.value, + nestedAttributesName = attribute.flatten() + ) + } +} + +private fun PackageSearchModuleVariant.Attribute.flatten(): List = + when (this) { + is PackageSearchModuleVariant.Attribute.NestedAttribute -> children.flatMap { it.flatten() } + is PackageSearchModuleVariant.Attribute.StringAttribute -> listOf(value) + } + + +private fun CoroutineScope.scrollToAttribute( + scrollState: ScrollState, + attributeGlobalPosition: MutableMap, + index: Int, +) { + launch { + scrollState.animateScrollTo( + value = attributeGlobalPosition[index] ?: 0, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessVeryLow + ) + ) + } +} + +@Composable +fun AttributeItem(modifier: Modifier = Modifier, attributeName: String, nestedAttributesName: List) { + + Row(modifier = modifier, verticalAlignment = Alignment.Top, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + modifier = Modifier.width(160.dp), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(text = attributeName) + LabelInfo(nestedAttributesName.size.toString()) + } + + Text(text = nestedAttributesName.joinToString("\n")) + } +} + + +//@Preview +//@Composable +//private fun HeaderAttributesPreviewTab() { +// val activeTabMock = InfoPanelContent.Attributes.FromVariant( +// tabTitle = "FromVariant", +// variantName = "jvm", +// attributes = generateAttributesMock() +// ) +// val scrollStateMock = ScrollState(0) +// Column(Modifier.padding(8.dp)) { +// IntUiTheme(true) { +// Column(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// HeaderAttributesTabImpl( +// content = activeTabMock, +// scrollState = scrollStateMock, +// contentTitle = "FromSearch results that support:", +// attributeTypeName = "Platforms:", +// sourceSetString = "Source Sets:" +// ) +// +// } +// +// Divider(orientation = Orientation.Horizontal, modifier = Modifier.padding(vertical = 16.dp)) +// +// } +// +// +// IntUiTheme(false) { +// Column(Modifier.background(LocalGlobalColors.current.paneBackground).padding(16.dp)) { +// HeaderAttributesTabImpl( +// content = activeTabMock, +// scrollState = scrollStateMock, +// contentTitle = "FromSearch results that support:", +// attributeTypeName = "Platforms:", +// sourceSetString = "Source Sets:" +// ) +// +// } +// } +// } +//} + +private fun generateAttributesMock(): List { + val attributes = mutableListOf() + + platformListMock.take(Random.nextInt(2, 10)).forEach { + attributes.add( + if (Random.nextBoolean()) { + PackageSearchModuleVariant.Attribute.NestedAttribute( + it, + platformListMock.take(Random.nextInt(2, 10)).map { + PackageSearchModuleVariant.Attribute.StringAttribute(it) + } + ) + + + } else { + + PackageSearchModuleVariant.Attribute.StringAttribute(it) + } + ) + } + + return attributes + +} + +internal val platformListMock get() = buildList { + add("Android") + add("android") + add("Apple") + add("iOS") + add("iosX64") + add("iosArm64") + add("iosSimulatorArm64") + add("macOS") + add("macosArm64") + add("macosX64") + + add("watchOS") + add("watchosArm32") + add("watchosArm64") + add("watchosX64") + add("watchosSimulatorArm64") + + add("tvOS") + add("tvosX64") + add("tvosArm64") + add("tvosSimulatorArm64") + + + add("Java") + add("jvm") + + add("JavaScript") + add("jsLegacy") + add("jslr") + + add("Linux") + add("LinuxMipsel32") + add("LinuxArm64") + add("LinuxArm32Hfp") + add("LinuxX64") + + add("Windows") + add("WindowsX64") + add("WindowsX86") + + add("WebAssembly") + add("wasm") + add("wasm32") + +} \ No newline at end of file diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt index 0a9f4a30..4cc06021 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/side/PackageSearchInfoPanel.kt @@ -65,6 +65,14 @@ fun PackageSearchInfoPanel( content = activeTab ) } + + is InfoPanelContent.Attributes.FromVariant -> { + HeaderAttributesTab(content = activeTab, scrollState = viewModel.scrollState) + + } + is InfoPanelContent.Attributes.FromSearch -> { + HeaderAttributesTab(content = activeTab, scrollState = viewModel.scrollState) + } } } VerticalScrollbar( diff --git a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt index 71e3e739..4f98fd43 100644 --- a/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt +++ b/plugin/src/main/kotlin/com/jetbrains/packagesearch/plugin/ui/panels/tree/PackageSearchModulesTree.kt @@ -30,10 +30,7 @@ import com.jetbrains.packagesearch.plugin.ui.model.tree.TreeItemModel import com.jetbrains.packagesearch.plugin.ui.model.tree.TreeViewModel import com.jetbrains.packagesearch.plugin.ui.viewModel import org.jetbrains.jewel.foundation.lazy.tree.Tree -import org.jetbrains.jewel.foundation.lazy.tree.buildTree -import org.jetbrains.jewel.foundation.lazy.tree.rememberTreeState import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.foundation.theme.ThemeDefinition import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.Divider import org.jetbrains.jewel.ui.component.Icon @@ -157,7 +154,7 @@ private fun TreeActionToolbar( modifier = Modifier.padding(5.dp), resource = "icons/intui/toggleOfflineMode.svg", iconClass = IconProvider::class.java, - contentDescription = "Package Search is offline." + contentDescription = "Package FromSearch is offline." ) }, ) diff --git a/plugin/src/main/resources/messages/packageSearchBundle.properties b/plugin/src/main/resources/messages/packageSearchBundle.properties index 96e92e95..56b0bdf9 100644 --- a/plugin/src/main/resources/messages/packageSearchBundle.properties +++ b/plugin/src/main/resources/messages/packageSearchBundle.properties @@ -215,6 +215,7 @@ packagesearch.toolwindow.loading.easterEgg.2=Asking the Mages in Dalaran to Tele packagesearch.toolwindow.loading.easterEgg.3=Hiring Goblin Engineers for High-Speed Package Data Transfer... packagesearch.toolwindow.loading.easterEgg.4=Deciphering Runes in Ulduar for Encrypted Package Data... packagesearch.ui.toolwindow.packages.details.info.overview=Overview +packagesearch.ui.toolwindow.packages.details.info.attributes=Attributes packagesearch.ui.toolwindow.packages.details.info.unknown=Unknown packagesearch.ui.tree.tooltips.offline=Package Search is offline packagesearch.ui.toolwindow.packages.details.info.scm.github=GitHub @@ -222,4 +223,8 @@ packagesearch.ui.toolwindow.packages.actions.remove=Remove packagesearch.ui.toolwindow.packages.details.info.scope=Scope: packagesearch.toolwindow.loading.dumbMode=Project is initializing... packagesearch.ui.tree.tooltips.expand=Expand all -packagesearch.ui.tree.tooltips.collapse=Collapse all \ No newline at end of file +packagesearch.ui.tree.tooltips.collapse=Collapse all +packagesearch.ui.toolwindow.sidepanel.platformsList=Platforms: +packagesearch.ui.toolwindow.sidepanel.platforms=Platforms +packagesearch.ui.toolwindow.sidepanel.searchResultSupport=Search results that support: +packagesearch.ui.toolwindow.sidepanel.searchResultSupport.sourceSets=Source Sets: