refactor(services): extract TeamsEmbedHost from TeamsLauncher
TeamsLauncher.cs was 665 lines / 30KB and mixed two unrelated lifecycles: launch / hide / show / in-call orchestration (the bulk of the file), and the Phase E.4 experimental SetParent-based embedding (~160 lines of distinct Win32 surface area + its own state machine). * Services/TeamsEmbedHost.cs (177L, new) — public static class owning EmbedTeamsInto / ResizeEmbedded / RestoreEmbed / IsEmbedded plus the Win32 p/invokes specific to embedding (SetParent, GetWindowLongPtr, SetWindowLongPtr, MoveWindow, SetWindowPos), the WS_* / SWP_* style + position constants, and the embed-state fields. The whole lifecycle (reparent → resize → restore) now lives in one place; the comment about WebView2 fragility moves with the code. * Services/TeamsLauncher.cs (was 665L → now 510L) — keeps launch / stop / join / hide / show / window-title / shortcut concerns. The internal helper that enumerates Teams top-level windows is now named EnumerateTopLevelTeamsWindows (was FindTeamsTopLevelWindows) and marked `internal` so TeamsEmbedHost can call it without duplicating the EnumWindows traversal — both classes use the same process-name heuristic and the launcher's hide/show paths also consume it. * TeamsEmbedWindow.xaml.cs — call sites moved from TeamsLauncher.* to TeamsEmbedHost.* (three references). No behavior change. Build clean; 56 + 104 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2640739bfc
commit
1f07992100
3 changed files with 191 additions and 169 deletions
177
src/TeamsISO.App/Services/TeamsEmbedHost.cs
Normal file
177
src/TeamsISO.App/Services/TeamsEmbedHost.cs
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace TeamsISO.App.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase E.4 — Embedded Teams via SetParent.
|
||||||
|
///
|
||||||
|
/// Reparents Teams' main top-level window into a TeamsISO-owned host
|
||||||
|
/// (typically a Border element's HWND). Strips the captured window's
|
||||||
|
/// caption + thick frame so it integrates flush with the host, and
|
||||||
|
/// remembers enough about the original to restore it cleanly later.
|
||||||
|
///
|
||||||
|
/// 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 ensure the restore path always runs (the
|
||||||
|
/// caller wraps Embed in a finally block) so operators can fall back to
|
||||||
|
/// auto-hide mode if embedding misbehaves on their specific Teams build.
|
||||||
|
///
|
||||||
|
/// Lives in its own static class — separated from <see cref="TeamsLauncher"/>
|
||||||
|
/// because the embedding lifecycle (reparent → resize → restore) is its
|
||||||
|
/// own thing, and the Win32 surface it requires (SetParent / window-style
|
||||||
|
/// muck / SetWindowPos / MoveWindow) isn't reused by the launch / hide /
|
||||||
|
/// in-call control paths.
|
||||||
|
/// </summary>
|
||||||
|
public static class TeamsEmbedHost
|
||||||
|
{
|
||||||
|
[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)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||||
|
private static extern int GetWindowTextLengthW(IntPtr hWnd);
|
||||||
|
|
||||||
|
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 = TeamsLauncher.EnumerateTopLevelTeamsWindows();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -271,168 +271,6 @@ public static class TeamsLauncher
|
||||||
private static extern int GetWindowTextLengthW(IntPtr hWnd);
|
private static extern int GetWindowTextLengthW(IntPtr hWnd);
|
||||||
|
|
||||||
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
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>
|
/// <summary>
|
||||||
/// Returns the title bar text of Teams' most-recently-used top-level
|
/// Returns the title bar text of Teams' most-recently-used top-level
|
||||||
/// window, or empty string if Teams isn't running. Modern Teams puts
|
/// window, or empty string if Teams isn't running. Modern Teams puts
|
||||||
|
|
@ -482,7 +320,14 @@ public static class TeamsLauncher
|
||||||
/// process. "Top-level" means GetWindow(GW_OWNER) is null (the window is
|
/// process. "Top-level" means GetWindow(GW_OWNER) is null (the window is
|
||||||
/// not a tooltip or popup of another). Used by Hide/Show.
|
/// not a tooltip or popup of another). Used by Hide/Show.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<IntPtr> FindTeamsTopLevelWindows()
|
/// <summary>
|
||||||
|
/// Return the visible top-level windows owned by any Teams process.
|
||||||
|
/// Exposed internal so <see cref="TeamsEmbedHost"/> can pick the
|
||||||
|
/// "best" candidate to reparent without re-implementing the
|
||||||
|
/// enumeration. Keep this in TeamsLauncher because the launch /
|
||||||
|
/// hide / show paths use the same list.
|
||||||
|
/// </summary>
|
||||||
|
internal static List<IntPtr> EnumerateTopLevelTeamsWindows()
|
||||||
{
|
{
|
||||||
var teamsPids = new HashSet<uint>(
|
var teamsPids = new HashSet<uint>(
|
||||||
TeamsProcessNames
|
TeamsProcessNames
|
||||||
|
|
@ -509,7 +354,7 @@ public static class TeamsLauncher
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static int HideWindows()
|
public static int HideWindows()
|
||||||
{
|
{
|
||||||
var windows = FindTeamsTopLevelWindows();
|
var windows = EnumerateTopLevelTeamsWindows();
|
||||||
foreach (var w in windows) ShowWindow(w, SW_HIDE);
|
foreach (var w in windows) ShowWindow(w, SW_HIDE);
|
||||||
return windows.Count;
|
return windows.Count;
|
||||||
}
|
}
|
||||||
|
|
@ -610,7 +455,7 @@ public static class TeamsLauncher
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool SendShortcut(ShortcutModifiers modifiers, int virtualKey)
|
public static bool SendShortcut(ShortcutModifiers modifiers, int virtualKey)
|
||||||
{
|
{
|
||||||
var windows = FindTeamsTopLevelWindows();
|
var windows = EnumerateTopLevelTeamsWindows();
|
||||||
if (windows.Count == 0) return false;
|
if (windows.Count == 0) return false;
|
||||||
var hwnd = windows[^1];
|
var hwnd = windows[^1];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ namespace TeamsISO.App;
|
||||||
/// instead of leaving the host blank.
|
/// instead of leaving the host blank.
|
||||||
/// • Restore-on-close runs in a finally block so a crash mid-host
|
/// • Restore-on-close runs in a finally block so a crash mid-host
|
||||||
/// can't leave Teams orphaned with stripped window styles.
|
/// can't leave Teams orphaned with stripped window styles.
|
||||||
/// • TeamsLauncher.RestoreEmbed is idempotent — safe to call even if
|
/// • TeamsEmbedHost.RestoreEmbed is idempotent — safe to call even if
|
||||||
/// embedding never succeeded.
|
/// embedding never succeeded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class TeamsEmbedWindow : Window
|
public partial class TeamsEmbedWindow : Window
|
||||||
|
|
@ -43,7 +43,7 @@ public partial class TeamsEmbedWindow : Window
|
||||||
|
|
||||||
var w = (int)EmbedHost.ActualWidth;
|
var w = (int)EmbedHost.ActualWidth;
|
||||||
var h = (int)EmbedHost.ActualHeight;
|
var h = (int)EmbedHost.ActualHeight;
|
||||||
if (!TeamsLauncher.EmbedTeamsInto(src.Handle, w, h))
|
if (!TeamsEmbedHost.EmbedTeamsInto(src.Handle, w, h))
|
||||||
{
|
{
|
||||||
MessageBox.Show(
|
MessageBox.Show(
|
||||||
"Couldn't find a Microsoft Teams window to embed. " +
|
"Couldn't find a Microsoft Teams window to embed. " +
|
||||||
|
|
@ -57,14 +57,14 @@ public partial class TeamsEmbedWindow : Window
|
||||||
{
|
{
|
||||||
// Keep Teams sized to match the host as the embed window resizes.
|
// Keep Teams sized to match the host as the embed window resizes.
|
||||||
// No-op when nothing is embedded.
|
// No-op when nothing is embedded.
|
||||||
TeamsLauncher.ResizeEmbedded((int)e.NewSize.Width, (int)e.NewSize.Height);
|
TeamsEmbedHost.ResizeEmbedded((int)e.NewSize.Width, (int)e.NewSize.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWindowClosed(object? sender, EventArgs e)
|
private void OnWindowClosed(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// ALWAYS restore Teams to top-level state when this window closes,
|
// ALWAYS restore Teams to top-level state when this window closes,
|
||||||
// even if the embed never succeeded. Idempotent.
|
// even if the embed never succeeded. Idempotent.
|
||||||
try { TeamsLauncher.RestoreEmbed(); }
|
try { TeamsEmbedHost.RestoreEmbed(); }
|
||||||
catch { /* defensive — restore is best-effort */ }
|
catch { /* defensive — restore is best-effort */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue