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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user