diff options
author | CartBlanche <savagesoftware@gmail.com> | 2019-09-16 21:19:56 +0300 |
---|---|---|
committer | CartBlanche <savagesoftware@gmail.com> | 2020-01-08 13:48:31 +0300 |
commit | 301b472e7483e05d4b5cbec71bc49629fc1bfe10 (patch) | |
tree | 83bbc7c9e78f6c34fd681ec07dae9075e1333789 | |
parent | 78a861400708d6e64bc6b935ff0a8c1698005e9f (diff) |
[Mac] Initial Autoresizing Control Implementationdominique-autoresizing
14 files changed, 954 insertions, 24 deletions
diff --git a/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingMaskView.cs b/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingMaskView.cs new file mode 100644 index 0000000..4216209 --- /dev/null +++ b/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingMaskView.cs @@ -0,0 +1,297 @@ +using System; +using AppKit; +using CoreGraphics; +using Xamarin.PropertyEditing.Common; + +namespace Xamarin.PropertyEditing.Mac +{ + internal class AutoResizingMaskView : NSView + { + public event EventHandler MaskChanged; + + private readonly NSColor internalBorderColor = NSColor.FromDeviceRgba (.54f, .54f, .54f, 1); + internal static NSColor activeArrowFillColor = NSColor.FromDeviceRgba (.96f, .43f, .31f, 1); + private readonly NSColor inactiveArrowFillColor = NSColor.FromDeviceRgba (.96f, .67f, .62f, 1); + internal static NSColor disabledArrowFillColor = NSColor.FromDeviceRgba (.96f, .43f, .31f, 0.5f); + internal static NSColor backgroundFillColor = NSColor.WindowBackground; + + private bool enabled; + private AutoResizingFlags mask; + private readonly AutoResizingFlags[] autoResizingFlagsList; + + public AutoResizingFlags Mask + { + get { return this.mask; } + + internal set + { + if (this.mask == value) + return; + + this.mask = value; + + // Our state has changed, repaint + MaskChanged?.Invoke (this, EventArgs.Empty); + NeedsDisplay = true; + } + } + + #region Overrriden Methods and Properties + + public override CGSize IntrinsicContentSize + { + get { return new CGSize (76, 76); } + } + + public override bool IsFlipped + { + get { return true; } + } + + public bool Enabled + { + get { return this.enabled; } + + internal set + { + if (this.enabled == value) + return; + + this.enabled = value; + + // Our state has changed, repaint + NeedsDisplay = true; + } + } + + public override bool CanBecomeKeyView + { + get { return this.enabled; } + } + + public override CGRect FocusRingMaskBounds => Bounds; + + public override bool AcceptsFirstResponder () + { + return this.enabled; + } + + public override void DrawRect (CGRect dirtyRect) + { + var rect = Bounds; + backgroundFillColor.Set (); + NSGraphics.RectFill (rect); + DrawingUtils.DrawLightShadedBezel (rect, flipped: IsFlipped); + + // Draw inner rectangle + var fourthWidth = (int)rect.Width / 4; + var fourthHeight = (int)rect.Height / 4; + var innerRect = new CGRect (fourthWidth, fourthHeight, 2 * fourthWidth, 2 * fourthHeight); + backgroundFillColor.Set (); + NSGraphics.RectFill (innerRect); + this.internalBorderColor.Set (); + NSGraphics.FrameRectWithWidth (innerRect, 1); + + const int ArrowOffset = 3; + + // Draw position arrows + DrawPositionArrow (new CGPoint (ArrowOffset, 2 * fourthHeight), + new CGPoint (fourthWidth - ArrowOffset, 2 * fourthHeight), + !Mask.HasFlag (AutoResizingFlags.FlexibleLeftMargin) && !Mask.HasFlag (AutoResizingFlags.FlexibleMargins)); + DrawPositionArrow (new CGPoint (3 * fourthWidth + ArrowOffset - 1, 2 * fourthHeight), + new CGPoint ((int)rect.Width - ArrowOffset - 1, 2 * fourthHeight), + !Mask.HasFlag (AutoResizingFlags.FlexibleRightMargin) && !Mask.HasFlag (AutoResizingFlags.FlexibleMargins)); + DrawPositionArrow (new CGPoint (2 * fourthWidth, ArrowOffset), + new CGPoint (2 * fourthWidth, fourthHeight - ArrowOffset), + !Mask.HasFlag (AutoResizingFlags.FlexibleTopMargin) && !Mask.HasFlag (AutoResizingFlags.FlexibleMargins)); + DrawPositionArrow (new CGPoint (2 * fourthWidth, 3 * fourthHeight + ArrowOffset - 1), + new CGPoint (2 * fourthWidth, (int)rect.Height - ArrowOffset - 1), + !Mask.HasFlag (AutoResizingFlags.FlexibleBottomMargin) && !Mask.HasFlag (AutoResizingFlags.FlexibleMargins)); + + // Draw size arrows + DrawSizeArrow (new CGPoint (fourthWidth + ArrowOffset, 2 * fourthHeight), new CGPoint (3 * fourthWidth - ArrowOffset, 2 * fourthHeight), + Mask.HasFlag (AutoResizingFlags.FlexibleWidth) || Mask.HasFlag (AutoResizingFlags.FlexibleDimensions)); + DrawSizeArrow (new CGPoint (2 * fourthWidth, fourthHeight + ArrowOffset), new CGPoint (2 * fourthWidth, 3 * fourthHeight - ArrowOffset), + Mask.HasFlag (AutoResizingFlags.FlexibleHeight) || Mask.HasFlag (AutoResizingFlags.FlexibleDimensions)); + } + + public override void MouseDown (NSEvent theEvent) + { + if (!this.enabled) + return; + + var mousePosition = ConvertPointFromView (Window.MouseLocationOutsideOfEventStream, null); + var x = mousePosition.X; + var y = mousePosition.Y; + + const int Tolerance = 5; + var rect = Bounds; + + var inHorizontal = y > rect.Height / 2 - Tolerance && y < rect.Height / 2 + Tolerance; + var inVertical = x > rect.Width / 2 - Tolerance && x < rect.Width / 2 + Tolerance; + if (!inHorizontal && !inVertical) + return; + + var fourthWidth = rect.Width / 4; + var fourthHeight = rect.Height / 4; + + if (inHorizontal) { + if (x < fourthWidth) + ToggleMaskFlag (AutoResizingFlags.FlexibleLeftMargin); + else if (x > 3 * fourthWidth) + ToggleMaskFlag (AutoResizingFlags.FlexibleRightMargin); + else + ToggleMaskFlag (AutoResizingFlags.FlexibleWidth); + } else { + if (y < fourthHeight) + ToggleMaskFlag (AutoResizingFlags.FlexibleTopMargin); + else if (y > 3 * fourthHeight) + ToggleMaskFlag (AutoResizingFlags.FlexibleBottomMargin); + else + ToggleMaskFlag (AutoResizingFlags.FlexibleHeight); + } + } + + public override void KeyUp (NSEvent theEvent) + { + if (!this.enabled) + return; + + var currentOriginIndex = this.autoResizingFlagsList.IndexOf (this.mask); + + switch (theEvent.KeyCode) { + // Move to next left flag position + case 123: + currentOriginIndex--; + if (currentOriginIndex < 0) + currentOriginIndex = this.autoResizingFlagsList.Length - 1; + + Mask = this.autoResizingFlagsList[currentOriginIndex]; + break; + + // Move to next right flag position + case 124: + currentOriginIndex++; + if (currentOriginIndex > this.autoResizingFlagsList.Length - 1) + currentOriginIndex = 0; + + Mask = this.autoResizingFlagsList[currentOriginIndex]; + break; + + default: + base.KeyUp (theEvent); + break; + } + } + + public override bool BecomeFirstResponder () + { + var willBecomeFirstResponder = base.BecomeFirstResponder (); + if (willBecomeFirstResponder) { + ScrollRectToVisible (Bounds); + } + + return willBecomeFirstResponder; + } + + public override void DrawFocusRingMask () + { + NSGraphics.RectFill (Bounds); + } + + public sealed override void ViewDidChangeEffectiveAppearance () + { + base.ViewDidChangeEffectiveAppearance (); + + AppearanceChanged (); + } + #endregion + + public AutoResizingMaskView () + { + this.autoResizingFlagsList = (AutoResizingFlags[])Enum.GetValues (typeof (AutoResizingFlags)); + AppearanceChanged (); + } + + private void DrawPositionArrow (CGPoint from, CGPoint to, bool full) + { + if (full && !this.enabled) + disabledArrowFillColor.Set (); + else if (full) + activeArrowFillColor.Set (); + else + this.inactiveArrowFillColor.Set (); + + DrawUtils.DrawStraightLine (from, to, full ? null : new int[] { 0, 2, 2, 1, 4, 1, 2 }); + + const int KnobLength = 5; + + Func<CGPoint, int, CGPoint> pointUpdater; + if (from.Y == to.Y) + pointUpdater = (p, m) => new CGPoint (p.X, p.Y + m * KnobLength + (1 - m) / 2); + else + pointUpdater = (p, m) => new CGPoint (p.X + m * KnobLength + (1 - m) / 2, p.Y); + + DrawUtils.DrawStraightLine (pointUpdater (from, -1), pointUpdater (from, 1), full ? null : new int[] { 0, 1 }); + DrawUtils.DrawStraightLine (pointUpdater (to, -1), pointUpdater (to, 1), full ? null : new int[] { 0, 1 }); + } + + private void DrawSizeArrow (CGPoint from, CGPoint to, bool full) + { + const int HeadOffset = 4; + + if (full && !this.enabled) + disabledArrowFillColor.Set (); + else if (full) + activeArrowFillColor.Set (); + else + this.inactiveArrowFillColor.Set (); + + //var padding = from.Y == to.Y ? new Size (3, 0) : new Size (0, 3); + DrawUtils.DrawStraightLine (from, to, full ? null : new int[] { 3, 1 }); + + Action<CGPoint, CGPoint> drawHead; + + // Draw head + if (from.Y == to.Y) { + // horizontal + drawHead = (f, t) => { + var path = new NSBezierPath (); + path.Append (new CGPoint[] { + new CGPoint ((f.X < t.X) ? t.X - HeadOffset : t.X + HeadOffset, t.Y + HeadOffset + .5f), + new CGPoint (t.X, t.Y + .5f), + new CGPoint ((f.X < t.X) ? t.X - HeadOffset : t.X + HeadOffset, t.Y - HeadOffset + .5f), + }); + path.Stroke (); + }; + } else { + // vertical + drawHead = (f, t) => { + var path = new NSBezierPath (); + path.Append (new CGPoint[] { + new CGPoint (t.X + HeadOffset + .5f, (f.Y < t.Y) ? t.Y - HeadOffset : t.Y + HeadOffset), + new CGPoint (t.X + .5f, t.Y), + new CGPoint (t.X - HeadOffset + .5f, (f.Y < t.Y) ? t.Y - HeadOffset : t.Y + HeadOffset) + }); + path.Stroke (); + }; + } + + drawHead (from, to); + drawHead (to, from); + } + + private void ToggleMaskFlag (AutoResizingFlags flag) + { + Mask ^= flag; + MaskChanged?.Invoke (this, EventArgs.Empty); + NeedsDisplay = true; + } + + private void AppearanceChanged () + { + // Placeholder to handle theme changes + DrawingUtils.UpdateBezelGreys (EffectiveAppearance.Name.Contains ("dark")); // TODO temporary hack + } + } +} diff --git a/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingPreviewView.cs b/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingPreviewView.cs new file mode 100644 index 0000000..ab505ac --- /dev/null +++ b/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingPreviewView.cs @@ -0,0 +1,263 @@ +using System; +using System.Drawing; +using AppKit; +using CoreGraphics; +using Foundation; +using Xamarin.PropertyEditing.Common; + +namespace Xamarin.PropertyEditing.Mac +{ + internal class AutoResizingPreviewView : NSView + { + private const int Height = 70; + private NSImage originalBackgroundImage; + private NSImage bgImage; + private CGSize bgImageSize; + + private readonly NSColor backgroundFillColor = AutoResizingMaskView.backgroundFillColor; + private readonly NSColor windowBorderColor = NSColor.FromDeviceRgba (.75f, .75f, .75f, 1); + private readonly NSColor windowFillColor = NSColor.White; + private readonly NSColor previewBorderColor = NSColor.FromDeviceRgba (.33f, .33f, .33f, 1); + private readonly NSColor enabledElementFillColor = AutoResizingMaskView.activeArrowFillColor; + private readonly NSColor disabledElementFillColor = AutoResizingMaskView.disabledArrowFillColor; + + private AutoResizingFlags mask; + private IDisposable currentAnimation; + private int currentAnimationRawValue; + private int currentAnimationValue; + + private CGRect lastBounds; + private NSTrackingArea trackArea; + private bool enabled; + + public AutoResizingFlags Mask + { + get { return this.mask; } + set + { + this.mask = value; + NeedsDisplay = true; + } + } + + public AutoResizingPreviewView (NSImage backgroundImage) + { + this.originalBackgroundImage = backgroundImage; + WantsLayer = true; + this.trackArea = new NSTrackingArea (Frame, NSTrackingAreaOptions.MouseEnteredAndExited | NSTrackingAreaOptions.ActiveInKeyWindow, this, null); + AddTrackingArea (this.trackArea); + + AppearanceChanged (); + } + + private void AppearanceChanged () + { + // Place holder so we can handle them changes + } + + #region Overrriden Methods and Properties + + public override CGSize IntrinsicContentSize + { + get { return new CGSize ((int)(Height * originalBackgroundImage.Size.Width / originalBackgroundImage.Size.Height), Height + 6); } + } + + public override bool IsFlipped + { + get { return true; } + } + + public override void UpdateTrackingAreas () + { + base.UpdateTrackingAreas (); + RemoveTrackingArea (trackArea); + this.trackArea = new NSTrackingArea (new CGRect (CGPoint.Empty, Frame.Size), NSTrackingAreaOptions.MouseEnteredAndExited | NSTrackingAreaOptions.ActiveInKeyWindow, this, null); + AddTrackingArea (this.trackArea); + } + + public override void DrawRect (CGRect dirtyRect) + { + var rect = Bounds; + UpdateBackgroundSurface (rect); + DrawPreviewBackground (rect); + + var x = (int)((rect.Width - bgImageSize.Width) / 2); + var y = (int)((rect.Height - bgImageSize.Height) / 2); + + // Overlay interface + var size = GetWindowSize (bgImageSize); + CGRect windowRect = new CGRect (x + 10, y + 10, size.Width, size.Height); + + var elementRect = GetElementRectForWindowAndMask (new RectangleF ((float)windowRect.X, (float)windowRect.Y, (float)windowRect.Width, (float)windowRect.Height), this.mask); + + this.windowFillColor.Set (); + NSGraphics.RectFill (windowRect); + this.windowBorderColor.Set (); + NSGraphics.FrameRectWithWidth (windowRect, 1); + + if (this.enabled) + this.enabledElementFillColor.Set (); + else + this.disabledElementFillColor.Set (); + NSGraphics.RectFill (new CGRect (elementRect.X, elementRect.Y, elementRect.Size.Width, elementRect.Size.Height)); + } + + public override void MouseEntered (NSEvent theEvent) + { + base.MouseEntered (theEvent); + if (this.enabled) + StartAnimation (); + } + + public override void MouseExited (NSEvent theEvent) + { + base.MouseExited (theEvent); + if (this.enabled) + StopAnimation (); + NeedsDisplay = true; + } + + public sealed override void ViewDidChangeEffectiveAppearance () + { + base.ViewDidChangeEffectiveAppearance (); + + AppearanceChanged (); + } + #endregion + + public bool Enabled + { + get { return this.enabled; } + + internal set + { + if (this.enabled == value) + return; + + this.enabled = value; + + // Our state has changed, repaint + NeedsDisplay = true; + } + } + + private void UpdateBackgroundSurface (CGRect bounds) + { + if (bounds == this.lastBounds) + return; + + var rect = bounds; + + this.bgImage = originalBackgroundImage; + this.bgImage.Size = GetBoxSize (bgImage, rect.Width - 8, rect.Height - 8); + this.bgImageSize = this.bgImage.Size; + this.lastBounds = bounds; + } + + private CGSize GetBoxSize (NSImage image, double maxWidth, double maxHeight) + { + var size = image.Size; + var ratio = (nfloat)Math.Min (maxWidth / size.Width, maxHeight / size.Height); + return new CGSize (size.Width * ratio, size.Height * ratio); + } + + private void DrawPreviewBackground (CGRect bounds) + { + var rect = bounds; + this.backgroundFillColor.Set (); + NSGraphics.RectFill (rect); + DrawingUtils.DrawLightShadedBezel (rect, flipped: IsFlipped); + + var x = (int)((rect.Width - bgImageSize.Width) / 2); + var y = (int)((rect.Height - bgImageSize.Height) / 2); + + + this.bgImage.Draw (new CGPoint (x, y), CGRect.Empty, NSCompositingOperation.SourceIn, 1); + this.previewBorderColor.Set (); + NSGraphics.FrameRectWithWidth (new CGRect (x, y, this.bgImage.Size.Width, this.bgImage.Size.Height), 1); + } + + private void StartAnimation () + { + if (this.currentAnimation != null) + return; + const int LowerBound = 90; + const int UpperBound = 110; + const int TimeStep = 50; + + this.currentAnimationRawValue = (UpperBound + LowerBound) / 2 - 1 - LowerBound; + + this.currentAnimation = NSTimer.CreateRepeatingScheduledTimer (TimeSpan.FromMilliseconds (TimeStep), _ => { + // A oscillating function based on abs(x) which values goes linearly from LowerBound to UpperBound + this.currentAnimationRawValue = ((this.currentAnimationRawValue + 1 + (UpperBound - LowerBound)) % (2 * UpperBound + 1 - 2 * LowerBound)) - (UpperBound - LowerBound); + this.currentAnimationValue = Math.Abs (this.currentAnimationRawValue) + LowerBound; + NeedsDisplay = true; + }); + } + + private void StopAnimation () + { + if (this.currentAnimation == null) + return; + this.currentAnimation.Dispose (); + this.currentAnimation = null; + } + + private Size GetWindowSize (CGSize canvasSize) + { + var baseWidth = (int)(canvasSize.Width / 2); + var baseHeight = (int)(canvasSize.Height / 2); + + if (this.currentAnimation != null) { + var adder = this.currentAnimationValue - 100; + baseWidth += adder; + baseHeight += adder; + } + + return new Size (baseWidth, baseHeight); + } + + public static RectangleF GetElementRectForWindowAndMask (RectangleF window, AutoResizingFlags mask) + { + const int Offset = 5; + var baseHeight = 10.0; + if (mask.HasFlag (AutoResizingFlags.FlexibleDimensions) || mask.HasFlag (AutoResizingFlags.FlexibleHeight)) { + baseHeight = window.Size.Height / 2.0; + if (!mask.HasFlag (AutoResizingFlags.FlexibleTopMargin) && !mask.HasFlag (AutoResizingFlags.FlexibleMargins)) + baseHeight += window.Size.Height / 4.0 - Offset; + if (!mask.HasFlag (AutoResizingFlags.FlexibleBottomMargin) && !mask.HasFlag (AutoResizingFlags.FlexibleMargins)) + baseHeight += window.Size.Height / 4.0 - Offset; + } + + var baseWidth = 10.0; + if (mask.HasFlag (AutoResizingFlags.FlexibleDimensions) || mask.HasFlag (AutoResizingFlags.FlexibleWidth)) { + baseWidth = window.Size.Width / 2.0; + if (!mask.HasFlag (AutoResizingFlags.FlexibleLeftMargin) && !mask.HasFlag (AutoResizingFlags.FlexibleMargins)) + baseWidth += window.Size.Width / 4.0 - Offset; + if (!mask.HasFlag (AutoResizingFlags.FlexibleRightMargin) && !mask.HasFlag (AutoResizingFlags.FlexibleMargins)) + baseWidth += window.Size.Width / 4.0 - Offset; + } + + double left = Offset; + if (mask.HasFlag (AutoResizingFlags.FlexibleLeftMargin) || mask.HasFlag (AutoResizingFlags.FlexibleMargins)) { + if (mask.HasFlag (AutoResizingFlags.FlexibleRightMargin) || mask.HasFlag (AutoResizingFlags.FlexibleMargins)) + left = (window.Size.Width - baseWidth) / 2.0; + else + left = window.Size.Width - Offset - baseWidth; + } + + double top = Offset; + if (mask.HasFlag (AutoResizingFlags.FlexibleTopMargin) || mask.HasFlag (AutoResizingFlags.FlexibleMargins)) { + if (mask.HasFlag (AutoResizingFlags.FlexibleBottomMargin) || mask.HasFlag (AutoResizingFlags.FlexibleMargins)) + top = (window.Size.Height - baseHeight) / 2.0; + else + top = window.Size.Height - Offset - baseHeight; + } + + return new RectangleF ((float)(window.X + left), + (float)(window.Y + top), + (float)(baseWidth), + (float)(baseHeight)); + } + } +} diff --git a/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingView.cs b/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingView.cs new file mode 100644 index 0000000..4b3f843 --- /dev/null +++ b/Xamarin.PropertyEditing.Mac/Controls/AutoResizing/AutoResizingView.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using AppKit; +using CoreGraphics; + +namespace Xamarin.PropertyEditing.Mac +{ + internal class AutoResizingView : NSView + { + private static NSImage defaultBackground; + private readonly UnfocusableTextField maskLabel; + private readonly UnfocusableTextField previewLabel; + private bool enabled; + private IHostResourceProvider hostResources; + + internal AutoResizingMaskView MaskView { + get; + private set; + } + + internal AutoResizingPreviewView PreviewView { + get; + private set; + } + + public bool Enabled { + get { return this.enabled; } + + internal set { + if (this.enabled == value) + return; + + this.enabled = value; + + MaskView.Enabled = this.enabled; + + PreviewView.Enabled = this.enabled; + } + } + + private const string DeviceFrameName = "pe-device-frame"; + + public AutoResizingView (IHostResourceProvider hostResources) + { + if (hostResources == null) + throw new ArgumentNullException (nameof (hostResources)); + + this.hostResources = hostResources; + + MaskView = new AutoResizingMaskView { + TranslatesAutoresizingMaskIntoConstraints = false, + }; + + AddSubview (MaskView); + + PreviewView = new AutoResizingPreviewView (GetBackgroundImage ()) { + TranslatesAutoresizingMaskIntoConstraints = false, + }; + + AddSubview (PreviewView); + + this.maskLabel = new UnfocusableTextField { + Font = NSFont.FromFontName (PropertyEditorControl.DefaultFontName, PropertyEditorControl.DefaultDescriptionLabelFontSize), + StringValue = Properties.Resources.Autosizing.ToUpper (), + TranslatesAutoresizingMaskIntoConstraints = false, + }; + + AddSubview (this.maskLabel); + + MaskView.MaskChanged += MaskChanged; + + this.previewLabel = new UnfocusableTextField { + Font = NSFont.FromFontName (PropertyEditorControl.DefaultFontName, PropertyEditorControl.DefaultDescriptionLabelFontSize), + StringValue = Properties.Resources.Example.ToUpper (), + TranslatesAutoresizingMaskIntoConstraints = false + }; + + AddSubview (this.previewLabel); + + AddConstraints (new NSLayoutConstraint[] { + NSLayoutConstraint.Create (MaskView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, this, NSLayoutAttribute.Left, 1, 0), + NSLayoutConstraint.Create (MaskView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this, NSLayoutAttribute.Top, 1, 0), + NSLayoutConstraint.Create (MaskView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, this, NSLayoutAttribute.Width, 1, 0), + + NSLayoutConstraint.Create (this.maskLabel, NSLayoutAttribute.Top, NSLayoutRelation.Equal, MaskView, NSLayoutAttribute.Bottom, 1, 0), + NSLayoutConstraint.Create (this.maskLabel, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1f, 18), + NSLayoutConstraint.Create (this.maskLabel, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, MaskView, NSLayoutAttribute.CenterX, 1, 0), + + NSLayoutConstraint.Create (PreviewView, NSLayoutAttribute.Left, NSLayoutRelation.Equal, MaskView, NSLayoutAttribute.Left, 1, 0), + NSLayoutConstraint.Create (PreviewView, NSLayoutAttribute.Top, NSLayoutRelation.Equal, this.maskLabel, NSLayoutAttribute.Bottom, 1, 5), + NSLayoutConstraint.Create (PreviewView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, MaskView, NSLayoutAttribute.Width, 1, 0), + + NSLayoutConstraint.Create (this.previewLabel, NSLayoutAttribute.Top, NSLayoutRelation.Equal, PreviewView, NSLayoutAttribute.Bottom, 1, 0), + NSLayoutConstraint.Create (this.previewLabel, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1f, 18), + NSLayoutConstraint.Create (this.previewLabel, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, PreviewView, NSLayoutAttribute.CenterX, 1, 0), + }); + + AppearanceChanged (); + } + + #region Overrriden Methods and Properties + + + public override bool IsFlipped { + get { return true; } + } + + public override CGSize IntrinsicContentSize { + get { return new CGSize (-1, 90); } + } + + public sealed override void ViewDidChangeEffectiveAppearance () + { + base.ViewDidChangeEffectiveAppearance (); + + AppearanceChanged (); + } + + #endregion + + private void MaskChanged (object sender, EventArgs e) + { + PreviewView.Mask = MaskView.Mask; + } + + internal void UpdateAccessibilityValues () + { + if (MaskView != null) { + MaskView.AccessibilityEnabled = this.enabled; + MaskView.AccessibilityTitle = Properties.Resources.AccessibilityMaskView; + } + + if (PreviewView != null) { + PreviewView.AccessibilityEnabled = this.enabled; + PreviewView.AccessibilityTitle = Properties.Resources.AccessibilityPreviewMaskView; + } + } + + private void AppearanceChanged () + { + NSColor labelColor = this.hostResources.GetNamedColor (NamedResources.DescriptionLabelColor); + this.maskLabel.TextColor = labelColor; + this.previewLabel.TextColor = labelColor; + } + + public NSImage GetBackgroundImage () + { + if (defaultBackground == null) { + defaultBackground = this.hostResources.GetNamedImage (DeviceFrameName); + // The workaround method isn't available in MonoMac +#pragma warning disable 0618 + defaultBackground.Flipped = true; +#pragma warning restore 0618 + } + return defaultBackground; + } + } + + internal struct Line + { + public CGPoint P1; + public CGPoint P2; + } + + internal static class DrawUtils + { + public static void DrawStraightLine (CGPoint p1, CGPoint p2, int[] dashes = null) + { + foreach (var segment in GetSegments (p1, p2, dashes)) { + var line = RectForLine (segment.P1, segment.P2); + NSGraphics.RectFill (line); + } + } + + public static IEnumerable<Line> GetSegments (CGPoint p1, CGPoint p2, int[] dashes) + { + if (dashes == null) { + yield return new Line { P1 = p1, P2 = p2 }; + yield break; + } + + int axisMultiplier = GetAxisAdder (p1, p2); + int initialOrder = GlobalOrder (p1, p2); + var currentStart = p1; + var currentEnd = p1; + int dashIndex = 0; + + while (GlobalOrder (currentEnd, p2) == initialOrder) { + var dash = dashes[dashIndex]; + currentEnd = new CGPoint (currentEnd.X + (dash * (short)(axisMultiplier >> 16)), + currentEnd.Y + (dash * (short)(axisMultiplier & 0xFFFF))); + + // if index is odd, we are skipping drawing and repositioning ourselves + if ((dashIndex % 2) == 1) { + currentStart = currentEnd; + dashIndex = (dashIndex + 1) % dashes.Length; + continue; + } + + if (GlobalOrder (currentEnd, p2) == initialOrder) + yield return new Line { P1 = currentStart, P2 = currentEnd }; + dashIndex = (dashIndex + 1) % dashes.Length; + } + + yield return new Line { P1 = currentStart, P2 = p2 }; + } + + private static int GlobalOrder (CGPoint p1, CGPoint p2) + { + return (p1.X < p2.X ? 1 : 0) << 1 | (p1.Y < p2.Y ? 1 : 0); + } + + private static int GetAxisAdder (CGPoint p1, CGPoint p2) + { + if (p1.Y == p2.Y) + return p1.X < p2.X ? ((short)1) << 16 : ((short)-1) << 16; + else + return p1.Y < p2.Y ? ((short)1) : ((short)-1); + } + + private static CGRect RectForLine (CGPoint p1, CGPoint p2) + { + return new CGRect (p1.X < p2.X ? p1.X : p2.X, + p1.Y < p2.Y ? p1.Y : p2.Y, + Math.Max (1, Math.Abs (p2.X - p1.X)), + Math.Max (1, Math.Abs (p2.Y - p1.Y))); + } + } + + internal static class DrawingUtils + { + private static nfloat[] lightBezelGreys, fullBezelGreys, fullBezelGreysPixelFix; + + public static void UpdateBezelGreys (bool isLightTheme) + { + if (isLightTheme) { + lightBezelGreys = new nfloat[] { .59f, .71f, .71f, .71f }; + fullBezelGreys = new nfloat[] { .71f, .96f, .71f, .96f, .61f, .89f, .96f }; + fullBezelGreysPixelFix = new nfloat[] { .61f, .73f }; + } else { + lightBezelGreys = new nfloat[] { .41f, .29f, .29f, .29f }; + fullBezelGreys = new nfloat[] { .29f, .04f, .29f, .04f, .39f, .11f, .04f }; + fullBezelGreysPixelFix = new nfloat[] { .39f, .27f }; + } + } + + /// <summary> + /// Draws a shaded, 1 pixel-wide, bezel around a rect + /// </summary> + public static CGRect DrawLightShadedBezel (CGRect rect, bool flipped = false) + { + return NSGraphics.DrawTiledRects (rect, rect, + new NSRectEdge[] { !flipped ? NSRectEdge.MaxYEdge : NSRectEdge.MinYEdge, NSRectEdge.MinXEdge, NSRectEdge.MaxXEdge, !flipped ? NSRectEdge.MinYEdge : NSRectEdge.MaxYEdge }, + lightBezelGreys); + } + + /// <summary> + /// Draws a shaded bezel of more than one pixel around a rect. Returned rectangle is the inner usable area. + /// </summary> + public static CGRect DrawFullShadedBezel (CGRect rect, bool flipped) + { + var top = !flipped ? NSRectEdge.MaxYEdge : NSRectEdge.MinYEdge; + var bottom = !flipped ? NSRectEdge.MinYEdge : NSRectEdge.MaxYEdge; + + var inner = NSGraphics.DrawTiledRects (rect, rect, + new NSRectEdge[] { NSRectEdge.MinXEdge, NSRectEdge.MinXEdge, NSRectEdge.MaxXEdge, NSRectEdge.MaxXEdge, top, top, top }, + fullBezelGreys); + // Redo the top and bottom to clear bad pixels + inner.Intersect (NSGraphics.DrawTiledRects (rect, rect, + new NSRectEdge[] { top, bottom }, + fullBezelGreysPixelFix)); + return inner; + } + } +} diff --git a/Xamarin.PropertyEditing.Mac/Controls/AutoResizingMaskEditorControl.cs b/Xamarin.PropertyEditing.Mac/Controls/AutoResizingMaskEditorControl.cs new file mode 100644 index 0000000..b48a267 --- /dev/null +++ b/Xamarin.PropertyEditing.Mac/Controls/AutoResizingMaskEditorControl.cs @@ -0,0 +1,78 @@ +using System; +using AppKit; +using Xamarin.PropertyEditing.ViewModels; + +namespace Xamarin.PropertyEditing.Mac +{ + internal class AutoResizingMaskEditorControl + : PropertyEditorControl<AutoResizingPropertyViewModel> + { + private readonly AutoResizingView sizeInspectorView; + + public AutoResizingMaskEditorControl (IHostResourceProvider hostResources) + : base (hostResources) + { + + this.sizeInspectorView = new AutoResizingView (hostResources) { + TranslatesAutoresizingMaskIntoConstraints = false, + }; + + this.sizeInspectorView.MaskView.MaskChanged += (o, e) => { + ViewModel.Value = this.sizeInspectorView.MaskView.Mask; + }; + + AddSubview (this.sizeInspectorView); + + AddConstraints (new[] { + NSLayoutConstraint.Create (this.sizeInspectorView, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal, this, NSLayoutAttribute.CenterY, 1, 0), + NSLayoutConstraint.Create (this.sizeInspectorView, NSLayoutAttribute.Width, NSLayoutRelation.GreaterThanOrEqual, 1, 70), + NSLayoutConstraint.Create (this.sizeInspectorView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, this, NSLayoutAttribute.Height, 1, -6) + }); + + AppearanceChanged (); + } + + #region Overridden Methods and Properties + + private NSView firstKeyView; + public override NSView FirstKeyView => this.firstKeyView; + private NSView lastKeyView; + public override NSView LastKeyView => this.lastKeyView; + + public override nint GetHeight (EditorViewModel vm) + { + return 200; + } + + protected override void OnViewModelChanged (PropertyViewModel oldModel) + { + base.OnViewModelChanged (oldModel); + + if (ViewModel == null) + return; + + if (this.firstKeyView == null) { + this.firstKeyView = this.sizeInspectorView.MaskView; + this.lastKeyView = this.sizeInspectorView.MaskView; + } + } + + protected override void SetEnabled () + { + this.sizeInspectorView.Enabled = ViewModel.Property.CanWrite; + } + + protected override void UpdateAccessibilityValues () + { + this.sizeInspectorView.AccessibilityEnabled = this.sizeInspectorView.Enabled; + this.sizeInspectorView.AccessibilityTitle = string.Format (Properties.Resources.AccessibilityBoolean, ViewModel.Property.Name); + this.sizeInspectorView.UpdateAccessibilityValues (); + } + + protected override void UpdateValue () + { + this.sizeInspectorView.MaskView.Mask = ViewModel.Value; + } + #endregion + } +} diff --git a/Xamarin.PropertyEditing.Mac/PropertyEditingResource/Contents/Resources/pe-device-frame.png b/Xamarin.PropertyEditing.Mac/PropertyEditingResource/Contents/Resources/pe-device-frame.png Binary files differnew file mode 100644 index 0000000..faa4db9 --- /dev/null +++ b/Xamarin.PropertyEditing.Mac/PropertyEditingResource/Contents/Resources/pe-device-frame.png diff --git a/Xamarin.PropertyEditing.Mac/PropertyEditingResource/Contents/Resources/pe-device-frame~dark.png b/Xamarin.PropertyEditing.Mac/PropertyEditingResource/Contents/Resources/pe-device-frame~dark.png Binary files differnew file mode 100644 index 0000000..faa4db9 --- /dev/null +++ b/Xamarin.PropertyEditing.Mac/PropertyEditingResource/Contents/Resources/pe-device-frame~dark.png diff --git a/Xamarin.PropertyEditing.Mac/PropertyEditorSelector.cs b/Xamarin.PropertyEditing.Mac/PropertyEditorSelector.cs index bff6d97..2d89d3d 100644 --- a/Xamarin.PropertyEditing.Mac/PropertyEditorSelector.cs +++ b/Xamarin.PropertyEditing.Mac/PropertyEditorSelector.cs @@ -61,7 +61,7 @@ namespace Xamarin.PropertyEditing.Mac {typeof (ObjectPropertyViewModel), typeof (ObjectEditorControl)}, {typeof (TypePropertyViewModel), typeof (TypeEditorControl)}, {typeof (CollectionPropertyViewModel), typeof (CollectionInlineEditorControl)}, - + {typeof (AutoResizingPropertyViewModel), typeof (AutoResizingMaskEditorControl)}, }; } } diff --git a/Xamarin.PropertyEditing.Mac/Xamarin.PropertyEditing.Mac.csproj b/Xamarin.PropertyEditing.Mac/Xamarin.PropertyEditing.Mac.csproj index eb49853..dedcb08 100644 --- a/Xamarin.PropertyEditing.Mac/Xamarin.PropertyEditing.Mac.csproj +++ b/Xamarin.PropertyEditing.Mac/Xamarin.PropertyEditing.Mac.csproj @@ -18,6 +18,9 @@ <ProjectReference Include="..\Xamarin.PropertyEditing\Xamarin.PropertyEditing.csproj" />
</ItemGroup>
+ <ItemGroup>
+ <Folder Include="Controls\AutoResizing\" />
+ </ItemGroup>
<Target Name="IncludeIconsInBundle" BeforeTargets="AssignTargetPaths">
<ItemGroup>
<PropertyEditingResourceBundlePath Include="PropertyEditingResource\**\*" />
diff --git a/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs b/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs index 991ef5b..dadfe4c 100644 --- a/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs +++ b/Xamarin.PropertyEditing.Tests/MockControls/MockSampleControl.cs @@ -12,6 +12,7 @@ namespace Xamarin.PropertyEditing.Tests.MockControls public MockSampleControl () { AddProperty<AutoResizingFlags> ("Autoresizing", ReadWrite, valueSources: ValueSources.Local, ignoreEnum: true); + AddProperty<AutoResizingFlags> ("ReadOnlyAutoresizing", ReadOnly, false, valueSources: ValueSources.Local, ignoreEnum: true); AddProperty<TimeSpan> ("TimeSpan", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding); AddProperty<TimeSpan> ("TimeSpanReadOnly", ReadOnly, canWrite: false, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding); AddProperty<bool> ("Boolean", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding); @@ -102,7 +103,7 @@ namespace Xamarin.PropertyEditing.Tests.MockControls AddProperty<FilePath> ("FilePath", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding); AddReadOnlyProperty<FilePath> ("ReadOnlyFilePath", ReadOnly); AddProperty<DateTime> ("DateTime", ReadWrite, valueSources: ValueSources.Local | ValueSources.Resource | ValueSources.Binding); - AddReadOnlyProperty<DateTime> ("ReadDateTime", ReadOnly); + AddReadOnlyProperty<DateTime> ("ReadOnlyDateTime", ReadOnly); AddEvents ("Click", "Hover", "Focus"); diff --git a/Xamarin.PropertyEditing/Common/AutoResizingFlags.cs b/Xamarin.PropertyEditing/Common/AutoResizingFlags.cs index 8c21b55..338c034 100644 --- a/Xamarin.PropertyEditing/Common/AutoResizingFlags.cs +++ b/Xamarin.PropertyEditing/Common/AutoResizingFlags.cs @@ -16,4 +16,4 @@ namespace Xamarin.PropertyEditing.Common FlexibleDimensions = FlexibleHeight | FlexibleWidth, All = FlexibleMargins | FlexibleDimensions } -} +}
\ No newline at end of file diff --git a/Xamarin.PropertyEditing/Properties/Resources.Designer.cs b/Xamarin.PropertyEditing/Properties/Resources.Designer.cs index 2218d26..188dfed 100644 --- a/Xamarin.PropertyEditing/Properties/Resources.Designer.cs +++ b/Xamarin.PropertyEditing/Properties/Resources.Designer.cs @@ -1475,6 +1475,18 @@ namespace Xamarin.PropertyEditing.Properties { } } + public static string AccessibilityMaskView { + get { + return ResourceManager.GetString("AccessibilityMaskView", resourceCulture); + } + } + + public static string AccessibilityPreviewMaskView { + get { + return ResourceManager.GetString("AccessibilityPreviewMaskView", resourceCulture); + } + } + public static string FilterTypePlaceholder { get { return ResourceManager.GetString("FilterTypePlaceholder", resourceCulture); diff --git a/Xamarin.PropertyEditing/Properties/Resources.resx b/Xamarin.PropertyEditing/Properties/Resources.resx index 87e5631..2aebabc 100644 --- a/Xamarin.PropertyEditing/Properties/Resources.resx +++ b/Xamarin.PropertyEditing/Properties/Resources.resx @@ -1039,6 +1039,14 @@ </data> <data name="ObjectTypeLabelNone" xml:space="preserve"> <value>None</value> + </data> + <data name="AccessibilityMaskView" xml:space="preserve"> + <value>{0} Autoresizing Mask Editor</value> + <comment>Editor that allows you to change auto resizing flags</comment> + </data> + <data name="AccessibilityPreviewMaskView" xml:space="preserve"> + <value>{0} Autoresizing Preview View</value> + <comment>Previewer that allows you to view the changes you've made in the Autoresizing Mask Edito</comment> </data> <data name="FilterTypePlaceholder" xml:space="preserve"> <value>Filter Types</value> diff --git a/Xamarin.PropertyEditing/ViewModels/AutoResizingPropertyViewModel.cs b/Xamarin.PropertyEditing/ViewModels/AutoResizingPropertyViewModel.cs index 2f29af8..a0441a8 100644 --- a/Xamarin.PropertyEditing/ViewModels/AutoResizingPropertyViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/AutoResizingPropertyViewModel.cs @@ -154,4 +154,4 @@ namespace Xamarin.PropertyEditing.ViewModels } } } -} +}
\ No newline at end of file diff --git a/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs index 380aea9..5862ad9 100644 --- a/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/PropertiesViewModel.cs @@ -604,27 +604,20 @@ namespace Xamarin.PropertyEditing.ViewModels private PropertyViewModel CreateViewModel (IPropertyInfo property, PropertyVariation variant = null) { PropertyViewModel vm; - if (ViewModelMap.TryGetValue (property.Type, out var vmFactory)) + Type[] interfaces = property.GetType ().GetInterfaces (); + + Type hasPredefinedValues = interfaces.FirstOrDefault (t => t.IsGenericType && t.GetGenericTypeDefinition () == typeof(IHavePredefinedValues<>)); + if (hasPredefinedValues != null) { + bool combinable = (bool) hasPredefinedValues.GetProperty (nameof(IHavePredefinedValues<bool>.IsValueCombinable)).GetValue (property); + Type type = combinable + ? typeof(CombinablePropertyViewModel<>).MakeGenericType (hasPredefinedValues.GenericTypeArguments[0]) + : typeof(PredefinedValuesViewModel<>).MakeGenericType (hasPredefinedValues.GenericTypeArguments[0]); + + vm = (PropertyViewModel) Activator.CreateInstance (type, TargetPlatform, property, this.objEditors, variant); + } else if (ViewModelMap.TryGetValue (property.Type, out var vmFactory)) vm = vmFactory (TargetPlatform, property, this.objEditors, variant); - else { - Type[] interfaces = property.GetType ().GetInterfaces (); - - Type hasPredefinedValues = interfaces.FirstOrDefault (t => - t.IsGenericType && t.GetGenericTypeDefinition () == typeof(IHavePredefinedValues<>)); - if (hasPredefinedValues != null) { - bool combinable = (bool) hasPredefinedValues - .GetProperty (nameof(IHavePredefinedValues<bool>.IsValueCombinable)).GetValue (property); - Type type = combinable - ? typeof(CombinablePropertyViewModel<>).MakeGenericType (hasPredefinedValues - .GenericTypeArguments[0]) - : typeof(PredefinedValuesViewModel<>).MakeGenericType ( - hasPredefinedValues.GenericTypeArguments[0]); - - vm = (PropertyViewModel) Activator.CreateInstance (type, TargetPlatform, property, this.objEditors, - variant); - } else - vm = new StringPropertyViewModel (TargetPlatform, property, this.objEditors, variant); - } + else + vm = new StringPropertyViewModel (TargetPlatform, property, this.objEditors, variant); vm.Parent = this; vm.VariantsChanged += OnVariantsChanged; |