feat(backdrop): Capture Window… in-app window pin (TASK 38, Good Dog ONE test)
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.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
using ytLive.Helpers;
|
||||
using ytLive.Models;
|
||||
|
||||
namespace ytLive.ViewModels;
|
||||
|
||||
public partial class MainViewModel
|
||||
{
|
||||
private const string WindowKeyPrefix = "window:";
|
||||
private const string PickerKeyPrefix = "picker:";
|
||||
|
||||
/// <summary>True for session-scoped capture keys that must not persist
|
||||
/// across a reload: "picker:" picks (OS GraphicsCapturePicker) and "window:"
|
||||
/// pins (an HWND can be recycled to a different window after restart).</summary>
|
||||
private static bool IsTransientCaptureKey(string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return false;
|
||||
return key.StartsWith(WindowKeyPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| key.StartsWith(PickerKeyPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>True when the given capture key is a window pin whose window is
|
||||
/// still among the enumerated open windows — the pin then outranks
|
||||
/// auto-detection (see OnFullscreenMonitorChanged).</summary>
|
||||
private bool IsAliveWindowPin(string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)
|
||||
|| !key.StartsWith(WindowKeyPrefix, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
var hwndText = key.AsSpan(WindowKeyPrefix.Length).TrimStart();
|
||||
if (hwndText.Length >= 2 && hwndText[0] == '0' && (hwndText[1] == 'x' || hwndText[1] == 'X'))
|
||||
hwndText = hwndText[2..];
|
||||
var hwnd = long.TryParse(hwndText, System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : 0;
|
||||
foreach (var window in _windowEnumerator.Enumerate())
|
||||
if (window.Hwnd == hwnd)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>ScreenCaptureManager's session failed (the pinned window closed,
|
||||
/// a monitor went away, a transient pick expired, …). For a transient
|
||||
/// session-scoped key the pin no longer resolves — fall back to the automatic
|
||||
/// capture (game → desktop → static), mirroring the reload heal.</summary>
|
||||
private void OnScreenCaptureFailed(string key, string message)
|
||||
{
|
||||
AppLog.Write($"ScreenCaptureManager: capture '{key}' failed: {message}");
|
||||
if (!IsTransientCaptureKey(key)) return;
|
||||
|
||||
var pinned = AllBackgrounds().FirstOrDefault(b => b.CaptureKey == key);
|
||||
if (pinned == null) return;
|
||||
ReacquireScreenCaptures();
|
||||
}
|
||||
|
||||
/// <summary>Open windows offered to the "Capture Window…" submenu (session
|
||||
/// snapshot; refresh via <see cref="RefreshWindowsCommand"/>).</summary>
|
||||
public ObservableCollection<WindowInfo> OpenWindows { get; } = new();
|
||||
|
||||
public ICommand RefreshWindowsCommand { get; }
|
||||
public ICommand CaptureWindowCommand { get; }
|
||||
|
||||
private void RefreshOpenWindows()
|
||||
{
|
||||
var windows = _windowEnumerator.Enumerate();
|
||||
OpenWindows.Clear();
|
||||
foreach (var window in windows)
|
||||
OpenWindows.Add(window);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user