using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace Kniip.Editor;
///
/// Werkbalk die zijn items op één regel zet, met s die de overige ruimte opvullen;
/// bij twee spacers komt de groep ertussen in het midden. Past het niet, dan begint elke groep op een nieuwe
/// regel (begin, midden of eind uitgelijnd) en lopen te lange groepen door. Als de werkbalk niet vergrendeld is, kun je de items verslepen om de volgorde te veranderen.
///
public sealed class ToolbarPanel : Panel
{
public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(
nameof(Orientation), typeof(Orientation), typeof(ToolbarPanel),
new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure, OnOrientationChanged));
public static readonly DependencyProperty IsLockedProperty = DependencyProperty.Register(
nameof(IsLocked), typeof(bool), typeof(ToolbarPanel), new PropertyMetadata(true, OnIsLockedChanged));
public static readonly DependencyProperty ItemIdProperty = DependencyProperty.RegisterAttached(
"ItemId", typeof(string), typeof(ToolbarPanel), new PropertyMetadata(null));
public static readonly DependencyProperty TitleProperty = DependencyProperty.RegisterAttached(
"Title", typeof(string), typeof(ToolbarPanel), new PropertyMetadata(null));
private const double DragThreshold = 4;
private readonly List _lines = new();
private readonly HashSet _skipped = new();
private UIElement? _pressedItem;
private Point _pressPoint;
private bool _dragging;
private int _insertIndex = -1;
private InsertionAdorner? _adorner;
public Orientation Orientation
{
get => (Orientation)GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public bool IsLocked
{
get => (bool)GetValue(IsLockedProperty);
set => SetValue(IsLockedProperty, value);
}
public static string? GetItemId(DependencyObject element) => (string?)element.GetValue(ItemIdProperty);
public static void SetItemId(DependencyObject element, string? value) => element.SetValue(ItemIdProperty, value);
public static string? GetTitle(DependencyObject element) => (string?)element.GetValue(TitleProperty);
public static void SetTitle(DependencyObject element, string? value) => element.SetValue(TitleProperty, value);
/// Na het verslepen van een item.
public event EventHandler? OrderChanged;
/// Lengte (breedte of hoogte) die de zichtbare items samen op één regel nodig hebben.
public double SingleLineLength { get; private set; }
public IEnumerable Items => InternalChildren.Cast();
private bool Horizontal => Orientation == Orientation.Horizontal;
private double Main(Size size) => Horizontal ? size.Width : size.Height;
private double Cross(Size size) => Horizontal ? size.Height : size.Width;
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
foreach (UIElement child in ((ToolbarPanel)d).InternalChildren)
{
child.InvalidateMeasure();
}
}
private static void OnIsLockedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (ToolbarPanel)d;
panel.Cursor = panel.IsLocked ? null : Cursors.SizeAll;
panel.Background = panel.IsLocked ? null : Brushes.Transparent;
panel.EndDrag(commit: false);
}
// ---------------------------------------------------------------- Opmaak
protected override Size MeasureOverride(Size availableSize)
{
var unlimited = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (UIElement child in InternalChildren)
{
child.Measure(unlimited);
}
_lines.Clear();
_skipped.Clear();
List items = WithoutLooseSeparators(InternalChildren.Cast()
.Where(c => c.Visibility != Visibility.Collapsed));
SingleLineLength = items.Sum(c => Main(c.DesiredSize));
double limit = Main(availableSize);
if (SingleLineLength <= limit)
{
var line = new ToolbarLine(align: 0);
items.ForEach(c => line.Add(c, Main(c.DesiredSize), Cross(c.DesiredSize)));
_lines.Add(line);
}
else
{
WrapInto(items, limit);
}
double main = _lines.Count == 0 ? 0 : _lines.Max(l => l.Main);
double cross = _lines.Sum(l => l.Cross);
return Horizontal ? new Size(main, cross) : new Size(cross, main);
}
private void WrapInto(List items, double limit)
{
ToolbarLine? line = null;
bool hasMiddle = items.Count(i => i is ToolbarSpacer) >= 2;
int segment = 0;
foreach (UIElement child in items)
{
if (child is ToolbarSpacer)
{
_skipped.Add(child);
segment++;
line = null;
continue;
}
double length = Main(child.DesiredSize);
if (line != null && line.Items.Count > 0 && line.Main + length > limit)
{
line.TrimTrailingSeparator(_skipped);
line = null;
}
if (line == null)
{
if (child is ToolbarSeparator)
{
_skipped.Add(child);
continue;
}
line = new ToolbarLine(segment == 0 ? 0 : hasMiddle && segment == 1 ? 0.5 : 1);
_lines.Add(line);
}
line.Add(child, length, Cross(child.DesiredSize));
}
line?.TrimTrailingSeparator(_skipped);
}
/// Scheidingslijnen aan het begin of eind, naast de spacer of direct na elkaar weglaten.
private List WithoutLooseSeparators(IEnumerable source)
{
var result = new List();
foreach (UIElement child in source)
{
bool loose = child is ToolbarSeparator
&& (result.Count == 0 || result[^1] is ToolbarSeparator or ToolbarSpacer);
if (child is ToolbarSpacer && result.Count > 0 && result[^1] is ToolbarSeparator)
{
_skipped.Add(result[^1]);
result.RemoveAt(result.Count - 1);
}
if (loose)
{
_skipped.Add(child);
}
else
{
result.Add(child);
}
}
if (result.Count > 0 && result[^1] is ToolbarSeparator last)
{
_skipped.Add(last);
result.RemoveAt(result.Count - 1);
}
return result;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement child in _skipped)
{
child.Arrange(new Rect());
}
double total = Main(finalSize);
double crossPos = 0;
foreach (ToolbarLine line in _lines)
{
double extra = Math.Max(0, total - line.Main);
Queue gaps = SpacerGaps(line, total, extra);
double offset = gaps.Count == 0 ? extra * line.Align : 0;
foreach (UIElement child in line.Items)
{
double length = Main(child.DesiredSize) + (child is ToolbarSpacer ? gaps.Dequeue() : 0);
child.Arrange(Horizontal
? new Rect(offset, crossPos, length, line.Cross)
: new Rect(crossPos, offset, line.Cross, length));
offset += length;
}
crossPos += line.Cross;
}
return finalSize;
}
///
/// Ruimte per spacer. Met twee spacers staat de middelste groep precies in het midden van de werkbalk,
/// tenzij hij dan over de groep links of rechts heen zou vallen.
///
private Queue SpacerGaps(ToolbarLine line, double total, double extra)
{
List spacers = line.Items.Select((item, index) => (item, index))
.Where(x => x.item is ToolbarSpacer).Select(x => x.index).ToList();
if (spacers.Count != 2)
{
return new Queue(spacers.Select(_ => extra / spacers.Count));
}
double Length(int from, int to) => line.Items.Skip(from).Take(to - from).Sum(i => Main(i.DesiredSize));
double left = Length(0, spacers[0]);
double middle = Length(spacers[0] + 1, spacers[1]);
double right = Length(spacers[1] + 1, line.Items.Count);
double start = Math.Clamp((total - middle) / 2, left, Math.Max(left, total - right - middle));
return new Queue(new[] { start - left, Math.Max(0, total - right - middle - start) });
}
// ---------------------------------------------------------------- Verslepen
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonDown(e);
if (IsLocked)
{
return;
}
// Ontgrendeld: knoppen reageren niet op klikken, je pakt ze alleen op.
e.Handled = true;
Point p = e.GetPosition(this);
UIElement? item = ItemAt(p);
if (item == null || item is ToolbarSpacer)
{
return;
}
_pressedItem = item;
_pressPoint = p;
CaptureMouse();
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (_pressedItem == null)
{
return;
}
Point p = e.GetPosition(this);
if (!_dragging)
{
if (Math.Abs(p.X - _pressPoint.X) < DragThreshold && Math.Abs(p.Y - _pressPoint.Y) < DragThreshold)
{
return;
}
_dragging = true;
_pressedItem.Opacity = 0.4;
if (AdornerLayer.GetAdornerLayer(this) is { } layer)
{
_adorner = new InsertionAdorner(this);
layer.Add(_adorner);
}
}
_insertIndex = FindInsertIndex(p, out Rect marker);
_adorner?.Show(_insertIndex < 0 ? Rect.Empty : marker);
e.Handled = true;
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonUp(e);
if (_pressedItem != null)
{
EndDrag(commit: _dragging);
e.Handled = true;
}
}
protected override void OnLostMouseCapture(MouseEventArgs e)
{
base.OnLostMouseCapture(e);
EndDrag(commit: false);
}
private void EndDrag(bool commit)
{
UIElement? item = _pressedItem;
int target = _insertIndex;
_pressedItem = null;
_insertIndex = -1;
_dragging = false;
if (_adorner != null)
{
AdornerLayer.GetAdornerLayer(this)?.Remove(_adorner);
_adorner = null;
}
if (item == null)
{
return;
}
item.Opacity = 1;
if (IsMouseCaptured)
{
ReleaseMouseCapture();
}
if (!commit || target < 0)
{
return;
}
int from = InternalChildren.IndexOf(item);
if (target > from)
{
target--;
}
if (target == from)
{
return;
}
InternalChildren.RemoveAt(from);
InternalChildren.Insert(target, item);
OrderChanged?.Invoke(this, EventArgs.Empty);
}
// Op positie in plaats van via het klikdoel: uitgeschakelde knoppen krijgen zelf geen muisinvoer.
private UIElement? ItemAt(Point p) =>
InternalChildren.Cast().FirstOrDefault(child =>
child.Visibility != Visibility.Collapsed && !_skipped.Contains(child)
&& LayoutInformation.GetLayoutSlot((FrameworkElement)child).Contains(p));
/// Plek in de lijst waar het gesleepte item terechtkomt, plus de positie van de invoegstreep.
private int FindInsertIndex(Point p, out Rect marker)
{
marker = Rect.Empty;
UIElement? best = null;
Rect bestRect = Rect.Empty;
double bestDistance = double.MaxValue;
foreach (UIElement child in InternalChildren)
{
if (child.Visibility == Visibility.Collapsed || _skipped.Contains(child))
{
continue;
}
Rect rect = LayoutInformation.GetLayoutSlot((FrameworkElement)child);
if (rect.Width <= 0 || rect.Height <= 0)
{
continue;
}
double dx = Math.Max(0, Math.Max(rect.Left - p.X, p.X - rect.Right));
double dy = Math.Max(0, Math.Max(rect.Top - p.Y, p.Y - rect.Bottom));
double distance = dx * dx + dy * dy;
if (distance < bestDistance)
{
bestDistance = distance;
best = child;
bestRect = rect;
}
}
if (best == null)
{
return -1;
}
bool after = Horizontal ? p.X > bestRect.Left + bestRect.Width / 2 : p.Y > bestRect.Top + bestRect.Height / 2;
marker = Horizontal
? new Rect((after ? bestRect.Right : bestRect.Left) - 1, bestRect.Top + 3, 2, Math.Max(0, bestRect.Height - 6))
: new Rect(bestRect.Left + 3, (after ? bestRect.Bottom : bestRect.Top) - 1, Math.Max(0, bestRect.Width - 6), 2);
return InternalChildren.IndexOf(best) + (after ? 1 : 0);
}
private sealed class ToolbarLine
{
public ToolbarLine(double align) => Align = align;
/// 0 = begin, 0,5 = midden, 1 = eind (als de regel geen spacer bevat).
public double Align { get; }
public List Items { get; } = new();
public double Main { get; private set; }
public double Cross { get; private set; }
public void Add(UIElement child, double main, double cross)
{
Items.Add(child);
Main += main;
Cross = Math.Max(Cross, cross);
}
public void TrimTrailingSeparator(HashSet skipped)
{
if (Items.Count > 0 && Items[^1] is ToolbarSeparator separator)
{
Items.RemoveAt(Items.Count - 1);
Main -= separator.DesiredSize.Width + separator.DesiredSize.Height;
skipped.Add(separator);
}
}
}
private sealed class InsertionAdorner : Adorner
{
private Rect _marker = Rect.Empty;
public InsertionAdorner(UIElement adornedElement) : base(adornedElement)
{
IsHitTestVisible = false;
}
public void Show(Rect marker)
{
_marker = marker;
InvalidateVisual();
}
protected override void OnRender(DrawingContext dc)
{
if (!_marker.IsEmpty)
{
dc.DrawRectangle(TryFindResource("AccentBrush") as Brush ?? Brushes.DodgerBlue, null, _marker);
}
}
}
}
/// Flexibele ruimte in een ; scheidt de groep aan het begin van die aan het eind.
public sealed class ToolbarSpacer : FrameworkElement
{
}
/// Dunne scheidingslijn die meedraait met de richting van de werkbalk.
public sealed class ToolbarSeparator : FrameworkElement
{
private const double Thickness = 13;
private static readonly DependencyProperty LineBrushProperty = DependencyProperty.Register(
nameof(LineBrush), typeof(Brush), typeof(ToolbarSeparator),
new FrameworkPropertyMetadata(Brushes.LightGray, FrameworkPropertyMetadataOptions.AffectsRender));
public ToolbarSeparator() => SetResourceReference(LineBrushProperty, "DividerBrush");
private Brush LineBrush => (Brush)GetValue(LineBrushProperty);
private bool Vertical => VisualTreeHelper.GetParent(this) is ToolbarPanel { Orientation: Orientation.Vertical };
protected override Size MeasureOverride(Size availableSize) =>
Vertical ? new Size(0, Thickness) : new Size(Thickness, 0);
protected override void OnRender(DrawingContext dc)
{
if (RenderSize.Width <= 0 || RenderSize.Height <= 0)
{
return;
}
Brush brush = LineBrush;
Rect line = Vertical
? new Rect(6, Math.Floor(RenderSize.Height / 2), Math.Max(0, RenderSize.Width - 12), 1)
: new Rect(Math.Floor(RenderSize.Width / 2), 6, 1, Math.Max(0, RenderSize.Height - 12));
dc.DrawRectangle(brush, null, line);
}
}