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)
This commit is contained in:
2026-08-06 08:33:05 -07:00
parent 7dc8490d96
commit a74162e34e
22 changed files with 876 additions and 230 deletions
+72
View File
@@ -0,0 +1,72 @@
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
namespace ytLive.Helpers;
/// <summary>
/// A ListBox that keeps selection and keyboard focus coherent when the
/// selected item is deleted. Focus lands on the item now at the deleted
/// position (the previous item when the last one was removed) or leaves
/// the box entirely when it becomes empty. Drag-to-reorder is untouched.
/// </summary>
public class FocusPreservingListBox : ListBox
{
/// <summary>
/// Predicate deciding which items the delete-focus fallback may land on
/// (e.g. skip hidden scenes). Null means every item is eligible.
/// </summary>
public Func<object, bool>? IsSelectable { get; set; }
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (e.Action != NotifyCollectionChangedAction.Remove) return;
var hadFocus = IsKeyboardFocusWithin || IsKeyboardFocused;
if (!hadFocus) return;
if (Mouse.Captured == this) return;
if (Items.Count == 0)
{
SelectedItem = null;
MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
return;
}
var targetIndex = FindSelectableIndex(Math.Clamp(e.OldStartingIndex, 0, Items.Count - 1));
if (targetIndex < 0)
{
SelectedItem = null;
return;
}
SelectedIndex = targetIndex;
var item = Items[targetIndex];
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, () =>
{
ScrollIntoView(item);
if (ItemContainerGenerator.ContainerFromItem(item) is ListBoxItem container)
container.Focus();
});
}
private int FindSelectableIndex(int start)
{
for (var i = start; i >= 0; i--)
if (IsItemSelectable(i)) return i;
for (var i = start + 1; i < Items.Count; i++)
if (IsItemSelectable(i)) return i;
return -1;
}
private bool IsItemSelectable(int index)
{
if (index < 0 || index >= Items.Count) return false;
var item = Items[index];
return item != null && (IsSelectable?.Invoke(item) ?? true);
}
}