using WirelessTextSyncer.Windows.Services; namespace WirelessTextSyncer.Windows.Tray; public sealed class TrayApplicationContext : ApplicationContext { private readonly KeyboardInjectionService keyboard; private readonly AudioControlService audio; private readonly WebSocketServerService server; private readonly DiscoveryResponderService discovery; private readonly NotifyIcon notifyIcon; private readonly Icon connectedIcon; private readonly Icon waitIcon; private readonly SynchronizationContext uiContext; private ToolStripMenuItem? clipboardPasteMenuItem; private ToolStripMenuItem? sendInputTypingMenuItem; private bool? lastConnectionState; private bool disposed; public TrayApplicationContext() { AppLogger.Info("Tray application starting."); uiContext = SynchronizationContext.Current ?? new WindowsFormsSynchronizationContext(); keyboard = new KeyboardInjectionService(); audio = new AudioControlService(); var handler = new SyncMessageHandler(keyboard, audio); server = new WebSocketServerService(handler); server.StatusChanged += (_, _) => uiContext.Post(_ => UpdateTrayStatus(), null); discovery = new DiscoveryResponderService(() => server.LocalIpAddress, () => server.Port); connectedIcon = TrayIconFactory.CreateConnectedIcon(); waitIcon = TrayIconFactory.CreateWaitIcon(); notifyIcon = new NotifyIcon { Icon = waitIcon, Text = "WirelessTextSyncer starting...", Visible = true, ContextMenuStrip = BuildMenu() }; notifyIcon.MouseClick += OnTrayIconClicked; try { server.Start(8181); discovery.Start(); UpdateTrayStatus(); } catch (Exception ex) { AppLogger.Error("Failed to start tray application.", ex); notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; notifyIcon.BalloonTipText = $"Failed to start: {ex.Message}"; notifyIcon.ShowBalloonTip(5000); } } protected override void Dispose(bool disposing) { if (disposing) { disposed = true; notifyIcon.Dispose(); connectedIcon.Dispose(); waitIcon.Dispose(); discovery.Dispose(); audio.Dispose(); server.Dispose(); } base.Dispose(disposing); } private ContextMenuStrip BuildMenu() { var menu = new ContextMenuStrip(); menu.Items.Add("Copy connection address", null, (_, _) => CopyConnectionAddress()); menu.Items.Add(BuildInputModeMenu()); menu.Items.Add("Exit", null, (_, _) => ExitThread()); return menu; } private ToolStripMenuItem BuildInputModeMenu() { var inputModeMenu = new ToolStripMenuItem("Input mode"); clipboardPasteMenuItem = new ToolStripMenuItem("Clipboard paste") { CheckOnClick = false }; sendInputTypingMenuItem = new ToolStripMenuItem("SendInput typing") { CheckOnClick = false }; clipboardPasteMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.ClipboardPaste); sendInputTypingMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.SendInputTyping); inputModeMenu.DropDownItems.Add(clipboardPasteMenuItem); inputModeMenu.DropDownItems.Add(sendInputTypingMenuItem); UpdateInputModeMenuChecks(); return inputModeMenu; } private void SetInputMode(TextInjectionMode mode) { keyboard.Mode = mode; AppLogger.Info($"Text injection mode changed to {mode}."); UpdateInputModeMenuChecks(); notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; notifyIcon.BalloonTipText = $"Input mode: {GetInputModeLabel(mode)}"; notifyIcon.ShowBalloonTip(1500); } private void UpdateInputModeMenuChecks() { if (clipboardPasteMenuItem is not null) { clipboardPasteMenuItem.Checked = keyboard.Mode == TextInjectionMode.ClipboardPaste; } if (sendInputTypingMenuItem is not null) { sendInputTypingMenuItem.Checked = keyboard.Mode == TextInjectionMode.SendInputTyping; } } private static string GetInputModeLabel(TextInjectionMode mode) { return mode == TextInjectionMode.ClipboardPaste ? "Clipboard paste" : "SendInput typing"; } private void OnTrayIconClicked(object? sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { CopyConnectionAddress(); } } private void CopyConnectionAddress() { Clipboard.SetText($"ws://{server.LocalIpAddress}:{server.Port}"); notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; notifyIcon.BalloonTipText = "Connection address copied."; notifyIcon.ShowBalloonTip(1500); } private void UpdateTrayStatus() { if (disposed) { return; } var connected = server.HasClient; var status = connected ? "connected" : "waiting"; var text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})"; notifyIcon.Icon = connected ? connectedIcon : waitIcon; notifyIcon.Text = text.Length > 63 ? text[..63] : text; if (lastConnectionState is not null && lastConnectionState != connected) { ShowConnectionToast(connected); } lastConnectionState = connected; } private void ShowConnectionToast(bool connected) { notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; notifyIcon.BalloonTipText = connected ? $"Device connected to {server.LocalIpAddress}:{server.Port}." : "Device disconnected. Waiting for connection."; notifyIcon.BalloonTipIcon = connected ? ToolTipIcon.Info : ToolTipIcon.Warning; notifyIcon.ShowBalloonTip(3000); } }