Skip to content
This repository has been archived by the owner on Sep 25, 2024. It is now read-only.

Commit

Permalink
[Mac] Initial Origin Control implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
CartBlanche committed Sep 7, 2019
1 parent d54d7ab commit ea97571
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 7 deletions.
64 changes: 58 additions & 6 deletions Xamarin.PropertyEditing.Mac/Controls/BaseRectangleEditorControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Drawing;
using AppKit;
using CoreGraphics;
using Xamarin.PropertyEditing.Drawing;
using Xamarin.PropertyEditing.ViewModels;

namespace Xamarin.PropertyEditing.Mac
Expand All @@ -22,6 +23,11 @@ internal abstract class BaseRectangleEditorControl<T> : PropertyEditorControl<Pr
public override NSView FirstKeyView => XEditor;
public override NSView LastKeyView => HeightEditor.DecrementButton;

public NSLayoutConstraint LeftXEditorEdgeConstraint { get; }
private OriginControl originView;
private NSLayoutConstraint originViewConstraint;
private CommonOrigin lastOrigin = CommonOrigin.TopLeft;

protected BaseRectangleEditorControl (IHostResourceProvider hostResources)
: base (hostResources)
{
Expand All @@ -35,7 +41,7 @@ protected BaseRectangleEditorControl (IHostResourceProvider hostResources)
};
XEditor.ValueChanged += OnInputUpdated;

YLabel = new UnfocusableTextField {
YLabel = new UnfocusableTextField {
Font = NSFont.FromFontName (DefaultFontName, DefaultDescriptionLabelFontSize),
TranslatesAutoresizingMaskIntoConstraints = false,
};
Expand All @@ -55,7 +61,7 @@ protected BaseRectangleEditorControl (IHostResourceProvider hostResources)
};
WidthEditor.ValueChanged += OnInputUpdated;

