Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.enaboapps.switchify.components

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp

@Composable
fun CollapsibleSection(
title: String,
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
subtitle: String? = null,
content: @Composable ColumnScope.() -> Unit
) {
Surface(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surface,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
enabled = enabled,
role = Role.Button,
onClick = { onExpandedChange(!expanded) }
)
.semantics {
stateDescription = if (expanded) "Expanded" else "Collapsed"
}
.padding(horizontal = 12.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant
)
subtitle?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Icon(
imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn(animationSpec = tween(180)) + expandVertically(animationSpec = tween(180)),
exit = fadeOut(animationSpec = tween(120)) + shrinkVertically(animationSpec = tween(120))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, bottom = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
content = content
)
}
}
}
}
67 changes: 65 additions & 2 deletions app/src/main/java/com/enaboapps/switchify/pc/PcKeyboardKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,36 @@ enum class PcKeyboardKey(
}

enum class PcKeyboardShortcutKey(val protocolValue: String) {
Meta("Meta"),
Ctrl("Ctrl"),
Alt("Alt"),
Shift("Shift"),
Meta("Meta"),
A("A"),
B("B"),
C("C"),
X("X")
D("D"),
E("E"),
F("F"),
G("G"),
H("H"),
I("I"),
J("J"),
K("K"),
L("L"),
M("M"),
N("N"),
O("O"),
P("P"),
Q("Q"),
R("R"),
S("S"),
T("T"),
U("U"),
V("V"),
W("W"),
X("X"),
Y("Y"),
Z("Z")
}

