fix(recording-dialog): +20% height so the file-name box is not clipped; unhook full-screen hook on shutdown

RenameRecordingDialog was 230px tall for ~225px of stacked content —
the file-name textbox clipped (old client area ~193px). Height is now 276
(+20%). Integration test shows the real dialog in the app host and asserts
the textbox bottom edge stays inside the client area; the dialog's Icon moved
to the explicit /ytLive;component/ URLC form MainWindow already uses so tests
can construct it (root-relative /Assets/... only resolves in production via
Application.ResourceAssembly).

Dependency fix: MainViewModel.Shutdown() now calls
_fullScreenDetector.StopWatching(). The global EVENT_SYSTEM_FOREGROUND hook
was never unhooked; after VM collection the next foreground event invoked a
garbage-collected WinEventProc delegate, crashing the whole test run. Harmless
in production (exits the process) but fatal in the multi-window test host.

Reference: WPF WinEvent hook lifecycle guidance — SetWinEventHook callbacks
must be unhooked before the owning object is collected (obs-projector
fullscreen-detection pattern).
This commit is contained in:
2026-09-18 08:33:13 -07:00
parent c11788574e
commit 0d0e55de31
4 changed files with 76 additions and 14 deletions
+24 -12
View File
@@ -1,15 +1,9 @@
# HANDOFF — 2026-09-18 (layer drag-reorder persistence fix committed locally — no push)
# HANDOFF — 2026-09-18 (drag-reorder persistence + recording-dialog height, both local — no push)
## Branch / Commit State
`main` HEAD = **layer drag-reorder now traps + persists** (local, this session — the new
`LayerReorderPersistenceTests`). Below it:
- `75723ac` fix(webcam): offer the Web Cam row only when a camera is attainable (live lock)
- `9d00955` feat(webcam): app-default gate slice — per-scene offer, Add places default directly,
identity survives removal
- `1e4017d` feat(webcam): resource lifecycle startup slice — poll-on-start, single-cam lock,
persistent Layers alert
`main` HEAD = **Rename Recording dialog 20% taller + teardown unhooks the full-screen hook** (local).
Below it: the layer drag-reorder persistence fix (`LayerReorderPersistenceTests`), then webcam work.
**No push yet.** Working tree clean.
@@ -70,6 +64,22 @@ ScheduleSave()`, i.e. the same background-save path every other mutation uses. *
calls the trap, pumps the dispatcher until the debounced save lands, asserts the DB `Source`
SortOrder matches the in-memory `Elements` order.
## ✅ Committed — Rename Recording dialog taller + full-screen hook unhooked on shutdown (2026-09-18)
- `RenameRecordingDialog.xaml` `Height` 230 → **276** (+20%, creator: textbox cut off). The fix is
real: content naturally needs ~225px, the old client area at 230 was ~193px (clipped), at 276 it's
~239px (fits). **Good Dog test** `RenameRecordingDialogSizingTests`: shows the real dialog in the
RealApp host and asserts the file-name textbox bottom edge is inside the client area. The dialog's
`Icon` was changed from root-relative `/Assets/...` to the explicit `/ytLive;component/...` (the
form `MainWindow.xaml` already uses) — root-relative only resolves in production via
`Application.ResourceAssembly`, so dialogs couldn't be constructed by tests otherwise. The other
dialogs still use the fragile form; fix as they get tested.
- **Teardown leak fix (dependency, discovered by the above):** `MainViewModel.Shutdown()` now calls
`_fullScreenDetector.StopWatching()`. The global `EVENT_SYSTEM_FOREGROUND` hook was never unhooked,
so after the VM was collected the next foreground event invoked a **garbage-collected
`WinEventProc` delegate → process crash** ("Test Run Aborted" in the suite). Harmless in production
(exits the process) but fatal to the multi-window test host when a test shows/foregrounds a window.
## ✅ Earlier committed — webcam resource lifecycle (startup slice, `1e4017d`)
Startup poll + tri-state (`ValidateWebcamResourceStartupAsync` after `LoadLayout`): 0 → run on,
@@ -84,9 +94,7 @@ claim — `CameraConflictProbe` reads process names only, no device handles; do
## ⚠️ Open items
- **RenameRecordingDialog too short (creator-reported, QUEUED next):** the recording save/confirm
dialog cuts off the file-name textbox — increase its height by ~20% (+ its one integration test,
Good Dog).
- **RenameRecordingDialog too short — DONE (see below).**
- **Webcam take fix 2 of 2 — the 09:16 crash is UNFIXED (hard, untested):** hypothesis — native WMF
death when `MediaCapture.Failed` fires mid-stream and `SafeStopAsync` (fire-and-forget) disposes
reader+capture while the frame thread sits in `TryAcquireLatestFrame`/`Marshal.Copy` (outside the
@@ -143,6 +151,10 @@ claim — `CameraConflictProbe` reads process names only, no device handles; do
- ffmpeg/ffprobe: `/mnt/c/Program Files/Krita (x64)/bin/` with Windows paths.
- `MyMistakes.md`: WINRT resource-allocation RECIPE, freeze-audit RECIPE, A/V sync recipe, deadline
pacing, CoreMessaging DQ, WGC-CLIP + slice blocks. Grep before re-deriving.
- **Test-host crash (fixed 2026-09-18):** "callback was made on a garbage collected delegate …
`Win32FullScreenDetector+WinEventProc`" — `Shutdown()` now unhooks the foreground-event hook; never
re-introduce `StartWatching` without a matching teardown, and dialogs whose `Icon` is the
root-relative `/Assets/…` form can't be instantiated by tests (use `/ytLive;component/…`).
- **Scene.Elements layer tests:** any seeded scene with `HasBackground=1` loads a healed
`Background` element pinned at `Elements[0]` (created by `EnsureBackground` on `StagedScene` set) —
compute expected layer orders from the live collection, never hardcode indices.
+2 -2
View File
@@ -1,8 +1,8 @@
<Window x:Class="ytLive.RenameRecordingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Rename Recording" Height="230" Width="460"
Icon="/Assets/llamacasty-icon.png"
Title="Rename Recording" Height="276" Width="460"
Icon="/ytLive;component/Assets/llamacasty-icon.png"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
ShowInTaskbar="False" Background="#1a1a2e">
+1
View File
@@ -418,6 +418,7 @@ public partial class MainViewModel : ViewModelBase
{
_saveDebounce?.Stop();
SaveLayoutNow();
_fullScreenDetector.StopWatching();
_audioMixer.Dispose();
_framePump.Dispose();
_cameraManager.Dispose();
@@ -0,0 +1,49 @@
using System.Windows;
using Xunit;
namespace ytLive.Tests;
/// <summary>
/// The Rename Recording modal (creator-reported, 2026-09-18) cut off the file-name
/// textbox: the fixed window was too short for its stacked content. The height is
/// bumped 20% (230 → 276); this test renders the real dialog and asserts the textbox
/// bottom edge lands inside the client area instead of clipping.
/// </summary>
[Collection("RealApp")]
public sealed class RenameRecordingDialogSizingTests
{
private readonly RealAppHost _app;
public RenameRecordingDialogSizingTests(RealAppHost app) => _app = app;
[Fact]
public void DialogHeight_Bumps20Percent_AndNameBoxIsNotClipped()
{
_app.Run(Run);
}
private void Run()
{
var dialog = new RenameRecordingDialog("rec-2026-09-18");
try
{
Assert.Equal(276, dialog.Height);
dialog.Show();
dialog.UpdateLayout();
var box = dialog.NameBox;
Assert.True(box.ActualWidth > 0, "file-name textbox must have a real layout");
var client = (FrameworkElement)dialog.Content;
var bottom = box.TransformToAncestor(client)
.TransformBounds(new Rect(box.RenderSize)).Bottom;
Assert.True(bottom <= client.ActualHeight,
$"textbox bottom ({bottom:0.##}) must sit inside the client area " +
$"({client.ActualHeight:0.##}) — the dialog is clipping the name box");
}
finally
{
dialog.Close();
}
}
}