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
21 changes: 9 additions & 12 deletions src/SwitchifyPc.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public partial class App : System.Windows.Application
private WindowsExistingInstanceSignal? existingInstanceSignal;
private NativeTrayIcon? trayIcon;
private SettingsWindow? settingsWindow;
private SwitchControlProfileWindow? switchControlProfileWindow;
private IndependentWindowHost<SwitchControlProfileWindow>? switchControlProfileWindowHost;
private SetupGuideWindow? setupGuideWindow;
private MainWindowViewModel mainWindowViewModel = new();
private SetupGuideViewModel setupGuideViewModel = new();
Expand Down Expand Up @@ -114,6 +114,7 @@ protected override void OnStartup(StartupEventArgs e)
trayIcon = new NativeTrayIcon(
ShowMainWindow,
ShowSettingsWindow,
ShowSwitchControlProfiles,
TrayStatusText,
CanDisconnectBluetoothDevices,
DisconnectBluetoothDevices,
Expand Down Expand Up @@ -171,7 +172,7 @@ protected override void OnExit(ExitEventArgs e)
trayIcon?.Dispose();
trayIcon = null;
settingsWindow = null;
switchControlProfileWindow = null;
switchControlProfileWindowHost = null;
setupGuideWindow = null;
telemetryReporter?.Dispose();
telemetryReporter = null;
Expand Down Expand Up @@ -201,7 +202,8 @@ private Window CreateMainWindow()
InstallAvailableUpdateAsync,
DisconnectBluetoothDevices,
AcceptPairingApprovalAsync,
RejectPairingApproval);
RejectPairingApproval,
ShowSwitchControlProfiles);
window.Closing += (_, eventArgs) =>
{
if (isQuitting) return;
Expand Down Expand Up @@ -296,16 +298,11 @@ private void ShowSwitchControlProfiles()
{
switchControlProfileStore ??= new JsonSwitchControlProfileStore(
Path.Combine(UserDataDirectory(), "switch-control-profiles.json"));
if (switchControlProfileWindow is null)
{
switchControlProfileWindow = new SwitchControlProfileWindow(
switchControlProfileWindowHost ??= new IndependentWindowHost<SwitchControlProfileWindow>(
() => new SwitchControlProfileWindow(
switchControlProfileStore,
() => switchControlSessionManager?.ActiveProfileId);
switchControlProfileWindow.Closed += (_, _) => switchControlProfileWindow = null;
}
switchControlProfileWindow.Owner = settingsWindow;
switchControlProfileWindow.Show();
switchControlProfileWindow.Activate();
() => switchControlSessionManager?.ActiveProfileId));
switchControlProfileWindowHost.Show();
}

private static void CenterWindow(Window window)
Expand Down
39 changes: 39 additions & 0 deletions src/SwitchifyPc.App/IndependentWindowHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Windows;

namespace SwitchifyPc.App;

internal sealed class IndependentWindowHost<TWindow>
where TWindow : Window
{
private readonly Func<TWindow> createWindow;
private TWindow? window;

public IndependentWindowHost(Func<TWindow> createWindow)
{
this.createWindow = createWindow;
}

public TWindow Show()
{
TWindow current = window ?? CreateWindow();
current.Owner = null;
current.Show();
current.WindowState = WindowState.Normal;
current.Activate();
return current;
}

private TWindow CreateWindow()
{
TWindow created = createWindow();
window = created;
created.Closed += (_, _) =>
{
if (ReferenceEquals(window, created))
{
window = null;
}
};
return created;
}
}
6 changes: 6 additions & 0 deletions src/SwitchifyPc.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@
FontWeight="SemiBold"
AutomationProperties.LiveSetting="Polite"
Visibility="{Binding HasActiveSwitchControlProfile, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Margin="0,14,0,0"
HorizontalAlignment="Center"
Padding="16,8"
Content="Switch control profiles"
AutomationProperties.Name="Switch control profiles"
Click="OpenSwitchControlProfiles_Click" />

<Border Margin="0,14,0,0"
Background="{DynamicResource AccentContainer}"
Expand Down
10 changes: 9 additions & 1 deletion src/SwitchifyPc.App/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace SwitchifyPc.App;
public partial class MainWindow : Window
{
private readonly Action? openSettings;
private readonly Action? openSwitchControlProfiles;
private readonly Action? openSetupGuide;
private readonly Func<Task<UpdateApplyResult>>? installUpdate;
private readonly Action? disconnectDevices;
Expand All @@ -22,9 +23,11 @@ public MainWindow(
Func<Task<UpdateApplyResult>>? installUpdate = null,
Action? disconnectDevices = null,
Func<string, Task>? acceptPairingApproval = null,
Action<string>? rejectPairingApproval = null)
Action<string>? rejectPairingApproval = null,
Action? openSwitchControlProfiles = null)
{
this.openSettings = openSettings;
this.openSwitchControlProfiles = openSwitchControlProfiles;
this.openSetupGuide = openSetupGuide;
this.installUpdate = installUpdate;
this.disconnectDevices = disconnectDevices;
Expand All @@ -39,6 +42,11 @@ private void OpenSettings_Click(object sender, RoutedEventArgs e)
openSettings?.Invoke();
}

private void OpenSwitchControlProfiles_Click(object sender, RoutedEventArgs e)
{
openSwitchControlProfiles?.Invoke();
}

private void OpenSetupGuide_Click(object sender, RoutedEventArgs e)
{
openSetupGuide?.Invoke();
Expand Down
45 changes: 33 additions & 12 deletions src/SwitchifyPc.App/NativeTrayIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,39 @@ namespace SwitchifyPc.App;
public sealed class NativeTrayIcon : IDisposable
{
private readonly Forms.NotifyIcon notifyIcon;
private readonly Forms.ToolStripMenuItem statusItem;
private readonly Forms.ToolStripMenuItem disconnectItem;

public NativeTrayIcon(
Action showMainWindow,
Action showSettingsWindow,
Action showSwitchControlProfiles,
Func<string> statusText,
Func<bool> canDisconnect,
Action disconnectDevices,
Action quit)
{
Forms.ContextMenuStrip menu = CreateMenu(
showMainWindow,
showSettingsWindow,
showSwitchControlProfiles,
statusText,
canDisconnect,
disconnectDevices,
quit);

notifyIcon = new Forms.NotifyIcon
{
Icon = LoadAppIcon(),
Text = "Switchify PC",
ContextMenuStrip = menu,
Visible = true
};
notifyIcon.DoubleClick += (_, _) => showMainWindow();
}

internal static Forms.ContextMenuStrip CreateMenu(
Action showMainWindow,
Action showSettingsWindow,
Action showSwitchControlProfiles,
Func<string> statusText,
Func<bool> canDisconnect,
Action disconnectDevices,
Expand All @@ -19,12 +46,13 @@ public NativeTrayIcon(
Forms.ContextMenuStrip menu = new();
menu.Items.Add("Show Switchify PC", null, (_, _) => showMainWindow());
menu.Items.Add("Open settings", null, (_, _) => showSettingsWindow());
menu.Items.Add("Switch control profiles", null, (_, _) => showSwitchControlProfiles());
menu.Items.Add(new Forms.ToolStripSeparator());
statusItem = new Forms.ToolStripMenuItem("Status unavailable")
Forms.ToolStripMenuItem statusItem = new("Status unavailable")
{
Enabled = false
};
disconnectItem = new Forms.ToolStripMenuItem("Disconnect devices", null, (_, _) => disconnectDevices());
Forms.ToolStripMenuItem disconnectItem = new("Disconnect devices", null, (_, _) => disconnectDevices());
menu.Items.Add(statusItem);
menu.Items.Add(disconnectItem);
menu.Items.Add(new Forms.ToolStripSeparator());
Expand All @@ -35,14 +63,7 @@ public NativeTrayIcon(
disconnectItem.Enabled = canDisconnect();
};

notifyIcon = new Forms.NotifyIcon
{
Icon = LoadAppIcon(),
Text = "Switchify PC",
ContextMenuStrip = menu,
Visible = true
};
notifyIcon.DoubleClick += (_, _) => showMainWindow();
return menu;
}

public void Dispose()
Expand Down
61 changes: 61 additions & 0 deletions src/SwitchifyPc.Tests/IndependentWindowHostTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Threading;
using System.Windows;
using SwitchifyPc.App;

namespace SwitchifyPc.Tests;

[Collection(WpfTestCollection.Name)]
public sealed class IndependentWindowHostTests
{
[Fact]
public void ReusesRestoresAndKeepsWindowIndependent()
{
RunOnSta(() =>
{
int created = 0;
Window owner = new();
owner.Show();
IndependentWindowHost<Window> host = new(() =>
{
created++;
return new Window { Owner = owner };
});

Window first = host.Show();
first.WindowState = WindowState.Minimized;
Window second = host.Show();

Assert.Same(first, second);
Assert.Equal(1, created);
Assert.Null(second.Owner);
Assert.Equal(WindowState.Normal, second.WindowState);

second.Close();
Window third = host.Show();
Assert.NotSame(first, third);
Assert.Equal(2, created);
third.Close();
owner.Close();
});
}

private static void RunOnSta(Action action)
{
Exception? error = null;
Thread thread = new(() =>
{
try
{
action();
}
catch (Exception exception)
{
error = exception;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (error is not null) throw error;
}
}
29 changes: 29 additions & 0 deletions src/SwitchifyPc.Tests/MainWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ public void MainWindowShowsSetupGuideEntryWithoutLegacyDownloadCard()
});
}

[Fact]
public void SwitchControlProfilesEntryRunsProfileAction()
{
RunOnSta(() =>
{
WpfTestApplication.ApplyTheme(AppTheme.Light);
int calls = 0;
MainWindow window = new(
new MainWindowViewModel(),
openSwitchControlProfiles: () => calls++);
try
{
window.Show();
window.UpdateLayout();

WpfButton button = Assert.IsType<WpfButton>(ButtonByContent(window, "Switch control profiles"));
Assert.Equal("Switch control profiles", AutomationProperties.GetName(button));

button.RaiseEvent(new RoutedEventArgs(WpfButton.ClickEvent));

Assert.Equal(1, calls);
}
finally
{
window.Close();
}
});
}

[Fact]
public void MainWindowUsesCustomChrome()
{
Expand Down
71 changes: 71 additions & 0 deletions src/SwitchifyPc.Tests/NativeTrayIconTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Threading;
using SwitchifyPc.App;
using Forms = System.Windows.Forms;

namespace SwitchifyPc.Tests;

public sealed class NativeTrayIconTests
{
[Fact]
public void MenuOffersDirectSwitchControlProfilesEntry()
{
RunOnSta(() =>
{
int mainCalls = 0;
int settingsCalls = 0;
int profileCalls = 0;
int disconnectCalls = 0;
int quitCalls = 0;
using Forms.ContextMenuStrip menu = NativeTrayIcon.CreateMenu(
() => mainCalls++,
() => settingsCalls++,
() => profileCalls++,
() => "Status: Bluetooth ready.",
() => true,
() => disconnectCalls++,
() => quitCalls++);

Assert.Equal(
[
"Show Switchify PC",
"Open settings",
"Switch control profiles",
"Status unavailable",
"Disconnect devices",
"Quit"
],
menu.Items.OfType<Forms.ToolStripMenuItem>().Select(item => item.Text));

Forms.ToolStripMenuItem profileItem = menu.Items
.OfType<Forms.ToolStripMenuItem>()
.Single(item => item.Text == "Switch control profiles");
profileItem.PerformClick();

Assert.Equal(1, profileCalls);
Assert.Equal(0, mainCalls);
Assert.Equal(0, settingsCalls);
Assert.Equal(0, disconnectCalls);
Assert.Equal(0, quitCalls);
});
}

private static void RunOnSta(Action action)
{
Exception? error = null;
Thread thread = new(() =>
{
try
{
action();
}
catch (Exception exception)
{
error = exception;
}
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
if (error is not null) throw error;
}
}