Control Surface UX: pill toggle, background rename, per-scene properties

- Sliding pill toggle (green ON / red OFF) replaces checkbox
- Geometry mismatch error for custom backgrounds
- Backdrop -> Background: C# rename, SQL column rename with migration
- Source.IsBackground: new -> override (fixes WPF binding)
- Legacy 'Backdrop' display name migrated to 'Background'
- Per-scene UseDefaultBackground + CustomBackgroundPath
- Asset files renamed backdrop -> background
- StagedScene/LiveScene split, thumbnail strip, edit gating
- TransitionService (Cut/Fade/Move)
- Context menu: Capture Desktop, Refresh Desktop
- Resolution badge removed, ON-AIR status light added
This commit is contained in:
2026-08-22 18:10:59 -07:00
parent e06bfa42a8
commit 4b9a8e515f
35 changed files with 1406 additions and 717 deletions
@@ -5,67 +5,67 @@ using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// The backdrop is a permanent, non-deletable bottom layer holding live
/// The background is a permanent, non-deletable bottom layer holding live
/// desktop/game capture, and it exists only in the canonical Live scene (see
/// <see cref="SceneCatalog"/>). Every backdrop-enabled scene
/// (<see cref="Scene.HasBackdrop"/>) is healed to have exactly one, inserted
/// <see cref="SceneCatalog"/>). Every background-enabled scene
/// (<see cref="Scene.HasBackground"/>) is healed to have exactly one, inserted
/// first; it is never reorderable or removable by the UI. Scenes without the
/// flag never get one.
/// </summary>
public class BackdropTests
public class BackgroundTests
{
[Fact]
public void EnsureBackdrop_OnEmptyScene_InsertsFullFrameBackdrop()
public void EnsureBackground_OnEmptyScene_InsertsFullFrameBackground()
{
var scene = new Scene { Name = "S", HasBackdrop = true };
var scene = new Scene { Name = "S", HasBackground = true };
var backdrop = MainViewModel.EnsureBackdrop(scene);
var background = MainViewModel.EnsureBackground(scene);
Assert.NotNull(backdrop);
Assert.NotNull(background);
Assert.Single(scene.Elements);
Assert.Same(backdrop, scene.Elements[0]);
Assert.True(backdrop.IsBackdrop);
Assert.True(backdrop.IsEnabled);
Assert.Equal(SourceType.DisplayCapture, backdrop.Type);
Assert.Equal(0, backdrop.X);
Assert.Equal(0, backdrop.Y);
Assert.Equal(1920, backdrop.Width);
Assert.Equal(1080, backdrop.Height);
Assert.Same(background, scene.Elements[0]);
Assert.True(background.IsBackground);
Assert.True(background.IsEnabled);
Assert.Equal(SourceType.DisplayCapture, background.Type);
Assert.Equal(0, background.X);
Assert.Equal(0, background.Y);
Assert.Equal(1920, background.Width);
Assert.Equal(1080, background.Height);
}
[Fact]
public void EnsureBackdrop_ExistingBackdrop_IsIdempotent()
public void EnsureBackground_ExistingBackground_IsIdempotent()
{
var scene = new Scene { Name = "S", HasBackdrop = true };
var first = MainViewModel.EnsureBackdrop(scene);
var second = MainViewModel.EnsureBackdrop(scene);
var scene = new Scene { Name = "S", HasBackground = true };
var first = MainViewModel.EnsureBackground(scene);
var second = MainViewModel.EnsureBackground(scene);
Assert.Same(first, second);
Assert.Single(scene.Elements);
}
[Fact]
public void EnsureBackdrop_HealsMissingBackdropAtIndexZero()
public void EnsureBackground_HealsMissingBackgroundAtIndexZero()
{
var scene = new Scene { Name = "S", HasBackdrop = true };
var scene = new Scene { Name = "S", HasBackground = true };
var image = new Source { Name = "Logo", Type = SourceType.Image };
scene.Elements.Add(image);
MainViewModel.EnsureBackdrop(scene);
MainViewModel.EnsureBackground(scene);
Assert.Equal(2, scene.Elements.Count);
Assert.True(((Source)scene.Elements[0]).IsBackdrop);
Assert.True(((Source)scene.Elements[0]).IsBackground);
Assert.Same(image, scene.Elements[1]);
}
[Fact]
public void EnsureBackdrop_SceneWithoutHasBackdrop_ReturnsNullAndInsertsNothing()
public void EnsureBackground_SceneWithoutHasBackground_ReturnsNullAndInsertsNothing()
{
var scene = new Scene { Name = "Ending", HasBackdrop = false };
var scene = new Scene { Name = "Ending", HasBackground = false };
var backdrop = MainViewModel.EnsureBackdrop(scene);
var background = MainViewModel.EnsureBackground(scene);
Assert.Null(backdrop);
Assert.Null(background);
Assert.Empty(scene.Elements);
}
+7 -7
View File
@@ -59,10 +59,10 @@ public class FramePumpTests
public void RaiseHealth(StreamHealth health) => HealthUpdated?.Invoke(this, health);
}
private static Scene BackdropScene()
private static Scene BackgroundScene()
{
var scene = new Scene { Name = "Live" };
scene.Elements.Add(new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" });
scene.Elements.Add(new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" });
return scene;
}
@@ -72,7 +72,7 @@ public class FramePumpTests
Func<(VideoFrame? Frame, SocialBarPosition Position)>? socialBar = null)
{
return new FramePump(
sceneProvider: scene ?? (() => BackdropScene()),
sceneProvider: scene ?? (() => BackgroundScene()),
frameResolver: resolve ?? (_ => null),
compositorOptions: () => new CompositorOptions
{
@@ -166,7 +166,7 @@ public class FramePumpTests
var red = SceneCompositorTests.Solid(64, 48, 255, 0, 0);
var encoder = new FakeEncoder();
using var pump = NewPump(encoder,
resolve: e => e is Source { IsBackdrop: true } ? red : null);
resolve: e => e is Source { IsBackground: true } ? red : null);
await pump.StartAsync();
Assert.True(pump.IsRunning);
@@ -177,7 +177,7 @@ public class FramePumpTests
var frame = encoder.Frames[0];
Assert.Equal(64, frame.Width);
Assert.Equal(48, frame.Height);
AssertColor(frame, 0, 0, 255, 0, 0); // the backdrop really was composited in
AssertColor(frame, 0, 0, 255, 0, 0); // the background really was composited in
await pump.StopAsync();
Assert.False(pump.IsRunning);
@@ -236,7 +236,7 @@ public class FramePumpTests
var encoder = new FakeEncoder();
var position = SocialBarPosition.Top;
using var pump = NewPump(encoder,
resolve: e => e is Source { IsBackdrop: true } ? red : null,
resolve: e => e is Source { IsBackground: true } ? red : null,
socialBar: () => (bar, position));
await pump.StartAsync();
@@ -246,7 +246,7 @@ public class FramePumpTests
// Flip to Bottom: the pump re-reads the seam each frame, so a later frame
// lands the bar at the bottom edge (SourceRectHeight - bar height) and the
// top corner clears back to backdrop.
// top corner clears back to background.
position = SocialBarPosition.Bottom;
VideoFrame? flipped = null;
var deadline = DateTime.UtcNow.AddSeconds(5);
+31 -31
View File
@@ -78,21 +78,21 @@ public class LayoutStorePersistenceTests
}
}
// The backdrop (schema v5): permanent bottom layer holding live desktop/game
// capture. IsBackdrop + CaptureKey must survive a save + reload so a
// The background (schema v5): permanent bottom layer holding live desktop/game
// capture. IsBackground + CaptureKey must survive a save + reload so a
// re-designated target isn't lost on restart.
[Fact]
public void Backdrop_IsBackdrop_And_CaptureKey_Survive_Save_And_Reload()
public void Background_IsBackground_And_CaptureKey_Survive_Save_And_Reload()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
{
using var store = new LayoutStore(path);
var backdrop = new Source
var background = new Source
{
Name = "Game Capture",
Type = SourceType.DisplayCapture,
IsBackdrop = true,
IsBackground = true,
IsEnabled = true,
X = 0,
Y = 0,
@@ -102,13 +102,13 @@ public class LayoutStorePersistenceTests
CaptureKey = "monitor:1",
};
var scene = new Scene { Name = "Starting" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
store.Save(new[] { scene }, null, null);
var reloaded = store.Load();
var restored = Assert.IsType<Source>(Assert.Single(reloaded[0].Elements));
Assert.True(restored.IsBackdrop);
Assert.True(restored.IsBackground);
Assert.Equal("monitor:1", restored.CaptureKey);
Assert.Equal(1, restored.MonitorIndex);
Assert.Equal(SourceType.DisplayCapture, restored.Type);
@@ -120,23 +120,23 @@ public class LayoutStorePersistenceTests
}
}
// Per-scene backdrop switch (schema v6): a scene whose backdrop was turned
// Per-scene background switch (schema v6): a scene whose background was turned
// off must stay off across a save + reload.
[Fact]
public void Scene_HasBackdrop_Survives_Save_And_Reload()
public void Scene_HasBackground_Survives_Save_And_Reload()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
{
using var store = new LayoutStore(path);
var on = new Scene { Name = "Live", HasBackdrop = true };
var off = new Scene { Name = "Ending", HasBackdrop = false };
var on = new Scene { Name = "Live", HasBackground = true };
var off = new Scene { Name = "Ending", HasBackground = false };
store.Save(new[] { on, off }, null, null);
var reloaded = store.Load();
Assert.True(reloaded.First(s => s.Name == "Live").HasBackdrop);
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackdrop);
Assert.True(reloaded.First(s => s.Name == "Live").HasBackground);
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackground);
}
finally
{
@@ -145,12 +145,12 @@ public class LayoutStorePersistenceTests
}
}
// v5 → v6 one-time backfill: pre-v6 DBs already have backdrops sitting in the
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackdrop
// column turns those scenes off and drops their backdrop sources; Live keeps
// its backdrop.
// v5 → v6 one-time backfill: pre-v6 DBs already have backgrounds sitting in the
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackground
// column turns those scenes off and drops their background sources; Live keeps
// its background.
[Fact]
public void V5_Database_Backfill_Removes_NonLive_Backdrops()
public void V5_Database_Backfill_Removes_NonLive_Backgrounds()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
@@ -183,19 +183,19 @@ public class LayoutStorePersistenceTests
DeviceId TEXT,
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
IsMirrored INTEGER NOT NULL DEFAULT 0,
IsBackdrop INTEGER NOT NULL DEFAULT 0,
IsBackground INTEGER NOT NULL DEFAULT 0,
CaptureKey TEXT,
SortOrder INTEGER NOT NULL DEFAULT 0
);
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-starting', 'Starting', 0);
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-live', 'Live', 1);
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-chat', 'Chat', 2);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
VALUES ('b-live', 's-live', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
VALUES ('b-live', 's-live', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
PRAGMA user_version = 5;
""";
cmd.ExecuteNonQuery();
@@ -205,16 +205,16 @@ public class LayoutStorePersistenceTests
var scenes = store.Load();
var starting = Assert.Single(scenes, s => s.Name == "Starting");
Assert.False(starting.HasBackdrop);
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackdrop: true });
Assert.False(starting.HasBackground);
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackground: true });
var chat = Assert.Single(scenes, s => s.Name == "Chat");
Assert.False(chat.HasBackdrop);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackdrop: true });
Assert.False(chat.HasBackground);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true });
var live = Assert.Single(scenes, s => s.Name == "Live");
Assert.True(live.HasBackdrop);
Assert.Single(live.Elements, e => e is Source { IsBackdrop: true });
Assert.True(live.HasBackground);
Assert.Single(live.Elements, e => e is Source { IsBackground: true });
}
finally
{
+1 -1
View File
@@ -48,7 +48,7 @@ public sealed class RoundClipInteractionTests
window.Show();
window.UpdateLayout();
var scene = vm.ActiveScene!;
var scene = vm.StagedScene!;
var webcam = new WebcamSceneConfig
{
WebcamId = "test-camera",
+21 -21
View File
@@ -7,7 +7,7 @@ namespace ytLive.Tests;
/// <summary>
/// The app is a five-scene product: Starting/Live/BRB/Chat/Ending. Scenes are
/// worked on by name; you can delete them but never add beyond the set, and the
/// live backdrop belongs to Live alone.
/// live background belongs to Live alone.
/// </summary>
public class SceneCatalogTests
{
@@ -40,38 +40,38 @@ public class SceneCatalogTests
[InlineData("BRB", false)]
[InlineData("Chat", false)]
[InlineData("Ending", false)]
public void SceneCatalog_HasBackdrop_Only_For_Live(string name, bool expected)
public void SceneCatalog_HasBackground_Only_For_Live(string name, bool expected)
{
Assert.Equal(expected, SceneCatalog.HasBackdrop(name));
Assert.Equal(expected, SceneCatalog.HasBackground(name));
}
[Fact]
public void EnforceBackdropPolicy_Removes_Backdrop_From_NonLive_Scenes()
public void EnforceBackgroundPolicy_Removes_Background_From_NonLive_Scenes()
{
var chat = new Scene { Name = "Chat", HasBackdrop = true };
var live = new Scene { Name = "Live", HasBackdrop = true };
var chatBackdrop = MainViewModel.EnsureBackdrop(chat)!;
var liveBackdrop = MainViewModel.EnsureBackdrop(live)!;
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackdrop));
var chat = new Scene { Name = "Chat", HasBackground = true };
var live = new Scene { Name = "Live", HasBackground = true };
var chatBackground = MainViewModel.EnsureBackground(chat)!;
var liveBackground = MainViewModel.EnsureBackground(live)!;
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackground));
MainViewModel.EnforceBackdropPolicy(new[] { chat, live });
MainViewModel.EnforceBackgroundPolicy(new[] { chat, live });
Assert.False(chat.HasBackdrop);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackdrop: true });
Assert.True(live.HasBackdrop);
Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackdrop));
Assert.False(chat.HasBackground);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true });
Assert.True(live.HasBackground);
Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackground));
}
[Fact]
public void EnforceBackdropPolicy_Marks_Live_And_Clears_A_Pre_Policy_Database()
public void EnforceBackgroundPolicy_Marks_Live_And_Clears_A_Pre_Policy_Database()
{
var starting = new Scene { Name = "Starting", HasBackdrop = true };
MainViewModel.EnsureBackdrop(starting);
var live = new Scene { Name = "Live", HasBackdrop = false };
var starting = new Scene { Name = "Starting", HasBackground = true };
MainViewModel.EnsureBackground(starting);
var live = new Scene { Name = "Live", HasBackground = false };
MainViewModel.EnforceBackdropPolicy(new[] { starting, live });
MainViewModel.EnforceBackgroundPolicy(new[] { starting, live });
Assert.False(starting.HasBackdrop);
Assert.True(live.HasBackdrop);
Assert.False(starting.HasBackground);
Assert.True(live.HasBackground);
}
}
+18 -18
View File
@@ -8,7 +8,7 @@ namespace ytLive.Tests;
/// <summary>
/// The output compositor (TASK 4 ship step 1): renders a scene into the encoder's
/// master frame, mirroring the XAML preview minus the editing chrome. The integration
/// test composites a full scene (backdrop + round webcam + images + mirror + border)
/// test composites a full scene (background + round webcam + images + mirror + border)
/// and asserts per-layer probe pixels; the vertical-tier test asserts the 9:16 crop
/// and upscale. Frames are pushed by the test — no capture managers involved.
/// </summary>
@@ -60,11 +60,11 @@ public class SceneCompositorTests
[Fact]
public void Composite_FullScene_MasterPixels()
{
var red = Solid(1920, 1080, 255, 0, 0); // backdrop
var red = Solid(1920, 1080, 255, 0, 0); // background
var green = Solid(1280, 720, 0, 255, 0); // webcam
var split = Split(100, 100, 0, 255, 255, 255, 0, 255); // image: left cyan, right magenta
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var roundWebcam = new WebcamSceneConfig { X = 100, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round };
var imageA = new Source { Type = SourceType.Image, X = 1200, Y = 600, Width = 200, Height = 200 };
var imageB = new Source
@@ -74,7 +74,7 @@ public class SceneCompositorTests
};
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
scene.Elements.Add(roundWebcam);
scene.Elements.Add(imageA);
scene.Elements.Add(imageB);
@@ -82,7 +82,7 @@ public class SceneCompositorTests
VideoFrame? FrameFor(SceneElement e) => e switch
{
WebcamSceneConfig => green,
Source { IsBackdrop: true } => red,
Source { IsBackground: true } => red,
Source { Type: SourceType.Image } => split,
_ => null,
};
@@ -98,11 +98,11 @@ public class SceneCompositorTests
Assert.Equal(1920, output.Width);
Assert.Equal(1080, output.Height);
// backdrop at the frame corner
// background at the frame corner
AssertColor(output, 0, 0, 255, 0, 0);
// round webcam: center is the (square-cropped) webcam feed
AssertColor(output, 250, 250, 0, 255, 0);
// round webcam: element-square corner is OUTSIDE the circle -> backdrop shows
// round webcam: element-square corner is OUTSIDE the circle -> background shows
AssertColor(output, 101, 101, 255, 0, 0);
// imageA (unmirrored): left half cyan, right half magenta
AssertColor(output, 1220, 700, 0, 255, 255);
@@ -120,16 +120,16 @@ public class SceneCompositorTests
var red = Solid(1920, 1080, 255, 0, 0);
var green = Solid(1280, 720, 0, 255, 0);
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var roundWebcam = new WebcamSceneConfig { X = 656, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
scene.Elements.Add(roundWebcam);
VideoFrame? FrameFor(SceneElement e) => e switch
{
WebcamSceneConfig => green,
Source { IsBackdrop: true } => red,
Source { IsBackground: true } => red,
_ => null,
};
@@ -143,11 +143,11 @@ public class SceneCompositorTests
Assert.Equal(1080, output.Width);
Assert.Equal(1920, output.Height);
// top-left of the crop is pure backdrop (webcam starts at crop y=100, inset from the corner)
// top-left of the crop is pure background (webcam starts at crop y=100, inset from the corner)
AssertColor(output, 0, 0, 255, 0, 0);
// the webcam center (crop 150,250) scales to output ~(267,444) and stays green
AssertColor(output, 267, 444, 0, 255, 0);
// far from the webcam, still backdrop
// far from the webcam, still background
AssertColor(output, 978, 1688, 255, 0, 0);
}
@@ -155,9 +155,9 @@ public class SceneCompositorTests
public void Composite_WithFlash_BlendsOverContent()
{
var red = Solid(1920, 1080, 255, 0, 0);
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
// flash: a semi-transparent white pixel at the center of a master-sized frame
var flash = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]);
@@ -185,9 +185,9 @@ public class SceneCompositorTests
public void Composite_WithSocialBar_OverlaysAboveFlash_AtTopOrBottom()
{
var red = Solid(1920, 1080, 255, 0, 0);
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
var options = new CompositorOptions
{
@@ -210,11 +210,11 @@ public class SceneCompositorTests
var top = compositor.Render(scene, _ => red, flash, options, bar, 0);
AssertColor(top, 0, 0, 0, 255, 0); // bar above flash at the top-left
AssertColor(top, 960, 540, 255, 127, 127); // 50% white over red
AssertColor(top, 100, 100, 255, 0, 0); // empty overlay area: backdrop
AssertColor(top, 100, 100, 255, 0, 0); // empty overlay area: background
var bottom = compositor.Render(scene, _ => red, flash, options, bar, 1040);
AssertColor(bottom, 0, 1040, 0, 255, 0); // bar drawn at the bottom edge
AssertColor(bottom, 0, 1039, 255, 0, 0); // backdrop just above the bar
AssertColor(bottom, 0, 1039, 255, 0, 0); // background just above the bar
AssertColor(bottom, 960, 540, 255, 0, 0); // bar region moved away from center
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ public sealed class SourceNamingTests
var vm = (MainViewModel)window.DataContext;
try
{
var scene = vm.ActiveScene!;
var scene = vm.StagedScene!;
vm.AddSourceCommand.Execute(SourceType.TextOverlay);
vm.AddSourceCommand.Execute(SourceType.TextOverlay);