Phase E.4 experimental: SetParent-embed Teams window inside TeamsISO
Some checks failed
CI / build-and-test (push) Failing after 28s
Some checks failed
CI / build-and-test (push) Failing after 28s
Reparents Teams' main top-level window into a TeamsISO-owned host via Win32 SetParent + window-style stripping. Operator gets Teams visually INSIDE TeamsISO instead of as a separate window — completes the 'Teams runs within this app' direction the user asked for after auto-hide. Strictly opt-in (DISPLAY tab → 'Embed Teams window (experimental)'). Modern Teams runs WebView2 in its main window; WebView2 is sensitive to parent changes and may render glitches or refuse focus. If so, operator unticks and falls back to auto-hide mode. Implementation: - TeamsLauncher.EmbedTeamsInto(hostHwnd, w, h): finds Teams' main window (longest-title heuristic — same as GetActiveWindowTitle), saves original parent + WS_STYLE, SetParents into host, strips WS_CAPTION + WS_THICKFRAME + WS_BORDER + WS_DLGFRAME + WS_POPUP, adds WS_CHILD, MoveWindow to fit. - TeamsLauncher.RestoreEmbed(): SetParent back to desktop + restore saved window styles. Idempotent — safe to call on shutdown even if nothing was embedded. - TeamsLauncher.ResizeEmbedded(w, h): MoveWindow to new dimensions; called from host SizeChanged event. - New TeamsEmbedWindow chromeless host with an EXPERIMENTAL pill in the caption. Loaded → grab HwndSource from EmbedHost Border → call EmbedTeamsInto. SizeChanged → ResizeEmbedded. Closed → RestoreEmbed (in try/finally so a crash can't leave Teams orphaned). Friendly fallback messages if no Teams window exists or HWND grab fails. - Settings → DISPLAY → checkbox + 'Open embed window' button (gated by the checkbox). Persisted via EmbedTeamsWindow on UIPreferences.
This commit is contained in:
parent
aa07ad9f08
commit
cc29c503a9
7 changed files with 368 additions and 2 deletions
|
|
@ -1343,6 +1343,25 @@
|
|||
Margin="0,8,0,0"
|
||||
ToolTip="When checked, recording auto-starts the moment Teams transitions into a call (IN CALL pill goes cyan) and auto-stops when the call ends. Removes the manual Record toggle step from unattended-show workflows."/>
|
||||
|
||||
<!-- Phase E.4 experimental — SetParent embed. WebView2 in modern
|
||||
Teams can render weirdly after reparent; if so, untick and
|
||||
fall back to auto-hide mode. -->
|
||||
<Border Margin="0,12,0,8"
|
||||
Height="1"
|
||||
Background="{DynamicResource Wd.Border}"/>
|
||||
<CheckBox Content="Embed Teams window inside TeamsISO (experimental)"
|
||||
IsChecked="{Binding Settings.EmbedTeamsWindow}"
|
||||
Margin="0,4,0,0"
|
||||
ToolTip="EXPERIMENTAL: Reparent Teams' main window into a TeamsISO-owned host so Teams appears INSIDE our window. WebView2 in modern Teams may render glitches or refuse focus after reparent — if so, untick and use auto-hide mode instead. Click 'Open embed window' below after enabling."/>
|
||||
<Button Style="{StaticResource Wd.Button.Ghost}"
|
||||
Content="Open embed window"
|
||||
Click="OnOpenEmbedWindowClick"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="0,8,0,0"
|
||||
Padding="0,8"
|
||||
IsEnabled="{Binding Settings.EmbedTeamsWindow}"
|
||||
ToolTip="Open the embed host window. Teams' main window will be reparented into it on load. Close the window to restore Teams to normal top-level state."/>
|
||||
|
||||
<Separator Margin="0,16,0,8"/>
|
||||
|
||||
<CheckBox Content="Record ISOs to disk"
|
||||
|
|
|
|||
|
|
@ -174,6 +174,19 @@ public partial class MainWindow : Window
|
|||
/// from the left-click so a normal click is "open / surface" rather than
|
||||
/// the previous "open OR ambush you with a stop dialog".
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Open the experimental Teams embed window. Operator enables the
|
||||
/// preference first; this button materializes the host. See
|
||||
/// <see cref="TeamsEmbedWindow"/> for the SetParent lifecycle.
|
||||
/// </summary>
|
||||
private void OnOpenEmbedWindowClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Non-modal so the operator can keep using TeamsISO's controls.
|
||||
// Owner = this so it minimizes / closes with TeamsISO.
|
||||
var w = new TeamsEmbedWindow { Owner = this };
|
||||
w.Show();
|
||||
}
|
||||
|
||||
private void OnLaunchTeamsRightClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!TeamsLauncher.IsRunning()) return;
|
||||
|
|
|
|||
|
|
@ -272,6 +272,167 @@ public static class TeamsLauncher
|
|||
|
||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// Phase E.4 — Embedded Teams via SetParent.
|
||||
//
|
||||
// Reparents Teams' main top-level window into a TeamsISO-owned host
|
||||
// (typically a Border element's HWND). The Win32 behavior is well
|
||||
// understood for classic Win32 apps but modern Teams runs WebView2 in
|
||||
// its main window; WebView2's renderer is sensitive to parent changes
|
||||
// and may flash white frames during reparent, drop input focus, or
|
||||
// refuse to redraw until forced.
|
||||
//
|
||||
// We mark the feature experimental and provide a clean restore path
|
||||
// (SetParent back to desktop + restore the original window styles)
|
||||
// so operators can fall back to auto-hide mode if embedding misbehaves
|
||||
// on their specific Teams build.
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int GetWindowLongPtr(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int SetWindowLongPtr(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, [MarshalAs(UnmanagedType.Bool)] bool bRepaint);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetDesktopWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
private const int GWL_STYLE = -16;
|
||||
private const long WS_CHILD = 0x40000000;
|
||||
private const long WS_POPUP = unchecked((long)0x80000000);
|
||||
private const long WS_CAPTION = 0x00C00000;
|
||||
private const long WS_THICKFRAME = 0x00040000;
|
||||
private const long WS_BORDER = 0x00800000;
|
||||
private const long WS_DLGFRAME = 0x00400000;
|
||||
private const uint SWP_FRAMECHANGED = 0x0020;
|
||||
private const uint SWP_NOMOVE = 0x0002;
|
||||
private const uint SWP_NOSIZE = 0x0001;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the original parent + window style so embedding can be
|
||||
/// reversed cleanly. Tracked per-HWND so multiple consecutive
|
||||
/// embed/unembed cycles don't lose the original chrome.
|
||||
/// </summary>
|
||||
private static (IntPtr OriginalParent, int OriginalStyle)? _embedSavedState;
|
||||
private static IntPtr _embeddedHwnd = IntPtr.Zero;
|
||||
|
||||
/// <summary>True when a Teams window is currently parented inside a TeamsISO host.</summary>
|
||||
public static bool IsEmbedded => _embeddedHwnd != IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Reparents Teams' most-recently-used top-level window into
|
||||
/// <paramref name="hostHwnd"/>. Strips Teams' caption + thick frame so
|
||||
/// it integrates flush with the host. Returns true on success, false
|
||||
/// if no Teams window could be found.
|
||||
///
|
||||
/// The host HWND is typically obtained via:
|
||||
/// var src = (System.Windows.Interop.HwndSource)
|
||||
/// PresentationSource.FromVisual(MyHostBorder);
|
||||
/// src.Handle // → IntPtr suitable for hostHwnd
|
||||
/// </summary>
|
||||
public static bool EmbedTeamsInto(IntPtr hostHwnd, int width, int height)
|
||||
{
|
||||
if (hostHwnd == IntPtr.Zero) return false;
|
||||
var teamsWindows = FindTeamsTopLevelWindows();
|
||||
if (teamsWindows.Count == 0) return false;
|
||||
|
||||
// Pick the longest-title window as the "main" one — same heuristic
|
||||
// GetActiveWindowTitle uses; matches the call/meeting window.
|
||||
IntPtr best = IntPtr.Zero;
|
||||
int bestLen = -1;
|
||||
foreach (var w in teamsWindows)
|
||||
{
|
||||
var len = GetWindowTextLengthW(w);
|
||||
if (len > bestLen) { bestLen = len; best = w; }
|
||||
}
|
||||
if (best == IntPtr.Zero) return false;
|
||||
|
||||
// Already embedded? Unembed first to clean state.
|
||||
if (_embeddedHwnd != IntPtr.Zero) RestoreEmbed();
|
||||
|
||||
// Save original style + parent so we can fully reverse later.
|
||||
var originalStyle = GetWindowLongPtr(best, GWL_STYLE);
|
||||
var originalParent = SetParent(best, hostHwnd); // returns old parent
|
||||
|
||||
_embedSavedState = (originalParent, originalStyle);
|
||||
_embeddedHwnd = best;
|
||||
|
||||
// Strip top-level decorations + add WS_CHILD so the OS treats it
|
||||
// as a child window of the host.
|
||||
var newStyle = originalStyle;
|
||||
unchecked
|
||||
{
|
||||
newStyle &= ~(int)WS_CAPTION;
|
||||
newStyle &= ~(int)WS_THICKFRAME;
|
||||
newStyle &= ~(int)WS_BORDER;
|
||||
newStyle &= ~(int)WS_DLGFRAME;
|
||||
newStyle &= ~(int)WS_POPUP;
|
||||
newStyle |= (int)WS_CHILD;
|
||||
}
|
||||
SetWindowLongPtr(best, GWL_STYLE, newStyle);
|
||||
|
||||
// Force a non-client recalculation so the style change takes effect.
|
||||
SetWindowPos(best, IntPtr.Zero, 0, 0, 0, 0,
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
// Place at top-left of host, full host size.
|
||||
MoveWindow(best, 0, 0, width, height, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resize the currently-embedded Teams window to <paramref name="width"/>
|
||||
/// × <paramref name="height"/>. Called when the host element resizes
|
||||
/// (window resize, layout change, etc.). No-op if nothing is embedded.
|
||||
/// </summary>
|
||||
public static void ResizeEmbedded(int width, int height)
|
||||
{
|
||||
if (_embeddedHwnd == IntPtr.Zero || width <= 0 || height <= 0) return;
|
||||
MoveWindow(_embeddedHwnd, 0, 0, width, height, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverse an active embed: SetParent back to desktop + restore the
|
||||
/// original window style so Teams looks/behaves like a normal top-level
|
||||
/// window again. Safe to call when nothing is embedded — no-op.
|
||||
/// </summary>
|
||||
public static void RestoreEmbed()
|
||||
{
|
||||
if (_embeddedHwnd == IntPtr.Zero || _embedSavedState is null) return;
|
||||
var (origParent, origStyle) = _embedSavedState.Value;
|
||||
try
|
||||
{
|
||||
// Restore original style FIRST so when we reparent the window's
|
||||
// top-level decorations come back correctly.
|
||||
SetWindowLongPtr(_embeddedHwnd, GWL_STYLE, origStyle);
|
||||
// SetParent(hwnd, Zero) returns to desktop. We could pass
|
||||
// origParent verbatim but for Teams that's always the desktop
|
||||
// anyway, and IntPtr.Zero is documented as "reparent to desktop".
|
||||
SetParent(_embeddedHwnd, IntPtr.Zero);
|
||||
SetWindowPos(_embeddedHwnd, IntPtr.Zero, 0, 0, 0, 0,
|
||||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
catch { /* defensive — restore must never throw */ }
|
||||
finally
|
||||
{
|
||||
_embedSavedState = null;
|
||||
_embeddedHwnd = IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the title bar text of Teams' most-recently-used top-level
|
||||
/// window, or empty string if Teams isn't running. Modern Teams puts
|
||||
|
|
|
|||
|
|
@ -50,7 +50,12 @@ public static class UIPreferences
|
|||
// (UIA Leave button appears), TeamsISO flips global recording on;
|
||||
// when the call ends, recording stops. Pairs naturally with the
|
||||
// headless workflow — operator never touches the recording toggle.
|
||||
bool AutoRecordOnCall = false);
|
||||
bool AutoRecordOnCall = false,
|
||||
// Experimental Phase E.4. SetParent-reparents Teams' main window
|
||||
// into a TeamsISO-owned host. WebView2 in modern Teams can render
|
||||
// weirdly after reparent; if so the operator unticks and falls
|
||||
// back to auto-hide mode. Off by default.
|
||||
bool EmbedTeamsWindow = false);
|
||||
|
||||
public static Prefs Load()
|
||||
{
|
||||
|
|
|
|||
76
src/TeamsISO.App/TeamsEmbedWindow.xaml
Normal file
76
src/TeamsISO.App/TeamsEmbedWindow.xaml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<Window x:Class="TeamsISO.App.TeamsEmbedWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
Title="Teams (embedded)"
|
||||
Icon="/Assets/teamsiso.ico"
|
||||
Width="1280" Height="720"
|
||||
MinWidth="640" MinHeight="360"
|
||||
Background="Black"
|
||||
WindowStyle="None"
|
||||
ResizeMode="CanResize"
|
||||
UseLayoutRounding="True">
|
||||
|
||||
<shell:WindowChrome.WindowChrome>
|
||||
<shell:WindowChrome
|
||||
CaptionHeight="32"
|
||||
ResizeBorderThickness="6"
|
||||
CornerRadius="0"
|
||||
GlassFrameThickness="0"
|
||||
UseAeroCaptionButtons="False"/>
|
||||
</shell:WindowChrome.WindowChrome>
|
||||
|
||||
<Border BorderBrush="{DynamicResource Wd.Border}" BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Caption with experimental warning. The X button restores
|
||||
Teams' chrome before closing, never leaves Teams in a
|
||||
reparented-orphan state. -->
|
||||
<Grid Grid.Row="0" Background="{DynamicResource Wd.Surface}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" Margin="14,0,0,0">
|
||||
<TextBlock Text="TEAMS (EMBEDDED)"
|
||||
Style="{StaticResource Wd.Text.Caption}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Style="{StaticResource Wd.Pill}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0,0,0"
|
||||
Padding="8,2"
|
||||
Background="{DynamicResource Wd.Accent.CoralBg}">
|
||||
<TextBlock Text="experimental"
|
||||
FontFamily="{StaticResource Wd.Font.Mono}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource Wd.Accent.Coral}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource Wd.Button.CaptionClose}"
|
||||
Click="OnClose"
|
||||
shell:WindowChrome.IsHitTestVisibleInChrome="True"
|
||||
ToolTip="Close embed window. Teams' chrome will be restored before this window closes.">
|
||||
<Path Data="M 0,0 L 10,10 M 10,0 L 0,10"
|
||||
Stroke="{DynamicResource Wd.Text.Primary}"
|
||||
StrokeThickness="1.2"
|
||||
Width="10" Height="10"
|
||||
Stretch="None"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Embed host: the Teams window gets SetParent-reparented
|
||||
into this Border's HWND on Loaded. SizeChanged drives
|
||||
MoveWindow to keep Teams fitted to our bounds. -->
|
||||
<Border x:Name="EmbedHost"
|
||||
Grid.Row="1"
|
||||
Background="Black"
|
||||
SizeChanged="OnHostSizeChanged"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
72
src/TeamsISO.App/TeamsEmbedWindow.xaml.cs
Normal file
72
src/TeamsISO.App/TeamsEmbedWindow.xaml.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using TeamsISO.App.Services;
|
||||
|
||||
namespace TeamsISO.App;
|
||||
|
||||
/// <summary>
|
||||
/// Phase E.4 experimental — hosts an embedded copy of the Teams main
|
||||
/// window via SetParent. Operator opens this from Settings → DISPLAY →
|
||||
/// 'Embed Teams window'. The host Border's HWND becomes Teams' parent on
|
||||
/// Loaded; SizeChanged keeps Teams fitted; Closing always restores Teams
|
||||
/// to a normal top-level window before we exit.
|
||||
///
|
||||
/// Failsafes:
|
||||
/// • If no Teams window is found at Loaded, show a friendly message
|
||||
/// instead of leaving the host blank.
|
||||
/// • Restore-on-close runs in a finally block so a crash mid-host
|
||||
/// can't leave Teams orphaned with stripped window styles.
|
||||
/// • TeamsLauncher.RestoreEmbed is idempotent — safe to call even if
|
||||
/// embedding never succeeded.
|
||||
/// </summary>
|
||||
public partial class TeamsEmbedWindow : Window
|
||||
{
|
||||
public TeamsEmbedWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnWindowLoaded;
|
||||
Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
private void OnWindowLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var src = PresentationSource.FromVisual(EmbedHost) as HwndSource;
|
||||
if (src is null || src.Handle == IntPtr.Zero)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Couldn't obtain a host HWND for the embed window. " +
|
||||
"Try closing and re-opening the embed window.",
|
||||
"TeamsISO — embed",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var w = (int)EmbedHost.ActualWidth;
|
||||
var h = (int)EmbedHost.ActualHeight;
|
||||
if (!TeamsLauncher.EmbedTeamsInto(src.Handle, w, h))
|
||||
{
|
||||
MessageBox.Show(
|
||||
"Couldn't find a Microsoft Teams window to embed. " +
|
||||
"Launch Teams first (rail camera icon), then re-open this window.",
|
||||
"TeamsISO — embed",
|
||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHostSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
// Keep Teams sized to match the host as the embed window resizes.
|
||||
// No-op when nothing is embedded.
|
||||
TeamsLauncher.ResizeEmbedded((int)e.NewSize.Width, (int)e.NewSize.Height);
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object? sender, EventArgs e)
|
||||
{
|
||||
// ALWAYS restore Teams to top-level state when this window closes,
|
||||
// even if the embed never succeeded. Idempotent.
|
||||
try { TeamsLauncher.RestoreEmbed(); }
|
||||
catch { /* defensive — restore is best-effort */ }
|
||||
}
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
|||
private bool _launchTeamsOnStartup;
|
||||
private bool _autoHideTeamsWindows;
|
||||
private bool _autoRecordOnCall;
|
||||
private bool _embedTeamsWindow;
|
||||
|
||||
public GlobalSettingsViewModel(IIsoController controller, ToastViewModel? toast = null)
|
||||
{
|
||||
|
|
@ -65,6 +66,7 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
|||
_launchTeamsOnStartup = uiPrefs.LaunchTeamsOnStartup;
|
||||
_autoHideTeamsWindows = uiPrefs.AutoHideTeamsWindows;
|
||||
_autoRecordOnCall = uiPrefs.AutoRecordOnCall;
|
||||
_embedTeamsWindow = uiPrefs.EmbedTeamsWindow;
|
||||
|
||||
// Bring the auto-apply flag in from the presets store so the checkbox
|
||||
// reflects the user's prior choice when the settings panel opens.
|
||||
|
|
@ -258,7 +260,8 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
|||
ControlSurfaceLanReachable: _controlSurfaceLanReachable,
|
||||
LaunchTeamsOnStartup: _launchTeamsOnStartup,
|
||||
AutoHideTeamsWindows: _autoHideTeamsWindows,
|
||||
AutoRecordOnCall: _autoRecordOnCall));
|
||||
AutoRecordOnCall: _autoRecordOnCall,
|
||||
EmbedTeamsWindow: _embedTeamsWindow));
|
||||
|
||||
/// <summary>
|
||||
/// Auto-launch the Microsoft Teams desktop client when TeamsISO starts.
|
||||
|
|
@ -307,6 +310,23 @@ public sealed class GlobalSettingsViewModel : ObservableObject
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EXPERIMENTAL: SetParent-reparents Teams' main window into a TeamsISO-
|
||||
/// owned host so Teams visually appears inside our window. WebView2 in
|
||||
/// modern Teams may render weirdly after reparent — if so, untick and
|
||||
/// fall back to the auto-hide flow. Polling logic in MainWindow.xaml.cs
|
||||
/// applies / restores the embed; this property is just the persisted
|
||||
/// toggle.
|
||||
/// </summary>
|
||||
public bool EmbedTeamsWindow
|
||||
{
|
||||
get => _embedTeamsWindow;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _embedTeamsWindow, value)) PersistUiPrefs();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record each newly-enabled ISO's normalized output to disk under
|
||||
/// <see cref="RecordingDirectory"/>. Already-running ISOs are not retroactively
|
||||
|
|
|
|||
Loading…
Reference in a new issue