4e0915a36e
Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.
Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.
Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.
Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
122 lines
4.1 KiB
C#
122 lines
4.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// Win32 implementation of <see cref="IWindowEnumerator"/>. Enumerates visible
|
|
/// top-level windows for the "Capture Window…" submenu, mirroring the style of
|
|
/// <see cref="Win32FullScreenDetector"/>: visible, non-cloaked, zero-rect
|
|
/// excluded, and windows belonging to this process excluded (the app must never
|
|
/// offer itself for pinning). Ordered by process name, then title.
|
|
/// </summary>
|
|
public sealed class Win32WindowEnumerator : IWindowEnumerator
|
|
{
|
|
private static readonly IntPtr OwnProcessId = new(Environment.ProcessId);
|
|
|
|
public IReadOnlyList<WindowInfo> Enumerate()
|
|
{
|
|
var results = new List<WindowInfo>();
|
|
EnumWindows((hwnd, _) =>
|
|
{
|
|
if (TryDescribe(hwnd, out var info))
|
|
results.Add(info);
|
|
return true; // keep enumerating
|
|
}, IntPtr.Zero);
|
|
|
|
results.Sort(static (a, b) =>
|
|
{
|
|
var byProcess = string.Compare(a.ProcessName, b.ProcessName, StringComparison.OrdinalIgnoreCase);
|
|
return byProcess != 0 ? byProcess
|
|
: string.Compare(a.Title, b.Title, StringComparison.OrdinalIgnoreCase);
|
|
});
|
|
return results;
|
|
}
|
|
|
|
private static bool TryDescribe(IntPtr hwnd, out WindowInfo info)
|
|
{
|
|
info = null!;
|
|
if (GetWindowThreadProcessId(hwnd, out var pid) == 0) return false;
|
|
if (pid == 0 || new IntPtr((long)pid) == OwnProcessId) return false;
|
|
if (!IsWindowVisible(hwnd)) return false;
|
|
if (IsCloaked(hwnd)) return false;
|
|
if (!TryGetRect(hwnd, out var rect) || rect.Right <= rect.Left || rect.Bottom <= rect.Top) return false;
|
|
|
|
var title = GetTitle(hwnd);
|
|
if (string.IsNullOrWhiteSpace(title)) return false;
|
|
|
|
var processName = ProcessName((int)pid);
|
|
if (string.IsNullOrWhiteSpace(processName)) return false;
|
|
|
|
info = new WindowInfo(title, processName, hwnd.ToInt64(), (int)pid);
|
|
return true;
|
|
}
|
|
|
|
private static string ProcessName(int pid)
|
|
{
|
|
try
|
|
{
|
|
using var process = Process.GetProcessById(pid);
|
|
return process.ProcessName;
|
|
}
|
|
catch { return string.Empty; }
|
|
}
|
|
|
|
private static string GetTitle(IntPtr hwnd)
|
|
{
|
|
var length = GetWindowTextLength(hwnd);
|
|
if (length <= 0) return string.Empty;
|
|
var sb = new StringBuilder(length + 1);
|
|
GetWindowText(hwnd, sb, sb.Capacity);
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static bool IsCloaked(IntPtr hwnd)
|
|
{
|
|
const int DWMWA_CLOAKED = 14;
|
|
var cloaked = 0;
|
|
return DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, out cloaked, sizeof(int)) == 0 && cloaked != 0;
|
|
}
|
|
|
|
private static bool TryGetRect(IntPtr hwnd, out Rect rect)
|
|
{
|
|
rect = default;
|
|
return GetWindowRect(hwnd, out rect);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct Rect
|
|
{
|
|
public int Left;
|
|
public int Top;
|
|
public int Right;
|
|
public int Bottom;
|
|
}
|
|
|
|
private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool IsWindowVisible(IntPtr hWnd);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
|
|
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern int GetWindowTextLength(IntPtr hWnd);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect);
|
|
|
|
[DllImport("dwmapi.dll")]
|
|
private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
|
} |