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); } }