Skip to content

Commit 310365b

Browse files
authored
Expose PC pointer speed to Android
Merge pointer speed profile, setting, picker, and copy updates.
1 parent 6c01178 commit 310365b

22 files changed

Lines changed: 398 additions & 26 deletions

src/SwitchifyPc.App/App.xaml.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,13 @@ private async Task StartBluetoothAsync()
282282
modifierOverlay = new WindowsModifierKeyOverlayNotifier(nativeInput);
283283
commandExecutor = new DesktopCommandExecutor(inputAdapter, cursorOverlay, modifierOverlay: modifierOverlay);
284284
mouseRepeatController = new MouseRepeatController(commandExecutor, mouseRepeatSettingsStore);
285+
PointerSpeedController pointerSpeedController = new(pointerSettingsStore, inputAdapter.SetPointerMovementSettings);
285286
ControlSession controlSession = new(
286287
new CommandAuthValidator(pairingStore),
287288
commandExecutor,
288289
new WindowsPointerProfileProvider(nativeInput, pointerSettingsStore, mouseRepeatSettingsStore),
289-
mouseRepeatController);
290+
mouseRepeatController,
291+
pointerSpeedController);
290292

291293
pairingApprovalManager ??= new PairingApprovalManager(pairingStore);
292294
RemoteControlSession remoteSession = new(

src/SwitchifyPc.App/SettingsWindow.xaml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@
370370
<StackPanel>
371371
<TextBlock Text="Pointer and cursor" Style="{StaticResource SectionTitle}" />
372372
<TextBlock Margin="0,6,0,0"
373-
Text="Tune movement distance and the visual cursor marker used while controlling this PC."
373+
Text="Tune pointer speed and the visual cursor marker used while controlling this PC."
374374
Style="{StaticResource MutedText}" />
375375

376376
<Border Margin="0,16,0,0" Style="{StaticResource SubtlePanel}">
@@ -381,9 +381,9 @@
381381
<ColumnDefinition Width="Auto" />
382382
</Grid.ColumnDefinitions>
383383
<StackPanel>
384-
<TextBlock Text="Movement distance" Style="{StaticResource DetailLabel}" />
384+
<TextBlock Text="Pointer speed" Style="{StaticResource DetailLabel}" />
385385
<TextBlock Margin="0,4,0,0"
386-
Text="Choose how far each Android pointer step moves on this display."
386+
Text="Choose how quickly Android pointer movement moves on this display."
387387
Style="{StaticResource MutedText}" />
388388
</StackPanel>
389389
<TextBlock Grid.Column="1"
@@ -393,7 +393,12 @@
393393
Style="{StaticResource DetailValue}" />
394394
</Grid>
395395

396-
<UniformGrid Margin="0,14,0,0" Columns="4">
396+
<UniformGrid Margin="0,14,0,0" Columns="5">
397+
<RadioButton Content="5%"
398+
GroupName="PointerScale"
399+
IsChecked="{Binding IsPointerScale5, Mode=OneWay}"
400+
Checked="PointerScale5_Checked"
401+
Style="{StaticResource SegmentRadio}" />
397402
<RadioButton Content="25%"
398403
GroupName="PointerScale"
399404
IsChecked="{Binding IsPointerScale25, Mode=OneWay}"

src/SwitchifyPc.App/SettingsWindow.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ await RunActionAsync(
5555
"Start with system could not be changed.");
5656
}
5757

58+
private void PointerScale5_Checked(object sender, RoutedEventArgs e) => SaveIfReady(() => controller!.SetPointerScalePercent(5));
59+
5860
private void PointerScale25_Checked(object sender, RoutedEventArgs e) => SaveIfReady(() => controller!.SetPointerScalePercent(25));
5961

6062
private void PointerScale50_Checked(object sender, RoutedEventArgs e) => SaveIfReady(() => controller!.SetPointerScalePercent(50));

src/SwitchifyPc.Core/Control/ControlSession.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,20 @@ public sealed class ControlSession
4444
private readonly DesktopCommandExecutor commandExecutor;
4545
private readonly IPointerProfileProvider pointerProfileProvider;
4646
private readonly MouseRepeatController? mouseRepeatController;
47+
private readonly PointerSpeedController? pointerSpeedController;
4748

