using System; using System.Linq; using System.Windows; using System.Windows.Input; using TeamsISO.App.Services; using TeamsISO.App.ViewModels; namespace TeamsISO.App; public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); SourceInitialized += OnSourceInitialized; Closing += OnClosing; // Esc dismisses the settings drawer when it's open. Bound at the // window level so any focused control inside the drawer also gets // the affordance. PreviewKeyDown += OnPreviewKeyDown; } public MainWindow(MainViewModel viewModel) : this() { DataContext = viewModel; // Hand the view-model the palette-opener callback so Ctrl+K's // KeyBinding (which lives on the VM as an ICommand) can reach // back into the view layer to materialize the window. viewModel.RegisterCommandPaletteOpener(() => OnCommandPaletteClick(this, new RoutedEventArgs())); } /// /// Restore the window's previous placement after the HWND is created (so /// SetWindowPos / WindowState transitions actually take effect). Falls /// silently back to the XAML-default startup location if no snapshot exists. /// private void OnSourceInitialized(object? sender, EventArgs e) { WindowStateStore.TryApply(this); } /// Persist the placement on close so next launch lands in the same spot. private void OnClosing(object? sender, System.ComponentModel.CancelEventArgs e) { WindowStateStore.Save(this); } /// Opens the About dialog — version, NDI runtime, build SHA. private void OnAboutClick(object sender, RoutedEventArgs e) { var about = new AboutWindow { Owner = this }; about.ShowDialog(); } /// /// Opens the operator-presets dialog. Hands it the current participants /// snapshot (so Save captures live state) and the engine controller (so /// Apply can reconcile enable/disable). Owner is set so the chromeless /// dialog centers over the main window and inherits z-order. /// private void OnPresetsClick(object sender, RoutedEventArgs e) { if (DataContext is not MainViewModel vm) return; var dialog = new PresetsDialog(vm.Controller, vm.Participants.ToList(), vm.Toast) { Owner = this, }; dialog.ShowDialog(); } /// /// Tracks whether we have hidden Teams' windows so the next click reverses /// the action. We treat this as "intent" rather than a query of OS state /// because hidden windows still report as hidden if the operator manually /// re-opens them and we only care about TeamsISO's own toggle history. /// private bool _teamsWindowsHidden; /// /// Phase E.2 toggle. Hides every visible top-level Teams window on first /// click; shows them again on the next. Surfaces the result via the toast /// so the operator gets feedback even though the affected windows aren't /// visible anymore. /// private void OnToggleTeamsWindowClick(object sender, RoutedEventArgs e) { if (!TeamsLauncher.IsRunning()) { MessageBox.Show( "Microsoft Teams isn't running. Click the camera icon above to launch it first.", "TeamsISO — Hide / show Teams", MessageBoxButton.OK, MessageBoxImage.Information); return; } var toast = (DataContext as MainViewModel)?.Toast; if (_teamsWindowsHidden) { var shown = TeamsLauncher.ShowWindows(); _teamsWindowsHidden = false; toast?.Show(shown > 0 ? $"Restored {shown} Teams window(s)" : "No Teams windows to restore"); } else { var hidden = TeamsLauncher.HideWindows(); _teamsWindowsHidden = hidden > 0; toast?.Show(hidden > 0 ? $"Hid {hidden} Teams window(s)" : "Teams has no visible windows yet"); } } /// /// Three-state click behavior matching operator intuition: /// 1. Teams not running → launch it via TeamsLauncher's fallback chain. /// 2. Teams running but its windows are hidden → restore + foreground them. /// 3. Teams running with visible windows → bring the most recent to front. /// (Stopping Teams is a right-click action; see OnLaunchTeamsRightClick.) /// private void OnLaunchTeamsClick(object sender, RoutedEventArgs e) { var toast = (DataContext as MainViewModel)?.Toast; if (!TeamsLauncher.IsRunning()) { if (!TeamsLauncher.TryLaunch(out var error)) { MessageBox.Show( $"Could not launch Microsoft Teams.\n\n{error}", "TeamsISO — Launch Teams", MessageBoxButton.OK, MessageBoxImage.Warning); } else { var autoHide = (DataContext as MainViewModel)?.Settings.AutoHideTeamsWindows ?? false; toast?.Show(autoHide ? "Launching Microsoft Teams (will hide windows automatically)…" : "Launching Microsoft Teams…"); if (autoHide) { _ = TeamsLauncher.AutoHideAfterLaunchAsync(); _teamsWindowsHidden = true; } else { _teamsWindowsHidden = false; } } return; } var shown = TeamsLauncher.ShowWindows(); _teamsWindowsHidden = false; toast?.Show(shown > 0 ? $"Teams is already running — surfaced {shown} window(s)" : "Teams is running but has no visible windows yet"); } /// /// Right-click on the Launch button asks to stop Teams. Split out from the /// left-click so a normal click is "open / surface" rather than the previous /// "open OR ambush you with a stop dialog". /// private void OnLaunchTeamsRightClick(object sender, MouseButtonEventArgs e) { if (!TeamsLauncher.IsRunning()) return; var confirm = MessageBox.Show( "Microsoft Teams is currently running.\n\nClose all Teams windows now?", "TeamsISO — Stop Teams", MessageBoxButton.YesNo, MessageBoxImage.Question); if (confirm != MessageBoxResult.Yes) return; var asked = TeamsLauncher.StopAll(); if (TeamsLauncher.IsRunning()) { MessageBox.Show( asked == 0 ? "No Teams windows responded to close." : $"Sent close to {asked} Teams window(s); some may still be exiting.", "TeamsISO — Stop Teams", MessageBoxButton.OK, MessageBoxImage.Information); } e.Handled = true; } /// /// Open the experimental Teams embed window. Operator enables the /// preference first; this button materializes the host. /// private void OnOpenEmbedWindowClick(object sender, RoutedEventArgs e) { var w = new TeamsEmbedWindow { Owner = this }; w.Show(); } /// /// Toggle the v2 settings drawer overlay. The header gear button and the /// drawer's own Close button both call this. State is held by the /// overlay's directly — no separate /// flag — so the toggle is idempotent regardless of how many entry /// points open / close it. /// private void OnSettingsToggleClick(object sender, RoutedEventArgs e) { if (SettingsDrawerOverlay is null) return; SettingsDrawerOverlay.Visibility = SettingsDrawerOverlay.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; } /// /// Clicking the scrim behind the drawer dismisses it — same affordance as /// every well-behaved slide-over on every platform. /// private void OnSettingsScrimClick(object sender, MouseButtonEventArgs e) { if (SettingsDrawerOverlay is null) return; SettingsDrawerOverlay.Visibility = Visibility.Collapsed; } /// /// Open the v2 Ctrl+K command palette. Bound to the header ⌘K button and /// to the Ctrl+K keyboard binding. The palette is a chromeless floating /// window owned by this MainWindow so it centers correctly, closes on /// Deactivated (click outside), and inherits z-order. We construct a /// fresh view-model each time so the filter starts empty. /// private void OnCommandPaletteClick(object sender, RoutedEventArgs e) { if (DataContext is not MainViewModel vm) return; var paletteVm = new ViewModels.CommandPaletteViewModel(vm, Dispatcher); var palette = new Views.CommandPaletteWindow(paletteVm) { Owner = this }; palette.ShowDialog(); } /// /// Esc closes the drawer when it's open. We use PreviewKeyDown rather than /// KeyDown so the drawer's nested inputs (TextBox, ComboBox) don't swallow /// the key before this handler sees it. /// private void OnPreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key != Key.Escape) return; if (SettingsDrawerOverlay?.Visibility == Visibility.Visible) { SettingsDrawerOverlay.Visibility = Visibility.Collapsed; e.Handled = true; } } }