Compare commits

...

2 commits

Author SHA1 Message Date
d8186c5eb8 Auto-launch + auto-hide Teams: 'I only see TeamsISO' experience
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.
2026-05-10 20:35:00 -04:00
598938ede5 Fix sidebar text cutoff + Teams launch ambush dialog
Two user-reported bugs:

1) CheckBox content was clipping in the 380px settings panel ('Control surface (Stream Deck / Companion / w...' / 'LAN-reachable (allow other machines on yo...'). The Wd.CheckBox template used a horizontal StackPanel which doesn't bound child width, so long Content strings ran off the column without wrapping. Replaced StackPanel with a Grid (Auto + *) and injected a TextBlock style with TextWrapping=Wrap into the ContentPresenter resources — when WPF auto-wraps a string Content in a TextBlock, the resource lookup gives it Wrap.

2) The rail Launch Teams button ambushed operators: clicking with Teams already running (which is common when the eye-toggle has hidden Teams' windows) opened a 'Close all Teams windows now?' dialog. Operators expect Launch to mean 'show me Teams', not 'stop Teams'. Split the actions:

   - Left-click: Teams not running → launch; Teams hidden → restore + foreground; Teams visible → bring to front. Always idempotent-progressive.

   - Right-click: ask to stop Teams (preserves the kill path for those who want it).

TeamsLauncher.TryLaunch now collects per-attempt errors instead of swallowing them — a real failure surfaces 'ms-teams: URI → <reason>' / 'AppsFolder shell → <reason>' / 'classic Update.exe → not found at <path>' so 'No Teams found' isn't a black box.

Also added a 2nd path: explorer.exe shell:appsFolder\\\\MSTeams_8wekyb3d8bbwe!MSTeams (AppX activation via the OS's own Start-menu verb) as a fallback if the URI handler is misconfigured. Removed the broken bare-stub call to %LOCALAPPDATA%\\\\Microsoft\\\\WindowsApps\\\\ms-teams.exe — that's a 0-byte AppX placeholder that never worked outside an AppX context.
2026-05-10 14:39:04 -04:00
7 changed files with 304 additions and 43 deletions

View file

@ -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.

View file

@ -123,11 +123,18 @@
</Grid> </Grid>
</Button> </Button>
<!-- Nav: Launch Teams. Subprocess-launches the MS Teams desktop client. --> <!-- Nav: Launch Teams. Click = launch / surface; right-click = stop.
The previous "left-click toggles" behavior ambushed operators
who'd hidden Teams' windows via the eye toggle next door —
they'd hit Launch expecting Teams to come back and instead
get a "Close all Teams windows now?" dialog. Now click is
always idempotent-progressive (running but hidden → show;
not running → launch). Right-click for stop. -->
<Button DockPanel.Dock="Top" <Button DockPanel.Dock="Top"
Style="{StaticResource Wd.Button.RailIcon}" Style="{StaticResource Wd.Button.RailIcon}"
Click="OnLaunchTeamsClick" Click="OnLaunchTeamsClick"
ToolTip="Launch Microsoft Teams"> MouseRightButtonUp="OnLaunchTeamsRightClick"
ToolTip="Launch Microsoft Teams (or surface its window). Right-click to stop Teams.">
<!-- Stylized 'video meeting' camera icon --> <!-- Stylized 'video meeting' camera icon -->
<Path Data="M 4,8 L 16,8 L 16,16 L 4,16 Z M 16,11 L 22,8 L 22,16 L 16,13 Z" <Path Data="M 4,8 L 16,8 L 16,16 L 4,16 Z M 16,11 L 22,8 L 22,16 L 16,13 Z"
Stroke="{DynamicResource Wd.Text.Secondary}" Stroke="{DynamicResource Wd.Text.Secondary}"
@ -1244,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"

View file

@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Input;
using System.Windows.Shapes; using System.Windows.Shapes;
using TeamsISO.App.Services; using TeamsISO.App.Services;
using TeamsISO.App.ViewModels; using TeamsISO.App.ViewModels;
@ -113,14 +114,70 @@ public partial class MainWindow : Window
} }
/// <summary> /// <summary>
/// Toggle behavior: if Teams is already running, ask to stop it; otherwise /// Three-state click behavior matching operator intuition:
/// launch via TeamsLauncher's fallback chain. First step toward the /// 1. Teams not running → launch it via TeamsLauncher's fallback chain.
/// Embedded-Teams roadmap (Phase E.1). /// 2. Teams running but its windows are hidden (we toggled them off, OR
/// it launched into the tray) → restore the windows + foreground them.
/// This is the case the previous "ask to stop" dialog was ambushing —
/// operators don't think of a hidden Teams as "running and ready to
/// stop", they think of it as "I clicked Launch and nothing happened".
/// 3. Teams running with visible windows → bring the most recent one to
/// the foreground. (Stopping Teams is now a right-click action;
/// see OnLaunchTeamsRightClick.)
/// </summary> /// </summary>
private void OnLaunchTeamsClick(object sender, RoutedEventArgs e) private void OnLaunchTeamsClick(object sender, RoutedEventArgs e)
{ {
if (TeamsLauncher.IsRunning()) 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;
}
// Teams is running. Always try to restore + foreground its window —
// if windows are already visible, ShowWindows is a SetForegroundWindow
// no-op besides bringing them forward; if they were hidden by our
// own toggle, this is the operator's intuitive "show me Teams" path.
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");
}
/// <summary>
/// Right-click on the rail 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".
/// </summary>
private void OnLaunchTeamsRightClick(object sender, MouseButtonEventArgs e)
{
if (!TeamsLauncher.IsRunning()) return;
var confirm = MessageBox.Show( var confirm = MessageBox.Show(
"Microsoft Teams is currently running.\n\nClose all Teams windows now?", "Microsoft Teams is currently running.\n\nClose all Teams windows now?",
"TeamsISO — Stop Teams", "TeamsISO — Stop Teams",
@ -139,17 +196,7 @@ public partial class MainWindow : Window
MessageBoxButton.OK, MessageBoxButton.OK,
MessageBoxImage.Information); MessageBoxImage.Information);
} }
return; e.Handled = true;
}
if (!TeamsLauncher.TryLaunch(out var error))
{
MessageBox.Show(
$"Could not launch Microsoft Teams.\n\n{error}",
"TeamsISO — Launch Teams",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
} }
/// <summary> /// <summary>

