Here’s what I have so far. Alas, resizing doesn’t work when the node is rotated. I don’t know if that is important to you or not.
The needed code is in a custom ResizingTool. The CustomResizingTool doesn’t resize the Part.ResizeElementName but the Part.SelectionElementName.
The design also depends on a new data property, Flip
, which has four values:
- 0: normal
- 1: flipped horizontally
- 2: flipped vertically
- 3: flipped both ways
I then defined two converters so that the LayoutTransform is bound to the “Flip” property, and so that the FromSpot and ToSpot are bound to the “Flip” property.
Flip.xaml:
<UserControl x:Class="WpfWindow1.Flip"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:go="http://schemas.nwoods.com/GoXam"
xmlns:local="clr-namespace:WpfWindow1">
<UserControl.Resources>
<local:FlipToTransformConverter x:Key="theFlipToTransformConverter"/>
<local:FlipToSpotConverter x:Key="theFlipToSpotConverter"/>
<DataTemplate x:Key="NodeTemplate">
<Grid
Background="Transparent"
go:Node.LocationSpot="Center"
go:Node.Location="{Binding Path=Data.Location, Mode=TwoWay}"
go:Part.Resizable="True" go:Part.ResizeElementName="PANEL"
go:Part.Rotatable="True">
<go:SpotPanel x:Name="PANEL" Grid.Row="0" Grid.Column="0" Background="Transparent"
LayoutTransform="{Binding Path=Data.Flip, Converter={StaticResource theFlipToTransformConverter}}">
<Path x:Name="SHAPE" Data="M0 80 L0 0 50 0 M0 40 L50 40"
Stroke="Green" StrokeThickness="4" Margin="2 2 2 2"
Stretch="Fill" />
<Rectangle x:Name="TR" Width="8" Height="8" go:Node.PortId="TR" Fill="LightBlue" StrokeThickness="0"
go:SpotPanel.Spot="TopRight" go:SpotPanel.Alignment="1 0 0 0"
go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
<Rectangle x:Name="R" Width="8" Height="8" go:Node.PortId="R" Fill="Pink" StrokeThickness="0"
go:SpotPanel.Spot="MiddleRight" go:SpotPanel.Alignment="1 0.5 0 0"
go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=R}"
go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
<Rectangle x:Name="B" Width="8" Height="8" go:Node.PortId="BL" Fill="LightGreen" StrokeThickness="0"
go:SpotPanel.Spot="BottomLeft" go:SpotPanel.Alignment="0 1 0 0"
go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=B}"
go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=B}"
go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
<Rectangle x:Name="TL" Width="8" Height="8" go:Node.PortId="TL" Fill="Yellow" StrokeThickness="0"
go:SpotPanel.Spot="TopLeft" go:SpotPanel.Alignment="0 0 0 0"
go:Node.FromSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=L}"
go:Node.ToSpot="{Binding Path=Data.Flip, Converter={StaticResource theFlipToSpotConverter}, ConverterParameter=L}"
go:Node.LinkableFrom="True" go:Node.LinkableTo="True" />
<ContextMenuService.ContextMenu>
<ContextMenu>
<MenuItem Header="Flip Horizontally" Click="FlipH_Click" />
<MenuItem Header="Flip Vertically" Click="FlipV_Click" />
</ContextMenu>
</ContextMenuService.ContextMenu>
</go:SpotPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="LinkTemplate">
<go:LinkShape Stroke="Black" StrokeThickness="2"
go:Part.SelectionAdorned="True" go:Part.Reshapable="True">
<go:Link.Route>
<go:Route Routing="Orthogonal" Corner="4" Curve="JumpOver"
RelinkableFrom="True" RelinkableTo="True" />
</go:Link.Route>
</go:LinkShape>
</DataTemplate>
</UserControl.Resources>
<Grid>
<go:Diagram x:Name="myDiagram"
Padding="10"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
InitialScale="2"
NodeTemplate="{StaticResource NodeTemplate}"
LinkTemplate="{StaticResource LinkTemplate}">
<go:Diagram.ResizingTool>
<local:CustomResizingTool />
</go:Diagram.ResizingTool>
</go:Diagram>
</Grid>
</UserControl>
Flip.xaml.cs:
using Northwoods.GoXam;
using Northwoods.GoXam.Model;
using Northwoods.GoXam.Tool;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace WpfWindow1 {
/// <summary>
/// Interaction logic for Flip.xaml
/// </summary>
public partial class Flip : UserControl {
public Flip() {
InitializeComponent();
// model is a GraphLinksModel using instances of MyNodeData as the node data
// and MyLinkData as the link data
var model = new GraphLinksModel<MyNodeData, String, String, MyLinkData>();
// Flip: 0 normal, 1 flipped horizontally, 2 flipped vertically, 3 flipped both ways
model.NodesSource = new ObservableCollection<MyNodeData>() {
new MyNodeData() { Key="Alpha", Location=new Point(0, 0) },
new MyNodeData() { Key="Beta", Location=new Point(150, 50), Flip=1 },
new MyNodeData() { Key="Gamma", Location=new Point(-80, 120), Flip=2 },
new MyNodeData() { Key="Delta", Location=new Point(100, 200), Flip=3 }
};
model.LinksSource = new ObservableCollection<MyLinkData>() {
new MyLinkData() { From="Alpha", FromPort="TR", To="Beta", ToPort="TR" },
new MyLinkData() { From="Alpha", FromPort="BL", To="Gamma", ToPort="TR" },
new MyLinkData() { From="Gamma", FromPort="R", To="Delta", ToPort="R" },
new MyLinkData() { From="Beta", FromPort="BL", To="Delta", ToPort="TL" }
};
model.Modifiable = true;
model.HasUndoManager = true;
myDiagram.Model = model;
}
private void FlipH_Click(object sender, RoutedEventArgs e) {
var elt = sender as FrameworkElement;
if (elt != null && elt.DataContext != null) {
var data = ((PartManager.PartBinding)elt.DataContext).Data as MyNodeData;
if (data != null) {
myDiagram.StartTransaction("FlippedH");
data.Flip ^= 1;
myDiagram.CommitTransaction("FlippedH");
}
}
}
private void FlipV_Click(object sender, RoutedEventArgs e) {
var elt = sender as FrameworkElement;
if (elt != null && elt.DataContext != null) {
var data = ((PartManager.PartBinding)elt.DataContext).Data as MyNodeData;
if (data != null) {
myDiagram.StartTransaction("FlippedV");
data.Flip ^= 2;
myDiagram.CommitTransaction("FlippedV");
}
}
}
}
// Define custom node data; the node key is of type String.
// Add a property named Color that might change.
[Serializable] // serializable in WPF to support the clipboard
public class MyNodeData : GraphLinksModelNodeData<String> {
public String Color {
get { return _Color; }
set {
if (_Color != value) {
String old = _Color;
_Color = value;
RaisePropertyChanged("Color", old, value);
}
}
}
private String _Color = "White";
public int Flip {
get { return _Flip; }
set {
if (_Flip != value) {
int old = _Flip;
_Flip = value;
RaisePropertyChanged("Flip", old, value);
}
}
}
private int _Flip = 0;
}
// Define custom link data; the node key is of type String,
// the port key should be of type String but is unused in this app.
[Serializable] // serializable in WPF to support the clipboard
public class MyLinkData : GraphLinksModelLinkData<String, String> {
// nothing to add
}
// Flip: 0 normal, 1 flipped horizontally, 2 flipped vertically, 3 flipped both ways
public class FlipToTransformConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
switch (value) {
case 1: return new ScaleTransform(-1, 1);
case 2: return new ScaleTransform(1, -1);
case 3: return new ScaleTransform(-1, -1);
default: return new ScaleTransform(1, 1);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
public class FlipToSpotConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
var name = parameter == null ? "R" : parameter.ToString();
if (name.Length == 0) name = "R";
switch (name[0]) {
case 'T': // top
switch (value) {
case 1: return new Spot(0.5, 0); // flipped horizontally
case 2: return new Spot(0.5, 1, 0, -8); // flipped vertically
case 3: return new Spot(0.5, 1, 0, -8); // flipped both
default: return new Spot(0.5, 0); // normal
}
case 'L': // left
switch (value) {
case 1: return new Spot(1, 0.5, -8, 0); // flipped horizontally
case 2: return new Spot(0, 0.5); // flipped vertically
case 3: return new Spot(1, 0.5, -8, 0); // flipped both
default: return new Spot(0, 0.5); // normal
}
case 'R': // right
switch (value) {
case 1: return new Spot(0, 0.5, 8, 0); // flipped horizontally
case 2: return new Spot(1, 0.5); // flipped vertically
case 3: return new Spot(0, 0.5, 8, 0); // flipped both
default: return new Spot(1, 0.5); // normal
}
default: // bottom
switch (value) {
case 1: return new Spot(0.5, 1); // flipped horizontally
case 2: return new Spot(0.5, 0, 0, 8); // flipped vertically
case 3: return new Spot(0.5, 0, 0, 8); // flipped both
default: return new Spot(0.5, 1); // normal
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
public class CustomResizingTool : ResizingTool {
public override void UpdateAdornments(Part part) {
if (part == null || part is Link) return; // can't resize links
var ToolCategory = "Resize";
Adornment adornment = null;
if (part.IsSelected) {
FrameworkElement selelt = null;
//String eltname = part.ResizeElementName;
//if (eltname != null) selelt = part.FindNamedDescendant(eltname);
if (selelt == null) selelt = part.SelectionElement;
if (selelt != null && part.CanResize() && Part.IsVisibleElement(selelt)) {
adornment = part.GetAdornment(ToolCategory);
if (adornment == null) {
DataTemplate template = part.ResizeAdornmentTemplate;
if (template == null) template = Diagram.FindDefault<DataTemplate>("DefaultResizeAdornmentTemplate");
adornment = part.MakeAdornment(selelt, template);
if (adornment != null) {
adornment.Category = ToolCategory;
adornment.LocationSpot = Spot.TopLeft;
}
}
if (adornment != null) {
Node node = (Node)part;
Point loc = part.GetElementPoint(selelt, Spot.TopLeft);
var shape = selelt as Shape;
if (shape != null) loc.Offset(3, 3);
// use the node's angle, not part.GetAngle(selelt)
//?? this can be wrong if there are additional rotational transforms in the tree
double angle = node.RotationAngle;
UpdateResizeHandles(adornment.VisualElement, angle);
adornment.Location = loc;
adornment.RotationAngle = angle;
adornment.Remeasure();
}
}
}
part.SetAdornment(ToolCategory, adornment);
}
// copied from ResizingTool.cs
private void UpdateResizeHandles(FrameworkElement elt, double angle) {
if (elt == null) return;
if (NodePanel.GetFigure(elt) != NodeFigure.None) {
SetResizeCursor(elt, angle);
} else {
int count = VisualTreeHelper.GetChildrenCount(elt);
for (int i = 0; i < count; i++) {
UpdateResizeHandles(VisualTreeHelper.GetChild(elt, i) as FrameworkElement, angle);
}
}
}
private void SetResizeCursor(FrameworkElement h, double angle) {
Spot spot = SpotPanel.GetSpot(h);
if (spot.IsNoSpot) spot = Spot.Center;
double a = angle;
if (spot.X <= 0) { // left
if (spot.Y <= 0) { // top-left
a += 225;
} else if (spot.Y >= 1) { // bottom-left
a += 135;
} else { // middle-left
a += 180;
}
} else if (spot.X >= 1) { // right
if (spot.Y <= 0) { // top-right
a += 315;
} else if (spot.Y >= 1) { // bottom-right
a += 45;
} else { // middle-right
// a += 0;
}
} else { // middle-X
if (spot.Y <= 0) { // top-middle
a += 270;
} else if (spot.Y >= 1) { // bottom-middle
a += 90;
} else {
// handle is in the middle-middle -- don't do anything
return;
}
}
if (a < 0) a += 360;
else if (a >= 360) a -= 360;
if (a < 22.5)
h.Cursor = Cursors.SizeWE;
else if (a < 67.5)
h.Cursor = Cursors.SizeNWSE;
else if (a < 112.5)
h.Cursor = Cursors.SizeNS;
else if (a < 157.5)
h.Cursor = Cursors.SizeNESW;
else if (a < 202.5)
h.Cursor = Cursors.SizeWE;
else if (a < 247.5)
h.Cursor = Cursors.SizeNWSE;
else if (a < 292.5)
h.Cursor = Cursors.SizeNS;
else if (a < 337.5)
h.Cursor = Cursors.SizeNESW;
else
h.Cursor = Cursors.SizeWE;
}
private Spot ComputeResizeSpot(FrameworkElement h) {
int flip = 0;
var data = this.AdornedNode.Data as MyNodeData;
if (data != null) flip = data.Flip;
Spot spot = SpotPanel.GetSpot(h);
if (spot.IsNoSpot) spot = Spot.Center;
double a = 0;
if (spot.X <= 0) { // left
if (spot.Y <= 0) { // top-left
a += 225;
} else if (spot.Y >= 1) { // bottom-left
a += 135;
} else { // middle-left
a += 180;
}
} else if (spot.X >= 1) { // right
if (spot.Y <= 0) { // top-right
a += 315;
} else if (spot.Y >= 1) { // bottom-right
a += 45;
} else { // middle-right
// a += 0;
}
} else { // middle-X
if (spot.Y <= 0) { // top-middle
a += 270;
} else if (spot.Y >= 1) { // bottom-middle
a += 90;
} else {
// handle is in the middle-middle -- don't do anything
return spot;
}
}
if (a < 0) a += 360;
else if (a >= 360) a -= 360;
var angle = a;
if (flip == 1) { // flip horizontally
a = 180 - a;
} else if (flip == 2) { // flip vertically
a = -a;
} else if (flip == 3) { // flipping both is same as rotating 180
a += 180;
}
if (a < 0) a += 360;
else if (a >= 360) a -= 360;
Spot newspot = spot;
if (a < 22.5)
newspot = Spot.MiddleRight;
else if (a < 67.5)
newspot = Spot.BottomRight;
else if (a < 112.5)
newspot = Spot.BottomCenter;
else if (a < 157.5)
newspot = Spot.BottomLeft;
else if (a < 202.5)
newspot = Spot.MiddleLeft;
else if (a < 247.5)
newspot = Spot.TopLeft;
else if (a < 292.5)
newspot = Spot.TopCenter;
else if (a < 337.5)
newspot = Spot.TopRight;
else
newspot = Spot.MiddleRight;
return newspot;
}
public override void DoActivate() {
base.DoActivate();
//var spot = SpotPanel.GetSpot(this.Handle);
var spot = ComputeResizeSpot(this.Handle);
var pt = this.AdornedNode.GetElementPoint(this.AdornedElement, spot.Opposite);
int flip = 0;
var data = this.AdornedNode.Data as MyNodeData;
if (data != null) flip = data.Flip;
switch (flip) {
case 1:
if (spot.X == 1) pt.X += this.OriginalBounds.Width;
else if (spot.X == 0) pt.X -= this.OriginalBounds.Width;
break;
case 2:
if (spot.Y == 1) pt.Y += this.OriginalBounds.Height;
else if (spot.Y == 0) pt.Y -= this.OriginalBounds.Height;
break;
case 3:
if (spot.X == 1) pt.X += this.OriginalBounds.Width;
else if (spot.X == 0) pt.X -= this.OriginalBounds.Width;
if (spot.Y == 1) pt.Y += this.OriginalBounds.Height;
else if (spot.Y == 0) pt.Y -= this.OriginalBounds.Height;
break;
default: break;
}
this.OriginalPoint = pt;
}
private Point OriginalPoint;
protected override void DoResize(Rect newr) {
Node node = this.AdornedNode;
FrameworkElement elt = node.FindNamedDescendant("SHAPE");
//var spot = SpotPanel.GetSpot(this.Handle);
var spot = ComputeResizeSpot(this.Handle);
elt.Width = newr.Width;
elt.Height = newr.Height;
node.Remeasure();
var pt = node.GetElementPoint(elt, spot.Opposite);
var newloc = node.Location;
newloc.X = newloc.X - pt.X + this.OriginalPoint.X;
newloc.Y = newloc.Y - pt.Y + this.OriginalPoint.Y;
node.Location = newloc;
}
}
}