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
131 changes: 68 additions & 63 deletions src/SwitchifyPc.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,77 @@
</StackPanel>
</Border>

<Border Margin="0,14,0,0"
Background="{DynamicResource AccentContainer}"
BorderBrush="{DynamicResource BrandPrimary}"
BorderThickness="1"
CornerRadius="10"
Padding="16"
AutomationProperties.AutomationId="PairingApprovalsPanel"
Visibility="{Binding HasPairingApprovals, Converter={StaticResource BooleanToVisibilityConverter}}">
<StackPanel>
<TextBlock Text="Pairing requests"
FontSize="18"
FontWeight="Bold"
Foreground="{DynamicResource OnAccentContainer}" />
<ItemsControl Margin="0,12,0,0"
ItemsSource="{Binding PairingApprovals}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,10"
Padding="12"
BorderBrush="{DynamicResource BrandPrimary}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource Surface}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding DeviceName}"
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource Text}" />
<TextBlock Margin="0,4,0,0"
Text="{Binding VerificationCode, StringFormat=Verification code {0}}"
Foreground="{DynamicResource Text}"
FontSize="13"
FontWeight="Bold" />
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Margin="16,0,0,0"
VerticalAlignment="Center">
<Button Padding="14,0"
Tag="{Binding RequestId}"
Click="AcceptPairing_Click"
Content="Accept"
AutomationProperties.Name="{Binding DeviceName, StringFormat=Accept pairing request from {0}}"
Style="{StaticResource PrimaryButton}" />
<Button Margin="8,0,0,0"
Padding="14,0"
Tag="{Binding RequestId}"
Click="RejectPairing_Click"
Content="Reject"
AutomationProperties.Name="{Binding DeviceName, StringFormat=Reject pairing request from {0}}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>

<Border Margin="0,14,0,0"
Padding="14,12"
HorizontalAlignment="Center"
MinWidth="280"
Style="{StaticResource SubtlePanel}"
Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}">
AutomationProperties.AutomationId="ConnectedDevicePanel"
Visibility="{Binding ShowConnectedDeviceUi, Converter={StaticResource BooleanToVisibilityConverter}}">
<StackPanel>
<TextBlock Text="Device connected"
TextAlignment="Center"
Expand All @@ -290,7 +355,8 @@
Content="Disconnect device"
Click="Disconnect_Click"
Style="{StaticResource PrimaryButton}"
Visibility="{Binding IsConnected, Converter={StaticResource BooleanToVisibilityConverter}}" />
AutomationProperties.AutomationId="DisconnectDeviceButton"
Visibility="{Binding ShowConnectedDeviceUi, Converter={StaticResource BooleanToVisibilityConverter}}" />
</StackPanel>
</Border>

Expand Down Expand Up @@ -319,67 +385,6 @@
</Grid>
</Border>

<Border Margin="0,0,0,20"
Background="{DynamicResource AccentContainer}"
BorderBrush="{DynamicResource BrandPrimary}"
BorderThickness="1"
CornerRadius="10"
Padding="16"
Visibility="{Binding HasPairingApprovals, Converter={StaticResource BooleanToVisibilityConverter}}">
<StackPanel>
<TextBlock Text="Pairing requests"
FontSize="18"
FontWeight="Bold"
Foreground="{DynamicResource OnAccentContainer}" />
<ItemsControl Margin="0,12,0,0"
ItemsSource="{Binding PairingApprovals}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,0,10"
Padding="12"
BorderBrush="{DynamicResource BrandPrimary}"
BorderThickness="1"
CornerRadius="8"
Background="{DynamicResource Surface}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding DeviceName}"
FontSize="14"
FontWeight="Bold"
Foreground="{DynamicResource Text}" />
<TextBlock Margin="0,4,0,0"
Text="{Binding VerificationCode, StringFormat=Verification code {0}}"
Foreground="{DynamicResource Text}"
FontSize="13"
FontWeight="Bold" />
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Margin="16,0,0,0"
VerticalAlignment="Center">
<Button Padding="14,0"
Tag="{Binding RequestId}"
Click="AcceptPairing_Click"
Content="Accept"
Style="{StaticResource PrimaryButton}" />
<Button Margin="8,0,0,0"
Padding="14,0"
Tag="{Binding RequestId}"
Click="RejectPairing_Click"
Content="Reject" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>

<Expander Header="Troubleshooting"
Foreground="{DynamicResource Text}"
FontWeight="Bold">
Expand Down
4 changes: 4 additions & 0 deletions src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public sealed class MainWindowViewModel : INotifyPropertyChanged

public bool IsConnected => desktopState == DesktopUiState.Connected;

public bool ShowConnectedDeviceUi => IsConnected && !HasPairingApprovals;

public string ConnectedDeviceSummary => bluetooth.ConnectedClientCount == 1
? "Connected now."
: $"{bluetooth.ConnectedClientCount} devices connected.";
Expand Down Expand Up @@ -96,6 +98,7 @@ public void SetPairingApprovals(IReadOnlyList<PendingPairingApprovalView> approv
.ToArray();
OnPropertyChanged(nameof(HasPairingApprovals));
OnPropertyChanged(nameof(PairingApprovals));
OnPropertyChanged(nameof(ShowConnectedDeviceUi));
}

