From d8186c5eb8409c2f33d1b6448d506d031364e6ca Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 10 May 2026 20:35:00 -0400 Subject: [PATCH] Auto-launch + auto-hide Teams: 'I only see TeamsISO' experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new persisted preferences in DISPLAY settings, paired to give operators the 'launch TeamsISO, never see Teams' experience the user asked for: - LaunchTeamsOnStartup: TeamsISO auto-starts Teams in the background each launch (fire-and-forget background task in App.OnStartup, after the main window has materialized so a slow Teams launch doesn't delay the UI). - AutoHideTeamsWindows: as soon as Teams' windows materialize after launch, hide them. New TeamsLauncher.AutoHideAfterLaunchAsync runs a polling loop (250ms / up to 15s) that catches the splash, main window, and any follow-up panels Teams opens. Teams takes 2-5s to render its main window and the splash arrives separately, so a one-shot hide right after launch wouldn't be enough. When TeamsISO starts and Teams is already running (from a prior session), the auto-hide path still fires so the 'I only see TeamsISO' rule applies even when Teams was launched externally. Operator drives everything through the IN-CALL bar (mute / camera / share / leave / marker) + participants DataGrid (ISO routing). Eye-toggle in the rail still restores Teams windows on demand. Both toggles default to off — opt-in. Persisted via UIPreferences so they survive process restart. --- src/TeamsISO.App/App.xaml.cs | 36 +++++++++++++ src/TeamsISO.App/MainWindow.xaml | 14 +++++ src/TeamsISO.App/MainWindow.xaml.cs | 15 +++++- src/TeamsISO.App/Services/TeamsLauncher.cs | 54 +++++++++++++++++++ src/TeamsISO.App/Services/UIPreferences.cs | 8 ++- .../ViewModels/GlobalSettingsViewModel.cs | 40 +++++++++++++- 6 files changed, 163 insertions(+), 4 deletions(-) diff --git a/src/TeamsISO.App/App.xaml.cs b/src/TeamsISO.App/App.xaml.cs index 25ca90c..fe0aa29 100644 --- a/src/TeamsISO.App/App.xaml.cs +++ b/src/TeamsISO.App/App.xaml.cs @@ -221,6 +221,42 @@ public partial class App : Application await _viewModel.InitializeAsync(CancellationToken.None); + // Auto-launch Teams in the background if the operator has opted in. + // Combined with AutoHideTeamsWindows this gives the "I only see + // TeamsISO" experience — Teams runs but never appears on screen, + // and all interaction routes through the IN-CALL bar + participants + // DataGrid. Fire-and-forget so a slow Teams launch doesn't delay + // TeamsISO's window from appearing. + if (_viewModel.Settings.LaunchTeamsOnStartup && !Services.TeamsLauncher.IsRunning()) + { + _ = Task.Run(() => + { + try + { + if (Services.TeamsLauncher.TryLaunch(out var launchError)) + { + if (_viewModel.Settings.AutoHideTeamsWindows) + _ = Services.TeamsLauncher.AutoHideAfterLaunchAsync(); + } + else + { + logger.LogWarning("Auto-launch Teams on startup failed: {Error}", launchError); + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "Auto-launch Teams on startup threw"); + } + }); + } + else if (_viewModel.Settings.AutoHideTeamsWindows && Services.TeamsLauncher.IsRunning()) + { + // Teams is already up from a previous session. If auto-hide is + // on, hide it now so the operator's "I only see TeamsISO" rule + // applies even when Teams was launched externally. + _ = Services.TeamsLauncher.AutoHideAfterLaunchAsync(); + } + // Background update check, throttled to once per 24h. Fire-and-forget // so a slow / offline update server never delays startup. Surfaces a // banner via UpdateBanner if newer; failures just log. diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index d6792d7..52eaf20 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -1251,6 +1251,20 @@ Margin="0,12,0,0" ToolTip="When checked, minimizing the window hides it and shows a tray icon. Useful for long unattended shows. Double-click the tray icon to restore."/> + + + + + + /// Fire-and-forget background watcher that polls every 250ms for up to + /// and hides any visible top-level Teams + /// windows it finds. Used after launch so the operator never sees the + /// Teams UI flash on screen — Teams takes 2-5s to splash + render its + /// main window, and the splash arrives separately from the main window + /// (so we keep polling past the first hide to catch follow-up windows). + /// + /// Returns the Task so callers can await completion if they want, but + /// production code should fire-and-forget. Exceptions are swallowed — + /// failure to hide is harmless (user just sees Teams briefly). + /// + public static Task AutoHideAfterLaunchAsync(TimeSpan? timeout = null, CancellationToken ct = default) + { + var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15)); + return Task.Run(async () => + { + try + { + var hiddenAny = false; + while (!ct.IsCancellationRequested && DateTime.UtcNow < deadline) + { + // Poll for visible windows. Each iteration may catch new + // ones — Teams sometimes opens a small splash, then a + // larger main window 1-2s later, then a "What's new" + // banner. Keep hiding until we've gone a full second + // with nothing new appearing. + var hidden = HideWindows(); + if (hidden > 0) + { + hiddenAny = true; + // Settling delay: after we hide windows, wait a beat + // before polling again so we don't busy-loop while + // Teams' window manager catches up. + await Task.Delay(750, ct).ConfigureAwait(false); + } + else if (hiddenAny) + { + // We hid at least once; if the next poll finds + // nothing, Teams has settled. Bail early. + return; + } + else + { + // Teams hasn't materialized yet; keep waiting. + await Task.Delay(250, ct).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) { /* expected on cancel */ } + catch { /* defensive — auto-hide is best-effort, never breaks the app */ } + }, ct); + } + // ──────────────────────────────────────────────────────────────────── // Keyboard-shortcut forwarding (PostMessage path). // diff --git a/src/TeamsISO.App/Services/UIPreferences.cs b/src/TeamsISO.App/Services/UIPreferences.cs index 0861919..55b3b66 100644 --- a/src/TeamsISO.App/Services/UIPreferences.cs +++ b/src/TeamsISO.App/Services/UIPreferences.cs @@ -39,7 +39,13 @@ public static class UIPreferences bool AutoDisableOnDeparture = false, SortMode ParticipantSort = SortMode.JoinOrder, bool MinimizeToTray = false, - bool ControlSurfaceLanReachable = false); + bool ControlSurfaceLanReachable = false, + // Phase E.1 / E.2 quality-of-life. With both true, the operator launches + // TeamsISO and never sees the Teams UI — Teams auto-starts in the + // background and its windows are auto-hidden as soon as they materialize. + // All control happens via the IN-CALL bar + participants DataGrid. + bool LaunchTeamsOnStartup = false, + bool AutoHideTeamsWindows = false); public static Prefs Load() { diff --git a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs index 000d23d..9d0f7f8 100644 --- a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs +++ b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs @@ -34,6 +34,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject private UIPreferences.SortMode _participantSort = UIPreferences.SortMode.JoinOrder; private bool _minimizeToTray; private bool _controlSurfaceLanReachable; + private bool _launchTeamsOnStartup; + private bool _autoHideTeamsWindows; public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null) { @@ -59,6 +61,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject _participantSort = uiPrefs.ParticipantSort; _minimizeToTray = uiPrefs.MinimizeToTray; _controlSurfaceLanReachable = uiPrefs.ControlSurfaceLanReachable; + _launchTeamsOnStartup = uiPrefs.LaunchTeamsOnStartup; + _autoHideTeamsWindows = uiPrefs.AutoHideTeamsWindows; // Bring the auto-apply flag in from the presets store so the checkbox // reflects the user's prior choice when the settings panel opens. @@ -222,7 +226,41 @@ public sealed class GlobalSettingsViewModel : ObservableObject AutoDisableOnDeparture: _autoDisableOnDeparture, ParticipantSort: _participantSort, MinimizeToTray: _minimizeToTray, - ControlSurfaceLanReachable: _controlSurfaceLanReachable)); + ControlSurfaceLanReachable: _controlSurfaceLanReachable, + LaunchTeamsOnStartup: _launchTeamsOnStartup, + AutoHideTeamsWindows: _autoHideTeamsWindows)); + + /// + /// Auto-launch the Microsoft Teams desktop client when TeamsISO starts. + /// Paired with gives the operator a + /// "TeamsISO is the only window I see" experience — Teams runs in the + /// background, all interaction happens through the participants DataGrid + /// + IN-CALL bar. + /// + public bool LaunchTeamsOnStartup + { + get => _launchTeamsOnStartup; + set + { + if (SetField(ref _launchTeamsOnStartup, value)) PersistUiPrefs(); + } + } + + /// + /// Auto-hide Teams' top-level windows as soon as they materialize after + /// a launch (whether triggered via , + /// the rail button, or the eye-toggle). Runs a brief background poll + /// that calls TeamsLauncher.HideWindows every ~250ms for up to + /// 15 seconds, catching splash + main + follow-up panels. + /// + public bool AutoHideTeamsWindows + { + get => _autoHideTeamsWindows; + set + { + if (SetField(ref _autoHideTeamsWindows, value)) PersistUiPrefs(); + } + } /// /// Record each newly-enabled ISO's normalized output to disk under