From dbf21405463c646cc1b26634401b0dba130c2994 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Fri, 29 Aug 2025 17:48:00 -0700 Subject: [PATCH 01/36] WIP --- .../ContentVisibleControl.kt | 71 ++++ .../FractionAlongEdgeControl.kt | 98 ++++++ .../UtilityAssociationDetails.kt | 245 +++++++++++++ .../UtilityAssociations.kt | 329 ++++++++++++++++++ .../UtilityAssociationsElement.kt | 200 +++++++++++ .../UtilityAssociationsElementState.kt | 120 +++++++ .../UtilityTerminalControl.kt | 46 +++ .../main/res/drawable/connection_end_left.xml | 10 + .../res/drawable/connection_end_right.xml | 10 + .../src/main/res/drawable/connection_mid.xml | 10 + .../res/drawable/connection_to_connection.xml | 9 + toolkit/popup/src/main/res/values/strings.xml | 14 + 12 files changed, 1162 insertions(+) create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/ContentVisibleControl.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/FractionAlongEdgeControl.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityTerminalControl.kt create mode 100644 toolkit/popup/src/main/res/drawable/connection_end_left.xml create mode 100644 toolkit/popup/src/main/res/drawable/connection_end_right.xml create mode 100644 toolkit/popup/src/main/res/drawable/connection_mid.xml create mode 100644 toolkit/popup/src/main/res/drawable/connection_to_connection.xml diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/ContentVisibleControl.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/ContentVisibleControl.kt new file mode 100644 index 000000000..8ce745a52 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/ContentVisibleControl.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.utilitynetworks.UtilityAssociation + +/** + * A composable that represents a content visible control of a [UtilityAssociation], specifically the + * [UtilityAssociation.isContainmentVisible] property. + * + * @param value The current value of the content visible control. + * @param enabled A boolean indicating whether the control is enabled or not. + * @param onValueChange A callback that is called when the value of the control changes. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun ContentVisibleControl( + value : Boolean, + enabled : Boolean, + onValueChange : (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = stringResource(R.string.containment_visible)) + Switch( + checked = value, + onCheckedChange = onValueChange, + enabled = enabled, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ContentVisibleControlPreview() { + ContentVisibleControl( + value = true, + onValueChange = {}, + enabled = false, + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/FractionAlongEdgeControl.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/FractionAlongEdgeControl.kt new file mode 100644 index 000000000..d855d0615 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/FractionAlongEdgeControl.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.Slider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.utilitynetworks.UtilityAssociation +import com.arcgismaps.utilitynetworks.UtilityElement + +/** + * A composable that represents a fraction along edge control of a [UtilityAssociation]. This can + * represent the [UtilityAssociation.fractionAlongEdge] property if one of the element is a non-spatial + * edge or the [UtilityElement.fractionAlongEdge] when the element represents a spatial edge. + * + * This control displays a slider that allows the user to select a value between 0 and 1, which is then + * multiplied by 100 to display the percentage value. + * + * @param fraction The current value of the fraction along edge control. + * @param enabled A boolean indicating whether the control is enabled or not. + * @param onValueChanged A callback that is called when the value of the control changes. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun FractionAlongEdgeControl( + fraction : Float, + enabled : Boolean, + onValueChanged : (Float) -> Unit, + modifier: Modifier = Modifier +) { + var value by remember { + mutableFloatStateOf(fraction) + } + val percent = (value * 100).toInt() + Card(modifier = modifier) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(15.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + PropertyRow( + title = stringResource(R.string.fraction_along_edge), + value = stringResource(R.string.percent_along_edge, percent), + modifier = Modifier.fillMaxWidth() + ) + Slider( + value = value, + onValueChange = { + value = it + }, + enabled = enabled, + modifier = Modifier.fillMaxWidth(), + onValueChangeFinished = { + onValueChanged(value) + } + ) + } + } +} + +@Preview +@Composable +private fun FractionAlongEdgeControlPreview() { + FractionAlongEdgeControl( + fraction = 0.5f, + onValueChanged = {}, + enabled = true, + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt new file mode 100644 index 000000000..bbbd61a03 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.utilitynetworks.UtilityAssociationResult +import com.arcgismaps.utilitynetworks.UtilityAssociationType +import com.arcgismaps.utilitynetworks.UtilityNetworkSourceType + +/** + * A composable that displays the details of a [UtilityAssociationResult]. + * + * @param state The [UtilityAssociationsElementState] of the element. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun UtilityAssociationDetails( + state: UtilityAssociationsElementState, + modifier: Modifier = Modifier +) { + val associationResult = state.selectedAssociationResult ?: return + val filter = state.selectedFilterResult?.filter ?: return + val association = associationResult.association + var showConfirmationDialog by remember { + mutableStateOf(false) + } + Column( + modifier = modifier, + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Card(modifier = Modifier.padding(24.dp)) { + Column { + PropertyRow( + title = stringResource(R.string.from_element), + value = "${association.fromElement.objectId}", + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + if (association.fromElement.terminal != null) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 20.dp)) + UtilityTerminalControl( + name = association.fromElement.terminal!!.name, + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + } + } + } + Card(modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp, bottom = 24.dp)) { + Column { + PropertyRow( + title = stringResource(R.string.to_element), + value = "${association.toElement.objectId}", + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + if (association.toElement.terminal != null) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 20.dp)) + UtilityTerminalControl( + name = association.toElement.terminal!!.name, + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + } + } + } + Card(modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp, bottom = 24.dp)) { + PropertyRow( + title = stringResource(R.string.association_type), + value = filter.filterType.toString(), + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + if (association.associationType is UtilityAssociationType.Containment) { + HorizontalDivider() + ContentVisibleControl( + value = association.isContainmentVisible, + enabled = false, + onValueChange = {}, + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 5.dp) + .fillMaxWidth() + ) + } + } + associationResult.getFractionAlongEdge()?.let { fraction -> + FractionAlongEdgeControl( + fraction = associationResult.getFractionAlongEdge()!!.toFloat(), + enabled = false, + onValueChanged = {}, + modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp, bottom = 24.dp) + ) + } + Spacer(modifier = Modifier.height(20.dp)) + Button(onClick = { showConfirmationDialog = true }) { + Text(text = stringResource(R.string.remove_association)) + } + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(R.string.remove_association_tooltip), + style = MaterialTheme.typography.bodySmall + ) + } + if (showConfirmationDialog) { + RemoveAssociationConfirmationDialog( + onDismiss = { showConfirmationDialog = false }, + onRemove = { + showConfirmationDialog = false + // Remove the association when the API is available. + } + ) + } +} + +/** + * A composable that displays a row with a title and value. + */ +@Composable +internal fun PropertyRow( + title: String, + value: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text( + text = value, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.38f + ) + ) + } +} + +/** + * A composable that displays a confirmation dialog for removing an association. + * + * @param onDismiss A callback that is called when the dialog is dismissed. + * @param onRemove A callback that is called when the remove button is clicked. + */ +@Composable +private fun RemoveAssociationConfirmationDialog( + onDismiss: () -> Unit, + onRemove: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text(text = "${stringResource(R.string.remove_association)}?") + }, + text = { + Text(text = stringResource(R.string.remove_association_tooltip)) + }, + confirmButton = { + TextButton(onRemove) { + Text(text = stringResource(R.string.remove)) + } + }, + dismissButton = { + TextButton(onDismiss) { + Text(text = stringResource(R.string.cancel)) + } + } + ) +} + +/** + * Extension function that returns the fraction along edge of the association result, if applicable. + * + * @return The fraction along edge of the association result, or null if not applicable. + */ +internal fun UtilityAssociationResult.getFractionAlongEdge(): Double? { + return when (association.associationType) { + + UtilityAssociationType.Connectivity -> { + when { + association.fromElement.networkSource.sourceType == UtilityNetworkSourceType.Edge -> { + association.fromElement.fractionAlongEdge + } + + association.toElement.networkSource.sourceType == UtilityNetworkSourceType.Edge -> { + association.toElement.fractionAlongEdge + } + + else -> null + } + } + + UtilityAssociationType.JunctionEdgeObjectConnectivityFromSide, + UtilityAssociationType.JunctionEdgeObjectConnectivityMidspan, + UtilityAssociationType.JunctionEdgeObjectConnectivityToSide -> { + association.fractionAlongEdge + } + + else -> null + } +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt new file mode 100644 index 000000000..1d23e2217 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt @@ -0,0 +1,329 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.arcgismaps.Guid +import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.utilitynetworks.UtilityAssociation +import com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult +import com.arcgismaps.utilitynetworks.UtilityAssociationType +import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult +import com.arcgismaps.utilitynetworks.UtilityElement + +/** + * Displays the provided [UtilityAssociationsFilterResult]. The filter result is displayed as a + * list of its groups as given by [UtilityAssociationsFilterResult.groupResults]. + * + * @param groupResults The [UtilityAssociationsFilterResult] to display. + * @param onGroupClick A callback that is called when a group is clicked with the index of the group. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun UtilityAssociationFilter( + groupResults: List, + onGroupClick: (UtilityAssociationGroupResult) -> Unit, + modifier: Modifier = Modifier +) { + // show the list of layers + Surface( + modifier = modifier, + shape = RoundedCornerShape(15.dp) + ) { + LazyColumn(modifier = Modifier) { + groupResults.forEachIndexed { index, group -> + item { + ListItem( + headlineContent = { + Text(text = group.name, modifier = Modifier.padding(start = 16.dp)) + }, + trailingContent = { + Text( + text = "${group.associationResults.count()}", + modifier = Modifier.padding(end = 16.dp) + ) + }, + modifier = Modifier.clickable { + onGroupClick(group) + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) + if (index < groupResults.count() - 1) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainer + ) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + } + } + } + } + } + } +} + +/** + * Displays the provided list of associations that are part of the [UtilityAssociationGroupResult]. + * + * @param groupResult The [UtilityAssociationGroupResult] to display. + * @param onItemClick A callback that is called when an association is clicked. + * @param onDetailsClick A callback that is called when the details icon is clicked. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun UtilityAssociations( + groupResult: UtilityAssociationGroupResult, + isNavigationEnabled: Boolean, + onItemClick: (Int) -> Unit, + onDetailsClick: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val lazyListState = rememberLazyListState() + Surface( + modifier = modifier.wrapContentHeight( + align = Alignment.Top + ), + shape = RoundedCornerShape(15.dp), + color = MaterialTheme.colorScheme.surfaceContainer + ) { + LazyColumn( + modifier = Modifier.clip(shape = RoundedCornerShape(15.dp)), + state = lazyListState + ) { + groupResult.associationResults.forEachIndexed { index, info -> + item(info.association.hashCode()) { + AssociationItem( + title = info.title, + association = info.association, + associatedFeature = info.associatedFeature, + enabled = isNavigationEnabled, + onClick = { + onItemClick(index) + }, + onDetailsClick = { + onDetailsClick(index) + }, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) + ) + if (index < groupResult.associationResults.count() - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = DividerDefaults.color.copy(alpha = 0.7f) + ) + } + } + } + } + } +} + +/** + * Displays the provided [UtilityAssociation] and its associated feature. + * + * The spec for displaying the association is based on the following rules: + * + * - If the association is of type JunctionEdgeObjectConnectivityMidspan, then only the fractionAlongEdge + * is displayed. + * + * - If the association is of type JunctionEdgeObjectConnectivityFromSide, JunctionEdgeObjectConnectivityToSide + * or Connectivity then fractionAlongEdge and the terminal (if present) is displayed. + * + * - For a Containment association, the isContainmentVisible property is displayed if the associated + * feature is the toElement. + * + */ +@Composable +private fun AssociationItem( + title: String, + association: UtilityAssociation, + associatedFeature: ArcGISFeature, + enabled: Boolean, + onClick: () -> Unit, + onDetailsClick: () -> Unit, + modifier: Modifier = Modifier +) { + val target = association.getTargetElement(associatedFeature) + // Text to display at the end of the row. + var trailingText = "" + // Text to display below the title. + var supportingText = "" + when (association.associationType) { + is UtilityAssociationType.JunctionEdgeObjectConnectivityMidspan -> { + trailingText = "${(association.fractionAlongEdge * 100).toInt()}%" + target.terminal?.let { terminal -> + supportingText = terminal.name + } + } + + is UtilityAssociationType.Connectivity, + UtilityAssociationType.JunctionEdgeObjectConnectivityFromSide, + UtilityAssociationType.JunctionEdgeObjectConnectivityToSide -> { + target.terminal?.let { terminal -> + supportingText = terminal.name + } + } + + is UtilityAssociationType.Containment -> { + if (associatedFeature.globalId == association.toElement.globalId) { + supportingText = if (association.isContainmentVisible) { + stringResource(R.string.visible_content) + } else { + stringResource(R.string.content) + } + } + } + + else -> {} + } + val contentColor = if (enabled) { + LocalContentColor.current + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + } + Row( + modifier = modifier + .clickable(enabled = enabled, onClick = onClick) + .height(56.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + association.getIcon()?.let { icon -> + Icon( + painter = icon, + contentDescription = "feature association icon", + modifier = Modifier.padding( + end = 12.dp + ) + ) + } + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = title, + modifier = Modifier.padding( + top = if (supportingText.isNotEmpty()) 6.dp else 0.dp, + ), + style = MaterialTheme.typography.bodyLarge, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = contentColor + ) + if (supportingText.isNotEmpty()) { + Text( + text = supportingText, + modifier = Modifier + .padding( + bottom = 6.dp + ), + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = contentColor + ) + } + } + if (trailingText.isNotEmpty()) { + Card(modifier = Modifier.wrapContentSize()) { + Text(text = trailingText, modifier = Modifier.padding(8.dp)) + } + } + } +} + +/** + * Returns the target [UtilityElement] of the association that equal to the provided [ArcGISFeature]. + */ +internal fun UtilityAssociation.getTargetElement(arcGISFeature: ArcGISFeature): UtilityElement { + return if (arcGISFeature.globalId == this.fromElement.globalId) { + this.fromElement + } else { + this.toElement + } +} + +/** + * Returns the global ID of the [ArcGISFeature]. + */ +internal val ArcGISFeature.globalId: Guid + get() = attributes["globalid"] as Guid + +/** + * Returns an icon for the association based on the association type. + */ +@Composable +internal fun UtilityAssociation.getIcon(): Painter? { + return when (this.associationType) { + + is UtilityAssociationType.Connectivity -> { + painterResource(R.drawable.connection_to_connection) + } + + is UtilityAssociationType.JunctionEdgeObjectConnectivityFromSide -> { + painterResource(R.drawable.connection_end_left) + } + + is UtilityAssociationType.JunctionEdgeObjectConnectivityMidspan -> { + painterResource(R.drawable.connection_mid) + } + + is UtilityAssociationType.JunctionEdgeObjectConnectivityToSide -> { + painterResource(R.drawable.connection_end_right) + } + + else -> null + } +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt new file mode 100644 index 000000000..16d187dc6 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowRight +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult + +/** + * A composable that represents a utility associations element. + * + * @param state The [UtilityAssociationsElementState] of the element. + * @param onItemClick A callback that is called when an item is clicked with the index of the item. + * The index is the index of the item in the [UtilityAssociationsElementState.filters] list. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun UtilityAssociationsElement( + state: UtilityAssociationsElementState, + onItemClick: (UtilityAssociationsFilterResult) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + ElementHeader( + state.label, + state.description, + Modifier.padding(top = 16.dp, end = 16.dp) + ) + when { + // Show loading indicator when loading + state.loading -> { + CircularProgressIndicator( + modifier = Modifier.size(20.dp) + ) + } + // If loaded and no filters found show no associations found text + !state.loading && state.filters.isEmpty() -> { + Text( + text = stringResource(R.string.no_associations_found), + style = MaterialTheme.typography.bodyMedium.copy( + fontStyle = FontStyle.Italic + ), + modifier = Modifier.padding(top = 16.dp) + ) + } + // Show filters when loaded and filters found + else -> { + Filters( + filterResults = state.filters, + onClick = onItemClick, + modifier = Modifier + .padding(top = 16.dp) + .clip(RoundedCornerShape(15.dp)) + ) + } + } + } +} + +/** + * Represents the header of a utility associations element. + * + * @param label The label of the element. + * @param description The description of the element. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +private fun ElementHeader( + label: String, + description: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium + ) + if (description.isNotEmpty()) { + Text( + text = description, + style = MaterialTheme.typography.labelMedium, + ) + } + } + } +} + +/** + * Displays the filters for the utility associations element. + * + * @param filterResults The list of [UtilityAssociationsFilterResult] to display. + * @param onClick A callback that is called when a filter is clicked with the index of the filter. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +private fun Filters( + filterResults: List, + onClick: (UtilityAssociationsFilterResult) -> Unit, + modifier: Modifier = Modifier +) { + Surface(modifier = modifier) { + Column { + filterResults.forEachIndexed { i, filterResult -> + ListItem( + headlineContent = { + Text(text = filterResult.filter.title) + }, + modifier = Modifier.clickable { + onClick(filterResult) + }, + supportingContent = if (filterResult.filter.description.isNotEmpty()) { + { + Text( + text = filterResult.filter.description, + style = MaterialTheme.typography.labelSmall + ) + } + } else null, + trailingContent = { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = filterResult.resultCount.toString()) + Image( + imageVector = Icons.AutoMirrored.Default.ArrowRight, + contentDescription = null, + modifier = Modifier.padding(start = 8.dp) + ) + } + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) + if (i < filterResults.size - 1) { + HorizontalDivider() + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ElementHeaderPreview() { + ElementHeader("Associations", "This is a description", Modifier.fillMaxWidth()) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt new file mode 100644 index 000000000..c25d37ea7 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement +import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState +import com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult +import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult +import com.arcgismaps.utilitynetworks.UtilityAssociationResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +/** + * State holder for the [UtilityAssociationsElement]. + * + * @param element The [UtilityAssociationsFormElement] to represent. + * @param scope The [CoroutineScope] to launch coroutines from. + */ +internal class UtilityAssociationsElementState( + element: UtilityAssociationsFormElement, + scope: CoroutineScope +) : PopupElementState() { + override val id : Int = element.hashCode() + val label : String = element.label + val description: String = element.description + val isVisible : StateFlow = element.isVisible + + private var _loading: MutableState = mutableStateOf(true) + + private var _filters: MutableState> = + mutableStateOf(emptyList()) + + /** + * Indicates if the state is loading data to fetch the filters [filters]. + * + * This property is observable and if used within a composition it will be notified on every change. + */ + val loading: Boolean + get() = _loading.value + + /** + * The list of [UtilityAssociationsFilterResult] to display. This is empty until the data is fetched + * as part of the initialization. + * + * This property is observable and if used within a composition it will be notified on every change. + */ + val filters: List + get() = _filters.value + + /** + * The selected [UtilityAssociationsFilterResult] to display. Use [setSelectedFilterResult] to + * set this value. + */ + var selectedFilterResult : UtilityAssociationsFilterResult? = null + private set + + /** + * The selected [UtilityAssociationGroupResult] to display. Use [setSelectedGroupResult] to + * set this value. + */ + var selectedGroupResult : UtilityAssociationGroupResult? = null + private set + + /** + * The selected [UtilityAssociationResult] to display. Use [setSelectedAssociationResult] to + * set this value. + */ + var selectedAssociationResult : UtilityAssociationResult? = null + private set + + init { + scope.launch { + // fetch the associations filter results for the element + element.fetchAssociationsFilterResults() + _filters.value = element.associationsFilterResults.filter { + // filter out the results that have no associations + it.resultCount > 0 + } + _loading.value = false + } + } + + /** + * Sets the selected [UtilityAssociationsFilterResult] to display. + */ + fun setSelectedFilterResult(filterResult: UtilityAssociationsFilterResult) { + selectedFilterResult = filterResult + } + + /** + * Sets the selected [UtilityAssociationGroupResult] to display. + */ + fun setSelectedGroupResult(groupResult: UtilityAssociationGroupResult?) { + selectedGroupResult = groupResult + } + + /** + * Sets the selected [UtilityAssociationResult] to display. + */ + fun setSelectedAssociationResult(associationResult: UtilityAssociationResult?) { + selectedAssociationResult = associationResult + } +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityTerminalControl.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityTerminalControl.kt new file mode 100644 index 000000000..de45581cb --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityTerminalControl.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.arcgismaps.toolkit.popup.R + +@Composable +internal fun UtilityTerminalControl( + name : String, + modifier: Modifier = Modifier +) { + PropertyRow( + title = stringResource(R.string.terminal), + value = name, + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +private fun UtilityTerminalControlPreview() { + val name = "SS" + UtilityTerminalControl( + name = name, + modifier = Modifier.fillMaxWidth() + ) +} diff --git a/toolkit/popup/src/main/res/drawable/connection_end_left.xml b/toolkit/popup/src/main/res/drawable/connection_end_left.xml new file mode 100644 index 000000000..f2c73e658 --- /dev/null +++ b/toolkit/popup/src/main/res/drawable/connection_end_left.xml @@ -0,0 +1,10 @@ + + + diff --git a/toolkit/popup/src/main/res/drawable/connection_end_right.xml b/toolkit/popup/src/main/res/drawable/connection_end_right.xml new file mode 100644 index 000000000..485ddf140 --- /dev/null +++ b/toolkit/popup/src/main/res/drawable/connection_end_right.xml @@ -0,0 +1,10 @@ + + + diff --git a/toolkit/popup/src/main/res/drawable/connection_mid.xml b/toolkit/popup/src/main/res/drawable/connection_mid.xml new file mode 100644 index 000000000..fc99c1760 --- /dev/null +++ b/toolkit/popup/src/main/res/drawable/connection_mid.xml @@ -0,0 +1,10 @@ + + + diff --git a/toolkit/popup/src/main/res/drawable/connection_to_connection.xml b/toolkit/popup/src/main/res/drawable/connection_to_connection.xml new file mode 100644 index 000000000..06ec2208e --- /dev/null +++ b/toolkit/popup/src/main/res/drawable/connection_to_connection.xml @@ -0,0 +1,9 @@ + + + diff --git a/toolkit/popup/src/main/res/values/strings.xml b/toolkit/popup/src/main/res/values/strings.xml index 878835907..2aacc0aa3 100644 --- a/toolkit/popup/src/main/res/values/strings.xml +++ b/toolkit/popup/src/main/res/values/strings.xml @@ -26,4 +26,18 @@ Other Back attachment thumbnail + Terminal + No associations found + Visible Content + Content + Containment Visible + Fraction Along Edge + %1$d %% + From Element + To Element + Association Type + Remove Association + Only removes the association. The feature remains. + Remove + Cancel From 1dac21a01f10338cae7024597712e1c7d001ebad Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Tue, 2 Sep 2025 11:17:59 -0700 Subject: [PATCH 02/36] WIP --- toolkit/popup/build.gradle.kts | 2 + .../com/arcgismaps/toolkit/popup/Popup.kt | 44 ++++- .../arcgismaps/toolkit/popup/PopupState.kt | 17 ++ .../internal/navigation/FeatureFormNavHost.kt | 165 ++++++++++++++++++ .../internal/navigation/NavigationAction.kt | 53 ++++++ .../internal/navigation/NavigationRoute.kt | 69 ++++++++ .../screens/UNAssociationsFilterScreen.kt | 74 ++++++++ .../internal/screens/UNAssociationsScreen.kt | 141 +++++++++++++++ 8 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt diff --git a/toolkit/popup/build.gradle.kts b/toolkit/popup/build.gradle.kts index 05489ca9a..8c2ad4cb1 100644 --- a/toolkit/popup/build.gradle.kts +++ b/toolkit/popup/build.gradle.kts @@ -99,6 +99,8 @@ dependencies { implementation(libs.bundles.composeCore) implementation(libs.bundles.core) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.navigation) + implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.material.icons) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.exoplayer.dash) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 08be480d9..111ecd26c 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -48,12 +48,14 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement import com.arcgismaps.mapping.popup.AttachmentsPopupElement import com.arcgismaps.mapping.popup.FieldsPopupElement import com.arcgismaps.mapping.popup.MediaPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupAttachment import com.arcgismaps.mapping.popup.TextPopupElement +import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.realtime.DynamicEntity import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsElementState import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsPopupElement @@ -69,11 +71,30 @@ import com.arcgismaps.toolkit.popup.internal.element.state.mutablePopupElementSt import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState import com.arcgismaps.toolkit.popup.internal.element.textelement.TextPopupElement import com.arcgismaps.toolkit.popup.internal.element.textelement.rememberTextElementState +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElement +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.FileViewer import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile +import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult + +//@Immutable +//private data class PopupState(@Stable val popup: Popup) @Immutable -private data class PopupState(@Stable val popup: Popup) +public sealed class ValidationErrorVisibility { + + /** + * Indicates that the validation errors are only visible for editable fields that have + * received focus. + */ + public object Automatic : ValidationErrorVisibility() + + /** + * Indicates the validation is run for all the editable fields regardless of their focus state, + * and any errors are shown. + */ + public object Visible : ValidationErrorVisibility() +} /** * A composable Popup toolkit component that enables users to see Popup content in a @@ -99,6 +120,14 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { Popup(stateData, modifier) } +@Composable +public fun Popup(popup: Popup, popupState: PopupState, modifier: Modifier = Modifier) { + val stateData = remember(popup) { + PopupState(popup) + } + Popup(stateData, modifier) +} + /** * Maintain list of attachments outside of SDK * https://devtopia.esri.com/runtime/apollo/issues/681 @@ -239,6 +268,12 @@ private fun PopupBody( } } + is UtilityAssociationsPopupElement -> { + item(contentType = UtilityAssociationsFormElement::class.java) { + + } + } + else -> { // other popup elements are not created } @@ -313,6 +348,13 @@ internal fun rememberStates( ) } + is UtilityAssociationsPopupElement -> { + states.add( + element, + UtilityAssociationsElementState(element., rememberCoroutineScope()) + ) + } + else -> { // TODO remove for release println("encountered element of type ${element::class.java}") diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt new file mode 100644 index 000000000..9591e241f --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -0,0 +1,17 @@ +package com.arcgismaps.toolkit.popup + +import androidx.compose.runtime.Stable +import com.arcgismaps.mapping.popup.Popup +import kotlinx.coroutines.CoroutineScope + +public class PopupState(@Stable public val popup: Popup) { + + public constructor(popup: Popup, scope: CoroutineScope) : this(popup) { + } + +} + + + + + diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt new file mode 100644 index 000000000..1fc912abb --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.navigation + +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.arcgismaps.mapping.featureforms.FeatureForm +import com.arcgismaps.mapping.featureforms.FieldFormElement +import com.arcgismaps.toolkit.popup.ValidationErrorVisibility +import com.arcgismaps.toolkit.popup.PopupState +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationDetails +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +import com.arcgismaps.toolkit.popup.internal.screens.FeatureFormScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen + +@Composable +internal fun FeatureFormNavHost( + navController: NavHostController, + state: PopupState, + isNavigationEnabled: Boolean, + validationErrorVisibility: ValidationErrorVisibility, + onSaveForm: suspend (FeatureForm, Boolean) -> Result, + onDiscardForm: suspend (Boolean) -> Unit, + onBarcodeButtonClick: ((FieldFormElement) -> Unit)?, + modifier: Modifier = Modifier, +) { + NavHost( + navController, + startDestination = NavigationRoute.FormView, + modifier = modifier, + enterTransition = { slideInHorizontally { h -> h } }, + exitTransition = { fadeOut() }, + popEnterTransition = { fadeIn() }, + popExitTransition = { slideOutHorizontally { h -> h } } + ) { + composable { backStackEntry -> + val formData = remember(backStackEntry) { state.getActiveFormStateData() } + FeatureFormScreen( + formStateData = formData, + onBarcodeButtonClick = onBarcodeButtonClick, + onUtilityFilterSelected = { state -> + val newRoute = NavigationRoute.UNFilterView(stateId = state.id) + // Navigate to the filter view + navController.navigateSafely(backStackEntry, newRoute) + } + ) + LaunchedEffect(formData) { + // Update the active feature form if we navigate back to this screen from another form. + state.updateActiveFeatureForm() + } + // launch a new side effect in a launched effect when validationErrorVisibility changes + // for a given form + LaunchedEffect(validationErrorVisibility, formData) { + // if it set to always show errors validate all fields + if (validationErrorVisibility == ValidationErrorVisibility.Visible) { + state.validateAllFields() + } + } + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val formData = remember(backStackEntry) { state.getActiveFormStateData() } + UNAssociationsFilterScreen( + formStateData = formData, + route = route, + onGroupSelected = { stateId -> + val newRoute = NavigationRoute.UNAssociationsView(stateId = stateId) + navController.navigateSafely(backStackEntry, newRoute) + }, + modifier = Modifier.fillMaxSize() + ) + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val formData = remember(backStackEntry) { state.getActiveFormStateData() } + UNAssociationsScreen( + formStateData = formData, + route = route, + isNavigationEnabled = isNavigationEnabled, + onSave = onSaveForm, + onDiscard = onDiscardForm, + onNavigateToFeature = { feature -> + // Request the state to navigate to the feature. + state.navigateTo(backStackEntry, feature) + }, + onNavigateToAssociation = { stateId -> + val route = NavigationRoute.UNAssociationDetailView(stateId = stateId) + // Request the state to navigate to the association. + navController.navigateSafely(backStackEntry, route) + }, + modifier = Modifier.fillMaxSize() + ) + LaunchedEffect(formData) { + // Update the active feature form when we navigate back to this screen from another + // form. + state.updateActiveFeatureForm() + } + } + + composable { backStackEntry -> + val route = backStackEntry.toRoute() + val formData = remember(backStackEntry) { state.getActiveFormStateData() } + // Get the selected UtilityAssociationsElementState from the state collection + val utilityAssociationsElementState = formData.stateCollection[route.stateId] + // guard against null value + as? UtilityAssociationsElementState ?: return@composable + // Display the association details + UtilityAssociationDetails( + state = utilityAssociationsElementState, + modifier = Modifier.fillMaxSize() + ) + } + } +} + +/** + * If the lifecycle is not resumed it means this NavBackStackEntry already processed a nav event. + * + * This is used to de-duplicate navigation events. + */ +internal fun NavBackStackEntry.lifecycleIsResumed() = + this.lifecycle.currentState == Lifecycle.State.RESUMED + +/** + * Navigate to the given route only if the lifecycle of the [backStackEntry] is resumed. + */ +internal fun NavHostController.navigateSafely( + backStackEntry: NavBackStackEntry, + route: T +): Boolean { + return if (backStackEntry.lifecycleIsResumed()) { + this.navigate(route) + true + } else false +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt new file mode 100644 index 000000000..c1d705914 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.navigation + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Indicates the action to take when navigating. + */ +@Parcelize +internal sealed class NavigationAction(val value: Int) : Parcelable { + + /** + * Indicates no action. + */ + @Parcelize + data object None : NavigationAction(0) + + /** + * Indicates an action to navigate back. + */ + @Parcelize + data object NavigateBack : NavigationAction(1) + + /** + * Indicates an action to dismiss the form. + */ + @Parcelize + data object Dismiss : NavigationAction(2) + + /** + * Indicates an action to navigate to an associated feature. + * + * @param index The index of the association to navigate to. + */ + @Parcelize + data class NavigateToFeature(val index : Int) : NavigationAction(3) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt new file mode 100644 index 000000000..db29313d0 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.navigation + +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +//import com.arcgismaps.toolkit.featureforms.internal.screens.FeatureFormScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen +import kotlinx.serialization.Serializable + +/** + * Navigation routes for the form. + */ +@Serializable +internal sealed class NavigationRoute { + + /** + * Represents the [FeatureFormScreen]. + */ + @Serializable + data object FormView : NavigationRoute() + + /** + * Represents a view for the [UNAssociationsFilterScreen]. + * + * @param stateId The state ID of the [UtilityAssociationsElementState] which contains the + * selected filter. + */ + @Serializable + data class UNFilterView( + val stateId: Int + ) : NavigationRoute() + + /** + * Represents a view for the [UNAssociationsScreen]. + * + * @param stateId The state ID of the [UtilityAssociationsElementState] which contains the + * selected group of associations. + */ + @Serializable + data class UNAssociationsView( + val stateId: Int + ) : NavigationRoute() + + /** + * Represents a view for the details of a specific association. + * + * @param stateId The state ID of the [UtilityAssociationsElementState] which contains the + * selected association. + */ + @Serializable + data class UNAssociationDetailView( + val stateId: Int + ) : NavigationRoute() +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt new file mode 100644 index 000000000..201979e24 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement +import com.arcgismaps.toolkit.featureforms.FormStateData +import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociationFilter +import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociationsElementState +import com.arcgismaps.toolkit.featureforms.internal.navigation.NavigationRoute +import com.arcgismaps.toolkit.featureforms.internal.utils.FeatureFormDialog + +/** + * Screen that displays the selected filter for a [UtilityAssociationsFormElement]. + * + * @param formStateData The form state data. + * @param route The [NavigationRoute.UNFilterView] route data of this screen. + * @param onGroupSelected The callback that is invoked when a group is selected. + * @param modifier The modifier to be applied to the layout. + */ +@Composable +internal fun UNAssociationsFilterScreen( + formStateData: FormStateData, + route : NavigationRoute.UNFilterView, + onGroupSelected: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val states = formStateData.stateCollection + // Get the selected UtilityAssociationsElementState from the state collection + val utilityAssociationsElementState = states[route.stateId] + // guard against null value + as? UtilityAssociationsElementState ?: return + // Get the selected filter from the UtilityAssociationsElementState + val filterResult = utilityAssociationsElementState.selectedFilterResult + // guard against null value + if (filterResult != null) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Top + ) { + UtilityAssociationFilter( + groupResults = filterResult.groupResults, + onGroupClick = { groupResult -> + utilityAssociationsElementState.setSelectedGroupResult(groupResult) + onGroupSelected(utilityAssociationsElementState.id) + }, + modifier = Modifier + .padding(16.dp) + .wrapContentSize() + ) + } + } + FeatureFormDialog(states) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt new file mode 100644 index 000000000..ac2c8a0e0 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -0,0 +1,141 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.screens + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.mapping.featureforms.FeatureForm +import com.arcgismaps.toolkit.featureforms.FormStateData +import com.arcgismaps.toolkit.featureforms.internal.components.dialogs.SaveEditsDialog +import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociations +import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociationsElementState +import com.arcgismaps.toolkit.featureforms.internal.navigation.NavigationAction +import com.arcgismaps.toolkit.featureforms.internal.navigation.NavigationRoute +import com.arcgismaps.toolkit.featureforms.internal.utils.FeatureFormDialog +import kotlinx.coroutines.launch + +/** + * Screen that displays the selected group of associations. + * + * @param formStateData The form state data. + * @param route The [NavigationRoute.UNAssociationsView] route data of this screen. + * @param onSave The callback to be invoked when the save button is clicked. The boolean parameter + * indicates whether this action should be followed by a forward navigation. The callback should + * return a [Result] that indicates the success or failure of the save operation. + * @param onDiscard The callback to be invoked when the discard button is clicked. The boolean parameter + * indicates whether this action should be followed by a forward navigation. + * @param onNavigateToFeature The callback to be invoked when the user selects a feature to navigate to. + * @param onNavigateToAssociation The callback to be invoked when the user selects an association to navigate to. + * @param modifier The modifier to be applied to the layout. + */ +@Composable +internal fun UNAssociationsScreen( + formStateData: FormStateData, + route: NavigationRoute.UNAssociationsView, + isNavigationEnabled: Boolean, + onSave: suspend (FeatureForm, Boolean) -> Result, + onDiscard: suspend (Boolean) -> Unit, + onNavigateToFeature: (ArcGISFeature) -> Unit, + onNavigateToAssociation: (Int) -> Unit, + modifier: Modifier = Modifier +) { + val featureForm = formStateData.featureForm + val states = formStateData.stateCollection + // Get the selected UtilityAssociationsElementState from the state collection + val utilityAssociationsElementState = states[route.stateId] as? + UtilityAssociationsElementState ?: return + // Get the selected filter from the UtilityAssociationsElementState + val filterResult = utilityAssociationsElementState.selectedFilterResult + // Get the selected group from the filter + val groupResult = utilityAssociationsElementState.selectedGroupResult + if (filterResult == null || groupResult == null) { + // guard against null values + return + } + val hasEdits by featureForm.hasEdits.collectAsState() + val scope = rememberCoroutineScope() + // State to hold the pending navigation action when the form has unsaved edits + var pendingNavigationAction: NavigationAction by rememberSaveable { + mutableStateOf(NavigationAction.None) + } + // Handler for navigating to a selected associated feature + val navigateToFeature: (NavigationAction) -> Unit = { action -> + if (action is NavigationAction.NavigateToFeature) { + val selectedIndex = action.index + groupResult.associationResults.getOrNull(selectedIndex)?.associatedFeature?.let { feature -> + onNavigateToFeature(feature) + } + } + } + UtilityAssociations( + groupResult = groupResult, + isNavigationEnabled = isNavigationEnabled, + onItemClick = { index -> + if (hasEdits) { + pendingNavigationAction = NavigationAction.NavigateToFeature(index) + } else { + val feature = groupResult.associationResults[index].associatedFeature + // Navigate to the next form if there are no edits. + onNavigateToFeature(feature) + } + }, + onDetailsClick = { index -> + val association = groupResult.associationResults[index] + utilityAssociationsElementState.setSelectedAssociationResult(association) + onNavigateToAssociation(utilityAssociationsElementState.id) + }, + modifier = modifier + .padding(16.dp) + .fillMaxSize() + ) + if (pendingNavigationAction != NavigationAction.None) { + SaveEditsDialog( + onDismissRequest = { + // Clear the pending navigation action when the dialog is dismissed + pendingNavigationAction = NavigationAction.None + }, + onSave = { + scope.launch { + onSave(featureForm, true).onSuccess { + // If the save is successful, navigate to the association + navigateToFeature(pendingNavigationAction) + } + pendingNavigationAction = NavigationAction.None + } + }, + onDiscard = { + scope.launch { + onDiscard(true) + // Navigate to the association after discarding changes + navigateToFeature(pendingNavigationAction) + pendingNavigationAction = NavigationAction.None + } + } + ) + } + FeatureFormDialog(states) +} From 7327a00673432e09acc1de2b5cba5652c878bfda Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 3 Sep 2025 13:42:53 -0700 Subject: [PATCH 03/36] Update Popup and UtilityAssociationsElementState --- .../src/main/java/com/arcgismaps/toolkit/popup/Popup.kt | 9 ++++++--- .../UtilityAssociationsElementState.kt | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 111ecd26c..e1c2a7013 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -269,8 +269,11 @@ private fun PopupBody( } is UtilityAssociationsPopupElement -> { - item(contentType = UtilityAssociationsFormElement::class.java) { - + item(contentType = UtilityAssociationsPopupElement::class.java) { + UtilityAssociationsElement( + entry.state as UtilityAssociationsElementState, + onItemClick = { } + ) } } @@ -351,7 +354,7 @@ internal fun rememberStates( is UtilityAssociationsPopupElement -> { states.add( element, - UtilityAssociationsElementState(element., rememberCoroutineScope()) + UtilityAssociationsElementState(element, rememberCoroutineScope()) ) } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt index c25d37ea7..e505dd475 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt @@ -19,6 +19,7 @@ package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement +import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState import com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult @@ -34,13 +35,14 @@ import kotlinx.coroutines.launch * @param scope The [CoroutineScope] to launch coroutines from. */ internal class UtilityAssociationsElementState( - element: UtilityAssociationsFormElement, + element: UtilityAssociationsPopupElement, scope: CoroutineScope ) : PopupElementState() { override val id : Int = element.hashCode() - val label : String = element.label + val label : String = element.title val description: String = element.description - val isVisible : StateFlow = element.isVisible + // val isVisible : StateFlow = element.isVisible + private var _loading: MutableState = mutableStateOf(true) From 9b7246d62ec219448266cd0c1a6b43cd50590747 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Thu, 4 Sep 2025 13:54:19 -0700 Subject: [PATCH 04/36] WIP --- .../com/arcgismaps/toolkit/popup/Popup.kt | 106 +++++++++--------- .../arcgismaps/toolkit/popup/PopupState.kt | 85 +++++++++++++- 2 files changed, 140 insertions(+), 51 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index e1c2a7013..4ffe26a42 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -114,65 +114,71 @@ public sealed class ValidationErrorVisibility { */ @Composable public fun Popup(popup: Popup, modifier: Modifier = Modifier) { + val scope = rememberCoroutineScope() val stateData = remember(popup) { - PopupState(popup) + PopupState(popup, scope) } - Popup(stateData, modifier) -} -@Composable -public fun Popup(popup: Popup, popupState: PopupState, modifier: Modifier = Modifier) { - val stateData = remember(popup) { - PopupState(popup) - } - Popup(stateData, modifier) + Popup(popup, stateData, modifier) } -/** - * Maintain list of attachments outside of SDK - * https://devtopia.esri.com/runtime/apollo/issues/681 - */ -private val attachments: MutableList = mutableListOf() - @Composable -private fun Popup(popupState: PopupState, modifier: Modifier = Modifier) { - val popup = popupState.popup - val dynamicEntity = (popup.geoElement as? DynamicEntity) - var evaluated by rememberSaveable(popup) { mutableStateOf(false) } - var fetched by rememberSaveable(popup) { mutableStateOf(false) } - var lastUpdatedEntityId by rememberSaveable(dynamicEntity) { mutableLongStateOf(dynamicEntity?.id ?: -1) } - if (dynamicEntity != null) { - LaunchedEffect(popup) { - dynamicEntity.dynamicEntityChangedEvent.collect { - // briefly show the initializing screen so it is clear the entity just pulsed - // and values may have changed. - popupState.popup.evaluateExpressions() - lastUpdatedEntityId = it.receivedObservation?.id ?: -1 - } - } - } +public fun Popup(popup: Popup, popupState: PopupState, modifier: Modifier = Modifier) { LaunchedEffect(popup) { - popupState.popup.evaluateExpressions() - if (!fetched) { - val element = popupState.popup.evaluatedElements - .filterIsInstance() - .firstOrNull() - - // make a copy of the attachments when first fetched. - attachments.clear() - element?.fetchAttachments()?.onSuccess { - attachments.addAll(element.attachments) - } - - fetched = true - } - - evaluated = true + popupState.evaluateExpressions() } + val states = rememberStates(popup, popupState.attachments) + popupState.setStates(states) - Popup(popupState, evaluated && fetched, lastUpdatedEntityId, modifier) +// Popup(popupState, modifier) + Popup(popupState, popupState.initialEvaluation.value, -1, modifier) } +///** +// * Maintain list of attachments outside of SDK +// * https://devtopia.esri.com/runtime/apollo/issues/681 +// */ +//private val attachments: MutableList = mutableListOf() +// +//@Composable +//private fun Popup(popupState: PopupState, modifier: Modifier = Modifier) { +// val popup = popupState.popup +// val dynamicEntity = (popup.geoElement as? DynamicEntity) +// var evaluated by rememberSaveable(popup) { mutableStateOf(false) } +// var fetched by rememberSaveable(popup) { mutableStateOf(false) } +// var lastUpdatedEntityId by rememberSaveable(dynamicEntity) { mutableLongStateOf(dynamicEntity?.id ?: -1) } +// if (dynamicEntity != null) { +// LaunchedEffect(popup) { +// dynamicEntity.dynamicEntityChangedEvent.collect { +// // briefly show the initializing screen so it is clear the entity just pulsed +// // and values may have changed. +// popupState.popup.evaluateExpressions() +// lastUpdatedEntityId = it.receivedObservation?.id ?: -1 +// } +// } +// } +// LaunchedEffect(popup) { +// popupState.popup.evaluateExpressions() +// if (!fetched) { +// val element = popupState.popup.evaluatedElements +// .filterIsInstance() +// .firstOrNull() +// +// // make a copy of the attachments when first fetched. +// attachments.clear() +// element?.fetchAttachments()?.onSuccess { +// attachments.addAll(element.attachments) +// } +// +// fetched = true +// } +// +// evaluated = true +// } +// +// Popup(popupState, evaluated && fetched, lastUpdatedEntityId, modifier) +//} + @Composable private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Long, modifier: Modifier = Modifier) { val scope = rememberCoroutineScope() @@ -222,9 +228,9 @@ private fun PopupBody( refreshed: Long, onFileClicked: (ViewableFile) -> Unit = {} ) { - val popup = popupState.popup +// val popup = popupState.popup val lazyListState = rememberLazyListState() - val states = rememberStates(popup, attachments) + val states = popupState.stateCollection LazyColumn( modifier = Modifier.semantics { contentDescription = "lazy column" }, state = lazyListState diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 9591e241f..1e9138354 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -1,17 +1,100 @@ package com.arcgismaps.toolkit.popup +import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import com.arcgismaps.mapping.featureforms.FeatureForm +import com.arcgismaps.mapping.featureforms.FormExpressionEvaluationError +import com.arcgismaps.mapping.popup.AttachmentsPopupElement import com.arcgismaps.mapping.popup.Popup +import com.arcgismaps.mapping.popup.PopupAttachment +import com.arcgismaps.mapping.popup.PopupExpressionEvaluation +import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateCollection import kotlinx.coroutines.CoroutineScope +import kotlin.collections.addAll +import kotlin.text.clear public class PopupState(@Stable public val popup: Popup) { + private val store: ArrayDeque = ArrayDeque() + + private lateinit var coroutineScope: CoroutineScope + + internal lateinit var stateCollection: PopupElementStateCollection + private set + + internal val attachments: MutableList = mutableListOf() + + /** + * Indicates if the evaluateExpression function for the [popup] has been run. + */ + internal var initialEvaluation : MutableState = mutableStateOf(false) + private set + + /** + * Indicates if the expressions for the [popup] are currently being evaluated. + */ + internal var isEvaluatingExpressions: MutableState = mutableStateOf(false) + private set + public constructor(popup: Popup, scope: CoroutineScope) : this(popup) { + this.coroutineScope = scope } -} + internal fun setStates(stateCollection: PopupElementStateCollection) { + this.stateCollection = stateCollection + // Add the provided state collection to the store. + val popupStateData = PopupStateData(this.popup, stateCollection) + store.addLast(popupStateData) + } + + /** + * Evaluates the expressions for the [popup] and returns the result. After a successful + * evaluation, the [initialEvaluation] is set to true. While this function is running, the + * [isEvaluatingExpressions] will be true. + */ + internal suspend fun evaluateExpressions() : Result> { + try { + isEvaluatingExpressions.value = true + return popup.evaluateExpressions().onSuccess { + val element = popup.evaluatedElements + .filterIsInstance() + .firstOrNull() + + // make a copy of the attachments when first fetched. + attachments.clear() + element?.fetchAttachments()?.onSuccess { + attachments.addAll(element.attachments) + } + // Set the initial evaluation to true after the first successful evaluation. + initialEvaluation.value = true + } + } finally { + isEvaluatingExpressions.value = false + } + } + /** + * Returns the [PopupStateData] that is currently on top of the stack. + */ + internal fun getActivePopupStateData(): PopupStateData { + return store.last() + } +} +/** + * A structure that holds the [Popup] and its associated [PopupElementStateCollection]. + * + * This class is also [Stable] and enables composition optimizations. + * + * @param popup the [Popup] to create the state for. + * @param stateCollection the [PopupElementStateCollection] created for the [Popup]. + */ +@Stable +internal data class PopupStateData( + val popup: Popup, + val stateCollection: PopupElementStateCollection +) From f78a605e8288443d6764786636dc65a10f3b62f0 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 8 Sep 2025 11:07:58 -0700 Subject: [PATCH 05/36] WIP --- .../internal/screens/ContentAwareTopBar.kt | 60 +-- toolkit/popup/build.gradle.kts | 1 + .../com/arcgismaps/toolkit/popup/Popup.kt | 444 +++++++++++------- .../arcgismaps/toolkit/popup/PopupState.kt | 232 +++++++-- .../fieldselement/FieldsElementState.kt | 34 +- .../internal/element/state/StateCollection.kt | 34 +- .../element/textelement/TextElementState.kt | 24 +- .../internal/navigation/FeatureFormNavHost.kt | 70 +-- .../internal/navigation/NavigationRoute.kt | 4 +- .../internal/screens/ContentAwareTopBar.kt | 415 ++++++++++++++++ .../popup/internal/screens/PopupScreen.kt | 216 +++++++++ .../screens/UNAssociationsFilterScreen.kt | 17 +- .../internal/screens/UNAssociationsScreen.kt | 93 ++-- toolkit/popup/src/main/res/values/strings.xml | 2 + 14 files changed, 1307 insertions(+), 339 deletions(-) create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt index 691d857ec..c2595e795 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt @@ -180,36 +180,36 @@ internal fun ContentAwareTopBar( evaluationProvider = { formData.isEvaluatingExpressions.value } ) } - if (pendingNavigationAction != NavigationAction.None) { - SaveEditsDialog( - onDismissRequest = { - // Clear the pending action when the dialog is dismissed - pendingNavigationAction = NavigationAction.None - }, - onSave = { - scope.launch(Dispatchers.Main) { - // Check if the pending action is to navigate back, since NavigateToAssociation - // is not triggered by the top bar - val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack - onSaveForm(formData.featureForm, willNavigate).onSuccess { - // Execute the pending navigation action after saving - onNavigationAction(pendingNavigationAction, false) - } - pendingNavigationAction = NavigationAction.None - } - }, - onDiscard = { - scope.launch(Dispatchers.Main) { - // Check if the pending action is to navigate back, since NavigateToAssociation - // is not triggered by the top bar - val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack - onDiscardForm(willNavigate) - onNavigationAction(pendingNavigationAction, false) - pendingNavigationAction = NavigationAction.None - } - } - ) - } +// if (pendingNavigationAction != NavigationAction.None) { +// SaveEditsDialog( +// onDismissRequest = { +// // Clear the pending action when the dialog is dismissed +// pendingNavigationAction = NavigationAction.None +// }, +// onSave = { +// scope.launch(Dispatchers.Main) { +// // Check if the pending action is to navigate back, since NavigateToAssociation +// // is not triggered by the top bar +// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack +// onSaveForm(formData.featureForm, willNavigate).onSuccess { +// // Execute the pending navigation action after saving +// onNavigationAction(pendingNavigationAction, false) +// } +// pendingNavigationAction = NavigationAction.None +// } +// }, +// onDiscard = { +// scope.launch(Dispatchers.Main) { +// // Check if the pending action is to navigate back, since NavigateToAssociation +// // is not triggered by the top bar +// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack +// onDiscardForm(willNavigate) +// onNavigationAction(pendingNavigationAction, false) +// pendingNavigationAction = NavigationAction.None +// } +// } +// ) +// } // only enable back navigation if there is a previous route BackHandler(hasBackStack) { onBackAction(backStackEntry) diff --git a/toolkit/popup/build.gradle.kts b/toolkit/popup/build.gradle.kts index 8c2ad4cb1..b496ab5fe 100644 --- a/toolkit/popup/build.gradle.kts +++ b/toolkit/popup/build.gradle.kts @@ -23,6 +23,7 @@ plugins { id("artifact-deploy") id("kotlin-parcelize") alias(libs.plugins.binary.compatibility.validator) apply true + alias(libs.plugins.kotlin.serialization) apply true } android { namespace = "com.arcgismaps.toolkit.popup" diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 4ffe26a42..4d41e2fc7 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -18,9 +18,12 @@ package com.arcgismaps.toolkit.popup +import android.content.Context import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -32,6 +35,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable @@ -40,14 +44,22 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.ComposeNavigator +import androidx.navigation.compose.DialogNavigator +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement import com.arcgismaps.mapping.popup.AttachmentsPopupElement import com.arcgismaps.mapping.popup.FieldsPopupElement @@ -62,7 +74,7 @@ import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsPopup import com.arcgismaps.toolkit.popup.internal.element.attachment.rememberAttachmentsElementState import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsElementState import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsPopupElement -import com.arcgismaps.toolkit.popup.internal.element.fieldselement.rememberFieldsElementState +//import com.arcgismaps.toolkit.popup.internal.element.fieldselement.rememberFieldsElementState import com.arcgismaps.toolkit.popup.internal.element.media.MediaElementState import com.arcgismaps.toolkit.popup.internal.element.media.MediaPopupElement import com.arcgismaps.toolkit.popup.internal.element.media.rememberMediaElementState @@ -70,12 +82,15 @@ import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateColl import com.arcgismaps.toolkit.popup.internal.element.state.mutablePopupElementStateCollection import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState import com.arcgismaps.toolkit.popup.internal.element.textelement.TextPopupElement -import com.arcgismaps.toolkit.popup.internal.element.textelement.rememberTextElementState +//import com.arcgismaps.toolkit.popup.internal.element.textelement.rememberTextElementState import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElement import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +import com.arcgismaps.toolkit.popup.internal.navigation.FeatureFormNavHost +import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.FileViewer import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult +import kotlinx.coroutines.CoroutineScope //@Immutable //private data class PopupState(@Stable val popup: Popup) @@ -123,15 +138,107 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { } @Composable -public fun Popup(popup: Popup, popupState: PopupState, modifier: Modifier = Modifier) { - LaunchedEffect(popup) { - popupState.evaluateExpressions() +public fun Popup( + popup: Popup, + popupState: PopupState, + modifier: Modifier = Modifier, + onDismiss: () -> Unit = {}, + showCloseIcon: Boolean = true, + showFormActions: Boolean = true, + isNavigationEnabled : Boolean = true, +) { + // Add the provided state collection to the store. + val popupStateData = PopupStateData(popup) +// popupState.store.addLast(popupStateData) +// LaunchedEffect(popup) { +// popupState.evaluateExpressions() +// } +// val states = rememberStates(popup, popupState.attachments, rememberCoroutineScope()) +// popupState.setStates(states) + + val navController = rememberNavController(popupState) + popupState.setNavigationCallback { route -> + navController.navigate(route) + } + popupState.setNavigateBack { + navController.navigateUp() + } + + PopupLayout( + topBar = { + val backStackEntry by navController.currentBackStackEntryAsState() + // Track if there is a back stack entry + val hasBackStack = remember(backStackEntry) { + navController.previousBackStackEntry != null + } + backStackEntry?.let { entry -> + ContentAwareTopBar( + backStackEntry = entry, + state = popupState, +// onSaveForm = ::saveForm, +// onDiscardForm = ::discardForm, + onDismissRequest = onDismiss, + hasBackStack = hasBackStack, + showFormActions = showFormActions, + showCloseIcon = showCloseIcon, + isNavigationEnabled = isNavigationEnabled, + modifier = Modifier + .padding( + vertical = 8.dp, + horizontal = if (hasBackStack) 8.dp else 16.dp + ) + .fillMaxWidth(), + ) + } + }, + content = { + FeatureFormNavHost( + navController = navController, + state = popupState, +// isNavigationEnabled = isNavigationEnabled, +// validationErrorVisibility = validationErrorVisibility, +// onSaveForm = ::saveForm, +// onDiscardForm = ::discardForm, +// onBarcodeButtonClick = onBarcodeButtonClick, + modifier = Modifier.fillMaxSize() + ) + }, + modifier = modifier + ) + DisposableEffect(popupState) { + onDispose { + // Clear the navigation actions when the composition is disposed + popupState.setNavigationCallback(null) + popupState.setNavigateBack(null) + } } - val states = rememberStates(popup, popupState.attachments) - popupState.setStates(states) + +// FeatureFormNavHost( +// navController = navController, +// state = popupState, +//// isNavigationEnabled = isNavigationEnabled, +//// validationErrorVisibility = validationErrorVisibility, +//// onSaveForm = ::saveForm, +//// onDiscardForm = ::discardForm, +//// onBarcodeButtonClick = onBarcodeButtonClick, +// modifier = Modifier.fillMaxSize() +// ) // Popup(popupState, modifier) - Popup(popupState, popupState.initialEvaluation.value, -1, modifier) +// Popup(popupState, popupState.initialEvaluation.value, -1, modifier) +} + +@Composable +internal fun PopupLayout( + topBar: @Composable ColumnScope.() -> Unit, + content: @Composable ColumnScope.() -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + topBar() + HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) + content() + } } ///** @@ -179,136 +286,136 @@ public fun Popup(popup: Popup, popupState: PopupState, modifier: Modifier = Modi // Popup(popupState, evaluated && fetched, lastUpdatedEntityId, modifier) //} -@Composable -private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Long, modifier: Modifier = Modifier) { - val scope = rememberCoroutineScope() - val popup = popupState.popup - val viewableFileState = rememberSaveable { mutableStateOf(null) } - viewableFileState.value?.let { viewableFile -> - FileViewer(scope, fileState = viewableFile) { - viewableFileState.value = null - } - } - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = popup.title, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(horizontal = 15.dp) - ) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(15.dp) - ) - InitializingExpressions(modifier = Modifier.fillMaxWidth()) { - initialized - } - HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) - if (initialized) { - PopupBody(popupState, refreshed) { - viewableFileState.value = it - } - } - } -} - -/** - * The body of the Popup composable - * - * @param popupState the immutable state object containing the Popup. - * @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity - * @param onFileClicked the callback to display an attachment or media image - */ -@Composable -private fun PopupBody( - popupState: PopupState, - refreshed: Long, - onFileClicked: (ViewableFile) -> Unit = {} -) { +//@Composable +//private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Long, modifier: Modifier = Modifier) { +// val scope = rememberCoroutineScope() // val popup = popupState.popup - val lazyListState = rememberLazyListState() - val states = popupState.stateCollection - LazyColumn( - modifier = Modifier.semantics { contentDescription = "lazy column" }, - state = lazyListState - ) { - states.forEach { entry -> - val element = entry.popupElement - when (element) { - is TextPopupElement -> { - // a contentType is needed to reuse the TextPopupElement composable inside a LazyColumn - item(contentType = TextPopupElement::class.java) { - TextPopupElement( - entry.state as TextElementState - ) - } - } - - is AttachmentsPopupElement -> { - item(contentType = AttachmentsPopupElement::class.java) { - AttachmentsPopupElement( - state = entry.state as AttachmentsElementState, - onSelectedAttachment = onFileClicked - ) - } - } - - is FieldsPopupElement -> { - item(contentType = FieldsPopupElement::class.java) { - FieldsPopupElement( - state = entry.state as FieldsElementState, - refreshed = refreshed - ) - } - } - - is MediaPopupElement -> { - item(contentType = MediaPopupElement::class.java) { - MediaPopupElement( - entry.state as MediaElementState, - onClickedMedia = onFileClicked - ) - } - } - - is UtilityAssociationsPopupElement -> { - item(contentType = UtilityAssociationsPopupElement::class.java) { - UtilityAssociationsElement( - entry.state as UtilityAssociationsElementState, - onItemClick = { } - ) - } - } - - else -> { - // other popup elements are not created - } - } - } - } -} - - -@Composable -internal fun InitializingExpressions( - modifier: Modifier = Modifier, - evaluationProvider: () -> Boolean -) { - val alpha by animateFloatAsState( - if (evaluationProvider()) 0f else 1f, - label = "evaluation loading alpha" - ) - Surface( - modifier = modifier.graphicsLayer { - this.alpha = alpha - } - ) { - LinearProgressIndicator(modifier) - } -} +// val viewableFileState = rememberSaveable { mutableStateOf(null) } +// viewableFileState.value?.let { viewableFile -> +// FileViewer(scope, fileState = viewableFile) { +// viewableFileState.value = null +// } +// } +// Column( +// modifier = modifier, +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// Text( +// text = popup.title, +// color = MaterialTheme.colorScheme.onBackground, +// modifier = Modifier.padding(horizontal = 15.dp) +// ) +// Spacer( +// modifier = Modifier +// .fillMaxWidth() +// .height(15.dp) +// ) +// InitializingExpressions(modifier = Modifier.fillMaxWidth()) { +// initialized +// } +// HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) +// if (initialized) { +// PopupBody(popupState, refreshed) { +// viewableFileState.value = it +// } +// } +// } +//} +// +///** +// * The body of the Popup composable +// * +// * @param popupState the immutable state object containing the Popup. +// * @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity +// * @param onFileClicked the callback to display an attachment or media image +// */ +//@Composable +//private fun PopupBody( +// popupState: PopupState, +// refreshed: Long, +// onFileClicked: (ViewableFile) -> Unit = {} +//) { +//// val popup = popupState.popup +// val lazyListState = rememberLazyListState() +// val states = popupState.stateCollection +// LazyColumn( +// modifier = Modifier.semantics { contentDescription = "lazy column" }, +// state = lazyListState +// ) { +// states.forEach { entry -> +// val element = entry.popupElement +// when (element) { +// is TextPopupElement -> { +// // a contentType is needed to reuse the TextPopupElement composable inside a LazyColumn +// item(contentType = TextPopupElement::class.java) { +// TextPopupElement( +// entry.state as TextElementState +// ) +// } +// } +// +// is AttachmentsPopupElement -> { +// item(contentType = AttachmentsPopupElement::class.java) { +// AttachmentsPopupElement( +// state = entry.state as AttachmentsElementState, +// onSelectedAttachment = onFileClicked +// ) +// } +// } +// +// is FieldsPopupElement -> { +// item(contentType = FieldsPopupElement::class.java) { +// FieldsPopupElement( +// state = entry.state as FieldsElementState, +// refreshed = refreshed +// ) +// } +// } +// +// is MediaPopupElement -> { +// item(contentType = MediaPopupElement::class.java) { +// MediaPopupElement( +// entry.state as MediaElementState, +// onClickedMedia = onFileClicked +// ) +// } +// } +// +// is UtilityAssociationsPopupElement -> { +// item(contentType = UtilityAssociationsPopupElement::class.java) { +// UtilityAssociationsElement( +// entry.state as UtilityAssociationsElementState, +// onItemClick = { } +// ) +// } +// } +// +// else -> { +// // other popup elements are not created +// } +// } +// } +// } +//} +// +// +//@Composable +//internal fun InitializingExpressions( +// modifier: Modifier = Modifier, +// evaluationProvider: () -> Boolean +//) { +// val alpha by animateFloatAsState( +// if (evaluationProvider()) 0f else 1f, +// label = "evaluation loading alpha" +// ) +// Surface( +// modifier = modifier.graphicsLayer { +// this.alpha = alpha +// } +// ) { +// LinearProgressIndicator(modifier) +// } +//} /** * Creates and remembers state objects for all the supported element types that are part of the @@ -317,10 +424,11 @@ internal fun InitializingExpressions( * @param popup the [Popup] to create the states for. * @return returns the [PopupElementStateCollection] created. */ -@Composable +//@Composable internal fun rememberStates( popup: Popup, - attachments: List + attachments: List, + coroutineScope: CoroutineScope ): PopupElementStateCollection { val states = mutablePopupElementStateCollection() popup.evaluatedElements.forEach { element -> @@ -328,39 +436,39 @@ internal fun rememberStates( is TextPopupElement -> { states.add( element, - rememberTextElementState(element = element, popup = popup) + TextElementState(element = element, popup = popup) ) } - is AttachmentsPopupElement -> { - states.add( - element, - rememberAttachmentsElementState( - popup = popup, - element = element, - attachments = attachments - ) - ) - } +// is AttachmentsPopupElement -> { +// states.add( +// element, +// rememberAttachmentsElementState( +// popup = popup, +// element = element, +// attachments = attachments +// ) +// ) +// } is FieldsPopupElement -> { states.add( element, - rememberFieldsElementState(element = element, popup = popup) + FieldsElementState(element = element, popup = popup) ) } - is MediaPopupElement -> { - states.add( - element, - rememberMediaElementState(element = element, popup = popup) - ) - } +// is MediaPopupElement -> { +// states.add( +// element, +// rememberMediaElementState(element = element, popup = popup) +// ) +// } is UtilityAssociationsPopupElement -> { states.add( element, - UtilityAssociationsElementState(element, rememberCoroutineScope()) + UtilityAssociationsElementState(element, coroutineScope) ) } @@ -373,3 +481,23 @@ internal fun rememberStates( return states } + +@Composable +internal fun rememberNavController(vararg inputs: Any): NavHostController { + val context = LocalContext.current + rememberNavController() + return rememberSaveable( + inputs = inputs, saver = Saver( + save = { it.saveState() }, + restore = { createNavController(context).apply { restoreState(it) } } + )) { + createNavController(context) + } +} + +private fun createNavController(context: Context): NavHostController { + return NavHostController(context).apply { + navigatorProvider.addNavigator(ComposeNavigator()) + navigatorProvider.addNavigator(DialogNavigator()) + } +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 1e9138354..21a526eca 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -1,18 +1,27 @@ package com.arcgismaps.toolkit.popup +import androidx.annotation.MainThread import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshotFlow +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination.Companion.hasRoute +import com.arcgismaps.data.ArcGISFeature +import com.arcgismaps.data.ArcGISFeatureTable import com.arcgismaps.mapping.featureforms.FeatureForm -import com.arcgismaps.mapping.featureforms.FormExpressionEvaluationError import com.arcgismaps.mapping.popup.AttachmentsPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupAttachment import com.arcgismaps.mapping.popup.PopupExpressionEvaluation import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateCollection +import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute +import com.arcgismaps.toolkit.popup.internal.navigation.lifecycleIsResumed +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope -import kotlin.collections.addAll -import kotlin.text.clear +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.launch public class PopupState(@Stable public val popup: Popup) { @@ -25,52 +34,150 @@ public class PopupState(@Stable public val popup: Popup) { internal val attachments: MutableList = mutableListOf() + /** - * Indicates if the evaluateExpression function for the [popup] has been run. + * A navigation callback that is called when navigating to a new [Popup]. This should + * be set by the composition that uses the NavController to the correct [NavigationRoute]. */ - internal var initialEvaluation : MutableState = mutableStateOf(false) - private set + private var navigateToRoute: ((NavigationRoute) -> Unit)? = null /** - * Indicates if the expressions for the [popup] are currently being evaluated. + * A navigation callback that is called when navigating back to a previous [Popup]. This + * should be set by the composition that uses the NavController to navigate back. */ - internal var isEvaluatingExpressions: MutableState = mutableStateOf(false) - private set + private var navigateBack: (() -> Boolean)? = null + + + private val _activePopup: MutableState = mutableStateOf(popup) + /** + * The currently active [Popup]. This property is updated when navigating between popups. + * + * Note that this property is observable and if you use it in the composable function it will be + * recomposed on every change. + * + * To observe changes to this property outside a restartable function, use [snapshotFlow]: + * ``` + * snapshotFlow { activePopup } + * ``` + */ + public val activePopup: Popup by _activePopup public constructor(popup: Popup, scope: CoroutineScope) : this(popup) { this.coroutineScope = scope + val popupStateData = PopupStateData(popup) + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + popupStateData.evaluateExpressions() + val states = rememberStates( + popup = popup, + attachments = attachments, + coroutineScope = coroutineScope + ) + popupStateData.setStates(states) + } + store.addLast(popupStateData) } - internal fun setStates(stateCollection: PopupElementStateCollection) { - this.stateCollection = stateCollection - // Add the provided state collection to the store. - val popupStateData = PopupStateData(this.popup, stateCollection) - store.addLast(popupStateData) + /** + * Sets the navigation callback to the provided [navigateToRoute] function. This function is + * called when navigating to a new [Popup]. Set this to null when the composition is + * disposed. + */ + internal fun setNavigationCallback(navigateToRoute: ((NavigationRoute) -> Unit)?) { + this.navigateToRoute = navigateToRoute } /** - * Evaluates the expressions for the [popup] and returns the result. After a successful - * evaluation, the [initialEvaluation] is set to true. While this function is running, the - * [isEvaluatingExpressions] will be true. + * Sets the navigation callback to the provided [navigateBack] function. This function is + * called when navigating back to a previous [Popup]. Set this to null when the composition + * is disposed. */ - internal suspend fun evaluateExpressions() : Result> { - try { - isEvaluatingExpressions.value = true - return popup.evaluateExpressions().onSuccess { - val element = popup.evaluatedElements - .filterIsInstance() - .firstOrNull() + internal fun setNavigateBack(navigateBack: (() -> Boolean)?) { + this.navigateBack = navigateBack + } - // make a copy of the attachments when first fetched. - attachments.clear() - element?.fetchAttachments()?.onSuccess { - attachments.addAll(element.attachments) + /** + * Updates the [activePopup] to the current popup on top of the stack. This should be + * called after navigating to a new popup or popping the current popup from the stack. + * + */ + internal suspend fun updateActivePopup() { + val popupStateData = getActivePopupStateData() + // Check if the active feature form is different from the current form. + if (_activePopup.value != popupStateData.popup) { + _activePopup.value = popupStateData.popup +// // refresh the feature to ensure the latest data is loaded. +// formStateData.featureForm.feature.refresh() +// if (formStateData.initialEvaluation.value.not()) { +// formStateData.evaluateExpressions() +// } + } + } + + /** + * Adds a new [FeatureForm] to the local stack and navigates to it. [updateActiveFeatureForm] + * must be called after this to update the [activeFeatureForm], preferably after the navigation + * is complete. + * + * @param backStackEntry the [NavBackStackEntry] of the current destination. + * @param feature the [ArcGISFeature] to create the [FeatureForm] for. + */ + @MainThread + internal fun navigateTo(backStackEntry: NavBackStackEntry, feature: ArcGISFeature): Boolean { + val navigateTo = navigateToRoute ?: return false + // Check if the backStackEntry is in the resumed state. + if (backStackEntry.lifecycleIsResumed().not()) return false + val popup = feature.toPopup() + val popupStateData = PopupStateData(popup) + coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { + popupStateData.evaluateExpressions() + val states = rememberStates( + popup = popup, + attachments = attachments, + coroutineScope = coroutineScope + ) + popupStateData.setStates(states) + } + // Add the new popup to the stack. + store.addLast(popupStateData) + // Navigate to the popup view. + navigateTo(NavigationRoute.PopupView) + return true + } + + /** + * Based on the current destination given by the [backStackEntry], this function navigates back + * to the previous view and pops the current [Popup] from the stack (if required). + * + * Note that this will pop the current popup from the stack even if there are edits. Check if + * there are edits before calling this function. + * + * [updateActivePopup] must be called after this to update the [activePopup], preferably + * after the navigation is complete. + * + * @return true if the navigation was successful, false otherwise. + */ + @MainThread + internal fun popBackStack(backStackEntry: NavBackStackEntry): Boolean { + val navigate = navigateBack ?: return false + // Check if the backStackEntry is in the resumed state. + if (backStackEntry.lifecycleIsResumed().not()) return false + // Check the current destination and pop the stack accordingly. + return when { + backStackEntry.destination.hasRoute() -> { + // Check if the stack has more than one popup. + if (store.size <= 1) { + false + } else { + // Remove the current popup from the stack. + store.removeLast() + // Navigate back to the popup view after popping the current form. + navigate() } - // Set the initial evaluation to true after the first successful evaluation. - initialEvaluation.value = true } - } finally { - isEvaluatingExpressions.value = false + + else -> { + navigate() + } } } @@ -93,8 +200,65 @@ public class PopupState(@Stable public val popup: Popup) { @Stable internal data class PopupStateData( val popup: Popup, - val stateCollection: PopupElementStateCollection -) +) { + internal lateinit var stateCollection: PopupElementStateCollection + private set + /** + * Indicates if the evaluateExpression function for the [popup] has been run. + */ + internal var initialEvaluation : MutableState = mutableStateOf(false) + private set + + /** + * Indicates if the expressions for the [popup] are currently being evaluated. + */ + internal var isEvaluatingExpressions: MutableState = mutableStateOf(false) + private set + + internal fun setStates(stateCollection: PopupElementStateCollection) { + this.stateCollection = stateCollection + } + + /** + * Evaluates the expressions for the [popup] and returns the result. After a successful + * evaluation, the [initialEvaluation] is set to true. While this function is running, the + * [isEvaluatingExpressions] will be true. + */ + internal suspend fun evaluateExpressions() : Result> { + try { + isEvaluatingExpressions.value = true + return popup.evaluateExpressions().onSuccess { + val element = popup.evaluatedElements + .filterIsInstance() + .firstOrNull() + // make a copy of the attachments when first fetched. +// attachments.clear() +// element?.fetchAttachments()?.onSuccess { +// attachments.addAll(element.attachments) +// } + // Set the initial evaluation to true after the first successful evaluation. + initialEvaluation.value = true + } + } finally { + isEvaluatingExpressions.value = false + } + } +} +internal fun ArcGISFeature.toPopup(): Popup { + val popupDefinition = when { + this.featureTable?.popupDefinition != null -> { + this.featureTable?.popupDefinition + } + this.getFeatureSubtype() != null && this.featureTable is ArcGISFeatureTable -> { + (this.featureTable as ArcGISFeatureTable).subtypeSubtables + .firstOrNull { it.name == this.getFeatureSubtype()?.name } + ?.popupDefinition + } + + else -> null + } + return Popup(this, popupDefinition) +} \ No newline at end of file diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt index 3b3de2202..16396670a 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt @@ -36,21 +36,29 @@ internal class FieldsElementState( val description: String, val fieldsToFormattedValues: Map, override val id: Int -) : Parcelable, PopupElementState() +) : Parcelable, PopupElementState() { -@Composable -internal fun rememberFieldsElementState( - element: FieldsPopupElement, - popup: Popup -): FieldsElementState = rememberSaveable( - inputs = arrayOf(popup, element) -) { - val fieldNames = element.fields.map { it.label } - val fieldsToFormattedValuesMap = fieldNames.zip(element.formattedValues).toMap() - FieldsElementState( + constructor(element: FieldsPopupElement, popup: Popup) : this( title = element.title, description = element.description, - fieldsToFormattedValuesMap, - id = PopupElementState.createId() + fieldsToFormattedValues = element.fields.map { it.label } + .zip(element.formattedValues) + .toMap(), + id = createId() ) } + +//internal fun createFieldsElementState( +// element: FieldsPopupElement, +// popup: Popup +//): FieldsElementState { +// val fieldNames = element.fields.map { it.label } +// val fieldsToFormattedValuesMap = fieldNames.zip(element.formattedValues).toMap() +// return FieldsElementState( +// title = element.title, +// description = element.description, +// fieldsToFormattedValuesMap, +// id = PopupElementState.createId() +// ) +//} + diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/state/StateCollection.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/state/StateCollection.kt index b8efd5b1b..3375e7d23 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/state/StateCollection.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/state/StateCollection.kt @@ -25,9 +25,20 @@ import com.arcgismaps.mapping.popup.PopupElement */ @Immutable internal interface PopupElementStateCollection : Iterable { + + /** + * Provides the bracket operator to the collection. + * + * @param id the unique identifier [PopupElementState.id] + * @return the [PopupElementState] associated with the id, or null if none. + */ + operator fun get(id: Int): PopupElementState? + interface Entry { val popupElement: PopupElement val state: PopupElementState + override fun equals(other: Any?): Boolean + override fun hashCode(): Int } } @@ -65,11 +76,32 @@ private class MutablePopupElementStateCollectionImpl : MutablePopupElementStateC entries.add(EntryImpl(popupElement, state)) } + override operator fun get(id: Int): PopupElementState? { + entries.forEach { entry -> + if (entry.state.id == id) { + return entry.state + } + } + return null + } + /** * Default implementation for a [PopupElementStateCollection.Entry]. */ class EntryImpl( override val popupElement: PopupElement, override val state: PopupElementState - ) : PopupElementStateCollection.Entry + ) : PopupElementStateCollection.Entry { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + + other as EntryImpl + + return popupElement == other.popupElement + } + + override fun hashCode(): Int = popupElement.hashCode() + } } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt index 62020209e..466eb430d 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt @@ -35,17 +35,21 @@ import kotlinx.parcelize.Parcelize internal class TextElementState( val value: String, override val id: Int -) : Parcelable, PopupElementState() +) : Parcelable, PopupElementState() { -@Composable -internal fun rememberTextElementState( - element: TextPopupElement, - popup: Popup -): TextElementState = rememberSaveable( - inputs = arrayOf(popup, element) -) { - TextElementState( + constructor(element: TextPopupElement, popup: Popup) : this( value = element.text, - id = PopupElementState.createId() + id = createId() ) } + +//internal fun createTextElementState( +// element: TextPopupElement, +// popup: Popup +//): TextElementState { +// return TextElementState( +// value = element.text, +// id = PopupElementState.createId() +// ) +//} + diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt index 1fc912abb..0c56c7d3f 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt @@ -37,7 +37,7 @@ import com.arcgismaps.toolkit.popup.ValidationErrorVisibility import com.arcgismaps.toolkit.popup.PopupState import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationDetails import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState -import com.arcgismaps.toolkit.popup.internal.screens.FeatureFormScreen +import com.arcgismaps.toolkit.popup.internal.screens.PopupScreen import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen @@ -45,52 +45,54 @@ import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen internal fun FeatureFormNavHost( navController: NavHostController, state: PopupState, - isNavigationEnabled: Boolean, - validationErrorVisibility: ValidationErrorVisibility, - onSaveForm: suspend (FeatureForm, Boolean) -> Result, - onDiscardForm: suspend (Boolean) -> Unit, - onBarcodeButtonClick: ((FieldFormElement) -> Unit)?, +// isNavigationEnabled: Boolean, +// validationErrorVisibility: ValidationErrorVisibility, +// onSaveForm: suspend (FeatureForm, Boolean) -> Result, +// onDiscardForm: suspend (Boolean) -> Unit, +// onBarcodeButtonClick: ((FieldFormElement) -> Unit)?, modifier: Modifier = Modifier, ) { NavHost( navController, - startDestination = NavigationRoute.FormView, + startDestination = NavigationRoute.PopupView, modifier = modifier, enterTransition = { slideInHorizontally { h -> h } }, exitTransition = { fadeOut() }, popEnterTransition = { fadeIn() }, popExitTransition = { slideOutHorizontally { h -> h } } ) { - composable { backStackEntry -> - val formData = remember(backStackEntry) { state.getActiveFormStateData() } - FeatureFormScreen( - formStateData = formData, - onBarcodeButtonClick = onBarcodeButtonClick, + composable { backStackEntry -> + val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } + PopupScreen( + state, + popupStateData, + popupStateData.initialEvaluation.value, -1, onUtilityFilterSelected = { state -> val newRoute = NavigationRoute.UNFilterView(stateId = state.id) // Navigate to the filter view navController.navigateSafely(backStackEntry, newRoute) - } + }, + modifier ) - LaunchedEffect(formData) { + LaunchedEffect(popupStateData) { // Update the active feature form if we navigate back to this screen from another form. - state.updateActiveFeatureForm() - } - // launch a new side effect in a launched effect when validationErrorVisibility changes - // for a given form - LaunchedEffect(validationErrorVisibility, formData) { - // if it set to always show errors validate all fields - if (validationErrorVisibility == ValidationErrorVisibility.Visible) { - state.validateAllFields() - } + state.updateActivePopup() } +// // launch a new side effect in a launched effect when validationErrorVisibility changes +// // for a given form +// LaunchedEffect(validationErrorVisibility, formData) { +// // if it set to always show errors validate all fields +// if (validationErrorVisibility == ValidationErrorVisibility.Visible) { +// state.validateAllFields() +// } +// } } composable { backStackEntry -> val route = backStackEntry.toRoute() - val formData = remember(backStackEntry) { state.getActiveFormStateData() } + val formData = remember(backStackEntry) { state.getActivePopupStateData() } UNAssociationsFilterScreen( - formStateData = formData, + popupStateData = formData, route = route, onGroupSelected = { stateId -> val newRoute = NavigationRoute.UNAssociationsView(stateId = stateId) @@ -102,13 +104,13 @@ internal fun FeatureFormNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() - val formData = remember(backStackEntry) { state.getActiveFormStateData() } + val formData = remember(backStackEntry) { state.getActivePopupStateData() } UNAssociationsScreen( - formStateData = formData, + popupStateData = formData, route = route, - isNavigationEnabled = isNavigationEnabled, - onSave = onSaveForm, - onDiscard = onDiscardForm, +// isNavigationEnabled = isNavigationEnabled, +// onSave = onSaveForm, +// onDiscard = onDiscardForm, onNavigateToFeature = { feature -> // Request the state to navigate to the feature. state.navigateTo(backStackEntry, feature) @@ -121,15 +123,15 @@ internal fun FeatureFormNavHost( modifier = Modifier.fillMaxSize() ) LaunchedEffect(formData) { - // Update the active feature form when we navigate back to this screen from another - // form. - state.updateActiveFeatureForm() + // Update the active popup when we navigate back to this screen from another + // popup. + state.updateActivePopup() } } composable { backStackEntry -> val route = backStackEntry.toRoute() - val formData = remember(backStackEntry) { state.getActiveFormStateData() } + val formData = remember(backStackEntry) { state.getActivePopupStateData() } // Get the selected UtilityAssociationsElementState from the state collection val utilityAssociationsElementState = formData.stateCollection[route.stateId] // guard against null value diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt index db29313d0..83fe33ab6 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt @@ -29,10 +29,10 @@ import kotlinx.serialization.Serializable internal sealed class NavigationRoute { /** - * Represents the [FeatureFormScreen]. + * Represents the [com.arcgismaps.toolkit.popup.internal.screens.PopupScreen]. */ @Serializable - data object FormView : NavigationRoute() + data object PopupView : NavigationRoute() /** * Represents a view for the [UNAssociationsFilterScreen]. diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt new file mode 100644 index 000000000..dd0cbaa5d --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt @@ -0,0 +1,415 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination.Companion.hasRoute +import androidx.navigation.toRoute +import com.arcgismaps.mapping.featureforms.FeatureForm +import com.arcgismaps.toolkit.popup.PopupState +import com.arcgismaps.toolkit.popup.PopupStateData +import com.arcgismaps.toolkit.popup.R +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +import com.arcgismaps.toolkit.popup.internal.navigation.NavigationAction +import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * A dynamic action bar that adapts its content based on the current navigation state. + * + * @param backStackEntry The [NavBackStackEntry] representing the current navigation state. + * @param state The [FeatureFormState] that holds the current form state data. + * @param hasBackStack Indicates if there is a previous route in the navigation stack. + * @param showFormActions Indicates if the form actions (save, discard) should be shown. + * @param showCloseIcon Indicates if the close icon should be displayed. + * @param onSaveForm The callback to invoke when the save button is clicked. It takes the current + * [FeatureForm] and a boolean indicating if the save is followed by a navigation action. + * @param onDiscardForm The callback to invoke when the discard button is clicked. It takes a boolean + * indicating if the discard is followed by a navigation action. + * @param onDismissRequest The callback to invoke when the close button is clicked. If the form has + * unsaved edits, this in invoked after the save or discard action is completed. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun ContentAwareTopBar( + backStackEntry: NavBackStackEntry, + state: PopupState, + hasBackStack: Boolean, + showFormActions: Boolean, + showCloseIcon: Boolean, + isNavigationEnabled: Boolean, +// onSaveForm: suspend (FeatureForm, Boolean) -> Result, +// onDiscardForm: suspend (Boolean) -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier +) { + val formData = remember(backStackEntry) { state.getActivePopupStateData() } + val scope = rememberCoroutineScope() +// val hasEdits by formData.featureForm.hasEdits.collectAsState() + val hasEdits = false + // State to hold the pending navigation action when the form has unsaved edits + var pendingNavigationAction: NavigationAction by rememberSaveable { + mutableStateOf(NavigationAction.None) + } + // Callback to handle navigation actions based on the form's edit state + val onNavigationAction: (NavigationAction, Boolean) -> Unit = { action, formHasEdits -> + if (formHasEdits) { + // If the form has edits, store the pending action + pendingNavigationAction = action + } else { + // Otherwise, execute the action immediately + when (action) { + is NavigationAction.NavigateBack -> { + state.popBackStack(backStackEntry) + } + + is NavigationAction.Dismiss -> { + onDismissRequest() + } + + else -> {} + } + } + } + val onBackAction: (NavBackStackEntry) -> Unit = { entry -> + when { + entry.destination.hasRoute() -> { + // Run the navigation action if the current view is the form view + onNavigationAction(NavigationAction.NavigateBack, hasEdits) + } + + else -> { + // Pop the back stack if the current view is not the form view + state.popBackStack(backStackEntry) + } + } + } + // Get the title and subtitle for the top bar based on the current navigation state + val (title, subTitle) = getTopBarTitleAndSubtitle(backStackEntry, formData) + val navigationEnabled = when { + // If the current destination is the form view, only then check if navigation is enabled + backStackEntry.destination.hasRoute() -> { + isNavigationEnabled + } + // For other destinations, always enable back navigation + else -> true + } + Column { + FeatureFormTitle( + title = title, + subTitle = subTitle, + hasEdits = if (showFormActions) hasEdits else false, + showCloseIcon = showCloseIcon, + showBackIcon = hasBackStack, + isNavigationEnabled = navigationEnabled, + onBackPressed = { + onBackAction(backStackEntry) + }, + onClose = { + onNavigationAction(NavigationAction.Dismiss, hasEdits) + }, +// onSave = { +// scope.launch { +// onSaveForm(formData.featureForm, false) +// } +// }, +// onDiscard = { +// scope.launch { +// onDiscardForm(false) +// } +// }, + modifier = modifier + ) +// InitializingExpressions( +// modifier = Modifier.fillMaxWidth(), +// evaluationProvider = { formData.isEvaluatingExpressions.value } +// ) + } +// if (pendingNavigationAction != NavigationAction.None) { +// SaveEditsDialog( +// onDismissRequest = { +// // Clear the pending action when the dialog is dismissed +// pendingNavigationAction = NavigationAction.None +// }, +// onSave = { +// scope.launch(Dispatchers.Main) { +// // Check if the pending action is to navigate back, since NavigateToAssociation +// // is not triggered by the top bar +// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack +// onSaveForm(formData.featureForm, willNavigate).onSuccess { +// // Execute the pending navigation action after saving +// onNavigationAction(pendingNavigationAction, false) +// } +// pendingNavigationAction = NavigationAction.None +// } +// }, +// onDiscard = { +// scope.launch(Dispatchers.Main) { +// // Check if the pending action is to navigate back, since NavigateToAssociation +// // is not triggered by the top bar +// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack +// onDiscardForm(willNavigate) +// onNavigationAction(pendingNavigationAction, false) +// pendingNavigationAction = NavigationAction.None +// } +// } +// ) +// } + // only enable back navigation if there is a previous route + BackHandler(hasBackStack) { + onBackAction(backStackEntry) + } +} + +@Composable +private fun getTopBarTitleAndSubtitle( + backStackEntry: NavBackStackEntry, + formData: PopupStateData, +): Pair { + var formTitle by remember(backStackEntry, formData) { + mutableStateOf(formData.popup.title) + } + +// LaunchedEffect(backStackEntry, formData) { +// formData.featureForm.title.collectLatest { +// formTitle = it +// } +// } + + val defaultTitle = stringResource(R.string.none_selected) + return when { + backStackEntry.destination.hasRoute() -> { + Pair( + formTitle, + "no description available" //formData.featureForm.description.value + ) + } + + backStackEntry.destination.hasRoute() -> { + var title = defaultTitle + val route = backStackEntry.toRoute() + (formData.stateCollection[route.stateId] as? UtilityAssociationsElementState)?.let { state -> + state.selectedFilterResult?.filter?.let { filter -> + title = filter.title + } + } + Pair(title, formTitle) + } + + backStackEntry.destination.hasRoute() -> { + var title = defaultTitle + var subTitle = defaultTitle + val route = backStackEntry.toRoute() + (formData.stateCollection[route.stateId] as? UtilityAssociationsElementState)?.let { state -> + state.selectedGroupResult?.let { group -> + title = group.name + } + state.selectedFilterResult?.filter?.let { filter -> + subTitle = filter.title + } + } + Pair(title, subTitle) + } + + backStackEntry.destination.hasRoute() -> { + Pair(stringResource(R.string.association_settings), "") + } + + else -> { + Pair(defaultTitle, defaultTitle) + } + } +} + +/** + * Represents the title bar of the form. + * + * @param title The title to display. + * @param subTitle The subtitle to display. + * @param hasEdits Indicates if the form has unsaved edits. An unsaved edits indicator is displayed + * along with the save and discard buttons if this is true. + * @param showCloseIcon Indicates if the close icon should be displayed. + * @param showBackIcon Indicates if the back icon should be displayed. + * @param onBackPressed The callback to invoke when the back button is clicked. + * @param onClose The callback to invoke when the close button is clicked. If null, the close button + * is not displayed. + * @param onSave The callback to invoke when the save button is clicked. + * @param onDiscard The callback to invoke when the discard button is clicked. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +private fun FeatureFormTitle( + title: String, + subTitle: String, + hasEdits: Boolean, + showCloseIcon: Boolean, + showBackIcon: Boolean, + isNavigationEnabled: Boolean, + onBackPressed: () -> Unit, + onClose: () -> Unit, +// onSave: () -> Unit, +// onDiscard: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + if (showBackIcon) { + IconButton(onClick = onBackPressed, enabled = isNavigationEnabled) { + Icon( + Icons.AutoMirrored.Outlined.ArrowBack, + contentDescription = "Navigate back" + ) + } + } + Column(modifier = Modifier.weight(1f)) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.weight(1f, fill = false) + ) + if (hasEdits) { + Spacer(Modifier.width(8.dp)) + Canvas(modifier = Modifier.size(10.dp)) { + drawCircle(color = Color(0xFFB3261E)) + } + } + } + if (subTitle.isNotEmpty()) { + Text( + text = subTitle, + style = MaterialTheme.typography.bodyMedium, + ) + } else if (hasEdits) { + Text( + text = "Unsaved Changes", + style = MaterialTheme.typography.bodyMedium, + ) + } + } + if (showCloseIcon) { + IconButton(onClick = onClose) { + Icon(imageVector = Icons.Default.Close, contentDescription = "close form") + } + } + } +// AnimatedVisibility(visible = hasEdits) { +// Row( +// modifier = Modifier.padding(top = 12.dp), +// horizontalArrangement = Arrangement.Start, +// verticalAlignment = Alignment.CenterVertically +// ) { +// Button(onClick = onSave) { +// Text( +// text = stringResource(R.string.save), +// style = MaterialTheme.typography.labelLarge +// ) +// } +// Spacer(Modifier.width(8.dp)) +// FilledTonalButton(onClick = onDiscard) { +// Text( +// text = stringResource(R.string.discard), +// style = MaterialTheme.typography.labelLarge +// ) +// } +// } +// } + } +} + +//@Composable +//private fun InitializingExpressions( +// modifier: Modifier = Modifier, +// evaluationProvider: () -> Boolean +//) { +// val alpha by animateFloatAsState( +// if (evaluationProvider()) 1f else 0f, +// label = "evaluation loading alpha" +// ) +// Surface( +// modifier = modifier.graphicsLayer { +// this.alpha = alpha +// } +// ) { +// LinearProgressIndicator(modifier) +// } +//} + +@Preview(showBackground = true) +@Composable +private fun FeatureFormTitlePreview() { + FeatureFormTitle( + title = "Structure Boundary", + subTitle = "Edit feature attributes", + hasEdits = true, + showCloseIcon = true, + showBackIcon = false, + isNavigationEnabled = true, + onBackPressed = {}, + onClose = {}, +// onSave = {}, +// onDiscard = {}, + modifier = Modifier.padding(8.dp) + ) +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt new file mode 100644 index 000000000..6098be0b1 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.screens + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import com.arcgismaps.mapping.popup.AttachmentsPopupElement +import com.arcgismaps.mapping.popup.FieldsPopupElement +import com.arcgismaps.mapping.popup.MediaPopupElement +import com.arcgismaps.mapping.popup.TextPopupElement +import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement +import com.arcgismaps.toolkit.popup.PopupState +import com.arcgismaps.toolkit.popup.PopupStateData +import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsElementState +import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsPopupElement +import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsElementState +import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsPopupElement +import com.arcgismaps.toolkit.popup.internal.element.media.MediaElementState +import com.arcgismaps.toolkit.popup.internal.element.media.MediaPopupElement +import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState +import com.arcgismaps.toolkit.popup.internal.element.textelement.TextPopupElement +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElement +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.FileViewer +import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile + + +@Composable +internal fun PopupScreen( + popupState: PopupState, +// initialized: Boolean, + popupStateData: PopupStateData, + initialized: Boolean, + refreshed: Long, + onUtilityFilterSelected: (UtilityAssociationsElementState) -> Unit, + modifier: Modifier = Modifier +) { + val scope = rememberCoroutineScope() + val popup = popupState.popup + val viewableFileState = rememberSaveable { mutableStateOf(null) } + viewableFileState.value?.let { viewableFile -> + FileViewer(scope, fileState = viewableFile) { + viewableFileState.value = null + } + } + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = popup.title, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 15.dp) + ) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(15.dp) + ) + InitializingExpressions(modifier = Modifier.fillMaxWidth()) { + initialized + } + HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) + if (initialized) { + PopupBody(popupStateData, refreshed, onUtilityFilterSelected) { + viewableFileState.value = it + } + } + } +} + +/** + * The body of the Popup composable + * + * @param popupState the immutable state object containing the Popup. + * @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity + * @param onFileClicked the callback to display an attachment or media image + */ +@Composable +private fun PopupBody( +// popupState: PopupState, + popupStateData: PopupStateData, + refreshed: Long, + onUtilityAssociationFilterClick: (UtilityAssociationsElementState) -> Unit, + onFileClicked: (ViewableFile) -> Unit = {} +) { +// val popup = popupState.popup + val lazyListState = rememberLazyListState() + val states = popupStateData.stateCollection + LazyColumn( + modifier = Modifier.semantics { contentDescription = "lazy column" }, + state = lazyListState + ) { + states.forEach { entry -> + val element = entry.popupElement + when (element) { + is TextPopupElement -> { + // a contentType is needed to reuse the TextPopupElement composable inside a LazyColumn + item(contentType = TextPopupElement::class.java) { + TextPopupElement( + entry.state as TextElementState + ) + } + } + + is AttachmentsPopupElement -> { + item(contentType = AttachmentsPopupElement::class.java) { + AttachmentsPopupElement( + state = entry.state as AttachmentsElementState, + onSelectedAttachment = onFileClicked + ) + } + } + + is FieldsPopupElement -> { + item(contentType = FieldsPopupElement::class.java) { + FieldsPopupElement( + state = entry.state as FieldsElementState, + refreshed = refreshed + ) + } + } + + is MediaPopupElement -> { + item(contentType = MediaPopupElement::class.java) { + MediaPopupElement( + entry.state as MediaElementState, + onClickedMedia = onFileClicked + ) + } + } + + is UtilityAssociationsPopupElement -> { + item(contentType = UtilityAssociationsPopupElement::class.java) { + val state = entry.state as UtilityAssociationsElementState + UtilityAssociationsElement( + state, + onItemClick = { selected -> + // Set the selected filter result in the state + state.setSelectedFilterResult(selected) + onUtilityAssociationFilterClick(state) + }, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 15.dp, + end = 15.dp, + top = 10.dp, + bottom = 20.dp + ) + ) + } + } + + else -> { + // other popup elements are not created + } + } + } + } +} + + +@Composable +internal fun InitializingExpressions( + modifier: Modifier = Modifier, + evaluationProvider: () -> Boolean +) { + val alpha by animateFloatAsState( + if (evaluationProvider()) 0f else 1f, + label = "evaluation loading alpha" + ) + Surface( + modifier = modifier.graphicsLayer { + this.alpha = alpha + } + ) { + LinearProgressIndicator(modifier) + } +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt index 201979e24..828f68ec2 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt @@ -24,28 +24,27 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement -import com.arcgismaps.toolkit.featureforms.FormStateData -import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociationFilter -import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociationsElementState -import com.arcgismaps.toolkit.featureforms.internal.navigation.NavigationRoute -import com.arcgismaps.toolkit.featureforms.internal.utils.FeatureFormDialog +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationFilter +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute +import com.arcgismaps.toolkit.popup.PopupStateData /** * Screen that displays the selected filter for a [UtilityAssociationsFormElement]. * - * @param formStateData The form state data. + * @param popupStateData The form state data. * @param route The [NavigationRoute.UNFilterView] route data of this screen. * @param onGroupSelected The callback that is invoked when a group is selected. * @param modifier The modifier to be applied to the layout. */ @Composable internal fun UNAssociationsFilterScreen( - formStateData: FormStateData, + popupStateData: PopupStateData, route : NavigationRoute.UNFilterView, onGroupSelected: (Int) -> Unit, modifier: Modifier = Modifier ) { - val states = formStateData.stateCollection + val states = popupStateData.stateCollection // Get the selected UtilityAssociationsElementState from the state collection val utilityAssociationsElementState = states[route.stateId] // guard against null value @@ -70,5 +69,5 @@ internal fun UNAssociationsFilterScreen( ) } } - FeatureFormDialog(states) +// FeatureFormDialog(states) } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt index ac2c8a0e0..d4ebb9bba 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -29,19 +29,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.mapping.featureforms.FeatureForm -import com.arcgismaps.toolkit.featureforms.FormStateData -import com.arcgismaps.toolkit.featureforms.internal.components.dialogs.SaveEditsDialog -import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociations -import com.arcgismaps.toolkit.featureforms.internal.components.utilitynetwork.UtilityAssociationsElementState -import com.arcgismaps.toolkit.featureforms.internal.navigation.NavigationAction -import com.arcgismaps.toolkit.featureforms.internal.navigation.NavigationRoute -import com.arcgismaps.toolkit.featureforms.internal.utils.FeatureFormDialog -import kotlinx.coroutines.launch +import com.arcgismaps.toolkit.popup.PopupStateData +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociations +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState +import com.arcgismaps.toolkit.popup.internal.navigation.NavigationAction +import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute /** * Screen that displays the selected group of associations. * - * @param formStateData The form state data. + * @param popupStateData The form state data. * @param route The [NavigationRoute.UNAssociationsView] route data of this screen. * @param onSave The callback to be invoked when the save button is clicked. The boolean parameter * indicates whether this action should be followed by a forward navigation. The callback should @@ -54,17 +51,17 @@ import kotlinx.coroutines.launch */ @Composable internal fun UNAssociationsScreen( - formStateData: FormStateData, + popupStateData: PopupStateData, route: NavigationRoute.UNAssociationsView, - isNavigationEnabled: Boolean, - onSave: suspend (FeatureForm, Boolean) -> Result, - onDiscard: suspend (Boolean) -> Unit, +// isNavigationEnabled: Boolean, +// onSave: suspend (FeatureForm, Boolean) -> Result, +// onDiscard: suspend (Boolean) -> Unit, onNavigateToFeature: (ArcGISFeature) -> Unit, onNavigateToAssociation: (Int) -> Unit, modifier: Modifier = Modifier ) { - val featureForm = formStateData.featureForm - val states = formStateData.stateCollection + val popup = popupStateData.popup + val states = popupStateData.stateCollection // Get the selected UtilityAssociationsElementState from the state collection val utilityAssociationsElementState = states[route.stateId] as? UtilityAssociationsElementState ?: return @@ -76,8 +73,8 @@ internal fun UNAssociationsScreen( // guard against null values return } - val hasEdits by featureForm.hasEdits.collectAsState() - val scope = rememberCoroutineScope() +// val hasEdits by popup.hasEdits.collectAsState() +// val scope = rememberCoroutineScope() // State to hold the pending navigation action when the form has unsaved edits var pendingNavigationAction: NavigationAction by rememberSaveable { mutableStateOf(NavigationAction.None) @@ -93,15 +90,15 @@ internal fun UNAssociationsScreen( } UtilityAssociations( groupResult = groupResult, - isNavigationEnabled = isNavigationEnabled, + isNavigationEnabled = true, onItemClick = { index -> - if (hasEdits) { - pendingNavigationAction = NavigationAction.NavigateToFeature(index) - } else { +// if (hasEdits) { +// pendingNavigationAction = NavigationAction.NavigateToFeature(index) +// } else { val feature = groupResult.associationResults[index].associatedFeature // Navigate to the next form if there are no edits. onNavigateToFeature(feature) - } +// } }, onDetailsClick = { index -> val association = groupResult.associationResults[index] @@ -112,30 +109,30 @@ internal fun UNAssociationsScreen( .padding(16.dp) .fillMaxSize() ) - if (pendingNavigationAction != NavigationAction.None) { - SaveEditsDialog( - onDismissRequest = { - // Clear the pending navigation action when the dialog is dismissed - pendingNavigationAction = NavigationAction.None - }, - onSave = { - scope.launch { - onSave(featureForm, true).onSuccess { - // If the save is successful, navigate to the association - navigateToFeature(pendingNavigationAction) - } - pendingNavigationAction = NavigationAction.None - } - }, - onDiscard = { - scope.launch { - onDiscard(true) - // Navigate to the association after discarding changes - navigateToFeature(pendingNavigationAction) - pendingNavigationAction = NavigationAction.None - } - } - ) - } - FeatureFormDialog(states) +// if (pendingNavigationAction != NavigationAction.None) { +// SaveEditsDialog( +// onDismissRequest = { +// // Clear the pending navigation action when the dialog is dismissed +// pendingNavigationAction = NavigationAction.None +// }, +// onSave = { +// scope.launch { +// onSave(featureForm, true).onSuccess { +// // If the save is successful, navigate to the association +// navigateToFeature(pendingNavigationAction) +// } +// pendingNavigationAction = NavigationAction.None +// } +// }, +// onDiscard = { +// scope.launch { +// onDiscard(true) +// // Navigate to the association after discarding changes +// navigateToFeature(pendingNavigationAction) +// pendingNavigationAction = NavigationAction.None +// } +// } +// ) +// } +// FeatureFormDialog(states) } diff --git a/toolkit/popup/src/main/res/values/strings.xml b/toolkit/popup/src/main/res/values/strings.xml index 2aacc0aa3..c7398d77d 100644 --- a/toolkit/popup/src/main/res/values/strings.xml +++ b/toolkit/popup/src/main/res/values/strings.xml @@ -40,4 +40,6 @@ Only removes the association. The feature remains. Remove Cancel + None Selected + Association Settings From 3564098099d339d45ec9ed6e4840ac144df638bb Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 10 Sep 2025 10:06:35 -0700 Subject: [PATCH 06/36] clean up code --- .../com/arcgismaps/toolkit/popup/Popup.kt | 257 +----------------- .../arcgismaps/toolkit/popup/PopupState.kt | 4 - .../internal/navigation/FeatureFormNavHost.kt | 19 -- 3 files changed, 3 insertions(+), 277 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 4d41e2fc7..7ebd71ec1 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -19,98 +19,41 @@ package com.arcgismaps.toolkit.popup import android.content.Context -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.ComposeNavigator import androidx.navigation.compose.DialogNavigator import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement -import com.arcgismaps.mapping.popup.AttachmentsPopupElement import com.arcgismaps.mapping.popup.FieldsPopupElement -import com.arcgismaps.mapping.popup.MediaPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupAttachment import com.arcgismaps.mapping.popup.TextPopupElement import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement -import com.arcgismaps.realtime.DynamicEntity -import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsElementState -import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsPopupElement -import com.arcgismaps.toolkit.popup.internal.element.attachment.rememberAttachmentsElementState import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsElementState -import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsPopupElement -//import com.arcgismaps.toolkit.popup.internal.element.fieldselement.rememberFieldsElementState -import com.arcgismaps.toolkit.popup.internal.element.media.MediaElementState -import com.arcgismaps.toolkit.popup.internal.element.media.MediaPopupElement -import com.arcgismaps.toolkit.popup.internal.element.media.rememberMediaElementState import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateCollection import com.arcgismaps.toolkit.popup.internal.element.state.mutablePopupElementStateCollection import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState -import com.arcgismaps.toolkit.popup.internal.element.textelement.TextPopupElement -//import com.arcgismaps.toolkit.popup.internal.element.textelement.rememberTextElementState -import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElement import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.FeatureFormNavHost import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar -import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.FileViewer -import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile -import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult import kotlinx.coroutines.CoroutineScope -//@Immutable -//private data class PopupState(@Stable val popup: Popup) - -@Immutable -public sealed class ValidationErrorVisibility { - - /** - * Indicates that the validation errors are only visible for editable fields that have - * received focus. - */ - public object Automatic : ValidationErrorVisibility() - - /** - * Indicates the validation is run for all the editable fields regardless of their focus state, - * and any errors are shown. - */ - public object Visible : ValidationErrorVisibility() -} - /** * A composable Popup toolkit component that enables users to see Popup content in a * layer that have been configured externally. @@ -134,7 +77,9 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { PopupState(popup, scope) } - Popup(popup, stateData, modifier) + if (stateData.getActivePopupStateData().initialEvaluation.value) { + Popup(popup, stateData, modifier) + } } @Composable @@ -175,8 +120,6 @@ public fun Popup( ContentAwareTopBar( backStackEntry = entry, state = popupState, -// onSaveForm = ::saveForm, -// onDiscardForm = ::discardForm, onDismissRequest = onDismiss, hasBackStack = hasBackStack, showFormActions = showFormActions, @@ -195,11 +138,6 @@ public fun Popup( FeatureFormNavHost( navController = navController, state = popupState, -// isNavigationEnabled = isNavigationEnabled, -// validationErrorVisibility = validationErrorVisibility, -// onSaveForm = ::saveForm, -// onDiscardForm = ::discardForm, -// onBarcodeButtonClick = onBarcodeButtonClick, modifier = Modifier.fillMaxSize() ) }, @@ -213,19 +151,6 @@ public fun Popup( } } -// FeatureFormNavHost( -// navController = navController, -// state = popupState, -//// isNavigationEnabled = isNavigationEnabled, -//// validationErrorVisibility = validationErrorVisibility, -//// onSaveForm = ::saveForm, -//// onDiscardForm = ::discardForm, -//// onBarcodeButtonClick = onBarcodeButtonClick, -// modifier = Modifier.fillMaxSize() -// ) - -// Popup(popupState, modifier) -// Popup(popupState, popupState.initialEvaluation.value, -1, modifier) } @Composable @@ -241,182 +166,6 @@ internal fun PopupLayout( } } -///** -// * Maintain list of attachments outside of SDK -// * https://devtopia.esri.com/runtime/apollo/issues/681 -// */ -//private val attachments: MutableList = mutableListOf() -// -//@Composable -//private fun Popup(popupState: PopupState, modifier: Modifier = Modifier) { -// val popup = popupState.popup -// val dynamicEntity = (popup.geoElement as? DynamicEntity) -// var evaluated by rememberSaveable(popup) { mutableStateOf(false) } -// var fetched by rememberSaveable(popup) { mutableStateOf(false) } -// var lastUpdatedEntityId by rememberSaveable(dynamicEntity) { mutableLongStateOf(dynamicEntity?.id ?: -1) } -// if (dynamicEntity != null) { -// LaunchedEffect(popup) { -// dynamicEntity.dynamicEntityChangedEvent.collect { -// // briefly show the initializing screen so it is clear the entity just pulsed -// // and values may have changed. -// popupState.popup.evaluateExpressions() -// lastUpdatedEntityId = it.receivedObservation?.id ?: -1 -// } -// } -// } -// LaunchedEffect(popup) { -// popupState.popup.evaluateExpressions() -// if (!fetched) { -// val element = popupState.popup.evaluatedElements -// .filterIsInstance() -// .firstOrNull() -// -// // make a copy of the attachments when first fetched. -// attachments.clear() -// element?.fetchAttachments()?.onSuccess { -// attachments.addAll(element.attachments) -// } -// -// fetched = true -// } -// -// evaluated = true -// } -// -// Popup(popupState, evaluated && fetched, lastUpdatedEntityId, modifier) -//} - -//@Composable -//private fun Popup(popupState: PopupState, initialized: Boolean, refreshed: Long, modifier: Modifier = Modifier) { -// val scope = rememberCoroutineScope() -// val popup = popupState.popup -// val viewableFileState = rememberSaveable { mutableStateOf(null) } -// viewableFileState.value?.let { viewableFile -> -// FileViewer(scope, fileState = viewableFile) { -// viewableFileState.value = null -// } -// } -// Column( -// modifier = modifier, -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Text( -// text = popup.title, -// color = MaterialTheme.colorScheme.onBackground, -// modifier = Modifier.padding(horizontal = 15.dp) -// ) -// Spacer( -// modifier = Modifier -// .fillMaxWidth() -// .height(15.dp) -// ) -// InitializingExpressions(modifier = Modifier.fillMaxWidth()) { -// initialized -// } -// HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) -// if (initialized) { -// PopupBody(popupState, refreshed) { -// viewableFileState.value = it -// } -// } -// } -//} -// -///** -// * The body of the Popup composable -// * -// * @param popupState the immutable state object containing the Popup. -// * @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity -// * @param onFileClicked the callback to display an attachment or media image -// */ -//@Composable -//private fun PopupBody( -// popupState: PopupState, -// refreshed: Long, -// onFileClicked: (ViewableFile) -> Unit = {} -//) { -//// val popup = popupState.popup -// val lazyListState = rememberLazyListState() -// val states = popupState.stateCollection -// LazyColumn( -// modifier = Modifier.semantics { contentDescription = "lazy column" }, -// state = lazyListState -// ) { -// states.forEach { entry -> -// val element = entry.popupElement -// when (element) { -// is TextPopupElement -> { -// // a contentType is needed to reuse the TextPopupElement composable inside a LazyColumn -// item(contentType = TextPopupElement::class.java) { -// TextPopupElement( -// entry.state as TextElementState -// ) -// } -// } -// -// is AttachmentsPopupElement -> { -// item(contentType = AttachmentsPopupElement::class.java) { -// AttachmentsPopupElement( -// state = entry.state as AttachmentsElementState, -// onSelectedAttachment = onFileClicked -// ) -// } -// } -// -// is FieldsPopupElement -> { -// item(contentType = FieldsPopupElement::class.java) { -// FieldsPopupElement( -// state = entry.state as FieldsElementState, -// refreshed = refreshed -// ) -// } -// } -// -// is MediaPopupElement -> { -// item(contentType = MediaPopupElement::class.java) { -// MediaPopupElement( -// entry.state as MediaElementState, -// onClickedMedia = onFileClicked -// ) -// } -// } -// -// is UtilityAssociationsPopupElement -> { -// item(contentType = UtilityAssociationsPopupElement::class.java) { -// UtilityAssociationsElement( -// entry.state as UtilityAssociationsElementState, -// onItemClick = { } -// ) -// } -// } -// -// else -> { -// // other popup elements are not created -// } -// } -// } -// } -//} -// -// -//@Composable -//internal fun InitializingExpressions( -// modifier: Modifier = Modifier, -// evaluationProvider: () -> Boolean -//) { -// val alpha by animateFloatAsState( -// if (evaluationProvider()) 0f else 1f, -// label = "evaluation loading alpha" -// ) -// Surface( -// modifier = modifier.graphicsLayer { -// this.alpha = alpha -// } -// ) { -// LinearProgressIndicator(modifier) -// } -//} - /** * Creates and remembers state objects for all the supported element types that are part of the * provided Popup. These state objects are returned as part of a [PopupElementStateCollection]. diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 21a526eca..d45d7344d 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -18,7 +18,6 @@ import com.arcgismaps.mapping.popup.PopupExpressionEvaluation import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateCollection import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute import com.arcgismaps.toolkit.popup.internal.navigation.lifecycleIsResumed -import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch @@ -29,9 +28,6 @@ public class PopupState(@Stable public val popup: Popup) { private lateinit var coroutineScope: CoroutineScope - internal lateinit var stateCollection: PopupElementStateCollection - private set - internal val attachments: MutableList = mutableListOf() diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt index 0c56c7d3f..c9cf19e2b 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt @@ -31,9 +31,6 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.arcgismaps.mapping.featureforms.FeatureForm -import com.arcgismaps.mapping.featureforms.FieldFormElement -import com.arcgismaps.toolkit.popup.ValidationErrorVisibility import com.arcgismaps.toolkit.popup.PopupState import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationDetails import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState @@ -45,11 +42,6 @@ import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen internal fun FeatureFormNavHost( navController: NavHostController, state: PopupState, -// isNavigationEnabled: Boolean, -// validationErrorVisibility: ValidationErrorVisibility, -// onSaveForm: suspend (FeatureForm, Boolean) -> Result, -// onDiscardForm: suspend (Boolean) -> Unit, -// onBarcodeButtonClick: ((FieldFormElement) -> Unit)?, modifier: Modifier = Modifier, ) { NavHost( @@ -78,14 +70,6 @@ internal fun FeatureFormNavHost( // Update the active feature form if we navigate back to this screen from another form. state.updateActivePopup() } -// // launch a new side effect in a launched effect when validationErrorVisibility changes -// // for a given form -// LaunchedEffect(validationErrorVisibility, formData) { -// // if it set to always show errors validate all fields -// if (validationErrorVisibility == ValidationErrorVisibility.Visible) { -// state.validateAllFields() -// } -// } } composable { backStackEntry -> @@ -108,9 +92,6 @@ internal fun FeatureFormNavHost( UNAssociationsScreen( popupStateData = formData, route = route, -// isNavigationEnabled = isNavigationEnabled, -// onSave = onSaveForm, -// onDiscard = onDiscardForm, onNavigateToFeature = { feature -> // Request the state to navigate to the feature. state.navigateTo(backStackEntry, feature) From 14b3590bafd9251324ce6d40a41fcf5d21ac3a66 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 10 Sep 2025 12:52:16 -0700 Subject: [PATCH 07/36] clean up imports and unused code --- .../com/arcgismaps/toolkit/popup/Popup.kt | 4 +- .../internal/navigation/NavigationRoute.kt | 2 +- ...{FeatureFormNavHost.kt => PopupNavHost.kt} | 2 +- .../internal/screens/ContentAwareTopBar.kt | 117 +----------------- .../popup/internal/screens/PopupScreen.kt | 5 +- .../screens/UNAssociationsFilterScreen.kt | 1 - .../internal/screens/UNAssociationsScreen.kt | 68 +--------- 7 files changed, 9 insertions(+), 190 deletions(-) rename toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/{FeatureFormNavHost.kt => PopupNavHost.kt} (99%) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 7ebd71ec1..f61a9f9a5 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -50,7 +50,7 @@ import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateColl import com.arcgismaps.toolkit.popup.internal.element.state.mutablePopupElementStateCollection import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState -import com.arcgismaps.toolkit.popup.internal.navigation.FeatureFormNavHost +import com.arcgismaps.toolkit.popup.internal.navigation.PopupNavHost import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar import kotlinx.coroutines.CoroutineScope @@ -135,7 +135,7 @@ public fun Popup( } }, content = { - FeatureFormNavHost( + PopupNavHost( navController = navController, state = popupState, modifier = Modifier.fillMaxSize() diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt index 83fe33ab6..6976ba5c9 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt @@ -23,7 +23,7 @@ import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen import kotlinx.serialization.Serializable /** - * Navigation routes for the form. + * Navigation routes for the Popup. */ @Serializable internal sealed class NavigationRoute { diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt similarity index 99% rename from toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt rename to toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index c9cf19e2b..8ee39f6e5 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/FeatureFormNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -39,7 +39,7 @@ import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen @Composable -internal fun FeatureFormNavHost( +internal fun PopupNavHost( navController: NavHostController, state: PopupState, modifier: Modifier = Modifier, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt index dd0cbaa5d..ed33873b3 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt @@ -17,31 +17,22 @@ package com.arcgismaps.toolkit.popup.internal.screens import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Button -import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -51,36 +42,27 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.toRoute -import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.toolkit.popup.PopupState import com.arcgismaps.toolkit.popup.PopupStateData import com.arcgismaps.toolkit.popup.R import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.NavigationAction import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch /** * A dynamic action bar that adapts its content based on the current navigation state. * * @param backStackEntry The [NavBackStackEntry] representing the current navigation state. - * @param state The [FeatureFormState] that holds the current form state data. + * @param state The [PopupState] that holds the current form state data. * @param hasBackStack Indicates if there is a previous route in the navigation stack. * @param showFormActions Indicates if the form actions (save, discard) should be shown. * @param showCloseIcon Indicates if the close icon should be displayed. - * @param onSaveForm The callback to invoke when the save button is clicked. It takes the current - * [FeatureForm] and a boolean indicating if the save is followed by a navigation action. - * @param onDiscardForm The callback to invoke when the discard button is clicked. It takes a boolean - * indicating if the discard is followed by a navigation action. * @param onDismissRequest The callback to invoke when the close button is clicked. If the form has * unsaved edits, this in invoked after the save or discard action is completed. * @param modifier The [Modifier] to apply to this layout. @@ -93,8 +75,6 @@ internal fun ContentAwareTopBar( showFormActions: Boolean, showCloseIcon: Boolean, isNavigationEnabled: Boolean, -// onSaveForm: suspend (FeatureForm, Boolean) -> Result, -// onDiscardForm: suspend (Boolean) -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier ) { @@ -163,53 +143,9 @@ internal fun ContentAwareTopBar( onClose = { onNavigationAction(NavigationAction.Dismiss, hasEdits) }, -// onSave = { -// scope.launch { -// onSaveForm(formData.featureForm, false) -// } -// }, -// onDiscard = { -// scope.launch { -// onDiscardForm(false) -// } -// }, modifier = modifier ) -// InitializingExpressions( -// modifier = Modifier.fillMaxWidth(), -// evaluationProvider = { formData.isEvaluatingExpressions.value } -// ) } -// if (pendingNavigationAction != NavigationAction.None) { -// SaveEditsDialog( -// onDismissRequest = { -// // Clear the pending action when the dialog is dismissed -// pendingNavigationAction = NavigationAction.None -// }, -// onSave = { -// scope.launch(Dispatchers.Main) { -// // Check if the pending action is to navigate back, since NavigateToAssociation -// // is not triggered by the top bar -// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack -// onSaveForm(formData.featureForm, willNavigate).onSuccess { -// // Execute the pending navigation action after saving -// onNavigationAction(pendingNavigationAction, false) -// } -// pendingNavigationAction = NavigationAction.None -// } -// }, -// onDiscard = { -// scope.launch(Dispatchers.Main) { -// // Check if the pending action is to navigate back, since NavigateToAssociation -// // is not triggered by the top bar -// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack -// onDiscardForm(willNavigate) -// onNavigationAction(pendingNavigationAction, false) -// pendingNavigationAction = NavigationAction.None -// } -// } -// ) -// } // only enable back navigation if there is a previous route BackHandler(hasBackStack) { onBackAction(backStackEntry) @@ -225,12 +161,6 @@ private fun getTopBarTitleAndSubtitle( mutableStateOf(formData.popup.title) } -// LaunchedEffect(backStackEntry, formData) { -// formData.featureForm.title.collectLatest { -// formTitle = it -// } -// } - val defaultTitle = stringResource(R.string.none_selected) return when { backStackEntry.destination.hasRoute() -> { @@ -288,8 +218,6 @@ private fun getTopBarTitleAndSubtitle( * @param onBackPressed The callback to invoke when the back button is clicked. * @param onClose The callback to invoke when the close button is clicked. If null, the close button * is not displayed. - * @param onSave The callback to invoke when the save button is clicked. - * @param onDiscard The callback to invoke when the discard button is clicked. * @param modifier The [Modifier] to apply to this layout. */ @Composable @@ -302,8 +230,6 @@ private fun FeatureFormTitle( isNavigationEnabled: Boolean, onBackPressed: () -> Unit, onClose: () -> Unit, -// onSave: () -> Unit, -// onDiscard: () -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -354,48 +280,9 @@ private fun FeatureFormTitle( } } } -// AnimatedVisibility(visible = hasEdits) { -// Row( -// modifier = Modifier.padding(top = 12.dp), -// horizontalArrangement = Arrangement.Start, -// verticalAlignment = Alignment.CenterVertically -// ) { -// Button(onClick = onSave) { -// Text( -// text = stringResource(R.string.save), -// style = MaterialTheme.typography.labelLarge -// ) -// } -// Spacer(Modifier.width(8.dp)) -// FilledTonalButton(onClick = onDiscard) { -// Text( -// text = stringResource(R.string.discard), -// style = MaterialTheme.typography.labelLarge -// ) -// } -// } -// } } } -//@Composable -//private fun InitializingExpressions( -// modifier: Modifier = Modifier, -// evaluationProvider: () -> Boolean -//) { -// val alpha by animateFloatAsState( -// if (evaluationProvider()) 1f else 0f, -// label = "evaluation loading alpha" -// ) -// Surface( -// modifier = modifier.graphicsLayer { -// this.alpha = alpha -// } -// ) { -// LinearProgressIndicator(modifier) -// } -//} - @Preview(showBackground = true) @Composable private fun FeatureFormTitlePreview() { @@ -408,8 +295,6 @@ private fun FeatureFormTitlePreview() { isNavigationEnabled = true, onBackPressed = {}, onClose = {}, -// onSave = {}, -// onDiscard = {}, modifier = Modifier.padding(8.dp) ) } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt index 6098be0b1..0b81e0de3 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt @@ -64,7 +64,6 @@ import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile @Composable internal fun PopupScreen( popupState: PopupState, -// initialized: Boolean, popupStateData: PopupStateData, initialized: Boolean, refreshed: Long, @@ -108,19 +107,17 @@ internal fun PopupScreen( /** * The body of the Popup composable * - * @param popupState the immutable state object containing the Popup. + * @param popupStateData the immutable state object containing the Popup. * @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity * @param onFileClicked the callback to display an attachment or media image */ @Composable private fun PopupBody( -// popupState: PopupState, popupStateData: PopupStateData, refreshed: Long, onUtilityAssociationFilterClick: (UtilityAssociationsElementState) -> Unit, onFileClicked: (ViewableFile) -> Unit = {} ) { -// val popup = popupState.popup val lazyListState = rememberLazyListState() val states = popupStateData.stateCollection LazyColumn( diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt index 828f68ec2..d43349b2a 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt @@ -69,5 +69,4 @@ internal fun UNAssociationsFilterScreen( ) } } -// FeatureFormDialog(states) } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt index d4ebb9bba..8fa4b59d4 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -19,20 +19,12 @@ package com.arcgismaps.toolkit.popup.internal.screens import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arcgismaps.data.ArcGISFeature -import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.toolkit.popup.PopupStateData import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociations import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState -import com.arcgismaps.toolkit.popup.internal.navigation.NavigationAction import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute /** @@ -40,11 +32,6 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute * * @param popupStateData The form state data. * @param route The [NavigationRoute.UNAssociationsView] route data of this screen. - * @param onSave The callback to be invoked when the save button is clicked. The boolean parameter - * indicates whether this action should be followed by a forward navigation. The callback should - * return a [Result] that indicates the success or failure of the save operation. - * @param onDiscard The callback to be invoked when the discard button is clicked. The boolean parameter - * indicates whether this action should be followed by a forward navigation. * @param onNavigateToFeature The callback to be invoked when the user selects a feature to navigate to. * @param onNavigateToAssociation The callback to be invoked when the user selects an association to navigate to. * @param modifier The modifier to be applied to the layout. @@ -53,14 +40,10 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute internal fun UNAssociationsScreen( popupStateData: PopupStateData, route: NavigationRoute.UNAssociationsView, -// isNavigationEnabled: Boolean, -// onSave: suspend (FeatureForm, Boolean) -> Result, -// onDiscard: suspend (Boolean) -> Unit, onNavigateToFeature: (ArcGISFeature) -> Unit, onNavigateToAssociation: (Int) -> Unit, modifier: Modifier = Modifier ) { - val popup = popupStateData.popup val states = popupStateData.stateCollection // Get the selected UtilityAssociationsElementState from the state collection val utilityAssociationsElementState = states[route.stateId] as? @@ -73,32 +56,13 @@ internal fun UNAssociationsScreen( // guard against null values return } -// val hasEdits by popup.hasEdits.collectAsState() -// val scope = rememberCoroutineScope() - // State to hold the pending navigation action when the form has unsaved edits - var pendingNavigationAction: NavigationAction by rememberSaveable { - mutableStateOf(NavigationAction.None) - } - // Handler for navigating to a selected associated feature - val navigateToFeature: (NavigationAction) -> Unit = { action -> - if (action is NavigationAction.NavigateToFeature) { - val selectedIndex = action.index - groupResult.associationResults.getOrNull(selectedIndex)?.associatedFeature?.let { feature -> - onNavigateToFeature(feature) - } - } - } UtilityAssociations( groupResult = groupResult, isNavigationEnabled = true, onItemClick = { index -> -// if (hasEdits) { -// pendingNavigationAction = NavigationAction.NavigateToFeature(index) -// } else { - val feature = groupResult.associationResults[index].associatedFeature - // Navigate to the next form if there are no edits. - onNavigateToFeature(feature) -// } + val feature = groupResult.associationResults[index].associatedFeature + // Navigate to the next form if there are no edits. + onNavigateToFeature(feature) }, onDetailsClick = { index -> val association = groupResult.associationResults[index] @@ -109,30 +73,4 @@ internal fun UNAssociationsScreen( .padding(16.dp) .fillMaxSize() ) -// if (pendingNavigationAction != NavigationAction.None) { -// SaveEditsDialog( -// onDismissRequest = { -// // Clear the pending navigation action when the dialog is dismissed -// pendingNavigationAction = NavigationAction.None -// }, -// onSave = { -// scope.launch { -// onSave(featureForm, true).onSuccess { -// // If the save is successful, navigate to the association -// navigateToFeature(pendingNavigationAction) -// } -// pendingNavigationAction = NavigationAction.None -// } -// }, -// onDiscard = { -// scope.launch { -// onDiscard(true) -// // Navigate to the association after discarding changes -// navigateToFeature(pendingNavigationAction) -// pendingNavigationAction = NavigationAction.None -// } -// } -// ) -// } -// FeatureFormDialog(states) } From 09a55c7f327461c60c977d99aa029d269b5872c9 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 10 Sep 2025 13:10:44 -0700 Subject: [PATCH 08/36] WIP --- .../internal/screens/ContentAwareTopBar.kt | 60 +++++++++---------- .../arcgismaps/toolkit/popup/PopupState.kt | 2 +- .../popup/internal/navigation/PopupNavHost.kt | 3 +- .../popup/internal/screens/PopupScreen.kt | 11 +++- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt index c2595e795..691d857ec 100644 --- a/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/featureforms/src/main/java/com/arcgismaps/toolkit/featureforms/internal/screens/ContentAwareTopBar.kt @@ -180,36 +180,36 @@ internal fun ContentAwareTopBar( evaluationProvider = { formData.isEvaluatingExpressions.value } ) } -// if (pendingNavigationAction != NavigationAction.None) { -// SaveEditsDialog( -// onDismissRequest = { -// // Clear the pending action when the dialog is dismissed -// pendingNavigationAction = NavigationAction.None -// }, -// onSave = { -// scope.launch(Dispatchers.Main) { -// // Check if the pending action is to navigate back, since NavigateToAssociation -// // is not triggered by the top bar -// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack -// onSaveForm(formData.featureForm, willNavigate).onSuccess { -// // Execute the pending navigation action after saving -// onNavigationAction(pendingNavigationAction, false) -// } -// pendingNavigationAction = NavigationAction.None -// } -// }, -// onDiscard = { -// scope.launch(Dispatchers.Main) { -// // Check if the pending action is to navigate back, since NavigateToAssociation -// // is not triggered by the top bar -// val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack -// onDiscardForm(willNavigate) -// onNavigationAction(pendingNavigationAction, false) -// pendingNavigationAction = NavigationAction.None -// } -// } -// ) -// } + if (pendingNavigationAction != NavigationAction.None) { + SaveEditsDialog( + onDismissRequest = { + // Clear the pending action when the dialog is dismissed + pendingNavigationAction = NavigationAction.None + }, + onSave = { + scope.launch(Dispatchers.Main) { + // Check if the pending action is to navigate back, since NavigateToAssociation + // is not triggered by the top bar + val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack + onSaveForm(formData.featureForm, willNavigate).onSuccess { + // Execute the pending navigation action after saving + onNavigationAction(pendingNavigationAction, false) + } + pendingNavigationAction = NavigationAction.None + } + }, + onDiscard = { + scope.launch(Dispatchers.Main) { + // Check if the pending action is to navigate back, since NavigateToAssociation + // is not triggered by the top bar + val willNavigate = pendingNavigationAction == NavigationAction.NavigateBack + onDiscardForm(willNavigate) + onNavigationAction(pendingNavigationAction, false) + pendingNavigationAction = NavigationAction.None + } + } + ) + } // only enable back navigation if there is a previous route BackHandler(hasBackStack) { onBackAction(backStackEntry) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index d45d7344d..9b45258fc 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -257,4 +257,4 @@ internal fun ArcGISFeature.toPopup(): Popup { else -> null } return Popup(this, popupDefinition) -} \ No newline at end of file +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 8ee39f6e5..79af971f1 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -58,7 +58,8 @@ internal fun PopupNavHost( PopupScreen( state, popupStateData, - popupStateData.initialEvaluation.value, -1, + popupStateData.initialEvaluation.value, + -1, onUtilityFilterSelected = { state -> val newRoute = NavigationRoute.UNFilterView(stateId = state.id) // Navigate to the filter view diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt index 0b81e0de3..395bca747 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt @@ -60,7 +60,16 @@ import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement. import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.FileViewer import com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ViewableFile - +/** + * Composable function that displays the Popup screen. + * + * @param popupState The popup state object containing the Popup. + * @param popupStateData The popup state data. + * @param initialized indicates whether the popup has been initialized. + * @param refreshed indicates that a new evaluation of elements has occurred. Only for DynamicEntity + * @param onUtilityFilterSelected The callback to be invoked when a utility filter is selected. + * @param modifier The modifier to be applied to the layout. + */ @Composable internal fun PopupScreen( popupState: PopupState, From bf29a40e081d86f234d2e7e2e7370a0692169728 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 10 Sep 2025 15:17:54 -0700 Subject: [PATCH 09/36] remove duplicate title from old popup remove support for hasEdits from contentAwareTopbar --- .../com/arcgismaps/toolkit/popup/Popup.kt | 4 +- .../internal/screens/ContentAwareTopBar.kt | 56 +++++-------------- .../popup/internal/screens/PopupScreen.kt | 11 ---- 3 files changed, 15 insertions(+), 56 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index f61a9f9a5..bb0f36b1b 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -78,7 +78,7 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { } if (stateData.getActivePopupStateData().initialEvaluation.value) { - Popup(popup, stateData, modifier) + Popup(popup, stateData, modifier, showCloseIcon = false) } } @@ -89,7 +89,6 @@ public fun Popup( modifier: Modifier = Modifier, onDismiss: () -> Unit = {}, showCloseIcon: Boolean = true, - showFormActions: Boolean = true, isNavigationEnabled : Boolean = true, ) { // Add the provided state collection to the store. @@ -122,7 +121,6 @@ public fun Popup( state = popupState, onDismissRequest = onDismiss, hasBackStack = hasBackStack, - showFormActions = showFormActions, showCloseIcon = showCloseIcon, isNavigationEnabled = isNavigationEnabled, modifier = Modifier diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt index ed33873b3..e3ba2a88c 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt @@ -17,14 +17,10 @@ package com.arcgismaps.toolkit.popup.internal.screens import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Close @@ -41,7 +37,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -61,7 +56,6 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute * @param backStackEntry The [NavBackStackEntry] representing the current navigation state. * @param state The [PopupState] that holds the current form state data. * @param hasBackStack Indicates if there is a previous route in the navigation stack. - * @param showFormActions Indicates if the form actions (save, discard) should be shown. * @param showCloseIcon Indicates if the close icon should be displayed. * @param onDismissRequest The callback to invoke when the close button is clicked. If the form has * unsaved edits, this in invoked after the save or discard action is completed. @@ -72,7 +66,6 @@ internal fun ContentAwareTopBar( backStackEntry: NavBackStackEntry, state: PopupState, hasBackStack: Boolean, - showFormActions: Boolean, showCloseIcon: Boolean, isNavigationEnabled: Boolean, onDismissRequest: () -> Unit, @@ -81,36 +74,31 @@ internal fun ContentAwareTopBar( val formData = remember(backStackEntry) { state.getActivePopupStateData() } val scope = rememberCoroutineScope() // val hasEdits by formData.featureForm.hasEdits.collectAsState() - val hasEdits = false +// val hasEdits = false // State to hold the pending navigation action when the form has unsaved edits var pendingNavigationAction: NavigationAction by rememberSaveable { mutableStateOf(NavigationAction.None) } // Callback to handle navigation actions based on the form's edit state - val onNavigationAction: (NavigationAction, Boolean) -> Unit = { action, formHasEdits -> - if (formHasEdits) { - // If the form has edits, store the pending action - pendingNavigationAction = action - } else { - // Otherwise, execute the action immediately - when (action) { - is NavigationAction.NavigateBack -> { - state.popBackStack(backStackEntry) - } - - is NavigationAction.Dismiss -> { - onDismissRequest() - } + val onNavigationAction: (NavigationAction) -> Unit = { action -> + // execute the action immediately + when (action) { + is NavigationAction.NavigateBack -> { + state.popBackStack(backStackEntry) + } - else -> {} + is NavigationAction.Dismiss -> { + onDismissRequest() } + + else -> {} } } val onBackAction: (NavBackStackEntry) -> Unit = { entry -> when { entry.destination.hasRoute() -> { // Run the navigation action if the current view is the form view - onNavigationAction(NavigationAction.NavigateBack, hasEdits) + onNavigationAction(NavigationAction.NavigateBack) } else -> { @@ -133,7 +121,6 @@ internal fun ContentAwareTopBar( FeatureFormTitle( title = title, subTitle = subTitle, - hasEdits = if (showFormActions) hasEdits else false, showCloseIcon = showCloseIcon, showBackIcon = hasBackStack, isNavigationEnabled = navigationEnabled, @@ -141,7 +128,7 @@ internal fun ContentAwareTopBar( onBackAction(backStackEntry) }, onClose = { - onNavigationAction(NavigationAction.Dismiss, hasEdits) + onNavigationAction(NavigationAction.Dismiss) }, modifier = modifier ) @@ -166,7 +153,7 @@ private fun getTopBarTitleAndSubtitle( backStackEntry.destination.hasRoute() -> { Pair( formTitle, - "no description available" //formData.featureForm.description.value + "" // No subtitle for the main Popup view ) } @@ -211,8 +198,6 @@ private fun getTopBarTitleAndSubtitle( * * @param title The title to display. * @param subTitle The subtitle to display. - * @param hasEdits Indicates if the form has unsaved edits. An unsaved edits indicator is displayed - * along with the save and discard buttons if this is true. * @param showCloseIcon Indicates if the close icon should be displayed. * @param showBackIcon Indicates if the back icon should be displayed. * @param onBackPressed The callback to invoke when the back button is clicked. @@ -224,7 +209,6 @@ private fun getTopBarTitleAndSubtitle( private fun FeatureFormTitle( title: String, subTitle: String, - hasEdits: Boolean, showCloseIcon: Boolean, showBackIcon: Boolean, isNavigationEnabled: Boolean, @@ -255,23 +239,12 @@ private fun FeatureFormTitle( style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f, fill = false) ) - if (hasEdits) { - Spacer(Modifier.width(8.dp)) - Canvas(modifier = Modifier.size(10.dp)) { - drawCircle(color = Color(0xFFB3261E)) - } - } } if (subTitle.isNotEmpty()) { Text( text = subTitle, style = MaterialTheme.typography.bodyMedium, ) - } else if (hasEdits) { - Text( - text = "Unsaved Changes", - style = MaterialTheme.typography.bodyMedium, - ) } } if (showCloseIcon) { @@ -289,7 +262,6 @@ private fun FeatureFormTitlePreview() { FeatureFormTitle( title = "Structure Boundary", subTitle = "Edit feature attributes", - hasEdits = true, showCloseIcon = true, showBackIcon = false, isNavigationEnabled = true, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt index 395bca747..8367171fc 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt @@ -91,20 +91,9 @@ internal fun PopupScreen( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = popup.title, - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.padding(horizontal = 15.dp) - ) - Spacer( - modifier = Modifier - .fillMaxWidth() - .height(15.dp) - ) InitializingExpressions(modifier = Modifier.fillMaxWidth()) { initialized } - HorizontalDivider(modifier = Modifier.fillMaxWidth(), thickness = 2.dp) if (initialized) { PopupBody(popupStateData, refreshed, onUtilityFilterSelected) { viewableFileState.value = it From bc0bed4d62937e1196930a597978409312671496 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Thu, 11 Sep 2025 10:14:53 -0700 Subject: [PATCH 10/36] move popupelementstates creation to PopupState replace form references with popup remove caching attachments in popupstate --- .../com/arcgismaps/toolkit/popup/Popup.kt | 65 --------- .../arcgismaps/toolkit/popup/PopupState.kt | 119 +++++++++++++---- .../attachment/AttachmentsElementState.kt | 33 ----- .../fieldselement/FieldsElementState.kt | 15 --- .../element/media/MediaElementDefaults.kt | 29 ++++ .../element/media/MediaElementState.kt | 125 ++++-------------- .../element/media/MediaPopupElement.kt | 31 ++++- .../element/textelement/TextElementState.kt | 11 -- .../UtilityAssociationsElementState.kt | 4 +- .../internal/navigation/NavigationAction.kt | 2 +- .../internal/navigation/NavigationRoute.kt | 4 +- .../popup/internal/navigation/PopupNavHost.kt | 16 +-- .../internal/screens/ContentAwareTopBar.kt | 53 +++----- .../screens/UNAssociationsFilterScreen.kt | 6 +- .../internal/screens/UNAssociationsScreen.kt | 4 +- 15 files changed, 210 insertions(+), 307 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index bb0f36b1b..05fc5621f 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -164,71 +164,6 @@ internal fun PopupLayout( } } -/** - * Creates and remembers state objects for all the supported element types that are part of the - * provided Popup. These state objects are returned as part of a [PopupElementStateCollection]. - * - * @param popup the [Popup] to create the states for. - * @return returns the [PopupElementStateCollection] created. - */ -//@Composable -internal fun rememberStates( - popup: Popup, - attachments: List, - coroutineScope: CoroutineScope -): PopupElementStateCollection { - val states = mutablePopupElementStateCollection() - popup.evaluatedElements.forEach { element -> - when (element) { - is TextPopupElement -> { - states.add( - element, - TextElementState(element = element, popup = popup) - ) - } - -// is AttachmentsPopupElement -> { -// states.add( -// element, -// rememberAttachmentsElementState( -// popup = popup, -// element = element, -// attachments = attachments -// ) -// ) -// } - - is FieldsPopupElement -> { - states.add( - element, - FieldsElementState(element = element, popup = popup) - ) - } - -// is MediaPopupElement -> { -// states.add( -// element, -// rememberMediaElementState(element = element, popup = popup) -// ) -// } - - is UtilityAssociationsPopupElement -> { - states.add( - element, - UtilityAssociationsElementState(element, coroutineScope) - ) - } - - else -> { - // TODO remove for release - println("encountered element of type ${element::class.java}") - } - } - } - - return states -} - @Composable internal fun rememberNavController(vararg inputs: Any): NavHostController { val context = LocalContext.current diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 9b45258fc..1c736dd0c 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -10,12 +10,20 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination.Companion.hasRoute import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.data.ArcGISFeatureTable -import com.arcgismaps.mapping.featureforms.FeatureForm import com.arcgismaps.mapping.popup.AttachmentsPopupElement +import com.arcgismaps.mapping.popup.FieldsPopupElement +import com.arcgismaps.mapping.popup.MediaPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupAttachment -import com.arcgismaps.mapping.popup.PopupExpressionEvaluation +import com.arcgismaps.mapping.popup.TextPopupElement +import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement +import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsElementState +import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsElementState +import com.arcgismaps.toolkit.popup.internal.element.media.MediaElementState import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateCollection +import com.arcgismaps.toolkit.popup.internal.element.state.mutablePopupElementStateCollection +import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute import com.arcgismaps.toolkit.popup.internal.navigation.lifecycleIsResumed import kotlinx.coroutines.CoroutineScope @@ -28,9 +36,6 @@ public class PopupState(@Stable public val popup: Popup) { private lateinit var coroutineScope: CoroutineScope - internal val attachments: MutableList = mutableListOf() - - /** * A navigation callback that is called when navigating to a new [Popup]. This should * be set by the composition that uses the NavController to the correct [NavigationRoute]. @@ -62,8 +67,8 @@ public class PopupState(@Stable public val popup: Popup) { this.coroutineScope = scope val popupStateData = PopupStateData(popup) coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - popupStateData.evaluateExpressions() - val states = rememberStates( + val attachments = popupStateData.evaluateExpressionsAndGetAttachments() + val states = createStates( popup = popup, attachments = attachments, coroutineScope = coroutineScope @@ -96,26 +101,21 @@ public class PopupState(@Stable public val popup: Popup) { * called after navigating to a new popup or popping the current popup from the stack. * */ - internal suspend fun updateActivePopup() { + internal fun updateActivePopup() { val popupStateData = getActivePopupStateData() - // Check if the active feature form is different from the current form. + // Check if the active popup is different from the current popup. if (_activePopup.value != popupStateData.popup) { _activePopup.value = popupStateData.popup -// // refresh the feature to ensure the latest data is loaded. -// formStateData.featureForm.feature.refresh() -// if (formStateData.initialEvaluation.value.not()) { -// formStateData.evaluateExpressions() -// } } } /** - * Adds a new [FeatureForm] to the local stack and navigates to it. [updateActiveFeatureForm] - * must be called after this to update the [activeFeatureForm], preferably after the navigation + * Adds a new [Popup] to the local stack and navigates to it. [updateActivePopup] + * must be called after this to update the [activePopup], preferably after the navigation * is complete. * * @param backStackEntry the [NavBackStackEntry] of the current destination. - * @param feature the [ArcGISFeature] to create the [FeatureForm] for. + * @param feature the [ArcGISFeature] to create the [Popup] for. */ @MainThread internal fun navigateTo(backStackEntry: NavBackStackEntry, feature: ArcGISFeature): Boolean { @@ -125,8 +125,8 @@ public class PopupState(@Stable public val popup: Popup) { val popup = feature.toPopup() val popupStateData = PopupStateData(popup) coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { - popupStateData.evaluateExpressions() - val states = rememberStates( + val attachments = popupStateData.evaluateExpressionsAndGetAttachments() + val states = createStates( popup = popup, attachments = attachments, coroutineScope = coroutineScope @@ -166,7 +166,7 @@ public class PopupState(@Stable public val popup: Popup) { } else { // Remove the current popup from the stack. store.removeLast() - // Navigate back to the popup view after popping the current form. + // Navigate back to the popup view after popping the current popup. navigate() } } @@ -216,32 +216,95 @@ internal data class PopupStateData( } /** - * Evaluates the expressions for the [popup] and returns the result. After a successful + * Evaluates the expressions for the [popup] and returns all the attachments. After a successful * evaluation, the [initialEvaluation] is set to true. While this function is running, the * [isEvaluatingExpressions] will be true. */ - internal suspend fun evaluateExpressions() : Result> { + internal suspend fun evaluateExpressionsAndGetAttachments() : List { try { isEvaluatingExpressions.value = true - return popup.evaluateExpressions().onSuccess { + val attachments = mutableListOf() + popup.evaluateExpressions().onSuccess { val element = popup.evaluatedElements .filterIsInstance() .firstOrNull() - // make a copy of the attachments when first fetched. -// attachments.clear() -// element?.fetchAttachments()?.onSuccess { -// attachments.addAll(element.attachments) -// } + element?.fetchAttachments()?.onSuccess { + attachments.addAll(element.attachments) + } // Set the initial evaluation to true after the first successful evaluation. initialEvaluation.value = true } + return attachments } finally { isEvaluatingExpressions.value = false } } } +/** + * Creates state objects for all the supported element types that are part of the + * provided Popup. These state objects are returned as part of a [PopupElementStateCollection]. + * + * @param popup the [Popup] to create the states for. + * @return returns the [PopupElementStateCollection] created. + */ +internal fun createStates( + popup: Popup, + attachments: List, + coroutineScope: CoroutineScope +): PopupElementStateCollection { + val states = mutablePopupElementStateCollection() + popup.evaluatedElements.forEach { element -> + when (element) { + is TextPopupElement -> { + states.add( + element, + TextElementState(element = element, popup = popup) + ) + } + + is AttachmentsPopupElement -> { + states.add( + element, + AttachmentsElementState( + attachmentPopupElement = element, + attachments = attachments + ) + ) + } + + is FieldsPopupElement -> { + states.add( + element, + FieldsElementState(element = element, popup = popup) + ) + } + + is MediaPopupElement -> { + states.add( + element, + MediaElementState(element = element, popup = popup) + ) + } + + is UtilityAssociationsPopupElement -> { + states.add( + element, + UtilityAssociationsElementState(element, coroutineScope) + ) + } + + else -> { + // TODO remove for release + println("encountered element of type ${element::class.java}") + } + } + } + + return states +} + internal fun ArcGISFeature.toPopup(): Popup { val popupDefinition = when { this.featureTable?.popupDefinition != null -> { diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt index bacffb02e..099e77dbc 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt @@ -55,39 +55,6 @@ internal class AttachmentsElementState( title = attachmentPopupElement.title, attachments = attachments.map { PopupAttachmentState(it) } ) - - companion object { - fun Saver( - element: AttachmentsPopupElement, - attachments: List - ): Saver = Saver( - save = { null }, - restore = { - AttachmentsElementState( - element, - attachments - ) - } - ) - - } -} - -@Composable -internal fun rememberAttachmentsElementState( - element: AttachmentsPopupElement, - popup: Popup, - attachments: List -): AttachmentsElementState { - return rememberSaveable( - inputs = arrayOf(popup, element), - saver = AttachmentsElementState.Saver(element, attachments) - ) { - AttachmentsElementState( - element, - attachments - ) - } } /** diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt index 16396670a..e64b1fd9b 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt @@ -47,18 +47,3 @@ internal class FieldsElementState( id = createId() ) } - -//internal fun createFieldsElementState( -// element: FieldsPopupElement, -// popup: Popup -//): FieldsElementState { -// val fieldNames = element.fields.map { it.label } -// val fieldsToFormattedValuesMap = fieldNames.zip(element.formattedValues).toMap() -// return FieldsElementState( -// title = element.title, -// description = element.description, -// fieldsToFormattedValuesMap, -// id = PopupElementState.createId() -// ) -//} - diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementDefaults.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementDefaults.kt index 1e58611a9..bf98d8931 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementDefaults.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementDefaults.kt @@ -16,12 +16,17 @@ package com.arcgismaps.toolkit.popup.internal.element.media +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.arcgismaps.mapping.ChartImageParameters +import com.arcgismaps.mapping.ChartImageStyle /** * A central place for theming values. To be promoted to a public theme type. @@ -50,6 +55,30 @@ internal object MediaElementDefaults { tileTextBackgroundColor = MaterialTheme.colorScheme.onBackground, tileTextColor = MaterialTheme.colorScheme.background ) + + val tileWidthForLocalDensity: Int + @Composable + get() = with(LocalDensity.current) { shapes().tileWidth.roundToPx() } + + val tileHeightForLocalDensity: Int + @Composable + get() = with(LocalDensity.current) { shapes().tileHeight.roundToPx() } + + val chartParams: ChartImageParameters + @Composable + get() = ChartImageParameters(tileWidthForLocalDensity, tileHeightForLocalDensity).apply { + style = if (isSystemInDarkTheme()) { + ChartImageStyle.Dark + } else { + ChartImageStyle.Light + } + this.screenScale = LocalDensity.current.density + } + + val mediaFolder: String + @Composable + get() = "${LocalContext.current.cacheDir.canonicalPath}/popup_media" + } internal data class MediaElementShapes( diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt index 15c6ae438..375906030 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt @@ -18,23 +18,13 @@ package com.arcgismaps.toolkit.popup.internal.element.media import android.content.Context import android.graphics.drawable.BitmapDrawable -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import coil.imageLoader import coil.request.ImageRequest import com.arcgismaps.mapping.ChartImageParameters -import com.arcgismaps.mapping.ChartImageStyle import com.arcgismaps.mapping.popup.MediaPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupMedia @@ -55,23 +45,31 @@ import java.util.UUID */ @Immutable internal class MediaElementState( - val description: String, - val title: String, - val media: List, + val element: MediaPopupElement, + val popup: Popup, override val id : Int = createId() ) : PopupElementState() { - constructor( - mediaPopupElement: MediaPopupElement, + lateinit var description: String + lateinit var title: String + lateinit var media: List + + /** + * Indicates if the evaluateExpression function for the [popup] has been run. + */ + internal var isInitialized : MutableState = mutableStateOf(false) + private set + + fun init( scope: CoroutineScope, mediaFolder: String, chartParams: ChartImageParameters, context: Context, models: List = listOf() - ) : this( - description = mediaPopupElement.description, - title = mediaPopupElement.title, - media = mediaPopupElement.media.mapIndexed { index, media -> + ) { + this.description = element.description + this.title = element.title + this.media = element.media.mapIndexed { index, media -> val model = models.getOrNull(index) ?: "" if (media.type.isChart) { PopupMediaState.createChartMediaState(media, model, scope, mediaFolder, chartParams) @@ -79,7 +77,16 @@ internal class MediaElementState( PopupMediaState.createImageMediaState(media, model, scope, mediaFolder, context) } } - ) + + val geoElement = this.popup.geoElement + if (geoElement is DynamicEntity) { + // For dynamic entities + // update chart providers to use the new instances of PopupMedia to reacquire updated charts. + updateMediaElement(element, scope) + } + isInitialized.value = true + } + /** * Update the PopupMedia so that a new chart image can be acquired. Only necessary @@ -95,84 +102,6 @@ internal class MediaElementState( } } } - - companion object { - internal fun Saver( - element: MediaPopupElement, - scope: CoroutineScope, - chartFolder: String, - chartParams: ChartImageParameters, - context: Context - ): Saver = listSaver( - save = { it.media.map { media -> media.imageUri.value } }, - restore = { - MediaElementState( - element, scope, chartFolder, chartParams, context - ) - } - ) - - } -} - -/** - * Creates a state object for a PopupMediaElement. - * - * @param element a MediaPopupElement - * @param popup the Popup which contains the element - */ -@Composable -internal fun rememberMediaElementState( - element: MediaPopupElement, - popup: Popup -): MediaElementState { - val scope = rememberCoroutineScope() - val darkMode = isSystemInDarkTheme() - val defaults = MediaElementDefaults.shapes() - val localDensity = LocalDensity.current - val width = with(localDensity) { - defaults.tileWidth.roundToPx() - } - val height = with(localDensity) { - defaults.tileHeight.roundToPx() - } - - // The chart image parameters are created here so they can use - // composition locals to access screen density, dark theme, etc. - val chartParams = remember(isSystemInDarkTheme()) { - ChartImageParameters(width, height).apply { - style = if (darkMode) { - ChartImageStyle.Dark - } else { - ChartImageStyle.Light - } - - this.screenScale = localDensity.density - } - } - // the composition local context provides the cacheDir to be ultimately passed into - // the chart provider so charts can be saved to disk. - val mediaFolder = "${LocalContext.current.cacheDir.canonicalPath}/popup_media" - val context = LocalContext.current - return rememberSaveable( - inputs = arrayOf(popup, element.toJson()), - saver = MediaElementState.Saver(element, scope, mediaFolder, chartParams, context) - ) { - MediaElementState( - element, - scope, - mediaFolder, - chartParams, - context - ) - }.apply { - val geoElement = popup.geoElement - if (geoElement is DynamicEntity) { - // For dynamic entities - // update chart providers to use the new instances of PopupMedia to reacquire updated charts. - updateMediaElement(element, scope) - } - } } /** diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt index 728df9353..12654f42a 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt @@ -25,7 +25,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -39,13 +42,27 @@ internal fun MediaPopupElement( state: MediaElementState, onClickedMedia: (ViewableFile) -> Unit ) { - MediaPopupElement( - title = state.title, - description = state.description, - stateId = state.id, - media = state.media, - onClickedMedia = onClickedMedia - ) + val scope = rememberCoroutineScope() + val mediaFolder = MediaElementDefaults.mediaFolder + val chartParams = MediaElementDefaults.chartParams + val context = LocalContext.current + LaunchedEffect(state) { + state.init( + scope = scope, + mediaFolder = mediaFolder, + chartParams = chartParams, + context = context + ) + } + if (state.isInitialized.value) { + MediaPopupElement( + title = state.title, + description = state.description, + stateId = state.id, + media = state.media, + onClickedMedia = onClickedMedia + ) + } } @Composable diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt index 466eb430d..400d969f6 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt @@ -42,14 +42,3 @@ internal class TextElementState( id = createId() ) } - -//internal fun createTextElementState( -// element: TextPopupElement, -// popup: Popup -//): TextElementState { -// return TextElementState( -// value = element.text, -// id = PopupElementState.createId() -// ) -//} - diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt index e505dd475..55a70ac44 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt @@ -18,20 +18,18 @@ package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState import com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult import com.arcgismaps.utilitynetworks.UtilityAssociationResult import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch /** * State holder for the [UtilityAssociationsElement]. * - * @param element The [UtilityAssociationsFormElement] to represent. + * @param element The [UtilityAssociationsPopupElement] to represent. * @param scope The [CoroutineScope] to launch coroutines from. */ internal class UtilityAssociationsElementState( diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt index c1d705914..b6d4b58f8 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt @@ -38,7 +38,7 @@ internal sealed class NavigationAction(val value: Int) : Parcelable { data object NavigateBack : NavigationAction(1) /** - * Indicates an action to dismiss the form. + * Indicates an action to dismiss the popup. */ @Parcelize data object Dismiss : NavigationAction(2) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt index 6976ba5c9..022e14116 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt @@ -17,7 +17,7 @@ package com.arcgismaps.toolkit.popup.internal.navigation import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState -//import com.arcgismaps.toolkit.featureforms.internal.screens.FeatureFormScreen +import com.arcgismaps.toolkit.popup.internal.screens.PopupScreen import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen import kotlinx.serialization.Serializable @@ -29,7 +29,7 @@ import kotlinx.serialization.Serializable internal sealed class NavigationRoute { /** - * Represents the [com.arcgismaps.toolkit.popup.internal.screens.PopupScreen]. + * Represents the [PopupScreen]. */ @Serializable data object PopupView : NavigationRoute() diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 79af971f1..0493a3ea4 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -68,16 +68,16 @@ internal fun PopupNavHost( modifier ) LaunchedEffect(popupStateData) { - // Update the active feature form if we navigate back to this screen from another form. + // Update the active popup if we navigate back to this screen from another popup. state.updateActivePopup() } } composable { backStackEntry -> val route = backStackEntry.toRoute() - val formData = remember(backStackEntry) { state.getActivePopupStateData() } + val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } UNAssociationsFilterScreen( - popupStateData = formData, + popupStateData = popupStateData, route = route, onGroupSelected = { stateId -> val newRoute = NavigationRoute.UNAssociationsView(stateId = stateId) @@ -89,9 +89,9 @@ internal fun PopupNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() - val formData = remember(backStackEntry) { state.getActivePopupStateData() } + val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } UNAssociationsScreen( - popupStateData = formData, + popupStateData = popupStateData, route = route, onNavigateToFeature = { feature -> // Request the state to navigate to the feature. @@ -104,7 +104,7 @@ internal fun PopupNavHost( }, modifier = Modifier.fillMaxSize() ) - LaunchedEffect(formData) { + LaunchedEffect(popupStateData) { // Update the active popup when we navigate back to this screen from another // popup. state.updateActivePopup() @@ -113,9 +113,9 @@ internal fun PopupNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() - val formData = remember(backStackEntry) { state.getActivePopupStateData() } + val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } // Get the selected UtilityAssociationsElementState from the state collection - val utilityAssociationsElementState = formData.stateCollection[route.stateId] + val utilityAssociationsElementState = popupStateData.stateCollection[route.stateId] // guard against null value as? UtilityAssociationsElementState ?: return@composable // Display the association details diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt index e3ba2a88c..99e7a44e8 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt @@ -32,8 +32,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -54,11 +52,10 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute * A dynamic action bar that adapts its content based on the current navigation state. * * @param backStackEntry The [NavBackStackEntry] representing the current navigation state. - * @param state The [PopupState] that holds the current form state data. + * @param state The [PopupState] that holds the current popup state data. * @param hasBackStack Indicates if there is a previous route in the navigation stack. * @param showCloseIcon Indicates if the close icon should be displayed. - * @param onDismissRequest The callback to invoke when the close button is clicked. If the form has - * unsaved edits, this in invoked after the save or discard action is completed. + * @param onDismissRequest The callback to invoke when the close button is clicked. * @param modifier The [Modifier] to apply to this layout. */ @Composable @@ -71,15 +68,9 @@ internal fun ContentAwareTopBar( onDismissRequest: () -> Unit, modifier: Modifier = Modifier ) { - val formData = remember(backStackEntry) { state.getActivePopupStateData() } - val scope = rememberCoroutineScope() -// val hasEdits by formData.featureForm.hasEdits.collectAsState() -// val hasEdits = false - // State to hold the pending navigation action when the form has unsaved edits - var pendingNavigationAction: NavigationAction by rememberSaveable { - mutableStateOf(NavigationAction.None) - } - // Callback to handle navigation actions based on the form's edit state + val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } + + // Callback to handle navigation actions val onNavigationAction: (NavigationAction) -> Unit = { action -> // execute the action immediately when (action) { @@ -97,20 +88,20 @@ internal fun ContentAwareTopBar( val onBackAction: (NavBackStackEntry) -> Unit = { entry -> when { entry.destination.hasRoute() -> { - // Run the navigation action if the current view is the form view + // Run the navigation action if the current view is the popup view onNavigationAction(NavigationAction.NavigateBack) } else -> { - // Pop the back stack if the current view is not the form view + // Pop the back stack if the current view is not the popup view state.popBackStack(backStackEntry) } } } // Get the title and subtitle for the top bar based on the current navigation state - val (title, subTitle) = getTopBarTitleAndSubtitle(backStackEntry, formData) + val (title, subTitle) = getTopBarTitleAndSubtitle(backStackEntry, popupStateData) val navigationEnabled = when { - // If the current destination is the form view, only then check if navigation is enabled + // If the current destination is the popup view, only then check if navigation is enabled backStackEntry.destination.hasRoute() -> { isNavigationEnabled } @@ -118,7 +109,7 @@ internal fun ContentAwareTopBar( else -> true } Column { - FeatureFormTitle( + PopupTitle( title = title, subTitle = subTitle, showCloseIcon = showCloseIcon, @@ -142,17 +133,17 @@ internal fun ContentAwareTopBar( @Composable private fun getTopBarTitleAndSubtitle( backStackEntry: NavBackStackEntry, - formData: PopupStateData, + popupStateData: PopupStateData, ): Pair { - var formTitle by remember(backStackEntry, formData) { - mutableStateOf(formData.popup.title) + var popupTitle by remember(backStackEntry, popupStateData) { + mutableStateOf(popupStateData.popup.title) } val defaultTitle = stringResource(R.string.none_selected) return when { backStackEntry.destination.hasRoute() -> { Pair( - formTitle, + popupTitle, "" // No subtitle for the main Popup view ) } @@ -160,19 +151,19 @@ private fun getTopBarTitleAndSubtitle( backStackEntry.destination.hasRoute() -> { var title = defaultTitle val route = backStackEntry.toRoute() - (formData.stateCollection[route.stateId] as? UtilityAssociationsElementState)?.let { state -> + (popupStateData.stateCollection[route.stateId] as? UtilityAssociationsElementState)?.let { state -> state.selectedFilterResult?.filter?.let { filter -> title = filter.title } } - Pair(title, formTitle) + Pair(title, popupTitle) } backStackEntry.destination.hasRoute() -> { var title = defaultTitle var subTitle = defaultTitle val route = backStackEntry.toRoute() - (formData.stateCollection[route.stateId] as? UtilityAssociationsElementState)?.let { state -> + (popupStateData.stateCollection[route.stateId] as? UtilityAssociationsElementState)?.let { state -> state.selectedGroupResult?.let { group -> title = group.name } @@ -194,7 +185,7 @@ private fun getTopBarTitleAndSubtitle( } /** - * Represents the title bar of the form. + * Represents the title bar of the popup. * * @param title The title to display. * @param subTitle The subtitle to display. @@ -206,7 +197,7 @@ private fun getTopBarTitleAndSubtitle( * @param modifier The [Modifier] to apply to this layout. */ @Composable -private fun FeatureFormTitle( +private fun PopupTitle( title: String, subTitle: String, showCloseIcon: Boolean, @@ -249,7 +240,7 @@ private fun FeatureFormTitle( } if (showCloseIcon) { IconButton(onClick = onClose) { - Icon(imageVector = Icons.Default.Close, contentDescription = "close form") + Icon(imageVector = Icons.Default.Close, contentDescription = "close popup") } } } @@ -258,8 +249,8 @@ private fun FeatureFormTitle( @Preview(showBackground = true) @Composable -private fun FeatureFormTitlePreview() { - FeatureFormTitle( +private fun PopupTitlePreview() { + PopupTitle( title = "Structure Boundary", subTitle = "Edit feature attributes", showCloseIcon = true, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt index d43349b2a..e1f9a3148 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt @@ -23,16 +23,16 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.arcgismaps.mapping.featureforms.UtilityAssociationsFormElement +import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationFilter import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute import com.arcgismaps.toolkit.popup.PopupStateData /** - * Screen that displays the selected filter for a [UtilityAssociationsFormElement]. + * Screen that displays the selected filter for a [UtilityAssociationsPopupElement]. * - * @param popupStateData The form state data. + * @param popupStateData The popup state data. * @param route The [NavigationRoute.UNFilterView] route data of this screen. * @param onGroupSelected The callback that is invoked when a group is selected. * @param modifier The modifier to be applied to the layout. diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt index 8fa4b59d4..446a167e5 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -30,7 +30,7 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute /** * Screen that displays the selected group of associations. * - * @param popupStateData The form state data. + * @param popupStateData The popup state data. * @param route The [NavigationRoute.UNAssociationsView] route data of this screen. * @param onNavigateToFeature The callback to be invoked when the user selects a feature to navigate to. * @param onNavigateToAssociation The callback to be invoked when the user selects an association to navigate to. @@ -61,7 +61,7 @@ internal fun UNAssociationsScreen( isNavigationEnabled = true, onItemClick = { index -> val feature = groupResult.associationResults[index].associatedFeature - // Navigate to the next form if there are no edits. + // Navigate to the next popup if there are no edits. onNavigateToFeature(feature) }, onDetailsClick = { index -> From ff8024131c724a6875a7d45b381d111f62aeac42 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Thu, 11 Sep 2025 14:55:19 -0700 Subject: [PATCH 11/36] update micro app --- .../toolkit/popupapp/MainActivity.kt | 7 ++- .../popupapp/screens/mapscreen/MainScreen.kt | 3 +- .../screens/mapscreen/MapViewModel.kt | 51 ++----------------- 3 files changed, 8 insertions(+), 53 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt index a2dd8ec3d..9bac51dd6 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt @@ -37,7 +37,6 @@ import com.arcgismaps.toolkit.popupapp.ui.theme.PopupAppTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val viewModel: MapViewModel by viewModels { MapViewModel.Factory } ArcGISEnvironment.authenticationManager.arcGISAuthenticationChallengeHandler = TestArcGISAuthenticationChallengeHandler( BuildConfig.webMapUser, @@ -45,15 +44,15 @@ class MainActivity : ComponentActivity() { ) setContent { PopupAppTheme { - PopupApp(viewModel) + PopupApp() } } } } @Composable -fun PopupApp(viewModel: MapViewModel) { - MainScreen(viewModel) +fun PopupApp() { + MainScreen() } class TestArcGISAuthenticationChallengeHandler( diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index 2709ed3f4..f956f9d57 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.arcgismaps.data.Feature import com.arcgismaps.mapping.GeoElement import com.arcgismaps.mapping.layers.FeatureLayer @@ -53,7 +54,7 @@ private fun unselectFeature(feature: GeoElement?, layer: Layer?) { } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainScreen(viewModel: MapViewModel) { +fun MainScreen(viewModel: MapViewModel = viewModel()) { val scope = rememberCoroutineScope() val context = LocalContext.current diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt index 256f6269a..1772bbd0f 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.GeoElement @@ -42,21 +43,12 @@ import kotlinx.coroutines.launch import java.io.Closeable import kotlin.coroutines.CoroutineContext -/** - * Base class for context aware AndroidViewModel. This class must have only a single application - * parameter. - */ -open class BaseMapViewModel(application: Application) : AndroidViewModel(application) - /** * Simple android view model for the Popup app map screen. */ -@Suppress("unused_parameter") class MapViewModel( - savedStateHandle: SavedStateHandle, application: Application, - coroutineScope: CoroutineScope = CloseableCoroutineScope() -) : BaseMapViewModel(application) { +) : AndroidViewModel(application) { private var _geoElement: GeoElement? = null val geoElement: GeoElement? @@ -93,7 +85,7 @@ class MapViewModel( val proxy: MapViewProxy = MapViewProxy() init { - coroutineScope.launch { + viewModelScope.launch { map.load() } } @@ -109,42 +101,5 @@ class MapViewModel( fun setPopup(popup: Popup?) { _popup.value = popup } - companion object { - - /** - * The factory needed by the androidx ktx component activity to instantiate the view model. - * See onCreate() for usage. - */ - val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: Class, - extras: CreationExtras - ): T { - // Get the Application object from extras - val application = checkNotNull(extras[APPLICATION_KEY]) - // Create a SavedStateHandle for this ViewModel from extras - val savedStateHandle = extras.createSavedStateHandle() - - return MapViewModel( - savedStateHandle, - application - ) as T - } - } - } - } -/** - * a CoroutineScope used by the view model. It will by closed when the view model exits its - * lifecycle. - */ -class CloseableCoroutineScope( - context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate -) : Closeable, CoroutineScope { - override val coroutineContext: CoroutineContext = context - override fun close() { - coroutineContext.cancel() - } -} From a74403a767d1a703ad741cfbb91190ab27824ba1 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Thu, 11 Sep 2025 15:18:05 -0700 Subject: [PATCH 12/36] optimize imports --- .../com/arcgismaps/toolkit/popupapp/MainActivity.kt | 2 -- .../popupapp/screens/mapscreen/MapViewModel.kt | 12 ------------ .../main/java/com/arcgismaps/toolkit/popup/Popup.kt | 10 ---------- 3 files changed, 24 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt index 9bac51dd6..2958e8a7d 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/MainActivity.kt @@ -23,7 +23,6 @@ package com.arcgismaps.toolkit.popupapp import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels import androidx.compose.runtime.Composable import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.httpcore.authentication.ArcGISAuthenticationChallenge @@ -31,7 +30,6 @@ import com.arcgismaps.httpcore.authentication.ArcGISAuthenticationChallengeHandl import com.arcgismaps.httpcore.authentication.ArcGISAuthenticationChallengeResponse import com.arcgismaps.httpcore.authentication.TokenCredential import com.arcgismaps.toolkit.popupapp.screens.mapscreen.MainScreen -import com.arcgismaps.toolkit.popupapp.screens.mapscreen.MapViewModel import com.arcgismaps.toolkit.popupapp.ui.theme.PopupAppTheme class MainActivity : ComponentActivity() { diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt index 1772bbd0f..82939c3c1 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt @@ -20,13 +20,7 @@ import android.app.Application import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY -import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.GeoElement import com.arcgismaps.mapping.PortalItem @@ -35,13 +29,7 @@ import com.arcgismaps.mapping.layers.Layer import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.portal.Portal import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import java.io.Closeable -import kotlin.coroutines.CoroutineContext /** * Simple android view model for the Popup app map screen. diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 05fc5621f..bfd91d0f8 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -40,19 +40,9 @@ import androidx.navigation.compose.ComposeNavigator import androidx.navigation.compose.DialogNavigator import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.arcgismaps.mapping.popup.FieldsPopupElement import com.arcgismaps.mapping.popup.Popup -import com.arcgismaps.mapping.popup.PopupAttachment -import com.arcgismaps.mapping.popup.TextPopupElement -import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement -import com.arcgismaps.toolkit.popup.internal.element.fieldselement.FieldsElementState -import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementStateCollection -import com.arcgismaps.toolkit.popup.internal.element.state.mutablePopupElementStateCollection -import com.arcgismaps.toolkit.popup.internal.element.textelement.TextElementState -import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.PopupNavHost import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar -import kotlinx.coroutines.CoroutineScope /** * A composable Popup toolkit component that enables users to see Popup content in a From acada3ea5f4144533704dc75146bba2e7aae423f Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Thu, 11 Sep 2025 16:50:39 -0700 Subject: [PATCH 13/36] enable isNavigationEnabled add doc --- .../com/arcgismaps/toolkit/popup/Popup.kt | 37 +++++++++++++------ .../popup/internal/navigation/PopupNavHost.kt | 2 + .../internal/screens/ContentAwareTopBar.kt | 1 + .../internal/screens/UNAssociationsScreen.kt | 3 +- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index bfd91d0f8..21936ff38 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -40,6 +40,7 @@ import androidx.navigation.compose.ComposeNavigator import androidx.navigation.compose.DialogNavigator import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.toolkit.popup.internal.navigation.PopupNavHost import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar @@ -68,27 +69,40 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { } if (stateData.getActivePopupStateData().initialEvaluation.value) { - Popup(popup, stateData, modifier, showCloseIcon = false) + Popup(stateData, modifier, showCloseIcon = false) } } +/** + * A composable Popup toolkit component that enables users to see Popup content in a + * layer that have been configured externally. + * + * Popups may be configured in the [Web Map Viewer](https://www.arcgis.com/home/webmap/viewer.html) + * or [Fields Maps Designer](https://www.arcgis.com/apps/fieldmaps/)). + * + * Note : Even though the [Popup] class is not stable, there exists an internal mechanism to + * enable smart recompositions. + * + * @param popupState The [PopupState] object that holds the state of the Popup. + * @param modifier The [Modifier] to be applied to layout corresponding to the content of this + * Popup. + * @param onDismiss Callback that is invoked when the user clicks the close icon in the top app bar. + * @param showCloseIcon Flag to indicate if the close icon should be shown in the top app bar. If true, the [onDismiss] + * callback will be invoked when the close icon is clicked. Default is true. + * @param isNavigationEnabled Indicates if the navigation is enabled for the popup when there are + * [UtilityAssociationsPopupElement]s present. When true, the user can navigate to associated features + * and back. If false, this navigation is disabled. Default is true + * + * @since 200.9.0 + */ @Composable public fun Popup( - popup: Popup, popupState: PopupState, modifier: Modifier = Modifier, onDismiss: () -> Unit = {}, showCloseIcon: Boolean = true, - isNavigationEnabled : Boolean = true, + isNavigationEnabled : Boolean = true ) { - // Add the provided state collection to the store. - val popupStateData = PopupStateData(popup) -// popupState.store.addLast(popupStateData) -// LaunchedEffect(popup) { -// popupState.evaluateExpressions() -// } -// val states = rememberStates(popup, popupState.attachments, rememberCoroutineScope()) -// popupState.setStates(states) val navController = rememberNavController(popupState) popupState.setNavigationCallback { route -> @@ -126,6 +140,7 @@ public fun Popup( PopupNavHost( navController = navController, state = popupState, + isNavigationEnabled = isNavigationEnabled, modifier = Modifier.fillMaxSize() ) }, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 0493a3ea4..54ab2a374 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -42,6 +42,7 @@ import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen internal fun PopupNavHost( navController: NavHostController, state: PopupState, + isNavigationEnabled: Boolean, modifier: Modifier = Modifier, ) { NavHost( @@ -93,6 +94,7 @@ internal fun PopupNavHost( UNAssociationsScreen( popupStateData = popupStateData, route = route, + isNavigationEnabled = isNavigationEnabled, onNavigateToFeature = { feature -> // Request the state to navigate to the feature. state.navigateTo(backStackEntry, feature) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt index 99e7a44e8..bf4f35768 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt @@ -55,6 +55,7 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute * @param state The [PopupState] that holds the current popup state data. * @param hasBackStack Indicates if there is a previous route in the navigation stack. * @param showCloseIcon Indicates if the close icon should be displayed. + * @param isNavigationEnabled Indicates if navigation actions are enabled. * @param onDismissRequest The callback to invoke when the close button is clicked. * @param modifier The [Modifier] to apply to this layout. */ diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt index 446a167e5..bb0c104f6 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -40,6 +40,7 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute internal fun UNAssociationsScreen( popupStateData: PopupStateData, route: NavigationRoute.UNAssociationsView, + isNavigationEnabled: Boolean, onNavigateToFeature: (ArcGISFeature) -> Unit, onNavigateToAssociation: (Int) -> Unit, modifier: Modifier = Modifier @@ -58,7 +59,7 @@ internal fun UNAssociationsScreen( } UtilityAssociations( groupResult = groupResult, - isNavigationEnabled = true, + isNavigationEnabled = isNavigationEnabled, onItemClick = { index -> val feature = groupResult.associationResults[index].associatedFeature // Navigate to the next popup if there are no edits. From a105da314fc4f68213d4232c019b9bf7fb0cc003 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 09:03:23 -0700 Subject: [PATCH 14/36] add support for ignore list --- .../com/arcgismaps/toolkit/popup/Popup.kt | 9 +- .../arcgismaps/toolkit/popup/PopupState.kt | 152 ++++++++++-------- 2 files changed, 94 insertions(+), 67 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 21936ff38..96fdba10d 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -65,7 +65,14 @@ import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar public fun Popup(popup: Popup, modifier: Modifier = Modifier) { val scope = rememberCoroutineScope() val stateData = remember(popup) { - PopupState(popup, scope) + PopupState( + popup, + scope, + // Ignore the UtilityAssociationsFormElement as it is not supported with this API + ignoreList = setOf( + UtilityAssociationsPopupElement::class.java + ) + ) } if (stateData.getActivePopupStateData().initialEvaluation.value) { diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 1c736dd0c..688d4cbcd 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -15,6 +15,7 @@ import com.arcgismaps.mapping.popup.FieldsPopupElement import com.arcgismaps.mapping.popup.MediaPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupAttachment +import com.arcgismaps.mapping.popup.PopupElement import com.arcgismaps.mapping.popup.TextPopupElement import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.toolkit.popup.internal.element.attachment.AttachmentsElementState @@ -63,21 +64,34 @@ public class PopupState(@Stable public val popup: Popup) { */ public val activePopup: Popup by _activePopup - public constructor(popup: Popup, scope: CoroutineScope) : this(popup) { - this.coroutineScope = scope + private fun initializePopupStateData( + popup: Popup, + ignoreList: Set> = emptySet() + ) { val popupStateData = PopupStateData(popup) coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { val attachments = popupStateData.evaluateExpressionsAndGetAttachments() val states = createStates( popup = popup, attachments = attachments, - coroutineScope = coroutineScope + coroutineScope = coroutineScope, + ignoreList = ignoreList ) popupStateData.setStates(states) } store.addLast(popupStateData) } + public constructor(popup: Popup, scope: CoroutineScope) : this(popup) { + this.coroutineScope = scope + initializePopupStateData(popup) + } + + internal constructor(popup: Popup, scope: CoroutineScope, ignoreList: Set>) : this(popup) { + this.coroutineScope = scope + initializePopupStateData(popup, ignoreList) + } + /** * Sets the navigation callback to the provided [navigateToRoute] function. This function is * called when navigating to a new [Popup]. Set this to null when the composition is @@ -183,6 +197,75 @@ public class PopupState(@Stable public val popup: Popup) { internal fun getActivePopupStateData(): PopupStateData { return store.last() } + + /** + * Creates state objects for all the supported element types that are part of the + * provided Popup. These state objects are returned as part of a [PopupElementStateCollection]. + * + * @param popup the [Popup] to create the states for. + * @return returns the [PopupElementStateCollection] created. + */ + internal fun createStates( + popup: Popup, + attachments: List, + coroutineScope: CoroutineScope, + ignoreList: Set> = emptySet(), + ): PopupElementStateCollection { + val states = mutablePopupElementStateCollection() + val elements: List = popup.evaluatedElements + // Filter out elements that are part of the ignore list. + val filteredElements = elements.filter { element -> + !ignoreList.contains(element::class.java) + } + filteredElements.forEach { element -> + when (element) { + is TextPopupElement -> { + states.add( + element, + TextElementState(element = element, popup = popup) + ) + } + + is AttachmentsPopupElement -> { + states.add( + element, + AttachmentsElementState( + attachmentPopupElement = element, + attachments = attachments + ) + ) + } + + is FieldsPopupElement -> { + states.add( + element, + FieldsElementState(element = element, popup = popup) + ) + } + + is MediaPopupElement -> { + states.add( + element, + MediaElementState(element = element, popup = popup) + ) + } + + is UtilityAssociationsPopupElement -> { + states.add( + element, + UtilityAssociationsElementState(element, coroutineScope) + ) + } + + else -> { + // TODO remove for release + println("encountered element of type ${element::class.java}") + } + } + } + + return states + } } /** @@ -242,69 +325,6 @@ internal data class PopupStateData( } } -/** - * Creates state objects for all the supported element types that are part of the - * provided Popup. These state objects are returned as part of a [PopupElementStateCollection]. - * - * @param popup the [Popup] to create the states for. - * @return returns the [PopupElementStateCollection] created. - */ -internal fun createStates( - popup: Popup, - attachments: List, - coroutineScope: CoroutineScope -): PopupElementStateCollection { - val states = mutablePopupElementStateCollection() - popup.evaluatedElements.forEach { element -> - when (element) { - is TextPopupElement -> { - states.add( - element, - TextElementState(element = element, popup = popup) - ) - } - - is AttachmentsPopupElement -> { - states.add( - element, - AttachmentsElementState( - attachmentPopupElement = element, - attachments = attachments - ) - ) - } - - is FieldsPopupElement -> { - states.add( - element, - FieldsElementState(element = element, popup = popup) - ) - } - - is MediaPopupElement -> { - states.add( - element, - MediaElementState(element = element, popup = popup) - ) - } - - is UtilityAssociationsPopupElement -> { - states.add( - element, - UtilityAssociationsElementState(element, coroutineScope) - ) - } - - else -> { - // TODO remove for release - println("encountered element of type ${element::class.java}") - } - } - } - - return states -} - internal fun ArcGISFeature.toPopup(): Popup { val popupDefinition = when { this.featureTable?.popupDefinition != null -> { From a4d2504fb216a676f59213f7165f8593107a6ce2 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 09:25:24 -0700 Subject: [PATCH 15/36] add support for dynamic entity --- .../com/arcgismaps/toolkit/popup/Popup.kt | 20 +++++++++++++++++++ .../popup/internal/navigation/PopupNavHost.kt | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 96fdba10d..1e395a3ae 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -27,11 +27,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -42,6 +45,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement import com.arcgismaps.mapping.popup.Popup +import com.arcgismaps.realtime.DynamicEntity import com.arcgismaps.toolkit.popup.internal.navigation.PopupNavHost import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar @@ -110,6 +114,21 @@ public fun Popup( showCloseIcon: Boolean = true, isNavigationEnabled : Boolean = true ) { + val popup = popupState.popup + val dynamicEntity = (popup.geoElement as? DynamicEntity) + // If the popup is for a dynamic entity, we want to refresh the popup periodically + // to get the latest data. + var lastUpdatedEntityId by rememberSaveable(dynamicEntity) { mutableLongStateOf(dynamicEntity?.id ?: -1) } + if (dynamicEntity != null) { + LaunchedEffect(popup) { + dynamicEntity.dynamicEntityChangedEvent.collect { + // briefly show the initializing screen so it is clear the entity just pulsed + // and values may have changed. + popupState.popup.evaluateExpressions() + lastUpdatedEntityId = it.receivedObservation?.id ?: -1 + } + } + } val navController = rememberNavController(popupState) popupState.setNavigationCallback { route -> @@ -147,6 +166,7 @@ public fun Popup( PopupNavHost( navController = navController, state = popupState, + refreshed = lastUpdatedEntityId, isNavigationEnabled = isNavigationEnabled, modifier = Modifier.fillMaxSize() ) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 54ab2a374..0873f0ef0 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -42,6 +42,7 @@ import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen internal fun PopupNavHost( navController: NavHostController, state: PopupState, + refreshed: Long, isNavigationEnabled: Boolean, modifier: Modifier = Modifier, ) { @@ -60,7 +61,7 @@ internal fun PopupNavHost( state, popupStateData, popupStateData.initialEvaluation.value, - -1, + refreshed, onUtilityFilterSelected = { state -> val newRoute = NavigationRoute.UNFilterView(stateId = state.id) // Navigate to the filter view From 02fccbd3d70a004c35b1e30c4dcbffa0156418e4 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 09:47:41 -0700 Subject: [PATCH 16/36] update api file deprecate popup composable --- toolkit/popup/api/popup.api | 43 ++++++++++++++++++- toolkit/popup/build.gradle.kts | 5 ++- .../com/arcgismaps/toolkit/popup/Popup.kt | 4 ++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/toolkit/popup/api/popup.api b/toolkit/popup/api/popup.api index 18e0e8002..3a591e42f 100644 --- a/toolkit/popup/api/popup.api +++ b/toolkit/popup/api/popup.api @@ -1,5 +1,14 @@ public final class com/arcgismaps/toolkit/popup/PopupKt { - public static final fun Popup (Lcom/arcgismaps/mapping/popup/Popup;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final synthetic fun Popup (Lcom/arcgismaps/mapping/popup/Popup;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun Popup (Lcom/arcgismaps/toolkit/popup/PopupState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;ZZLandroidx/compose/runtime/Composer;II)V +} + +public final class com/arcgismaps/toolkit/popup/PopupState { + public static final field $stable I + public fun (Lcom/arcgismaps/mapping/popup/Popup;)V + public fun (Lcom/arcgismaps/mapping/popup/Popup;Lkotlinx/coroutines/CoroutineScope;)V + public final fun getActivePopup ()Lcom/arcgismaps/mapping/popup/Popup; + public final fun getPopup ()Lcom/arcgismaps/mapping/popup/Popup; } public final class com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState$Creator : android/os/Parcelable$Creator { @@ -18,6 +27,38 @@ public final class com/arcgismaps/toolkit/popup/internal/element/textelement/Tex public synthetic fun newArray (I)[Ljava/lang/Object; } +public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$Dismiss$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$Dismiss; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$Dismiss; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateBack$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateBack; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateBack; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateToFeature$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateToFeature; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateToFeature; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + +public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$None$Creator : android/os/Parcelable$Creator { + public fun ()V + public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$None; + public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; + public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$None; + public synthetic fun newArray (I)[Ljava/lang/Object; +} + public final class com/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile$Creator : android/os/Parcelable$Creator { public fun ()V public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile; diff --git a/toolkit/popup/build.gradle.kts b/toolkit/popup/build.gradle.kts index b496ab5fe..179b042f2 100644 --- a/toolkit/popup/build.gradle.kts +++ b/toolkit/popup/build.gradle.kts @@ -86,7 +86,10 @@ apiValidation { val composableSingletons = listOf( "com.arcgismaps.toolkit.popup.internal.ui.ComposableSingletons\$ExpandableCardKt", "com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ComposableSingletons\$FileViewerKt", - "com.arcgismaps.toolkit.popup.internal.ui.expandablecard.ComposableSingletons\$ExpandableCardKt" + "com.arcgismaps.toolkit.popup.internal.ui.expandablecard.ComposableSingletons\$ExpandableCardKt", + "com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.ComposableSingletons\$UtilityAssociationDetailsKt", + "com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.ComposableSingletons\$UtilityAssociationsKt", + "com.arcgismaps.toolkit.popup.internal.screens.ComposableSingletons\$ContentAwareTopBarKt" ) ignoredClasses.addAll(composableSingletons) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 1e395a3ae..5e34edd7c 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -65,6 +65,10 @@ import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar * * @since 200.5.0 */ +@Deprecated( + message = "Maintained for binary compatibility. Use the overload that uses the PopupState object.", + level = DeprecationLevel.HIDDEN +) @Composable public fun Popup(popup: Popup, modifier: Modifier = Modifier) { val scope = rememberCoroutineScope() From 29241d5f56a82affb466d9eaf4150c8d1b983f5f Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 10:04:05 -0700 Subject: [PATCH 17/36] update micro app --- .../popupapp/screens/mapscreen/MainScreen.kt | 8 ++++---- .../popupapp/screens/mapscreen/MapViewModel.kt | 13 +++++++------ .../toolkit/popup/internal/screens/PopupScreen.kt | 1 - 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index f956f9d57..b51b62841 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -66,8 +66,8 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { ) BottomSheetScaffold( sheetContent = { - if (viewModel.popup != null) { - Popup(viewModel.popup!!, Modifier.animateContentSize()) + if (viewModel.popupState != null) { + Popup(viewModel.popupState!!, Modifier.animateContentSize()) } }, modifier = Modifier.fillMaxSize(), @@ -91,7 +91,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { ).onSuccess { results -> if (results.isEmpty()) { unselectFeature(viewModel.geoElement, viewModel.layer) - viewModel.setPopup(null) + viewModel.updatePopupState(null) viewModel.setLayer(null) viewModel.setGeoElement(null) if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) { @@ -165,7 +165,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { throw IllegalStateException("popups on sublayers are not supported by the PopupApp") } } - viewModel.setPopup(popup) + viewModel.updatePopupState(popup) scaffoldState.bottomSheetState.expand() } } catch (e: Exception) { diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt index 82939c3c1..80752b777 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt @@ -29,6 +29,7 @@ import com.arcgismaps.mapping.layers.Layer import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.portal.Portal import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy +import com.arcgismaps.toolkit.popup.PopupState import kotlinx.coroutines.launch /** @@ -54,12 +55,12 @@ class MapViewModel( private val streamServiceMap = "aef32323d1f248368b1663cfc938995e" /** - * The Popup read by the composition is held as a state variable. + * The PopupState read by the composition is held as a state variable. * We want the composition to recompose when the Popup changes. */ - private var _popup: MutableState = mutableStateOf(null) - val popup: Popup? - get() = _popup.value + private var _popupState: MutableState = mutableStateOf(null) + val popupState: PopupState? + get() = _popupState.value val map = ArcGISMap( PortalItem( @@ -86,8 +87,8 @@ class MapViewModel( _layer = layer } - fun setPopup(popup: Popup?) { - _popup.value = popup + fun updatePopupState(popup: Popup?) { + _popupState.value = PopupState(popup!!, viewModelScope) } } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt index 8367171fc..162a86bee 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt @@ -80,7 +80,6 @@ internal fun PopupScreen( modifier: Modifier = Modifier ) { val scope = rememberCoroutineScope() - val popup = popupState.popup val viewableFileState = rememberSaveable { mutableStateOf(null) } viewableFileState.value?.let { viewableFile -> FileViewer(scope, fileState = viewableFile) { From 4cbab74bbeb451ede79e516b116af4d35e0f1205 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 10:06:53 -0700 Subject: [PATCH 18/36] update micro app to not show close icon --- .../arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index b51b62841..b383f73b4 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -67,7 +67,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { BottomSheetScaffold( sheetContent = { if (viewModel.popupState != null) { - Popup(viewModel.popupState!!, Modifier.animateContentSize()) + Popup(viewModel.popupState!!, Modifier.animateContentSize(), showCloseIcon = false) } }, modifier = Modifier.fillMaxSize(), From 5f1e78b93462d0fb3c2a2c8a0d29bc2fecd841cd Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 10:13:20 -0700 Subject: [PATCH 19/36] add copyright disable lint for missing translations --- toolkit/popup/build.gradle.kts | 4 ++++ .../com/arcgismaps/toolkit/popup/PopupState.kt | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/toolkit/popup/build.gradle.kts b/toolkit/popup/build.gradle.kts index 179b042f2..950d1ea3f 100644 --- a/toolkit/popup/build.gradle.kts +++ b/toolkit/popup/build.gradle.kts @@ -77,6 +77,10 @@ android { } } + lint { + disable += "MissingTranslation" + } + } apiValidation { diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 688d4cbcd..f8e163d3c 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.arcgismaps.toolkit.popup import androidx.annotation.MainThread From bb55c826de8248778e948cb1fbbde64cff45c726 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 15 Sep 2025 17:31:22 -0700 Subject: [PATCH 20/36] remove parcelize where it is not being used update API file --- toolkit/popup/api/popup.api | 56 ------------------- .../attachment/AttachmentsElementState.kt | 3 - .../fieldselement/FieldsElementState.kt | 7 +-- .../element/textelement/TextElementState.kt | 7 +-- 4 files changed, 2 insertions(+), 71 deletions(-) diff --git a/toolkit/popup/api/popup.api b/toolkit/popup/api/popup.api index 3a591e42f..36eda366a 100644 --- a/toolkit/popup/api/popup.api +++ b/toolkit/popup/api/popup.api @@ -11,59 +11,3 @@ public final class com/arcgismaps/toolkit/popup/PopupState { public final fun getPopup ()Lcom/arcgismaps/mapping/popup/Popup; } -public final class com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$Dismiss$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$Dismiss; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$Dismiss; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateBack$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateBack; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateBack; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateToFeature$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateToFeature; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$NavigateToFeature; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$None$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$None; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/navigation/NavigationAction$None; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - -public final class com/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile$Creator : android/os/Parcelable$Creator { - public fun ()V - public final fun createFromParcel (Landroid/os/Parcel;)Lcom/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile; - public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object; - public final fun newArray (I)[Lcom/arcgismaps/toolkit/popup/internal/ui/fileviewer/ViewableFile; - public synthetic fun newArray (I)[Ljava/lang/Object; -} - diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt index 099e77dbc..c5c57b25e 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/attachment/AttachmentsElementState.kt @@ -26,12 +26,9 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.graphics.vector.ImageVector import com.arcgismaps.LoadStatus import com.arcgismaps.mapping.popup.AttachmentsPopupElement -import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.PopupAttachment import com.arcgismaps.mapping.popup.PopupAttachmentType import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt index e64b1fd9b..9a6843f65 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt @@ -15,14 +15,10 @@ */ package com.arcgismaps.toolkit.popup.internal.element.fieldselement -import android.os.Parcelable -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.saveable.rememberSaveable import com.arcgismaps.mapping.popup.FieldsPopupElement import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState -import kotlinx.parcelize.Parcelize /** * A class to handle the state of a [FieldsPopupElement]. @@ -30,13 +26,12 @@ import kotlinx.parcelize.Parcelize * @since 200.5.0 */ @Immutable -@Parcelize internal class FieldsElementState( val title: String, val description: String, val fieldsToFormattedValues: Map, override val id: Int -) : Parcelable, PopupElementState() { +) : PopupElementState() { constructor(element: FieldsPopupElement, popup: Popup) : this( title = element.title, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt index 400d969f6..41a48e99e 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/textelement/TextElementState.kt @@ -16,14 +16,10 @@ package com.arcgismaps.toolkit.popup.internal.element.textelement -import android.os.Parcelable -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.saveable.rememberSaveable import com.arcgismaps.mapping.popup.Popup import com.arcgismaps.mapping.popup.TextPopupElement import com.arcgismaps.toolkit.popup.internal.element.state.PopupElementState -import kotlinx.parcelize.Parcelize /** * A class to handle the state of a [TextPopupElement]. @@ -31,11 +27,10 @@ import kotlinx.parcelize.Parcelize * @param value the text of the element. */ @Immutable -@Parcelize internal class TextElementState( val value: String, override val id: Int -) : Parcelable, PopupElementState() { +) : PopupElementState() { constructor(element: TextPopupElement, popup: Popup) : this( value = element.text, From 7209c508360826444b335acbdbe67df905eed79c Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 17 Sep 2025 14:29:27 -0700 Subject: [PATCH 21/36] address code review comments --- .../screens/mapscreen/MapViewModel.kt | 2 +- .../com/arcgismaps/toolkit/popup/Popup.kt | 12 +++++------- .../arcgismaps/toolkit/popup/PopupState.kt | 19 ++++++++++++++++++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt index 80752b777..b9468c16d 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MapViewModel.kt @@ -88,7 +88,7 @@ class MapViewModel( } fun updatePopupState(popup: Popup?) { - _popupState.value = PopupState(popup!!, viewModelScope) + _popupState.value = popup?.let { PopupState(it, viewModelScope) } } } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 5e34edd7c..69bedf8d1 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -66,8 +66,9 @@ import com.arcgismaps.toolkit.popup.internal.screens.ContentAwareTopBar * @since 200.5.0 */ @Deprecated( - message = "Maintained for binary compatibility. Use the overload that uses the PopupState object.", - level = DeprecationLevel.HIDDEN + message = "Use the overload that uses the PopupState object. This will become an error" + + " in a future release.", + level = DeprecationLevel.WARNING ) @Composable public fun Popup(popup: Popup, modifier: Modifier = Modifier) { @@ -82,10 +83,7 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { ) ) } - - if (stateData.getActivePopupStateData().initialEvaluation.value) { - Popup(stateData, modifier, showCloseIcon = false) - } + Popup(stateData, modifier, showCloseIcon = false) } /** @@ -108,7 +106,7 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { * [UtilityAssociationsPopupElement]s present. When true, the user can navigate to associated features * and back. If false, this navigation is disabled. Default is true * - * @since 200.9.0 + * @since 300.0.0 */ @Composable public fun Popup( diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index f8e163d3c..c5d2244a5 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -46,7 +46,24 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.launch -public class PopupState(@Stable public val popup: Popup) { +/** + * The state object for a [Popup] used by the Popup composable. This class is + * responsible for managing the state of the Popup and its elements. Hoist this state out of the + * composition to ensure that the state is not lost during configuration changes. + * + * This class also provides a way to navigate between different [Popup]s of different [ArcGISFeature]s + * when viewing associations for an [UtilityAssociationsPopupElement], if it is part of the provided + * [Popup]. Use the [activePopup] property to get the currently active popup as it is + * is updated when navigating from one popup to another. + * + * [Popup.evaluateExpressions] is called automatically when navigating to a new [Popup] + * or when navigating back to a previous [Popup]. Expressions are also run when this class is + * created so you do not need to call [Popup.evaluateExpressions] manually. + * + * @since 300.0.0 + */ +@Stable +public class PopupState private constructor(internal val popup: Popup) { private val store: ArrayDeque = ArrayDeque() From a4532341bc1d4922bd3d1fd5a0d5100cc7a55ede Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 17 Sep 2025 15:31:35 -0700 Subject: [PATCH 22/36] remove support for isNavigationEnabled and NavigationAction --- .../com/arcgismaps/toolkit/popup/Popup.kt | 6 --- .../UtilityAssociations.kt | 11 +--- .../UtilityAssociationsElementState.kt | 2 - .../internal/navigation/NavigationAction.kt | 53 ------------------- .../popup/internal/navigation/PopupNavHost.kt | 2 - .../internal/screens/ContentAwareTopBar.kt | 51 +++--------------- .../internal/screens/UNAssociationsScreen.kt | 2 - 7 files changed, 8 insertions(+), 119 deletions(-) delete mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt index 69bedf8d1..676898c10 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/Popup.kt @@ -102,9 +102,6 @@ public fun Popup(popup: Popup, modifier: Modifier = Modifier) { * @param onDismiss Callback that is invoked when the user clicks the close icon in the top app bar. * @param showCloseIcon Flag to indicate if the close icon should be shown in the top app bar. If true, the [onDismiss] * callback will be invoked when the close icon is clicked. Default is true. - * @param isNavigationEnabled Indicates if the navigation is enabled for the popup when there are - * [UtilityAssociationsPopupElement]s present. When true, the user can navigate to associated features - * and back. If false, this navigation is disabled. Default is true * * @since 300.0.0 */ @@ -114,7 +111,6 @@ public fun Popup( modifier: Modifier = Modifier, onDismiss: () -> Unit = {}, showCloseIcon: Boolean = true, - isNavigationEnabled : Boolean = true ) { val popup = popupState.popup val dynamicEntity = (popup.geoElement as? DynamicEntity) @@ -154,7 +150,6 @@ public fun Popup( onDismissRequest = onDismiss, hasBackStack = hasBackStack, showCloseIcon = showCloseIcon, - isNavigationEnabled = isNavigationEnabled, modifier = Modifier .padding( vertical = 8.dp, @@ -169,7 +164,6 @@ public fun Popup( navController = navController, state = popupState, refreshed = lastUpdatedEntityId, - isNavigationEnabled = isNavigationEnabled, modifier = Modifier.fillMaxSize() ) }, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt index 1d23e2217..86c09c48a 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt @@ -121,7 +121,6 @@ internal fun UtilityAssociationFilter( @Composable internal fun UtilityAssociations( groupResult: UtilityAssociationGroupResult, - isNavigationEnabled: Boolean, onItemClick: (Int) -> Unit, onDetailsClick: (Int) -> Unit, modifier: Modifier = Modifier @@ -144,7 +143,6 @@ internal fun UtilityAssociations( title = info.title, association = info.association, associatedFeature = info.associatedFeature, - enabled = isNavigationEnabled, onClick = { onItemClick(index) }, @@ -185,7 +183,6 @@ private fun AssociationItem( title: String, association: UtilityAssociation, associatedFeature: ArcGISFeature, - enabled: Boolean, onClick: () -> Unit, onDetailsClick: () -> Unit, modifier: Modifier = Modifier @@ -223,14 +220,10 @@ private fun AssociationItem( else -> {} } - val contentColor = if (enabled) { - LocalContentColor.current - } else { - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - } + val contentColor = LocalContentColor.current Row( modifier = modifier - .clickable(enabled = enabled, onClick = onClick) + .clickable(onClick = onClick) .height(56.dp) .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt index 55a70ac44..68c029fde 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt @@ -39,8 +39,6 @@ internal class UtilityAssociationsElementState( override val id : Int = element.hashCode() val label : String = element.title val description: String = element.description - // val isVisible : StateFlow = element.isVisible - private var _loading: MutableState = mutableStateOf(true) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt deleted file mode 100644 index b6d4b58f8..000000000 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationAction.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2025 Esri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.arcgismaps.toolkit.popup.internal.navigation - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -/** - * Indicates the action to take when navigating. - */ -@Parcelize -internal sealed class NavigationAction(val value: Int) : Parcelable { - - /** - * Indicates no action. - */ - @Parcelize - data object None : NavigationAction(0) - - /** - * Indicates an action to navigate back. - */ - @Parcelize - data object NavigateBack : NavigationAction(1) - - /** - * Indicates an action to dismiss the popup. - */ - @Parcelize - data object Dismiss : NavigationAction(2) - - /** - * Indicates an action to navigate to an associated feature. - * - * @param index The index of the association to navigate to. - */ - @Parcelize - data class NavigateToFeature(val index : Int) : NavigationAction(3) -} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 0873f0ef0..7e6009d07 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -43,7 +43,6 @@ internal fun PopupNavHost( navController: NavHostController, state: PopupState, refreshed: Long, - isNavigationEnabled: Boolean, modifier: Modifier = Modifier, ) { NavHost( @@ -95,7 +94,6 @@ internal fun PopupNavHost( UNAssociationsScreen( popupStateData = popupStateData, route = route, - isNavigationEnabled = isNavigationEnabled, onNavigateToFeature = { feature -> // Request the state to navigate to the feature. state.navigateTo(backStackEntry, feature) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt index bf4f35768..3778d9ec8 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/ContentAwareTopBar.kt @@ -45,7 +45,6 @@ import com.arcgismaps.toolkit.popup.PopupState import com.arcgismaps.toolkit.popup.PopupStateData import com.arcgismaps.toolkit.popup.R import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState -import com.arcgismaps.toolkit.popup.internal.navigation.NavigationAction import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute /** @@ -55,7 +54,6 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute * @param state The [PopupState] that holds the current popup state data. * @param hasBackStack Indicates if there is a previous route in the navigation stack. * @param showCloseIcon Indicates if the close icon should be displayed. - * @param isNavigationEnabled Indicates if navigation actions are enabled. * @param onDismissRequest The callback to invoke when the close button is clicked. * @param modifier The [Modifier] to apply to this layout. */ @@ -65,69 +63,34 @@ internal fun ContentAwareTopBar( state: PopupState, hasBackStack: Boolean, showCloseIcon: Boolean, - isNavigationEnabled: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier ) { val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } - // Callback to handle navigation actions - val onNavigationAction: (NavigationAction) -> Unit = { action -> - // execute the action immediately - when (action) { - is NavigationAction.NavigateBack -> { - state.popBackStack(backStackEntry) - } - - is NavigationAction.Dismiss -> { - onDismissRequest() - } - - else -> {} - } - } - val onBackAction: (NavBackStackEntry) -> Unit = { entry -> - when { - entry.destination.hasRoute() -> { - // Run the navigation action if the current view is the popup view - onNavigationAction(NavigationAction.NavigateBack) - } + val onBackAction = { state.popBackStack(backStackEntry) } - else -> { - // Pop the back stack if the current view is not the popup view - state.popBackStack(backStackEntry) - } - } - } // Get the title and subtitle for the top bar based on the current navigation state val (title, subTitle) = getTopBarTitleAndSubtitle(backStackEntry, popupStateData) - val navigationEnabled = when { - // If the current destination is the popup view, only then check if navigation is enabled - backStackEntry.destination.hasRoute() -> { - isNavigationEnabled - } - // For other destinations, always enable back navigation - else -> true - } + Column { PopupTitle( title = title, subTitle = subTitle, showCloseIcon = showCloseIcon, showBackIcon = hasBackStack, - isNavigationEnabled = navigationEnabled, onBackPressed = { - onBackAction(backStackEntry) + onBackAction() }, onClose = { - onNavigationAction(NavigationAction.Dismiss) + onDismissRequest() }, modifier = modifier ) } // only enable back navigation if there is a previous route BackHandler(hasBackStack) { - onBackAction(backStackEntry) + onBackAction() } } @@ -203,7 +166,6 @@ private fun PopupTitle( subTitle: String, showCloseIcon: Boolean, showBackIcon: Boolean, - isNavigationEnabled: Boolean, onBackPressed: () -> Unit, onClose: () -> Unit, modifier: Modifier = Modifier, @@ -214,7 +176,7 @@ private fun PopupTitle( horizontalArrangement = Arrangement.Start, ) { if (showBackIcon) { - IconButton(onClick = onBackPressed, enabled = isNavigationEnabled) { + IconButton(onClick = onBackPressed) { Icon( Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = "Navigate back" @@ -256,7 +218,6 @@ private fun PopupTitlePreview() { subTitle = "Edit feature attributes", showCloseIcon = true, showBackIcon = false, - isNavigationEnabled = true, onBackPressed = {}, onClose = {}, modifier = Modifier.padding(8.dp) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt index bb0c104f6..b0207b4a6 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -40,7 +40,6 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute internal fun UNAssociationsScreen( popupStateData: PopupStateData, route: NavigationRoute.UNAssociationsView, - isNavigationEnabled: Boolean, onNavigateToFeature: (ArcGISFeature) -> Unit, onNavigateToAssociation: (Int) -> Unit, modifier: Modifier = Modifier @@ -59,7 +58,6 @@ internal fun UNAssociationsScreen( } UtilityAssociations( groupResult = groupResult, - isNavigationEnabled = isNavigationEnabled, onItemClick = { index -> val feature = groupResult.associationResults[index].associatedFeature // Navigate to the next popup if there are no edits. From 1d5cdb7e706b2ed8ec1e5f99443ca0dfa45bde7f Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 17 Sep 2025 16:08:46 -0700 Subject: [PATCH 23/36] remove showConfirmationDialog in UtilityAssocationsDetails --- .../UtilityAssociationDetails.kt | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt index bbbd61a03..2eeaca687 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt @@ -58,9 +58,6 @@ internal fun UtilityAssociationDetails( val associationResult = state.selectedAssociationResult ?: return val filter = state.selectedFilterResult?.filter ?: return val association = associationResult.association - var showConfirmationDialog by remember { - mutableStateOf(false) - } Column( modifier = modifier, verticalArrangement = Arrangement.Top, @@ -134,24 +131,6 @@ internal fun UtilityAssociationDetails( modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp, bottom = 24.dp) ) } - Spacer(modifier = Modifier.height(20.dp)) - Button(onClick = { showConfirmationDialog = true }) { - Text(text = stringResource(R.string.remove_association)) - } - Spacer(modifier = Modifier.height(10.dp)) - Text( - text = stringResource(R.string.remove_association_tooltip), - style = MaterialTheme.typography.bodySmall - ) - } - if (showConfirmationDialog) { - RemoveAssociationConfirmationDialog( - onDismiss = { showConfirmationDialog = false }, - onRemove = { - showConfirmationDialog = false - // Remove the association when the API is available. - } - ) } } @@ -180,38 +159,6 @@ internal fun PropertyRow( } } -/** - * A composable that displays a confirmation dialog for removing an association. - * - * @param onDismiss A callback that is called when the dialog is dismissed. - * @param onRemove A callback that is called when the remove button is clicked. - */ -@Composable -private fun RemoveAssociationConfirmationDialog( - onDismiss: () -> Unit, - onRemove: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text(text = "${stringResource(R.string.remove_association)}?") - }, - text = { - Text(text = stringResource(R.string.remove_association_tooltip)) - }, - confirmButton = { - TextButton(onRemove) { - Text(text = stringResource(R.string.remove)) - } - }, - dismissButton = { - TextButton(onDismiss) { - Text(text = stringResource(R.string.cancel)) - } - } - ) -} - /** * Extension function that returns the fraction along edge of the association result, if applicable. * From b54f7d00a37607e61c832f4ed44850e708533647 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 17 Sep 2025 18:12:55 -0700 Subject: [PATCH 24/36] add support for display count add some suggested optimizations --- .../arcgismaps/toolkit/popup/PopupState.kt | 2 +- .../fieldselement/FieldsElementState.kt | 4 +- .../UtilityAssociations.kt | 62 +++++++++++++++---- .../UtilityAssociationsElementState.kt | 1 + .../internal/screens/UNAssociationsScreen.kt | 3 +- toolkit/popup/src/main/res/values/strings.xml | 1 + 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index c5d2244a5..71ca267c1 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -365,7 +365,7 @@ internal fun ArcGISFeature.toPopup(): Popup { this.getFeatureSubtype() != null && this.featureTable is ArcGISFeatureTable -> { (this.featureTable as ArcGISFeatureTable).subtypeSubtables - .firstOrNull { it.name == this.getFeatureSubtype()?.name } + .firstOrNull { it.subtype == this.getFeatureSubtype()} ?.popupDefinition } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt index 9a6843f65..3d7b04ff5 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/fieldselement/FieldsElementState.kt @@ -36,8 +36,8 @@ internal class FieldsElementState( constructor(element: FieldsPopupElement, popup: Popup) : this( title = element.title, description = element.description, - fieldsToFormattedValues = element.fields.map { it.label } - .zip(element.formattedValues) + fieldsToFormattedValues = element.fields + .mapIndexed { index, field -> field.label to element.formattedValues[index] } .toMap(), id = createId() ) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt index 86c09c48a..a559b06ef 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt @@ -29,6 +29,8 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material3.Card import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider @@ -40,6 +42,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -117,19 +124,23 @@ internal fun UtilityAssociationFilter( * @param onItemClick A callback that is called when an association is clicked. * @param onDetailsClick A callback that is called when the details icon is clicked. * @param modifier The [Modifier] to apply to this layout. + * @param displayCount The number of associations to display. */ @Composable internal fun UtilityAssociations( groupResult: UtilityAssociationGroupResult, onItemClick: (Int) -> Unit, onDetailsClick: (Int) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + displayCount: Int = 3 ) { + val associationResults = groupResult.associationResults + var showAll by rememberSaveable { mutableStateOf(false) } + val itemsToShow = if (showAll) associationResults else associationResults.take(displayCount) val lazyListState = rememberLazyListState() + Surface( - modifier = modifier.wrapContentHeight( - align = Alignment.Top - ), + modifier = modifier.wrapContentHeight(align = Alignment.Top), shape = RoundedCornerShape(15.dp), color = MaterialTheme.colorScheme.surfaceContainer ) { @@ -137,21 +148,17 @@ internal fun UtilityAssociations( modifier = Modifier.clip(shape = RoundedCornerShape(15.dp)), state = lazyListState ) { - groupResult.associationResults.forEachIndexed { index, info -> + itemsToShow.forEachIndexed { index, info -> item(info.association.hashCode()) { AssociationItem( title = info.title, association = info.association, associatedFeature = info.associatedFeature, - onClick = { - onItemClick(index) - }, - onDetailsClick = { - onDetailsClick(index) - }, + onClick = { onItemClick(index) }, + onDetailsClick = { onDetailsClick(index) }, modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) ) - if (index < groupResult.associationResults.count() - 1) { + if (index < itemsToShow.size - 1) { HorizontalDivider( modifier = Modifier.padding(horizontal = 16.dp), color = DividerDefaults.color.copy(alpha = 0.7f) @@ -159,6 +166,37 @@ internal fun UtilityAssociations( } } } + if (!showAll && associationResults.size > displayCount) { + item("show_all") { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { showAll = true } + .padding(vertical = 12.dp, horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.show_all), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "Total: ${associationResults.size}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.List, + contentDescription = "List bullet", + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + } + } } } } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt index 68c029fde..9c73ac0ca 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt @@ -39,6 +39,7 @@ internal class UtilityAssociationsElementState( override val id : Int = element.hashCode() val label : String = element.title val description: String = element.description + val displayCount: Int = element.displayCount private var _loading: MutableState = mutableStateOf(true) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt index b0207b4a6..8259430bf 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt @@ -70,6 +70,7 @@ internal fun UNAssociationsScreen( }, modifier = modifier .padding(16.dp) - .fillMaxSize() + .fillMaxSize(), + displayCount = utilityAssociationsElementState.displayCount ) } diff --git a/toolkit/popup/src/main/res/values/strings.xml b/toolkit/popup/src/main/res/values/strings.xml index c7398d77d..29a3863e2 100644 --- a/toolkit/popup/src/main/res/values/strings.xml +++ b/toolkit/popup/src/main/res/values/strings.xml @@ -42,4 +42,5 @@ Cancel None Selected Association Settings + Show all From 857f62cf512aeb15dcb511c84961d8a11ca92260 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 17 Sep 2025 18:58:01 -0700 Subject: [PATCH 25/36] Optimize imports optimize mediaElementState --- .../arcgismaps/toolkit/popup/PopupState.kt | 19 ++++++++++--------- .../element/media/MediaElementState.kt | 18 +++++++++--------- .../element/media/MediaPopupElement.kt | 10 +++++----- .../UtilityAssociationDetails.kt | 9 --------- .../UtilityAssociations.kt | 1 - .../popup/internal/navigation/PopupNavHost.kt | 2 +- .../popup/internal/screens/PopupScreen.kt | 5 ----- 7 files changed, 25 insertions(+), 39 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 71ca267c1..c8e3a07a5 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination.Companion.hasRoute @@ -278,7 +279,7 @@ public class PopupState private constructor(internal val popup: Popup) { is MediaPopupElement -> { states.add( element, - MediaElementState(element = element, popup = popup) + MediaElementState(element = element, popup = popup, scope = coroutineScope) ) } @@ -312,21 +313,21 @@ public class PopupState private constructor(internal val popup: Popup) { internal data class PopupStateData( val popup: Popup, ) { - internal lateinit var stateCollection: PopupElementStateCollection + lateinit var stateCollection: PopupElementStateCollection private set /** * Indicates if the evaluateExpression function for the [popup] has been run. */ - internal var initialEvaluation : MutableState = mutableStateOf(false) + var initialEvaluation: Boolean by mutableStateOf(false) private set /** * Indicates if the expressions for the [popup] are currently being evaluated. */ - internal var isEvaluatingExpressions: MutableState = mutableStateOf(false) + var isEvaluatingExpressions: Boolean by mutableStateOf(false) private set - internal fun setStates(stateCollection: PopupElementStateCollection) { + fun setStates(stateCollection: PopupElementStateCollection) { this.stateCollection = stateCollection } @@ -335,9 +336,9 @@ internal data class PopupStateData( * evaluation, the [initialEvaluation] is set to true. While this function is running, the * [isEvaluatingExpressions] will be true. */ - internal suspend fun evaluateExpressionsAndGetAttachments() : List { + suspend fun evaluateExpressionsAndGetAttachments() : List { try { - isEvaluatingExpressions.value = true + isEvaluatingExpressions = true val attachments = mutableListOf() popup.evaluateExpressions().onSuccess { val element = popup.evaluatedElements @@ -348,11 +349,11 @@ internal data class PopupStateData( attachments.addAll(element.attachments) } // Set the initial evaluation to true after the first successful evaluation. - initialEvaluation.value = true + initialEvaluation = true } return attachments } finally { - isEvaluatingExpressions.value = false + isEvaluatingExpressions = false } } } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt index 375906030..f6ed991dd 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt @@ -21,7 +21,9 @@ import android.graphics.drawable.BitmapDrawable import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import coil.imageLoader import coil.request.ImageRequest import com.arcgismaps.mapping.ChartImageParameters @@ -47,28 +49,26 @@ import java.util.UUID internal class MediaElementState( val element: MediaPopupElement, val popup: Popup, + val scope: CoroutineScope, override val id : Int = createId() ) : PopupElementState() { - lateinit var description: String - lateinit var title: String + val description: String = element.description + val title: String = element.title lateinit var media: List /** - * Indicates if the evaluateExpression function for the [popup] has been run. + * Indicates if the media list for the [popup] has been created. */ - internal var isInitialized : MutableState = mutableStateOf(false) + internal var isPopupMediaCreated: Boolean by mutableStateOf(false) private set - fun init( - scope: CoroutineScope, + fun createPopupMedia( mediaFolder: String, chartParams: ChartImageParameters, context: Context, models: List = listOf() ) { - this.description = element.description - this.title = element.title this.media = element.media.mapIndexed { index, media -> val model = models.getOrNull(index) ?: "" if (media.type.isChart) { @@ -84,7 +84,7 @@ internal class MediaElementState( // update chart providers to use the new instances of PopupMedia to reacquire updated charts. updateMediaElement(element, scope) } - isInitialized.value = true + isPopupMediaCreated = true } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt index 12654f42a..ba9b091fc 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight @@ -42,19 +41,20 @@ internal fun MediaPopupElement( state: MediaElementState, onClickedMedia: (ViewableFile) -> Unit ) { - val scope = rememberCoroutineScope() val mediaFolder = MediaElementDefaults.mediaFolder val chartParams = MediaElementDefaults.chartParams val context = LocalContext.current LaunchedEffect(state) { - state.init( - scope = scope, + if (state.isPopupMediaCreated) { + return@LaunchedEffect + } + state.createPopupMedia( mediaFolder = mediaFolder, chartParams = chartParams, context = context ) } - if (state.isInitialized.value) { + if (state.isPopupMediaCreated) { MediaPopupElement( title = state.title, description = state.description, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt index 2eeaca687..d9ecd8491 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt @@ -19,22 +19,13 @@ package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton 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.res.stringResource diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt index a559b06ef..092b63222 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt @@ -44,7 +44,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 7e6009d07..645a77589 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -59,7 +59,7 @@ internal fun PopupNavHost( PopupScreen( state, popupStateData, - popupStateData.initialEvaluation.value, + popupStateData.initialEvaluation, refreshed, onUtilityFilterSelected = { state -> val newRoute = NavigationRoute.UNFilterView(stateId = state.id) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt index 162a86bee..ad8ef6502 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/PopupScreen.kt @@ -18,17 +18,12 @@ package com.arcgismaps.toolkit.popup.internal.screens import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf From 10ecb16ff71dfdb9f69cc8d3baabaa70547af61a Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Thu, 18 Sep 2025 11:10:35 -0700 Subject: [PATCH 26/36] update logic in mediaPopupElement composable --- .../element/media/MediaPopupElement.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt index ba9b091fc..8d0cbc459 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaPopupElement.kt @@ -44,17 +44,15 @@ internal fun MediaPopupElement( val mediaFolder = MediaElementDefaults.mediaFolder val chartParams = MediaElementDefaults.chartParams val context = LocalContext.current - LaunchedEffect(state) { - if (state.isPopupMediaCreated) { - return@LaunchedEffect + if (!state.isPopupMediaCreated) { + LaunchedEffect(state) { + state.createPopupMedia( + mediaFolder = mediaFolder, + chartParams = chartParams, + context = context + ) } - state.createPopupMedia( - mediaFolder = mediaFolder, - chartParams = chartParams, - context = context - ) - } - if (state.isPopupMediaCreated) { + } else { MediaPopupElement( title = state.title, description = state.description, From 35a051ac30cc6ba34cd9304a6f68a3d0fa18bbaf Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Fri, 19 Sep 2025 15:20:25 -0700 Subject: [PATCH 27/36] enable close button add doc --- .../toolkit/popupapp/screens/mapscreen/MainScreen.kt | 6 +++++- .../main/java/com/arcgismaps/toolkit/popup/PopupState.kt | 7 +++++++ .../popup/internal/element/media/MediaElementState.kt | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index b383f73b4..33210ed83 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -67,7 +67,11 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { BottomSheetScaffold( sheetContent = { if (viewModel.popupState != null) { - Popup(viewModel.popupState!!, Modifier.animateContentSize(), showCloseIcon = false) + Popup( + viewModel.popupState!!, + Modifier.animateContentSize(), + onDismiss = { scope.launch { scaffoldState.bottomSheetState.partialExpand() } } + ) } }, modifier = Modifier.fillMaxSize(), diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index c8e3a07a5..8dfacb499 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -115,6 +115,13 @@ public class PopupState private constructor(internal val popup: Popup) { store.addLast(popupStateData) } + /** + * Represents the state of the Popup. + * + * @param popup the [Popup] to create the state for. + * @param scope a [CoroutineScope] to use for asynchronous operations. + * @since 300.0.0 + */ public constructor(popup: Popup, scope: CoroutineScope) : this(popup) { this.coroutineScope = scope initializePopupStateData(popup) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt index f6ed991dd..edd8fdcc6 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/media/MediaElementState.kt @@ -55,7 +55,7 @@ internal class MediaElementState( val description: String = element.description val title: String = element.title - lateinit var media: List + var media: List = emptyList() /** * Indicates if the media list for the [popup] has been created. From d5e75ae61ad96f7987bb520625da22b01158c3bc Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 22 Sep 2025 10:31:28 -0700 Subject: [PATCH 28/36] add search box to filter UtilityAssociationGroupResults with a user specified string --- .../UtilityAssociations.kt | 139 +++++++++++------- toolkit/popup/src/main/res/values/strings.xml | 1 + 2 files changed, 90 insertions(+), 50 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt index 092b63222..1adbec203 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -31,14 +32,18 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Card import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -135,64 +140,98 @@ internal fun UtilityAssociations( ) { val associationResults = groupResult.associationResults var showAll by rememberSaveable { mutableStateOf(false) } - val itemsToShow = if (showAll) associationResults else associationResults.take(displayCount) + var searchQuery by rememberSaveable { mutableStateOf("") } + val filteredResults = associationResults.filter { + it.title.contains(searchQuery, ignoreCase = true) + } + val itemsToShow = if (showAll) filteredResults else filteredResults.take(displayCount) val lazyListState = rememberLazyListState() - Surface( - modifier = modifier.wrapContentHeight(align = Alignment.Top), - shape = RoundedCornerShape(15.dp), - color = MaterialTheme.colorScheme.surfaceContainer - ) { - LazyColumn( - modifier = Modifier.clip(shape = RoundedCornerShape(15.dp)), - state = lazyListState - ) { - itemsToShow.forEachIndexed { index, info -> - item(info.association.hashCode()) { - AssociationItem( - title = info.title, - association = info.association, - associatedFeature = info.associatedFeature, - onClick = { onItemClick(index) }, - onDetailsClick = { onDetailsClick(index) }, - modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) - ) - if (index < itemsToShow.size - 1) { - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = DividerDefaults.color.copy(alpha = 0.7f) + Column(modifier = modifier) { + // Search bar + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier + .fillMaxWidth(), + placeholder = { Text(stringResource(R.string.filter_by_feature_title)) }, + singleLine = true, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = "Search" + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Clear search" ) } } } - if (!showAll && associationResults.size > displayCount) { - item("show_all") { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { showAll = true } - .padding(vertical = 12.dp, horizontal = 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(R.string.show_all), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = "Total: ${associationResults.size}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + Spacer(modifier = Modifier.height(14.dp)) + Surface( + modifier = Modifier.wrapContentHeight(align = Alignment.Top), + shape = RoundedCornerShape(15.dp), + color = MaterialTheme.colorScheme.surfaceContainer + ) { + Column { + LazyColumn( + modifier = Modifier.clip(shape = RoundedCornerShape(15.dp)), + state = lazyListState + ) { + itemsToShow.forEachIndexed { index, info -> + item(info.association.hashCode()) { + AssociationItem( + title = info.title, + association = info.association, + associatedFeature = info.associatedFeature, + onClick = { onItemClick(index) }, + onDetailsClick = { onDetailsClick(index) }, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) ) + if (index < itemsToShow.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = DividerDefaults.color.copy(alpha = 0.7f) + ) + } + } + } + if (!showAll && filteredResults.size > displayCount) { + item("show_all") { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { showAll = true } + .padding(vertical = 12.dp, horizontal = 24.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.show_all), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "Total: ${filteredResults.size}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.List, + contentDescription = "List bullet", + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + } } - Icon( - imageVector = Icons.AutoMirrored.Filled.List, - contentDescription = "List bullet", - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) } } } diff --git a/toolkit/popup/src/main/res/values/strings.xml b/toolkit/popup/src/main/res/values/strings.xml index 29a3863e2..e808338b6 100644 --- a/toolkit/popup/src/main/res/values/strings.xml +++ b/toolkit/popup/src/main/res/values/strings.xml @@ -43,4 +43,5 @@ None Selected Association Settings Show all + Filter by feature title From 74c30479fc59a4017e9ff756c52ba9894e99e9a6 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 22 Sep 2025 12:22:18 -0700 Subject: [PATCH 29/36] update showall behavior on search update AssociationItem properties to display with the latest on FeatureBranch --- .../UtilityAssociations.kt | 21 +++++++++++-------- toolkit/popup/src/main/res/values/strings.xml | 1 + 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt index 1adbec203..389b41add 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt @@ -151,7 +151,10 @@ internal fun UtilityAssociations( // Search bar OutlinedTextField( value = searchQuery, - onValueChange = { searchQuery = it }, + onValueChange = { newValue -> + searchQuery = newValue + showAll = true + }, modifier = Modifier .fillMaxWidth(), placeholder = { Text(stringResource(R.string.filter_by_feature_title)) }, @@ -270,9 +273,10 @@ private fun AssociationItem( var supportingText = "" when (association.associationType) { is UtilityAssociationType.JunctionEdgeObjectConnectivityMidspan -> { - trailingText = "${(association.fractionAlongEdge * 100).toInt()}%" - target.terminal?.let { terminal -> - supportingText = terminal.name + supportingText = if (target.terminal != null) { + "${target.terminal?.name}, ${(association.fractionAlongEdge * 100).toInt()}%" + } else { + "${(association.fractionAlongEdge * 100).toInt()}%" } } @@ -286,11 +290,10 @@ private fun AssociationItem( is UtilityAssociationType.Containment -> { if (associatedFeature.globalId == association.toElement.globalId) { - supportingText = if (association.isContainmentVisible) { - stringResource(R.string.visible_content) - } else { - stringResource(R.string.content) - } + supportingText = stringResource( + R.string.containment_visible_value, + association.isContainmentVisible + ) } } diff --git a/toolkit/popup/src/main/res/values/strings.xml b/toolkit/popup/src/main/res/values/strings.xml index e808338b6..2a5a8b05c 100644 --- a/toolkit/popup/src/main/res/values/strings.xml +++ b/toolkit/popup/src/main/res/values/strings.xml @@ -44,4 +44,5 @@ Association Settings Show all Filter by feature title + Visible: %1$s From 76ec0334a19f2677804dac8b19a56042fca8eaa4 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 22 Sep 2025 14:48:08 -0700 Subject: [PATCH 30/36] Add fix for navigation not working Change bottomsheet behavior for onDismiss --- .../popupapp/screens/mapscreen/MainScreen.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index 33210ed83..124db0bca 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -22,14 +22,18 @@ package com.arcgismaps.toolkit.popupapp.screens.mapscreen import android.widget.Toast import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SheetValue import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -70,11 +74,17 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { Popup( viewModel.popupState!!, Modifier.animateContentSize(), - onDismiss = { scope.launch { scaffoldState.bottomSheetState.partialExpand() } } + onDismiss = { scope.launch { + viewModel.updatePopupState(null) + unselectFeature(viewModel.geoElement, viewModel.layer) + scaffoldState.bottomSheetState.hide() + } } ) } }, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(WindowInsets.safeDrawing.asPaddingValues()), scaffoldState = scaffoldState, sheetSwipeEnabled = true, topBar = null From c062f866a78b33981481c5478dffd4fb4f97caf2 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Mon, 22 Sep 2025 15:54:39 -0700 Subject: [PATCH 31/36] Add default value for UtilityElement title --- .../utilityassociationselement/UtilityAssociationsElement.kt | 2 +- toolkit/popup/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt index 16d187dc6..4d197e62b 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElement.kt @@ -124,7 +124,7 @@ private fun ElementHeader( modifier = Modifier.fillMaxWidth() ) { Text( - text = label, + text = label.ifEmpty { stringResource(R.string.associations) }, style = MaterialTheme.typography.titleMedium ) if (description.isNotEmpty()) { diff --git a/toolkit/popup/src/main/res/values/strings.xml b/toolkit/popup/src/main/res/values/strings.xml index 2a5a8b05c..af379b5e4 100644 --- a/toolkit/popup/src/main/res/values/strings.xml +++ b/toolkit/popup/src/main/res/values/strings.xml @@ -45,4 +45,5 @@ Show all Filter by feature title Visible: %1$s + Associations From b1dc7e7fac835dfdf8ee1b32e0d96560a521cb07 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Tue, 23 Sep 2025 10:31:01 -0700 Subject: [PATCH 32/36] reset geoElement when the bottomsheet is dismissed --- .../toolkit/popupapp/screens/mapscreen/MainScreen.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index 124db0bca..63a0cdf75 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -68,6 +68,14 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { skipHiddenState = false ) ) + LaunchedEffect(scaffoldState.bottomSheetState.currentValue) { + if (scaffoldState.bottomSheetState.currentValue == SheetValue.Hidden) { + unselectFeature(viewModel.geoElement, viewModel.layer) + viewModel.updatePopupState(null) + viewModel.setGeoElement(null) + } + } + BottomSheetScaffold( sheetContent = { if (viewModel.popupState != null) { @@ -77,6 +85,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { onDismiss = { scope.launch { viewModel.updatePopupState(null) unselectFeature(viewModel.geoElement, viewModel.layer) + viewModel.setGeoElement(null) scaffoldState.bottomSheetState.hide() } } ) From c4783374f413fb6b776ca70b9605751fbf9c0a61 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Tue, 23 Sep 2025 15:46:55 -0700 Subject: [PATCH 33/36] Pick up refactor changes from feature forms Update API file --- toolkit/popup/api/popup.api | 6 +- toolkit/popup/build.gradle.kts | 3 +- ...ns.kt => UtilityAssociationGroupResult.kt} | 61 +------------ .../UtilityAssociationsFilterResult.kt | 88 +++++++++++++++++++ .../internal/navigation/NavigationRoute.kt | 8 +- .../popup/internal/navigation/PopupNavHost.kt | 8 +- ...n.kt => UNAssociationGroupResultScreen.kt} | 10 +-- ...kt => UNAssociationsFilterResultScreen.kt} | 6 +- 8 files changed, 110 insertions(+), 80 deletions(-) rename toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/{UtilityAssociations.kt => UtilityAssociationGroupResult.kt} (85%) create mode 100644 toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsFilterResult.kt rename toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/{UNAssociationsScreen.kt => UNAssociationGroupResultScreen.kt} (91%) rename toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/{UNAssociationsFilterScreen.kt => UNAssociationsFilterResultScreen.kt} (95%) diff --git a/toolkit/popup/api/popup.api b/toolkit/popup/api/popup.api index 36eda366a..b18d53895 100644 --- a/toolkit/popup/api/popup.api +++ b/toolkit/popup/api/popup.api @@ -1,13 +1,11 @@ public final class com/arcgismaps/toolkit/popup/PopupKt { - public static final synthetic fun Popup (Lcom/arcgismaps/mapping/popup/Popup;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V - public static final fun Popup (Lcom/arcgismaps/toolkit/popup/PopupState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;ZZLandroidx/compose/runtime/Composer;II)V + public static final fun Popup (Lcom/arcgismaps/mapping/popup/Popup;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun Popup (Lcom/arcgismaps/toolkit/popup/PopupState;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function0;ZLandroidx/compose/runtime/Composer;II)V } public final class com/arcgismaps/toolkit/popup/PopupState { public static final field $stable I - public fun (Lcom/arcgismaps/mapping/popup/Popup;)V public fun (Lcom/arcgismaps/mapping/popup/Popup;Lkotlinx/coroutines/CoroutineScope;)V public final fun getActivePopup ()Lcom/arcgismaps/mapping/popup/Popup; - public final fun getPopup ()Lcom/arcgismaps/mapping/popup/Popup; } diff --git a/toolkit/popup/build.gradle.kts b/toolkit/popup/build.gradle.kts index 751b26951..71722bcaf 100644 --- a/toolkit/popup/build.gradle.kts +++ b/toolkit/popup/build.gradle.kts @@ -91,7 +91,8 @@ apiValidation { "com.arcgismaps.toolkit.popup.internal.ui.fileviewer.ComposableSingletons\$FileViewerKt", "com.arcgismaps.toolkit.popup.internal.ui.expandablecard.ComposableSingletons\$ExpandableCardKt", "com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.ComposableSingletons\$UtilityAssociationDetailsKt", - "com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.ComposableSingletons\$UtilityAssociationsKt", + "com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.ComposableSingletons\$UtilityAssociationsFilterResultKt", + "com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.ComposableSingletons\$UtilityAssociationGroupResultKt", "com.arcgismaps.toolkit.popup.internal.screens.ComposableSingletons\$ContentAwareTopBarKt" ) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationGroupResult.kt similarity index 85% rename from toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt rename to toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationGroupResult.kt index 389b41add..cd2fc2ab4 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociations.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationGroupResult.kt @@ -39,8 +39,6 @@ import androidx.compose.material3.DividerDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField @@ -65,64 +63,11 @@ import com.arcgismaps.toolkit.popup.R import com.arcgismaps.utilitynetworks.UtilityAssociation import com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult import com.arcgismaps.utilitynetworks.UtilityAssociationType -import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult import com.arcgismaps.utilitynetworks.UtilityElement /** - * Displays the provided [UtilityAssociationsFilterResult]. The filter result is displayed as a - * list of its groups as given by [UtilityAssociationsFilterResult.groupResults]. - * - * @param groupResults The [UtilityAssociationsFilterResult] to display. - * @param onGroupClick A callback that is called when a group is clicked with the index of the group. - * @param modifier The [Modifier] to apply to this layout. - */ -@Composable -internal fun UtilityAssociationFilter( - groupResults: List, - onGroupClick: (UtilityAssociationGroupResult) -> Unit, - modifier: Modifier = Modifier -) { - // show the list of layers - Surface( - modifier = modifier, - shape = RoundedCornerShape(15.dp) - ) { - LazyColumn(modifier = Modifier) { - groupResults.forEachIndexed { index, group -> - item { - ListItem( - headlineContent = { - Text(text = group.name, modifier = Modifier.padding(start = 16.dp)) - }, - trailingContent = { - Text( - text = "${group.associationResults.count()}", - modifier = Modifier.padding(end = 16.dp) - ) - }, - modifier = Modifier.clickable { - onGroupClick(group) - }, - colors = ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.surfaceContainer - ) - ) - if (index < groupResults.count() - 1) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceContainer - ) { - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) - } - } - } - } - } - } -} - -/** - * Displays the provided list of associations that are part of the [UtilityAssociationGroupResult]. + * Displays the provided list of associations that are part of the + * [com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult]. * * @param groupResult The [UtilityAssociationGroupResult] to display. * @param onItemClick A callback that is called when an association is clicked. @@ -131,7 +76,7 @@ internal fun UtilityAssociationFilter( * @param displayCount The number of associations to display. */ @Composable -internal fun UtilityAssociations( +internal fun UtilityAssociationGroupResult( groupResult: UtilityAssociationGroupResult, onItemClick: (Int) -> Unit, onDetailsClick: (Int) -> Unit, diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsFilterResult.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsFilterResult.kt new file mode 100644 index 000000000..ebd572585 --- /dev/null +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsFilterResult.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Esri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.arcgismaps.utilitynetworks.UtilityAssociationGroupResult +import com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult + +/** + * Displays the provided [com.arcgismaps.utilitynetworks.UtilityAssociationsFilterResult]. + * The filter result is displayed as a list of its groups as given by + * [UtilityAssociationsFilterResult.groupResults]. + * + * @param groupResults The [UtilityAssociationsFilterResult] to display. + * @param onGroupClick A callback that is called when a group is clicked with the index of the group. + * @param modifier The [Modifier] to apply to this layout. + */ +@Composable +internal fun UtilityAssociationsFilterResult( + groupResults: List, + onGroupClick: (UtilityAssociationGroupResult) -> Unit, + modifier: Modifier = Modifier +) { + // show the list of layers + Surface( + modifier = modifier, + shape = RoundedCornerShape(15.dp) + ) { + LazyColumn(modifier = Modifier) { + groupResults.forEachIndexed { index, group -> + item { + ListItem( + headlineContent = { + Text(text = group.name, modifier = Modifier.padding(start = 16.dp)) + }, + trailingContent = { + Text( + text = "${group.associationResults.count()}", + modifier = Modifier.padding(end = 16.dp) + ) + }, + modifier = Modifier.clickable { + onGroupClick(group) + }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) + if (index < groupResults.count() - 1) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainer + ) { + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp)) + } + } + } + } + } + } +} diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt index 022e14116..f1eeed8a9 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/NavigationRoute.kt @@ -18,8 +18,8 @@ package com.arcgismaps.toolkit.popup.internal.navigation import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.screens.PopupScreen -import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen -import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterResultScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationGroupResultScreen import kotlinx.serialization.Serializable /** @@ -35,7 +35,7 @@ internal sealed class NavigationRoute { data object PopupView : NavigationRoute() /** - * Represents a view for the [UNAssociationsFilterScreen]. + * Represents a view for the [UNAssociationsFilterResultScreen]. * * @param stateId The state ID of the [UtilityAssociationsElementState] which contains the * selected filter. @@ -46,7 +46,7 @@ internal sealed class NavigationRoute { ) : NavigationRoute() /** - * Represents a view for the [UNAssociationsScreen]. + * Represents a view for the [UNAssociationGroupResultScreen]. * * @param stateId The state ID of the [UtilityAssociationsElementState] which contains the * selected group of associations. diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index 645a77589..d75375e32 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -35,8 +35,8 @@ import com.arcgismaps.toolkit.popup.PopupState import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationDetails import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.screens.PopupScreen -import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterScreen -import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationsFilterResultScreen +import com.arcgismaps.toolkit.popup.internal.screens.UNAssociationGroupResultScreen @Composable internal fun PopupNavHost( @@ -77,7 +77,7 @@ internal fun PopupNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } - UNAssociationsFilterScreen( + UNAssociationsFilterResultScreen( popupStateData = popupStateData, route = route, onGroupSelected = { stateId -> @@ -91,7 +91,7 @@ internal fun PopupNavHost( composable { backStackEntry -> val route = backStackEntry.toRoute() val popupStateData = remember(backStackEntry) { state.getActivePopupStateData() } - UNAssociationsScreen( + UNAssociationGroupResultScreen( popupStateData = popupStateData, route = route, onNavigateToFeature = { feature -> diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationGroupResultScreen.kt similarity index 91% rename from toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt rename to toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationGroupResultScreen.kt index 8259430bf..d8790efb9 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationGroupResultScreen.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.toolkit.popup.PopupStateData -import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociations +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationGroupResult import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute @@ -37,7 +37,7 @@ import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute * @param modifier The modifier to be applied to the layout. */ @Composable -internal fun UNAssociationsScreen( +internal fun UNAssociationGroupResultScreen( popupStateData: PopupStateData, route: NavigationRoute.UNAssociationsView, onNavigateToFeature: (ArcGISFeature) -> Unit, @@ -48,15 +48,13 @@ internal fun UNAssociationsScreen( // Get the selected UtilityAssociationsElementState from the state collection val utilityAssociationsElementState = states[route.stateId] as? UtilityAssociationsElementState ?: return - // Get the selected filter from the UtilityAssociationsElementState - val filterResult = utilityAssociationsElementState.selectedFilterResult // Get the selected group from the filter val groupResult = utilityAssociationsElementState.selectedGroupResult - if (filterResult == null || groupResult == null) { + if (groupResult == null) { // guard against null values return } - UtilityAssociations( + UtilityAssociationGroupResult( groupResult = groupResult, onItemClick = { index -> val feature = groupResult.associationResults[index].associatedFeature diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterResultScreen.kt similarity index 95% rename from toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt rename to toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterResultScreen.kt index e1f9a3148..39c5187b4 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterScreen.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/screens/UNAssociationsFilterResultScreen.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.arcgismaps.mapping.popup.UtilityAssociationsPopupElement -import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationFilter +import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsFilterResult import com.arcgismaps.toolkit.popup.internal.element.utilityassociationselement.UtilityAssociationsElementState import com.arcgismaps.toolkit.popup.internal.navigation.NavigationRoute import com.arcgismaps.toolkit.popup.PopupStateData @@ -38,7 +38,7 @@ import com.arcgismaps.toolkit.popup.PopupStateData * @param modifier The modifier to be applied to the layout. */ @Composable -internal fun UNAssociationsFilterScreen( +internal fun UNAssociationsFilterResultScreen( popupStateData: PopupStateData, route : NavigationRoute.UNFilterView, onGroupSelected: (Int) -> Unit, @@ -57,7 +57,7 @@ internal fun UNAssociationsFilterScreen( modifier = modifier, verticalArrangement = Arrangement.Top ) { - UtilityAssociationFilter( + UtilityAssociationsFilterResult( groupResults = filterResult.groupResults, onGroupClick = { groupResult -> utilityAssociationsElementState.setSelectedGroupResult(groupResult) From c5822535b3e68abd78a068278bcb30063d4d9001 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Tue, 23 Sep 2025 15:47:35 -0700 Subject: [PATCH 34/36] update micro app --- .../popupapp/screens/mapscreen/MainScreen.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index 63a0cdf75..be8abb140 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -70,9 +70,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { ) LaunchedEffect(scaffoldState.bottomSheetState.currentValue) { if (scaffoldState.bottomSheetState.currentValue == SheetValue.Hidden) { - unselectFeature(viewModel.geoElement, viewModel.layer) - viewModel.updatePopupState(null) - viewModel.setGeoElement(null) + resetSelection(viewModel) } } @@ -83,9 +81,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { viewModel.popupState!!, Modifier.animateContentSize(), onDismiss = { scope.launch { - viewModel.updatePopupState(null) - unselectFeature(viewModel.geoElement, viewModel.layer) - viewModel.setGeoElement(null) + resetSelection(viewModel) scaffoldState.bottomSheetState.hide() } } ) @@ -209,6 +205,12 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { } } +private fun resetSelection(viewModel: MapViewModel) { + viewModel.updatePopupState(null) + unselectFeature(viewModel.geoElement, viewModel.layer) + viewModel.setGeoElement(null) +} + private fun GeoElement?.sameSelection(other: GeoElement): Boolean = if (this == null) { false From b32827258f71bf1d7607b35dc5c4874e9db2646e Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 24 Sep 2025 08:54:57 -0700 Subject: [PATCH 35/36] address code review comments --- .../popupapp/screens/mapscreen/MainScreen.kt | 5 +++-- .../com/arcgismaps/toolkit/popup/PopupState.kt | 3 +-- .../UtilityAssociationDetails.kt | 11 ++++++----- .../UtilityAssociationsElementState.kt | 6 +++--- .../popup/internal/navigation/PopupNavHost.kt | 17 ++++++++++++----- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index be8abb140..8af351467 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -76,9 +76,10 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { BottomSheetScaffold( sheetContent = { - if (viewModel.popupState != null) { + val state = viewModel.popupState + if (state != null) { Popup( - viewModel.popupState!!, + state, Modifier.animateContentSize(), onDismiss = { scope.launch { resetSelection(viewModel) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt index 8dfacb499..b41c5cab6 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/PopupState.kt @@ -298,8 +298,7 @@ public class PopupState private constructor(internal val popup: Popup) { } else -> { - // TODO remove for release - println("encountered element of type ${element::class.java}") + // Unsupported element type. } } } diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt index d9ecd8491..aec954855 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationDetails.kt @@ -33,21 +33,22 @@ import androidx.compose.ui.unit.dp import com.arcgismaps.toolkit.popup.R import com.arcgismaps.utilitynetworks.UtilityAssociationResult import com.arcgismaps.utilitynetworks.UtilityAssociationType +import com.arcgismaps.utilitynetworks.UtilityAssociationsFilter import com.arcgismaps.utilitynetworks.UtilityNetworkSourceType /** * A composable that displays the details of a [UtilityAssociationResult]. * - * @param state The [UtilityAssociationsElementState] of the element. + * @param associationResult The [UtilityAssociationResult] to display details for. + * @param filter The [UtilityAssociationsFilter] used to obtain the association result. * @param modifier The [Modifier] to apply to this layout. */ @Composable internal fun UtilityAssociationDetails( - state: UtilityAssociationsElementState, + associationResult: UtilityAssociationResult, + filter: UtilityAssociationsFilter, modifier: Modifier = Modifier ) { - val associationResult = state.selectedAssociationResult ?: return - val filter = state.selectedFilterResult?.filter ?: return val association = associationResult.association Column( modifier = modifier, @@ -116,7 +117,7 @@ internal fun UtilityAssociationDetails( } associationResult.getFractionAlongEdge()?.let { fraction -> FractionAlongEdgeControl( - fraction = associationResult.getFractionAlongEdge()!!.toFloat(), + fraction = fraction.toFloat(), enabled = false, onValueChanged = {}, modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp, bottom = 24.dp) diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt index 9c73ac0ca..a721f6c7a 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/element/utilityassociationselement/UtilityAssociationsElementState.kt @@ -43,9 +43,6 @@ internal class UtilityAssociationsElementState( private var _loading: MutableState = mutableStateOf(true) - private var _filters: MutableState> = - mutableStateOf(emptyList()) - /** * Indicates if the state is loading data to fetch the filters [filters]. * @@ -54,6 +51,9 @@ internal class UtilityAssociationsElementState( val loading: Boolean get() = _loading.value + private var _filters: MutableState> = + mutableStateOf(emptyList()) + /** * The list of [UtilityAssociationsFilterResult] to display. This is empty until the data is fetched * as part of the initialization. diff --git a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt index d75375e32..36f03df31 100644 --- a/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt +++ b/toolkit/popup/src/main/java/com/arcgismaps/toolkit/popup/internal/navigation/PopupNavHost.kt @@ -119,11 +119,18 @@ internal fun PopupNavHost( val utilityAssociationsElementState = popupStateData.stateCollection[route.stateId] // guard against null value as? UtilityAssociationsElementState ?: return@composable - // Display the association details - UtilityAssociationDetails( - state = utilityAssociationsElementState, - modifier = Modifier.fillMaxSize() - ) + + val associationResult = utilityAssociationsElementState.selectedAssociationResult + val filter = utilityAssociationsElementState.selectedFilterResult?.filter + + if (associationResult != null && filter != null) { + // Display the association details + UtilityAssociationDetails( + associationResult = associationResult, + filter = filter, + modifier = Modifier.fillMaxSize() + ) + } } } } From e8172f6f1e23e48758c67b98b3dd11441f51ae81 Mon Sep 17 00:00:00 2001 From: Puneet Prakash Date: Wed, 24 Sep 2025 13:20:55 -0700 Subject: [PATCH 36/36] FIx navigation issue --- .../toolkit/popupapp/screens/mapscreen/MainScreen.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt index 8af351467..4a2df3813 100644 --- a/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt +++ b/microapps/PopupApp/app/src/main/java/com/arcgismaps/toolkit/popupapp/screens/mapscreen/MainScreen.kt @@ -103,6 +103,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { .padding(padding) .fillMaxSize(), onSingleTapConfirmed = { + resetSelection(viewModel) scope.launch { viewModel.proxy.identifyLayers( screenCoordinate = it.screenCoordinate, @@ -110,10 +111,7 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { returnPopupsOnly = true ).onSuccess { results -> if (results.isEmpty()) { - unselectFeature(viewModel.geoElement, viewModel.layer) - viewModel.updatePopupState(null) viewModel.setLayer(null) - viewModel.setGeoElement(null) if (scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) { scaffoldState.bottomSheetState.hide() } @@ -154,11 +152,6 @@ fun MainScreen(viewModel: MapViewModel = viewModel()) { ).show() } } else { - unselectFeature( - viewModel.geoElement, - viewModel.layer - ) - when (newLayer) { is FeatureLayer -> { // the Popup exists on a FeatureLayer