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
4 changes: 3 additions & 1 deletion src/SwitchifyPc.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,13 @@ private async Task StartBluetoothAsync()
modifierOverlay = new WindowsModifierKeyOverlayNotifier(nativeInput);
commandExecutor = new DesktopCommandExecutor(inputAdapter, cursorOverlay, modifierOverlay: modifierOverlay);
mouseRepeatController = new MouseRepeatController(commandExecutor, mouseRepeatSettingsStore);
PointerSpeedController pointerSpeedController = new(pointerSettingsStore, inputAdapter.SetPointerMovementSettings);
ControlSession controlSession = new(
new CommandAuthValidator(pairingStore),
commandExecutor,
new WindowsPointerProfileProvider(nativeInput, pointerSettingsStore, mouseRepeatSettingsStore),
mouseRepeatController);
mouseRepeatController,
pointerSpeedController);

pairingApprovalManager ??= new PairingApprovalManager(pairingStore);
RemoteControlSession remoteSession = new(
Expand Down
13 changes: 9 additions & 4 deletions src/SwitchifyPc.App/SettingsWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@
<StackPanel>
<TextBlock Text="Pointer and cursor" Style="{StaticResource SectionTitle}" />
<TextBlock Margin="0,6,0,0"
Text="Tune movement distance and the visual cursor marker used while controlling this PC."
Text="Tune pointer speed and the visual cursor marker used while controlling this PC."
Style="{StaticResource MutedText}" />

<Border Margin="0,16,0,0" Style="{StaticResource SubtlePanel}">
Expand All @@ -381,9 +381,9 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="Movement distance" Style="{StaticResource DetailLabel}" />
<TextBlock Text="Pointer speed" Style="{StaticResource DetailLabel}" />
<TextBlock Margin="0,4,0,0"
Text="Choose how far each Android pointer step moves on this display."
Text="Choose how quickly Android pointer movement moves on this display."
Style="{StaticResource MutedText}" />
</StackPanel>
<TextBlock Grid.Column="1"
Expand All @@ -393,7 +393,12 @@
Style="{StaticResource DetailValue}" />
</Grid>

<UniformGrid Margin="0,14,0,0" Columns="4">
<UniformGrid Margin="0,14,0,0" Columns="5">
<RadioButton Content="5%"
GroupName="PointerScale"
IsChecked="{Binding IsPointerScale5, Mode=OneWay}"
Checked="PointerScale5_Checked"
Style="{StaticResource SegmentRadio}" />
<RadioButton Content="25%"
GroupName="PointerScale"
IsChecked="{Binding IsPointerScale25, Mode=OneWay}"
Expand Down
2 changes: 2 additions & 0 deletions src/SwitchifyPc.App/SettingsWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ await RunActionAsync(
"Start with system could not be changed.");
}

private void PointerScale5_Checked(object sender, RoutedEventArgs e) => SaveIfReady(() => controller!.SetPointerScalePercent(5));

private void PointerScale25_Checked(object sender, RoutedEventArgs e) => SaveIfReady(() => controller!.SetPointerScalePercent(25));

private void PointerScale50_Checked(object sender, RoutedEventArgs e) => SaveIfReady(() => controller!.SetPointerScalePercent(50));
Expand Down
30 changes: 28 additions & 2 deletions src/SwitchifyPc.Core/Control/ControlSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,20 @@ public sealed class ControlSession
private readonly DesktopCommandExecutor commandExecutor;
private readonly IPointerProfileProvider pointerProfileProvider;
private readonly MouseRepeatController? mouseRepeatController;
private readonly PointerSpeedController? pointerSpeedController;

public ControlSession(
CommandAuthValidator authValidator,
DesktopCommandExecutor commandExecutor,
IPointerProfileProvider pointerProfileProvider,
MouseRepeatController? mouseRepeatController = null)
MouseRepeatController? mouseRepeatController = null,
PointerSpeedController? pointerSpeedController = null)
{
this.authValidator = authValidator;
this.commandExecutor = commandExecutor;
this.pointerProfileProvider = pointerProfileProvider;
this.mouseRepeatController = mouseRepeatController;
this.pointerSpeedController = pointerSpeedController;
}

