Files
ytLlive/MainWindow.xaml.cs
T

530 lines
18 KiB
C#

using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.ViewModels;
namespace ytLive;
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
public MainWindow()
{
AppLog.Write("MainWindow ctor: before InitializeComponent");
InitializeComponent();
AppLog.Write("MainWindow ctor: after InitializeComponent");
_viewModel = (MainViewModel)DataContext;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
// When a focused scene is deleted, never land focus on a hidden scene.
SceneList.IsSelectable = item => item is Scene { IsHidden: false };
UpdateTaskbarOverlay();
UpdateSelectionOverlay();
AppLog.Write("MainWindow ctor: end");
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
AppLog.Write("MainWindow loaded");
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsLive))
UpdateTaskbarOverlay();
else if (e.PropertyName == nameof(MainViewModel.SelectedElement))
UpdateSelectionOverlay();
}
private void MainWindow_Closing(object? sender, CancelEventArgs e)
{
_viewModel.Shutdown();
}
private void MainWindow_Activated(object? sender, EventArgs e)
{
_viewModel.RefreshBackdropAutoCapture();
}
private void MainWindow_Deactivated(object? sender, EventArgs e)
{
_viewModel.NoteBackgroundWindow();
}
private void UpdateTaskbarOverlay()
{
TaskbarInfo.Overlay = _viewModel.IsLive
? (ImageSource)FindResource("LiveOverlay")
: null;
}
private void SceneNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is TextBox { IsVisible: true } box)
{
box.Focus();
box.SelectAll();
}
}
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 MicLabel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement { DataContext: MainViewModel vm })
vm.OpenMicPickerCommand.Execute(null);
}
private void VolumeSlider_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement { DataContext: MainViewModel vm })
vm.SetVolumeAdjusting(true);
}
private void VolumeSlider_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement { DataContext: MainViewModel vm })
vm.SetVolumeAdjusting(false);
}
private void VolumeSlider_LostMouseCapture(object sender, MouseEventArgs e)
{
if (sender is FrameworkElement { DataContext: MainViewModel vm })
vm.SetVolumeAdjusting(false);
}
private void MicSpeaker_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement { DataContext: MainViewModel vm })
{
vm.ToggleMicMuteCommand.Execute(null);
}
}
private void GearButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button { ContextMenu: { } menu } button)
{
menu.PlacementTarget = button;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
menu.IsOpen = true;
}
}
private void AboutButton_Click(object sender, RoutedEventArgs e)
{
// LGPL/BSD/MIT notices ship next to the exe; open in the OS text viewer.
var path = Path.Combine(AppContext.BaseDirectory, "THIRD-PARTY-NOTICES.txt");
if (!File.Exists(path)) return;
try
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
}
catch (Exception ex)
{
AppLog.Write(ex, "About: failed to open THIRD-PARTY-NOTICES.txt");
}
}
private void AddSourceButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button { ContextMenu: { } menu } button)
{
menu.PlacementTarget = button;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
menu.IsOpen = true;
}
}
private void AddSceneButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button { ContextMenu: { } menu } button)
{
menu.PlacementTarget = button;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
menu.IsOpen = true;
}
}
// ─── Preview image selection / move / resize ───
private bool _isDraggingOverlay;
private bool _isResizing;
private Point _grabOffset;
private double _resizeAspect;
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is not DependencyObject original) return;
if (IsDescendantOf(original, PreviewGrid)) return;
if (IsDescendantOf(original, SourceList)) return;
if (IsDescendantOf(original, OpacityChip)) return;
_viewModel.SelectedElement = null;
}
private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
{
while (child != null && !ReferenceEquals(child, ancestor))
child = VisualTreeHelper.GetParent(child);
return child != null;
}
private void UpdateSelectionOverlay()
{
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.SelectedElement!.Opacity * 100)}%";
}
// Static images and the webcam share the move/resize/selection behavior.
private static bool IsDraggableElement(SceneElement element)
=> element is Source { Type: SourceType.Image } or WebcamSceneConfig;
private void MirrorButton_Click(object sender, RoutedEventArgs e)
{
if (_viewModel.SelectedElement is WebcamSceneConfig webcam)
webcam.IsMirrored = !webcam.IsMirrored;
}
private void ShapeButton_Click(object sender, RoutedEventArgs e)
{
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 BackdropMenu_ChangeCapture(object sender, RoutedEventArgs e)
{
_ = _viewModel.ChangeBackdropCaptureAsync();
}
private void BackdropMenu_RefreshCapture(object sender, RoutedEventArgs e)
{
_viewModel.RefreshBackdropAutoCapture();
}
private void SourceMenu_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)
=> OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%";
// ─── Social bar drag: vertical only, direction-snaps to the top/bottom edge ───
private const double SocialBarBottomTop = 1040;
private bool _isDraggingSocialBar;
private double _socialBarDragStartY;
private SocialBarPosition? _socialBarDragDirection;
private void SocialBar_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var bar = (FrameworkElement)sender;
_isDraggingSocialBar = true;
_socialBarDragDirection = null;
var toCanvas = CanvasGrid.TransformToVisual(bar).Inverse;
var p = toCanvas.Transform(e.GetPosition(bar));
_socialBarDragStartY = p.Y;
bar.CaptureMouse();
e.Handled = true;
}
private void SocialBar_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_isDraggingSocialBar) return;
var bar = (FrameworkElement)sender;
var toCanvas = CanvasGrid.TransformToVisual(bar).Inverse;
var p = toCanvas.Transform(e.GetPosition(bar));
// Derive the drag direction once it commits (past the deadzone) and keep it
// for the rest of the drag — the bar snaps to the edge it's being dragged
// toward and is never left mid-screen.
var direction = _socialBarDragDirection
?? SocialBarSnap.Decide(p.Y - _socialBarDragStartY);
if (direction != null)
{
_socialBarDragDirection = direction;
Canvas.SetTop(bar, direction == SocialBarPosition.Top ? 0 : SocialBarBottomTop);
}
e.Handled = true;
}
private void SocialBar_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!_isDraggingSocialBar) return;
var bar = (FrameworkElement)sender;
bar.ReleaseMouseCapture();
_isDraggingSocialBar = false;
var direction = _socialBarDragDirection
?? (Canvas.GetTop(bar) <= SocialBarBottomTop / 2.0
? SocialBarPosition.Top : SocialBarPosition.Bottom);
// ClearValue removes the local value Canvas.SetTop applied during the drag —
// a local value permanently overrides the {Binding SocialBarTop}, which is
// why the release snap never used to show. Clearing re-engages the binding.
bar.ClearValue(Canvas.TopProperty);
_viewModel.SetSocialBarPosition(direction);
e.Handled = true;
}
private void Preview_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (OpacityChip.IsMouseOver) return;
var grid = (Grid)sender;
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
var p = toCanvas.Transform(e.GetPosition(grid));
var selected = _viewModel.SelectedElement;
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);
grid.CaptureMouse();
e.Handled = true;
return;
}
var hit = HitElement(p);
if (hit != null)
{
_viewModel.SelectedElement = hit;
_isDraggingOverlay = true;
_grabOffset = new Point(p.X - hit.X, p.Y - hit.Y);
grid.CaptureMouse();
e.Handled = true;
return;
}
_viewModel.SelectedElement = null;
}
private void Preview_MouseMove(object sender, MouseEventArgs e)
{
if (!_isDraggingOverlay && !_isResizing) return;
var grid = (Grid)sender;
var toCanvas = CanvasGrid.TransformToVisual(grid).Inverse;
var p = toCanvas.Transform(e.GetPosition(grid));
var selected = _viewModel.SelectedElement;
if (selected == null)
{
EndPreviewDrag(grid);
return;
}
if (_isDraggingOverlay)
{
var minX = -(selected.Width - 20);
var maxX = 1920 - 20;
var minY = -(selected.Height - 20);
var maxY = 1080 - 20;
selected.X = Math.Clamp(p.X - _grabOffset.X, Math.Min(minX, maxX), Math.Max(minX, maxX));
selected.Y = Math.Clamp(p.Y - _grabOffset.Y, Math.Min(minY, maxY), Math.Max(minY, maxY));
}
else if (_isResizing)
{
var isWebcam = selected is WebcamSceneConfig;
var maxW = isWebcam ? MainViewModel.MaxWebcamWidthFor(_viewModel.ActiveScene?.Name) : 1920;
var maxH = isWebcam ? MainViewModel.MaxWebcamHeightFor(_viewModel.ActiveScene?.Name) : 1080;
var newW = Math.Clamp(p.X - selected.X, 32, maxW);
var newH = newW / _resizeAspect;
if (newH > maxH)
{
newH = maxH;
newW = newH * _resizeAspect;
}
selected.Width = newW;
selected.Height = newH;
}
}
private void Preview_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
=> EndPreviewDrag((Grid)sender);
private void EndPreviewDrag(Grid grid)
{
if (_isDraggingOverlay || _isResizing)
grid.ReleaseMouseCapture();
_isDraggingOverlay = false;
_isResizing = false;
}
private bool HitHandle(Point mouseScreen, SceneElement element)
{
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 SceneElement? HitElement(Point p)
{
var scene = _viewModel.ActiveScene;
if (scene == null) return null;
for (var i = scene.Elements.Count - 1; i >= 0; i--)
{
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;
}
private void SceneNameBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key is Key.Enter or Key.Escape)
{
CommitSceneEdit(sender);
e.Handled = true;
}
}
private void SceneNameBox_LostFocus(object sender, RoutedEventArgs e)
{
CommitSceneEdit(sender);
}
private static void CommitSceneEdit(object sender)
{
if (sender is FrameworkElement { DataContext: Scene scene })
scene.IsEditing = false;
}
// ─── List drag-to-reorder (scenes list + sources list) ───
private Point _dragStartPoint;
private int _dragIndex = -1;
private bool _isDragging;
private object? _lastHoveredItem;
private void List_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var listBox = (ListBox)sender;
var item = FindItemContainerAt(listBox, e.GetPosition(listBox));
if (item == null)
{
if (ReferenceEquals(listBox, SourceList))
_viewModel.SelectedElement = null;
_dragIndex = -1;
return;
}
if (listBox.ItemsSource is not IList items || items.Count == 0)
{
_dragIndex = -1;
return;
}
_dragIndex = listBox.Items.IndexOf(item.DataContext);
_dragStartPoint = e.GetPosition(listBox);
_isDragging = false;
_lastHoveredItem = null;
}
private void List_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (_dragIndex < 0) return;
var listBox = (ListBox)sender;
if (e.LeftButton != MouseButtonState.Pressed || listBox.ItemsSource is not IList items)
{
EndListDrag(listBox);
return;
}
var position = e.GetPosition(listBox);
if (!_isDragging)
{
if (Math.Abs(position.X - _dragStartPoint.X) < SystemParameters.MinimumHorizontalDragDistance &&
Math.Abs(position.Y - _dragStartPoint.Y) < SystemParameters.MinimumVerticalDragDistance)
return;
_isDragging = true;
listBox.CaptureMouse();
}
var item = FindItemContainerAt(listBox, position);
if (item == null) return;
var targetItem = item.DataContext;
if (ReferenceEquals(targetItem, items[_dragIndex]) || ReferenceEquals(targetItem, _lastHoveredItem))
return;
var targetIndex = listBox.Items.IndexOf(targetItem);
if (targetIndex < 0) return;
var dragged = items[_dragIndex];
items.RemoveAt(_dragIndex);
items.Insert(targetIndex, dragged);
_dragIndex = targetIndex;
_lastHoveredItem = targetItem;
}
private void List_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
EndListDrag((ListBox)sender);
}
private void EndListDrag(ListBox listBox)
{
if (_isDragging)
listBox.ReleaseMouseCapture();
_dragIndex = -1;
_isDragging = false;
_lastHoveredItem = null;
}
private static ListBoxItem? FindItemContainerAt(ListBox listBox, Point position)
{
var hit = listBox.InputHitTest(position) as DependencyObject;
while (hit != null && hit != listBox)
{
if (hit is ListBoxItem item) return item;
hit = VisualTreeHelper.GetParent(hit);
}
return null;
}
}