Desktop 'lock' fix: the window no longer steals focus when a full-screen game owns the foreground at launch. Opening after an exclusive-fullscreen game (creator's THE DIVISION 2 gameplay-stream start) the window ACTIVATED by default, Windows dropped the game to the taskbar and the 1920x1040 window covered the whole work area - 'the desktop is locked and I can't reach the game with the mouse'. MainWindow ctor now sets ShowActivated = !MainViewModel.FullscreenGameRunningNow (GetForegroundFullScreenMonitorIndex() != null), so an in-game launch keeps the game focused and live; a normal launch is unchanged. New FullScreenDetectorOverride test seam (mirrors WindowEnumeratorOverride - deterministic, no real Win32 foreground state) + DesktopLockTests: game at launch -> ShowActivated=false, normal launch -> true, FullscreenGameRunningNow reflects the detector. 422 pass / 1 known env flake, 0 build warnings. (Unit: launch focus)

This commit is contained in:
2026-10-10 11:52:17 -07:00
parent c2d64dcd6d
commit 63bf8444bd
6 changed files with 221 additions and 4 deletions
+31 -3
View File
@@ -1,9 +1,30 @@
# HANDOFF — 2026-10-09 (TASK 51 quality pass COMPLETE: jitter + blur-heavy both fixed) # HANDOFF — 2026-10-10 (desktop-"lock" fix local, uncommitted)
## At a glance
- **Branch** main (no feature branch). Pushed through `5685c5d`; everything after is local, **not
pushed** (push at the creator's call).
- Three work units in the working tree: (1) the desktop-lock fix below, (2) the TASK 51 webcam unit,
(3) the creator's pinned-chat unit. Do NOT mix them in one commit.
## Branch ## Branch
- main (no feature branch). Pushed through `5685c5d` (TRAX toggle); everything after is local, - main (no feature branch). Pushed through `5685c5d` (TRAX toggle); everything after is local,
**not pushed** (push at the creator's call). **not pushed** (push at the creator's call).
## New work unit (this handoff) — READY TO VERIFY, do not commit yet
- **Fix** for the "desktop is locked once the app starts" report — `MainWindow.xaml.cs` +
`ViewModels/MainViewModel.Background.cs` (`ShowActivated = !FullscreenGameRunningNow`, see
`bugs.md` entry 2026-10-10). Scope-locked files: `MainWindow.xaml.cs`,
`ViewModels/MainViewModel.Background.cs`, `ViewModels/MainViewModel.cs` (detector seam line),
`ytLive.Tests/DesktopLockTests.cs` (new), `bugs.md`, this file.
- **Regression tests:** `DesktopLockTests` (3 tests) drive the rule through the NEW
`FullScreenDetectorOverride` test seam (the same seam pattern as `WindowEnumeratorOverride`) —
no real Win32 foreground state is touched: full-screen game at launch ⇒ `ShowActivated=false`;
normal launch ⇒ still `true`; `FullscreenGameRunningNow` reflects the detector. Full suite
**422 passed / 0 failed / 0 warnings**. Real-app reproduction still deferred to the creator.
- **To commit later (this unit only):** the two code files + the two memory files listed above.
- Verify with the creator first: launch while an exclusive-fullscreen game is the foreground — the
game must stay live and the app must open behind/alongside it without focus steal.
## Dirty files — split by ownership ## Dirty files — split by ownership
- **This work unit (COMMIT these):** `Services/BackgroundEffects/MaskSmoother.cs` (new), the smoother - **This work unit (COMMIT these):** `Services/BackgroundEffects/MaskSmoother.cs` (new), the smoother
wiring in `Services/CameraManager.cs`, `ytLive.Tests/CameraManagerTests.cs` (new integration test), wiring in `Services/CameraManager.cs`, `ytLive.Tests/CameraManagerTests.cs` (new integration test),
@@ -34,8 +55,15 @@
the 0.5 rounding (3 box passes dilute a spike) — use a step edge, not a spike, to measure blur. the 0.5 rounding (3 box passes dilute a spike) — use a step edge, not a spike, to measure blur.
## Next step ## Next step
- Creator re-runs the app: webcam → Layers properties → Background → Remove / Blur + Strength, and - Creator verifies the desktop-lock fix: while THE DIVISION 2 (or any exclusive-fullscreen game) is the
reports whether the jitter is gone and the blur feels right at the slider's default and full range. foreground window, launch the app — the game must stay live and the app must not yank focus. Also
confirm a normal launch is unchanged. Then I can commit the unit and run the full suite when the
mouse is free.
## Tests
- Full suite **422 passed / 0 failed / 0 skipped / 0 warnings** (2026-10-10; includes the 3 new
`DesktopLockTests`). Known env flakes remain `Mix_HonorsProviderGains…` (fails while system audio
plays) and `RealMouseDrag…` (needs an idle desktop).
## Also still open ## Also still open
- **Vertical-format regression (STALE, not fixed):** 1080p60 vertical selection no longer shrinks the - **Vertical-format regression (STALE, not fixed):** 1080p60 vertical selection no longer shrinks the
+4
View File
@@ -23,6 +23,10 @@ public partial class MainWindow : Window
AppLog.Write("MainWindow ctor: after InitializeComponent"); AppLog.Write("MainWindow ctor: after InitializeComponent");
_viewModel = (MainViewModel)DataContext; _viewModel = (MainViewModel)DataContext;
_viewModel.PropertyChanged += OnViewModelPropertyChanged; _viewModel.PropertyChanged += OnViewModelPropertyChanged;
// Don't steal focus from an exclusive-fullscreen game the creator is already
// playing (e.g. while starting a gameplay stream): activating our window would
// drop that game to the taskbar and make the desktop look "locked".
ShowActivated = !_viewModel.FullscreenGameRunningNow;
_topBar = (Controls.TopBar)FindName("TopBar")!; _topBar = (Controls.TopBar)FindName("TopBar")!;
_previewPane = (Controls.PreviewPane)FindName("PreviewPane")!; _previewPane = (Controls.PreviewPane)FindName("PreviewPane")!;
_leftPanel = (Controls.LeftPanel)FindName("LeftPanel")!; _leftPanel = (Controls.LeftPanel)FindName("LeftPanel")!;
+15
View File
@@ -24,6 +24,12 @@ public partial class MainViewModel
/// swaps in a fake enumerator to drive the "window dies -> re-heal" flow.</summary> /// swaps in a fake enumerator to drive the "window dies -> re-heal" flow.</summary>
internal static IWindowEnumerator? WindowEnumeratorOverride { get; set; } internal static IWindowEnumerator? WindowEnumeratorOverride { get; set; }
/// <summary>Test seam for the launch focus decision: a fake detector simulates
/// a full-screen game owning the foreground without touching any real window,
/// so the <c>ShowActivated = !FullscreenGameRunningNow</c> rule (desktop-"lock"
/// fix 2026-10-10) is pinned without Win32 state.</summary>
internal static IFullScreenDetector? FullScreenDetectorOverride { get; set; }
private ImageSource? _activeBackgroundImage; private ImageSource? _activeBackgroundImage;
private ImageSource? _backgroundImage; private ImageSource? _backgroundImage;
private bool _backgroundSizeMismatch; private bool _backgroundSizeMismatch;
@@ -168,6 +174,15 @@ public partial class MainViewModel
return null; return null;
} }
/// <summary>True when a full-screen game/app owns the foreground right now.
/// MainWindow reads this at launch to decide whether to steal focus: opening
/// LlamaCasty while the creator is playing an exclusive-fullscreen game would
/// minimize that game out of fullscreen — the "desktop is locked and I can't
/// get back into the game" bug (2026-10-10) — so the window opens without
/// activating instead.</summary>
public bool FullscreenGameRunningNow
=> _fullScreenDetector.GetForegroundFullScreenMonitorIndex() != null;
// After a layout load / file open: make sure every background-enabled scene // After a layout load / file open: make sure every background-enabled scene
// has a background, give any background without a persisted key the auto-detected // has a background, give any background without a persisted key the auto-detected
// one, then acquire one capture per unique background key. ScreenCaptureManager // one, then acquire one capture per unique background key. ScreenCaptureManager
+1 -1
View File
@@ -312,7 +312,7 @@ public partial class MainViewModel : ViewModelBase
_musicPlayer.PlaybackEnded += OnTraxPlaybackEnded; _musicPlayer.PlaybackEnded += OnTraxPlaybackEnded;
_ = StartMicCaptureAsync(); _ = StartMicCaptureAsync();
_fullScreenDetector = new Win32FullScreenDetector(); _fullScreenDetector = FullScreenDetectorOverride ?? new Win32FullScreenDetector();
_fullScreenDetector.FullscreenMonitorChanged += OnFullscreenMonitorChanged; _fullScreenDetector.FullscreenMonitorChanged += OnFullscreenMonitorChanged;
foreach (var display in _fullScreenDetector.GetDisplays()) foreach (var display in _fullScreenDetector.GetDisplays())
Displays.Add(display); Displays.Add(display);
+18
View File
@@ -1,5 +1,23 @@
# Bugs # Bugs
## Desktop "locked" after the app starts while a fullscreen game is running (FIXED 2026-10-10)
Report: once the app is started, the desktop is "locked" — the mouse can no longer interact with it —
which made streaming live gameplay impossible.
Root cause: at launch the main window **activates** (WPF default `ShowActivated=true`). If the creator
is already playing an exclusive-fullscreen game (e.g. THE DIVISION 2 while starting a gameplay stream),
activating LlamaCasty makes Windows drop that game to the taskbar; the app's 1920×1040 window then sits
over the whole work area, so the desktop looks locked and the game can't be reached with the mouse.
Fix (same commit as this entry): the window no longer steals focus when a full-screen game owns the
foreground at launch — `MainWindow.xaml.cs` ctor sets `ShowActivated = !_viewModel.FullscreenGameRunningNow`,
and the new `MainViewModel.FullscreenGameRunningNow` (initially added in `MainViewModel.Background.cs`)
wraps `GetForegroundFullScreenMonitorIndex() != null`. Result: opening the app while in-game leaves the
game focused and alive; opening it normally behaves exactly as before.
Verification: build clean (0 warnings). Real-app reproduction/test launch deferred until the creator is
off-mouse — the real-mouse + fullscreen-detector tests must not run while a game owns the screen.
## iTunes music showing on mic meter ## iTunes music showing on mic meter
Playing music via iTunes causes the mic meter to show audio levels. The WASAPI Playing music via iTunes causes the mic meter to show audio levels. The WASAPI
mic capture (`DataFlow.Capture`) and loopback capture (`DataFlow.Loopback`) are mic capture (`DataFlow.Capture`) and loopback capture (`DataFlow.Loopback`) are
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using Microsoft.Data.Sqlite;
using Xunit;
using ytLive.Services;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// The desktop-"lock" bug (2026-10-10): at launch the main window ACTIVES
/// (WPF default <c>ShowActivated = true</c>), and when the creator is already
/// playing an exclusive-fullscreen game, activating LlamaCasty makes Windows
/// drop that game to the taskbar; the app's 1920×1040 window then sits over the
/// whole work area, so the desktop looks locked and the game can't be reached
/// with the mouse. Fix: the window opens WITHOUT activating when a full-screen
/// game owns the foreground at launch — <c>ShowActivated = !FullscreenGameRunningNow</c>.
/// These tests pin that rule using the <c>FullScreenDetectorOverride</c> seam,
/// so no real Win32 foreground state is touched.
/// </summary>
[Collection("RealApp")]
public sealed class DesktopLockTests
{
private readonly RealAppHost _app;
public DesktopLockTests(RealAppHost app) => _app = app;
[Fact]
public void MainWindow_Does_Not_Steal_Focus_When_A_Fullscreen_Game_Is_Foreground()
{
_app.Run(() =>
{
using var _ = new TempLayout();
MainViewModel.FullScreenDetectorOverride = new StubFullScreenDetector(foregroundMonitor: 0);
try
{
var window = new MainWindow();
try
{
// The regression: a full-screen game already owns the screen, so
// the window must open WITHOUT activating to keep the game live.
Assert.False(window.ShowActivated,
"launching while a full-screen game owns the foreground must not steal focus");
}
finally
{
window.Close();
}
}
finally
{
MainViewModel.FullScreenDetectorOverride = null;
}
});
}
[Fact]
public void MainWindow_Normal_Launch_Still_Activates()
{
_app.Run(() =>
{
using var _ = new TempLayout();
MainViewModel.FullScreenDetectorOverride = new StubFullScreenDetector(foregroundMonitor: null);
try
{
var window = new MainWindow();
try
{
// No game running: behaviour is exactly as before the fix.
Assert.True(window.ShowActivated,
"a normal launch (no full-screen game) must keep the default activate-on-show");
}
finally
{
window.Close();
}
}
finally
{
MainViewModel.FullScreenDetectorOverride = null;
}
});
}
[Fact]
public void FullscreenGameRunningNow_Reflects_The_Foreground_Monitor()
{
_app.Run(() =>
{
MainViewModel.FullScreenDetectorOverride = new StubFullScreenDetector(foregroundMonitor: 0);
try
{
var gameVm = new MainViewModel();
Assert.True(gameVm.FullscreenGameRunningNow);
}
finally
{
MainViewModel.FullScreenDetectorOverride = null;
}
MainViewModel.FullScreenDetectorOverride = new StubFullScreenDetector(foregroundMonitor: null);
try
{
var idleVm = new MainViewModel();
Assert.False(idleVm.FullscreenGameRunningNow);
}
finally
{
MainViewModel.FullScreenDetectorOverride = null;
}
});
}
private sealed class TempLayout : IDisposable
{
private readonly string _path;
public TempLayout()
{
_path = Path.Combine(Path.GetTempPath(), $"ytLive-desktoplock-{Guid.NewGuid():N}.db");
using (var schema = new LayoutStore(_path)) { }
SqliteConnection.ClearAllPools();
MainViewModel.LayoutPathOverride = _path;
}
public void Dispose()
{
MainViewModel.LayoutPathOverride = null;
SqliteConnection.ClearAllPools();
try { File.Delete(_path); } catch { /* best-effort cleanup */ }
}
}
/// <summary>A deterministic stand-in for <see cref="Win32FullScreenDetector"/>:
/// claims a full-screen window owns the given foreground monitor (or none).</summary>
#pragma warning disable CS0067 // fullscreen-changed wiring is not part of this seam
private sealed class StubFullScreenDetector : IFullScreenDetector
{
private readonly int? _foregroundMonitor;
public StubFullScreenDetector(int? foregroundMonitor) => _foregroundMonitor = foregroundMonitor;
public int? GetForegroundFullScreenMonitorIndex() => _foregroundMonitor;
public int PrimaryMonitorIndex() => 0;
public IReadOnlyList<DisplayInfo> GetDisplays() => Array.Empty<DisplayInfo>();
public event Action<int?>? FullscreenMonitorChanged;
public void StartWatching() { }
public void StopWatching() { }
}
}