View file

@ -44,21 +44,39 @@ public static class TeamsLauncher
/// <summary> /// <summary>
/// Launches Teams. Returns true if a launch was started successfully (the /// Launches Teams. Returns true if a launch was started successfully (the
/// process may take a few seconds to actually appear). False if every /// process may take a few seconds to actually appear). False if every
/// fallback path failed. /// fallback path failed; <paramref name="errorMessage"/> includes the
/// reasons each attempt was rejected so the operator can see why.
///
/// Path order matters:
/// 1. <c>ms-teams:</c> URI — new Teams (MSTeams AppX) registers this
/// handler at install. Activates through the AppX shell so the
/// stub <c>ms-teams.exe</c> in WindowsApps gets the right context.
/// 2. AppsFolder shell verb — direct AppX activation. Belt-and-braces
/// fallback if a misconfigured registry breaks the URI handler.
/// 3. Classic Teams Update.exe — pre-2024 Teams installations.
/// We deliberately DON'T try the bare <c>ms-teams.exe</c> WindowsApps
/// path: it's a 0-byte AppX placeholder that fails silently when invoked
/// without AppX activation context. Looked plausible, never worked.
/// </summary> /// </summary>
public static bool TryLaunch(out string? errorMessage) public static bool TryLaunch(out string? errorMessage)
{ {
errorMessage = null; errorMessage = null;
var attempts = new List<string>();
// Path 1: URI scheme. The shell handler picks whichever Teams client // Path 1: URI scheme. The shell handler picks the registered Teams
// is registered (new MSTeams.exe takes priority on modern Windows). // (new MSTeams takes priority on modern Windows). UseShellExecute=true
if (TryStart("ms-teams:", useShell: true)) return true; // is required — Win32 Process creation can't open URIs directly.
if (TryStart("ms-teams:", useShell: true, out var err1)) return true;
attempts.Add($"ms-teams: URI → {err1}");
// Path 2: new Teams' WindowsApps shim. // Path 2: AppX activation via the explorer.exe shell. Modern Teams
var newTeams = Path.Combine( // ships as MSTeams_8wekyb3d8bbwe; if other code on the box has
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), // clobbered the URI registration, this still works because it goes
"Microsoft", "WindowsApps", "ms-teams.exe"); // through the AppsFolder verb the OS itself uses for Start menu launches.
if (File.Exists(newTeams) && TryStart(newTeams, useShell: false)) return true; if (TryStart("explorer.exe", false, out var err2,
arguments: "shell:appsFolder\\MSTeams_8wekyb3d8bbwe!MSTeams"))
return true;
attempts.Add($"AppsFolder shell → {err2}");
// Path 3: classic Teams Update.exe with --processStart hands off to // Path 3: classic Teams Update.exe with --processStart hands off to
// the actual Teams.exe via Squirrel. // the actual Teams.exe via Squirrel.
@ -80,11 +98,17 @@ public static class TeamsLauncher
} }
catch (Exception ex) catch (Exception ex)
{ {
errorMessage = ex.Message; attempts.Add($"classic Update.exe → {ex.Message}");
} }
} }
else
{
attempts.Add($"classic Update.exe → not found at {classicUpdater}");
}
errorMessage ??= "No Microsoft Teams installation was found. Install Teams from https://www.microsoft.com/microsoft-teams and try again."; errorMessage = "No Microsoft Teams installation could be launched. " +
"Install Teams from https://www.microsoft.com/microsoft-teams and try again.\n\n" +
"Attempts:\n • " + string.Join("\n • ", attempts);
return false; return false;
} }
@ -123,8 +147,9 @@ public static class TeamsLauncher
return asked; return asked;
} }
private static bool TryStart(string target, bool useShell) private static bool TryStart(string target, bool useShell, out string error, string? arguments = null)
{ {
error = string.Empty;
try try
{ {
var info = new ProcessStartInfo var info = new ProcessStartInfo
@ -133,11 +158,13 @@ public static class TeamsLauncher
UseShellExecute = useShell, UseShellExecute = useShell,
CreateNoWindow = true, CreateNoWindow = true,
}; };
if (arguments is not null) info.Arguments = arguments;
Process.Start(info); Process.Start(info);
return true; return true;
} }
catch catch (Exception ex)
{ {
error = ex.Message;
return false; return false;
} }
} }
@ -224,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).
// //

