Skip to content

Commit

Permalink
Build two levels dropdown to select, configure and apply mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
eskatos committed Jun 13, 2024
1 parent ba4511f commit 1b2da54
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Int>, DEFAULT_INT)

MutationParameterKind.StringParameter ->
argument(param as MutationParameter<String>, "com.example.foo.bar")

MutationParameterKind.BooleanParameter ->
argument(param as MutationParameter<Boolean>, false)
}
}
}
val target = TextMutationApplicationTarget(documentWithResolution, schema)
val result = MutationAsTextRunner().runMutation(mutationDefinition, defaultArgumentValues, target)
val result = MutationAsTextRunner().runMutation(mutationDefinition, mutationArgumentsContainer, target)

result.stepResults.filterIsInstance<ModelMutationStepResult.ModelMutationStepApplied>().lastOrNull()?.let {
file.writeText(it.newDocumentText)
}
}

private const val DEFAULT_INT = 42
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -127,12 +129,13 @@ class GetDeclarativeDocuments : GetModelAction.GetCompositeModelAction<ResolvedD
domWithConventions.overlayResolutionContainer,
highlightingContext,
mutationApplicability,
onRunMutation = { mutationDefinition ->
onRunMutation = { mutationDefinition, mutationArgumentsContainer ->
MutationUtils.runMutation(
selectedBuildFile.value,
domWithConventions.inputOverlay,
projectEvaluationSchema,
mutationDefinition
mutationDefinition,
mutationArgumentsContainer
)
// Trigger recomposition:
fileUpdatesCount.value += 1
Expand Down Expand Up @@ -257,7 +260,7 @@ class ModelTreeRendering(
val resolutionContainer: DocumentResolutionContainer,
val highlightingContext: HighlightingContext,
val mutationApplicability: NodeData<List<ApplicableMutation>>,
val onRunMutation: (MutationDefinition) -> Unit
val onRunMutation: (MutationDefinition, MutationArgumentContainer) -> Unit
) {
private val indentDp = MaterialTheme.spacing.level2

Expand Down Expand Up @@ -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<ApplicableMutation?>(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<MutationArgumentState> 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<MutationArgumentState>.toMutationArgumentsContainer(): MutationArgumentContainer =
mutationArguments {
forEach { argumentState ->
when (argumentState) {
is MutationArgumentState.IntArgument ->
argument(argumentState.parameter as MutationParameter<Int>, requireNotNull(argumentState.value))

is MutationArgumentState.StringArgument ->
argument(argumentState.parameter as MutationParameter<String>, requireNotNull(argumentState.value))

is MutationArgumentState.BooleanArgument ->
argument(argumentState.parameter as MutationParameter<Boolean>, requireNotNull(argumentState.value))
}
}
}


private data class HighlightingContext(
val overlayOriginContainer: OverlayOriginContainer,
val highlightedSourceRange: MutableState<Map<String, IntRange>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 1b2da54

Please sign in to comment.