4849
public ControlSession(
4950
CommandAuthValidator authValidator,
5051
DesktopCommandExecutor commandExecutor,
5152
IPointerProfileProvider pointerProfileProvider,
52-
MouseRepeatController? mouseRepeatController = null)
53+
MouseRepeatController? mouseRepeatController = null,
54+
PointerSpeedController? pointerSpeedController = null)
5355
{
5456
this.authValidator = authValidator;
5557
this.commandExecutor = commandExecutor;
5658
this.pointerProfileProvider = pointerProfileProvider;
5759
this.mouseRepeatController = mouseRepeatController;
60+
this.pointerSpeedController = pointerSpeedController;
5861
}
5962

6063
public async Task<ControlSessionResult> ProcessMessageAsync(string rawMessage, CancellationToken cancellationToken = default)
@@ -124,9 +127,21 @@ public async Task<ControlSessionResult> ProcessMessageAsync(string rawMessage, C
124127
await StopRepeatAsync(auth.DeviceId ?? "").ConfigureAwait(false);
125128
result = CommandExecutionResult.Success;
126129
}
130+
else if (type == "pointer.speed.set")
131+
{
132+
if (pointerSpeedController is null)
133+
{
134+
result = CommandExecutionResult.Failure("unsupported_command", "Pointer speed settings are not available.");
135+
}
136+
else
137+
{
138+
pointerSpeedController.SetScalePercent(request.GetProperty("payload").GetProperty("scalePercent").GetDouble());
139+
result = CommandExecutionResult.Success;
140+
}
141+
}
127142
else
128143
{
129-
if (type is not ("connection.ping" or "pointer.profile"))
144+
if (type is not ("connection.ping" or "pointer.profile" or "pointer.speed.set"))
130145
{
131146
await StopRepeatAsync(auth.DeviceId ?? "").ConfigureAwait(false);
132147
}
@@ -273,6 +288,17 @@ private static JsonObject PointerProfileResponse(string id, PointerMovementProfi
273288
["scrollIntervalMs"] = profile.Capabilities.MouseRepeat.ScrollIntervalMs,
274289
["minIntervalMs"] = profile.Capabilities.MouseRepeat.MinIntervalMs,
275290
["maxIntervalMs"] = profile.Capabilities.MouseRepeat.MaxIntervalMs
291+
},
292+
["pointerSpeed"] = new JsonObject
293+
{
294+
["supported"] = profile.Capabilities.PointerSpeed.Supported,
295+
["setSupported"] = profile.Capabilities.PointerSpeed.SetSupported,
296+
["scalePercent"] = profile.Capabilities.PointerSpeed.ScalePercent,
297+
["minScalePercent"] = profile.Capabilities.PointerSpeed.MinScalePercent,
298+
["maxScalePercent"] = profile.Capabilities.PointerSpeed.MaxScalePercent,
299+
["stepPercent"] = profile.Capabilities.PointerSpeed.StepPercent,
300+
["baseMoveDelta"] = profile.Capabilities.PointerSpeed.BaseMoveDelta,
301+
["effectiveMoveDelta"] = profile.Capabilities.PointerSpeed.EffectiveMoveDelta
276302
}
277303
}
278304
},

src/SwitchifyPc.Core/Input/PointerProfile.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@ public sealed record MouseRepeatCapabilities(
1616
int ScrollIntervalMs,
1717
int MinIntervalMs,
1818
int MaxIntervalMs);
19-
public sealed record PointerCapabilities(bool NoAckMouseMove, IReadOnlyList<string> NoAckCommands, IReadOnlyList<string> SupportedCommands, MouseRepeatCapabilities MouseRepeat);
19+
public sealed record PointerSpeedCapabilities(
20+
bool Supported,
21+
bool SetSupported,
22+
double ScalePercent,
23+
double MinScalePercent,
24+
double MaxScalePercent,
25+
double StepPercent,
26+
int BaseMoveDelta,
27+
int EffectiveMoveDelta);
28+
public sealed record PointerCapabilities(
29+
bool NoAckMouseMove,
30+
IReadOnlyList<string> NoAckCommands,
31+
IReadOnlyList<string> SupportedCommands,
32+
MouseRepeatCapabilities MouseRepeat,
33+
PointerSpeedCapabilities PointerSpeed);
2034
public sealed record PointerMovementProfile(
2135
string DisplayId,
2236
double ScaleFactor,
@@ -70,7 +84,8 @@ public static PointerMovementProfile Create(PointerProfileInput input)
7084
MoveIntervalMs: MouseRepeatSettingsModel.Default.MoveIntervalMs,
7185
ScrollIntervalMs: MouseRepeatSettingsModel.Default.ScrollIntervalMs,
7286
MinIntervalMs: MouseRepeatSettingsModel.MinIntervalMs,
73-
MaxIntervalMs: MouseRepeatSettingsModel.MaxIntervalMs)));
87+
MaxIntervalMs: MouseRepeatSettingsModel.MaxIntervalMs),
88+
PointerSpeed: PointerSpeedFor(PointerMovementSettingsModel.Default)));
7489
}
7590

