Multi-scene webcam (schema v3/v4): singleton Webcam + per-scene WebcamSceneConfig, right-click OBS-style border/context menu, persisted round-to-rect restore + legacy-square 16:9 heal, dark MenuItem template, tests (25 passing)
This commit is contained in:
+62
-29
@@ -37,7 +37,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.IsLive))
|
||||
UpdateTaskbarOverlay();
|
||||
else if (e.PropertyName == nameof(MainViewModel.SelectedSource))
|
||||
else if (e.PropertyName == nameof(MainViewModel.SelectedElement))
|
||||
UpdateSelectionOverlay();
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public partial class MainWindow : Window
|
||||
if (IsDescendantOf(original, PreviewGrid)) return;
|
||||
if (IsDescendantOf(original, SourceList)) return;
|
||||
if (IsDescendantOf(original, OpacityChip)) return;
|
||||
_viewModel.SelectedSource = null;
|
||||
_viewModel.SelectedElement = null;
|
||||
}
|
||||
|
||||
private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
|
||||
@@ -115,27 +115,55 @@ public partial class MainWindow : Window
|
||||
|
||||
private void UpdateSelectionOverlay()
|
||||
{
|
||||
var selected = _viewModel.SelectedSource is { } s && IsDraggableSource(s);
|
||||
var selected = _viewModel.SelectedElement is { } s && IsDraggableElement(s);
|
||||
SelectionOverlay.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
|
||||
OpacityChip.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (selected)
|
||||
OpacityValueText.Text = $"{Math.Round(_viewModel.SelectedSource!.Opacity * 100)}%";
|
||||
OpacityValueText.Text = $"{Math.Round(_viewModel.SelectedElement!.Opacity * 100)}%";
|
||||
}
|
||||
|
||||
// Static images and the webcam share the move/resize/selection behavior.
|
||||
private static bool IsDraggableSource(Source source)
|
||||
=> source.Type is SourceType.Image or SourceType.Webcam;
|
||||
private static bool IsDraggableElement(SceneElement element)
|
||||
=> element is Source { Type: SourceType.Image } or WebcamSceneConfig;
|
||||
|
||||
private void MirrorButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel.SelectedSource is { Type: SourceType.Webcam } source)
|
||||
source.IsMirrored = !source.IsMirrored;
|
||||
if (_viewModel.SelectedElement is WebcamSceneConfig webcam)
|
||||
webcam.IsMirrored = !webcam.IsMirrored;
|
||||
}
|
||||
|
||||
private void ShapeButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_viewModel.SelectedSource is { Type: SourceType.Webcam } source)
|
||||
source.ClipShape = source.ClipShape == ClipShape.Traditional ? ClipShape.Round : ClipShape.Traditional;
|
||||
if (_viewModel.SelectedElement is WebcamSceneConfig webcam)
|
||||
webcam.ToggleClipShape();
|
||||
}
|
||||
|
||||
private void WebcamMenu_ChangeWebcam(object sender, RoutedEventArgs e)
|
||||
=> _viewModel.ChangeWebcamCommand.Execute(null);
|
||||
|
||||
private void WebcamMenu_SetBorderAnimation(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: WebcamSceneConfig webcam, Tag: string tag }
|
||||
&& Enum.TryParse<BorderAnimation>(tag, out var animation))
|
||||
webcam.BorderAnimation = animation;
|
||||
}
|
||||
|
||||
private void WebcamMenu_SetBorderColor(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: WebcamSceneConfig webcam, Tag: string color })
|
||||
webcam.BorderColor = color;
|
||||
}
|
||||
|
||||
private void WebcamMenu_HideInScene(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: WebcamSceneConfig webcam })
|
||||
webcam.IsVisible = false;
|
||||
}
|
||||
|
||||
private void WebcamMenu_Remove(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is MenuItem { DataContext: SceneElement element })
|
||||
_viewModel.RemoveSourceCommand.Execute(element);
|
||||
}
|
||||
|
||||
private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
@@ -148,9 +176,9 @@ public partial class MainWindow : Window
|
||||
var grid = (Grid)sender;
|
||||
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
|
||||
var p = toCanvas.Transform(e.GetPosition(grid));
|
||||
var selected = _viewModel.SelectedSource;
|
||||
var selected = _viewModel.SelectedElement;
|
||||
|
||||
if (selected is { } sel && IsDraggableSource(sel) && HitHandle(e.GetPosition(grid), sel))
|
||||
if (selected is { } sel && IsDraggableElement(sel) && HitHandle(e.GetPosition(grid), sel))
|
||||
{
|
||||
_isResizing = true;
|
||||
_resizeAspect = sel.ClipShape == ClipShape.Round ? 1 : sel.Width / Math.Max(1, sel.Height);
|
||||
@@ -159,10 +187,10 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
var hit = HitImage(p);
|
||||
var hit = HitElement(p);
|
||||
if (hit != null)
|
||||
{
|
||||
_viewModel.SelectedSource = hit;
|
||||
_viewModel.SelectedElement = hit;
|
||||
_isDraggingOverlay = true;
|
||||
_grabOffset = new Point(p.X - hit.X, p.Y - hit.Y);
|
||||
grid.CaptureMouse();
|
||||
@@ -170,7 +198,7 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
_viewModel.SelectedSource = null;
|
||||
_viewModel.SelectedElement = null;
|
||||
}
|
||||
|
||||
private void Preview_MouseMove(object sender, MouseEventArgs e)
|
||||
@@ -180,7 +208,7 @@ public partial class MainWindow : Window
|
||||
var grid = (Grid)sender;
|
||||
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
|
||||
var p = toCanvas.Transform(e.GetPosition(grid));
|
||||
var selected = _viewModel.SelectedSource;
|
||||
var selected = _viewModel.SelectedElement;
|
||||
if (selected == null)
|
||||
{
|
||||
EndPreviewDrag(grid);
|
||||
@@ -198,11 +226,14 @@ public partial class MainWindow : Window
|
||||
}
|
||||
else if (_isResizing)
|
||||
{
|
||||
var newW = Math.Clamp(p.X - selected.X, 32, 1920);
|
||||
var isWebcam = selected is WebcamSceneConfig;
|
||||
var maxW = isWebcam ? MainViewModel.WebcamMaxWidth : 1920;
|
||||
var maxH = isWebcam ? MainViewModel.WebcamMaxHeight : 1080;
|
||||
var newW = Math.Clamp(p.X - selected.X, 32, maxW);
|
||||
var newH = newW / _resizeAspect;
|
||||
if (newH > 1080)
|
||||
if (newH > maxH)
|
||||
{
|
||||
newH = 1080;
|
||||
newH = maxH;
|
||||
newW = newH * _resizeAspect;
|
||||
}
|
||||
selected.Width = newW;
|
||||
@@ -221,23 +252,25 @@ public partial class MainWindow : Window
|
||||
_isResizing = false;
|
||||
}
|
||||
|
||||
private bool HitHandle(Point mouseScreen, Source source)
|
||||
private bool HitHandle(Point mouseScreen, SceneElement element)
|
||||
{
|
||||
var corner = CanvasGrid.TransformToVisual(PreviewGrid).Transform(new Point(source.X + source.Width, source.Y + source.Height));
|
||||
var corner = CanvasGrid.TransformToVisual(PreviewGrid).Transform(new Point(element.X + element.Width, element.Y + element.Height));
|
||||
return Math.Abs(mouseScreen.X - corner.X) <= 20 && Math.Abs(mouseScreen.Y - corner.Y) <= 20;
|
||||
}
|
||||
|
||||
private Source? HitImage(Point p)
|
||||
private SceneElement? HitElement(Point p)
|
||||
{
|
||||
var scene = _viewModel.ActiveScene;
|
||||
if (scene == null) return null;
|
||||
for (var i = scene.Sources.Count - 1; i >= 0; i--)
|
||||
for (var i = scene.Elements.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var source = scene.Sources[i];
|
||||
if (!IsDraggableSource(source) || !source.IsEnabled) continue;
|
||||
if (p.X >= source.X && p.X <= source.X + source.Width &&
|
||||
p.Y >= source.Y && p.Y <= source.Y + source.Height)
|
||||
return source;
|
||||
var element = scene.Elements[i];
|
||||
if (!IsDraggableElement(element)) continue;
|
||||
if (element is Source { IsEnabled: false }) continue;
|
||||
if (element is WebcamSceneConfig { IsVisible: false }) continue;
|
||||
if (p.X >= element.X && p.X <= element.X + element.Width &&
|
||||
p.Y >= element.Y && p.Y <= element.Y + element.Height)
|
||||
return element;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -275,7 +308,7 @@ public partial class MainWindow : Window
|
||||
if (item == null)
|
||||
{
|
||||
if (ReferenceEquals(listBox, SourceList))
|
||||
_viewModel.SelectedSource = null;
|
||||
_viewModel.SelectedElement = null;
|
||||
_dragIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user