diff --git a/HANDOFF.md b/HANDOFF.md index f7f6321..df17483 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -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 - main (no feature branch). Pushed through `5685c5d` (TRAX toggle); everything after is local, **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 - **This work unit (COMMIT these):** `Services/BackgroundEffects/MaskSmoother.cs` (new), the smoother 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. ## Next step -- Creator re-runs the app: webcam → Layers properties → Background → Remove / Blur + Strength, and - reports whether the jitter is gone and the blur feels right at the slider's default and full range. +- Creator verifies the desktop-lock fix: while THE DIVISION 2 (or any exclusive-fullscreen game) is the + 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 - **Vertical-format regression (STALE, not fixed):** 1080p60 vertical selection no longer shrinks the diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 680980d..4f66f1a 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -23,6 +23,10 @@ public partial class MainWindow : Window AppLog.Write("MainWindow ctor: after InitializeComponent"); _viewModel = (MainViewModel)DataContext; _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")!; _previewPane = (Controls.PreviewPane)FindName("PreviewPane")!; _leftPanel = (Controls.LeftPanel)FindName("LeftPanel")!; diff --git a/ViewModels/MainViewModel.Background.cs b/ViewModels/MainViewModel.Background.cs index 87cfc99..0d0c6fd 100644 --- a/ViewModels/MainViewModel.Background.cs +++ b/ViewModels/MainViewModel.Background.cs @@ -24,6 +24,12 @@ public partial class MainViewModel /// swaps in a fake enumerator to drive the "window dies -> re-heal" flow. internal static IWindowEnumerator? WindowEnumeratorOverride { get; set; } + /// 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 ShowActivated = !FullscreenGameRunningNow rule (desktop-"lock" + /// fix 2026-10-10) is pinned without Win32 state. + internal static IFullScreenDetector? FullScreenDetectorOverride { get; set; } + private ImageSource? _activeBackgroundImage; private ImageSource? _backgroundImage; private bool _backgroundSizeMismatch; @@ -168,6 +174,15 @@ public partial class MainViewModel return null; } + /// 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. + public bool FullscreenGameRunningNow + => _fullScreenDetector.GetForegroundFullScreenMonitorIndex() != null; + // 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 // one, then acquire one capture per unique background key. ScreenCaptureManager diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 1738803..4941fcd 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -312,7 +312,7 @@ public partial class MainViewModel : ViewModelBase _musicPlayer.PlaybackEnded += OnTraxPlaybackEnded; _ = StartMicCaptureAsync(); - _fullScreenDetector = new Win32FullScreenDetector(); + _fullScreenDetector = FullScreenDetectorOverride ?? new Win32FullScreenDetector(); _fullScreenDetector.FullscreenMonitorChanged += OnFullscreenMonitorChanged; foreach (var display in _fullScreenDetector.GetDisplays()) Displays.Add(display); diff --git a/bugs.md b/bugs.md index 98b4573..5ee0b88 100644 --- a/bugs.md +++ b/bugs.md @@ -1,5 +1,23 @@ # 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 Playing music via iTunes causes the mic meter to show audio levels. The WASAPI mic capture (`DataFlow.Capture`) and loopback capture (`DataFlow.Loopback`) are diff --git a/ytLive.Tests/DesktopLockTests.cs b/ytLive.Tests/DesktopLockTests.cs new file mode 100644 index 0000000..6d9dbde --- /dev/null +++ b/ytLive.Tests/DesktopLockTests.cs @@ -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; + +/// +/// The desktop-"lock" bug (2026-10-10): at launch the main window ACTIVES +/// (WPF default ShowActivated = true), 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 — ShowActivated = !FullscreenGameRunningNow. +/// These tests pin that rule using the FullScreenDetectorOverride seam, +/// so no real Win32 foreground state is touched. +/// +[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 */ } + } + } + + /// A deterministic stand-in for : + /// claims a full-screen window owns the given foreground monitor (or none). + #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 GetDisplays() => Array.Empty(); + public event Action? FullscreenMonitorChanged; + public void StartWatching() { } + public void StopWatching() { } + } +} \ No newline at end of file