From 1b2da54fb667ea00a57f62bd00dfa234a115ee62 Mon Sep 17 00:00:00 2001 From: Paul Merlin Date: Thu, 13 Jun 2024 17:39:55 +0200 Subject: [PATCH] Build two levels dropdown to select, configure and apply mutations --- .../client/core/gradle/dcl/MutationUtils.kt | 29 +-- .../org/gradle/client/ui/composables/Texts.kt | 9 + .../actions/GetDeclarativeDocuments.kt | 208 +++++++++++++++++- .../org/gradle/client/ui/theme/Spacing.kt | 5 + .../src/main/kotlin/MutationDefinitions.kt | 28 ++- 5 files changed, 233 insertions(+), 46 deletions(-) diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/MutationUtils.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/MutationUtils.kt index 11f93d7..5a6aa3b 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/MutationUtils.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/core/gradle/dcl/MutationUtils.kt @@ -1,6 +1,9 @@ package org.gradle.client.core.gradle.dcl -import org.gradle.client.demo.mutations.* +import org.gradle.client.demo.mutations.SetNamespaceMutation +import org.gradle.client.demo.mutations.SetVersionCodeMutation +import org.gradle.client.demo.mutations.addTestingDependencyMutation +import org.gradle.client.demo.mutations.addTopLevelDependencyMutation import org.gradle.declarative.dsl.evaluation.EvaluationSchema import org.gradle.declarative.dsl.schema.AnalysisSchema import org.gradle.internal.declarativedsl.dom.DeclarativeDocument @@ -39,31 +42,17 @@ object MutationUtils { file: File, documentWithResolution: DocumentWithResolution, schema: EvaluationSchema, - mutationDefinition: MutationDefinition + mutationDefinition: MutationDefinition, + mutationArgumentsContainer: MutationArgumentContainer ) { - @Suppress("UNCHECKED_CAST") - val defaultArgumentValues = mutationArguments { - mutationDefinition.parameters.forEach { param -> - when (param.kind) { - MutationParameterKind.IntParameter -> - argument(param as MutationParameter, DEFAULT_INT) - - MutationParameterKind.StringParameter -> - argument(param as MutationParameter, "com.example.foo.bar") - - MutationParameterKind.BooleanParameter -> - argument(param as MutationParameter, false) - } - } - } val target = TextMutationApplicationTarget(documentWithResolution, schema) - val result = MutationAsTextRunner().runMutation(mutationDefinition, defaultArgumentValues, target) - + val result = MutationAsTextRunner().runMutation(mutationDefinition, mutationArgumentsContainer, target) + result.stepResults.filterIsInstance().lastOrNull()?.let { file.writeText(it.newDocumentText) } } - + private const val DEFAULT_INT = 42 } diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/Texts.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/Texts.kt index 8216f13..db28922 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/Texts.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/composables/Texts.kt @@ -65,6 +65,15 @@ fun LabelMedium(text: String, modifier: Modifier = Modifier) { ) } +@Composable +fun HeadlineSmall(text: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = text, + style = MaterialTheme.typography.headlineSmall, + ) +} + @Composable fun CodeBlock( modifier: Modifier = Modifier, diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/GetDeclarativeDocuments.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/GetDeclarativeDocuments.kt index 4cb0a89..539f456 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/GetDeclarativeDocuments.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/connected/actions/GetDeclarativeDocuments.kt @@ -3,7 +3,10 @@ package org.gradle.client.ui.connected.actions import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.* import androidx.compose.foundation.onClick +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.* import androidx.compose.runtime.* @@ -15,8 +18,8 @@ import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times import org.gradle.client.build.action.GetResolvedDomAction import org.gradle.client.build.model.ResolvedDomPrerequisites @@ -35,8 +38,7 @@ import org.gradle.internal.declarativedsl.dom.DeclarativeDocument import org.gradle.internal.declarativedsl.dom.DocumentResolution.ElementResolution.SuccessfulElementResolution.ContainerElementResolved import org.gradle.internal.declarativedsl.dom.data.NodeData import org.gradle.internal.declarativedsl.dom.data.collectToMap -import org.gradle.internal.declarativedsl.dom.mutation.ApplicableMutation -import org.gradle.internal.declarativedsl.dom.mutation.MutationDefinition +import org.gradle.internal.declarativedsl.dom.mutation.* import org.gradle.internal.declarativedsl.dom.operations.overlay.OverlayNodeOrigin.* import org.gradle.internal.declarativedsl.dom.operations.overlay.OverlayOriginContainer import org.gradle.internal.declarativedsl.dom.resolution.DocumentResolutionContainer @@ -127,12 +129,13 @@ class GetDeclarativeDocuments : GetModelAction.GetCompositeModelAction + onRunMutation = { mutationDefinition, mutationArgumentsContainer -> MutationUtils.runMutation( selectedBuildFile.value, domWithConventions.inputOverlay, projectEvaluationSchema, - mutationDefinition + mutationDefinition, + mutationArgumentsContainer ) // Trigger recomposition: fileUpdatesCount.value += 1 @@ -257,7 +260,7 @@ class ModelTreeRendering( val resolutionContainer: DocumentResolutionContainer, val highlightingContext: HighlightingContext, val mutationApplicability: NodeData>, - val onRunMutation: (MutationDefinition) -> Unit + val onRunMutation: (MutationDefinition, MutationArgumentContainer) -> Unit ) { private val indentDp = MaterialTheme.spacing.level2 @@ -400,24 +403,207 @@ class ModelTreeRendering( @Composable private fun ApplicableMutations(node: DeclarativeDocument.DocumentNode) { - mutationApplicability.data(node).forEach { - val tooltip = "Apply mutation: ${it.mutationDefinition.name}" + val applicableMutations = mutationApplicability.data(node) + if (applicableMutations.isNotEmpty()) { + var isMutationMenuVisible by remember { mutableStateOf(false) } + val tooltip = "Applicable mutations" PlainTextTooltip(tooltip) { IconButton( - modifier = Modifier.padding(0.dp).sizeIn(maxWidth = 24.dp, maxHeight = 24.dp), - onClick = { onRunMutation(it.mutationDefinition) } + modifier = Modifier + .padding(MaterialTheme.spacing.level0) + .sizeIn(maxWidth = MaterialTheme.spacing.level6, maxHeight = MaterialTheme.spacing.level6), + onClick = { isMutationMenuVisible = true } ) { Icon( Icons.Default.Edit, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(MaterialTheme.spacing.level6), contentDescription = tooltip ) } } + DropdownMenu( + expanded = isMutationMenuVisible, + onDismissRequest = { isMutationMenuVisible = false }, + ) { + var selectedMutation by remember { mutableStateOf(null) } + when (val mutation = selectedMutation) { + null -> { + MutationDropDownTitle(tooltip) + applicableMutations.forEach { applicableMutation -> + ListItem( + leadingContent = { + Icon( + Icons.Default.ChevronRight, + applicableMutation.mutationDefinition.name + ) + }, + headlineContent = { Text(applicableMutation.mutationDefinition.name) }, + supportingContent = { Text(applicableMutation.mutationDefinition.description) }, + modifier = Modifier.selectable(selected = false, onClick = { + selectedMutation = applicableMutation + }), + ) + } + } + + else -> { + var mutationArguments: List by remember { + mutableStateOf(mutation.mutationDefinition.parameters.map { parameter -> + when (parameter.kind) { + MutationParameterKind.BooleanParameter -> + MutationArgumentState.BooleanArgument(parameter) + + MutationParameterKind.IntParameter -> + MutationArgumentState.IntArgument(parameter) + + MutationParameterKind.StringParameter -> + MutationArgumentState.StringArgument(parameter) + } + }) + } + val validArguments by derivedStateOf { + mutationArguments.all { argument -> + when (argument) { + is MutationArgumentState.BooleanArgument -> argument.value != null + is MutationArgumentState.IntArgument -> argument.value != null + is MutationArgumentState.StringArgument -> argument.value?.isNotBlank() == true + } + } + } + MutationDropDownTitle( + headline = mutation.mutationDefinition.name, + supporting = mutation.mutationDefinition.description + ) + mutationArguments.forEachIndexed { index, argument -> + when (argument) { + is MutationArgumentState.BooleanArgument -> + ListItem( + headlineContent = { Text(argument.parameter.name) }, + supportingContent = { Text(argument.parameter.description) }, + trailingContent = { + Checkbox( + checked = argument.value ?: false, + onCheckedChange = { newChecked -> + mutationArguments = mutationArguments.toMutableList().apply { + this[index] = argument.copy(value = newChecked) + } + } + ) + } + ) + + is MutationArgumentState.IntArgument -> + ListItem(headlineContent = { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + label = { Text(argument.parameter.name) }, + placeholder = { Text(argument.parameter.description) }, + value = argument.value?.toString() ?: "", + onValueChange = { newValue -> + mutationArguments = mutationArguments.toMutableList().apply { + this[index] = argument.copy(value = newValue.toIntOrNull()) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) + }) + + is MutationArgumentState.StringArgument -> + ListItem(headlineContent = { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + label = { Text(argument.parameter.name) }, + placeholder = { Text(argument.parameter.description) }, + value = argument.value ?: "", + onValueChange = { newValue -> + mutationArguments = mutationArguments.toMutableList().apply { + this[index] = argument.copy(value = newValue) + } + } + ) + }) + } + } + ListItem(headlineContent = {}, trailingContent = { + Button( + content = { + val text = "Apply mutation" + Icon(Icons.Default.Edit, text) + MaterialTheme.spacing.HorizontalLevel2() + Text(text) + }, + enabled = validArguments, + onClick = { + onRunMutation( + mutation.mutationDefinition, + mutationArguments.toMutationArgumentsContainer() + ) + isMutationMenuVisible = false + }, + ) + }) + } + } + } } } } +@Composable +private fun MutationDropDownTitle( + headline: String, + supporting: String? = null +) { + ListItem( + headlineContent = { TitleMedium(headline) }, + supportingContent = supporting?.let { { Text(supporting) } }, + colors = ListItemDefaults.colors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + headlineColor = MaterialTheme.colorScheme.onSecondaryContainer, + supportingColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) + ) + +} + +private sealed interface MutationArgumentState { + + val parameter: MutationParameter<*> + + data class IntArgument( + override val parameter: MutationParameter<*>, + val value: Int? = null, + ) : MutationArgumentState + + data class StringArgument( + override val parameter: MutationParameter<*>, + val value: String? = null + ) : MutationArgumentState + + data class BooleanArgument( + override val parameter: MutationParameter<*>, + val value: Boolean? = null + ) : MutationArgumentState +} + +@Suppress("UNCHECKED_CAST") +private fun List.toMutationArgumentsContainer(): MutationArgumentContainer = + mutationArguments { + forEach { argumentState -> + when (argumentState) { + is MutationArgumentState.IntArgument -> + argument(argumentState.parameter as MutationParameter, requireNotNull(argumentState.value)) + + is MutationArgumentState.StringArgument -> + argument(argumentState.parameter as MutationParameter, requireNotNull(argumentState.value)) + + is MutationArgumentState.BooleanArgument -> + argument(argumentState.parameter as MutationParameter, requireNotNull(argumentState.value)) + } + } + } + + private data class HighlightingContext( val overlayOriginContainer: OverlayOriginContainer, val highlightedSourceRange: MutableState> diff --git a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/theme/Spacing.kt b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/theme/Spacing.kt index dbf804f..95656ef 100644 --- a/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/theme/Spacing.kt +++ b/gradle-client/src/jvmMain/kotlin/org/gradle/client/ui/theme/Spacing.kt @@ -18,6 +18,11 @@ data object Spacing { val paneSpacing = level6 val topBarHeight = 56.dp + @Composable + fun HorizontalLevel2() { + Spacer(Modifier.width(level2)) + } + @Composable fun VerticalLevel2() { Spacer(Modifier.height(level2)) diff --git a/mutations-demo/src/main/kotlin/MutationDefinitions.kt b/mutations-demo/src/main/kotlin/MutationDefinitions.kt index 7d735a0..d884635 100644 --- a/mutations-demo/src/main/kotlin/MutationDefinitions.kt +++ b/mutations-demo/src/main/kotlin/MutationDefinitions.kt @@ -17,7 +17,7 @@ object SetVersionCodeMutation : MutationDefinition { override val description: String = "Update versionCode in androidApplication" val versionCodeParam = - MutationParameter("new version code", "new value for versionCode", MutationParameterKind.IntParameter) + MutationParameter("New version code", "New value for versionCode", MutationParameterKind.IntParameter) override val parameters: List> get() = listOf(versionCodeParam) @@ -49,7 +49,7 @@ object SetNamespaceMutation : MutationDefinition { override val description: String = "Updates the namespace in an Android library" val newNamespaceParam = - MutationParameter("new namespace", "new value for versionCode", MutationParameterKind.StringParameter) + MutationParameter("New namespace", "New value for the namespace", MutationParameterKind.StringParameter) override val parameters: List> get() = listOf(newNamespaceParam) @@ -113,8 +113,8 @@ class AddDependencyMutation(override val id: String, private val scopeLocation: val dependencyCoordinatesParam = MutationParameter( - "dependency coordinates", - "coordinates of the dependency to add", + "Dependency coordinates", + "Coordinates of the dependency to add", MutationParameterKind.StringParameter ) @@ -125,16 +125,14 @@ class AddDependencyMutation(override val id: String, private val scopeLocation: projectAnalysisSchema.findTypeFor() != null override fun defineModelMutationSequence(projectAnalysisSchema: AnalysisSchema): List = - with(projectAnalysisSchema) { - listOf( - ModelMutationRequest( - scopeLocation(projectAnalysisSchema), - ModelMutation.AddNewElement( - NewElementNodeProvider.ArgumentBased { args -> - elementFromString("implementation(\"" + args[dependencyCoordinatesParam] + "\")")!! - } - ), - ) + listOf( + ModelMutationRequest( + scopeLocation(projectAnalysisSchema), + ModelMutation.AddNewElement( + NewElementNodeProvider.ArgumentBased { args -> + elementFromString("implementation(\"" + args[dependencyCoordinatesParam] + "\")")!! + } + ), ) - } + ) } \ No newline at end of file