diff --git a/Controls/SettingsDrawer.xaml b/Controls/SettingsDrawer.xaml
index bab50fe..45f9ac8 100644
--- a/Controls/SettingsDrawer.xaml
+++ b/Controls/SettingsDrawer.xaml
@@ -123,6 +123,13 @@
Value="{Binding TransitionDurationMs, Mode=TwoWay}" VerticalAlignment="Center"/>
+
+
+
+
+
diff --git a/Controls/index.md b/Controls/index.md
index a559ddc..21cfabe 100644
--- a/Controls/index.md
+++ b/Controls/index.md
@@ -13,7 +13,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions.
| `PreviewPane.xaml(.cs)` | Row 2 Col 1 center: 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, static TextOverlay preview image from the shared cached text raster, text drawer, outgoing-picture transition overlay, right-edge Stream Info/YPP/Socials/TEST tabs and drawers, and live controls (TRAX / game / mic meters + sliders + speakers). The list-selected draggable layer is temporarily raised in preview z-order and custom hit-testing; deselection restores the scene's unchanged order (`SelectedLayerPreviewHitTestingTests`). **TASK 57:** dragging/resizing a webcam in the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` / `ResizeWebcamInVerticalTier` for the six-spot snap or Chat camera's two sizes. |
| `SceneThumbnailStrip.xaml(.cs)` | Top row over the preview column: five fixed-size scene cards with scene name, hotkey, staged `PROGRAM` badge, orange staged/live border |
| `LeftPanel.xaml(.cs)` | Row 2 Col 0 left panel: explicit EditingActive toggle on → Scene Layers+properties; off → YouTube chat, independent of stream status. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept; TextOverlay properties edit multiline content, font type/family, size, color via the shared palette, emphasis, and horizontal/vertical alignment with a static-text notice; sub-options are indented under their headings. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged. **TASK 61:** the four geometry fields (X/Y/W/H) share one fixed width (44px — the largest value, 1920, plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill the row. **TASK 62:** the border-colour field shows the palette's name beside the hex (`SelectedElement.BorderColorName`, empty for a custom hex) and keeps a fixed width so the name stays next to the hex. **TASK 63:** the border colour is picked with the **app's own drop-down** (`YtComboBox`) — the `...` button and its code-built `ContextMenu` are gone; the closed box shows the swatch + the colour's NAME, the rows show swatch + hex + name (the panel's own `DataTemplate`s + `Helpers/ClosedBoxTemplateSelector.cs`), and the hex field stays for a custom colour; `IsDescendantOf` falls back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent` returns `null` (at `PopupRoot`) so clicking a popup row does not trigger `MainWindow`'s deselection guard |
-| `OverlayHost.xaml(.cs)` | Root-grid overlays: App Settings / Report Bug / Feature Request / About; About includes the output-quality conversion copy and an explicit llamacasty.com link, while the licensing/legal panel remains in-app. App Settings groups recording, brand-logo, preview, and transition controls. `BackToAbout_Click` lives here |
+| `OverlayHost.xaml(.cs)` | Root-grid overlays: App Settings / Report Bug / Feature Request / About; About includes the output-quality conversion copy and an explicit llamacasty.com link, while the licensing/legal panel remains in-app. App Settings groups recording, brand-logo, preview, transition, and layout-default controls. `BackToAbout_Click` lives here |
| `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above |
| `LeftPanel.xaml.cs` | Code-behind for the panel above |
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index b5c31f6..06dc237 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -191,6 +191,8 @@ public partial class MainViewModel : ViewModelBase
public ICommand AddMediaCommand { get; }
/// TASK 65: add the replay layer to the staged scene.
public ICommand AddReplayLayerCommand { get; }
+ /// TASK 37: replace the current scene set with its factory snapshot.
+ public ICommand RestoreAllLayoutDefaultsCommand { get; }
public ICommand ChangeMediaFileCommand { get; }
/// Accepts the detector's green/blue-screen offer on a clip (TASK 64).
public ICommand AcceptChromaSuggestionCommand { get; }
@@ -271,6 +273,8 @@ public partial class MainViewModel : ViewModelBase
AddImageCommand = new RelayCommand(_ => AddImage());
AddMediaCommand = new RelayCommand(_ => AddMedia());
AddReplayLayerCommand = new RelayCommand(_ => AddReplayLayer());
+ RestoreAllLayoutDefaultsCommand = new RelayCommand(_ => RestoreAllLayoutDefaults(),
+ _ => IsOffline && !IsRecording);
ChangeMediaFileCommand = new RelayCommand(
element => ChangeMediaFile(element as Source),
element => element is Source { IsMediaSource: true });
@@ -632,6 +636,12 @@ public partial class MainViewModel : ViewModelBase
}
}
+ private void RestoreAllLayoutDefaults()
+ {
+ if (!IsOffline || IsRecording || !_layoutStore.RestoreAllDefaults()) return;
+ LoadLayout();
+ }
+
private void ScheduleSave()
{
if (_isLoading || _saveDebounce == null) return;
diff --git a/ytLive.Tests/LayoutResetIntegrationTests.cs b/ytLive.Tests/LayoutResetIntegrationTests.cs
new file mode 100644
index 0000000..b83adfc
--- /dev/null
+++ b/ytLive.Tests/LayoutResetIntegrationTests.cs
@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.Data.Sqlite;
+using Xunit;
+using ytLive.Models;
+using ytLive.ViewModels;
+
+namespace ytLive.Tests;
+
+[Collection("RealApp")]
+public sealed class LayoutResetIntegrationTests
+{
+ private readonly RealAppHost _app;
+
+ public LayoutResetIntegrationTests(RealAppHost app) => _app = app;
+
+ [Fact]
+ public void SettingsRestoreAll_ReplacesEditedScenesWithFactoryLayout() => _app.Run(() =>
+ {
+ var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-reset-{Guid.NewGuid():N}.db");
+ MainViewModel.LayoutPathOverride = tempDb;
+ MainViewModel? vm = null;
+ try
+ {
+ vm = new MainViewModel();
+ var starting = vm.Scenes.Single(scene => SceneCatalog.Is(scene.Name, SceneCatalog.Starting));
+ starting.Elements.Add(new Source
+ {
+ Name = "Creator-only layer",
+ Type = SourceType.TextOverlay,
+ TextContent = "Not in factory layout",
+ });
+ vm.SaveLayoutNow();
+
+ Assert.True(vm.RestoreAllLayoutDefaultsCommand.CanExecute(null));
+ vm.RestoreAllLayoutDefaultsCommand.Execute(null);
+
+ Assert.DoesNotContain(vm.Scenes.SelectMany(scene => scene.Elements).OfType(),
+ source => source.Name == "Creator-only layer");
+
+ var settingsXaml = SourceAssert.Text("Controls", "SettingsDrawer.xaml");
+ Assert.Contains("Content=\"Restore all scenes\"", settingsXaml);
+ Assert.Contains("Command=\"{Binding RestoreAllLayoutDefaultsCommand}\"", settingsXaml);
+ }
+ finally
+ {
+ vm?.Shutdown();
+ MainViewModel.LayoutPathOverride = null;
+ SqliteConnection.ClearAllPools();
+ try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
+ }
+ });
+}