Files
LlamaCasty/Controls/LeftPanel.xaml.cs
T
gramps 987fddac24 TASK 63: the border-colour picker IS the app's own drop-down
The creator (2026-10-21), on Live -> Layers -> webcam -> Properties:

  "why does the drop down under the web cam properties not show the same style for
   the border color drop down list box?"

  "one of the first things we did was define branding. We established look-and-feel
   standards. And yet you keep ignoring these and I'm wondering why"

WHY IT WAS MISSED - the answer the creator is owed.
The TASK 58 sweep had already walked the whole app for un-themed controls and the
border picker STILL was not themed, because the sweep looked for control TYPES
(ComboBox / CheckBox / TextBox) carrying no theme style - and the border picker was
NONE of those. It was a "..." Button whose Click handler built a ContextMenu of
swatches in code. It was never in the sweep's scope, and PropertiesStyleTests.cs
(a type-based guard) could not see it either. A picker that LOOKS like a drop-down
is invisible to a type-based audit; the guard has to ask "is this the app's widget?".

WHAT CHANGED
- The picker is now the app's YtComboBox - the widget the Display and Font pickers
  wear - bound to the palette (SelectedElement.BorderColorOptions) with its selection
  bound by VALUE (SelectedItem -> BorderColorEntry), because WPF's SelectedValue
  matching is ordinal and the palette mixes #FFFFFF and #e94560.
- The closed box shows the swatch + the colour's NAME; the rows show swatch + hex +
  name (the rows the creator approved in TASK 60). A ComboBox cannot do that with one
  ItemTemplate - WPF copies it into the read-only SelectionBoxItemTemplate - so
  Helpers/ClosedBoxTemplateSelector.cs picks by the ContentPresenter's templated
  parent: the ComboBox for the closed box, the ComboBoxItem for a row. Verified
  against WPF with a throwaway probe, not assumed.
- The swatch is painted by the THEME (Helpers/HexToBrushConverter.cs, registered as
  HexToBrush) - the hand-painted SolidColorBrush(Color.FromRgb(...)) in the old
  click handler was the exact anti-pattern the theme forbids. The swatch also carries
  a 1px #2a3a5e border now, so black and dark swatches are visible on the dark menu
  (the follow-up TASK 60 left open).
- The hex field stays (60px, editable): a colour outside the palette can still be
  typed, and the drop-down then carries that hex as a NAMELESS entry ("no name" is the
  honest answer). The palette's name moved out of TASK 62's TextBlock into the
  drop-down's closed box.
- The hand-built menu, its ContextMenu/MenuItem/SolidColorBrush code and
  WebcamBorderColorPicker_Click are gone.

MODEL: SceneElement.BorderColorName -> BorderColorEntry (the palette entry for the
current hex, with its name; setting it writes the hex back) + BorderColorOptions (the
palette plus the custom entry), both raised when BorderColor changes.
BorderColorPalette gains EntryFor(hex) and OptionsFor(hex); NameOf delegates.

Guards: ytLive.Tests/BorderColorPickerTests.cs (the ONE test for this change - the
model, the panel, and the REAL widget: with the app's theme style and the panel's own
templates the closed box names the colour and the rows show the hex AND the name).
The TASK 60 guard (BorderColorPaletteTests) was updated because the mechanism it
asserted - a code-built menu - no longer exists; it now guards the new wiring (the
drop-down is bound to the palette and neither the panel nor the code-behind carries a
second list of colours).

Full suite: 435/435 passed, 0 warnings.
2026-10-21 23:09:11 -07:00

287 lines
9.2 KiB
C#