private void NotifyStatusChanged()
Expand All @@ -107,6 +110,7 @@ private void NotifyStatusChanged()
OnPropertyChanged(nameof(StatusBadgeTone));
OnPropertyChanged(nameof(BluetoothStatus));
OnPropertyChanged(nameof(IsConnected));
OnPropertyChanged(nameof(ShowConnectedDeviceUi));
OnPropertyChanged(nameof(ConnectedDeviceSummary));
OnPropertyChanged(nameof(SystemBluetooth));
OnPropertyChanged(nameof(BluetoothCapabilities));
Expand Down
100 changes: 100 additions & 0 deletions src/SwitchifyPc.Tests/MainWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using SwitchifyPc.App.Chrome;
using SwitchifyPc.App.Themes;
using SwitchifyPc.Core.Bluetooth;
using SwitchifyPc.Core.Pairing;
using SwitchifyPc.Core.Ui;
using SwitchifyPc.Core.Updates;
using WpfButton = System.Windows.Controls.Button;
Expand Down Expand Up @@ -168,6 +169,75 @@ BluetoothStatusModel.DefaultStatus with
});
}

[Fact]
public void PairingApprovalsReplaceConnectedDeviceUiAndPreserveAcceptRequest()
{
RunOnSta(() =>
{
WpfTestApplication.ApplyTheme(AppTheme.Light);
MainWindowViewModel viewModel = new();
viewModel.SetBluetoothState(
DesktopUiState.Connected,
BluetoothStatusModel.DefaultStatus with
{
Status = "connected",
ConnectedClientCount = 1,
System = BluetoothStatusModel.DefaultSystemStatus with
{
AdapterPresent = true,
RadioState = "on"
}
});
viewModel.SetPairingApprovals([
new PendingPairingApprovalView("approval-1", "Pixel 9", "123456", 1, 2, null),
new PendingPairingApprovalView("approval-2", "Galaxy Tab", "654321", 3, 4, null)
]);
string? acceptedRequestId = null;
MainWindow window = new(
viewModel,
acceptPairingApproval: requestId =>
{
acceptedRequestId = requestId;
return Task.CompletedTask;
});
try
{
window.Show();
window.UpdateLayout();

FrameworkElement pairingPanel = Assert.Single(ElementsByAutomationId(window, "PairingApprovalsPanel"));
FrameworkElement connectedPanel = Assert.IsType<Border>(ElementByAutomationId(window, "ConnectedDevicePanel"));
WpfButton disconnectButton = Assert.IsType<WpfButton>(ElementByAutomationId(window, "DisconnectDeviceButton"));

Assert.Equal(Visibility.Visible, pairingPanel.Visibility);
Assert.Equal(Visibility.Collapsed, connectedPanel.Visibility);
Assert.Equal(Visibility.Collapsed, disconnectButton.Visibility);
Assert.Contains("Pixel 9", TextBlocks(pairingPanel));
Assert.Contains("Verification code 123456", TextBlocks(pairingPanel));
Assert.Contains("Galaxy Tab", TextBlocks(pairingPanel));
Assert.Contains("Verification code 654321", TextBlocks(pairingPanel));

WpfButton acceptSecond = Assert.IsType<WpfButton>(ButtonByContentAndTag(pairingPanel, "Accept", "approval-2"));
WpfButton rejectSecond = Assert.IsType<WpfButton>(ButtonByContentAndTag(pairingPanel, "Reject", "approval-2"));
Assert.Equal("Accept pairing request from Galaxy Tab", AutomationProperties.GetName(acceptSecond));
Assert.Equal("Reject pairing request from Galaxy Tab", AutomationProperties.GetName(rejectSecond));
acceptSecond.RaiseEvent(new RoutedEventArgs(WpfButton.ClickEvent));
Assert.Equal("approval-2", acceptedRequestId);

viewModel.SetPairingApprovals([]);
window.UpdateLayout();

Assert.Equal(Visibility.Collapsed, pairingPanel.Visibility);
Assert.Equal(Visibility.Visible, connectedPanel.Visibility);
Assert.Equal(Visibility.Visible, disconnectButton.Visibility);
}
finally
{
window.Close();
}
});
}

private static void RunOnSta(Action action)
{
Exception? exception = null;
Expand Down Expand Up @@ -241,6 +311,22 @@ private static IReadOnlyList<string> ButtonContent(DependencyObject root)
return result;
}

private static WpfButton? ButtonByContentAndTag(DependencyObject root, string content, string tag)
{
WpfButton? result = null;
Collect(root, node =>
{
if (result is null &&
node is WpfButton { Content: string text, Tag: string requestId } button &&
text == content &&
requestId == tag)
{
result = button;
}
});
return result;
}

private static FrameworkElement? ElementByAutomationId(DependencyObject root, string automationId)
{
FrameworkElement? result = null;
Expand All @@ -256,6 +342,20 @@ node is FrameworkElement element &&
return result;
}

private static IReadOnlyList<FrameworkElement> ElementsByAutomationId(DependencyObject root, string automationId)
{
List<FrameworkElement> results = [];
Collect(root, node =>
{
if (node is FrameworkElement element &&
AutomationProperties.GetAutomationId(element) == automationId)
{
results.Add(element);
}
});
return results;
}

private static T? Ancestor<T>(DependencyObject node)
where T : DependencyObject
{
Expand Down
Loading