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);
|
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
|
// Background update check, throttled to once per 24h. Fire-and-forget
|
||||||
// so a slow / offline update server never delays startup. Surfaces a
|
// so a slow / offline update server never delays startup. Surfaces a
|
||||||
// banner via UpdateBanner if newer; failures just log.
|
// banner via UpdateBanner if newer; failures just log.
|
||||||
|
|
|
||||||
|
|
@ -1251,6 +1251,20 @@
|
||||||
Margin="0,12,0,0"
|
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."/>
|
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"/>
|
<Separator Margin="0,16,0,8"/>
|
||||||
|
|
||||||
<CheckBox Content="Record ISOs to disk"
|
<CheckBox Content="Record ISOs to disk"
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,19 @@ public partial class MainWindow : Window
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
toast?.Show("Launching Microsoft Teams…");
|
var autoHide = (DataContext as MainViewModel)?.Settings.AutoHideTeamsWindows ?? false;
|
||||||
_teamsWindowsHidden = false;
|
toast?.Show(autoHide
|
||||||
|
? "Launching Microsoft Teams (will hide windows automatically)…"
|
||||||
|
: "Launching Microsoft Teams…");
|
||||||
|
if (autoHide)
|
||||||
|
{
|
||||||
|
_ = TeamsLauncher.AutoHideAfterLaunchAsync();
|
||||||
|
_teamsWindowsHidden = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_teamsWindowsHidden = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,60 @@ public static class TeamsLauncher
|
||||||
return windows.Count;
|
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).
|
// Keyboard-shortcut forwarding (PostMessage path).
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,13 @@ public static class UIPreferences
|
||||||
bool AutoDisableOnDeparture = false,
|
bool AutoDisableOnDeparture = false,
|
||||||
SortMode ParticipantSort = SortMode.JoinOrder,
|
SortMode ParticipantSort = SortMode.JoinOrder,
|
||||||
bool MinimizeToTray = false,
|
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()
|
public static Prefs Load()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
||||||
private UIPreferences.SortMode _participantSort = UIPreferences.SortMode.JoinOrder;
|
private UIPreferences.SortMode _participantSort = UIPreferences.SortMode.JoinOrder;
|
||||||
private bool _minimizeToTray;
|
private bool _minimizeToTray;
|
||||||
private bool _controlSurfaceLanReachable;
|
private bool _controlSurfaceLanReachable;
|
||||||
|
private bool _launchTeamsOnStartup;
|
||||||
|
private bool _autoHideTeamsWindows;
|
||||||
|
|
||||||
public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null)
|
public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null)
|
||||||
{
|
{
|
||||||
|
|
@ -59,6 +61,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
||||||
_participantSort = uiPrefs.ParticipantSort;
|
_participantSort = uiPrefs.ParticipantSort;
|
||||||
_minimizeToTray = uiPrefs.MinimizeToTray;
|
_minimizeToTray = uiPrefs.MinimizeToTray;
|
||||||
_controlSurfaceLanReachable = uiPrefs.ControlSurfaceLanReachable;
|
_controlSurfaceLanReachable = uiPrefs.ControlSurfaceLanReachable;
|
||||||
|
_launchTeamsOnStartup = uiPrefs.LaunchTeamsOnStartup;
|
||||||
|
_autoHideTeamsWindows = uiPrefs.AutoHideTeamsWindows;
|
||||||
|
|
||||||
// Bring the auto-apply flag in from the presets store so the checkbox
|
// Bring the auto-apply flag in from the presets store so the checkbox
|
||||||
// reflects the user's prior choice when the settings panel opens.
|
// reflects the user's prior choice when the settings panel opens.
|
||||||
|
|
@ -222,7 +226,41 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
||||||
AutoDisableOnDeparture: _autoDisableOnDeparture,
|
AutoDisableOnDeparture: _autoDisableOnDeparture,
|
||||||
ParticipantSort: _participantSort,
|
ParticipantSort: _participantSort,
|
||||||
MinimizeToTray: _minimizeToTray,
|
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>
|
/// <summary>
|
||||||
/// Record each newly-enabled ISO's normalized output to disk under
|
/// Record each newly-enabled ISO's normalized output to disk under
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue