From b326556565428610a70481436c94c4c3d6372fb0 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Fri, 3 Jul 2026 00:42:42 -0500 Subject: [PATCH 1/8] Add on-screen control appearance customization --- .../dialog/ControlAppearanceEditor.kt | 608 ++++++++++++++++++ .../component/dialog/ElementEditorDialog.kt | 450 +++++++++---- .../xserver/InputControlsProfileCopy.kt | 46 ++ .../ui/screen/xserver/XServerScreen.kt | 41 +- .../inputcontrols/ControlElement.java | 454 +++++++++++-- .../inputcontrols/ControlsProfile.java | 9 + .../winlator/inputcontrols/RangeScroller.java | 19 +- .../winlator/widget/InputControlsView.java | 6 +- app/src/main/res/values-da/strings.xml | 23 + app/src/main/res/values-de/strings.xml | 23 + app/src/main/res/values-es/strings.xml | 23 + app/src/main/res/values-fr/strings.xml | 23 + app/src/main/res/values-it/strings.xml | 23 + app/src/main/res/values-ja/strings.xml | 23 + app/src/main/res/values-ko/strings.xml | 23 + app/src/main/res/values-pl/strings.xml | 23 + app/src/main/res/values-pt-rBR/strings.xml | 23 + app/src/main/res/values-ro/strings.xml | 23 + app/src/main/res/values-ru/strings.xml | 23 + app/src/main/res/values-uk/strings.xml | 23 + app/src/main/res/values-zh-rCN/strings.xml | 23 + app/src/main/res/values-zh-rTW/strings.xml | 23 + app/src/main/res/values/strings.xml | 23 + 23 files changed, 1770 insertions(+), 208 deletions(-) create mode 100644 app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt create mode 100644 app/src/main/java/app/gamenative/ui/screen/xserver/InputControlsProfileCopy.kt diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt new file mode 100644 index 0000000000..f25779207f --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -0,0 +1,608 @@ +package app.gamenative.ui.component.dialog + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.gamenative.R +import app.gamenative.ui.component.settings.SettingsTextField +import app.gamenative.ui.theme.settingsTileColors +import app.gamenative.ui.util.SnackbarManager +import com.winlator.inputcontrols.ControlElement +import com.winlator.widget.InputControlsView + +internal data class ControlAppearance( + val color: Int, + val activeColor: Int, + val opacity: Float, + val strokeScale: Float, + val shooterLookThrough: Boolean +) { + fun applyTo(element: ControlElement) { + element.setButtonColor(color) + element.setButtonActiveColor(activeColor) + element.setButtonOpacity(opacity) + element.setButtonStrokeScale(strokeScale) + element.setShooterLookThrough(shooterLookThrough) + } + + companion object { + fun capture(element: ControlElement) = ControlAppearance( + color = element.buttonColor, + activeColor = element.buttonActiveColor, + opacity = element.buttonOpacity, + strokeScale = element.buttonStrokeScale, + shooterLookThrough = element.isShooterLookThrough + ) + } +} + +internal val controlStrokeWidthScales = floatArrayOf(0.75f, 1.0f, 1.5f, 2.0f) + +internal fun closestStrokeWidthIndex(strokeScale: Float): Int { + return controlStrokeWidthScales.indices.minBy { index -> + kotlin.math.abs(controlStrokeWidthScales[index] - strokeScale) + } +} + +private val controlColorPresets = listOf( + 0xffffff, 0xf44336, 0xff9800, 0xffeb3b, + 0x4caf50, 0x03a9f4, 0x3f51b5, 0x9c27b0 +) + +internal fun rgbToHex(color: Int): String = ControlElement.formatRgbColor(color) + +internal fun parseRgbHex(value: String): Int? { + val normalized = value.trim().removePrefix("#") + if (normalized.length != 6) return null + return normalized.toIntOrNull(16)?.and(0x00ffffff) +} + +private fun rgbToComposeColor(color: Int, opacity: Float = 1.0f): Color { + val rgb = color and 0x00ffffff + return Color( + red = ((rgb shr 16) and 0xff) / 255f, + green = ((rgb shr 8) and 0xff) / 255f, + blue = (rgb and 0xff) / 255f, + alpha = opacity.coerceIn(0f, 1f) + ) +} + +@Composable +internal fun ControlAppearancePreview( + type: ControlElement.Type, + text: String, + shape: ControlElement.Shape, + orientation: Int, + segmentCount: Int, + color: Int, + activeColor: Int, + opacity: Float, + strokeScale: Float +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.preview), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.width(88.dp) + ) + ControlPreviewPill( + type = type, + text = text, + shape = shape, + orientation = orientation, + segmentCount = segmentCount, + normalColor = color, + activeColor = activeColor, + opacity = opacity, + strokeScale = strokeScale, + active = false, + modifier = Modifier.weight(1f) + ) + ControlPreviewPill( + type = type, + text = text, + shape = shape, + orientation = orientation, + segmentCount = segmentCount, + normalColor = color, + activeColor = activeColor, + opacity = opacity, + strokeScale = strokeScale, + active = true, + modifier = Modifier.weight(1f), + label = stringResource(R.string.active) + ) + } +} + +@Composable +private fun ControlPreviewPill( + type: ControlElement.Type, + text: String, + shape: ControlElement.Shape, + orientation: Int, + segmentCount: Int, + normalColor: Int, + activeColor: Int, + opacity: Float, + strokeScale: Float, + active: Boolean, + modifier: Modifier = Modifier, + label: String? = null +) { + val normalDrawColor = rgbToComposeColor(normalColor, opacity) + val activeDrawColor = rgbToComposeColor(activeColor, opacity) + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = label ?: " ", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + when (type) { + ControlElement.Type.D_PAD -> DPadPreview(normalDrawColor, activeDrawColor, active, strokeScale) + ControlElement.Type.RANGE_BUTTON -> RangeButtonPreview(normalDrawColor, activeDrawColor, active, orientation, segmentCount, strokeScale) + ControlElement.Type.STICK -> StickPreview(normalDrawColor, activeDrawColor, active, strokeScale) + ControlElement.Type.TRACKPAD -> TrackpadPreview(normalDrawColor, activeDrawColor, active, strokeScale) + ControlElement.Type.SHOOTER_MODE -> ShooterModePreview(normalDrawColor, activeDrawColor, active, strokeScale) + else -> ButtonPreview( + text = text, + shape = shape, + normalColor = normalDrawColor, + activeColor = activeDrawColor, + active = active, + strokeScale = strokeScale + ) + } + } +} + +@Composable +private fun ButtonPreview( + text: String, + shape: ControlElement.Shape, + normalColor: Color, + activeColor: Color, + active: Boolean, + strokeScale: Float +) { + val composeShape = buttonPreviewShape(shape) + val drawColor = if (active) activeColor else normalColor + val strokeWidth = 2.dp * strokeScale + Box( + modifier = Modifier + .then(if (shape == ControlElement.Shape.CIRCLE || shape == ControlElement.Shape.SQUARE) Modifier.size(56.dp) else Modifier.height(48.dp).fillMaxWidth()) + .clip(composeShape) + .border(strokeWidth, drawColor, composeShape), + contentAlignment = Alignment.Center + ) { + Text( + text = text.take(8), + color = drawColor, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun DPadPreview(normalColor: Color, activeColor: Color, active: Boolean, strokeScale: Float) { + Canvas(modifier = Modifier.size(72.dp)) { + val unit = size.minDimension / 14f + val cx = size.width * 0.5f + val cy = size.height * 0.5f + val start = unit + val offsetX = unit * 2f + val offsetY = unit * 3f + val stroke = Stroke(width = 2.dp.toPx() * strokeScale) + + fun directionPath(direction: Int) = Path().apply { + when (direction) { + 0 -> { + moveTo(cx, cy - start) + lineTo(cx - offsetX, cy - offsetY) + lineTo(cx - offsetX, 0f) + lineTo(cx + offsetX, 0f) + lineTo(cx + offsetX, cy - offsetY) + } + 1 -> { + moveTo(cx + start, cy) + lineTo(cx + offsetY, cy - offsetX) + lineTo(size.width, cy - offsetX) + lineTo(size.width, cy + offsetX) + lineTo(cx + offsetY, cy + offsetX) + } + 2 -> { + moveTo(cx, cy + start) + lineTo(cx - offsetX, cy + offsetY) + lineTo(cx - offsetX, size.height) + lineTo(cx + offsetX, size.height) + lineTo(cx + offsetX, cy + offsetY) + } + else -> { + moveTo(cx - start, cy) + lineTo(cx - offsetY, cy - offsetX) + lineTo(0f, cy - offsetX) + lineTo(0f, cy + offsetX) + lineTo(cx - offsetY, cy + offsetX) + } + } + close() + } + + repeat(4) { direction -> + val directionActive = active && direction == 3 + val path = directionPath(direction) + drawPath(path, if (directionActive) activeColor else normalColor, style = stroke) + } + } +} + +@Composable +private fun RangeButtonPreview( + normalColor: Color, + activeColor: Color, + active: Boolean, + orientation: Int, + segmentCount: Int, + strokeScale: Float +) { + val count = segmentCount.coerceIn(2, 5) + val activeIndex = if (count > 1) 1 else 0 + val previewModifier = if (orientation == 0) Modifier.height(48.dp).fillMaxWidth() else Modifier.width(56.dp).height(104.dp) + + Box(modifier = previewModifier) { + Canvas(Modifier.matchParentSize()) { + val strokeWidth = (2.dp.toPx() * strokeScale).coerceAtLeast(1f) + val activeStrokeWidth = strokeWidth * 1.5f + val halfStroke = strokeWidth * 0.5f + val radius = 12.dp.toPx() + val activeRadius = radius + (activeStrokeWidth - strokeWidth) * 0.5f + val outlinePath = Path() + val activePath = Path() + + if (orientation == 0) { + val left = halfStroke + val right = size.width - halfStroke + val top = halfStroke + val bottom = size.height - halfStroke + val segmentWidth = (size.width - strokeWidth) / count + val activeLeft = activeIndex * segmentWidth + halfStroke + val activeRight = (activeIndex + 1) * segmentWidth + halfStroke + + if (active) { + if (activeLeft > left) { + outlinePath.moveTo(left + radius, top) + outlinePath.lineTo(maxOf(left + radius, activeLeft), top) + outlinePath.moveTo(left + radius, bottom) + outlinePath.lineTo(maxOf(left + radius, activeLeft), bottom) + outlinePath.moveTo(left + radius, top) + outlinePath.quadraticTo(left, top, left, top + radius) + outlinePath.lineTo(left, bottom - radius) + outlinePath.quadraticTo(left, bottom, left + radius, bottom) + } + if (activeRight < right) { + outlinePath.moveTo(minOf(right - radius, activeRight), top) + outlinePath.lineTo(right - radius, top) + outlinePath.quadraticTo(right, top, right, top + radius) + outlinePath.lineTo(right, bottom - radius) + outlinePath.quadraticTo(right, bottom, right - radius, bottom) + outlinePath.moveTo(minOf(right - radius, activeRight), bottom) + outlinePath.lineTo(right - radius, bottom) + } + drawPath(outlinePath, normalColor, style = Stroke(width = strokeWidth)) + } else { + drawRoundRect( + color = normalColor, + topLeft = Offset(left, top), + size = Size(size.width - strokeWidth, size.height - strokeWidth), + cornerRadius = CornerRadius(radius, radius), + style = Stroke(width = strokeWidth) + ) + } + + repeat(count - 1) { divider -> + val boundaryIndex = divider + 1 + val x = boundaryIndex * segmentWidth + halfStroke + val activeBoundary = active && (boundaryIndex == activeIndex || boundaryIndex == activeIndex + 1) + if (!activeBoundary) drawLine(normalColor, Offset(x, top), Offset(x, bottom), strokeWidth = strokeWidth) + } + + if (active) { + val leftRadius = if (activeIndex == 0) activeRadius.coerceAtMost(segmentWidth * 0.5f) else 0f + val rightRadius = if (activeIndex == count - 1) activeRadius.coerceAtMost(segmentWidth * 0.5f) else 0f + activePath.moveTo(activeLeft + leftRadius, top) + activePath.lineTo(activeRight - rightRadius, top) + if (rightRadius > 0f) { + activePath.quadraticTo(activeRight, top, activeRight, top + rightRadius) + activePath.lineTo(activeRight, bottom - rightRadius) + activePath.quadraticTo(activeRight, bottom, activeRight - rightRadius, bottom) + } else { + activePath.lineTo(activeRight, bottom) + } + activePath.lineTo(activeLeft + leftRadius, bottom) + if (leftRadius > 0f) { + activePath.quadraticTo(activeLeft, bottom, activeLeft, bottom - leftRadius) + activePath.lineTo(activeLeft, top + leftRadius) + activePath.quadraticTo(activeLeft, top, activeLeft + leftRadius, top) + } else { + activePath.lineTo(activeLeft, top) + } + activePath.close() + drawPath(activePath, activeColor, style = Stroke(width = activeStrokeWidth)) + } + } else { + val left = halfStroke + val right = size.width - halfStroke + val top = halfStroke + val bottom = size.height - halfStroke + val segmentHeight = (size.height - strokeWidth) / count + val activeTop = activeIndex * segmentHeight + halfStroke + val activeBottom = (activeIndex + 1) * segmentHeight + halfStroke + + if (active) { + if (activeTop > top) { + outlinePath.moveTo(left + radius, top) + outlinePath.lineTo(right - radius, top) + outlinePath.quadraticTo(right, top, right, top + radius) + outlinePath.moveTo(left, top + radius) + outlinePath.lineTo(left, maxOf(top + radius, activeTop)) + outlinePath.moveTo(right, top + radius) + outlinePath.lineTo(right, maxOf(top + radius, activeTop)) + } + if (activeBottom < bottom) { + outlinePath.moveTo(left, minOf(bottom - radius, activeBottom)) + outlinePath.lineTo(left, bottom - radius) + outlinePath.quadraticTo(left, bottom, left + radius, bottom) + outlinePath.lineTo(right - radius, bottom) + outlinePath.quadraticTo(right, bottom, right, bottom - radius) + outlinePath.moveTo(right, minOf(bottom - radius, activeBottom)) + outlinePath.lineTo(right, bottom - radius) + } + drawPath(outlinePath, normalColor, style = Stroke(width = strokeWidth)) + } else { + drawRoundRect( + color = normalColor, + topLeft = Offset(left, top), + size = Size(size.width - strokeWidth, size.height - strokeWidth), + cornerRadius = CornerRadius(radius, radius), + style = Stroke(width = strokeWidth) + ) + } + + repeat(count - 1) { divider -> + val boundaryIndex = divider + 1 + val y = boundaryIndex * segmentHeight + halfStroke + val activeBoundary = active && (boundaryIndex == activeIndex || boundaryIndex == activeIndex + 1) + if (!activeBoundary) drawLine(normalColor, Offset(left, y), Offset(right, y), strokeWidth = strokeWidth) + } + + if (active) { + val topRadius = if (activeIndex == 0) activeRadius.coerceAtMost(segmentHeight * 0.5f) else 0f + val bottomRadius = if (activeIndex == count - 1) activeRadius.coerceAtMost(segmentHeight * 0.5f) else 0f + activePath.moveTo(left + topRadius, activeTop) + activePath.lineTo(right - topRadius, activeTop) + if (topRadius > 0f) { + activePath.quadraticTo(right, activeTop, right, activeTop + topRadius) + } else { + activePath.lineTo(right, activeTop) + } + activePath.lineTo(right, activeBottom - bottomRadius) + if (bottomRadius > 0f) { + activePath.quadraticTo(right, activeBottom, right - bottomRadius, activeBottom) + activePath.lineTo(left + bottomRadius, activeBottom) + activePath.quadraticTo(left, activeBottom, left, activeBottom - bottomRadius) + } else { + activePath.lineTo(right, activeBottom) + activePath.lineTo(left, activeBottom) + } + activePath.lineTo(left, activeTop + topRadius) + if (topRadius > 0f) { + activePath.quadraticTo(left, activeTop, left + topRadius, activeTop) + } else { + activePath.lineTo(left, activeTop) + } + activePath.close() + drawPath(activePath, activeColor, style = Stroke(width = activeStrokeWidth)) + } + } + } + + if (orientation == 0) { + Row(Modifier.fillMaxSize()) { + repeat(count) { index -> + RangeSegment(index, normalColor, activeColor, active && index == activeIndex, Modifier.weight(1f).fillMaxHeight()) + } + } + } else { + Column(Modifier.fillMaxSize()) { + repeat(count) { index -> + RangeSegment(index, normalColor, activeColor, active && index == activeIndex, Modifier.weight(1f).fillMaxWidth()) + } + } + } + } +} + +@Composable +private fun RangeSegment(index: Int, normalColor: Color, activeColor: Color, active: Boolean, modifier: Modifier) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Text( + text = "${index + 1}", + color = if (active) activeColor else normalColor, + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun StickPreview(normalColor: Color, activeColor: Color, active: Boolean, strokeScale: Float) { + Canvas(modifier = Modifier.size(72.dp)) { + val stroke = Stroke(width = 2.dp.toPx() * strokeScale) + val center = Offset(size.width * 0.5f, size.height * 0.5f) + val radius = size.minDimension * 0.42f + val thumbCenter = if (active) Offset(center.x + radius * 0.28f, center.y - radius * 0.28f) else center + val thumbRadius = radius * 0.42f + drawCircle(if (active) activeColor else normalColor, radius, center, style = stroke) + drawCircle(activeFillColor(if (active) activeColor else normalColor), thumbRadius, thumbCenter) + drawCircle(if (active) activeColor else normalColor, thumbRadius, thumbCenter, style = stroke) + } +} + +@Composable +private fun TrackpadPreview(normalColor: Color, activeColor: Color, active: Boolean, strokeScale: Float) { + Canvas(modifier = Modifier.height(56.dp).fillMaxWidth()) { + val strokeWidth = 2.dp.toPx() * strokeScale + val stroke = Stroke(width = strokeWidth) + val corner = CornerRadius(size.height * 0.15f, size.height * 0.15f) + drawRoundRect(normalColor, size = size, cornerRadius = corner, style = stroke) + val inset = strokeWidth * 4f + drawRoundRect( + color = if (active) activeColor else normalColor, + topLeft = Offset(inset, inset), + size = Size(size.width - inset * 2f, size.height - inset * 2f), + cornerRadius = CornerRadius(size.height * 0.08f, size.height * 0.08f), + style = Stroke(width = strokeWidth * 1.5f) + ) + } +} + +@Composable +private fun ShooterModePreview(normalColor: Color, activeColor: Color, active: Boolean, strokeScale: Float) { + val drawColor = if (active) activeColor else normalColor + val strokeWidth = 2.dp * strokeScale + Box( + modifier = Modifier + .size(56.dp) + .clip(CircleShape) + .background(if (active) activeFillColor(activeColor) else Color.Transparent) + .border(strokeWidth, drawColor, CircleShape), + contentAlignment = Alignment.Center + ) { + Text( + text = "DJ", + color = drawColor, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold + ) + } +} + +private fun activeFillColor(color: Color): Color = color.copy(alpha = minOf(color.alpha, 0.28f)) + +private fun buttonPreviewShape(shape: ControlElement.Shape): Shape = when (shape) { + ControlElement.Shape.CIRCLE -> CircleShape + ControlElement.Shape.RECT -> RoundedCornerShape(0.dp) + ControlElement.Shape.ROUND_RECT -> RoundedCornerShape(24.dp) + ControlElement.Shape.SQUARE -> RoundedCornerShape(8.dp) +} + +@Composable +internal fun ControlColorField( + title: String, + subtitle: String, + value: String, + selectedColor: Int, + onValueChange: (String) -> Unit, + onPresetSelected: (Int) -> Unit +) { + SettingsTextField( + colors = settingsTileColors(), + title = { Text(title) }, + subtitle = { Text(subtitle) }, + value = value, + onValueChange = onValueChange, + action = { + Box( + modifier = Modifier + .size(24.dp) + .background(rgbToComposeColor(selectedColor), CircleShape) + .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape) + ) + } + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + controlColorPresets.forEach { preset -> + Box( + modifier = Modifier + .size(28.dp) + .background(rgbToComposeColor(preset), CircleShape) + .border( + width = if ((preset and 0x00ffffff) == (selectedColor and 0x00ffffff)) 3.dp else 1.dp, + color = if ((preset and 0x00ffffff) == (selectedColor and 0x00ffffff)) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, + shape = CircleShape + ) + .clickable { onPresetSelected(preset) } + ) + } + } +} + +internal fun showCopyAppearanceDialog( + context: android.content.Context, + currentElement: ControlElement, + view: InputControlsView, + onAppearanceCopied: (ControlAppearance) -> Unit +) { + val controls = view.profile?.elements + ?.filter { it != currentElement } + .orEmpty() + + if (controls.isEmpty()) { + SnackbarManager.show(context.getString(R.string.toast_no_buttons_to_copy_appearance)) + return + } + + val names = controls.map { element -> + val label = if (!element.text.isNullOrEmpty()) { + element.text + } else { + val binding = element.getBindingAt(0) + if (binding != null && binding != com.winlator.inputcontrols.Binding.NONE) binding.toString().take(15) else "No Binding" + } + "${element.type.name.replace("_", " ")} - ${rgbToHex(element.buttonColor)} - $label" + }.toTypedArray() + + android.app.AlertDialog.Builder(context) + .setTitle(context.getString(R.string.copy_appearance_from_button)) + .setItems(names) { _, which -> + onAppearanceCopied(ControlAppearance.capture(controls[which])) + SnackbarManager.show(context.getString(R.string.toast_copied_button_appearance)) + } + .setNegativeButton(context.getString(R.string.cancel), null) + .show() +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt index e481e25bf8..705a7c98d0 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt @@ -1,6 +1,5 @@ package app.gamenative.ui.component.dialog -import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -75,7 +74,7 @@ fun ElementEditorDialog( // Store original bindings for restore on cancel val originalBindings by remember { mutableStateOf( - (0 until 4).map { element.getBindingAt(it) } + (0 until element.bindingCount).map { element.getBindingAt(it) } ) } @@ -113,6 +112,7 @@ fun ElementEditorDialog( // Current editing values with live preview var currentScale by remember { mutableFloatStateOf(element.scale) } var currentText by remember { mutableStateOf(initialDisplayText) } + var currentTextEdited by remember { mutableStateOf(false) } var showBindingsEditor by remember { mutableStateOf(false) } var bindingSlotToEdit by remember { mutableStateOf?>(null) } @@ -121,8 +121,7 @@ fun ElementEditorDialog( // Track current dropdown selections var currentTypeIndex by remember { mutableIntStateOf(element.type.ordinal) } - var currentShapeIndex by remember { mutableIntStateOf(element.shape.ordinal) } - + var currentShape by remember { mutableStateOf(element.shape) } // State for size adjustment mode var showSizeAdjuster by remember { mutableStateOf(false) } @@ -181,32 +180,120 @@ fun ElementEditorDialog( val originalVisibleSegments by remember { mutableIntStateOf(element.bindingCount) } val originalScrollLocked by remember { mutableStateOf(element.isScrollLocked) } - // Button settings state + // Control appearance state var currentToggleSwitch by remember { mutableStateOf(element.isToggleSwitch) } val originalToggleSwitch by remember { mutableStateOf(element.isToggleSwitch) } + var currentButtonColor by remember { mutableIntStateOf(element.buttonColor) } + var currentButtonColorText by remember { mutableStateOf(rgbToHex(element.buttonColor)) } + var currentButtonActiveColor by remember { mutableIntStateOf(element.buttonActiveColor) } + var currentButtonActiveColorText by remember { mutableStateOf(rgbToHex(element.buttonActiveColor)) } + var currentButtonOpacityInherited by remember { mutableStateOf(element.buttonOpacity < 0f) } + var currentButtonOpacity by remember { + mutableFloatStateOf(if (element.buttonOpacity >= 0f) element.buttonOpacity else view.overlayOpacity) + } + var currentButtonStrokeScale by remember { mutableFloatStateOf(element.buttonStrokeScale) } + var currentShooterLookThrough by remember { mutableStateOf(element.isShooterLookThrough) } + val originalControlAppearances by remember { + mutableStateOf( + view.profile?.elements + ?.associateWith { ControlAppearance.capture(it) } + .orEmpty() + ) + } // Get types array for saving val types = remember { ControlElement.Type.values() } + fun applyCurrentControlAppearance() { + element.setButtonColor(currentButtonColor) + element.setButtonActiveColor(currentButtonActiveColor) + element.setButtonOpacity( + if (currentButtonOpacityInherited) ControlElement.INHERIT_BUTTON_OPACITY else currentButtonOpacity + ) + element.setButtonStrokeScale(currentButtonStrokeScale) + element.setShooterLookThrough(currentShooterLookThrough) + } + + fun currentAppearance() = ControlAppearance( + color = currentButtonColor, + activeColor = currentButtonActiveColor, + opacity = if (currentButtonOpacityInherited) ControlElement.INHERIT_BUTTON_OPACITY else currentButtonOpacity, + strokeScale = currentButtonStrokeScale, + shooterLookThrough = currentShooterLookThrough + ) + + fun textForSave(): String? { + if (!currentTextEdited) return originalText + return currentText.ifEmpty { null } + } + + fun saveChanges() { + element.setScale(currentScale) + element.setText(textForSave()) + + val selectedType = types[currentTypeIndex] + if (element.type != selectedType) { + element.setTypeWithoutReset(selectedType) + } + element.setShape(currentShape) + + if (selectedType == ControlElement.Type.SHOOTER_MODE) { + element.shooterMovementType = movementTypeOptions[currentMovementTypeIndex] + element.shooterLookType = lookTypeOptions[currentLookTypeIndex] + element.shooterLookSensitivity = currentLookSensitivity + element.shooterJoystickSize = currentJoystickSize + } + + if (selectedType == ControlElement.Type.RANGE_BUTTON) { + element.setRange(rangeTypes[currentRangeTypeIndex]) + element.setOrientation(currentOrientation.toByte()) + element.setBindingCount(currentVisibleSegments) + element.isScrollLocked = currentScrollLocked + } + + if (selectedType == ControlElement.Type.BUTTON) { + element.setToggleSwitch(currentToggleSwitch) + } + + applyCurrentControlAppearance() + view.profile?.save() + view.invalidate() + hasUnsavedChanges = false + } + // Apply changes to element for live preview LaunchedEffect(currentScale) { element.setScale(currentScale) view.invalidate() } - // Only update element text if user has actually modified it - // Don't apply preview for initial display text - LaunchedEffect(currentText) { - // Only set custom text if user has explicitly modified it from the initial value - // AND it's not empty (empty should remain null for binding-based display) - if (currentText != initialDisplayText && currentText.isNotEmpty()) { - element.setText(currentText) + LaunchedEffect(currentText, currentTextEdited) { + if (currentTextEdited) { + element.setText(currentText.ifEmpty { null }) view.invalidate() } } + LaunchedEffect( + currentButtonColor, + currentButtonActiveColor, + currentButtonOpacity, + currentButtonOpacityInherited, + currentButtonStrokeScale, + currentShooterLookThrough, + currentTypeIndex + ) { + applyCurrentControlAppearance() + view.invalidate() + } + + LaunchedEffect(currentShape) { + element.setShape(currentShape) + view.invalidate() + } + Dialog( onDismissRequest = { if (hasUnsavedChanges) { @@ -228,7 +315,10 @@ fun ElementEditorDialog( element = element, view = view, currentScale = currentScale, - onScaleChange = { currentScale = it }, + onScaleChange = { + currentScale = it + hasUnsavedChanges = true + }, onConfirm = { showSizeAdjuster = false }, @@ -241,6 +331,7 @@ fun ElementEditorDialog( onReset = { currentScale = 1.0f element.setScale(1.0f) + hasUnsavedChanges = true view.invalidate() } ) @@ -280,47 +371,7 @@ fun ElementEditorDialog( }, actions = { IconButton(onClick = { - // Save changes to element - element.setScale(currentScale) - // If currentText is empty string, set to null to use binding-based text - // Otherwise use the current text value - element.setText(if (currentText.isEmpty()) null else currentText) - // Change type without resetting bindings - if (element.type != types[currentTypeIndex]) { - element.setTypeWithoutReset(types[currentTypeIndex]) - } - - // Save shooter mode properties - if (types[currentTypeIndex] == ControlElement.Type.SHOOTER_MODE) { - element.shooterMovementType = movementTypeOptions[currentMovementTypeIndex] - element.shooterLookType = lookTypeOptions[currentLookTypeIndex] - element.shooterLookSensitivity = currentLookSensitivity - element.shooterJoystickSize = currentJoystickSize - } - - // Save range button properties - if (types[currentTypeIndex] == ControlElement.Type.RANGE_BUTTON) { - element.setRange(rangeTypes[currentRangeTypeIndex]) - element.setOrientation(currentOrientation.toByte()) - element.setBindingCount(currentVisibleSegments) - element.isScrollLocked = currentScrollLocked - } - - // Save button properties - if (types[currentTypeIndex] == ControlElement.Type.BUTTON) { - element.setToggleSwitch(currentToggleSwitch) - } - - // Save to disk - view.profile?.save() - - // Update canvas to show new bindings - view.invalidate() - - // Mark as saved - hasUnsavedChanges = false - - // Close dialog + saveChanges() onSave() }) { Icon(Icons.Default.Save, null) @@ -354,13 +405,19 @@ fun ElementEditorDialog( title = { Text(stringResource(R.string.label_text)) }, subtitle = { Text(stringResource(R.string.label_text_subtitle)) }, value = currentText, - onValueChange = { currentText = it }, + onValueChange = { + currentText = it + currentTextEdited = true + hasUnsavedChanges = true + }, action = { // Reset button to restore original text IconButton(onClick = { // Reset to initial display text (binding-based or original custom text) currentText = initialDisplayText + currentTextEdited = false element.setText(originalText) + hasUnsavedChanges = true view.invalidate() }) { Icon( @@ -388,6 +445,13 @@ fun ElementEditorDialog( onItemSelected = { index -> val newType = types[index] currentTypeIndex = index + currentShape = when (newType) { + ControlElement.Type.STICK, + ControlElement.Type.SHOOTER_MODE -> ControlElement.Shape.CIRCLE + ControlElement.Type.TRACKPAD, + ControlElement.Type.RANGE_BUTTON -> ControlElement.Shape.ROUND_RECT + else -> currentShape + } element.setTypeWithoutReset(newType) // Mark as having unsaved changes @@ -402,7 +466,8 @@ fun ElementEditorDialog( // Element Shape (with restrictions) // STICK, TRACKPAD, RANGE_BUTTON have fixed rendering shapes // D_PAD uses custom cross-shaped path and doesn't respect shape - val availableShapes = when (element.type) { + val selectedType = types[currentTypeIndex] + val availableShapes = when (selectedType) { ControlElement.Type.STICK -> { // Stick is always rendered as CIRCLE listOf(ControlElement.Shape.CIRCLE) @@ -415,7 +480,7 @@ fun ElementEditorDialog( ControlElement.Type.D_PAD -> { // D-Pad uses custom cross-shaped path, shape doesn't affect rendering // Don't allow changing shape - listOf(element.shape) + listOf(currentShape) } ControlElement.Type.SHOOTER_MODE -> { // Shooter Mode is always rendered as CIRCLE @@ -425,12 +490,11 @@ fun ElementEditorDialog( // Buttons fully support all shapes ControlElement.Shape.values().toList() } - else -> ControlElement.Shape.values().toList() } if (availableShapes.size > 1) { val shapeNames = availableShapes.map { it.name.replace("_", " ") } - val currentShapeIndexInList = availableShapes.indexOf(element.shape).coerceAtLeast(0) + val currentShapeIndexInList = availableShapes.indexOf(currentShape).coerceAtLeast(0) SettingsListDropdown( colors = settingsTileColors(), title = { Text(stringResource(R.string.shape)) }, @@ -438,39 +502,22 @@ fun ElementEditorDialog( value = currentShapeIndexInList, items = shapeNames, onItemSelected = { index -> - element.shape = availableShapes[index] - view.invalidate() + currentShape = availableShapes[index] + hasUnsavedChanges = true } ) - } else if (availableShapes.size == 1 && element.type != ControlElement.Type.D_PAD) { + } else if (availableShapes.size == 1 && selectedType != ControlElement.Type.D_PAD) { // Show info for restricted types (but not D-PAD since it's obvious) SettingsMenuLink( colors = settingsTileColors(), title = { Text(stringResource(R.string.shape)) }, - subtitle = { Text(stringResource(R.string.shape_restricted, element.type.name, availableShapes[0].name.replace("_", " "))) }, + subtitle = { Text(stringResource(R.string.shape_restricted, selectedType.name, availableShapes[0].name.replace("_", " "))) }, enabled = false, onClick = {} ) } } - // Button Settings Section (only for BUTTON type) - if (types[currentTypeIndex] == ControlElement.Type.BUTTON) { - SettingsGroup(title = { Text(stringResource(R.string.button_settings)) }) { - SettingsSwitch( - colors = settingsTileColorsAlt(), - title = { Text(stringResource(R.string.button_toggleable)) }, - subtitle = { Text(stringResource(R.string.button_toggleable_subtitle)) }, - state = currentToggleSwitch, - onCheckedChange = { - currentToggleSwitch = it - element.setToggleSwitch(it) - hasUnsavedChanges = true - }, - ) - } - } - // Bindings Section // Use key() with bindingsRefreshKey to force recomposition when bindings change key(bindingsRefreshKey) { @@ -749,6 +796,201 @@ fun ElementEditorDialog( } } + // Control Appearance Section + SettingsGroup(title = { Text(stringResource(R.string.control_appearance)) }) { + ControlAppearancePreview( + type = types[currentTypeIndex], + text = currentText.ifEmpty { bindingShortLabel(element.getBindingAt(0)).ifEmpty { "A" } }, + shape = currentShape, + orientation = currentOrientation, + segmentCount = currentVisibleSegments, + color = currentButtonColor, + activeColor = currentButtonActiveColor, + opacity = currentButtonOpacity, + strokeScale = currentButtonStrokeScale + ) + + ControlColorField( + title = stringResource(R.string.button_color), + subtitle = stringResource(R.string.button_color_subtitle), + value = currentButtonColorText, + selectedColor = currentButtonColor, + onValueChange = { value -> + currentButtonColorText = value.uppercase(Locale.US) + parseRgbHex(value)?.let { + currentButtonColor = it + hasUnsavedChanges = true + } + }, + onPresetSelected = { + currentButtonColor = it + currentButtonColorText = rgbToHex(it) + hasUnsavedChanges = true + } + ) + + ControlColorField( + title = stringResource(R.string.button_active_color), + subtitle = stringResource(R.string.button_active_color_subtitle), + value = currentButtonActiveColorText, + selectedColor = currentButtonActiveColor, + onValueChange = { value -> + currentButtonActiveColorText = value.uppercase(Locale.US) + parseRgbHex(value)?.let { + currentButtonActiveColor = it + hasUnsavedChanges = true + } + }, + onPresetSelected = { + currentButtonActiveColor = it + currentButtonActiveColorText = rgbToHex(it) + hasUnsavedChanges = true + } + ) + + SettingsMenuLink( + colors = settingsTileColors(), + title = { Text(stringResource(R.string.button_opacity)) }, + subtitle = { + Text( + if (currentButtonOpacityInherited) { + stringResource(R.string.button_opacity_inherited, (currentButtonOpacity * 100).roundToInt()) + } else { + stringResource(R.string.button_opacity_value, (currentButtonOpacity * 100).roundToInt()) + } + ) + }, + onClick = {} + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Slider( + value = currentButtonOpacity, + onValueChange = { + currentButtonOpacity = it + currentButtonOpacityInherited = false + hasUnsavedChanges = true + }, + valueRange = 0.1f..1.0f, + modifier = Modifier.weight(1f) + ) + Text( + text = "${(currentButtonOpacity * 100).roundToInt()}%", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + } + + val strokeWidthItems = listOf( + stringResource(R.string.control_stroke_width_thin), + stringResource(R.string.control_stroke_width_default), + stringResource(R.string.control_stroke_width_thick), + stringResource(R.string.control_stroke_width_extra_thick) + ) + val strokeWidthIndex = closestStrokeWidthIndex(currentButtonStrokeScale) + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(stringResource(R.string.control_stroke_width)) }, + subtitle = { Text(stringResource(R.string.control_stroke_width_subtitle)) }, + value = strokeWidthIndex, + items = strokeWidthItems, + onItemSelected = { index -> + currentButtonStrokeScale = controlStrokeWidthScales[index] + hasUnsavedChanges = true + } + ) + + if (types[currentTypeIndex] == ControlElement.Type.BUTTON) { + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(stringResource(R.string.button_toggleable)) }, + subtitle = { Text(stringResource(R.string.button_toggleable_subtitle)) }, + state = currentToggleSwitch, + onCheckedChange = { + currentToggleSwitch = it + element.setToggleSwitch(it) + hasUnsavedChanges = true + }, + ) + + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(stringResource(R.string.button_shooter_look_through)) }, + subtitle = { Text(stringResource(R.string.button_shooter_look_through_subtitle)) }, + state = currentShooterLookThrough, + onCheckedChange = { + currentShooterLookThrough = it + hasUnsavedChanges = true + }, + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedButton( + onClick = { + showCopyAppearanceDialog(context, element, view) { appearance -> + currentButtonColor = appearance.color + currentButtonColorText = rgbToHex(appearance.color) + currentButtonActiveColor = appearance.activeColor + currentButtonActiveColorText = rgbToHex(appearance.activeColor) + currentButtonOpacityInherited = appearance.opacity < 0f + currentButtonOpacity = if (appearance.opacity >= 0f) appearance.opacity else view.overlayOpacity + currentButtonStrokeScale = appearance.strokeScale + currentShooterLookThrough = appearance.shooterLookThrough + hasUnsavedChanges = true + } + }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 8.dp) + ) { + Text(stringResource(R.string.copy), style = MaterialTheme.typography.labelSmall) + } + + OutlinedButton( + onClick = { + val appearance = currentAppearance() + view.profile?.elements + ?.forEach { appearance.applyTo(it) } + view.invalidate() + hasUnsavedChanges = true + SnackbarManager.show(context.getString(R.string.toast_applied_button_appearance)) + }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 8.dp) + ) { + Text(stringResource(R.string.apply_all), style = MaterialTheme.typography.labelSmall) + } + + OutlinedButton( + onClick = { + currentButtonColor = ControlElement.DEFAULT_BUTTON_COLOR + currentButtonColorText = rgbToHex(ControlElement.DEFAULT_BUTTON_COLOR) + currentButtonActiveColor = ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR + currentButtonActiveColorText = rgbToHex(ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR) + currentButtonOpacityInherited = true + currentButtonOpacity = view.overlayOpacity + currentButtonStrokeScale = ControlElement.DEFAULT_BUTTON_STROKE_SCALE + currentShooterLookThrough = true + hasUnsavedChanges = true + }, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(horizontal = 6.dp, vertical = 8.dp) + ) { + Text(stringResource(R.string.reset), style = MaterialTheme.typography.labelSmall) + } + } + } + // Properties Section SettingsGroup(title = { Text(stringResource(R.string.properties)) }) { SettingsMenuLink( @@ -789,6 +1031,7 @@ fun ElementEditorDialog( if (customText.isNullOrEmpty() || customText == bindingShortLabel(currentBinding)) { // Clear custom text so new binding text will show element.setText(null) + currentTextEdited = false // Update currentText state to show what will actually be displayed (new binding text) val newBindingText = bindingShortLabel(binding) @@ -829,33 +1072,7 @@ fun ElementEditorDialog( text = { Text(stringResource(R.string.unsaved_changes_message)) }, confirmButton = { TextButton(onClick = { - // Save and close - element.setScale(currentScale) - element.setText(if (currentText.isEmpty()) null else currentText) - // Change type without resetting bindings - if (element.type != types[currentTypeIndex]) { - element.setTypeWithoutReset(types[currentTypeIndex]) - } - // Save shooter mode properties - if (types[currentTypeIndex] == ControlElement.Type.SHOOTER_MODE) { - element.shooterMovementType = movementTypeOptions[currentMovementTypeIndex] - element.shooterLookType = lookTypeOptions[currentLookTypeIndex] - element.shooterLookSensitivity = currentLookSensitivity - element.shooterJoystickSize = currentJoystickSize - } - // Save range button properties - if (types[currentTypeIndex] == ControlElement.Type.RANGE_BUTTON) { - element.setRange(rangeTypes[currentRangeTypeIndex]) - element.setOrientation(currentOrientation.toByte()) - element.setBindingCount(currentVisibleSegments) - element.isScrollLocked = currentScrollLocked - } - // Save button properties - if (types[currentTypeIndex] == ControlElement.Type.BUTTON) { - element.setToggleSwitch(currentToggleSwitch) - } - view.profile?.save() - view.invalidate() + saveChanges() showExitConfirmation = false onDismiss() }) { @@ -866,9 +1083,17 @@ fun ElementEditorDialog( TextButton(onClick = { // Discard changes and close element.setScale(originalScale) - element.setText(originalText) element.type = originalType element.shape = originalShape + // Restore original range button properties before bindings; setBindingCount resets bindings. + if (originalType == ControlElement.Type.RANGE_BUTTON) { + element.setRange(originalRange) + element.setOrientation(originalOrientation.toByte()) + element.setBindingCount(originalVisibleSegments) + element.isScrollLocked = originalScrollLocked + } else if (element.bindingCount != originalBindings.size) { + element.setBindingCount(originalBindings.size) + } // Restore original bindings originalBindings.forEachIndexed { index, binding -> if (binding != null) { @@ -880,13 +1105,12 @@ fun ElementEditorDialog( element.shooterLookType = originalLookType element.shooterLookSensitivity = originalLookSensitivity element.shooterJoystickSize = originalJoystickSize - // Restore original range button properties - element.setRange(originalRange) - element.setOrientation(originalOrientation.toByte()) - element.setBindingCount(originalVisibleSegments) - element.isScrollLocked = originalScrollLocked // Restore original button properties element.setToggleSwitch(originalToggleSwitch) + element.setText(originalText) + originalControlAppearances.forEach { (control, appearance) -> + appearance.applyTo(control) + } view.invalidate() showExitConfirmation = false onDismiss() diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/InputControlsProfileCopy.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/InputControlsProfileCopy.kt new file mode 100644 index 0000000000..5e73d20c57 --- /dev/null +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/InputControlsProfileCopy.kt @@ -0,0 +1,46 @@ +package app.gamenative.ui.screen.xserver + +import com.winlator.inputcontrols.ControlElement +import com.winlator.inputcontrols.ControlsProfile +import com.winlator.widget.InputControlsView + +internal fun copyInputControlsProfileElements( + sourceProfile: ControlsProfile, + targetProfile: ControlsProfile, + view: InputControlsView +) { + sourceProfile.loadElements(view) + targetProfile.elements.toList().forEach(targetProfile::removeElement) + sourceProfile.elements.forEach { targetProfile.addElement(it.copyForView(view)) } + view.invalidate() +} + +private fun ControlElement.copyForView(view: InputControlsView) = ControlElement(view).also { newElement -> + newElement.setType(type) + newElement.setShape(shape) + newElement.setX(x.toInt()) + newElement.setY(y.toInt()) + newElement.setScale(scale) + newElement.setText(text) + newElement.setIconId(iconId.toInt()) + newElement.setToggleSwitch(isToggleSwitch) + newElement.copyButtonAppearanceFrom(this) + + if (type == ControlElement.Type.RANGE_BUTTON) { + newElement.setRange(range) + newElement.setOrientation(orientation) + newElement.setBindingCount(bindingCount) + newElement.isScrollLocked = isScrollLocked + } + + for (i in 0 until bindingCount) { + newElement.setBindingAt(i, getBindingAt(i)) + } + + if (type == ControlElement.Type.SHOOTER_MODE) { + newElement.shooterMovementType = shooterMovementType + newElement.shooterLookType = shooterLookType + newElement.shooterLookSensitivity = shooterLookSensitivity + newElement.shooterJoystickSize = shooterJoystickSize + } +} diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 274fee3796..dce6e3a0a6 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -2493,46 +2493,7 @@ fun XServerScreen( // Wait for view to be laid out before loading elements PluviaApp.inputControlsView?.let { icView -> icView.post { - // Load Profile 0 elements (with valid dimensions) - profile.loadElements(icView) - - // Clear current profile elements and copy from Profile 0 - val elementsToRemove = currentProfile.elements.toList() - elementsToRemove.forEach { currentProfile.removeElement(it) } - - profile.elements.forEach { element -> - val newElement = com.winlator.inputcontrols.ControlElement(icView) - newElement.setType(element.type) - newElement.setShape(element.shape) - newElement.setX(element.x.toInt()) - newElement.setY(element.y.toInt()) - newElement.setScale(element.scale) - newElement.setText(element.text) - newElement.setIconId(element.iconId.toInt()) - newElement.setToggleSwitch(element.isToggleSwitch) - // Copy range button properties — must set binding count - // BEFORE copying bindings, because setBindingCount resets - // the bindings array to NONE. - if (element.type == com.winlator.inputcontrols.ControlElement.Type.RANGE_BUTTON) { - newElement.setRange(element.range) - newElement.setOrientation(element.orientation) - newElement.setBindingCount(element.bindingCount) - newElement.isScrollLocked = element.isScrollLocked - } - for (i in 0 until element.bindingCount) { - newElement.setBindingAt(i, element.getBindingAt(i)) - } - // Copy shooter mode properties - if (element.type == com.winlator.inputcontrols.ControlElement.Type.SHOOTER_MODE) { - newElement.shooterMovementType = element.shooterMovementType - newElement.shooterLookType = element.shooterLookType - newElement.shooterLookSensitivity = element.shooterLookSensitivity - newElement.shooterJoystickSize = element.shooterJoystickSize - } - currentProfile.addElement(newElement) - } - - icView.invalidate() + copyInputControlsProfileElements(profile, currentProfile, icView) SnackbarManager.show(context.getString(R.string.toast_controls_reset)) } } diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index 5bdd692c73..3c8e696e4b 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -5,6 +5,8 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import androidx.core.graphics.ColorUtils; @@ -19,6 +21,7 @@ import org.json.JSONObject; import java.util.Arrays; +import java.util.Locale; public class ControlElement { public static final float STICK_DEAD_ZONE = 0.15f; @@ -28,6 +31,10 @@ public class ControlElement { public static final float TRACKPAD_MAX_SPEED = 20.0f; public static final byte TRACKPAD_ACCELERATION_THRESHOLD = 4; public static final short BUTTON_MIN_TIME_TO_KEEP_PRESSED = 300; + public static final int DEFAULT_BUTTON_COLOR = 0x00ffffff; + public static final int DEFAULT_BUTTON_ACTIVE_COLOR = 0x00ffffff; + public static final float INHERIT_BUTTON_OPACITY = -1.0f; + public static final float DEFAULT_BUTTON_STROKE_SCALE = 1.0f; public enum Type { BUTTON, D_PAD, RANGE_BUTTON, STICK, TRACKPAD, SHOOTER_MODE; @@ -89,6 +96,11 @@ public static String[] names() { private String shooterLookType = "mouse"; private float shooterLookSensitivity = 1.0f; private float shooterJoystickSize = 1.0f; + private int buttonColor = DEFAULT_BUTTON_COLOR; + private int buttonActiveColor = DEFAULT_BUTTON_ACTIVE_COLOR; + private float buttonOpacity = INHERIT_BUTTON_OPACITY; + private float buttonStrokeScale = DEFAULT_BUTTON_STROKE_SCALE; + private boolean shooterLookThrough = true; public ControlElement(InputControlsView inputControlsView) { this.inputControlsView = inputControlsView; @@ -255,6 +267,84 @@ public void setShooterJoystickSize(float shooterJoystickSize) { this.shooterJoystickSize = shooterJoystickSize; } + public int getButtonColor() { + return buttonColor; + } + + public void setButtonColor(int buttonColor) { + this.buttonColor = buttonColor & 0x00ffffff; + } + + public int getButtonActiveColor() { + return buttonActiveColor; + } + + public void setButtonActiveColor(int buttonActiveColor) { + this.buttonActiveColor = buttonActiveColor & 0x00ffffff; + } + + public float getButtonOpacity() { + return buttonOpacity; + } + + public void setButtonOpacity(float buttonOpacity) { + if (buttonOpacity < 0) { + this.buttonOpacity = INHERIT_BUTTON_OPACITY; + } + else { + this.buttonOpacity = Mathf.clamp(buttonOpacity, 0.0f, 1.0f); + } + } + + public float getButtonStrokeScale() { + return buttonStrokeScale; + } + + public void setButtonStrokeScale(float buttonStrokeScale) { + this.buttonStrokeScale = Mathf.clamp(buttonStrokeScale, 0.5f, 2.0f); + } + + public boolean isShooterLookThrough() { + return shooterLookThrough; + } + + public void setShooterLookThrough(boolean shooterLookThrough) { + this.shooterLookThrough = shooterLookThrough; + } + + public void copyButtonAppearanceFrom(ControlElement element) { + buttonColor = element.buttonColor; + buttonActiveColor = element.buttonActiveColor; + buttonOpacity = element.buttonOpacity; + buttonStrokeScale = element.buttonStrokeScale; + shooterLookThrough = element.shooterLookThrough; + } + + public float getEffectiveButtonOpacity(float fallbackOpacity) { + return buttonOpacity >= 0 ? buttonOpacity : fallbackOpacity; + } + + public static int parseRgbColor(Object value, int fallbackColor) { + if (value instanceof Number) return ((Number)value).intValue() & 0x00ffffff; + if (value instanceof String) { + String hex = ((String)value).trim(); + if (hex.startsWith("#")) hex = hex.substring(1); + if (hex.length() == 8) hex = hex.substring(2); + if (hex.length() == 6) { + try { + return (int)Long.parseLong(hex, 16) & 0x00ffffff; + } + catch (NumberFormatException ignored) { + } + } + } + return fallbackColor; + } + + public static String formatRgbColor(int color) { + return String.format(Locale.US, "#%06X", color & 0x00ffffff); + } + public float getScale() { return scale; } @@ -412,14 +502,196 @@ private static String getRangeTextForIndex(Range range, int index) { return text; } + private int getAppearanceDrawColor(boolean active) { + int rgb = active ? buttonActiveColor : buttonColor; + int alpha = (int)(getEffectiveButtonOpacity(inputControlsView.getOverlayOpacity()) * 255); + return ColorUtils.setAlphaComponent(0xff000000 | rgb, alpha); + } + + private int getEditorSelectionDrawColor() { + int rgb = buttonActiveColor != DEFAULT_BUTTON_ACTIVE_COLOR ? buttonActiveColor : inputControlsView.getSecondaryColor(); + int alpha = (int)(getEffectiveButtonOpacity(inputControlsView.getOverlayOpacity()) * 255); + return ColorUtils.setAlphaComponent(0xff000000 | (rgb & 0x00ffffff), alpha); + } + + private static void setDPadDirectionPath(Path path, byte direction, Rect boundingBox, float cx, float cy, float offsetX, float offsetY, float start) { + path.reset(); + switch (direction) { + case 0: + path.moveTo(cx, cy - start); + path.lineTo(cx - offsetX, cy - offsetY); + path.lineTo(cx - offsetX, boundingBox.top); + path.lineTo(cx + offsetX, boundingBox.top); + path.lineTo(cx + offsetX, cy - offsetY); + break; + case 1: + path.moveTo(cx + start, cy); + path.lineTo(cx + offsetY, cy - offsetX); + path.lineTo(boundingBox.right, cy - offsetX); + path.lineTo(boundingBox.right, cy + offsetX); + path.lineTo(cx + offsetY, cy + offsetX); + break; + case 2: + path.moveTo(cx, cy + start); + path.lineTo(cx - offsetX, cy + offsetY); + path.lineTo(cx - offsetX, boundingBox.bottom); + path.lineTo(cx + offsetX, boundingBox.bottom); + path.lineTo(cx + offsetX, cy + offsetY); + break; + case 3: + path.moveTo(cx - start, cy); + path.lineTo(cx - offsetY, cy - offsetX); + path.lineTo(boundingBox.left, cy - offsetX); + path.lineTo(boundingBox.left, cy + offsetX); + path.lineTo(cx - offsetY, cy + offsetX); + break; + } + path.close(); + } + + private static void setHorizontalRangeSegmentPath(Path path, Rect boundingBox, float segmentLeft, float segmentRight, float radius) { + float left = Math.max(segmentLeft, boundingBox.left); + float right = Math.min(segmentRight, boundingBox.right); + float top = boundingBox.top; + float bottom = boundingBox.bottom; + float width = Math.max(0.0f, right - left); + float leftRadius = segmentLeft <= boundingBox.left ? Math.min(radius, width * 0.5f) : 0.0f; + float rightRadius = segmentRight >= boundingBox.right ? Math.min(radius, width * 0.5f) : 0.0f; + + path.reset(); + path.moveTo(left + leftRadius, top); + path.lineTo(right - rightRadius, top); + if (rightRadius > 0.0f) { + path.quadTo(right, top, right, top + rightRadius); + path.lineTo(right, bottom - rightRadius); + path.quadTo(right, bottom, right - rightRadius, bottom); + } + else { + path.lineTo(right, bottom); + } + path.lineTo(left + leftRadius, bottom); + if (leftRadius > 0.0f) { + path.quadTo(left, bottom, left, bottom - leftRadius); + path.lineTo(left, top + leftRadius); + path.quadTo(left, top, left + leftRadius, top); + } + else { + path.lineTo(left, top); + } + path.close(); + } + + private static void setVerticalRangeSegmentPath(Path path, Rect boundingBox, float segmentTop, float segmentBottom, float radius) { + float left = boundingBox.left; + float right = boundingBox.right; + float top = Math.max(segmentTop, boundingBox.top); + float bottom = Math.min(segmentBottom, boundingBox.bottom); + float height = Math.max(0.0f, bottom - top); + float topRadius = segmentTop <= boundingBox.top ? Math.min(radius, height * 0.5f) : 0.0f; + float bottomRadius = segmentBottom >= boundingBox.bottom ? Math.min(radius, height * 0.5f) : 0.0f; + + path.reset(); + path.moveTo(left + topRadius, top); + path.lineTo(right - topRadius, top); + if (topRadius > 0.0f) { + path.quadTo(right, top, right, top + topRadius); + } + else { + path.lineTo(right, top); + } + path.lineTo(right, bottom - bottomRadius); + if (bottomRadius > 0.0f) { + path.quadTo(right, bottom, right - bottomRadius, bottom); + path.lineTo(left + bottomRadius, bottom); + path.quadTo(left, bottom, left, bottom - bottomRadius); + } + else { + path.lineTo(right, bottom); + path.lineTo(left, bottom); + } + path.lineTo(left, top + topRadius); + if (topRadius > 0.0f) { + path.quadTo(left, top, left + topRadius, top); + } + else { + path.lineTo(left, top); + } + path.close(); + } + + private static void setHorizontalRangeOutlinePath(Path path, Rect boundingBox, float radius, float skipLeft, float skipRight) { + float left = boundingBox.left; + float top = boundingBox.top; + float right = boundingBox.right; + float bottom = boundingBox.bottom; + + path.reset(); + if (skipLeft > left) { + float stop = Math.min(skipLeft, right); + path.moveTo(left + radius, top); + path.lineTo(Math.max(left + radius, stop), top); + path.moveTo(left + radius, bottom); + path.lineTo(Math.max(left + radius, stop), bottom); + path.moveTo(left + radius, top); + path.quadTo(left, top, left, top + radius); + path.lineTo(left, bottom - radius); + path.quadTo(left, bottom, left + radius, bottom); + } + + if (skipRight < right) { + float start = Math.max(skipRight, left); + path.moveTo(Math.min(right - radius, start), top); + path.lineTo(right - radius, top); + path.quadTo(right, top, right, top + radius); + path.lineTo(right, bottom - radius); + path.quadTo(right, bottom, right - radius, bottom); + path.moveTo(Math.min(right - radius, start), bottom); + path.lineTo(right - radius, bottom); + } + } + + private static void setVerticalRangeOutlinePath(Path path, Rect boundingBox, float radius, float skipTop, float skipBottom) { + float left = boundingBox.left; + float top = boundingBox.top; + float right = boundingBox.right; + float bottom = boundingBox.bottom; + + path.reset(); + if (skipTop > top) { + float stop = Math.min(skipTop, bottom); + path.moveTo(left + radius, top); + path.lineTo(right - radius, top); + path.quadTo(right, top, right, top + radius); + path.moveTo(left, top + radius); + path.lineTo(left, Math.max(top + radius, stop)); + path.moveTo(right, top + radius); + path.lineTo(right, Math.max(top + radius, stop)); + } + + if (skipBottom < bottom) { + float start = Math.max(skipBottom, top); + path.moveTo(left, Math.min(bottom - radius, start)); + path.lineTo(left, bottom - radius); + path.quadTo(left, bottom, left + radius, bottom); + path.lineTo(right - radius, bottom); + path.quadTo(right, bottom, right, bottom - radius); + path.moveTo(right, Math.min(bottom - radius, start)); + path.lineTo(right, bottom - radius); + } + } + public void draw(Canvas canvas) { int snappingSize = inputControlsView.getSnappingSize(); Paint paint = inputControlsView.getPaint(); - int primaryColor = inputControlsView.getPrimaryColor(); + boolean active = selected || currentPointerId != -1; + boolean editSelected = selected && inputControlsView.isEditMode(); + int normalColor = getAppearanceDrawColor(false); + int activeColor = editSelected ? getEditorSelectionDrawColor() : getAppearanceDrawColor(true); + int primaryColor = active ? activeColor : normalColor; - paint.setColor(selected ? inputControlsView.getSecondaryColor() : primaryColor); + paint.setColor(primaryColor); paint.setStyle(Paint.Style.STROKE); - float strokeWidth = snappingSize * 0.25f; + float strokeWidth = snappingSize * 0.25f * buttonStrokeScale; paint.setStrokeWidth(strokeWidth); Rect boundingBox = getBoundingBox(); @@ -448,7 +720,7 @@ public void draw(Canvas canvas) { } if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); + drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, primaryColor); } else { String text = getDisplayText(); @@ -467,55 +739,41 @@ public void draw(Canvas canvas) { float offsetY = snappingSize * 3 * scale; float start = snappingSize * scale; Path path = inputControlsView.getPath(); - path.reset(); - - path.moveTo(cx, cy - start); - path.lineTo(cx - offsetX, cy - offsetY); - path.lineTo(cx - offsetX, boundingBox.top); - path.lineTo(cx + offsetX, boundingBox.top); - path.lineTo(cx + offsetX, cy - offsetY); - path.close(); - path.moveTo(cx - start, cy); - path.lineTo(cx - offsetY, cy - offsetX); - path.lineTo(boundingBox.left, cy - offsetX); - path.lineTo(boundingBox.left, cy + offsetX); - path.lineTo(cx - offsetY, cy + offsetX); - path.close(); - - path.moveTo(cx, cy + start); - path.lineTo(cx - offsetX, cy + offsetY); - path.lineTo(cx - offsetX, boundingBox.bottom); - path.lineTo(cx + offsetX, boundingBox.bottom); - path.lineTo(cx + offsetX, cy + offsetY); - path.close(); - - path.moveTo(cx + start, cy); - path.lineTo(cx + offsetY, cy - offsetX); - path.lineTo(boundingBox.right, cy - offsetX); - path.lineTo(boundingBox.right, cy + offsetX); - path.lineTo(cx + offsetY, cy + offsetX); - path.close(); + for (byte i = 0; i < 4; i++) { + setDPadDirectionPath(path, i, boundingBox, cx, cy, offsetX, offsetY, start); + boolean directionActive = editSelected || states[i]; - canvas.drawPath(path, paint); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(strokeWidth); + paint.setColor(directionActive ? activeColor : normalColor); + canvas.drawPath(path, paint); + } break; } case RANGE_BUTTON: { Range range = getRange(); - int oldColor = paint.getColor(); + int oldColor = editSelected ? activeColor : normalColor; + int activeRangeIndex = editSelected ? -1 : scroller.getActiveIndex(); float radius = snappingSize * 0.75f * scale; + float activeStrokeWidth = strokeWidth * 1.5f; + float activeRadius = radius + (activeStrokeWidth - strokeWidth) * 0.5f; float elementSize = scroller.getElementSize(); float minTextSize = snappingSize * 2 * scale; float scrollOffset = scroller.getScrollOffset(); byte[] rangeIndex = scroller.getRangeIndex(); Path path = inputControlsView.getPath(); path.reset(); + paint.setColor(oldColor); + paint.setStrokeWidth(strokeWidth); if (orientation == 0) { float lineTop = boundingBox.top + strokeWidth * 0.5f; float lineBottom = boundingBox.bottom - strokeWidth * 0.5f; float startX = boundingBox.left; - canvas.drawRoundRect(startX, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint); + boolean drawActiveSegmentOutline = false; + float activeSegmentLeft = 0; + float activeSegmentRight = 0; canvas.save(); path.addRoundRect(startX, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, Path.Direction.CW); @@ -524,15 +782,29 @@ public void draw(Canvas canvas) { for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { int index = i % range.max; - paint.setStyle(Paint.Style.STROKE); - paint.setColor(oldColor); + float segmentLeft = startX; + float segmentRight = startX + elementSize; + boolean segmentActive = index == activeRangeIndex; + boolean segmentVisible = segmentLeft < boundingBox.right && segmentRight > boundingBox.left; - if (startX > boundingBox.left && startX < boundingBox.right) canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + paint.setStyle(Paint.Style.STROKE); + int previousIndex = (index - 1 + range.max) % range.max; + boolean activeBoundary = index == activeRangeIndex || previousIndex == activeRangeIndex; + paint.setColor(activeBoundary ? activeColor : oldColor); + paint.setStrokeWidth(activeBoundary ? activeStrokeWidth : strokeWidth); + + if (!activeBoundary && startX > boundingBox.left && startX < boundingBox.right) canvas.drawLine(startX, lineTop, startX, lineBottom, paint); + if (segmentActive && segmentVisible) { + drawActiveSegmentOutline = true; + activeSegmentLeft = segmentLeft; + activeSegmentRight = segmentRight; + } + paint.setStrokeWidth(strokeWidth); String text = getRangeTextForIndex(range, index); if (startX < boundingBox.right && startX + elementSize > boundingBox.left) { paint.setStyle(Paint.Style.FILL); - paint.setColor(primaryColor); + paint.setColor(segmentActive ? activeColor : oldColor); paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, elementSize - strokeWidth * 2), minTextSize)); paint.setTextAlign(Paint.Align.CENTER); canvas.drawText(text, startX + elementSize * 0.5f, (y - ((paint.descent() + paint.ascent()) * 0.5f)), paint); @@ -543,12 +815,34 @@ public void draw(Canvas canvas) { paint.setStyle(Paint.Style.STROKE); paint.setColor(oldColor); canvas.restore(); + + paint.setStyle(Paint.Style.STROKE); + paint.setColor(oldColor); + paint.setStrokeWidth(strokeWidth); + if (drawActiveSegmentOutline) { + setHorizontalRangeOutlinePath(path, boundingBox, radius, activeSegmentLeft, activeSegmentRight); + canvas.drawPath(path, paint); + } + else { + canvas.drawRoundRect(boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint); + } + + if (drawActiveSegmentOutline) { + setHorizontalRangeSegmentPath(path, boundingBox, activeSegmentLeft, activeSegmentRight, activeRadius); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(activeColor); + paint.setStrokeWidth(activeStrokeWidth); + canvas.drawPath(path, paint); + paint.setStrokeWidth(strokeWidth); + } } else { float lineLeft = boundingBox.left + strokeWidth * 0.5f; float lineRight = boundingBox.right - strokeWidth * 0.5f; float startY = boundingBox.top; - canvas.drawRoundRect(boundingBox.left, startY, boundingBox.right, boundingBox.bottom, radius, radius, paint); + boolean drawActiveSegmentOutline = false; + float activeSegmentTop = 0; + float activeSegmentBottom = 0; canvas.save(); path.addRoundRect(boundingBox.left, startY, boundingBox.right, boundingBox.bottom, radius, radius, Path.Direction.CW); @@ -556,15 +850,30 @@ public void draw(Canvas canvas) { startY -= scrollOffset % elementSize; for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { - paint.setStyle(Paint.Style.STROKE); - paint.setColor(oldColor); + int index = i % range.max; + float segmentTop = startY; + float segmentBottom = startY + elementSize; + boolean segmentActive = index == activeRangeIndex; + boolean segmentVisible = segmentTop < boundingBox.bottom && segmentBottom > boundingBox.top; - if (startY > boundingBox.top && startY < boundingBox.bottom) canvas.drawLine(lineLeft, startY, lineRight, startY, paint); - String text = getRangeTextForIndex(range, i); + paint.setStyle(Paint.Style.STROKE); + int previousIndex = (index - 1 + range.max) % range.max; + boolean activeBoundary = index == activeRangeIndex || previousIndex == activeRangeIndex; + paint.setColor(activeBoundary ? activeColor : oldColor); + paint.setStrokeWidth(activeBoundary ? activeStrokeWidth : strokeWidth); + + if (!activeBoundary && startY > boundingBox.top && startY < boundingBox.bottom) canvas.drawLine(lineLeft, startY, lineRight, startY, paint); + if (segmentActive && segmentVisible) { + drawActiveSegmentOutline = true; + activeSegmentTop = segmentTop; + activeSegmentBottom = segmentBottom; + } + paint.setStrokeWidth(strokeWidth); + String text = getRangeTextForIndex(range, index); if (startY < boundingBox.bottom && startY + elementSize > boundingBox.top) { paint.setStyle(Paint.Style.FILL); - paint.setColor(primaryColor); + paint.setColor(segmentActive ? activeColor : oldColor); paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), minTextSize)); paint.setTextAlign(Paint.Align.CENTER); canvas.drawText(text, x, startY + elementSize * 0.5f - ((paint.descent() + paint.ascent()) * 0.5f), paint); @@ -575,6 +884,34 @@ public void draw(Canvas canvas) { paint.setStyle(Paint.Style.STROKE); paint.setColor(oldColor); canvas.restore(); + + paint.setStyle(Paint.Style.STROKE); + paint.setColor(oldColor); + paint.setStrokeWidth(strokeWidth); + if (drawActiveSegmentOutline) { + setVerticalRangeOutlinePath(path, boundingBox, radius, activeSegmentTop, activeSegmentBottom); + canvas.drawPath(path, paint); + } + else { + canvas.drawRoundRect(boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, radius, radius, paint); + } + + if (drawActiveSegmentOutline) { + setVerticalRangeSegmentPath(path, boundingBox, activeSegmentTop, activeSegmentBottom, activeRadius); + paint.setStyle(Paint.Style.STROKE); + paint.setColor(activeColor); + paint.setStrokeWidth(activeStrokeWidth); + canvas.drawPath(path, paint); + paint.setStrokeWidth(strokeWidth); + } + } + + if (editSelected) { + paint.setStyle(Paint.Style.STROKE); + paint.setColor(activeColor); + paint.setStrokeWidth(activeStrokeWidth); + canvas.drawRoundRect(boundingBox.left, boundingBox.top, boundingBox.right, boundingBox.bottom, activeRadius, activeRadius, paint); + paint.setStrokeWidth(strokeWidth); } break; } @@ -589,7 +926,7 @@ public void draw(Canvas canvas) { short thumbRadius = (short) (snappingSize * 3.5f * scale); paint.setStyle(Paint.Style.FILL); - paint.setColor(ColorUtils.setAlphaComponent(primaryColor, 50)); + paint.setColor(ColorUtils.setAlphaComponent(primaryColor, Math.min(50, primaryColor >>> 24))); canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius, paint); paint.setStyle(Paint.Style.STROKE); @@ -614,21 +951,20 @@ public void draw(Canvas canvas) { float halfW = boundingBox.width() * 0.5f; if (selected) { - // Fill with semi-transparent blue when active paint.setStyle(Paint.Style.FILL); - paint.setColor(ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), 80)); + paint.setColor(ColorUtils.setAlphaComponent(primaryColor, Math.min(80, primaryColor >>> 24))); canvas.drawCircle(cx, cy, halfW, paint); } // Draw outline paint.setStyle(Paint.Style.STROKE); - paint.setColor(selected ? inputControlsView.getSecondaryColor() : primaryColor); + paint.setColor(primaryColor); paint.setStrokeWidth(strokeWidth); canvas.drawCircle(cx, cy, halfW, paint); // Draw icon or fallback text if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId); + drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, primaryColor); } else { String displayText = (text != null && !text.isEmpty()) ? text : "DJ"; paint.setTextSize(Math.min(getTextSizeForWidth(paint, displayText, boundingBox.width() - strokeWidth * 2), snappingSize * 2 * scale)); @@ -642,10 +978,10 @@ public void draw(Canvas canvas) { } } - private void drawIcon(Canvas canvas, float cx, float cy, float width, float height, int iconId) { + private void drawIcon(Canvas canvas, float cx, float cy, float width, float height, int iconId, int tintColor) { Paint paint = inputControlsView.getPaint(); Bitmap icon = inputControlsView.getIcon((byte)iconId); - paint.setColorFilter(inputControlsView.getColorFilter()); + paint.setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); int margin = (int)(inputControlsView.getSnappingSize() * (shape == Shape.CIRCLE || shape == Shape.SQUARE ? 2.0f : 1.0f) * scale); int halfSize = (int)((Math.min(width, height) - margin) * 0.5f); @@ -685,6 +1021,12 @@ public JSONObject toJSONObject() { elementJSONObject.put("shooterJoystickSize", (double) shooterJoystickSize); } + if (buttonColor != DEFAULT_BUTTON_COLOR) elementJSONObject.put("buttonColor", formatRgbColor(buttonColor)); + if (buttonActiveColor != DEFAULT_BUTTON_ACTIVE_COLOR) elementJSONObject.put("buttonActiveColor", formatRgbColor(buttonActiveColor)); + if (buttonOpacity >= 0) elementJSONObject.put("buttonOpacity", (double)buttonOpacity); + if (buttonStrokeScale != DEFAULT_BUTTON_STROKE_SCALE) elementJSONObject.put("buttonStrokeScale", (double)buttonStrokeScale); + if (type == Type.BUTTON && !shooterLookThrough) elementJSONObject.put("shooterLookThrough", false); + return elementJSONObject; } catch (JSONException e) { @@ -704,12 +1046,14 @@ private boolean isKeepButtonPressedAfterMinTime() { public boolean handleTouchDown(int pointerId, float x, float y) { if (currentPointerId == -1 && containsPoint(x, y)) { currentPointerId = pointerId; + inputControlsView.invalidate(); if (type == Type.BUTTON) { if (isKeepButtonPressedAfterMinTime()) touchTime = System.currentTimeMillis(); if (!toggleSwitch || !selected) { inputControlsView.handleInputEvent(getBindingAt(0), true); inputControlsView.handleInputEvent(getBindingAt(1), true); } + inputControlsView.invalidate(); return true; } else if (type == Type.SHOOTER_MODE) { @@ -832,6 +1176,8 @@ else if (binding == Binding.MOUSE_MOVE_UP || binding == Binding.MOUSE_MOVE_DOWN) inputControlsView.handleInputEvent(binding, state, value); this.states[i] = state; } + + inputControlsView.invalidate(); } return true; @@ -864,6 +1210,9 @@ else if (!toggleSwitch || selected) { selected = !selected; inputControlsView.invalidate(); } + else { + inputControlsView.invalidate(); + } } else if (type == Type.RANGE_BUTTON || type == Type.D_PAD || type == Type.STICK || type == Type.TRACKPAD) { for (byte i = 0; i < states.length; i++) { @@ -886,6 +1235,7 @@ else if (type == Type.SHOOTER_MODE) { inputControlsView.invalidate(); } currentPointerId = -1; + inputControlsView.invalidate(); return true; } return false; diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java index c816bd9b82..304d105b2f 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java @@ -278,6 +278,15 @@ public void loadElements(InputControlsView inputControlsView) { if (elementJSONObject.has("shooterLookType")) element.setShooterLookType(elementJSONObject.getString("shooterLookType")); if (elementJSONObject.has("shooterLookSensitivity")) element.setShooterLookSensitivity((float)elementJSONObject.getDouble("shooterLookSensitivity")); if (elementJSONObject.has("shooterJoystickSize")) element.setShooterJoystickSize((float)elementJSONObject.getDouble("shooterJoystickSize")); + if (elementJSONObject.has("buttonColor")) { + element.setButtonColor(ControlElement.parseRgbColor(elementJSONObject.get("buttonColor"), ControlElement.DEFAULT_BUTTON_COLOR)); + } + if (elementJSONObject.has("buttonActiveColor")) { + element.setButtonActiveColor(ControlElement.parseRgbColor(elementJSONObject.get("buttonActiveColor"), ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR)); + } + if (elementJSONObject.has("buttonOpacity")) element.setButtonOpacity((float)elementJSONObject.getDouble("buttonOpacity")); + if (elementJSONObject.has("buttonStrokeScale")) element.setButtonStrokeScale((float)elementJSONObject.getDouble("buttonStrokeScale")); + if (elementJSONObject.has("shooterLookThrough")) element.setShooterLookThrough(elementJSONObject.getBoolean("shooterLookThrough")); boolean hasGamepadBinding = true; JSONArray bindingsJSONArray = elementJSONObject.getJSONArray("bindings"); diff --git a/app/src/main/java/com/winlator/inputcontrols/RangeScroller.java b/app/src/main/java/com/winlator/inputcontrols/RangeScroller.java index 2601afb998..112ff6b275 100644 --- a/app/src/main/java/com/winlator/inputcontrols/RangeScroller.java +++ b/app/src/main/java/com/winlator/inputcontrols/RangeScroller.java @@ -16,6 +16,7 @@ public class RangeScroller { private float lastPosition; private long touchTime; private Binding binding = Binding.NONE; + private int activeIndex = -1; private boolean isActionDown = false; private boolean scrolling = false; private Timer timer; @@ -46,13 +47,21 @@ public byte[] getRangeIndex() { return new byte[]{from, to}; } - private Binding getBindingByPosition(float x, float y) { + public int getActiveIndex() { + return activeIndex; + } + + private int getIndexByPosition(float x, float y) { Rect boundingBox = element.getBoundingBox(); ControlElement.Range range = element.getRange(); float offset = element.getOrientation() == 0 ? x - boundingBox.left - currentOffset : y - boundingBox.top - currentOffset; int index = (int)Math.floor((offset / getElementSize()) % range.max); if (index < 0) index = range.max + index; + return index; + } + private Binding getBindingByIndex(int index) { + ControlElement.Range range = element.getRange(); switch (range) { case FROM_A_TO_Z: return Binding.valueOf("KEY_"+((char)(65 + index))); @@ -83,10 +92,12 @@ public void handleTouchDown(float x, float y) { scrolling = false; isActionDown = true; - binding = getBindingByPosition(x, y); + activeIndex = getIndexByPosition(x, y); + binding = getBindingByIndex(activeIndex); touchTime = System.currentTimeMillis(); lastPosition = element.getOrientation() == 0 ? x : y; element.setBinding(Binding.NONE); + inputControlsView.invalidate(); timer = new Timer(true); timer.schedule(new TimerTask() { @@ -106,7 +117,9 @@ public void handleTouchMove(float x, float y) { if (Math.abs(deltaPosition) >= TouchpadView.MAX_TAP_TRAVEL_DISTANCE) { scrolling = true; + activeIndex = -1; destroyTimer(); + inputControlsView.invalidate(); } if (scrolling) { @@ -133,5 +146,7 @@ public void handleTouchUp() { else inputControlsView.handleInputEvent(binding, false); } isActionDown = false; + activeIndex = -1; + inputControlsView.invalidate(); } } diff --git a/app/src/main/java/com/winlator/widget/InputControlsView.java b/app/src/main/java/com/winlator/widget/InputControlsView.java index e82c66cb7e..c5d433b39e 100644 --- a/app/src/main/java/com/winlator/widget/InputControlsView.java +++ b/app/src/main/java/com/winlator/widget/InputControlsView.java @@ -146,6 +146,10 @@ public void setOverlayOpacity(float overlayOpacity) { this.overlayOpacity = overlayOpacity; } + public float getOverlayOpacity() { + return overlayOpacity; + } + public int getSnappingSize() { return snappingSize; } @@ -1019,7 +1023,7 @@ private boolean handleShooterTouchDown(int pointerId, float x, float y) { if (element.handleTouchDown(pointerId, x, y)) { performHapticFeedback(android.view.HapticFeedbackConstants.VIRTUAL_KEY); handled = true; - if (element.getType() == ControlElement.Type.BUTTON) { + if (element.getType() == ControlElement.Type.BUTTON && element.isShooterLookThrough()) { startPendingButtonLookPointer(pointerId, x, y, element); } break; diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 48b5470542..943d196bf5 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1899,4 +1899,27 @@ Tryk sprint-input én gang, når sprint-ringen nås, i stedet for at holde det nede Sprintbinding Gamepad + Kontroludseende + Kontrolfarve + Hexfarve for kontrollens omrids, ikon og tekst + Trykket farve + Farve mens den trykkes eller er aktiv + Kontrolopacitet + %1$d%% + %1$d%% (global standard) + Linjetykkelse + Juster tykkelsen på kontrollens omrids og opdelere + Tynd + Standard + Tyk + Ekstra tyk + Shooter-gennemkig + Tillad at denne knapberøring også styrer dynamisk shooter-bevægelse og blik + Forhåndsvisning + Aktiv + Anvend alle + Kopiér udseende fra kontrol + Ingen andre kontroller at kopiere udseende fra + Kontroludseende kopieret + Udseende anvendt på alle kontroller diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 004188c0e8..1ba05f6e0e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1969,4 +1969,27 @@ Sprint-Eingabe beim Erreichen des Sprintrings einmal antippen statt gedrückt halten Sprintbindung Gamepad + Steuerungsdarstellung + Steuerungsfarbe + Hex-Farbe für Umriss, Symbol und Text der Steuerung + Farbe beim Drücken + Farbe während gedrückt oder aktiv + Deckkraft der Steuerung + %1$d%% + %1$d%% (globaler Standard) + Linienstärke + Stärke von Umriss und Trennlinien der Steuerung anpassen + Dünn + Standard + Dick + Extra dick + Shooter-Durchgriff + Diese Tastenberührung auch dynamische Shooter-Bewegung und Blick steuern lassen + Vorschau + Aktiv + Auf alle anwenden + Darstellung von Steuerung kopieren + Keine anderen Steuerungen zum Kopieren der Darstellung verfügbar + Steuerungsdarstellung kopiert + Darstellung auf alle Steuerungen angewendet diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2b1dcc117b..66765fec4f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2027,4 +2027,27 @@ Toca la entrada de sprint una vez al entrar en el anillo de sprint en vez de mantenerla presionada Acción de sprint Gamepad + Apariencia del control + Color del control + Color hexadecimal para el contorno, icono y texto del control + Color al pulsar + Color mientras está pulsado o activo + Opacidad del control + %1$d%% + %1$d%% (valor global predeterminado) + Grosor de línea + Ajusta el grosor del contorno y los divisores del control + Fino + Predeterminado + Grueso + Extra grueso + Paso de mirada shooter + Permitir que este toque de botón también controle el movimiento y la mirada shooter dinámicos + Vista previa + Activo + Aplicar a todos + Copiar apariencia de control + No hay otros controles disponibles para copiar la apariencia + Apariencia del control copiada + Apariencia aplicada a todos los controles diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f582345e63..92e74a34cb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2029,4 +2029,27 @@ Appuie une fois sur l’entrée de sprint en entrant dans l’anneau de sprint au lieu de la maintenir Action de sprint Manette + Apparence du contrôle + Couleur du contrôle + Couleur hexadécimale pour le contour, icône et texte du contrôle + Couleur enfoncée + Couleur quand le contrôle est enfoncé ou actif + Opacité du contrôle + %1$d%% + %1$d%% (valeur globale par défaut) + Épaisseur des lignes + Ajuster épaisseur du contour et des séparateurs du contrôle + Fin + Par défaut + Épais + Très épais + Passage du regard shooter + Permettre à ce toucher de bouton de contrôler aussi le mouvement et le regard shooter dynamiques + Aperçu + Actif + Appliquer à tous + Copier apparence depuis un contrôle + Aucun autre contrôle disponible pour copier apparence + Apparence du contrôle copiée + Apparence appliquée à tous les contrôles diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1c9e1f6a59..c2c5fed92d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2020,4 +2020,27 @@ Tocca una volta input sprint entrando nell\'anello sprint invece di tenerlo premuto Azione sprint Gamepad + Aspetto controllo + Colore controllo + Colore esadecimale per contorno, icona e testo del controllo + Colore premuto + Colore mentre è premuto o attivo + Opacità controllo + %1$d%% + %1$d%% (predefinito globale) + Spessore linea + Regola lo spessore del contorno e dei divisori del controllo + Sottile + Predefinito + Spesso + Molto spesso + Passaggio mira shooter + Consenti a questo tocco del pulsante di controllare anche movimento e mira shooter dinamici + Anteprima + Attivo + Applica a tutti + Copia aspetto da controllo + Nessun altro controllo disponibile da cui copiare aspetto + Aspetto del controllo copiato + Aspetto applicato a tutti i controlli diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b64d93618c..c2ace16492 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1986,4 +1986,27 @@ スプリントリングに入ったとき、長押しではなく一度だけスプリント入力をタップします スプリント入力 ゲームパッド + コントロールの外観 + コントロールの色 + コントロールの枠線、アイコン、テキストの16進カラー + 押下時の色 + 押下中またはアクティブ時の色 + コントロールの不透明度 + %1$d%% + %1$d%%(グローバル既定値) + 線の太さ + コントロールの枠線と区切り線の太さを調整 + 細い + 既定 + 太い + 極太 + シューター透過操作 + このボタンのタッチでも動的シューター移動や視点操作を行う + プレビュー + アクティブ + すべてに適用 + コントロールから外観をコピー + 外観をコピーできる他のコントロールがありません + コントロールの外観をコピーしました + すべてのコントロールに外観を適用しました diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4863f8fd75..35860b3b84 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2027,4 +2027,27 @@ 스프린트 링에 들어갈 때 길게 누르는 대신 스프린트 입력을 한 번 탭합니다 스프린트 입력 게임패드 + 컨트롤 모양 + 컨트롤 색상 + 컨트롤 윤곽선, 아이콘, 텍스트의 16진수 색상 + 누름 색상 + 누르거나 활성 상태일 때의 색상 + 컨트롤 불투명도 + %1$d%% + %1$d%% (전역 기본값) + 선 두께 + 컨트롤 윤곽선과 구분선 두께 조정 + 얇게 + 기본 + 두껍게 + 매우 두껍게 + 슈터 통과 입력 + 이 버튼 터치가 동적 슈터 이동 및 시점도 제어하도록 허용 + 미리보기 + 활성 + 모두 적용 + 컨트롤에서 모양 복사 + 모양을 복사할 다른 컨트롤이 없습니다 + 컨트롤 모양을 복사했습니다 + 모든 컨트롤에 모양을 적용했습니다 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index fdbecc34be..4732e112e8 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2027,4 +2027,27 @@ Dotknij wejścia sprintu raz po wejściu w pierścień sprintu zamiast je przytrzymywać Akcja sprintu Gamepad + Wygląd elementu sterującego + Kolor elementu + Kolor hex obrysu, ikony i tekstu elementu + Kolor po naciśnięciu + Kolor podczas naciśnięcia lub aktywności + Nieprzezroczystość elementu + %1$d%% + %1$d%% (domyślne globalne) + Grubość linii + Dostosuj grubość obrysu i separatorów elementu + Cienka + Domyślna + Gruba + Bardzo gruba + Przepuszczanie widoku shooter + Pozwól temu dotknięciu przycisku także sterować dynamicznym ruchem i widokiem shooter + Podgląd + Aktywny + Zastosuj do wszystkich + Kopiuj wygląd z elementu + Brak innych elementów do skopiowania wyglądu + Skopiowano wygląd elementu + Zastosowano wygląd do wszystkich elementów diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b1ad1f63e3..e02a10b643 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1899,4 +1899,27 @@ Toca a entrada de sprint uma vez ao entrar no anel de sprint em vez de manter pressionado Ação de sprint Gamepad + Aparência do controle + Cor do controle + Cor hexadecimal do contorno, ícone e texto do controle + Cor ao pressionar + Cor enquanto pressionado ou ativo + Opacidade do controle + %1$d%% + %1$d%% (padrão global) + Espessura da linha + Ajusta a espessura do contorno e divisórias do controle + Fina + Padrão + Grossa + Extra grossa + Passagem de mira shooter + Permitir que este toque de botão também controle movimento e mira shooter dinâmicos + Prévia + Ativo + Aplicar a todos + Copiar aparência de um controle + Nenhum outro controle disponível para copiar aparência + Aparência do controle copiada + Aparência aplicada a todos os controles diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ae8fd886c5..9bab97cf26 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2030,4 +2030,27 @@ Atinge input-ul de sprint o dată la intrarea în inelul de sprint în loc să îl menții apăsat Acțiune sprint Gamepad + Aspect control + Culoare control + Culoare hex pentru conturul, pictograma și textul controlului + Culoare la apăsare + Culoare când este apăsat sau activ + Opacitate control + %1$d%% + %1$d%% (implicit global) + Grosime linie + Ajustează grosimea conturului și separatorilor controlului + Subțire + Implicit + Gros + Foarte gros + Trecere privire shooter + Permite ca această atingere a butonului să controleze și mișcarea și privirea shooter dinamice + Previzualizare + Activ + Aplică tuturor + Copiază aspectul de la control + Nu există alte controale de la care să copiezi aspectul + Aspectul controlului a fost copiat + Aspectul a fost aplicat tuturor controalelor diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c38dba5481..df46d0a679 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1955,4 +1955,27 @@ https://gamenative.app Однократно нажимает ввод спринта при входе в кольцо спринта вместо удержания Действие спринта Геймпад + Внешний вид элемента + Цвет элемента + HEX-цвет контура, значка и текста элемента + Цвет при нажатии + Цвет при нажатии или активности + Непрозрачность элемента + %1$d%% + %1$d%% (глобально по умолчанию) + Толщина линии + Настроить толщину контура и разделителей элемента + Тонкая + По умолчанию + Толстая + Очень толстая + Сквозной ввод shooter + Разрешить этому касанию кнопки также управлять динамическим движением и обзором shooter + Предпросмотр + Активно + Применить ко всем + Копировать внешний вид из элемента + Нет других элементов для копирования внешнего вида + Внешний вид элемента скопирован + Внешний вид применен ко всем элементам diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 004c33e167..1348b74557 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2023,4 +2023,27 @@ Один раз натискає ввід спринту під час входу в кільце спринту замість утримання Дія спринту Геймпад + Вигляд елемента + Колір елемента + HEX-колір контуру, піктограми та тексту елемента + Колір під час натискання + Колір, коли елемент натиснуто або активний + Непрозорість елемента + %1$d%% + %1$d%% (глобально за замовчуванням) + Товщина лінії + Налаштувати товщину контуру та розділювачів елемента + Тонка + За замовчуванням + Товста + Дуже товста + Наскрізне введення shooter + Дозволити цьому дотику кнопки також керувати динамічним рухом і оглядом shooter + Попередній перегляд + Активно + Застосувати до всіх + Копіювати вигляд з елемента + Немає інших елементів для копіювання вигляду + Вигляд елемента скопійовано + Вигляд застосовано до всіх елементів diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0e16397428..999327f84b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2047,4 +2047,27 @@ 进入冲刺环时点按一次冲刺输入,而不是按住 冲刺绑定 游戏手柄 + 控件外观 + 控件颜色 + 控件轮廓、图标和文本的十六进制颜色 + 按下颜色 + 按下或激活时的颜色 + 控件不透明度 + %1$d%% + %1$d%%(全局默认) + 线条粗细 + 调整控件轮廓和分隔线粗细 + + 默认 + + 特粗 + 射击模式穿透 + 允许此按钮触摸同时控制动态射击移动和视角 + 预览 + 激活 + 全部应用 + 从控件复制外观 + 没有其他可复制外观的控件 + 已复制控件外观 + 已将外观应用到所有控件 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f3044c8809..d96003706f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2038,4 +2038,27 @@ 進入衝刺環時點按一次衝刺輸入,而不是按住 衝刺綁定 遊戲手把 + 控制項外觀 + 控制項顏色 + 控制項外框、圖示和文字的十六進位顏色 + 按下顏色 + 按下或啟用時的顏色 + 控制項不透明度 + %1$d%% + %1$d%%(全域預設) + 線條粗細 + 調整控制項外框和分隔線粗細 + + 預設 + + 特粗 + 射擊模式穿透 + 允許此按鈕觸控同時控制動態射擊移動和視角 + 預覽 + 啟用 + 全部套用 + 從控制項複製外觀 + 沒有其他可複製外觀的控制項 + 已複製控制項外觀 + 已將外觀套用到所有控制項 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89e1faaa8a..d1144c498d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -394,6 +394,29 @@ Button Settings Toggleable Tap to activate, tap again to deactivate + Control Appearance + Control Color + Hex color for the control outline, icon, and text + Pressed Color + Color while pressed or active + Control Opacity + %1$d%% + %1$d%% (global default) + Line Thickness + Adjust control outline and divider thickness + Thin + Default + Thick + Extra thick + Shooter Look-through + Allow this button touch to also control dynamic shooter movement/look + Preview + Active + Apply All + Copy Appearance From Control + No other controls available to copy appearance from + Copied control appearance + Applied appearance to all controls Primary Action Secondary Action Up From e9628d2bdf46214eb45242c6660b4cd8ccd084e9 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Fri, 3 Jul 2026 19:20:10 -0500 Subject: [PATCH 2/8] Fix rectangular control appearance preview sizing --- .../gamenative/ui/component/dialog/ControlAppearanceEditor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt index f25779207f..7cde754ce6 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -196,7 +196,7 @@ private fun ButtonPreview( val strokeWidth = 2.dp * strokeScale Box( modifier = Modifier - .then(if (shape == ControlElement.Shape.CIRCLE || shape == ControlElement.Shape.SQUARE) Modifier.size(56.dp) else Modifier.height(48.dp).fillMaxWidth()) + .then(if (shape == ControlElement.Shape.CIRCLE || shape == ControlElement.Shape.SQUARE) Modifier.size(56.dp) else Modifier.width(96.dp).height(48.dp)) .clip(composeShape) .border(strokeWidth, drawColor, composeShape), contentAlignment = Alignment.Center From 7dcccbf96b774a8b405365f0a9fa10e12d1db1c3 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Sun, 5 Jul 2026 00:10:53 -0500 Subject: [PATCH 3/8] Preserve default selected control coloring --- .../dialog/ControlAppearanceEditor.kt | 4 ++- .../component/dialog/ElementEditorDialog.kt | 8 ++++- .../inputcontrols/ControlElement.java | 33 +++++++++++++++---- .../inputcontrols/ControlsProfile.java | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt index 7cde754ce6..14c0e45318 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -33,13 +33,14 @@ import com.winlator.widget.InputControlsView internal data class ControlAppearance( val color: Int, val activeColor: Int, + val activeColorCustom: Boolean, val opacity: Float, val strokeScale: Float, val shooterLookThrough: Boolean ) { fun applyTo(element: ControlElement) { element.setButtonColor(color) - element.setButtonActiveColor(activeColor) + element.setButtonActiveColor(activeColor, activeColorCustom) element.setButtonOpacity(opacity) element.setButtonStrokeScale(strokeScale) element.setShooterLookThrough(shooterLookThrough) @@ -49,6 +50,7 @@ internal data class ControlAppearance( fun capture(element: ControlElement) = ControlAppearance( color = element.buttonColor, activeColor = element.buttonActiveColor, + activeColorCustom = element.hasCustomButtonActiveColor(), opacity = element.buttonOpacity, strokeScale = element.buttonStrokeScale, shooterLookThrough = element.isShooterLookThrough diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt index 705a7c98d0..3545de590d 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt @@ -188,6 +188,7 @@ fun ElementEditorDialog( var currentButtonColor by remember { mutableIntStateOf(element.buttonColor) } var currentButtonColorText by remember { mutableStateOf(rgbToHex(element.buttonColor)) } var currentButtonActiveColor by remember { mutableIntStateOf(element.buttonActiveColor) } + var currentButtonActiveColorCustom by remember { mutableStateOf(element.hasCustomButtonActiveColor()) } var currentButtonActiveColorText by remember { mutableStateOf(rgbToHex(element.buttonActiveColor)) } var currentButtonOpacityInherited by remember { mutableStateOf(element.buttonOpacity < 0f) } var currentButtonOpacity by remember { @@ -208,7 +209,7 @@ fun ElementEditorDialog( fun applyCurrentControlAppearance() { element.setButtonColor(currentButtonColor) - element.setButtonActiveColor(currentButtonActiveColor) + element.setButtonActiveColor(currentButtonActiveColor, currentButtonActiveColorCustom) element.setButtonOpacity( if (currentButtonOpacityInherited) ControlElement.INHERIT_BUTTON_OPACITY else currentButtonOpacity ) @@ -219,6 +220,7 @@ fun ElementEditorDialog( fun currentAppearance() = ControlAppearance( color = currentButtonColor, activeColor = currentButtonActiveColor, + activeColorCustom = currentButtonActiveColorCustom, opacity = if (currentButtonOpacityInherited) ControlElement.INHERIT_BUTTON_OPACITY else currentButtonOpacity, strokeScale = currentButtonStrokeScale, shooterLookThrough = currentShooterLookThrough @@ -838,11 +840,13 @@ fun ElementEditorDialog( currentButtonActiveColorText = value.uppercase(Locale.US) parseRgbHex(value)?.let { currentButtonActiveColor = it + currentButtonActiveColorCustom = true hasUnsavedChanges = true } }, onPresetSelected = { currentButtonActiveColor = it + currentButtonActiveColorCustom = true currentButtonActiveColorText = rgbToHex(it) hasUnsavedChanges = true } @@ -942,6 +946,7 @@ fun ElementEditorDialog( currentButtonColor = appearance.color currentButtonColorText = rgbToHex(appearance.color) currentButtonActiveColor = appearance.activeColor + currentButtonActiveColorCustom = appearance.activeColorCustom currentButtonActiveColorText = rgbToHex(appearance.activeColor) currentButtonOpacityInherited = appearance.opacity < 0f currentButtonOpacity = if (appearance.opacity >= 0f) appearance.opacity else view.overlayOpacity @@ -976,6 +981,7 @@ fun ElementEditorDialog( currentButtonColor = ControlElement.DEFAULT_BUTTON_COLOR currentButtonColorText = rgbToHex(ControlElement.DEFAULT_BUTTON_COLOR) currentButtonActiveColor = ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR + currentButtonActiveColorCustom = false currentButtonActiveColorText = rgbToHex(ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR) currentButtonOpacityInherited = true currentButtonOpacity = view.overlayOpacity diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index 3c8e696e4b..ec6f67db20 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -98,6 +98,7 @@ public static String[] names() { private float shooterJoystickSize = 1.0f; private int buttonColor = DEFAULT_BUTTON_COLOR; private int buttonActiveColor = DEFAULT_BUTTON_ACTIVE_COLOR; + private boolean buttonActiveColorCustom = false; private float buttonOpacity = INHERIT_BUTTON_OPACITY; private float buttonStrokeScale = DEFAULT_BUTTON_STROKE_SCALE; private boolean shooterLookThrough = true; @@ -280,7 +281,16 @@ public int getButtonActiveColor() { } public void setButtonActiveColor(int buttonActiveColor) { + setButtonActiveColor(buttonActiveColor, true); + } + + public void setButtonActiveColor(int buttonActiveColor, boolean custom) { this.buttonActiveColor = buttonActiveColor & 0x00ffffff; + this.buttonActiveColorCustom = custom; + } + + public boolean hasCustomButtonActiveColor() { + return buttonActiveColorCustom; } public float getButtonOpacity() { @@ -315,6 +325,7 @@ public void setShooterLookThrough(boolean shooterLookThrough) { public void copyButtonAppearanceFrom(ControlElement element) { buttonColor = element.buttonColor; buttonActiveColor = element.buttonActiveColor; + buttonActiveColorCustom = element.buttonActiveColorCustom; buttonOpacity = element.buttonOpacity; buttonStrokeScale = element.buttonStrokeScale; shooterLookThrough = element.shooterLookThrough; @@ -509,11 +520,17 @@ private int getAppearanceDrawColor(boolean active) { } private int getEditorSelectionDrawColor() { - int rgb = buttonActiveColor != DEFAULT_BUTTON_ACTIVE_COLOR ? buttonActiveColor : inputControlsView.getSecondaryColor(); + int rgb = buttonActiveColorCustom ? buttonActiveColor : inputControlsView.getSecondaryColor(); int alpha = (int)(getEffectiveButtonOpacity(inputControlsView.getOverlayOpacity()) * 255); return ColorUtils.setAlphaComponent(0xff000000 | (rgb & 0x00ffffff), alpha); } + private int getRuntimeSelectedDrawColor() { + if (buttonActiveColorCustom) return getAppearanceDrawColor(true); + int alpha = (int)(getEffectiveButtonOpacity(inputControlsView.getOverlayOpacity()) * 255); + return ColorUtils.setAlphaComponent(inputControlsView.getSecondaryColor(), alpha); + } + private static void setDPadDirectionPath(Path path, byte direction, Rect boundingBox, float cx, float cy, float offsetX, float offsetY, float start) { path.reset(); switch (direction) { @@ -687,7 +704,9 @@ public void draw(Canvas canvas) { boolean editSelected = selected && inputControlsView.isEditMode(); int normalColor = getAppearanceDrawColor(false); int activeColor = editSelected ? getEditorSelectionDrawColor() : getAppearanceDrawColor(true); + if (selected && !editSelected) activeColor = getRuntimeSelectedDrawColor(); int primaryColor = active ? activeColor : normalColor; + int contentColor = selected && !buttonActiveColorCustom ? normalColor : primaryColor; paint.setColor(primaryColor); paint.setStyle(Paint.Style.STROKE); @@ -720,14 +739,14 @@ public void draw(Canvas canvas) { } if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, primaryColor); + drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, contentColor); } else { String text = getDisplayText(); paint.setTextSize(Math.min(getTextSizeForWidth(paint, text, boundingBox.width() - strokeWidth * 2), snappingSize * 2 * scale)); paint.setTextAlign(Paint.Align.CENTER); paint.setStyle(Paint.Style.FILL); - paint.setColor(primaryColor); + paint.setColor(contentColor); canvas.drawText(text, x, (y - ((paint.descent() + paint.ascent()) * 0.5f)), paint); } break; @@ -926,7 +945,7 @@ public void draw(Canvas canvas) { short thumbRadius = (short) (snappingSize * 3.5f * scale); paint.setStyle(Paint.Style.FILL); - paint.setColor(ColorUtils.setAlphaComponent(primaryColor, Math.min(50, primaryColor >>> 24))); + paint.setColor(ColorUtils.setAlphaComponent(contentColor, Math.min(50, contentColor >>> 24))); canvas.drawCircle(thumbstickX, thumbstickY, thumbRadius, paint); paint.setStyle(Paint.Style.STROKE); @@ -964,13 +983,13 @@ public void draw(Canvas canvas) { // Draw icon or fallback text if (iconId > 0) { - drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, primaryColor); + drawIcon(canvas, cx, cy, boundingBox.width(), boundingBox.height(), iconId, contentColor); } else { String displayText = (text != null && !text.isEmpty()) ? text : "DJ"; paint.setTextSize(Math.min(getTextSizeForWidth(paint, displayText, boundingBox.width() - strokeWidth * 2), snappingSize * 2 * scale)); paint.setTextAlign(Paint.Align.CENTER); paint.setStyle(Paint.Style.FILL); - paint.setColor(primaryColor); + paint.setColor(contentColor); canvas.drawText(displayText, cx, (cy - ((paint.descent() + paint.ascent()) * 0.5f)), paint); } break; @@ -1022,7 +1041,7 @@ public JSONObject toJSONObject() { } if (buttonColor != DEFAULT_BUTTON_COLOR) elementJSONObject.put("buttonColor", formatRgbColor(buttonColor)); - if (buttonActiveColor != DEFAULT_BUTTON_ACTIVE_COLOR) elementJSONObject.put("buttonActiveColor", formatRgbColor(buttonActiveColor)); + if (buttonActiveColorCustom || buttonActiveColor != DEFAULT_BUTTON_ACTIVE_COLOR) elementJSONObject.put("buttonActiveColor", formatRgbColor(buttonActiveColor)); if (buttonOpacity >= 0) elementJSONObject.put("buttonOpacity", (double)buttonOpacity); if (buttonStrokeScale != DEFAULT_BUTTON_STROKE_SCALE) elementJSONObject.put("buttonStrokeScale", (double)buttonStrokeScale); if (type == Type.BUTTON && !shooterLookThrough) elementJSONObject.put("shooterLookThrough", false); diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java index 304d105b2f..91b40b01e8 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlsProfile.java @@ -282,7 +282,7 @@ public void loadElements(InputControlsView inputControlsView) { element.setButtonColor(ControlElement.parseRgbColor(elementJSONObject.get("buttonColor"), ControlElement.DEFAULT_BUTTON_COLOR)); } if (elementJSONObject.has("buttonActiveColor")) { - element.setButtonActiveColor(ControlElement.parseRgbColor(elementJSONObject.get("buttonActiveColor"), ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR)); + element.setButtonActiveColor(ControlElement.parseRgbColor(elementJSONObject.get("buttonActiveColor"), ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR), true); } if (elementJSONObject.has("buttonOpacity")) element.setButtonOpacity((float)elementJSONObject.getDouble("buttonOpacity")); if (elementJSONObject.has("buttonStrokeScale")) element.setButtonStrokeScale((float)elementJSONObject.getDouble("buttonStrokeScale")); From fb56e97fa8e544c9d62b9fcd17e54671b75f418b Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Sun, 5 Jul 2026 11:44:27 -0500 Subject: [PATCH 4/8] Fix range and trackpad appearance previews --- .../ui/component/dialog/ControlAppearanceEditor.kt | 14 +++++++++++--- .../com/winlator/inputcontrols/ControlElement.java | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt index 14c0e45318..e8b0276f99 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -372,7 +372,8 @@ private fun RangeButtonPreview( outlinePath.moveTo(left + radius, top) outlinePath.lineTo(right - radius, top) outlinePath.quadraticTo(right, top, right, top + radius) - outlinePath.moveTo(left, top + radius) + outlinePath.moveTo(left + radius, top) + outlinePath.quadraticTo(left, top, left, top + radius) outlinePath.lineTo(left, maxOf(top + radius, activeTop)) outlinePath.moveTo(right, top + radius) outlinePath.lineTo(right, maxOf(top + radius, activeTop)) @@ -482,11 +483,18 @@ private fun StickPreview(normalColor: Color, activeColor: Color, active: Boolean @Composable private fun TrackpadPreview(normalColor: Color, activeColor: Color, active: Boolean, strokeScale: Float) { - Canvas(modifier = Modifier.height(56.dp).fillMaxWidth()) { + Canvas(modifier = Modifier.size(72.dp)) { val strokeWidth = 2.dp.toPx() * strokeScale val stroke = Stroke(width = strokeWidth) + val halfStroke = strokeWidth * 0.5f val corner = CornerRadius(size.height * 0.15f, size.height * 0.15f) - drawRoundRect(normalColor, size = size, cornerRadius = corner, style = stroke) + drawRoundRect( + color = normalColor, + topLeft = Offset(halfStroke, halfStroke), + size = Size(size.width - strokeWidth, size.height - strokeWidth), + cornerRadius = corner, + style = stroke + ) val inset = strokeWidth * 4f drawRoundRect( color = if (active) activeColor else normalColor, diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index ec6f67db20..6c06831ce4 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -679,7 +679,8 @@ private static void setVerticalRangeOutlinePath(Path path, Rect boundingBox, flo path.moveTo(left + radius, top); path.lineTo(right - radius, top); path.quadTo(right, top, right, top + radius); - path.moveTo(left, top + radius); + path.moveTo(left + radius, top); + path.quadTo(left, top, left, top + radius); path.lineTo(left, Math.max(top + radius, stop)); path.moveTo(right, top + radius); path.lineTo(right, Math.max(top + radius, stop)); From 0c94228cf9ed82497b0f8b7f693f0707492cec11 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Sun, 5 Jul 2026 12:07:54 -0500 Subject: [PATCH 5/8] Fix active trackpad appearance preview color --- .../ui/component/dialog/ControlAppearanceEditor.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt index e8b0276f99..ffcf4cb612 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -484,12 +484,13 @@ private fun StickPreview(normalColor: Color, activeColor: Color, active: Boolean @Composable private fun TrackpadPreview(normalColor: Color, activeColor: Color, active: Boolean, strokeScale: Float) { Canvas(modifier = Modifier.size(72.dp)) { + val drawColor = if (active) activeColor else normalColor val strokeWidth = 2.dp.toPx() * strokeScale val stroke = Stroke(width = strokeWidth) val halfStroke = strokeWidth * 0.5f val corner = CornerRadius(size.height * 0.15f, size.height * 0.15f) drawRoundRect( - color = normalColor, + color = drawColor, topLeft = Offset(halfStroke, halfStroke), size = Size(size.width - strokeWidth, size.height - strokeWidth), cornerRadius = corner, @@ -497,7 +498,7 @@ private fun TrackpadPreview(normalColor: Color, activeColor: Color, active: Bool ) val inset = strokeWidth * 4f drawRoundRect( - color = if (active) activeColor else normalColor, + color = drawColor, topLeft = Offset(inset, inset), size = Size(size.width - inset * 2f, size.height - inset * 2f), cornerRadius = CornerRadius(size.height * 0.08f, size.height * 0.08f), From 6e4f62a2e4d802d59e5b993c75144ddc8b8818a8 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Sun, 5 Jul 2026 13:33:18 -0500 Subject: [PATCH 6/8] Address appearance review follow-ups --- .../gamenative/ui/component/dialog/ControlAppearanceEditor.kt | 2 +- .../app/gamenative/ui/component/dialog/ElementEditorDialog.kt | 4 ++-- .../main/java/com/winlator/inputcontrols/ControlElement.java | 2 +- app/src/main/res/values-de/strings.xml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt index ffcf4cb612..0d6f193a2a 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -603,7 +603,7 @@ internal fun showCopyAppearanceDialog( element.text } else { val binding = element.getBindingAt(0) - if (binding != null && binding != com.winlator.inputcontrols.Binding.NONE) binding.toString().take(15) else "No Binding" + if (binding != null && binding != com.winlator.inputcontrols.Binding.NONE) binding.toString().take(15) else context.getString(R.string.binding_none) } "${element.type.name.replace("_", " ")} - ${rgbToHex(element.buttonColor)} - $label" }.toTypedArray() diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt index 3545de590d..dc2f0aaf87 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt @@ -1089,7 +1089,7 @@ fun ElementEditorDialog( TextButton(onClick = { // Discard changes and close element.setScale(originalScale) - element.type = originalType + element.setTypeWithoutReset(originalType) element.shape = originalShape // Restore original range button properties before bindings; setBindingCount resets bindings. if (originalType == ControlElement.Type.RANGE_BUTTON) { @@ -1302,7 +1302,7 @@ private fun showCopySizeDialog( if (binding != null && binding != com.winlator.inputcontrols.Binding.NONE) { binding.toString().take(15) } else { - "No Binding" + context.getString(R.string.binding_none) } } diff --git a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java index 6c06831ce4..7ef84158a9 100644 --- a/app/src/main/java/com/winlator/inputcontrols/ControlElement.java +++ b/app/src/main/java/com/winlator/inputcontrols/ControlElement.java @@ -866,7 +866,7 @@ public void draw(Canvas canvas) { canvas.save(); path.addRoundRect(boundingBox.left, startY, boundingBox.right, boundingBox.bottom, radius, radius, Path.Direction.CW); - canvas.clipPath(inputControlsView.getPath()); + canvas.clipPath(path); startY -= scrollOffset % elementSize; for (byte i = rangeIndex[0]; i < rangeIndex[1]; i++) { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1ba05f6e0e..2b0f5180ab 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1984,7 +1984,7 @@ Dick Extra dick Shooter-Durchgriff - Diese Tastenberührung auch dynamische Shooter-Bewegung und Blick steuern lassen + Diese Tastenberührung auch zur Steuerung dynamischer Shooter-Bewegung und des Blicks verwenden Vorschau Aktiv Auf alle anwenden From cc123c8067b13a00d9fcfa1db8bdcb7af6ac1e84 Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Fri, 10 Jul 2026 19:56:06 -0500 Subject: [PATCH 7/8] Include size in control appearance actions --- .../dialog/ControlAppearanceEditor.kt | 6 ++++- .../component/dialog/ElementEditorDialog.kt | 24 +++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt index 0d6f193a2a..2cf31e3436 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ControlAppearanceEditor.kt @@ -31,6 +31,7 @@ import com.winlator.inputcontrols.ControlElement import com.winlator.widget.InputControlsView internal data class ControlAppearance( + val scale: Float, val color: Int, val activeColor: Int, val activeColorCustom: Boolean, @@ -39,6 +40,7 @@ internal data class ControlAppearance( val shooterLookThrough: Boolean ) { fun applyTo(element: ControlElement) { + element.setScale(scale) element.setButtonColor(color) element.setButtonActiveColor(activeColor, activeColorCustom) element.setButtonOpacity(opacity) @@ -48,6 +50,7 @@ internal data class ControlAppearance( companion object { fun capture(element: ControlElement) = ControlAppearance( + scale = element.scale, color = element.buttonColor, activeColor = element.buttonActiveColor, activeColorCustom = element.hasCustomButtonActiveColor(), @@ -599,13 +602,14 @@ internal fun showCopyAppearanceDialog( } val names = controls.map { element -> + val scale = String.format(java.util.Locale.US, "%.2fx", element.scale) val label = if (!element.text.isNullOrEmpty()) { element.text } else { val binding = element.getBindingAt(0) if (binding != null && binding != com.winlator.inputcontrols.Binding.NONE) binding.toString().take(15) else context.getString(R.string.binding_none) } - "${element.type.name.replace("_", " ")} - ${rgbToHex(element.buttonColor)} - $label" + "${element.type.name.replace("_", " ")} - $scale - ${rgbToHex(element.buttonColor)} - $label" }.toTypedArray() android.app.AlertDialog.Builder(context) diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt index dc2f0aaf87..caa8404214 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ElementEditorDialog.kt @@ -218,6 +218,7 @@ fun ElementEditorDialog( } fun currentAppearance() = ControlAppearance( + scale = currentScale, color = currentButtonColor, activeColor = currentButtonActiveColor, activeColorCustom = currentButtonActiveColorCustom, @@ -390,16 +391,6 @@ fun ElementEditorDialog( ) { // Appearance Section SettingsGroup(title = { Text(stringResource(R.string.appearance)) }) { - // Scale/Size - click to enter adjustment mode - SettingsMenuLink( - colors = settingsTileColors(), - title = { Text(stringResource(R.string.size)) }, - subtitle = { Text(stringResource(R.string.size_subtitle, currentScale)) }, - onClick = { - showSizeAdjuster = true - } - ) - // Text/Label - only for BUTTON type if (element.type == ControlElement.Type.BUTTON) { SettingsTextField( @@ -890,6 +881,15 @@ fun ElementEditorDialog( ) } + SettingsMenuLink( + colors = settingsTileColors(), + title = { Text(stringResource(R.string.size)) }, + subtitle = { Text(stringResource(R.string.size_subtitle, currentScale)) }, + onClick = { + showSizeAdjuster = true + } + ) + val strokeWidthItems = listOf( stringResource(R.string.control_stroke_width_thin), stringResource(R.string.control_stroke_width_default), @@ -943,6 +943,7 @@ fun ElementEditorDialog( OutlinedButton( onClick = { showCopyAppearanceDialog(context, element, view) { appearance -> + currentScale = appearance.scale currentButtonColor = appearance.color currentButtonColorText = rgbToHex(appearance.color) currentButtonActiveColor = appearance.activeColor @@ -985,8 +986,11 @@ fun ElementEditorDialog( currentButtonActiveColorText = rgbToHex(ControlElement.DEFAULT_BUTTON_ACTIVE_COLOR) currentButtonOpacityInherited = true currentButtonOpacity = view.overlayOpacity + currentScale = 1.0f + element.setScale(1.0f) currentButtonStrokeScale = ControlElement.DEFAULT_BUTTON_STROKE_SCALE currentShooterLookThrough = true + view.invalidate() hasUnsavedChanges = true }, modifier = Modifier.weight(1f), From c3f55a8dcc0c53276992a5cf38599b931b1bbd4e Mon Sep 17 00:00:00 2001 From: Nightwalker743 Date: Sun, 12 Jul 2026 12:55:45 -0500 Subject: [PATCH 8/8] Update Italian shooter look-through label --- app/src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c2c5fed92d..e934b065fe 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2034,7 +2034,7 @@ Predefinito Spesso Molto spesso - Passaggio mira shooter + Movimento e mira in modalità sparatutto Consenti a questo tocco del pulsante di controllare anche movimento e mira shooter dinamici Anteprima Attivo