enum class PcKeyboardModifierKey(
Expand All @@ -53,3 +78,41 @@ enum class PcKeyboardModifierKey(
Shift("Shift", R.string.pc_modifier_shift),
Meta("Meta", R.string.pc_modifier_start)
}

fun PcKeyboardModifierKey.toShortcutKey(): PcKeyboardShortcutKey {
return when (this) {
PcKeyboardModifierKey.Ctrl -> PcKeyboardShortcutKey.Ctrl
PcKeyboardModifierKey.Alt -> PcKeyboardShortcutKey.Alt
PcKeyboardModifierKey.Shift -> PcKeyboardShortcutKey.Shift
PcKeyboardModifierKey.Meta -> PcKeyboardShortcutKey.Meta
}
}

val PC_SHORTCUT_LETTER_KEYS: List<PcKeyboardShortcutKey> = listOf(
PcKeyboardShortcutKey.A,
PcKeyboardShortcutKey.B,
PcKeyboardShortcutKey.C,
PcKeyboardShortcutKey.D,
PcKeyboardShortcutKey.E,
PcKeyboardShortcutKey.F,
PcKeyboardShortcutKey.G,
PcKeyboardShortcutKey.H,
PcKeyboardShortcutKey.I,
PcKeyboardShortcutKey.J,
PcKeyboardShortcutKey.K,
PcKeyboardShortcutKey.L,
PcKeyboardShortcutKey.M,
PcKeyboardShortcutKey.N,
PcKeyboardShortcutKey.O,
PcKeyboardShortcutKey.P,
PcKeyboardShortcutKey.Q,
PcKeyboardShortcutKey.R,
PcKeyboardShortcutKey.S,
PcKeyboardShortcutKey.T,
PcKeyboardShortcutKey.U,
PcKeyboardShortcutKey.V,
PcKeyboardShortcutKey.W,
PcKeyboardShortcutKey.X,
PcKeyboardShortcutKey.Y,
PcKeyboardShortcutKey.Z
)
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ data class PcCompactCommandCell(
val selected: Boolean = false
)

data class PcCompactTextCommandCell(
val label: String,
val enabled: Boolean,
val onClick: () -> Unit,
val selected: Boolean = false
)

/**
* Small connection indicator shown in the navbar next to the surface switcher.
* Non-interactive; the connection state is exposed through semantics so speech
Expand Down Expand Up @@ -224,6 +231,44 @@ fun PcCompactCommandGrid(
}
}

@Composable
fun PcCompactTextCommandGrid(
columns: Int,
minTileHeightDp: Int,
horizontalGapDp: Int = 8,
verticalGapDp: Int = 8,
cells: List<PcCompactTextCommandCell>,
modifier: Modifier = Modifier
) {
require(columns > 0)
val horizontalGap = horizontalGapDp.dp
val verticalGap = verticalGapDp.dp

BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
val tileWidth = (maxWidth - horizontalGap * (columns - 1)) / columns
Column(verticalArrangement = Arrangement.spacedBy(verticalGap)) {
cells.chunked(columns).forEach { rowCells ->
EqualHeightGridRow(
items = rowCells,
columns = columns,
itemWidth = tileWidth,
minItemHeight = minTileHeightDp.dp,
horizontalGap = horizontalGap
) { cell, itemModifier ->
PcScannedCommandTile(
label = cell.label,
enabled = cell.enabled,
onClick = cell.onClick,
selected = cell.selected,
minHeightDp = minTileHeightDp,
modifier = itemModifier
)
}
}
}
}
}

@Composable
private fun PcCommandSectionTitle(@StringRes titleResId: Int) {
Text(
Expand All @@ -245,6 +290,33 @@ fun PcScannedCommandTile(
selected: Boolean = false,
minHeightDp: Int = 52,
square: Boolean = false
) {
PcScannedCommandTile(
label = stringResource(labelResId),
enabled = enabled,
onClick = onClick,
modifier = modifier,
icon = icon,
iconRotationDegrees = iconRotationDegrees,
tone = tone,
selected = selected,
minHeightDp = minHeightDp,
square = square
)
}

@Composable
fun PcScannedCommandTile(
label: String,
enabled: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector? = null,
iconRotationDegrees: Float = 0f,
tone: PcCommandTone = PcCommandTone.Neutral,
selected: Boolean = false,
minHeightDp: Int = 52,
square: Boolean = false
) {
val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()
Expand Down Expand Up @@ -321,7 +393,7 @@ fun PcScannedCommandTile(
)
}
Text(
text = stringResource(labelResId),
text = label,
style = MaterialTheme.typography.labelLarge,
fontWeight = if (tone == PcCommandTone.Primary) FontWeight.SemiBold else FontWeight.Medium,
color = contentColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ private fun PcMouseControlScreen(
enabled = surfaceEnabled,
activeModifiers = uiState.activeModifiers,
onModifierSelected = viewModel::toggleModifier,
onShortcutLetterSelected = viewModel::sendShortcutLetter,
onCommandSelected = viewModel::send
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.enaboapps.switchify.pc.PcControlCommand
import com.enaboapps.switchify.pc.PcErrorReason
import com.enaboapps.switchify.pc.PcKeyboardKey
import com.enaboapps.switchify.pc.PcKeyboardModifierKey
import com.enaboapps.switchify.pc.PcKeyboardShortcutKey
import com.enaboapps.switchify.pc.PcMouseRepeatManager
import com.enaboapps.switchify.pc.PcServiceConnectResult
import com.enaboapps.switchify.pc.PcServiceConnectionController
Expand All @@ -23,6 +24,7 @@ import com.enaboapps.switchify.pc.isSafePcTypedText
import com.enaboapps.switchify.pc.pcTextStreamItemsFor
import com.enaboapps.switchify.pc.supportsModifierToggle
import com.enaboapps.switchify.pc.supportsTextStreams
import com.enaboapps.switchify.pc.toShortcutKey
import com.enaboapps.switchify.service.core.ServiceCore
import java.util.UUID
import kotlinx.coroutines.CoroutineStart
Expand Down Expand Up @@ -174,6 +176,28 @@ class PcMouseControlViewModel(
}
}

fun sendShortcutLetter(letter: PcKeyboardShortcutKey) {
val modifiers = orderedShortcutModifiers(_uiState.value.activeModifiers)
if (modifiers.isEmpty()) {
_uiState.update { it.copy(message = SELECT_SHORTCUT_MODIFIER_MESSAGE) }
return
}
val keys = modifiers.map { it.toShortcutKey() } + letter
viewModelScope.launch {
when (sendNoAckCommandNow(PcControlCommand.KeyboardShortcut(keys)) {
it.copy(
isBusy = false,
busyCommand = null,
message = null
)
}) {
PcCommandResult.Ack -> releaseActiveModifiersIfPossible()
is PcCommandResult.AuthFailed,
is PcCommandResult.Failed -> Unit
}
}
}

fun sendMouseCommand(command: PcControlCommand, repeatable: Boolean) {
val mouseRepeat = currentMouseRepeatCapabilities()
if (repeatable && mouseRepeat?.supported == true) {
Expand Down Expand Up @@ -474,7 +498,7 @@ class PcMouseControlViewModel(
}

private fun releaseActiveModifiersIfPossible() {
val modifiers = _uiState.value.activeModifiers.toList()
val modifiers = orderedShortcutModifiers(_uiState.value.activeModifiers).asReversed()
if (modifiers.isEmpty()) return
_uiState.update { it.copy(activeModifiers = emptySet()) }
val controller = serviceControllerProvider()
Expand Down Expand Up @@ -1086,6 +1110,7 @@ class PcMouseControlViewModel(
const val KEY_FAILED_MESSAGE = "Could not send key to PC."
const val TEXT_TOO_LONG_MESSAGE = "Text is too long."
const val TEXT_UNSUPPORTED_MESSAGE = "Text includes unsupported characters."
const val SELECT_SHORTCUT_MODIFIER_MESSAGE = "Choose Ctrl, Alt, Shift, or Start first."
const val TEXT_STREAM_SEND_DELAY_MS = 250L
const val TEXT_STREAM_RECONNECT_TIMEOUT_MS = 15_000L
const val TEXT_STREAM_RECONNECT_RETRY_LIMIT = 3
Expand Down
Loading