Skip to content

Commit 80acf2c

Browse files
authored
Merge pull request #2339 from switchifyapp/feature/pc-medium-pointer-profile-movement-2338
Add PC mouse movement size selection
2 parents 6c3edb9 + 2b09978 commit 80acf2c

8 files changed

Lines changed: 307 additions & 37 deletions

File tree

app/src/main/java/com/enaboapps/switchify/backend/preferences/PreferenceManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class PreferenceManager(context: Context) {
2525
const val PREFERENCE_KEY_PAUSE_TIMEOUT = "pause_timeout"
2626
const val PREFERENCE_KEY_MOVE_REPEAT = "move_repeat"
2727
const val PREFERENCE_KEY_MOVE_REPEAT_DELAY = "move_repeat_delay"
28+
const val PREFERENCE_KEY_PC_MOUSE_MOVEMENT_SIZE = "pc_mouse_movement_size"
2829
const val PREFERENCE_KEY_AUTOMATICALLY_START_SCAN_AFTER_SELECTION =
2930
"automatically_start_scan_after_selection"
3031
const val PREFERENCE_KEY_PAUSE_ON_FIRST_ITEM = "pause_on_first_item"

app/src/main/java/com/enaboapps/switchify/screens/pc/PcMouseCommandGrid.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import androidx.compose.foundation.lazy.grid.GridCells
1010
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
1111
import androidx.compose.foundation.lazy.grid.items
1212
import androidx.compose.material3.Button
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.material3.SegmentedButton
15+
import androidx.compose.material3.SegmentedButtonDefaults
16+
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
1317
import androidx.compose.material3.Text
1418
import androidx.compose.runtime.Composable
1519
import androidx.compose.ui.Modifier
@@ -20,7 +24,7 @@ import com.enaboapps.switchify.R
2024
import com.enaboapps.switchify.pc.PcMouseCommand
2125

2226
data class PcMouseControlSpec(
23-
@StringRes val labelResId: Int,
27+
@param:StringRes val labelResId: Int,
2428
val command: PcMouseCommand
2529
)
2630

@@ -56,6 +60,36 @@ fun PcMouseCommandGrid(
5660
}
5761
}
5862

