Skip to content

Commit aadd1b6

Browse files
authored
reimplement feature-selection in compose (#6773)
1 parent 5cf584b commit aadd1b6

File tree

1,220 files changed

+1054
-920
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,220 files changed

+1054
-920
lines changed

app/build.gradle.kts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,13 @@ tasks.register<UpdatePresetsTask>("updatePresets") {
404404
group = "streetcomplete"
405405
version = presetsVersion
406406
languageCodes = bcp47ExportLanguages
407-
targetDir = "$projectDir/src/androidMain/assets/osmfeatures/default"
407+
targetDir = "$projectDir/src/commonMain/composeResources/files/osmfeatures/default"
408408
}
409409

410410
tasks.register<UpdateNsiPresetsTask>("updateNsiPresets") {
411411
group = "streetcomplete"
412412
version = nsiVersion
413-
targetDir = "$projectDir/src/androidMain/assets/osmfeatures/brands"
413+
targetDir = "$projectDir/src/commonMain/composeResources/files/osmfeatures/brands"
414414
}
415415

416416
// tasks.register<DownloadBrandLogosTask>("downloadBrandLogos") {
@@ -422,10 +422,10 @@ tasks.register<UpdateNsiPresetsTask>("updateNsiPresets") {
422422
tasks.register<DownloadAndConvertPresetIconsTask>("downloadAndConvertPresetIcons") {
423423
group = "streetcomplete"
424424
version = presetsVersion
425-
targetDir = "$projectDir/src/androidMain/res/drawable/"
425+
targetDir = "$projectDir/src/commonMain/composeResources/drawable/"
426426
iconSize = 34
427427
transformName = { "preset_" + it.replace('-', '_') }
428-
indexFile = "$projectDir/src/androidMain/kotlin/de/westnordost/streetcomplete/view/PresetIconIndex.kt"
428+
indexFile = "$projectDir/src/androidMain/kotlin/de/westnordost/streetcomplete/view/PresetIconIndex.kt" // necessary as long as map is not compose based yet
429429
}
430430

431431
tasks.register<UpdateAppTranslationsTask>("updateTranslations") {
@@ -476,12 +476,14 @@ tasks.register("copyDefaultStringsToEnStrings") {
476476
}
477477
}
478478

479+
// necessary as long as map hasn't been converted to compose yet
479480
val copySharedResToAndroid by tasks.registering(Copy::class) {
480481
val target = "build/generated/androidMain/res/drawable"
481482
from("src/commonMain/composeResources/drawable")
482483
into(target)
483484
include {
484485
it.name.startsWith("building_") ||
486+
it.name.startsWith("preset_") ||
485487
it.name == "sport_volleyball.xml" ||
486488
it.name == "religion_christian.xml" ||
487489
it.name == "religion_jewish.xml" ||

app/src/androidMain/kotlin/de/westnordost/streetcomplete/data/meta/MetadataModule.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ val metadataModule = module {
2020
}
2121
single<Lazy<FeatureDictionary>>(named("FeatureDictionaryLazy")) {
2222
lazy {
23-
FeatureDictionary.create(get<AssetManager>(), "osmfeatures/default", "osmfeatures/brands")
23+
val p = "composeResources/de.westnordost.streetcomplete.resources/files/"
24+
FeatureDictionary.create(get<AssetManager>(), p+"osmfeatures/default", p+"osmfeatures/brands")
2425
}
2526
}
2627
}

app/src/androidMain/kotlin/de/westnordost/streetcomplete/overlays/places/PlacesOverlayForm.kt

Lines changed: 111 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ package de.westnordost.streetcomplete.overlays.places
33
import android.os.Bundle
44
import android.view.View
55
import androidx.appcompat.app.AlertDialog
6+
import androidx.compose.foundation.layout.Arrangement
7+
import androidx.compose.foundation.layout.Box
68
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.defaultMinSize
11+
import androidx.compose.foundation.layout.fillMaxWidth
712
import androidx.compose.foundation.layout.padding
13+
import androidx.compose.foundation.layout.size
814
import androidx.compose.material.ContentAlpha
915
import androidx.compose.material.LocalContentColor
1016
import androidx.compose.material.LocalTextStyle
@@ -31,7 +37,7 @@ import de.westnordost.streetcomplete.data.osm.geometry.ElementGeometry
3137
import de.westnordost.streetcomplete.data.osm.mapdata.Element
3238
import de.westnordost.streetcomplete.data.osm.mapdata.ElementType
3339
import de.westnordost.streetcomplete.data.preferences.Preferences
34-
import de.westnordost.streetcomplete.databinding.FragmentOverlayPlacesBinding
40+
import de.westnordost.streetcomplete.databinding.ComposeViewBinding
3541
import de.westnordost.streetcomplete.osm.POPULAR_PLACE_FEATURE_IDS
3642
import de.westnordost.streetcomplete.osm.applyReplacePlaceTo
3743
import de.westnordost.streetcomplete.osm.applyTo
@@ -47,15 +53,17 @@ import de.westnordost.streetcomplete.overlays.AnswerItem
4753
import de.westnordost.streetcomplete.resources.Res
4854
import de.westnordost.streetcomplete.resources.name_label
4955
import de.westnordost.streetcomplete.resources.quest_placeName_no_name_answer
56+
import de.westnordost.streetcomplete.ui.common.feature.FeatureIcon
57+
import de.westnordost.streetcomplete.ui.common.feature.FeatureSelect
58+
import de.westnordost.streetcomplete.ui.common.last_picked.LastPickedChipsRow
5059
import de.westnordost.streetcomplete.ui.common.localized_name.LocalizedNamesForm
5160
import de.westnordost.streetcomplete.ui.util.content
5261
import de.westnordost.streetcomplete.ui.util.rememberSerializable
53-
import de.westnordost.streetcomplete.util.getLanguagesForFeatureDictionary
5462
import de.westnordost.streetcomplete.util.getLocationSpanned
5563
import de.westnordost.streetcomplete.util.ktx.geometryType
5664
import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
57-
import de.westnordost.streetcomplete.view.controller.FeatureViewController
58-
import de.westnordost.streetcomplete.view.dialogs.SearchFeaturesDialog
65+
import de.westnordost.streetcomplete.util.locale.getLanguagesForFeatureDictionary
66+
import de.westnordost.streetcomplete.util.takeFavorites
5967
import kotlinx.coroutines.launch
6068
import kotlinx.coroutines.suspendCancellableCoroutine
6169
import org.jetbrains.compose.resources.stringResource
@@ -64,19 +72,31 @@ import kotlin.coroutines.resume
6472

6573
class PlacesOverlayForm : AbstractOverlayForm() {
6674

67-
override val contentLayoutResId = R.layout.fragment_overlay_places
68-
private val binding by contentViewBinding(FragmentOverlayPlacesBinding::bind)
75+
override val contentLayoutResId = R.layout.compose_view
76+
private val binding by contentViewBinding(ComposeViewBinding::bind)
6977

7078
private val prefs: Preferences by inject()
7179

7280
private var originalFeature: Feature? = null
7381
private var originalNoName: Boolean = false
7482
private var originalNames: List<LocalizedName> = emptyList()
75-
83+
private var selectedFeature: MutableState<Feature?> = mutableStateOf(null)
7684
private var localizedNames: MutableState<List<LocalizedName>> = mutableStateOf(emptyList())
7785
private var isNoName: MutableState<Boolean> = mutableStateOf(false)
7886

79-
private lateinit var featureCtrl: FeatureViewController
87+
private val lastPickedFeatures: List<Feature> by lazy {
88+
val languages = getLanguagesForFeatureDictionary()
89+
prefs.getLastPicked<String>(this::class.simpleName!!)
90+
.takeFavorites(n = 5, first = 1)
91+
.mapNotNull { featureId ->
92+
featureDictionary.getById(
93+
id = featureId,
94+
languages = languages,
95+
country = countryOrSubdivisionCode,
96+
)
97+
}
98+
}
99+
80100

81101
private lateinit var vacantShopFeature: Feature
82102

@@ -88,10 +108,11 @@ class PlacesOverlayForm : AbstractOverlayForm() {
88108
override fun onCreate(savedInstanceState: Bundle?) {
89109
super.onCreate(savedInstanceState)
90110

91-
val languages = getLanguagesForFeatureDictionary(resources.configuration)
111+
val languages = getLanguagesForFeatureDictionary()
92112
vacantShopFeature = featureDictionary.getById("shop/vacant", languages)!!
93113
originalNames = parseLocalizedNames(element?.tags.orEmpty()).orEmpty()
94114
originalFeature = getOriginalFeature()
115+
selectedFeature.value = originalFeature
95116
originalNoName = element?.tags?.get("name:signed") == "no" || element?.tags?.get("noname") == "yes"
96117
}
97118

@@ -110,7 +131,7 @@ class PlacesOverlayForm : AbstractOverlayForm() {
110131
}
111132

112133
private fun getFeatureDictionaryFeature(element: Element): Feature? {
113-
val languages = getLanguagesForFeatureDictionary(resources.configuration)
134+
val languages = getLanguagesForFeatureDictionary()
114135
val geometryType = if (element.type == ElementType.NODE) null else element.geometryType
115136

116137
return featureDictionary.getByTags(
@@ -128,24 +149,6 @@ class PlacesOverlayForm : AbstractOverlayForm() {
128149
setTitleHintLabel(element?.tags?.let { getLocationSpanned(it, resources) })
129150
setMarkerIcon(R.drawable.quest_shop)
130151

131-
featureCtrl = FeatureViewController(featureDictionary, binding.featureTextView, binding.featureIconView)
132-
featureCtrl.countryOrSubdivisionCode = countryOrSubdivisionCode
133-
featureCtrl.feature = originalFeature
134-
135-
binding.featureView.setOnClickListener {
136-
SearchFeaturesDialog(
137-
requireContext(),
138-
featureDictionary,
139-
element?.geometryType ?: GeometryType.POINT,
140-
countryOrSubdivisionCode,
141-
featureCtrl.feature?.name,
142-
{ it.toElement().isPlace() || it.id == "shop/vacant" },
143-
::onSelectedFeature,
144-
POPULAR_PLACE_FEATURE_IDS,
145-
).show()
146-
}
147-
148-
149152
val selectableLanguages = (
150153
countryInfo.officialLanguages + countryInfo.additionalStreetsignLanguages
151154
).distinct().toMutableList()
@@ -156,66 +159,105 @@ class PlacesOverlayForm : AbstractOverlayForm() {
156159
}
157160
}
158161

159-
binding.names.composeViewBase.content { Surface {
162+
binding.composeViewBase.content { Surface {
160163
localizedNames = rememberSerializable {
161164
mutableStateOf(originalNames.takeIf { it.isNotEmpty() } ?: defaultNames())
162165
}
163166
isNoName = rememberSaveable { mutableStateOf(originalNoName) }
164167

165-
Column {
166-
Text(
167-
text = stringResource(Res.string.name_label),
168-
style = MaterialTheme.typography.caption.copy(
169-
color = LocalContentColor.current.copy(alpha = ContentAlpha.medium)
170-
)
168+
Column(
169+
modifier = Modifier
170+
.defaultMinSize(minHeight = 96.dp)
171+
.fillMaxWidth(),
172+
horizontalAlignment = Alignment.CenterHorizontally,
173+
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
174+
) {
175+
val feature = selectedFeature.value
176+
177+
FeatureSelect(
178+
feature = feature,
179+
onSelectedFeature = ::onSelectedFeature,
180+
featureDictionary = featureDictionary,
181+
geometryType = element?.geometryType ?: GeometryType.POINT,
182+
countryCode = countryOrSubdivisionCode,
183+
filterFn = { it.toElement().isPlace() || it.id == "shop/vacant" },
184+
codesOfDefaultFeatures = POPULAR_PLACE_FEATURE_IDS,
171185
)
172-
if (isNoName.value && localizedNames.value.isEmpty()) {
173-
Text(
174-
text = stringResource(Res.string.quest_placeName_no_name_answer),
175-
style = LocalTextStyle.current.copy(
176-
fontWeight = FontWeight.Bold,
177-
color = LocalContentColor.current.copy(alpha = ContentAlpha.medium)
178-
),
179-
modifier = Modifier
180-
.padding(20.dp)
181-
.align(Alignment.CenterHorizontally)
186+
if (feature != null && !feature.hasFixedName) {
187+
Column {
188+
Text(
189+
text = stringResource(Res.string.name_label),
190+
style = MaterialTheme.typography.caption.copy(
191+
color = LocalContentColor.current.copy(alpha = ContentAlpha.medium)
192+
)
193+
)
194+
if (isNoName.value && localizedNames.value.isEmpty()) {
195+
Text(
196+
text = stringResource(Res.string.quest_placeName_no_name_answer),
197+
style = LocalTextStyle.current.copy(
198+
fontWeight = FontWeight.Bold,
199+
color = LocalContentColor.current.copy(alpha = ContentAlpha.medium)
200+
),
201+
modifier = Modifier
202+
.padding(20.dp)
203+
.align(Alignment.CenterHorizontally)
204+
)
205+
}
206+
LocalizedNamesForm(
207+
localizedNames = localizedNames.value,
208+
onChanged = {
209+
localizedNames.value = it
210+
if (it.isNotEmpty()) isNoName.value = false
211+
checkIsFormComplete()
212+
},
213+
languageTags = selectableLanguages,
214+
)
215+
}
216+
}
217+
// show only for adding new POIs becaues it gets too busy with also the name form
218+
// being displayed
219+
if (lastPickedFeatures.isNotEmpty() && element == null && selectedFeature.value == null) {
220+
LastPickedChipsRow(
221+
items = lastPickedFeatures,
222+
onClick = {
223+
selectedFeature.value = it
224+
checkIsFormComplete()
225+
},
226+
modifier = Modifier.padding(start = 48.dp, end = 56.dp),
227+
itemContent = {
228+
FeatureIcon(
229+
feature = it,
230+
modifier = Modifier.size(22.5.dp)
231+
)
232+
}
182233
)
234+
} else {
235+
Spacer(Modifier.size(48.dp))
183236
}
184-
LocalizedNamesForm(
185-
localizedNames = localizedNames.value,
186-
onChanged = {
187-
localizedNames.value = it
188-
if (it.isNotEmpty()) isNoName.value = false
189-
checkIsFormComplete()
190-
},
191-
languageTags = selectableLanguages,
192-
)
193237
}
194238
} }
195-
196-
updateNameContainerVisibility()
239+
checkIsFormComplete()
197240
}
198241

199242
private fun onSelectedFeature(feature: Feature) {
200-
featureCtrl.feature = feature
243+
selectedFeature.value = feature
201244
isNoName.value = false
202245
// clear previous names (if necessary, and if any)
203-
if (feature.hasFixedName) {
246+
if (feature.hasFixedName == true) {
204247
localizedNames.value = listOf()
205248
} else {
206249
localizedNames.value = defaultNames()
207250
}
208-
updateNameContainerVisibility()
209251
checkIsFormComplete()
210252
}
211253

212254
private fun setVacant() {
213-
val languages = getLanguagesForFeatureDictionary(resources.configuration)
255+
val languages = getLanguagesForFeatureDictionary()
214256
onSelectedFeature(featureDictionary.getById("shop/vacant", languages)!!)
215257
}
216258

217259
private fun createNoNameAnswer(): AnswerItem? {
218-
val feature = featureCtrl.feature
260+
val feature = selectedFeature.value
219261
return if (feature == null || isNoName.value || feature.hasFixedName) {
220262
null
221263
} else {
@@ -235,34 +277,33 @@ class PlacesOverlayForm : AbstractOverlayForm() {
235277
checkIsFormComplete()
236278
}
237279

238-
private fun updateNameContainerVisibility() {
239-
val feature = featureCtrl.feature
240-
val isNameInputInvisible = feature == null || feature.hasFixedName
241-
binding.names.root.isGone = isNameInputInvisible
242-
}
243-
244280
private fun defaultNames(): List<LocalizedName> =
245281
listOf(LocalizedName(countryInfo.language.orEmpty(), ""))
246282

247283
override fun hasChanges(): Boolean =
248-
originalFeature != featureCtrl.feature
284+
originalFeature != selectedFeature.value
249285
|| originalNames != localizedNames.value.filter { it.name.isNotEmpty() }
250286
|| originalNoName != isNoName.value
251287

252288
override fun isFormComplete(): Boolean =
253-
featureCtrl.feature != null
289+
selectedFeature.value != null
254290
// name is not necessary
255291

256292
override fun onClickOk() {
257293
val inputNames = localizedNames.value.filter { it.name.isNotEmpty() }
258294
val firstLanguage = inputNames.firstOrNull()?.languageTag
259295
if (!firstLanguage.isNullOrEmpty()) prefs.preferredLanguageForNames = firstLanguage
260296

297+
val feature = selectedFeature.value!!
298+
if (!feature.isSuggestion) {
299+
prefs.addLastPicked(this::class.simpleName!!, feature.id)
300+
}
301+
261302
viewLifecycleScope.launch {
262303
applyEdit(createEditAction(
263304
element, geometry,
264305
inputNames, originalNames,
265-
featureCtrl.feature!!, originalFeature,
306+
selectedFeature.value!!, originalFeature,
266307
isNoName.value,
267308
::confirmReplaceShop
268309
))

app/src/androidMain/kotlin/de/westnordost/streetcomplete/overlays/surface/SurfaceOverlayForm.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ import de.westnordost.streetcomplete.overlays.ItemPairSelectOverlayForm
3434
import de.westnordost.streetcomplete.overlays.ItemSelectOverlayForm
3535
import de.westnordost.streetcomplete.ui.common.item_select.ImageWithLabel
3636
import de.westnordost.streetcomplete.ui.util.content
37-
import de.westnordost.streetcomplete.util.getLanguagesForFeatureDictionary
3837
import de.westnordost.streetcomplete.util.ktx.couldBeSteps
38+
import de.westnordost.streetcomplete.util.locale.getLanguagesForFeatureDictionary
3939
import de.westnordost.streetcomplete.util.takeFavorites
4040
import org.jetbrains.compose.resources.painterResource
4141
import org.jetbrains.compose.resources.stringResource
@@ -114,14 +114,12 @@ class SurfaceOverlayForm : AbstractOverlayForm() {
114114
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
115115
super.onViewCreated(view, savedInstanceState)
116116

117-
val languages = getLanguagesForFeatureDictionary(resources.configuration)
117+
val languages = getLanguagesForFeatureDictionary()
118118
val footwayLabel = featureDictionary.getById("highway/footway", languages)?.name.orEmpty()
119119
val cyclewayLabel = featureDictionary.getById("highway/cycleway", languages)?.name.orEmpty()
120120

121121
binding.composeViewBase.content { Surface {
122-
val item = selectedItem.value
123-
124-
when (item) {
122+
when (val item = selectedItem.value) {
125123
is SingleSurface -> {
126124
val lastPickedSingleSurfaces = remember { lastPickedSingleSurfaces }
127125
ItemSelectOverlayForm(

0 commit comments

Comments
 (0)