diff --git a/src/SwitchifyPc.App/MainWindow.xaml b/src/SwitchifyPc.App/MainWindow.xaml
index 98a1b4b..a5d4c88 100644
--- a/src/SwitchifyPc.App/MainWindow.xaml
+++ b/src/SwitchifyPc.App/MainWindow.xaml
@@ -266,12 +266,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AutomationProperties.AutomationId="ConnectedDevicePanel"
+ Visibility="{Binding ShowConnectedDeviceUi, Converter={StaticResource BooleanToVisibilityConverter}}">
+ AutomationProperties.AutomationId="DisconnectDeviceButton"
+ Visibility="{Binding ShowConnectedDeviceUi, Converter={StaticResource BooleanToVisibilityConverter}}" />
@@ -319,67 +385,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs b/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
index 23fe802..d509318 100644
--- a/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
+++ b/src/SwitchifyPc.Core/Ui/MainWindowViewModel.cs
@@ -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.";
@@ -96,6 +98,7 @@ public void SetPairingApprovals(IReadOnlyList approv
.ToArray();
OnPropertyChanged(nameof(HasPairingApprovals));
OnPropertyChanged(nameof(PairingApprovals));
+ OnPropertyChanged(nameof(ShowConnectedDeviceUi));
}
private void NotifyStatusChanged()
@@ -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));
diff --git a/src/SwitchifyPc.Tests/MainWindowTests.cs b/src/SwitchifyPc.Tests/MainWindowTests.cs
index 702a391..1ae17f2 100644
--- a/src/SwitchifyPc.Tests/MainWindowTests.cs
+++ b/src/SwitchifyPc.Tests/MainWindowTests.cs
@@ -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;
@@ -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(ElementByAutomationId(window, "ConnectedDevicePanel"));
+ WpfButton disconnectButton = Assert.IsType(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(ButtonByContentAndTag(pairingPanel, "Accept", "approval-2"));
+ WpfButton rejectSecond = Assert.IsType(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;
@@ -241,6 +311,22 @@ private static IReadOnlyList 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;
@@ -256,6 +342,20 @@ node is FrameworkElement element &&
return result;
}
+ private static IReadOnlyList ElementsByAutomationId(DependencyObject root, string automationId)
+ {
+ List results = [];
+ Collect(root, node =>
+ {
+ if (node is FrameworkElement element &&
+ AutomationProperties.GetAutomationId(element) == automationId)
+ {
+ results.Add(element);
+ }
+ });
+ return results;
+ }
+
private static T? Ancestor(DependencyObject node)
where T : DependencyObject
{
diff --git a/src/SwitchifyPc.Tests/MainWindowViewModelTests.cs b/src/SwitchifyPc.Tests/MainWindowViewModelTests.cs
index 18c9bad..206347e 100644
--- a/src/SwitchifyPc.Tests/MainWindowViewModelTests.cs
+++ b/src/SwitchifyPc.Tests/MainWindowViewModelTests.cs
@@ -18,6 +18,7 @@ public void DefaultsToStartingBluetoothCopy()
Assert.Equal("Starting...", viewModel.StatusBadgeLabel);
Assert.Equal("waiting", viewModel.StatusBadgeTone);
Assert.False(viewModel.IsConnected);
+ Assert.False(viewModel.ShowConnectedDeviceUi);
Assert.Equal("Bluetooth radio state unknown.", viewModel.SystemBluetooth);
Assert.False(viewModel.HasUpdateBanner);
}
@@ -66,6 +67,7 @@ BluetoothStatusModel.DefaultStatus with
Assert.Contains(nameof(MainWindowViewModel.StatusBadgeLabel), changed);
Assert.Contains(nameof(MainWindowViewModel.BluetoothStatus), changed);
Assert.Contains(nameof(MainWindowViewModel.IsConnected), changed);
+ Assert.Contains(nameof(MainWindowViewModel.ShowConnectedDeviceUi), changed);
Assert.Contains(nameof(MainWindowViewModel.BluetoothLastChecked), changed);
Assert.Contains(nameof(MainWindowViewModel.BluetoothLastChanged), changed);
Assert.Contains(nameof(MainWindowViewModel.RecentBluetoothEvents), changed);
@@ -76,6 +78,8 @@ BluetoothStatusModel.DefaultStatus with
public void ExposesConnectedStateForMainWindowActions()
{
MainWindowViewModel viewModel = new();
+ List changed = [];
+ viewModel.PropertyChanged += (_, eventArgs) => changed.Add(eventArgs.PropertyName);
viewModel.SetBluetoothState(
DesktopUiState.Connected,
@@ -87,9 +91,11 @@ BluetoothStatusModel.DefaultStatus with
});
Assert.True(viewModel.IsConnected);
+ Assert.True(viewModel.ShowConnectedDeviceUi);
Assert.Equal("Connected", viewModel.StatusBadgeLabel);
Assert.Equal("connected", viewModel.StatusBadgeTone);
Assert.Equal("2 devices connected.", viewModel.ConnectedDeviceSummary);
+ Assert.Contains(nameof(MainWindowViewModel.ShowConnectedDeviceUi), changed);
}
[Fact]
@@ -135,12 +141,55 @@ public void UpdatesPairingApprovalProperties()
]);
Assert.True(viewModel.HasPairingApprovals);
+ Assert.False(viewModel.ShowConnectedDeviceUi);
PendingPairingApprovalView approval = Assert.Single(viewModel.PairingApprovals);
Assert.Equal("approval-1", approval.RequestId);
Assert.Equal("Pixel 9", approval.DeviceName);
Assert.Equal("123456", approval.VerificationCode);
Assert.Contains(nameof(MainWindowViewModel.HasPairingApprovals), changed);
Assert.Contains(nameof(MainWindowViewModel.PairingApprovals), changed);
+ Assert.Contains(nameof(MainWindowViewModel.ShowConnectedDeviceUi), changed);
+ }
+
+ [Fact]
+ public void PairingApprovalsHideConnectedUiUntilFinalApprovalIsCleared()
+ {
+ MainWindowViewModel viewModel = new();
+ viewModel.SetBluetoothState(
+ DesktopUiState.Connected,
+ BluetoothStatusModel.DefaultStatus with
+ {
+ Status = "connected",
+ ConnectedClientCount = 1
+ });
+ List changed = [];
+ viewModel.PropertyChanged += (_, eventArgs) => changed.Add(eventArgs.PropertyName);
+
+ viewModel.SetPairingApprovals([
+ new PendingPairingApprovalView("approval-1", "Pixel 9", "123456", 1, 2, null)
+ ]);
+
+ Assert.False(viewModel.ShowConnectedDeviceUi);
+ Assert.Equal(
+ [
+ nameof(MainWindowViewModel.HasPairingApprovals),
+ nameof(MainWindowViewModel.PairingApprovals),
+ nameof(MainWindowViewModel.ShowConnectedDeviceUi)
+ ],
+ changed);
+
+ changed.Clear();
+ viewModel.SetPairingApprovals([]);
+
+ Assert.False(viewModel.HasPairingApprovals);
+ Assert.True(viewModel.ShowConnectedDeviceUi);
+ Assert.Equal(
+ [
+ nameof(MainWindowViewModel.HasPairingApprovals),
+ nameof(MainWindowViewModel.PairingApprovals),
+ nameof(MainWindowViewModel.ShowConnectedDeviceUi)
+ ],
+ changed);
}
[Fact]