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;
}
}
}