63+
@Composable
64+
fun PcMouseMovementSizeSelector(
65+
selectedSize: PcMouseMovementSize,
66+
onSizeSelected: (PcMouseMovementSize) -> Unit,
67+
modifier: Modifier = Modifier
68+
) {
69+
val sizes = PcMouseMovementSize.entries
70+
val colors = SegmentedButtonDefaults.colors(
71+
activeContainerColor = MaterialTheme.colorScheme.primary,
72+
activeContentColor = MaterialTheme.colorScheme.onPrimary,
73+
activeBorderColor = MaterialTheme.colorScheme.primary,
74+
inactiveContainerColor = MaterialTheme.colorScheme.surface,
75+
inactiveContentColor = MaterialTheme.colorScheme.onSurface,
76+
inactiveBorderColor = MaterialTheme.colorScheme.outline
77+
)
78+
79+
SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
80+
sizes.forEachIndexed { index, size ->
81+
SegmentedButton(
82+
selected = selectedSize == size,
83+
onClick = { onSizeSelected(size) },
84+
shape = SegmentedButtonDefaults.itemShape(index = index, count = sizes.size),
85+
colors = colors
86+
) {
87+
Text(text = stringResource(size.labelResId))
88+
}
89+
}
90+
}
91+
}
92+
5993
fun pcMouseControlSpecs(moveStep: Int): List<PcMouseControlSpec> {
6094
val step = moveStep.coerceAtLeast(1)
6195
val scrollStep = 5

app/src/main/java/com/enaboapps/switchify/screens/pc/PcMouseControlActivity.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ private fun PcMouseControlScreen(
116116
color = MaterialTheme.colorScheme.onSurfaceVariant
117117
)
118118
}
119+
Text(
120+
text = stringResource(R.string.pc_mouse_movement_size),
121+
style = MaterialTheme.typography.titleMedium,
122+
color = MaterialTheme.colorScheme.onSurface
123+
)
124+
PcMouseMovementSizeSelector(
125+
selectedSize = uiState.selectedMovementSize,
126+
onSizeSelected = viewModel::selectMovementSize
127+
)
119128
PcMouseCommandGrid(
120129
connected = uiState.connectedDisplayName != null,
121130
movementStep = uiState.movementStep,

app/src/main/java/com/enaboapps/switchify/screens/pc/PcMouseControlViewModel.kt

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import com.enaboapps.switchify.pc.PcDeviceIdentityRepository
1313
import com.enaboapps.switchify.pc.PcMouseCommand
1414
import com.enaboapps.switchify.pc.PcAuthenticatedSession
1515
import com.enaboapps.switchify.pc.PcPairingTokenStore
16-
import com.enaboapps.switchify.pc.PcPointerMovementProfile
1716
import com.enaboapps.switchify.pc.PcTokenStore
1817
import com.enaboapps.switchify.pc.SwitchifyPcClient
1918
import kotlinx.coroutines.CompletableDeferred
@@ -28,37 +27,52 @@ import kotlinx.coroutines.launch
2827

2928
data class PcMouseControlUiState(
3029
val connectedDisplayName: String? = null,
31-
val movementStep: Int = PcMouseControlViewModel.DEFAULT_MOVE_STEP,
30+
val selectedMovementSize: PcMouseMovementSize = PcMouseMovementSize.Small,
31+
val movementStep: Int = PcMouseControlViewModel.FALLBACK_MOVEMENT_STEPS.small,
3232
val isBusy: Boolean = false,
3333
val busyCommand: PcMouseCommand? = null,
3434
val message: String? = null
3535
)
3636

3737
class PcMouseControlViewModel(
3838
private val tokenStore: PcPairingTokenStore,
39-
private val connector: PcConnector
39+
private val connector: PcConnector,
40+
private val movementSizeStore: PcMouseMovementSizeStore
4041
) : ViewModel() {
4142
constructor(context: Context) : this(
4243
tokenStore = PcTokenStore(context.applicationContext),
4344
connector = SwitchifyPcClient(
4445
PcDeviceIdentityRepository(context.applicationContext),
4546
PcTokenStore(context.applicationContext)
46-
)
47+
),
48+
movementSizeStore = PcMouseMovementPreferenceStore(context.applicationContext)
4749
)
4850

4951
private val _uiState = MutableStateFlow(PcMouseControlUiState())
5052
val uiState: StateFlow<PcMouseControlUiState> = _uiState.asStateFlow()
5153
private var liveConnection: PcMouseControlConnection? = null
5254
private var liveSession: PcAuthenticatedSession? = null
5355
private var liveConnectionDeferred: Deferred<PcMouseControlConnection?>? = null
56+
private var movementSteps = FALLBACK_MOVEMENT_STEPS
5457

5558
init {
59+
val selectedSize = movementSizeStore.getSelectedSize()
60+
_uiState.update {
61+
it.copy(
62+
selectedMovementSize = selectedSize,
63+
movementStep = movementSteps.stepFor(selectedSize)
64+
)
65+
}
5666
viewModelScope.launch {
5767
PcConnectionStateHolder.connectionState.collect { state ->
5868
_uiState.update {
5969
it.copy(
6070
connectedDisplayName = (state as? PcConnectionState.Connected)?.displayName,
61-
movementStep = if (state is PcConnectionState.Connected) it.movementStep else DEFAULT_MOVE_STEP
71+
movementStep = if (state is PcConnectionState.Connected) {
72+
it.movementStep
73+
} else {
74+
fallbackStepFor(it.selectedMovementSize)
75+
}
6276
)
6377
}
6478
when (state) {
@@ -69,6 +83,16 @@ class PcMouseControlViewModel(
6983
}
7084
}
7185

86+
fun selectMovementSize(size: PcMouseMovementSize) {
87+
movementSizeStore.setSelectedSize(size)
88+
_uiState.update {
89+
it.copy(
90+
selectedMovementSize = size,
91+
movementStep = movementSteps.stepFor(size)
92+
)
93+
}
94+
}
95+
7296
fun send(command: PcMouseCommand) {
7397
val connected = PcConnectionStateHolder.connectionState.value as? PcConnectionState.Connected
7498
if (connected == null) {
@@ -134,18 +158,20 @@ class PcMouseControlViewModel(
134158
when (val result = connector.openMouseControlSession(session)) {
135159
is PcLiveControlResult.Connected -> {
136160
liveConnection = result.connection
161+
movementSteps = result.connection.pointerProfile?.toMouseMovementSteps() ?: FALLBACK_MOVEMENT_STEPS
137162
_uiState.update {
138-
it.copy(movementStep = result.connection.pointerProfile?.smallMovementStep() ?: DEFAULT_MOVE_STEP)
163+
it.copy(movementStep = movementSteps.stepFor(it.selectedMovementSize))
139164
}
140165
result.connection
141166
}
142167
is PcLiveControlResult.AuthFailed -> {
143168
tokenStore.clearToken(session.desktopId)
144169
PcConnectionStateHolder.setDisconnected()
170+
movementSteps = FALLBACK_MOVEMENT_STEPS
145171
_uiState.update {
146172
it.copy(
147173
connectedDisplayName = null,
148-
movementStep = DEFAULT_MOVE_STEP,
174+
movementStep = movementSteps.stepFor(it.selectedMovementSize),
149175
isBusy = false,
150176
busyCommand = null,
151177
message = CONNECT_FIRST_MESSAGE
@@ -173,12 +199,17 @@ class PcMouseControlViewModel(
173199
liveConnectionDeferred = null
174200
}
175201

176-
private fun PcPointerMovementProfile.smallMovementStep(): Int {
177-
return recommendedDeltas.small.coerceIn(1, maxDelta)
202+
private fun fallbackStepFor(size: PcMouseMovementSize): Int {
203+
movementSteps = FALLBACK_MOVEMENT_STEPS
204+
return movementSteps.stepFor(size)
178205
}
179206

180207
companion object {
181-
const val DEFAULT_MOVE_STEP = 40
208+
val FALLBACK_MOVEMENT_STEPS = PcMouseMovementSteps(
209+
small = 40,
210+
medium = 80,
211+
large = 160
212+
)
182213
const val CONNECT_FIRST_MESSAGE = "Connect to PC from Switchify first."
183214
const val COMMAND_FAILED_MESSAGE = "Could not send command to PC."
184215
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.enaboapps.switchify.screens.pc
2+
3+
import android.content.Context
4+
import androidx.annotation.StringRes
5+
import com.enaboapps.switchify.R
6+
import com.enaboapps.switchify.backend.preferences.PreferenceManager
7+
import com.enaboapps.switchify.pc.PcPointerMovementProfile
8+
9+
enum class PcMouseMovementSize(
10+
val preferenceValue: String,
11+
@param:StringRes val labelResId: Int
12+
) {
13+
Small("small", R.string.pc_mouse_movement_small),
14+
Medium("medium", R.string.pc_mouse_movement_medium),
15+
Large("large", R.string.pc_mouse_movement_large);
16+
17+
companion object {
18+
fun fromPreferenceValue(value: String): PcMouseMovementSize {
19+
return entries.firstOrNull { it.preferenceValue == value } ?: Small
20+
}
21+
}
22+
}
23+
24+
data class PcMouseMovementSteps(
25+
val small: Int,
26+
val medium: Int,
27+
val large: Int
28+
) {
29+
fun stepFor(size: PcMouseMovementSize): Int {
30+
return when (size) {
31+
PcMouseMovementSize.Small -> small
32+
PcMouseMovementSize.Medium -> medium
33+
PcMouseMovementSize.Large -> large
34+
}
35+
}
36+
}
37+
38+
interface PcMouseMovementSizeStore {
39+
fun getSelectedSize(): PcMouseMovementSize
40+
fun setSelectedSize(size: PcMouseMovementSize)
41+
}
42+
43+
class PcMouseMovementPreferenceStore(context: Context) : PcMouseMovementSizeStore {
44+
private val preferenceManager = PreferenceManager(context.applicationContext)
45+
46+
override fun getSelectedSize(): PcMouseMovementSize {
47+
return PcMouseMovementSize.fromPreferenceValue(
48+
preferenceManager.getStringValue(PreferenceManager.PREFERENCE_KEY_PC_MOUSE_MOVEMENT_SIZE)
49+
)
50+
}
51+
52+
override fun setSelectedSize(size: PcMouseMovementSize) {
53+
preferenceManager.setStringValue(
54+
PreferenceManager.PREFERENCE_KEY_PC_MOUSE_MOVEMENT_SIZE,
55+
size.preferenceValue
56+
)
57+
}
58+
}
59+
60+
fun PcPointerMovementProfile.toMouseMovementSteps(): PcMouseMovementSteps {
61+
return PcMouseMovementSteps(
62+
small = recommendedDeltas.small.coerceIn(1, maxDelta),
63+
medium = recommendedDeltas.medium.coerceIn(1, maxDelta),
64+
large = recommendedDeltas.large.coerceIn(1, maxDelta)
65+
)
66+
}

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@
277277
<string name="pc_control_could_not_connect">Could not connect to PC.</string>
278278
<string name="pc_control_command_failed">Could not send command to PC.</string>
279279
<string name="pc_mouse_control_connected">Connected to %1$s.</string>
280+
<string name="pc_mouse_movement_size">Movement size</string>
281+
<string name="pc_mouse_movement_small">Small</string>
282+
<string name="pc_mouse_movement_medium">Medium</string>
283+
<string name="pc_mouse_movement_large">Large</string>
280284
<string name="request_rejected">Request rejected.</string>
281285
<string name="request_expired_try_again">Request expired. Try again.</string>
282286
<string name="pc_mouse_up_left">Up left</string>

app/src/test/java/com/enaboapps/switchify/screens/pc/PcMouseCommandGridTest.kt

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,31 @@ import org.junit.Test
66

77
class PcMouseCommandGridTest {
88
@Test
9-
fun movementCommandsUseProvidedStep() {
10-
val commands = pcMouseControlSpecs(130).map { it.command }
9+
fun movementCommandsUseSmallFallbackStep() {
10+
val commands = pcMouseControlSpecs(40).map { it.command }
11+
12+
assertEquals(PcMouseCommand.Move(-40, -40), commands[0])
13+
assertEquals(PcMouseCommand.Move(0, -40), commands[1])
14+
assertEquals(PcMouseCommand.Move(40, -40), commands[2])
15+
assertEquals(PcMouseCommand.Move(-40, 0), commands[3])
16+
assertEquals(PcMouseCommand.Move(40, 0), commands[5])
17+
assertEquals(PcMouseCommand.Move(-40, 40), commands[6])
18+
assertEquals(PcMouseCommand.Move(0, 40), commands[7])
19+
assertEquals(PcMouseCommand.Move(40, 40), commands[8])
20+
}
21+
22+
@Test
23+
fun movementCommandsUseMediumFallbackStep() {
24+
val commands = pcMouseControlSpecs(80).map { it.command }
25+
26+
assertEquals(PcMouseCommand.Move(80, 0), commands[5])
27+
}
28+
29+
@Test
30+
fun movementCommandsUseLargeFallbackStep() {
31+
val commands = pcMouseControlSpecs(160).map { it.command }
1132

12-
assertEquals(PcMouseCommand.Move(-130, -130), commands[0])
13-
assertEquals(PcMouseCommand.Move(0, -130), commands[1])
14-
assertEquals(PcMouseCommand.Move(130, -130), commands[2])
15-
assertEquals(PcMouseCommand.Move(-130, 0), commands[3])
16-
assertEquals(PcMouseCommand.Move(130, 0), commands[5])
17-
assertEquals(PcMouseCommand.Move(-130, 130), commands[6])
18-
assertEquals(PcMouseCommand.Move(0, 130), commands[7])
19-
assertEquals(PcMouseCommand.Move(130, 130), commands[8])
33+
assertEquals(PcMouseCommand.Move(160, 0), commands[5])
2034
}
2135

2236
@Test

0 commit comments

Comments
 (0)