From ae6d3439134d9074c8b8265b371aa2b5f6f1eee4 Mon Sep 17 00:00:00 2001 From: Nebojsa Vuksic Date: Tue, 12 Aug 2025 17:56:38 +0200 Subject: [PATCH] Add a context menu for location items and add remove location functionality --- .../template/components/ContextPopupMenu.kt | 60 ++++++++++++++ .../weatherApp/ui/WeatherAppSample.kt | 80 +++++++++++++++++-- .../messages/ComposeTemplate.properties | 4 +- 3 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/org/jetbrains/plugins/template/components/ContextPopupMenu.kt diff --git a/src/main/kotlin/org/jetbrains/plugins/template/components/ContextPopupMenu.kt b/src/main/kotlin/org/jetbrains/plugins/template/components/ContextPopupMenu.kt new file mode 100644 index 0000000..0bf0bf6 --- /dev/null +++ b/src/main/kotlin/org/jetbrains/plugins/template/components/ContextPopupMenu.kt @@ -0,0 +1,60 @@ +package org.jetbrains.plugins.template.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.onClick +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupPositionProvider +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.PopupContainer +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.icon.IconKey + +@Composable +fun ContextPopupMenu( + popupPositionProvider: PopupPositionProvider, + onDismissRequest: () -> Unit, + content: @Composable () -> Unit, +) { + PopupContainer( + popupPositionProvider = popupPositionProvider, + modifier = Modifier.wrapContentSize(), + onDismissRequest = { onDismissRequest() }, + horizontalAlignment = Alignment.Start + ) { + content() + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ContextPopupMenuItem( + actionText: String, + actionIcon: IconKey, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .widthIn(min = 100.dp) + .padding(8.dp) + .onClick { onClick() }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + actionIcon, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = actionText, + style = JewelTheme.defaultTextStyle + ) + } +} diff --git a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt index 43b6a6d..52f8e34 100644 --- a/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt +++ b/src/main/kotlin/org/jetbrains/plugins/template/weatherApp/ui/WeatherAppSample.kt @@ -1,14 +1,20 @@ package org.jetbrains.plugins.template.weatherApp.ui import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.PopupPositionProvider import org.jetbrains.jewel.foundation.lazy.SelectableLazyColumn import org.jetbrains.jewel.foundation.lazy.SelectionMode import org.jetbrains.jewel.foundation.lazy.itemsIndexed @@ -18,8 +24,10 @@ import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.icon.IconKey import org.jetbrains.jewel.ui.icons.AllIconsKeys import org.jetbrains.plugins.template.ComposeTemplateBundle +import org.jetbrains.plugins.template.components.ContextPopupMenu +import org.jetbrains.plugins.template.components.ContextPopupMenuItem import org.jetbrains.plugins.template.weatherApp.model.Location -import org.jetbrains.plugins.template.weatherApp.services.* +import org.jetbrains.plugins.template.weatherApp.services.SearchAutoCompletionItemProvider import org.jetbrains.plugins.template.weatherApp.ui.components.SearchToolbarMenu import org.jetbrains.plugins.template.weatherApp.ui.components.WeatherDetailsCard @@ -119,6 +127,7 @@ private fun EmptyListPlaceholder( } } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun MyLocationList( myLocationsUIState: LocationsUIState, @@ -158,9 +167,66 @@ private fun MyLocationList( itemsIndexed( items = myLocationsUIState.locations, key = { _, item -> item.label }, - ) { index, item -> + ) { index, locationItem -> - SimpleListItem(text = item.label, isSelected = myLocationsUIState.selectedIndex == index, isActive = isActive) + Box(Modifier.wrapContentSize()) { + val showPopup = remember { mutableStateOf(false) } + val popupPosition = remember { mutableStateOf(IntOffset.Zero) } + val itemPosition = remember { mutableStateOf(Offset.Zero) } + + SimpleListItem( + text = locationItem.label, + isSelected = myLocationsUIState.selectedIndex == index, + isActive = isActive, + modifier = Modifier + .onGloballyPositioned { coordinates -> + itemPosition.value = coordinates.positionInWindow() + } + .onPointerEvent(PointerEventType.Press) { pointerEvent -> + if (!pointerEvent.buttons.isSecondaryPressed) return@onPointerEvent + + // Calculate exact click position + val clickOffset = pointerEvent.changes.first().position + popupPosition.value = IntOffset( + x = (itemPosition.value.x + clickOffset.x).toInt(), + y = (itemPosition.value.y + clickOffset.y).toInt() + ) + + showPopup.value = true + } + ) + + if (showPopup.value) { + val popupPositionProvider = remember(popupPosition.value) { + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset = popupPosition.value + } + } + + ContextPopupMenu( + popupPositionProvider, + onDismissRequest = { + showPopup.value = false + popupPosition.value = IntOffset.Zero + itemPosition.value = Offset.Zero + } + ) { + ContextPopupMenuItem( + ComposeTemplateBundle.getMessage("weather.app.context.menu.delete.option"), + AllIconsKeys.General.Delete + ) { + showPopup.value = false + + myLocationsViewModelApi.onDeleteLocation(locationItem) + } + } + } + } } } } diff --git a/src/main/resources/messages/ComposeTemplate.properties b/src/main/resources/messages/ComposeTemplate.properties index 9ec96a2..ef40e3b 100644 --- a/src/main/resources/messages/ComposeTemplate.properties +++ b/src/main/resources/messages/ComposeTemplate.properties @@ -9,4 +9,6 @@ weather.app.search.toolbar.menu.add.button.text=Add weather.app.search.toolbar.menu.add.button.content.description=Add a place to a watch list. weather.app.7days.forecast.title.text=7-day Forecast -weather.app.clear.button.content.description=Clear \ No newline at end of file +weather.app.context.menu.delete.option=Delete + +weather.app.clear.button.content.description=Clear