using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ytLive.Models;
using ytLive.ViewModels;
namespace ytLive.Controls;
public partial class LeftPanel : UserControl
{
private Models.SceneElement? _snapshotElement;
private string _snapshotBorderColor = string.Empty;
private int _snapshotBorderWidth;
private int _snapshotTimerMinutes = 15;
private string? _snapshotWebUri;
private Point _dragStartPoint;
private int _dragIndex = -1;
private bool _isDragging;
private object? _lastHoveredItem;
private bool _dragReordered;
public LeftPanel()
{
InitializeComponent();
Loaded += (_, _) =>
{
if (DataContext is MainViewModel vm)
vm.ChatMessages.CollectionChanged += OnChatMessagesChanged;
};
Unloaded += (_, _) =>
{
if (DataContext is MainViewModel vm)
vm.ChatMessages.CollectionChanged -= OnChatMessagesChanged;
};
}
// Called by the window's UpdateSelectionOverlay (edit mode only).
public void OnSelectionChanged(MainViewModel vm)
{
if (vm.SelectedElement is { } s && PreviewPane.IsDraggableElement(s))
OpacityValueText.Text = $"{Math.Round(s.Opacity * 100)}%";
TakeSnapshot(vm);
}
// Deselection guard: true when the click is inside the source list or the
// element property scroller (window's Window_PreviewMouseLeftButtonDown).
public bool IsClickInside(DependencyObject original)
=> IsDescendantOf(original, SourceList) || IsDescendantOf(original, ElementsScrollViewer);
private static bool IsDescendantOf(DependencyObject? child, DependencyObject ancestor)
{
while (child != null && !ReferenceEquals(child, ancestor))
{
child = child is Visual or System.Windows.Media.Media3D.Visual3D
? VisualTreeHelper.GetParent(child)
: LogicalTreeHelper.GetParent(child);
}
return child != null;
}
private void SourceNameBox_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is TextBox { IsVisible: true } box)
{
box.Focus();
box.SelectAll();
}
}
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 OnChatMessagesChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
ChatListBox.ScrollIntoView(ChatListBox.Items[^1]);
}
}
private void MirrorButton_Click(object sender, RoutedEventArgs e)
{
if ((DataContext as MainViewModel)?.SelectedElement is WebcamSceneConfig webcam)
webcam.IsMirrored = !webcam.IsMirrored;
}
private void SourceMenu_Remove(object sender, RoutedEventArgs e)
{
if (sender is MenuItem { DataContext: SceneElement element } && DataContext is MainViewModel vm)
vm.RemoveSourceCommand.Execute(element);
}
private void BackgroundMenu_ChangeCapture(object sender, RoutedEventArgs e)
{
_ = (DataContext as MainViewModel)?.ChangeBackgroundCaptureAsync();
}
private void BackgroundMenu_RefreshCapture(object sender, RoutedEventArgs e)
{
(DataContext as MainViewModel)?.RefreshBackgroundAutoCapture();
}
private void BackgroundMenu_RefreshWindowList(object sender, RoutedEventArgs e)
{
(DataContext as MainViewModel)?.RefreshWindowsCommand.Execute(null);
}
private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
=> OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%";
private void SourceNameBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key is Key.Enter or Key.Escape)
{
CommitSourceEdit(sender);
e.Handled = true;
}
}
private void SourceNameBox_LostFocus(object sender, RoutedEventArgs e)
{
CommitSourceEdit(sender);
}
private static void CommitSourceEdit(object sender)
{
if (sender is FrameworkElement { DataContext: SceneElement element })
element.IsEditing = false;
}
// ─── List drag-to-reorder (scenes list + sources list) ───
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))
(DataContext as MainViewModel)!.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;
_dragReordered = false;
}
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;
_dragReordered = true;
}
private void List_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
EndListDrag((ListBox)sender);
}
private void EndListDrag(ListBox listBox)
{
if (_isDragging)
listBox.ReleaseMouseCapture();
var reordered = _dragReordered;
_dragIndex = -1;
_isDragging = false;
_lastHoveredItem = null;
_dragReordered = false;
if (reordered && DataContext is MainViewModel vm)
vm.OnSceneElementsReordered();
}
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;
}
private void TakeSnapshot(MainViewModel vm)
{
if (vm.SelectedElement is Models.Source src)
{
_snapshotElement = src;
_snapshotBorderColor = src.BorderColor;
_snapshotBorderWidth = src.BorderWidth;
_snapshotTimerMinutes = src.TimerMinutes;
_snapshotWebUri = src.WebUri;
}
}
private void WebcamBorderRevert_Click(object sender, RoutedEventArgs e)
{
if (_snapshotElement is Models.Source src)
{
src.BorderColor = _snapshotBorderColor;
src.BorderWidth = _snapshotBorderWidth;
}
}
private void WebcamBorderAccept_Click(object sender, RoutedEventArgs e)
{
if (_snapshotElement is Models.Source src)
{
_snapshotBorderColor = src.BorderColor;
_snapshotBorderWidth = src.BorderWidth;
}
}
private void CountdownRevert_Click(object sender, RoutedEventArgs e)
{
if (_snapshotElement is Models.Source src)
src.TimerMinutes = _snapshotTimerMinutes;
}
private void CountdownAccept_Click(object sender, RoutedEventArgs e)
{
if (_snapshotElement is Models.Source src)
_snapshotTimerMinutes = src.TimerMinutes;
}
private void WebRevert_Click(object sender, RoutedEventArgs e)
{
if (_snapshotElement is Models.Source src)
src.WebUri = string.Empty;
_snapshotWebUri = string.Empty;
}
private void WebAccept_Click(object sender, RoutedEventArgs e)
{
if (_snapshotElement is Models.Source src)
_snapshotWebUri = src.WebUri;
}
}