View file

@ -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()
{ {

View file

@ -538,11 +538,25 @@
<Setter Property="Foreground" Value="{StaticResource Wd.Text.Primary}"/> <Setter Property="Foreground" Value="{StaticResource Wd.Text.Primary}"/>
<Setter Property="Cursor" Value="Hand"/> <Setter Property="Cursor" Value="Hand"/>
<Setter Property="Padding" Value="10,0,0,0"/> <Setter Property="Padding" Value="10,0,0,0"/>
<!--
Stretch + a Grid template lets long Content strings wrap rather
than get clipped by the 380px settings panel. Without this, the
previous StackPanel template let the ContentPresenter grow
unbounded horizontally and the parent's clip-to-bounds chopped
off the right side.
-->
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="CheckBox"> <ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal"> <Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="Box" <Border x:Name="Box"
Grid.Column="0"
Width="18" Height="18" Width="18" Height="18"
BorderBrush="{StaticResource Wd.BorderStrong}" BorderBrush="{StaticResource Wd.BorderStrong}"
BorderThickness="1" BorderThickness="1"
@ -556,9 +570,27 @@
StrokeEndLineCap="Round" StrokeEndLineCap="Round"
Visibility="Collapsed"/> Visibility="Collapsed"/>
</Border> </Border>
<ContentPresenter Margin="{TemplateBinding Padding}" <!--
VerticalAlignment="Center"/> ContentPresenter resources inject a TextBlock
</StackPanel> style with TextWrapping=Wrap. When CheckBox's
Content is a plain string (the common case here),
WPF wraps it in an auto-generated TextBlock; the
resource lookup applies our wrapping default so
long labels flow onto multiple lines instead of
being clipped at the column edge.
-->
<ContentPresenter Grid.Column="1"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
RecognizesAccessKey="True">
<ContentPresenter.Resources>
<Style TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</Grid>
<ControlTemplate.Triggers> <ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True"> <Trigger Property="IsChecked" Value="True">
<Setter TargetName="Box" Property="Background" Value="{StaticResource Wd.Accent.Cyan}"/> <Setter TargetName="Box" Property="Background" Value="{StaticResource Wd.Accent.Cyan}"/>

View file

@ -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