public async Task<ControlSessionResult> ProcessMessageAsync(string rawMessage, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -124,9 +127,21 @@ public async Task<ControlSessionResult> ProcessMessageAsync(string rawMessage, C
await StopRepeatAsync(auth.DeviceId ?? "").ConfigureAwait(false);
result = CommandExecutionResult.Success;
}
else if (type == "pointer.speed.set")
{
if (pointerSpeedController is null)
{
result = CommandExecutionResult.Failure("unsupported_command", "Pointer speed settings are not available.");
}
else
{
pointerSpeedController.SetScalePercent(request.GetProperty("payload").GetProperty("scalePercent").GetDouble());
result = CommandExecutionResult.Success;
}
}
else
{
if (type is not ("connection.ping" or "pointer.profile"))
if (type is not ("connection.ping" or "pointer.profile" or "pointer.speed.set"))
{
await StopRepeatAsync(auth.DeviceId ?? "").ConfigureAwait(false);
}
Expand Down Expand Up @@ -273,6 +288,17 @@ private static JsonObject PointerProfileResponse(string id, PointerMovementProfi
["scrollIntervalMs"] = profile.Capabilities.MouseRepeat.ScrollIntervalMs,
["minIntervalMs"] = profile.Capabilities.MouseRepeat.MinIntervalMs,
["maxIntervalMs"] = profile.Capabilities.MouseRepeat.MaxIntervalMs
},
["pointerSpeed"] = new JsonObject
{
["supported"] = profile.Capabilities.PointerSpeed.Supported,
["setSupported"] = profile.Capabilities.PointerSpeed.SetSupported,
["scalePercent"] = profile.Capabilities.PointerSpeed.ScalePercent,
["minScalePercent"] = profile.Capabilities.PointerSpeed.MinScalePercent,
["maxScalePercent"] = profile.Capabilities.PointerSpeed.MaxScalePercent,
["stepPercent"] = profile.Capabilities.PointerSpeed.StepPercent,
["baseMoveDelta"] = profile.Capabilities.PointerSpeed.BaseMoveDelta,
["effectiveMoveDelta"] = profile.Capabilities.PointerSpeed.EffectiveMoveDelta
}
}
},
Expand Down
36 changes: 34 additions & 2 deletions src/SwitchifyPc.Core/Input/PointerProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,21 @@ public sealed record MouseRepeatCapabilities(
int ScrollIntervalMs,
int MinIntervalMs,
int MaxIntervalMs);
public sealed record PointerCapabilities(bool NoAckMouseMove, IReadOnlyList<string> NoAckCommands, IReadOnlyList<string> SupportedCommands, MouseRepeatCapabilities MouseRepeat);
public sealed record PointerSpeedCapabilities(
bool Supported,
bool SetSupported,
double ScalePercent,
double MinScalePercent,
double MaxScalePercent,
double StepPercent,
int BaseMoveDelta,
int EffectiveMoveDelta);
public sealed record PointerCapabilities(
bool NoAckMouseMove,
IReadOnlyList<string> NoAckCommands,
IReadOnlyList<string> SupportedCommands,
MouseRepeatCapabilities MouseRepeat,
PointerSpeedCapabilities PointerSpeed);
public sealed record PointerMovementProfile(
string DisplayId,
double ScaleFactor,
Expand Down Expand Up @@ -70,7 +84,8 @@ public static PointerMovementProfile Create(PointerProfileInput input)
MoveIntervalMs: MouseRepeatSettingsModel.Default.MoveIntervalMs,
ScrollIntervalMs: MouseRepeatSettingsModel.Default.ScrollIntervalMs,
MinIntervalMs: MouseRepeatSettingsModel.MinIntervalMs,
MaxIntervalMs: MouseRepeatSettingsModel.MaxIntervalMs)));
MaxIntervalMs: MouseRepeatSettingsModel.MaxIntervalMs),
PointerSpeed: PointerSpeedFor(PointerMovementSettingsModel.Default)));
}

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

