Auto-launch + auto-hide Teams: 'I only see TeamsISO' experience
Some checks failed
CI / build-and-test (push) Failing after 28s
Some checks failed
CI / build-and-test (push) Failing after 28s
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.
This commit is contained in:
parent
598938ede5
commit
d8186c5eb8
6 changed files with 163 additions and 4 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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."/>
|
||||
|
||||
<!-- Phase E.1/E.2 "I only see TeamsISO" pair. With both ticked,
|
||||
the operator launches TeamsISO and never sees the Teams UI —
|
||||
Teams runs in the background and all interaction routes
|
||||
through the IN-CALL bar + participants DataGrid. -->
|
||||
<CheckBox Content="Launch Microsoft Teams on TeamsISO startup"
|
||||
IsChecked="{Binding Settings.LaunchTeamsOnStartup}"
|
||||
Margin="0,16,0,0"
|
||||
ToolTip="When checked, TeamsISO will auto-launch Microsoft Teams in the background each time it starts. Combine with 'Auto-hide Teams windows' for the 'I only see TeamsISO' experience."/>
|
||||
|
||||
<CheckBox Content="Auto-hide Teams windows when launched"
|
||||
IsChecked="{Binding Settings.AutoHideTeamsWindows}"
|
||||
Margin="0,8,0,0"
|
||||
ToolTip="When checked, Teams' windows are hidden as soon as they materialize after launch. Use the eye-icon button in the rail to restore them when needed. Drives Teams via the IN-CALL bar (mute / camera / share / leave / marker) and the participants DataGrid for ISO routing."/>
|
||||
|
||||
<Separator Margin="0,16,0,8"/>
|
||||
|
||||
<CheckBox Content="Record ISOs to disk"
|
||||
|
|
|
|||
|
|
@ -141,8 +141,19 @@ public partial class MainWindow : Window
|
|||
}
|
||||
else
|
||||
{
|
||||
toast?.Show("Launching Microsoft Teams…");
|
||||
_teamsWindowsHidden = false;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,60 @@ public static class TeamsLauncher
|
|||
return windows.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fire-and-forget background watcher that polls every 250ms for up to
|
||||
/// <paramref name="timeout"/> 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).
|
||||
/// </summary>
|
||||
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).
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
/// <summary>
|
||||
/// Auto-launch the Microsoft Teams desktop client when TeamsISO starts.
|
||||
/// Paired with <see cref="AutoHideTeamsWindows"/> 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.
|
||||
/// </summary>
|
||||
public bool LaunchTeamsOnStartup
|
||||
{
|
||||
get => _launchTeamsOnStartup;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _launchTeamsOnStartup, value)) PersistUiPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-hide Teams' top-level windows as soon as they materialize after
|
||||
/// a launch (whether triggered via <see cref="LaunchTeamsOnStartup"/>,
|
||||
/// the rail button, or the eye-toggle). Runs a brief background poll
|
||||
/// that calls <c>TeamsLauncher.HideWindows</c> every ~250ms for up to
|
||||
/// 15 seconds, catching splash + main + follow-up panels.
|
||||
/// </summary>
|
||||
public bool AutoHideTeamsWindows
|
||||
{
|
||||
get => _autoHideTeamsWindows;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _autoHideTeamsWindows, value)) PersistUiPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record each newly-enabled ISO's normalized output to disk under
|
||||
|
|
|
|||
Loading…
Reference in a new issue