// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team // // Permission is hereby granted, free of charge, to any person obtaining a copy of this // software and associated documentation files (the "Software"), to deal in the Software // without restriction, including without limitation the rights to use, copy, modify, merge, // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons // to whom the Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Windows; using ICSharpCode.WpfDesign.Adorners; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; using System.Diagnostics; using System.Windows.Input; namespace ICSharpCode.WpfDesign.Designer.Extensions { public class SnaplinePlacementBehavior : RasterPlacementBehavior { public static bool GetDisableSnaplines(DependencyObject obj) { return (bool)obj.GetValue(DisableSnaplinesProperty); } public static void SetDisableSnaplines(DependencyObject obj, bool value) { obj.SetValue(DisableSnaplinesProperty, value); } public static readonly DependencyProperty DisableSnaplinesProperty = DependencyProperty.RegisterAttached("DisableSnaplines", typeof(bool), typeof(SnaplinePlacementBehavior), new PropertyMetadata(false)); AdornerPanel adornerPanel; Canvas surface; List horizontalMap; List verticalMap; double? baseline; public const double Accuracy = 5; public const double Margin = 8; public override void BeginPlacement(PlacementOperation operation) { base.BeginPlacement(operation); CreateSurface(operation); } public override void EndPlacement(PlacementOperation operation) { base.EndPlacement(operation); DeleteSurface(); } public override void EnterContainer(PlacementOperation operation) { base.EnterContainer(operation); CreateSurface(operation); } public override void LeaveContainer(PlacementOperation operation) { base.LeaveContainer(operation); DeleteSurface(); } public override void BeforeSetPosition(PlacementOperation operation) { base.BeforeSetPosition(operation); if (surface == null) return; DesignPanel designPanel = ExtendedItem.Services.DesignPanel as DesignPanel; if (designPanel == null || !designPanel.UseSnaplinePlacement) return; surface.Children.Clear(); if (Keyboard.IsKeyDown(Key.LeftCtrl)) return; Rect bounds = Rect.Empty; foreach (var item in operation.PlacedItems) { bounds.Union(item.Bounds); } var horizontalInput = new List(); var verticalInput = new List(); var info = operation.PlacedItems[0]; if (operation.Type == PlacementType.Resize) { AddLines(bounds, 0, false, horizontalInput, verticalInput, info.ResizeThumbAlignment); } else { AddLines(bounds, 0, false, horizontalInput, verticalInput, null); if (baseline.HasValue) { var textOffset = bounds.Top + baseline.Value; horizontalInput.Add(new Snapline() { Group = 1, Offset = textOffset, Start = bounds.Left, End = bounds.Right }); } } // debug //foreach (var t in horizontalMap.Concat(horizontalInput)) { // surface.Children.Add(new Line() { X1 = t.Start, X2 = t.End, Y1 = t.Offset, Y2 = t.Offset, Stroke = Brushes.Black }); //} //foreach (var t in verticalMap.Concat(verticalInput)) { // surface.Children.Add(new Line() { X1 = t.Offset, X2 = t.Offset, Y1 = t.Start , Y2 = t.End, Stroke = Brushes.Black }); //} //return; List drawLines; double delta; if (Snap(horizontalInput, horizontalMap, Accuracy, out drawLines, out delta)) { if (operation.Type == PlacementType.Resize) { if (info.ResizeThumbAlignment != null && info.ResizeThumbAlignment.Value.Vertical == VerticalAlignment.Top) { bounds.Y += delta; bounds.Height = Math.Max(0, bounds.Height - delta); } else { bounds.Height = Math.Max(0, bounds.Height + delta); } info.Bounds = bounds; } else { foreach (var item in operation.PlacedItems) { var r = item.Bounds; r.Y += delta; item.Bounds = r; } } foreach (var d in drawLines) { DrawLine(d.Start, d.Offset, d.End, d.Offset); } } if (Snap(verticalInput, verticalMap, Accuracy, out drawLines, out delta)) { if (operation.Type == PlacementType.Resize) { if (info.ResizeThumbAlignment != null && info.ResizeThumbAlignment.Value.Horizontal == HorizontalAlignment.Left) { bounds.X += delta; bounds.Width = Math.Max(0, bounds.Width - delta); } else { bounds.Width = Math.Max(0, bounds.Width + delta); } info.Bounds = bounds; } else { foreach (var item in operation.PlacedItems) { var r = item.Bounds; r.X += delta; item.Bounds = r; } } foreach (var d in drawLines) { DrawLine(d.Offset, d.Start, d.Offset, d.End); } } } void CreateSurface(PlacementOperation operation) { if (ExtendedItem.Services.GetService() != null) { surface = new Canvas(); adornerPanel = new AdornerPanel(); adornerPanel.SetAdornedElement(ExtendedItem.View, ExtendedItem); AdornerPanel.SetPlacement(surface, AdornerPlacement.FillContent); adornerPanel.Children.Add(surface); ExtendedItem.Services.DesignPanel.Adorners.Add(adornerPanel); BuildMaps(operation); if (operation.Type != PlacementType.Resize && operation.PlacedItems.Count == 1) { baseline = GetBaseline(operation.PlacedItems[0].Item.View); } } } private IEnumerable AllDesignItems(DesignItem designItem = null) { if (designItem == null && this.ExtendedItem.Services.DesignPanel is DesignPanel) { designItem = this.ExtendedItem.Services.DesignPanel.Context.RootItem; if (designItem != null) { yield return designItem; if (designItem.ContentProperty.Value != null) { yield return designItem.ContentProperty.Value; designItem = ExtendedItem; //set designitem back to current control after yield } } } if (designItem != null && designItem.ContentProperty != null && designItem.ContentProperty.IsCollection) foreach (var collectionElement in designItem.ContentProperty.CollectionElements) { if (collectionElement != null) yield return collectionElement; foreach (var el in AllDesignItems(collectionElement)) { if (el != null) yield return el; } } } void BuildMaps(PlacementOperation operation) { horizontalMap = new List(); verticalMap = new List(); var containerRect = new Rect(0, 0, ModelTools.GetWidth(ExtendedItem.View), ModelTools.GetHeight(ExtendedItem.View)); AddLines(containerRect, -Margin, false); AddLines(containerRect, 0, false); if (!CanPlace(operation.PlacedItems.Select(x => x.Item), operation.Type, PlacementAlignment.Center)) return; foreach (var item in AllDesignItems() /* ExtendedItem.ContentProperty.CollectionElements */ .Except(operation.PlacedItems.Select(f => f.Item)) .Where(x=> x.View != null && !GetDisableSnaplines(x.View))) { if (item != null) { var bounds = GetPosition(operation, item); AddLines(bounds, 0, false); AddLines(bounds, Margin, true); AddBaseline(item, bounds, horizontalMap); } } } void AddLines(Rect r, double inflate, bool requireOverlap) { AddLines(r, inflate, requireOverlap, horizontalMap, verticalMap, null); } void AddLines(Rect r, double inflate, bool requireOverlap, List h, List v, PlacementAlignment? filter) { if (r != Rect.Empty) { Rect r2 = r; r2.Inflate(inflate, inflate); if (filter == null || filter.Value.Vertical == VerticalAlignment.Top) h.Add(new Snapline() { RequireOverlap = requireOverlap, Offset = r2.Top - 1, Start = r.Left, End = r.Right }); if (filter == null || filter.Value.Vertical == VerticalAlignment.Bottom) h.Add(new Snapline() { RequireOverlap = requireOverlap, Offset = r2.Bottom - 1, Start = r.Left, End = r.Right }); if (filter == null || filter.Value.Horizontal == HorizontalAlignment.Left) v.Add(new Snapline() { RequireOverlap = requireOverlap, Offset = r2.Left - 1, Start = r.Top, End = r.Bottom }); if (filter == null || filter.Value.Horizontal == HorizontalAlignment.Right) v.Add(new Snapline() { RequireOverlap = requireOverlap, Offset = r2.Right - 1, Start = r.Top, End = r.Bottom }); if (filter == null) { h.Add(new Snapline() { RequireOverlap = requireOverlap, Offset = r2.Top + Math.Abs((r2.Top - r2.Bottom) / 2), Start = r.Left, End = r.Right }); v.Add(new Snapline() { RequireOverlap = requireOverlap, Offset = r2.Left + Math.Abs((r2.Left - r2.Right) / 2), Start = r.Top, End = r.Bottom }); } } } void AddBaseline(DesignItem item, Rect bounds, List list) { var baseline = GetBaseline(item.View); if (baseline.HasValue) { var textOffset = item.View.TranslatePoint(new Point(0, baseline.Value), ExtendedItem.View).Y; list.Add(new Snapline() { Group = 1, Offset = textOffset, Start = bounds.Left, End = bounds.Right }); } } void DeleteSurface() { if (surface != null) { ExtendedItem.Services.DesignPanel.Adorners.Remove(adornerPanel); adornerPanel = null; surface = null; horizontalMap = null; verticalMap = null; } } void DrawLine(double x1, double y1, double x2, double y2) { if (double.IsInfinity(x1) || double.IsNaN(x1) || double.IsInfinity(y1) || double.IsNaN(y1) || double.IsInfinity(x2) || double.IsNaN(x2) || double.IsInfinity(y2) || double.IsNaN(y2)) return; var line1 = new Line() { X1 = x1, Y1 = y1, X2 = x2, Y2 = y2, StrokeThickness = 1, Stroke = Brushes.White }; surface.Children.Add(line1); var line2 = new Line() { X1 = x1, Y1 = y1, X2 = x2, Y2 = y2, StrokeThickness = 1, Stroke = Brushes.Orange, StrokeDashArray = new DoubleCollection(new double[] { 5, 2 }), StrokeDashOffset = x1 + y1 // fix dashes }; surface.Children.Add(line2); } //TODO: GlyphRun must be used static double? GetBaseline(UIElement element) { var textBox = element as TextBox; if (textBox != null) { var r = textBox.GetRectFromCharacterIndex(0).Bottom; return textBox.TranslatePoint(new Point(0, r), element).Y; } var textBlock = element as TextBlock; if (textBlock != null) return textBlock.TranslatePoint(new Point(0, textBlock.ActualHeight), element).Y; return null; } static bool Snap(List input, List map, double accuracy, out List drawLines, out double delta) { delta = double.MaxValue; drawLines = null; foreach (var inputLine in input) { foreach (var mapLine in map) { if (Math.Abs(mapLine.Offset - inputLine.Offset) <= accuracy) { if (!inputLine.RequireOverlap && !mapLine.RequireOverlap || Math.Max(inputLine.Start, mapLine.Start) < Math.Min(inputLine.End, mapLine.End)) { if (mapLine.Group == inputLine.Group) delta = mapLine.Offset - inputLine.Offset; } } } } if (delta == double.MaxValue) return false; var offsetDict = new Dictionary(); foreach (var inputLine in input) { inputLine.Offset += delta; foreach (var mapLine in map) { if (inputLine.Offset == mapLine.Offset) { var offset = mapLine.Offset; Snapline drawLine; if (!offsetDict.TryGetValue(offset, out drawLine)) { drawLine = new Snapline(); drawLine.Start = double.MaxValue; drawLine.End = double.MinValue; offsetDict[offset] = drawLine; } drawLine.Offset = offset; drawLine.Start = Math.Min(drawLine.Start, Math.Min(inputLine.Start, mapLine.Start)); drawLine.End = Math.Max(drawLine.End, Math.Max(inputLine.End, mapLine.End)); } } } drawLines = offsetDict.Values.ToList(); return true; } [DebuggerDisplay("Snapline: {Offset}")] class Snapline { public double Offset; public double Start; public double End; public bool RequireOverlap; public int Group; } } }