Files
ytLlive/MainWindow.xaml.cs
T
gramps a74162e34e Resolution dropdown, list-focus preservation, unified dark theme, startup logging, and memory map
- Bottom-bar resolution dropdown (1080p60/1080p30/720p60/480p30) with tooltip
  on finding upload bandwidth; disabled while live; no in-app speed test
- FocusPreservingListBox keeps selection/focus coherent when a selected
  scene/source is deleted (skip hidden scenes; leave list when empty)
- Themes/Controls.xaml: single dark-theme dictionary merged once in App.xaml;
  custom ComboBox template fixes SelectionBoxItem rendering; GoLiveWindow and
  ReuseImageDialog consolidated onto shared styles
- AppLog file logger + AppDomain/Dispatcher exception hooks; checkpointed
  MainWindow/VM/dialog constructors (caught MenuItemRole.Separator XAML crash)
- Memory map: schema.md conventions, per-directory index.md, updated ai.md/
  TASKS.md/README.md (OAuth creds real; token persistence still pending)
2026-08-06 08:33:05 -07:00

339 lines
11 KiB
C#

using System.Collections;
using System.ComponentModel;
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.SelectedSource))
UpdateSelectionOverlay();
}
private void MainWindow_Closing(object? sender, CancelEventArgs e)
{
_viewModel.Shutdown();
}
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 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 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;
}
}
// ─── 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.SelectedSource = 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.SelectedSource is { Type: SourceType.Image };
SelectionOverlay.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
OpacityChip.Visibility = selected ? Visibility.Visible : Visibility.Collapsed;
if (selected)
OpacityValueText.Text = $"{Math.Round(_viewModel.SelectedSource!.Opacity * 100)}%";
}
private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
=> OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%";
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.SelectedSource;
if (selected is { Type: SourceType.Image } && HitHandle(e.GetPosition(grid), selected))
{
_isResizing = true;
_resizeAspect = selected.Width / Math.Max(1, selected.Height);
grid.CaptureMouse();
e.Handled = true;
return;
}
var hit = HitImage(p);
if (hit != null)
{
_viewModel.SelectedSource = hit;
_isDraggingOverlay = true;
_grabOffset = new Point(p.X - hit.X, p.Y - hit.Y);
grid.CaptureMouse();
e.Handled = true;
return;
}
_viewModel.SelectedSource = 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.SelectedSource;
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 newW = Math.Clamp(p.X - selected.X, 32, 1920);
var newH = newW / _resizeAspect;
if (newH > 1080)
{
newH = 1080;
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, Source source)
{
var corner = CanvasGrid.TransformToVisual(PreviewGrid).Transform(new Point(source.X + source.Width, source.Y + source.Height));
return Math.Abs(mouseScreen.X - corner.X) <= 20 && Math.Abs(mouseScreen.Y - corner.Y) <= 20;
}
private Source? HitImage(Point p)
{
var scene = _viewModel.ActiveScene;
if (scene == null) return null;
for (var i = scene.Sources.Count - 1; i >= 0; i--)
{
var source = scene.Sources[i];
if (source.Type != SourceType.Image || !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;
}
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.SelectedSource = 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;
}
}