From cc29c503a99e404d35732f2c4dfd54fdb4709ba9 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 10 May 2026 21:14:42 -0400 Subject: [PATCH] Phase E.4 experimental: SetParent-embed Teams window inside TeamsISO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/TeamsISO.App/MainWindow.xaml | 19 +++ src/TeamsISO.App/MainWindow.xaml.cs | 13 ++ src/TeamsISO.App/Services/TeamsLauncher.cs | 161 ++++++++++++++++++ src/TeamsISO.App/Services/UIPreferences.cs | 7 +- src/TeamsISO.App/TeamsEmbedWindow.xaml | 76 +++++++++ src/TeamsISO.App/TeamsEmbedWindow.xaml.cs | 72 ++++++++ .../ViewModels/GlobalSettingsViewModel.cs | 22 ++- 7 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 src/TeamsISO.App/TeamsEmbedWindow.xaml create mode 100644 src/TeamsISO.App/TeamsEmbedWindow.xaml.cs diff --git a/src/TeamsISO.App/MainWindow.xaml b/src/TeamsISO.App/MainWindow.xaml index 5ab756a..d1fc1c0 100644 --- a/src/TeamsISO.App/MainWindow.xaml +++ b/src/TeamsISO.App/MainWindow.xaml @@ -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."/> + + + + + + + + + + + diff --git a/src/TeamsISO.App/TeamsEmbedWindow.xaml.cs b/src/TeamsISO.App/TeamsEmbedWindow.xaml.cs new file mode 100644 index 0000000..4e0d0db --- /dev/null +++ b/src/TeamsISO.App/TeamsEmbedWindow.xaml.cs @@ -0,0 +1,72 @@ +using System.Windows; +using System.Windows.Interop; +using TeamsISO.App.Services; + +namespace TeamsISO.App; + +/// +/// 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. +/// +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(); +} diff --git a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs index d621f2d..aaf98b3 100644 --- a/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs +++ b/src/TeamsISO.App/ViewModels/GlobalSettingsViewModel.cs @@ -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)); /// /// Auto-launch the Microsoft Teams desktop client when TeamsISO starts. @@ -307,6 +310,23 @@ public sealed class GlobalSettingsViewModel : ObservableObject } } + /// + /// 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. + /// + public bool EmbedTeamsWindow + { + get => _embedTeamsWindow; + set + { + if (SetField(ref _embedTeamsWindow, value)) PersistUiPrefs(); + } + } + /// /// Record each newly-enabled ISO's normalized output to disk under /// . Already-running ISOs are not retroactively