Compare commits
2 commits
e020d1c2ac
...
d8186c5eb8
| Author | SHA1 | Date | |
|---|---|---|---|
| d8186c5eb8 | |||
| 598938ede5 |
7 changed files with 304 additions and 43 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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,43 +114,89 @@ 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;
|
||||||
{
|
|
||||||
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())
|
||||||
if (TeamsLauncher.IsRunning())
|
{
|
||||||
|
if (!TeamsLauncher.TryLaunch(out var error))
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
asked == 0
|
$"Could not launch Microsoft Teams.\n\n{error}",
|
||||||
? "No Teams windows responded to close."
|
"TeamsISO — Launch Teams",
|
||||||
: $"Sent close to {asked} Teams window(s); some may still be exiting.",
|
|
||||||
"TeamsISO — Stop Teams",
|
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Information);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TeamsLauncher.TryLaunch(out var error))
|
// 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(
|
||||||
|
"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(
|
MessageBox.Show(
|
||||||
$"Could not launch Microsoft Teams.\n\n{error}",
|
asked == 0
|
||||||
"TeamsISO — Launch Teams",
|
? "No Teams windows responded to close."
|
||||||
|
: $"Sent close to {asked} Teams window(s); some may still be exiting.",
|
||||||
|
"TeamsISO — Stop Teams",
|
||||||
MessageBoxButton.OK,
|
MessageBoxButton.OK,
|
||||||
MessageBoxImage.Warning);
|
MessageBoxImage.Information);
|
||||||
}
|
}
|
||||||
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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}"/>
|
||||||
|
|
|
||||||
|
|
@ -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