public static PointerSpeedCapabilities PointerSpeedFor(PointerMovementSettings settings)
{
double scalePercent = PointerMovementSettingsModel.ScalePercentFor(settings);
return new PointerSpeedCapabilities(
Supported: true,
SetSupported: true,
ScalePercent: scalePercent,
MinScalePercent: PointerMovementSettingsModel.PointerMovementScaleMin,
MaxScalePercent: PointerMovementSettingsModel.PointerMovementScaleMax,
StepPercent: PointerMovementSettingsModel.PointerMovementScaleStep,
BaseMoveDelta: PointerMovementSettingsModel.BaseMoveDelta,
EffectiveMoveDelta: Clamp(
(int)Math.Round(PointerMovementSettingsModel.BaseMoveDelta * (scalePercent / 100), MidpointRounding.AwayFromZero),
1,
ProtocolConstants.MaxPointerDelta));
}

private static int Clamp(int value, int min, int max)
{
return Math.Min(max, Math.Max(min, value));
Expand Down
29 changes: 29 additions & 0 deletions src/SwitchifyPc.Core/Input/PointerSpeedController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using SwitchifyPc.Core.Settings;

namespace SwitchifyPc.Core.Input;

public sealed class PointerSpeedController
{
private readonly IPointerMovementSettingsStore settingsStore;
private readonly Action<PointerMovementSettings> applyLiveSettings;

public PointerSpeedController(
IPointerMovementSettingsStore settingsStore,
Action<PointerMovementSettings> applyLiveSettings)
{
this.settingsStore = settingsStore;
this.applyLiveSettings = applyLiveSettings;
}

public PointerMovementSettings Current()
{
return settingsStore.Load();
}

public PointerMovementSettings SetScalePercent(double scalePercent)
{
PointerMovementSettings saved = settingsStore.Save(new PointerMovementSettings(scalePercent));
applyLiveSettings(saved);
return saved;
}
}
5 changes: 3 additions & 2 deletions src/SwitchifyPc.Core/Settings/PointerMovementSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ public sealed record PointerMovementSettings(double ScalePercent);

public static class PointerMovementSettingsModel
{
public const double PointerMovementScaleMin = 25;
public const double PointerMovementScaleMax = 200;
public const double PointerMovementScaleMin = 5;
public const double PointerMovementScaleMax = 225;
public const double PointerMovementScaleStep = 5;
public const int BaseMoveDelta = 128;

private const double DisplayPercentageMin = 1;
private const double DisplayPercentageMax = 50;
Expand Down
3 changes: 3 additions & 0 deletions src/SwitchifyPc.Core/Ui/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public string StartWithSystemMessage

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

public bool IsPointerScale5 => Math.Abs(PointerScalePercent - 5) < 0.1;

public bool IsPointerScale25 => Math.Abs(PointerScalePercent - 25) < 0.1;

public bool IsPointerScale50 => Math.Abs(PointerScalePercent - 50) < 0.1;
Expand Down Expand Up @@ -202,6 +204,7 @@ public void SetPointerMovementSettings(PointerMovementSettings settings)
pointerMovementSettings = PointerMovementSettingsModel.Normalize(settings);
OnPropertyChanged(nameof(PointerMovementSettings));
OnPropertyChanged(nameof(PointerScalePercent));
OnPropertyChanged(nameof(IsPointerScale5));
OnPropertyChanged(nameof(IsPointerScale25));
OnPropertyChanged(nameof(IsPointerScale50));
OnPropertyChanged(nameof(IsPointerScale75));
Expand Down
1 change: 1 addition & 0 deletions src/SwitchifyPc.Protocol/ProtocolConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public static class ProtocolConstants
"media.control",
"window.control",
"pointer.profile",
"pointer.speed.set",
"connection.ping",
"connection.disconnecting"
};
Expand Down
32 changes: 32 additions & 0 deletions src/SwitchifyPc.Protocol/ProtocolValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ private static ProtocolValidationResult ValidateCommandPayload(string type, Json
: Invalid("invalid_payload", "Mouse button is invalid."),
"mouse.rightClick" or "connection.ping" or "connection.disconnecting" or "pointer.profile" =>
ObjectPropertyCount(payload) == 0 ? Valid(payload) : Invalid("invalid_payload", "Payload must be empty."),
"pointer.speed.set" =>
TryGetPositiveFiniteNumber(payload, "scalePercent", out _) && ObjectPropertyCount(payload) == 1
? Valid(payload)
: Invalid("invalid_payload", "Pointer speed is invalid."),
"keyboard.key" =>
TryGetString(payload, "key", out string? key) && ProtocolConstants.KeyboardKeys.Contains(key)
? Valid(payload)
Expand Down Expand Up @@ -352,6 +356,11 @@ private static ProtocolValidationResult ValidatePointerProfilePayload(JsonElemen
{
return Invalid("invalid_payload", "Mouse repeat capability is invalid.");
}

if (capabilities.TryGetProperty("pointerSpeed", out JsonElement pointerSpeed) && !ValidatePointerSpeedCapability(pointerSpeed))
{
return Invalid("invalid_payload", "Pointer speed capability is invalid.");
}
}

return Valid(payload);
Expand Down Expand Up @@ -406,6 +415,29 @@ private static bool ValidateOptionalInterval(JsonElement value, string propertyN
intervalMs <= maxIntervalMs;
}

private static bool ValidatePointerSpeedCapability(JsonElement pointerSpeed)
{
if (!IsObject(pointerSpeed) ||
!TryGetBoolean(pointerSpeed, "supported", out _) ||
!TryGetBoolean(pointerSpeed, "setSupported", out _) ||
!TryGetPositiveFiniteNumber(pointerSpeed, "scalePercent", out double scalePercent) ||
!TryGetPositiveFiniteNumber(pointerSpeed, "minScalePercent", out double minScalePercent) ||
!TryGetPositiveFiniteNumber(pointerSpeed, "maxScalePercent", out double maxScalePercent) ||
!TryGetPositiveFiniteNumber(pointerSpeed, "stepPercent", out _) ||
!TryGetInteger(pointerSpeed, "baseMoveDelta", out int baseMoveDelta) ||
baseMoveDelta <= 0 ||
!TryGetInteger(pointerSpeed, "effectiveMoveDelta", out int effectiveMoveDelta) ||
effectiveMoveDelta <= 0)
{
return false;
}

return minScalePercent <= scalePercent &&
scalePercent <= maxScalePercent &&
baseMoveDelta <= ProtocolConstants.MaxPointerDelta &&
effectiveMoveDelta <= ProtocolConstants.MaxPointerDelta;
}

private static bool ValidateCommandArrayCapability(JsonElement capabilities, string propertyName, IReadOnlySet<string> allowed)
{
if (!capabilities.TryGetProperty(propertyName, out JsonElement commands)) return true;
Expand Down
4 changes: 3 additions & 1 deletion src/SwitchifyPc.Tests/BluetoothControlFrameProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using SwitchifyPc.Core.Control;
using SwitchifyPc.Core.Input;
using SwitchifyPc.Core.Pairing;
using SwitchifyPc.Core.Settings;
using SwitchifyPc.Protocol;

namespace SwitchifyPc.Tests;
Expand Down Expand Up @@ -132,7 +133,8 @@ private static PointerCapabilities TestPointerCapabilities()
true,
ProtocolConstants.NoAckControlCommandTypes.ToArray(),
ProtocolConstants.CommandTypes.ToArray(),
new MouseRepeatCapabilities(true, true, 250, 250, 250, 100, 2000));
new MouseRepeatCapabilities(true, true, 250, 250, 250, 100, 2000),
PointerProfile.PointerSpeedFor(PointerMovementSettingsModel.Default));
}

private static string SignedCommand(string type, object payload, string id = "request-1", string? responseMode = null)
Expand Down
4 changes: 3 additions & 1 deletion src/SwitchifyPc.Tests/BluetoothRemoteFrameProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using SwitchifyPc.Core.Control;
using SwitchifyPc.Core.Input;
using SwitchifyPc.Core.Pairing;
using SwitchifyPc.Core.Settings;
using SwitchifyPc.Protocol;

namespace SwitchifyPc.Tests;
Expand Down Expand Up @@ -203,7 +204,8 @@ private static PointerCapabilities TestPointerCapabilities()
true,
ProtocolConstants.NoAckControlCommandTypes.ToArray(),
ProtocolConstants.CommandTypes.ToArray(),
new MouseRepeatCapabilities(true, true, 250, 250, 250, 100, 2000));
new MouseRepeatCapabilities(true, true, 250, 250, 250, 100, 2000),
PointerProfile.PointerSpeedFor(PointerMovementSettingsModel.Default));
}

private static JsonDocument Reassemble(IReadOnlyList<BluetoothFrame> frames)
Expand Down
Loading