7691
private static Bounds NormalizeBounds(Bounds bounds)
@@ -97,6 +112,23 @@ private static int ToLogicalDelta(double nativePixels, double scaleFactor, int m
97112
return Clamp((int)Math.Round(nativePixels / scaleFactor, MidpointRounding.AwayFromZero), 1, maxDelta);
98113
}
99114

115+
public static PointerSpeedCapabilities PointerSpeedFor(PointerMovementSettings settings)
116+
{
117+
double scalePercent = PointerMovementSettingsModel.ScalePercentFor(settings);
118+
return new PointerSpeedCapabilities(
119+
Supported: true,
120+
SetSupported: true,
121+
ScalePercent: scalePercent,
122+
MinScalePercent: PointerMovementSettingsModel.PointerMovementScaleMin,
123+
MaxScalePercent: PointerMovementSettingsModel.PointerMovementScaleMax,
124+
StepPercent: PointerMovementSettingsModel.PointerMovementScaleStep,
125+
BaseMoveDelta: PointerMovementSettingsModel.BaseMoveDelta,
126+
EffectiveMoveDelta: Clamp(
127+
(int)Math.Round(PointerMovementSettingsModel.BaseMoveDelta * (scalePercent / 100), MidpointRounding.AwayFromZero),
128+
1,
129+
ProtocolConstants.MaxPointerDelta));
130+
}
131+
100132
private static int Clamp(int value, int min, int max)
101133
{
102134
return Math.Min(max, Math.Max(min, value));
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using SwitchifyPc.Core.Settings;
2+
3+
namespace SwitchifyPc.Core.Input;
4+
5+
public sealed class PointerSpeedController
6+
{
7+
private readonly IPointerMovementSettingsStore settingsStore;
8+
private readonly Action<PointerMovementSettings> applyLiveSettings;
9+
10+
public PointerSpeedController(
11+
IPointerMovementSettingsStore settingsStore,
12+
Action<PointerMovementSettings> applyLiveSettings)
13+
{
14+
this.settingsStore = settingsStore;
15+
this.applyLiveSettings = applyLiveSettings;
16+
}
17+
18+
public PointerMovementSettings Current()
19+
{
20+
return settingsStore.Load();
21+
}
22+
23+
public PointerMovementSettings SetScalePercent(double scalePercent)
24+
{
25+
PointerMovementSettings saved = settingsStore.Save(new PointerMovementSettings(scalePercent));
26+
applyLiveSettings(saved);
27+
return saved;
28+
}
29+
}

src/SwitchifyPc.Core/Settings/PointerMovementSettings.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ public sealed record PointerMovementSettings(double ScalePercent);
1515

1616
public static class PointerMovementSettingsModel
1717
{
18-
public const double PointerMovementScaleMin = 25;
19-
public const double PointerMovementScaleMax = 200;
18+
public const double PointerMovementScaleMin = 5;
19+
public const double PointerMovementScaleMax = 225;
2020
public const double PointerMovementScaleStep = 5;
21+
public const int BaseMoveDelta = 128;
2122

2223
private const double DisplayPercentageMin = 1;
2324
private const double DisplayPercentageMax = 50;

src/SwitchifyPc.Core/Ui/SettingsViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public string StartWithSystemMessage
8181

8282
public double PointerScalePercent => PointerMovementSettingsModel.ScalePercentFor(pointerMovementSettings);
8383

84+
public bool IsPointerScale5 => Math.Abs(PointerScalePercent - 5) < 0.1;
85+
8486
public bool IsPointerScale25 => Math.Abs(PointerScalePercent - 25) < 0.1;
8587

8688
public bool IsPointerScale50 => Math.Abs(PointerScalePercent - 50) < 0.1;
@@ -202,6 +204,7 @@ public void SetPointerMovementSettings(PointerMovementSettings settings)
202204
pointerMovementSettings = PointerMovementSettingsModel.Normalize(settings);
203205
OnPropertyChanged(nameof(PointerMovementSettings));
204206
OnPropertyChanged(nameof(PointerScalePercent));
207+
OnPropertyChanged(nameof(IsPointerScale5));
205208
OnPropertyChanged(nameof(IsPointerScale25));
206209
OnPropertyChanged(nameof(IsPointerScale50));
207210
OnPropertyChanged(nameof(IsPointerScale75));

src/SwitchifyPc.Protocol/ProtocolConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public static class ProtocolConstants
3636
"media.control",
3737
"window.control",
3838
"pointer.profile",
39+
"pointer.speed.set",
3940
"connection.ping",
4041
"connection.disconnecting"
4142
};

src/SwitchifyPc.Protocol/ProtocolValidator.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ private static ProtocolValidationResult ValidateCommandPayload(string type, Json
144144
: Invalid("invalid_payload", "Mouse button is invalid."),
145145
"mouse.rightClick" or "connection.ping" or "connection.disconnecting" or "pointer.profile" =>
146146
ObjectPropertyCount(payload) == 0 ? Valid(payload) : Invalid("invalid_payload", "Payload must be empty."),
147+
"pointer.speed.set" =>
148+
TryGetPositiveFiniteNumber(payload, "scalePercent", out _) && ObjectPropertyCount(payload) == 1
149+
? Valid(payload)
150+
: Invalid("invalid_payload", "Pointer speed is invalid."),
147151
"keyboard.key" =>
148152
TryGetString(payload, "key", out string? key) && ProtocolConstants.KeyboardKeys.Contains(key)
149153
? Valid(payload)
@@ -352,6 +356,11 @@ private static ProtocolValidationResult ValidatePointerProfilePayload(JsonElemen
352356
{
353357
return Invalid("invalid_payload", "Mouse repeat capability is invalid.");
354358
}
359+
360+
if (capabilities.TryGetProperty("pointerSpeed", out JsonElement pointerSpeed) && !ValidatePointerSpeedCapability(pointerSpeed))
361+
{
362+
return Invalid("invalid_payload", "Pointer speed capability is invalid.");
363+
}
355364
}
356365

357366
return Valid(payload);
@@ -406,6 +415,29 @@ private static bool ValidateOptionalInterval(JsonElement value, string propertyN
406415
intervalMs <= maxIntervalMs;
407416
}
408417

418+
private static bool ValidatePointerSpeedCapability(JsonElement pointerSpeed)
419+
{
420+
if (!IsObject(pointerSpeed) ||
421+
!TryGetBoolean(pointerSpeed, "supported", out _) ||
422+
!TryGetBoolean(pointerSpeed, "setSupported", out _) ||
423+
!TryGetPositiveFiniteNumber(pointerSpeed, "scalePercent", out double scalePercent) ||
424+
!TryGetPositiveFiniteNumber(pointerSpeed, "minScalePercent", out double minScalePercent) ||
425+
!TryGetPositiveFiniteNumber(pointerSpeed, "maxScalePercent", out double maxScalePercent) ||
426+
!TryGetPositiveFiniteNumber(pointerSpeed, "stepPercent", out _) ||
427+
!TryGetInteger(pointerSpeed, "baseMoveDelta", out int baseMoveDelta) ||
428+
baseMoveDelta <= 0 ||
429+
!TryGetInteger(pointerSpeed, "effectiveMoveDelta", out int effectiveMoveDelta) ||
430+
effectiveMoveDelta <= 0)
431+
{
432+
return false;
433+
}
434+
435+
return minScalePercent <= scalePercent &&
436+
scalePercent <= maxScalePercent &&
437+
baseMoveDelta <= ProtocolConstants.MaxPointerDelta &&
438+
effectiveMoveDelta <= ProtocolConstants.MaxPointerDelta;
439+
}
440+
409441
private static bool ValidateCommandArrayCapability(JsonElement capabilities, string propertyName, IReadOnlySet<string> allowed)
410442
{
411443
if (!capabilities.TryGetProperty(propertyName, out JsonElement commands)) return true;

0 commit comments

Comments
 (0)