diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 20d28d78..3b096ee0 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -1,18 +1,20 @@ # CoreVideo Pro — ranked backlog -## Now — owner-reported scene naming blocker, November 6 +## Now — owner-reported Tiles scene shortcut, November 6 -The owner explicitly ranked scene naming/renaming as high priority after testing the new beta. This supersedes the completed timed windows below. No subagents are authorized. +The owner requested a quick Tiles scene option immediately after the scene naming fix. This is the next scene-management fix and supersedes the completed timed windows below. No subagents are authorized. | Order | Issue | Scope and acceptance | |---|---|---| -| 1 | [#1196](https://github.com/iamfatness/CoreVideoPro/issues/1196) | Name new scenes and rename existing scenes through the real UI; persist names while preserving scene identity, contents, unsaved Preview edits and Program output. Specification and execution plan are in the issue. | +| 1 | [#1199](https://github.com/iamfatness/CoreVideoPro/issues/1199) | Add a named CoreVideo Tiles starter to the New scene chooser, using real Tiles settings, Preview-only creation and persistence. Specification and execution plan are in the issue. | -The prior window is complete: beta `beta-2026-11-06-74495ad` is public; #867 and #941 are closed with merged fixes. #1178 remains open after merged diagnostic coverage, and #1180 remains held. Unranked intake: [#1195](https://github.com/iamfatness/CoreVideoPro/issues/1195), Metal GPU admission timeout handling; it does not displace #1196. +Scene naming #1196 is complete through merged #1198; its 1,999 managed tests, 101 compiled UI checks and 17 hosted checks passed. It is on main and is not yet in the published beta. + +The prior window is complete: beta `beta-2026-11-06-74495ad` is public; #867 and #941 are closed with merged fixes. #1178 remains open after merged diagnostic coverage, and #1180 remains held. Unranked intake: [#1195](https://github.com/iamfatness/CoreVideoPro/issues/1195), Metal GPU admission timeout handling; it does not displace #1199. | Unranked intake | Finding | |---|---| -| [#1197](https://github.com/iamfatness/CoreVideoPro/issues/1197) | A new Solo scene becomes unsaved during queued editor initialization before any rename. Reproduced separately; does not displace #1196. | +| [#1197](https://github.com/iamfatness/CoreVideoPro/issues/1197) | A new Solo scene becomes unsaved during queued editor initialization before any rename. Reproduced separately; does not displace #1199. | ## Active replenishing eight-hour window — November 6, 07:36–15:36 Eastern diff --git a/docs/reference/scenes.md b/docs/reference/scenes.md index 407107a0..8935619c 100644 --- a/docs/reference/scenes.md +++ b/docs/reference/scenes.md @@ -77,7 +77,12 @@ set that would empty the rail is applied only as far as one visible scene remain ### Getting a scene back -- **New scene** (Sources tab, `+`) has a starter for every catalog layout: Solo (`host-focus`), +- **New scene** (Sources tab, `+`) includes **CoreVideo Tiles · Auto-reflow wall** + (`dynamic-gallery`, #1199). It uses the shared naming dialog and creates one + editable Tiles wall on Preview, with core-owned eligible membership and no + duplicate participant routes. The existing `NewDynamicGalleryCommand` uses the + same starter/creation path. The wall remains a normal named scene in the list. +- The same menu has a starter for every catalog layout: Solo (`host-focus`), Two people (`two-up`), Screen + presenter (`speaker-slides`), **Panel grid** (`smart-grid`, Inputs 1 to 6) and **Closing** (`outro`), `StarterSceneFactory`. Each binds stable Show Inputs, never the active speaker. diff --git a/native-shell/CoreVideoPro.WinUI.Tests/StarterSceneFactoryTests.cs b/native-shell/CoreVideoPro.WinUI.Tests/StarterSceneFactoryTests.cs index 7716dafe..82ad42e3 100644 --- a/native-shell/CoreVideoPro.WinUI.Tests/StarterSceneFactoryTests.cs +++ b/native-shell/CoreVideoPro.WinUI.Tests/StarterSceneFactoryTests.cs @@ -7,6 +7,28 @@ namespace CoreVideoPro.WinUI.Tests; public sealed class StarterSceneFactoryTests { + [Fact] + public void TilesStarterPersistsCoreOwnedMembershipAndIsDistinctFromPanelGrid() + { + var (tiles, routes) = StarterSceneFactory.Create("custom-wall", "tiles", ["CoreVideo Tiles"]); + Assert.Equal("dynamic-gallery", tiles.Layout); + Assert.NotEqual("CoreVideo Tiles", tiles.Name); + Assert.NotNull(tiles.DynamicGallery); + Assert.True(tiles.DynamicGallery.AutoFill); + Assert.Equal("eligible", tiles.DynamicGallery.MembershipMode); + Assert.Empty(routes); + var restored = ScenePersistenceService.SceneFromPersisted(ScenePersistenceService.ToPersisted(tiles, routes)); + Assert.Equal(tiles.Id, restored.Id); + Assert.Equal(tiles.Name, restored.Name); + Assert.Equal(16, restored.DynamicGallery!.MaxTiles); + var (panel, fixedRoutes) = StarterSceneFactory.Create("custom-panel", "panel-grid", []); + Assert.Null(panel.DynamicGallery); + Assert.Equal(6, fixedRoutes.Count); + var (secondWall, _) = StarterSceneFactory.Create("custom-wall-2", "tiles", []); + tiles.DynamicGallery.MaxTiles = 4; + Assert.Equal(16, secondWall.DynamicGallery!.MaxTiles); + } + [Theory] [InlineData("solo", 1)] [InlineData("two-person", 2)] diff --git a/native-shell/CoreVideoPro.WinUI/Services/SceneNamingIntegrationProbe.cs b/native-shell/CoreVideoPro.WinUI/Services/SceneNamingIntegrationProbe.cs index ac8483d1..a1a9eafe 100644 --- a/native-shell/CoreVideoPro.WinUI/Services/SceneNamingIntegrationProbe.cs +++ b/native-shell/CoreVideoPro.WinUI/Services/SceneNamingIntegrationProbe.cs @@ -32,16 +32,18 @@ internal static async Task CheckAsync(Window window, List checks, string window.AppWindow.Resize(new Windows.Graphics.SizeInt32(1366, 768)); await Eventually(() => page.IsLoaded && page.Workspace is not null); Require(page.FindName("RenameSceneButton") is Button, "Scene Builder has no Rename scene action (#1196)"); + Require(((MenuFlyout)((Button)page.FindName("NewSceneButton")).Flyout).Items.OfType() + .Any(item => item.Tag as string == "tiles"), "New scene chooser has no CoreVideo Tiles starter (#1199)"); var programId = studio.ActiveSceneId; var previewId = studio.PreviewSceneId; var count = studio.Scenes.Count; - async Task OpenCreate() + async Task OpenCreate(string template = "solo") { var button = (Button)page.FindName("NewSceneButton"); var flyout = (MenuFlyout)button.Flyout; flyout.ShowAt(button); - var choice = flyout.Items.OfType().Single(item => item.Tag as string == "solo"); + var choice = flyout.Items.OfType().Single(item => item.Tag as string == template); await Eventually(() => choice.IsLoaded); ((IInvokeProvider)new MenuFlyoutItemAutomationPeer(choice).GetPattern(PatternInterface.Invoke)).Invoke(); return await OpenDialog(page.XamlRoot); @@ -126,8 +128,37 @@ await Eventually(() => (builderRow = Descendants(page).OfType().FirstOrDef "Builder context rename targeted the selected scene instead of its row or changed Preview/Program"); checks.Add("compiled Builder context menu renames its own row while preserving the selected Preview and on-air scene"); - studio.NewDynamicGalleryCommand.Execute(null); + var beforeTilesCount = studio.Scenes.Count; + dialog = await OpenCreate("tiles"); + Require(Descendants(dialog).OfType().Single(text => text.Name == "SceneNameField").Text == "CoreVideo Tiles", + "Tiles shortcut suggested a fixed-layout scene name"); + Invoke(DialogButton(dialog, "CloseButton")); + await Eventually(() => FindDialog(page.XamlRoot) is null); + Require(studio.Scenes.Count == beforeTilesCount && studio.PreviewSceneId == namedId && studio.ActiveSceneId == programId, + "Cancelling the Tiles shortcut changed the show"); + dialog = await OpenCreate("tiles"); + Descendants(dialog).OfType().Single(text => text.Name == "SceneNameField").Text = " Audience wall "; + Invoke(DialogButton(dialog, "PrimaryButton")); + await Eventually(() => FindDialog(page.XamlRoot) is null && page.Workspace!.Items.Any(item => item.IsTilesWall)); var wallId = studio.PreviewSceneId; + Require(studio.Scenes.Count == beforeTilesCount + 1 && studio.ActiveSceneId == programId && + studio.PreviewScene is { Name: "Audience wall", Layout: "dynamic-gallery", DynamicGallery.AutoFill: true } && + page.Workspace!.Items.Count == 1 && page.Workspace.Items[0].IsTilesWall, + "Tiles shortcut failed to create one editable auto-reflow wall on Preview without changing Program"); + Require(store.Value.CustomScenes.Single(scene => scene.Id == wallId) is + { Name: "Audience wall", Layout: "dynamic-gallery", DynamicGallery.MembershipMode: "eligible", Routes.Count: 0 }, + "Tiles shortcut did not persist the named wall or added duplicate participant routes"); + studio.SelectSceneCommand.Execute(namedId); + studio.SelectSceneCommand.Execute(wallId); + await Eventually(() => page.Workspace!.Items.Any(item => item.IsTilesWall)); + Require(studio.PreviewScene.Name == "Audience wall" && studio.ActiveSceneId == programId, + "Created Tiles scene could not be selected again without changing Program"); + var inspector = (Pivot)page.FindName("InspectorTabs"); + await Eventually(() => inspector.Items.Cast().Select(item => item.Header?.ToString()) + .SequenceEqual(new[] { "Layout", "Style", "Members", "Transform" })); + if (reviewDirectory is not null) + await ReviewImageWriter.SaveAsync(page, Path.Combine(reviewDirectory, "scene-tiles-shortcut.png")); + checks.Add("compiled Tiles starter supports naming and Cancel, creates one core-owned wall on Preview, exposes wall controls, persists and can be selected again"); studio.ActiveSceneId = wallId; ((ITransportHost)studio).RefreshPreviewRoutingState(); var liveGutter = studio.ProgramScene.DynamicGallery!.GutterPercent; diff --git a/native-shell/CoreVideoPro.WinUI/Services/StarterSceneFactory.cs b/native-shell/CoreVideoPro.WinUI/Services/StarterSceneFactory.cs index 1a80b5d2..e0e8e9ed 100644 --- a/native-shell/CoreVideoPro.WinUI/Services/StarterSceneFactory.cs +++ b/native-shell/CoreVideoPro.WinUI/Services/StarterSceneFactory.cs @@ -2,7 +2,7 @@ namespace CoreVideoPro.WinUI.Services; -/// New scenes bind show inputs, never whichever guest happens to be speaking. +/// Fixed layouts bind show inputs; Tiles uses the core-owned auto-reflow wall. public static class StarterSceneFactory { public static (Scene Scene, List Routes) Create( @@ -14,6 +14,7 @@ public static (Scene Scene, List Routes) Create( { "two-person" => ("Two people", "two-up"), "screen-presenter" => ("Screen + presenter", "speaker-slides"), + "tiles" => ("CoreVideo Tiles", "dynamic-gallery"), "panel-grid" => ("Panel grid", "smart-grid"), "closing" => ("Closing", "outro"), _ => ("Solo", "host-focus") @@ -23,9 +24,13 @@ public static (Scene Scene, List Routes) Create( Id = id, Name = ScenePersistenceService.MakeUniqueSceneName(name, existingNames), Layout = layout, - Automation = "Custom canvas" + Automation = template == "tiles" ? "Auto-reflow Zoom gallery" : "Custom canvas", + DynamicGallery = template == "tiles" ? new DynamicGallerySettings() : null }; - var routes = SceneRoutingService.GetStarterRoutes(scene).Select(route => route.Clone()).ToList(); + // Tiles membership is composed by the core, not duplicate shell routes. + var routes = scene.DynamicGallery is null + ? SceneRoutingService.GetStarterRoutes(scene).Select(route => route.Clone()).ToList() + : new List(); SceneCanvasLayoutService.EnsureCanvasRects(routes, layout); return (scene, routes); } diff --git a/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.SceneNaming.cs b/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.SceneNaming.cs index 8a1b903b..59e0f0f1 100644 --- a/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.SceneNaming.cs +++ b/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.SceneNaming.cs @@ -12,6 +12,13 @@ public string SuggestedSceneName(string? template) => [RelayCommand] private void NewScene(string? template) => TryCreateNamedScene(template, SuggestedSceneName(template), out _); + [RelayCommand] + private void NewDynamicGallery() + { + if (TryCreateNamedScene("tiles", SuggestedSceneName("tiles"), out _)) + ActiveTab = StudioTab.Sources; + } + public bool TryCreateNamedScene(string? template, string? name, out string? error) { error = SceneNamingPolicy.Validate(_scenes, null, name); @@ -21,6 +28,7 @@ public bool TryCreateNamedScene(string? template, string? name, out string? erro var scene = SceneNamingPolicy.WithName(starter, name!.Trim()); _scenes.Add(scene); _sceneRoutes[id] = routes; + if (scene.DynamicGallery is not null) ReconcileDynamicGalleryRoutes(scene, routes); RefreshSceneItems(); PreviewSceneId = id; CommandStatus = $"{scene.Name} created and queued on preview"; diff --git a/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs b/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs index deeaace7..fe0f8bcb 100644 --- a/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs +++ b/native-shell/CoreVideoPro.WinUI/ViewModels/StudioViewModel.cs @@ -4333,31 +4333,6 @@ private static SourceRoute BuildSoloRoute(string sceneId, ParticipantSurfaceTile }; } - [RelayCommand] - private void NewDynamicGallery() - { - var newId = NewCustomSceneId(); - var galleryNumber = _scenes.Count(scene => scene.DynamicGallery is not null) + 1; - var scene = new Scene - { - Id = newId, - Name = galleryNumber == 1 ? "CoreVideo Tiles" : $"CoreVideo Tiles {galleryNumber}", - Layout = "dynamic-gallery", - Automation = "Auto-reflow Zoom gallery", - DynamicGallery = new DynamicGallerySettings() - }; - - _scenes.Add(scene); - _sceneRoutes[newId] = []; - ReconcileDynamicGalleryRoutes(scene, _sceneRoutes[newId]); - RefreshSceneItems(); - PreviewSceneId = newId; - ActiveTab = StudioTab.Sources; - CommandStatus = $"{scene.Name} created with {RoomVideoParticipants.Count} live Zoom sources"; - SchedulePreviewRoutingRefresh(); - SaveProductionOutputPreferences(); - } - [RelayCommand] private void DuplicateScene(string? sceneId) { diff --git a/native-shell/CoreVideoPro.WinUI/Views/SourcesPage.xaml b/native-shell/CoreVideoPro.WinUI/Views/SourcesPage.xaml index 1dae21c9..5b35b6f1 100644 --- a/native-shell/CoreVideoPro.WinUI/Views/SourcesPage.xaml +++ b/native-shell/CoreVideoPro.WinUI/Views/SourcesPage.xaml @@ -77,6 +77,7 @@ +