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
+25 -27
View File
@@ -26,8 +26,6 @@ public partial class MainWindow : Window
_viewModel = (MainViewModel)DataContext;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
_viewModel.ChatMessages.CollectionChanged += OnChatMessagesChanged;
// When a focused scene is deleted, never land focus on a hidden scene.
SceneList.IsSelectable = item => item is Scene { IsHidden: false };
UpdateTaskbarOverlay();
UpdateSelectionOverlay();
LoadAvatarImage();
@@ -39,6 +37,12 @@ public partial class MainWindow : Window
AppLog.Write("MainWindow loaded");
}
private void Window_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (!_viewModel.IsEditMode)
e.Handled = true;
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsLive))
@@ -57,7 +61,7 @@ public partial class MainWindow : Window
private void MainWindow_Activated(object? sender, EventArgs e)
{
_viewModel.RefreshBackdropAutoCapture();
_viewModel.RefreshBackgroundAutoCapture();
}
private void MainWindow_Deactivated(object? sender, EventArgs e)
@@ -102,15 +106,6 @@ public partial class MainWindow : Window
}
}
private void SceneList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0 && e.AddedItems[0] is Scene { IsHidden: true } hidden)
{
var list = (ListBox)sender;
list.SelectedItem = _viewModel.ActiveScene;
}
}
private void VolumeSlider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is Slider slider)
@@ -234,17 +229,13 @@ public partial class MainWindow : Window
}
}
private void AddSceneButton_Click(object sender, RoutedEventArgs e)
// ─── Preview image selection / move / resize ───
private void Thumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is Button { ContextMenu: { } menu } button)
{
menu.PlacementTarget = button;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
menu.IsOpen = true;
}
if (sender is Border { DataContext: Scene scene })
_viewModel.TransitionToScene(scene);
}
// ─── Preview image selection / move / resize ───
private bool _isDraggingOverlay;
private bool _isResizing;
private Point _grabOffset;
@@ -268,6 +259,11 @@ public partial class MainWindow : Window
private void UpdateSelectionOverlay()
{
if (!_viewModel.IsEditMode)
{
SelectionOverlay.Visibility = Visibility.Collapsed;
return;
}
var selected = _viewModel.SelectedElement is { } s && IsDraggableElement(s);
SelectionOverlay.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
if (selected)
@@ -441,14 +437,14 @@ public partial class MainWindow : Window
_viewModel.RemoveSourceCommand.Execute(element);
}
private void BackdropMenu_ChangeCapture(object sender, RoutedEventArgs e)
private void BackgroundMenu_ChangeCapture(object sender, RoutedEventArgs e)
{
_ = _viewModel.ChangeBackdropCaptureAsync();
_ = _viewModel.ChangeBackgroundCaptureAsync();
}
private void BackdropMenu_RefreshCapture(object sender, RoutedEventArgs e)
private void BackgroundMenu_RefreshCapture(object sender, RoutedEventArgs e)
{
_viewModel.RefreshBackdropAutoCapture();
_viewModel.RefreshBackgroundAutoCapture();
}
private void SourceMenu_Remove(object sender, RoutedEventArgs e)
@@ -471,6 +467,7 @@ public partial class MainWindow : Window
private void Preview_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!_viewModel.IsEditMode) return;
var grid = (Grid)sender;
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
var p = toCanvas.Transform(e.GetPosition(grid));
@@ -501,6 +498,7 @@ public partial class MainWindow : Window
private void Preview_MouseMove(object sender, MouseEventArgs e)
{
if (!_viewModel.IsEditMode) return;
if (!_isDraggingOverlay && !_isResizing) return;
var grid = (Grid)sender;
@@ -527,8 +525,8 @@ public partial class MainWindow : Window
var isWebcam = selected is WebcamSceneConfig;
var isRound = selected.ClipShape == ClipShape.Round;
var proportional = isRound || Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
var maxW = isWebcam ? MainViewModel.MaxWebcamWidthFor(_viewModel.ActiveScene?.Name) : 1920;
var maxH = isWebcam ? MainViewModel.MaxWebcamHeightFor(_viewModel.ActiveScene?.Name) : 1080;
var maxW = isWebcam ? MainViewModel.MaxWebcamWidthFor(_viewModel.StagedScene?.Name) : 1920;
var maxH = isWebcam ? MainViewModel.MaxWebcamHeightFor(_viewModel.StagedScene?.Name) : 1080;
var newW = Math.Clamp(p.X - selected.X, 32, maxW);
var newH = proportional ? newW / _resizeAspect : Math.Clamp(p.Y - selected.Y, 32, maxH);
if (proportional && newH > maxH)
@@ -560,7 +558,7 @@ public partial class MainWindow : Window
private SceneElement? HitElement(Point p)
{
var scene = _viewModel.ActiveScene;
var scene = _viewModel.StagedScene;
if (scene == null) return null;
for (var i = scene.Elements.Count - 1; i >= 0; i--)
{