HeightLabel = new UnfocusableTextField {
HeightLabel = new UnfocusableTextField {
Font = NSFont.FromFontName (DefaultFontName, DefaultDescriptionLabelFontSize),
TranslatesAutoresizingMaskIntoConstraints = false,
};
Expand All @@ -74,9 +80,11 @@ protected BaseRectangleEditorControl (IHostResourceProvider hostResources)
AddSubview (HeightLabel);
AddSubview (HeightEditor);

this.AddConstraints (new[] {
LeftXEditorEdgeConstraint = NSLayoutConstraint.Create (XEditor, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1f, 0);

AddConstraints (new[] {
NSLayoutConstraint.Create (XEditor, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1f, 3f),
NSLayoutConstraint.Create (XEditor, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1f, 0f),
LeftXEditorEdgeConstraint,
NSLayoutConstraint.Create (XEditor, NSLayoutAttribute.Right, NSLayoutRelation.Equal, YEditor, NSLayoutAttribute.Left, 1f, -10f),
NSLayoutConstraint.Create (XEditor, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1f, 18),

Expand All @@ -92,7 +100,7 @@ protected BaseRectangleEditorControl (IHostResourceProvider hostResources)
NSLayoutConstraint.Create (YLabel, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1f, 18),

NSLayoutConstraint.Create (WidthEditor, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1f, 33f),
NSLayoutConstraint.Create (WidthEditor, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1f, 0f),
NSLayoutConstraint.Create (WidthEditor, NSLayoutAttribute.Left, NSLayoutRelation.Equal, XEditor, NSLayoutAttribute.Left, 1f, 0f),
NSLayoutConstraint.Create (WidthEditor, NSLayoutAttribute.Right, NSLayoutRelation.Equal, HeightEditor, NSLayoutAttribute.Left, 1f, -10f),
NSLayoutConstraint.Create (WidthEditor, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1f, 18),

Expand All @@ -119,7 +127,7 @@ protected BaseRectangleEditorControl (IHostResourceProvider hostResources)

protected virtual void OnInputUpdated (object sender, EventArgs e)
{
ViewModel.Value = (T)Activator.CreateInstance (typeof(T), XEditor.Value, YEditor.Value, WidthEditor.Value, HeightEditor.Value);
ViewModel.Value = (T)Activator.CreateInstance (typeof (T), XEditor.Value, YEditor.Value, WidthEditor.Value, HeightEditor.Value);
}

protected override void SetEnabled ()
Expand Down Expand Up @@ -154,5 +162,49 @@ protected override void AppearanceChanged ()
WidthLabel.TextColor = HostResources.GetNamedColor (NamedResources.DescriptionLabelColor);
HeightLabel.TextColor = HostResources.GetNamedColor (NamedResources.DescriptionLabelColor);
}

protected override void OnViewModelChanged (PropertyViewModel oldModel)
{
base.OnViewModelChanged (oldModel);

if (ViewModel == null)
return;

LeftXEditorEdgeConstraint.Active = true;

if (ViewModel is RectanglePropertyViewModel rpvm && rpvm.HasOrigin) {
LeftXEditorEdgeConstraint.Active = false;
if (this.originView == null) {
this.originView = new OriginControl (HostResources) {
TranslatesAutoresizingMaskIntoConstraints = false,
};

this.originView.OriginChanged += (s, e) => {
this.lastOrigin = this.originView.Origin;
/* TODO MetricsPropertyEntryHelper.HandleOriginChanged (this, this.originView.Origin, v => {
if (XEditor.Value != v)
XEditor.Value = v;
}, v => {
if (YEditor.Value != v)
YEditor.Value = v;
});*/
};
this.originView.Origin = this.lastOrigin;

AddSubview (this.originView);

this.originViewConstraint = NSLayoutConstraint.Create (this.originView, NSLayoutAttribute.Right, NSLayoutRelation.Equal, XEditor, NSLayoutAttribute.Left, 1, -4);

AddConstraints (new[] {
NSLayoutConstraint.Create (this.originView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, XEditor, NSLayoutAttribute.Top, 1f, 0),
NSLayoutConstraint.Create (this.originView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1f, 0),
this.originViewConstraint,
NSLayoutConstraint.Create (this.originView, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this, NSLayoutAttribute.Bottom, 1, -2f),
});

// TODO this.FirstKeyView = this.originView;
}
}
}
}
}
246 changes: 246 additions & 0 deletions Xamarin.PropertyEditing.Mac/Controls/OriginControl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
using System;
using System.Linq;
using AppKit;
using CoreGraphics;
using Xamarin.PropertyEditing.Drawing;

namespace Xamarin.PropertyEditing.Mac
{
internal class OriginControl : NSView
{
public event EventHandler OriginChanged;

private CommonOrigin origin = CommonOrigin.TopLeft;
private CGPoint[] points;

private NSColor innerFillColor = NSColor.White;
private NSColor positionPointColor = NSColor.Black;

private nfloat[] lightBezelGreys = { .41f, .29f, .29f, .29f };

protected UnfocusableTextField OriginLabel { get; set; }

private readonly IHostResourceProvider hostResources;

internal OriginControl (IHostResourceProvider hostResources)
{
if (hostResources == null)
throw new ArgumentNullException (nameof (hostResources));

this.hostResources = hostResources;

OriginLabel = new UnfocusableTextField {
Font = NSFont.FromFontName (PropertyEditorControl.DefaultFontName, PropertyEditorControl.DefaultDescriptionLabelFontSize),
StringValue = "ORIGIN",
TranslatesAutoresizingMaskIntoConstraints = false,
};

AddSubview (OriginLabel);

AddConstraints (new[] {
NSLayoutConstraint.Create (OriginLabel, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, this, NSLayoutAttribute.Bottom, 1f, 1f),
NSLayoutConstraint.Create (OriginLabel, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1f, 18),
NSLayoutConstraint.Create (OriginLabel, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, this, NSLayoutAttribute.CenterX, 1f, 0),
});

AppearanceChanged ();
}

public sealed override void ViewDidChangeEffectiveAppearance ()
{
base.ViewDidChangeEffectiveAppearance ();

AppearanceChanged ();
}

private void AppearanceChanged ()
{
if (EffectiveAppearance.Name.ToLower().Contains("dark")) {
this.lightBezelGreys = new nfloat[] { .59f, .71f, .71f, .71f };
this.positionPointColor = NSColor.DarkGray;
} else {
this.lightBezelGreys = new nfloat[] { .41f, .29f, .29f, .29f };
this.positionPointColor = NSColor.LightGray;
}

this.innerFillColor = this.hostResources.GetNamedColor (NamedResources.ControlBackground);

OriginLabel.TextColor = this.hostResources.GetNamedColor (NamedResources.DescriptionLabelColor);

NeedsDisplay = true;
}

public CommonOrigin Origin {
get {
return this.origin;
}
set {
if (this.origin.Equals (value))
return;
this.origin = value;

OriginChanged?.Invoke (this, EventArgs.Empty);
NeedsDisplay = true;
}
}

public override CGSize IntrinsicContentSize {
get {
return new CGSize (70, 70);
}
}

public override bool IsFlipped {
get {
return true;
}
}

public override void DrawRect (CGRect dirtyRect)
{
const int radius = 1;
var rect = Bounds;
rect.Inflate (-(0 + radius), -(6 + radius));
rect.Y = 1;
this.innerFillColor.Set ();
NSGraphics.RectFill (rect);
NSGraphics.DrawTiledRects (rect, rect, new NSRectEdge[] {
NSRectEdge.MinYEdge, NSRectEdge.MinXEdge, NSRectEdge.MaxXEdge, NSRectEdge.MaxYEdge
},
this.lightBezelGreys);

var innerRect = rect;
innerRect.Inflate (-(5 + radius), -(5 + radius));
// Draw focus points
this.points = new CGPoint[] {
new CGPoint (innerRect.Left, innerRect.Top),
new CGPoint (innerRect.Center ().X, innerRect.Top),
new CGPoint (innerRect.Right, innerRect.Top),
new CGPoint (innerRect.Left, innerRect.Center ().Y),
innerRect.Center (),
new CGPoint (innerRect.Right, innerRect.Center ().Y),
new CGPoint (innerRect.Left, innerRect.Bottom),
new CGPoint (innerRect.Center ().X, innerRect.Bottom),
new CGPoint (innerRect.Right, innerRect.Bottom)
};

this.positionPointColor.Set ();
foreach (var point in this.points) {
var path = new NSBezierPath ();
path.AppendPathWithArc (point, radius, 0, 360);
path.Fill ();
}

// Draw selection arrows
NSColor.Red.Set ();
var center = GetRectCenterForOrigin (Origin, innerRect);
const int ArrowLength = 8;
foreach (var dir in Origin.GetArrowDirections ()) {
switch (dir) {
case CommonOrigin.Direction.Left:
DrawArrow (center, new CGPoint (center.X - ArrowLength, center.Y));
break;
case CommonOrigin.Direction.Right:
DrawArrow (center, new CGPoint (center.X + ArrowLength, center.Y));
break;
case CommonOrigin.Direction.Up:
DrawArrow (center, new CGPoint (center.X, center.Y - ArrowLength));
break;
case CommonOrigin.Direction.Down:
DrawArrow (center, new CGPoint (center.X, center.Y + ArrowLength));
break;
}
}
}

static CGPoint GetRectCenterForOrigin (CommonOrigin origin, CGRect rect)
{
nfloat x = rect.Left;
nfloat y = rect.Top;

switch (origin.Horizontal) {
case CommonOrigin.Position.Middle:
x = rect.Center ().X;
break;

case CommonOrigin.Position.End:
x = rect.Right;
break;
}

switch (origin.Vertical) {
case CommonOrigin.Position.Middle:
y = rect.Center ().Y;
break;

case CommonOrigin.Position.End:
y = rect.Bottom;
break;
}

return new CGPoint (x, y);
}

void DrawArrow (CGPoint from, CGPoint to, int thickness = 1)
{
const int HeadOffset = 3;

var path = new NSBezierPath ();
// Draw body
path.SetLineDash (new nfloat[] { 2, 1 }, 0);
path.MoveTo (from);
path.LineTo (to);
path.Stroke ();

path = new NSBezierPath ();
// Draw head
if (from.Y == to.Y) {
// horizontal
path.MoveTo (to);
path.LineTo (new CGPoint ((from.X < to.X) ? to.X - HeadOffset : to.X + HeadOffset, to.Y + HeadOffset));
path.MoveTo (to);
path.LineTo (new CGPoint ((from.X < to.X) ? to.X - HeadOffset : to.X + HeadOffset, to.Y - HeadOffset));
path.Stroke ();
} else {
// vertical
path.MoveTo (to);
path.LineTo (new CGPoint (to.X + HeadOffset, (from.Y < to.Y) ? to.Y - HeadOffset : to.Y + HeadOffset));
path.MoveTo (to);
path.LineTo (new CGPoint (to.X - HeadOffset, (from.Y < to.Y) ? to.Y - HeadOffset : to.Y + HeadOffset));
path.Stroke ();
}
}

public override void MouseUp (NSEvent theEvent)
{
base.MouseUp (theEvent);

if (this.points == null)
return;

var mousePosition = ConvertPointFromView (Window.MouseLocationOutsideOfEventStream, null);
var x = mousePosition.X;
var y = mousePosition.Y;

Func<CGPoint, double> distance = p => Math.Sqrt (Math.Pow (y - p.Y, 2) + Math.Pow (x - p.X, 2));
var closestPoint = points.OrderBy (distance).First ();

const int MinDistance = 5;
if (distance (closestPoint) < MinDistance) {
int index = Array.IndexOf (points, closestPoint);
Origin = new CommonOrigin {
Vertical = (CommonOrigin.Position)(index / 3),
Horizontal = (CommonOrigin.Position)(index % 3)
};
}
}
}

public static class CGRectEx
{
public static CGPoint Center (this CGRect rect)
{
return new CGPoint (rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public MockSampleControl ()

AddEvents ("Click", "Hover", "Focus");

AddProperty<CommonRectangle> ("RectangleWithOrigin", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding);
}

// Categories
Expand Down
Loading

0 comments on commit ea97571

Please sign in to comment.