diff options
Diffstat (limited to 'Xamarin.Forms.Platform.MacOS/Renderers')
32 files changed, 4458 insertions, 0 deletions
diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ActivityIndicatorRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ActivityIndicatorRenderer.cs new file mode 100644 index 00000000..2e179161 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ActivityIndicatorRenderer.cs @@ -0,0 +1,70 @@ +using System.ComponentModel; +using System.Drawing; +using AppKit; +using CoreImage; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class ActivityIndicatorRenderer : ViewRenderer<ActivityIndicator, NSProgressIndicator> + { + static CIColorPolynomial s_currentColorFilter; + static NSColor s_currentColor; + + protected override void OnElementChanged(ElementChangedEventArgs<ActivityIndicator> e) + { + if (e.NewElement != null) + { + if (Control == null) + SetNativeControl(new NSProgressIndicator(RectangleF.Empty) { Style = NSProgressIndicatorStyle.Spinning }); + + UpdateColor(); + UpdateIsRunning(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == ActivityIndicator.ColorProperty.PropertyName) + UpdateColor(); + else if (e.PropertyName == ActivityIndicator.IsRunningProperty.PropertyName) + UpdateIsRunning(); + } + + void UpdateColor() + { + var color = Element.Color; + if (s_currentColorFilter == null && color.IsDefault) + return; + + if (color.IsDefault) + Control.ContentFilters = new CIFilter[0]; + + var newColor = Element.Color.ToNSColor(); + if (Equals(s_currentColor, newColor)) + return; + + s_currentColor = newColor; + + s_currentColorFilter = new CIColorPolynomial + { + RedCoefficients = new CIVector(s_currentColor.RedComponent), + BlueCoefficients = new CIVector(s_currentColor.BlueComponent), + GreenCoefficients = new CIVector(s_currentColor.GreenComponent) + }; + + Control.ContentFilters = new CIFilter[] { s_currentColorFilter }; + } + + void UpdateIsRunning() + { + if (Element.IsRunning) + Control.StartAnimation(this); + else + Control.StopAnimation(this); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/BoxViewRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/BoxViewRenderer.cs new file mode 100644 index 00000000..d4e9ad05 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/BoxViewRenderer.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class BoxViewRenderer : ViewRenderer<BoxView, NSView> + { + protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e) + { + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new NSView()); + } + SetBackgroundColor(Element.Color); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if (e.PropertyName == BoxView.ColorProperty.PropertyName) + SetBackgroundColor(Element.BackgroundColor); + else if (e.PropertyName == VisualElement.IsVisibleProperty.PropertyName && Element.IsVisible) + SetNeedsDisplayInRect(Bounds); + } + + protected override void SetBackgroundColor(Color color) + { + if (Element == null || Control == null) + return; + Control.WantsLayer = true; + Control.Layer.BackgroundColor = color.ToCGColor(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ButtonRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ButtonRenderer.cs new file mode 100644 index 00000000..05b87fa2 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ButtonRenderer.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using AppKit; +using Foundation; +using SizeF = CoreGraphics.CGSize; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class ButtonRenderer : ViewRenderer<Button, NSButton> + { + protected override void Dispose(bool disposing) + { + if (Control != null) + Control.Activated -= OnButtonActivated; + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Button> e) + { + base.OnElementChanged(e); + + if (e.NewElement != null) + { + if (Control == null) + { + var btn = new NSButton(); + btn.SetButtonType(NSButtonType.MomentaryPushIn); + SetNativeControl(btn); + + Control.Activated += OnButtonActivated; + } + + UpdateText(); + UpdateFont(); + UpdateBorder(); + UpdateImage(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Button.TextProperty.PropertyName || e.PropertyName == Button.TextColorProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == Button.FontProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Button.BorderWidthProperty.PropertyName || + e.PropertyName == Button.BorderRadiusProperty.PropertyName || + e.PropertyName == Button.BorderColorProperty.PropertyName) + UpdateBorder(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundVisibility(); + else if (e.PropertyName == Button.ImageProperty.PropertyName) + UpdateImage(); + } + + void OnButtonActivated(object sender, EventArgs eventArgs) + { + ((IButtonController)Element)?.SendClicked(); + } + + void UpdateBackgroundVisibility() + { + var model = Element; + var shouldDrawImage = model.BackgroundColor == Color.Default; + if (!shouldDrawImage) + Control.Cell.BackgroundColor = model.BackgroundColor.ToNSColor(); + } + + void UpdateBorder() + { + var uiButton = Control; + var button = Element; + + if (button.BorderColor != Color.Default) + uiButton.Layer.BorderColor = button.BorderColor.ToCGColor(); + + uiButton.Layer.BorderWidth = (float)button.BorderWidth; + uiButton.Layer.CornerRadius = button.BorderRadius; + + UpdateBackgroundVisibility(); + } + + void UpdateFont() + { + Control.Font = Element.Font.ToNSFont(); + } + + async void UpdateImage() + { + IImageSourceHandler handler; + FileImageSource source = Element.Image; + if (source != null && (handler = Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) + { + NSImage uiimage; + try + { + uiimage = await handler.LoadImageAsync(source); + } + catch (OperationCanceledException) + { + uiimage = null; + } + NSButton button = Control; + if (button != null && uiimage != null) + { + button.Image = uiimage; + if (!string.IsNullOrEmpty(button.Title)) + button.ImagePosition = Element.ToNSCellImagePosition(); + } + } + ((IVisualElementController)Element).NativeSizeChanged(); + } + + void UpdateText() + { + var color = Element.TextColor; + if (color == Color.Default) + { + Control.Title = Element.Text ?? ""; + } + else + { + var textWithColor = new NSAttributedString(Element.Text ?? "", foregroundColor: color.ToNSColor()); + Control.AttributedTitle = textWithColor; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/CarouselPageRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/CarouselPageRenderer.cs new file mode 100644 index 00000000..014d503b --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/CarouselPageRenderer.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + [Register("CarouselPageRenderer")] + public class CarouselPageRenderer : NSPageController, IVisualElementRenderer + { + bool _appeared; + bool _disposed; + EventTracker _events; + VisualElementTracker _tracker; + + public CarouselPageRenderer() + { + View = new NSView + { + WantsLayer = true, + Layer = { BackgroundColor = NSColor.White.CGColor } + }; + } + + public CarouselPageRenderer(IntPtr handle) : base(handle) + { + } + + IElementController ElementController => Element; + + IPageController PageController => (IPageController)Element; + + public override nint SelectedIndex + { + get { return base.SelectedIndex; } + set + { + if (base.SelectedIndex == value) + return; + base.SelectedIndex = value; + if (Carousel != null) + Carousel.CurrentPage = (ContentPage)ElementController.LogicalChildren[(int)SelectedIndex]; + } + } + + public VisualElement Element { get; private set; } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return NativeView.GetSizeRequest(widthConstraint, heightConstraint); + } + + public NSView NativeView => View; + + public void SetElement(VisualElement element) + { + VisualElement oldElement = Element; + Element = element; + + Init(); + + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + } + + public void SetElementSize(Size size) + { + Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); + } + + public NSViewController ViewController => this; + + public override void ViewDidAppear() + { + base.ViewDidAppear(); + if (_appeared || _disposed) + return; + + _appeared = true; + PageController.SendAppearing(); + } + + public override void ViewDidDisappear() + { + base.ViewDidDisappear(); + + if (!_appeared || _disposed) + return; + + _appeared = false; + PageController.SendDisappearing(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + if (Carousel != null) + { + Carousel.PropertyChanged -= OnPropertyChanged; + Carousel.PagesChanged -= OnPagesChanged; + } + + Platform.SetRenderer(Element, null); + + if (_appeared) + { + _appeared = false; + PageController?.SendDisappearing(); + } + + if (_events != null) + { + _events.Dispose(); + _events = null; + } + + if (_tracker != null) + { + _tracker.Dispose(); + _tracker = null; + } + + Element = null; + _disposed = true; + } + + base.Dispose(disposing); + } + + void OnElementChanged(VisualElementChangedEventArgs e) + { + ElementChanged?.Invoke(this, e); + } + + void ConfigureNSPageController() + { + TransitionStyle = NSPageControllerTransitionStyle.HorizontalStrip; + } + + CarouselPage Carousel => Element as CarouselPage; + + void Init() + { + Delegate = new PageControllerDelegate(); + + _tracker = new VisualElementTracker(this); + _events = new EventTracker(this); + _events.LoadEvents(View); + + ConfigureNSPageController(); + + UpdateBackground(); + UpdateSource(); + + Carousel.PropertyChanged += OnPropertyChanged; + Carousel.PagesChanged += OnPagesChanged; + } + + void UpdateSource() + { + var pages = new List<NSPageContainer>(); + for (var i = 0; i < ElementController.LogicalChildren.Count; i++) + { + Element element = ElementController.LogicalChildren[i]; + var child = element as ContentPage; + if (child != null) + pages.Add(new NSPageContainer(child, i)); + } + + ArrangedObjects = pages.ToArray(); + UpdateCurrentPage(false); + } + + void OnPagesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateSource(); + } + + void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TabbedPage.CurrentPage)) + UpdateCurrentPage(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackground(); + else if (e.PropertyName == Page.BackgroundImageProperty.PropertyName) + UpdateBackground(); + } + + void UpdateBackground() + { + if (View.Layer == null) + return; + + string bgImage = ((Page)Element).BackgroundImage; + + if (!string.IsNullOrEmpty(bgImage)) + { + View.Layer.BackgroundColor = NSColor.FromPatternImage(NSImage.ImageNamed(bgImage)).CGColor; + return; + } + + Color bgColor = Element.BackgroundColor; + View.Layer.BackgroundColor = bgColor.IsDefault ? NSColor.White.CGColor : bgColor.ToCGColor(); + } + + void UpdateCurrentPage(bool animated = true) + { + ContentPage current = Carousel.CurrentPage; + if (current != null) + { + int index = Carousel.CurrentPage != null ? CarouselPage.GetIndex(Carousel.CurrentPage) : 0; + if (index < 0) + index = 0; + + if (SelectedIndex == index) + return; + + if (animated) + NSAnimationContext.RunAnimation(context => { ((NSPageController)Animator).SelectedIndex = index; }, + CompleteTransition); + else SelectedIndex = index; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/CustomNSTableHeaderView.cs b/Xamarin.Forms.Platform.MacOS/Renderers/CustomNSTableHeaderView.cs new file mode 100644 index 00000000..79628b62 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/CustomNSTableHeaderView.cs @@ -0,0 +1,46 @@ +using AppKit; +using CoreGraphics; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + sealed class CustomNSTableHeaderView : NSTableHeaderView + { + public CustomNSTableHeaderView() : this(0, null) { } + public CustomNSTableHeaderView(double width, IVisualElementRenderer headerRenderer) + { + var view = new NSView { WantsLayer = true, Layer = { BackgroundColor = NSColor.Clear.CGColor } }; + AddSubview(view); + Update(width, headerRenderer); + } + + public void Update(double width, IVisualElementRenderer headerRenderer) + { + double height = 1; + if (headerRenderer != null) + { + var headerView = headerRenderer.Element; + var request = headerView.Measure(double.PositiveInfinity, double.PositiveInfinity, MeasureFlags.IncludeMargins); + height = request.Request.Height; + var bounds = new Rectangle(0, 0, width, height); + Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(headerView, bounds); + headerRenderer.NativeView.Frame = bounds.ToRectangleF(); + AddSubview(headerRenderer.NativeView); + + } + Frame = new CGRect(0, 0, width, height); + } + + //hides default text field + public override NSAttributedString PageHeader => new NSAttributedString(""); + + public override void DrawRect(CGRect dirtyRect) { } + + public override void Layout() + { + foreach (var view in Subviews) + view.Frame = Frame; + base.Layout(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/DatePickerRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/DatePickerRenderer.cs new file mode 100644 index 00000000..f31fa361 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/DatePickerRenderer.cs @@ -0,0 +1,138 @@ +using System; +using System.ComponentModel; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class DatePickerRenderer : ViewRenderer<DatePicker, NSDatePicker> + { + NSDatePicker _picker; + NSColor _defaultTextColor; + NSColor _defaultBackgroundColor; + bool _disposed; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e) + { + base.OnElementChanged(e); + + if (e.OldElement == null) + { + if (Control == null) + { + _picker = new NSDatePicker + { + DatePickerMode = NSDatePickerMode.Single, + TimeZone = new NSTimeZone("UTC"), + DatePickerStyle = NSDatePickerStyle.TextFieldAndStepper, + DatePickerElements = NSDatePickerElementFlags.YearMonthDateDay + }; + _picker.ValidateProposedDateValue += HandleValueChanged; + _defaultTextColor = _picker.TextColor; + _defaultBackgroundColor = _picker.BackgroundColor; + + SetNativeControl(_picker); + } + } + + UpdateDateFromModel(); + UpdateMaximumDate(); + UpdateMinimumDate(); + UpdateTextColor(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == DatePicker.DateProperty.PropertyName || + e.PropertyName == DatePicker.FormatProperty.PropertyName) + UpdateDateFromModel(); + else if (e.PropertyName == DatePicker.MinimumDateProperty.PropertyName) + UpdateMinimumDate(); + else if (e.PropertyName == DatePicker.MaximumDateProperty.PropertyName) + UpdateMaximumDate(); + else if (e.PropertyName == DatePicker.TextColorProperty.PropertyName || + e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateTextColor(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + if (_picker != null) + _picker.ValidateProposedDateValue -= HandleValueChanged; + + _disposed = true; + } + base.Dispose(disposing); + } + + protected override void SetBackgroundColor(Color color) + { + base.SetBackgroundColor(color); + + if (Control == null) + return; + + if (color == Color.Default) + Control.BackgroundColor = _defaultBackgroundColor; + else + Control.BackgroundColor = color.ToNSColor(); + } + + void HandleValueChanged(object sender, NSDatePickerValidatorEventArgs e) + { + if (Control == null || Element == null) + return; + ElementController?.SetValueFromRenderer(DatePicker.DateProperty, _picker.DateValue.ToDateTime().Date); + } + + void OnEnded(object sender, EventArgs eventArgs) + { + ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + } + + void OnStarted(object sender, EventArgs eventArgs) + { + ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + } + + void UpdateDateFromModel() + { + if (Control == null || Element == null) + return; + if (_picker.DateValue.ToDateTime().Date != Element.Date.Date) + _picker.DateValue = Element.Date.ToNSDate(); + } + + void UpdateMaximumDate() + { + if (Control == null || Element == null) + return; + _picker.MaxDate = Element.MaximumDate.ToNSDate(); + } + + void UpdateMinimumDate() + { + if (Control == null || Element == null) + return; + _picker.MinDate = Element.MinimumDate.ToNSDate(); + } + + void UpdateTextColor() + { + if (Control == null || Element == null) + return; + var textColor = Element.TextColor; + + if (textColor.IsDefault || !Element.IsEnabled) + Control.TextColor = _defaultTextColor; + else + Control.TextColor = textColor.ToNSColor(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/DefaultRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/DefaultRenderer.cs new file mode 100644 index 00000000..3d1d7cd0 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/DefaultRenderer.cs @@ -0,0 +1,6 @@ +namespace Xamarin.Forms.Platform.MacOS +{ + internal class DefaultRenderer : VisualElementRenderer<VisualElement> + { + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/EditorRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/EditorRenderer.cs new file mode 100644 index 00000000..75a6020b --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/EditorRenderer.cs @@ -0,0 +1,128 @@ +using System; +using System.ComponentModel; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class EditorRenderer : ViewRenderer<Editor, NSTextField> + { + const string NewLineSelector = "insertNewline"; + bool _disposed; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<Editor> e) + { + base.OnElementChanged(e); + + if (Control == null) + { + SetNativeControl(new NSTextField { UsesSingleLineMode = false }); + Control.Cell.Scrollable = true; + Control.Cell.Wraps = true; + Control.Changed += HandleChanged; + Control.EditingBegan += OnEditingBegan; + Control.EditingEnded += OnEditingEnded; + Control.DoCommandBySelector = (control, textView, commandSelector) => + { + var result = false; + if (commandSelector.Name.StartsWith(NewLineSelector, StringComparison.InvariantCultureIgnoreCase)) + { + textView.InsertText(new NSString(Environment.NewLine)); + result = true; + } + return result; + }; + } + + if (e.NewElement == null) return; + UpdateText(); + UpdateFont(); + UpdateTextColor(); + UpdateEditable(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Editor.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateEditable(); + else if (e.PropertyName == Editor.TextColorProperty.PropertyName) + UpdateTextColor(); + else if (e.PropertyName == Editor.FontAttributesProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Editor.FontFamilyProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Editor.FontSizeProperty.PropertyName) + UpdateFont(); + } + + protected override void SetBackgroundColor(Color color) + { + if (Control == null) + return; + + Control.BackgroundColor = color == Color.Default ? NSColor.Clear : color.ToNSColor(); + + base.SetBackgroundColor(color); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + if (Control != null) + { + Control.Changed -= HandleChanged; + Control.EditingBegan -= OnEditingBegan; + Control.EditingEnded -= OnEditingEnded; + } + } + base.Dispose(disposing); + } + + void HandleChanged(object sender, EventArgs e) + { + ElementController.SetValueFromRenderer(Editor.TextProperty, Control.StringValue); + } + + void OnEditingEnded(object sender, EventArgs eventArgs) + { + Element.SetValue(VisualElement.IsFocusedPropertyKey, false); + Element.SendCompleted(); + } + + void OnEditingBegan(object sender, EventArgs eventArgs) + { + ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + } + + void UpdateEditable() + { + Control.Editable = Element.IsEnabled; + } + + void UpdateFont() + { + Control.Font = Element.ToNSFont(); + } + + void UpdateText() + { + if (Control.StringValue != Element.Text) + Control.StringValue = Element.Text ?? string.Empty; + } + + void UpdateTextColor() + { + var textColor = Element.TextColor; + + Control.TextColor = textColor.IsDefault ? NSColor.Black : textColor.ToNSColor(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/EntryRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/EntryRenderer.cs new file mode 100644 index 00000000..5fb65542 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/EntryRenderer.cs @@ -0,0 +1,205 @@ +using System; +using System.ComponentModel; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class EntryRenderer : ViewRenderer<Entry, NSTextField> + { + class BoolEventArgs : EventArgs + { + public BoolEventArgs(bool value) + { + Value = value; + } + public bool Value + { + get; + private set; + } + } + class FormsNSTextField : NSTextField + { + public EventHandler<BoolEventArgs> FocusChanged; + public override bool ResignFirstResponder() + { + FocusChanged?.Invoke(this, new BoolEventArgs(false)); + return base.ResignFirstResponder(); + } + public override bool BecomeFirstResponder() + { + FocusChanged?.Invoke(this, new BoolEventArgs(true)); + return base.BecomeFirstResponder(); + } + } + + bool _disposed; + NSColor _defaultTextColor; + + IElementController ElementController => Element; + + IEntryController EntryController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<Entry> e) + { + base.OnElementChanged(e); + + if (Control == null) + { + NSTextField textField; + if (e.NewElement.IsPassword) + textField = new NSSecureTextField(); + else + { + textField = new FormsNSTextField(); + (textField as FormsNSTextField).FocusChanged += TextFieldFocusChanged; + } + + SetNativeControl(textField); + + _defaultTextColor = textField.TextColor; + + textField.Changed += OnChanged; + textField.EditingBegan += OnEditingBegan; + textField.EditingEnded += OnEditingEnded; + } + + if (e.NewElement != null) + { + UpdatePlaceholder(); + UpdateText(); + UpdateColor(); + UpdateFont(); + UpdateAlignment(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Entry.PlaceholderProperty.PropertyName || + e.PropertyName == Entry.PlaceholderColorProperty.PropertyName) + UpdatePlaceholder(); + else if (e.PropertyName == Entry.IsPasswordProperty.PropertyName) + UpdatePassword(); + else if (e.PropertyName == Entry.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == Entry.TextColorProperty.PropertyName) + UpdateColor(); + else if (e.PropertyName == Entry.HorizontalTextAlignmentProperty.PropertyName) + UpdateAlignment(); + else if (e.PropertyName == Entry.FontAttributesProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Entry.FontFamilyProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == Entry.FontSizeProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + { + UpdateColor(); + UpdatePlaceholder(); + } + + base.OnElementPropertyChanged(sender, e); + } + + protected override void SetBackgroundColor(Color color) + { + if (Control == null) + return; + Control.BackgroundColor = color == Color.Default ? NSColor.Clear : color.ToNSColor(); + + base.SetBackgroundColor(color); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + if (Control != null) + { + Control.EditingBegan -= OnEditingBegan; + Control.Changed -= OnChanged; + Control.EditingEnded -= OnEditingEnded; + var formsNSTextField = (Control as FormsNSTextField); + if (formsNSTextField != null) + formsNSTextField.FocusChanged -= TextFieldFocusChanged; + } + } + + base.Dispose(disposing); + } + void TextFieldFocusChanged(object sender, BoolEventArgs e) + { + ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, e.Value); + } + + void OnEditingBegan(object sender, EventArgs e) + { + ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + } + + void OnChanged(object sender, EventArgs eventArgs) + { + ElementController.SetValueFromRenderer(Entry.TextProperty, Control.StringValue); + } + + void OnEditingEnded(object sender, EventArgs e) + { + ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + EntryController?.SendCompleted(); + } + + void UpdateAlignment() + { + Control.Alignment = Element.HorizontalTextAlignment.ToNativeTextAlignment(); + } + + void UpdateColor() + { + var textColor = Element.TextColor; + + if (textColor.IsDefault || !Element.IsEnabled) + Control.TextColor = _defaultTextColor; + else + Control.TextColor = textColor.ToNSColor(); + } + + void UpdatePassword() + { + if (Element.IsPassword && (Control is NSSecureTextField)) + return; + if (!Element.IsPassword && !(Control is NSSecureTextField)) + return; + } + + void UpdateFont() + { + Control.Font = Element.ToNSFont(); + } + + void UpdatePlaceholder() + { + var formatted = (FormattedString)Element.Placeholder; + + if (formatted == null) + return; + + var targetColor = Element.PlaceholderColor; + + // Placeholder default color is 70% gray + // https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UITextField_Class/index.html#//apple_ref/occ/instp/UITextField/placeholder + + var color = Element.IsEnabled && !targetColor.IsDefault ? targetColor : ColorExtensions.SeventyPercentGrey.ToColor(); + + Control.PlaceholderAttributedString = formatted.ToAttributed(Element, color); + } + + void UpdateText() + { + // ReSharper disable once RedundantCheckBeforeAssignment + if (Control.StringValue != Element.Text) + Control.StringValue = Element.Text ?? string.Empty; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/FrameRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/FrameRenderer.cs new file mode 100644 index 00000000..7cbabc12 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/FrameRenderer.cs @@ -0,0 +1,57 @@ +using System.ComponentModel; +using System.Drawing; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class FrameRenderer : VisualElementRenderer<Frame> + { + protected override void OnElementChanged(ElementChangedEventArgs<Frame> e) + { + base.OnElementChanged(e); + + if (e.NewElement != null) + SetupLayer(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName || + e.PropertyName == Xamarin.Forms.Frame.OutlineColorProperty.PropertyName || + e.PropertyName == Xamarin.Forms.Frame.HasShadowProperty.PropertyName) + SetupLayer(); + } + + void SetupLayer() + { + Layer.CornerRadius = 5; + if (Element.BackgroundColor == Color.Default) + Layer.BackgroundColor = NSColor.White.CGColor; + else + Layer.BackgroundColor = Element.BackgroundColor.ToCGColor(); + + if (Element.HasShadow) + { + Layer.ShadowRadius = 5; + Layer.ShadowColor = NSColor.Black.CGColor; + Layer.ShadowOpacity = 0.8f; + Layer.ShadowOffset = new SizeF(); + } + else + Layer.ShadowOpacity = 0; + + if (Element.OutlineColor == Color.Default) + Layer.BorderColor = NSColor.Clear.CGColor; + else + { + Layer.BorderColor = Element.OutlineColor.ToCGColor(); + Layer.BorderWidth = 1; + } + + Layer.RasterizationScale = NSScreen.MainScreen.BackingScaleFactor; + Layer.ShouldRasterize = true; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ImageRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ImageRenderer.cs new file mode 100644 index 00000000..090df9a6 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ImageRenderer.cs @@ -0,0 +1,117 @@ +using System; +using System.ComponentModel; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class ImageRenderer : ViewRenderer<Image, NSImageView> + { + bool _isDisposed; + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + if (disposing) + { + NSImage oldUIImage; + if (Control != null && (oldUIImage = Control.Image) != null) + { + oldUIImage.Dispose(); + } + } + + _isDisposed = true; + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<Image> e) + { + if (Control == null) + { + var imageView = new FormsNSImageView(); + SetNativeControl(imageView); + } + + if (e.NewElement != null) + { + SetAspect(); + SetImage(e.OldElement); + SetOpacity(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if (e.PropertyName == Image.SourceProperty.PropertyName) + SetImage(); + else if (e.PropertyName == Image.IsOpaqueProperty.PropertyName) + SetOpacity(); + else if (e.PropertyName == Image.AspectProperty.PropertyName) + SetAspect(); + } + + void SetAspect() + { + //TODO: Implement set Image Aspect + //Control.ContentMode = Element.Aspect.ToUIViewContentMode(); + } + + async void SetImage(Image oldElement = null) + { + var source = Element.Source; + + if (oldElement != null) + { + var oldSource = oldElement.Source; + if (Equals(oldSource, source)) + return; + + var imageSource = oldSource as FileImageSource; + if (imageSource != null && source is FileImageSource && imageSource.File == ((FileImageSource)source).File) + return; + + Control.Image = null; + } + + IImageSourceHandler handler; + + ((IImageController)Element).SetIsLoading(true); + + if (source != null && (handler = Registrar.Registered.GetHandler<IImageSourceHandler>(source.GetType())) != null) + { + NSImage uiimage; + try + { + uiimage = await handler.LoadImageAsync(source, scale: (float)NSScreen.MainScreen.BackingScaleFactor); + } + catch (OperationCanceledException) + { + uiimage = null; + } + + var imageView = Control; + if (imageView != null) + imageView.Image = uiimage; + + if (!_isDisposed) + ((IVisualElementController)Element).NativeSizeChanged(); + } + else + Control.Image = null; + + if (!_isDisposed) + ((IImageController)Element).SetIsLoading(false); + } + + void SetOpacity() + { + (Control as FormsNSImageView)?.SetIsOpaque(Element.IsOpaque); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/LayoutRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/LayoutRenderer.cs new file mode 100644 index 00000000..1be5a1ad --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/LayoutRenderer.cs @@ -0,0 +1,39 @@ +using System; +using CoreGraphics; + +namespace Xamarin.Forms.Platform.MacOS +{ + internal class LayoutRenderer : DefaultRenderer + { + CGRect _bounds; + + public override void Layout() + { + base.Layout(); + + if (_bounds == Bounds) + return; + + _bounds = Bounds; + + //when the layout changes we might need to update the children position based in our new size, + //this is only needed in MacOS because of the inversion of the Y coordinate. + //Forms layout system doesn't know we need to relayout the other items ,(first ones for example) + //so we do it here + for (int i = 0; i < Subviews.Length; i++) + { + var item = Subviews[i] as IVisualElementRenderer; + if (item == null) + continue; + var oldFrame = item.NativeView.Frame; + + var newY = Math.Max(0, (float)(Element.Height - item.Element.Height - item.Element.Y)); + if (oldFrame.Y == newY) + continue; + var newPosition = new CGPoint(oldFrame.X, newY); + item.NativeView.Frame = new CGRect(newPosition, oldFrame.Size); + Console.WriteLine($"New Frame - {item.NativeView.Frame}"); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ListViewDataSource.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ListViewDataSource.cs new file mode 100644 index 00000000..6742053c --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ListViewDataSource.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + internal class ListViewDataSource : NSTableViewSource + { + IVisualElementRenderer _prototype; + const int DefaultItemTemplateId = 1; + static int s_dataTemplateIncrementer = 2; // lets start at not 0 because + static int s_sectionCount; + readonly nfloat _defaultSectionHeight; + readonly Dictionary<DataTemplate, int> _templateToId = new Dictionary<DataTemplate, int>(); + readonly NSTableView _nsTableView; + protected readonly ListView List; + + IListViewController Controller => List; + + ITemplatedItemsView<Cell> TemplatedItemsView => List; + + bool _selectionFromNative; + + public virtual bool IsGroupingEnabled => List.IsGroupingEnabled; + + public Dictionary<int, int> Counts { get; set; } + + public ListViewDataSource(ListViewDataSource source) + { + List = source.List; + _nsTableView = source._nsTableView; + _defaultSectionHeight = source._defaultSectionHeight; + _selectionFromNative = source._selectionFromNative; + Counts = new Dictionary<int, int>(); + } + + public ListViewDataSource(ListView list, NSTableView tableView) + { + List = list; + List.ItemSelected += OnItemSelected; + _nsTableView = tableView; + Counts = new Dictionary<int, int>(); + } + + public void Update() + { + _nsTableView.ReloadData(); + } + + public void OnRowClicked() + { + var selectedRow = _nsTableView.SelectedRow; + if (selectedRow == -1) + return; + + Cell cell = null; + NSIndexPath indexPath = GetPathFromRow(selectedRow, ref cell); + + if (cell == null) + return; + + _selectionFromNative = true; + Controller.NotifyRowTapped((int)indexPath.Section, (int)indexPath.Item, cell); + } + + + public void OnItemSelected(object sender, SelectedItemChangedEventArgs eventArg) + { + if (_selectionFromNative) + { + _selectionFromNative = false; + return; + } + + var location = TemplatedItemsView.TemplatedItems.GetGroupAndIndexOfItem(eventArg.SelectedItem); + if (location.Item1 == -1 || location.Item2 == -1) + { + var row = _nsTableView.SelectedRow; + int groupIndex = 1; + var selectedIndexPath = NSIndexPath.FromItemSection(row, groupIndex); + if (selectedIndexPath != null) + _nsTableView.DeselectRow(selectedIndexPath.Item); + return; + } + + var rowId = location.Item2; + + _nsTableView.SelectRow(rowId, false); + } + + public override bool IsGroupRow(NSTableView tableView, nint row) + { + if (!IsGroupingEnabled) + return false; + + int sectionIndex; + bool isGroupHeader; + int itemIndexInSection; + + GetComputedIndexes(row, out sectionIndex, out itemIndexInSection, out isGroupHeader); + return isGroupHeader; + } + + public override bool ShouldSelectRow(NSTableView tableView, nint row) + { + return !IsGroupRow(tableView, row); + } + + public override nfloat GetRowHeight(NSTableView tableView, nint row) + { + if (!List.HasUnevenRows) + return List.RowHeight == -1 ? ListViewRenderer.DefaultRowHeight : List.RowHeight; + + Cell cell = null; + GetPathFromRow(row, ref cell); + + return CalculateHeightForCell(tableView, cell); + } + + public override nint GetRowCount(NSTableView tableView) + { + var templatedItems = TemplatedItemsView.TemplatedItems; + nint count = 0; + + if (!IsGroupingEnabled) + { + count = templatedItems.Count; + } + else + { + var sections = templatedItems.Count; + for (int i = 0; i < sections; i++) + { + var group = (IList)((IList)templatedItems)[i]; + count += group.Count + 1; + } + s_sectionCount = sections; + } + return count; + } + + public override NSView GetViewForItem(NSTableView tableView, NSTableColumn tableColumn, nint row) + { + var sectionIndex = 0; + var itemIndexInSection = (int)row; + Cell cell; + + var isHeader = false; + + if (IsGroupingEnabled) + GetComputedIndexes(row, out sectionIndex, out itemIndexInSection, out isHeader); + + var indexPath = NSIndexPath.FromItemSection(itemIndexInSection, sectionIndex); + var templateId = isHeader ? "headerCell" : TemplateIdForPath(indexPath).ToString(); + + NSView nativeCell; + + var cachingStrategy = Controller.CachingStrategy; + if (cachingStrategy == ListViewCachingStrategy.RetainElement) + { + cell = GetCellForPath(indexPath, isHeader); + nativeCell = CellNSView.GetNativeCell(tableView, cell, templateId, isHeader); + } + else if (cachingStrategy == ListViewCachingStrategy.RecycleElement) + { + nativeCell = tableView.MakeView(templateId, tableView); + if (nativeCell == null) + { + cell = GetCellForPath(indexPath, isHeader); + nativeCell = CellNSView.GetNativeCell(tableView, cell, templateId, isHeader, true); + } + else + { + var templatedList = TemplatedItemsView.TemplatedItems.GetGroup(sectionIndex); + cell = (Cell)((INativeElementView)nativeCell).Element; + ICellController controller = cell; + controller.SendDisappearing(); + templatedList.UpdateContent(cell, itemIndexInSection); + controller.SendAppearing(); + } + } + else + throw new NotSupportedException(); + return nativeCell; + } + + protected virtual Cell GetCellForPath(NSIndexPath indexPath, bool isGroupHeader) + { + var templatedItems = TemplatedItemsView.TemplatedItems; + if (IsGroupingEnabled) + templatedItems = (TemplatedItemsList<ItemsView<Cell>, Cell>)((IList)templatedItems)[(int)indexPath.Section]; + + var cell = isGroupHeader ? templatedItems.HeaderContent : templatedItems[(int)indexPath.Item]; + return cell; + } + + int TemplateIdForPath(NSIndexPath indexPath) + { + var itemTemplate = List.ItemTemplate; + var selector = itemTemplate as DataTemplateSelector; + if (selector == null) + return DefaultItemTemplateId; + + var templatedList = TemplatedItemsView.TemplatedItems; + if (List.IsGroupingEnabled) + templatedList = (TemplatedItemsList<ItemsView<Cell>, Cell>)((IList)templatedList)[(int)indexPath.Section]; + + var item = templatedList.ListProxy[(int)indexPath.Item]; + + itemTemplate = selector.SelectTemplate(item, List); + int key; + if (!_templateToId.TryGetValue(itemTemplate, out key)) + { + s_dataTemplateIncrementer++; + key = s_dataTemplateIncrementer; + _templateToId[itemTemplate] = key; + } + return key; + } + + NSIndexPath GetPathFromRow(nint row, ref Cell cell) + { + var sectionIndex = 0; + bool isGroupHeader = false; + int itemIndexInSection; + if (IsGroupingEnabled) + GetComputedIndexes(row, out sectionIndex, out itemIndexInSection, out isGroupHeader); + else + itemIndexInSection = (int)row; + NSIndexPath indexPath = NSIndexPath.FromItemSection(itemIndexInSection, sectionIndex); + cell = GetCellForPath(indexPath, isGroupHeader); + return indexPath; + } + + nfloat CalculateHeightForCell(NSTableView tableView, Cell cell) + { + var viewCell = cell as ViewCell; + double renderHeight; + if (List.RowHeight == -1 && viewCell?.View != null) + { + var target = viewCell.View; + if (_prototype == null) + { + _prototype = Platform.CreateRenderer(target); + Platform.SetRenderer(target, _prototype); + } + else + { + _prototype.SetElement(target); + Platform.SetRenderer(target, _prototype); + } + + var req = target.Measure(tableView.Frame.Width, double.PositiveInfinity, MeasureFlags.IncludeMargins); + + target.ClearValue(Platform.RendererProperty); + foreach (var descendant in target.Descendants()) + descendant.ClearValue(Platform.RendererProperty); + + renderHeight = req.Request.Height; + } + else + { + renderHeight = cell.RenderHeight; + } + + return renderHeight > 0 ? (nfloat)renderHeight : ListViewRenderer.DefaultRowHeight; + } + + void GetComputedIndexes(nint row, out int sectionIndex, out int itemIndexInSection, out bool isHeader) + { + var templatedItems = TemplatedItemsView.TemplatedItems; + var totalItems = 0; + isHeader = false; + sectionIndex = 0; + itemIndexInSection = 0; + + for (int i = 0; i < s_sectionCount; i++) + { + var group = (IList)((IList)templatedItems)[i]; + var itemsInSection = group.Count + 1; + + if (row < totalItems + itemsInSection) + { + sectionIndex = i; + itemIndexInSection = (int)row - totalItems; + isHeader = itemIndexInSection == 0; + if (isHeader) + itemIndexInSection = -1; + else + itemIndexInSection = itemIndexInSection - 1; + break; + } + totalItems += itemsInSection; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ListViewRenderer.cs new file mode 100644 index 00000000..08265b33 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ListViewRenderer.cs @@ -0,0 +1,342 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using AppKit; +using Foundation; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class ListViewRenderer : ViewRenderer<ListView, NSView> + { + bool _disposed; + NSTableView _table; + ListViewDataSource _dataSource; + IVisualElementRenderer _headerRenderer; + IVisualElementRenderer _footerRenderer; + + IListViewController Controller => Element; + + ITemplatedItemsView<Cell> TemplatedItemsView => Element; + + public const int DefaultRowHeight = 16; + + public NSTableView NativeTableView => _table; + + public override void ViewWillDraw() + { + UpdateHeader(); + base.ViewWillDraw(); + } + + protected virtual NSTableView CreateNSTableView(ListView list) + { + NSTableView table = new FormsNSTableView().AsListViewLook(); + table.Source = _dataSource = new ListViewDataSource(list, table); + return table; + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + + var viewsToLookAt = new Stack<NSView>(Subviews); + while (viewsToLookAt.Count > 0) + { + var view = viewsToLookAt.Pop(); + var viewCellRenderer = view as ViewCellNSView; + if (viewCellRenderer != null) + viewCellRenderer.Dispose(); + else + { + foreach (var child in view.Subviews) + viewsToLookAt.Push(child); + } + } + + if (Element != null) + { + var templatedItems = TemplatedItemsView.TemplatedItems; + templatedItems.CollectionChanged -= OnCollectionChanged; + templatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; + } + } + + if (disposing) + { + ClearHeader(); + if (_footerRenderer != null) + { + Platform.DisposeModelAndChildrenRenderers(_footerRenderer.Element); + _footerRenderer = null; + } + } + + base.Dispose(disposing); + } + + protected override void SetBackgroundColor(Color color) + { + base.SetBackgroundColor(color); + if (_table == null) + return; + + _table.BackgroundColor = color.ToNSColor(NSColor.White); + } + + protected override void OnElementChanged(ElementChangedEventArgs<ListView> e) + { + if (e.OldElement != null) + { + var controller = (IListViewController)e.OldElement; + controller.ScrollToRequested -= OnScrollToRequested; + + var templatedItems = ((ITemplatedItemsView<Cell>)e.OldElement).TemplatedItems; + templatedItems.CollectionChanged -= OnCollectionChanged; + templatedItems.GroupedCollectionChanged -= OnGroupedCollectionChanged; + } + + if (e.NewElement != null) + { + if (Control == null) + { + var scroller = new NSScrollView + { + AutoresizingMask = NSViewResizingMask.HeightSizable | NSViewResizingMask.WidthSizable, + DocumentView = _table = CreateNSTableView(e.NewElement) + }; + SetNativeControl(scroller); + } + + var controller = (IListViewController)e.NewElement; + controller.ScrollToRequested += OnScrollToRequested; + + var templatedItems = ((ITemplatedItemsView<Cell>)e.NewElement).TemplatedItems; + templatedItems.CollectionChanged += OnCollectionChanged; + templatedItems.GroupedCollectionChanged += OnGroupedCollectionChanged; + + UpdateRowHeight(); + UpdateHeader(); + UpdateFooter(); + UpdatePullToRefreshEnabled(); + UpdateIsRefreshing(); + UpdateSeparatorColor(); + UpdateSeparatorVisibility(); + + var selected = e.NewElement.SelectedItem; + if (selected != null) + _dataSource.OnItemSelected(null, new SelectedItemChangedEventArgs(selected)); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if (e.PropertyName == ListView.RowHeightProperty.PropertyName) + UpdateRowHeight(); + else if (e.PropertyName == ListView.IsGroupingEnabledProperty.PropertyName || + (e.PropertyName == ListView.HasUnevenRowsProperty.PropertyName)) + _dataSource.Update(); + else if (e.PropertyName == ListView.IsPullToRefreshEnabledProperty.PropertyName) + UpdatePullToRefreshEnabled(); + else if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName) + UpdateIsRefreshing(); + else if (e.PropertyName == ListView.SeparatorColorProperty.PropertyName) + UpdateSeparatorColor(); + else if (e.PropertyName == ListView.SeparatorVisibilityProperty.PropertyName) + UpdateSeparatorVisibility(); + else if (e.PropertyName == "HeaderElement") + UpdateHeader(); + else if (e.PropertyName == "FooterElement") + UpdateFooter(); + else if (e.PropertyName == "RefreshAllowed") + UpdatePullToRefreshEnabled(); + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateItems(e, 0, true); + } + + void OnGroupedCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + var til = (TemplatedItemsList<ItemsView<Cell>, Cell>)sender; + + var templatedItems = TemplatedItemsView.TemplatedItems; + var groupIndex = templatedItems.IndexOf(til.HeaderContent); + UpdateItems(e, groupIndex, false); + } + + void UpdateHeader() + { + var header = Controller.HeaderElement; + var headerView = (View)header; + + if (headerView != null) + { + //Header reuse is not working for now , problem with size of something that is not inside a layout + //if (_headerRenderer != null) + //{ + // if (header != null && _headerRenderer.GetType() == Registrar.Registered.GetHandlerType(header.GetType())) + // { + // _headerRenderer.SetElement(headerView); + // _table.HeaderView = new CustomNSTableHeaderView(Bounds.Width, _headerRenderer); + // // Layout(); + // //var customNSTableHeaderView = _table.HeaderView as CustomNSTableHeaderView; + // //customNSTableHeaderView?.Update(Bounds.Width, _headerRenderer); + // //NativeView.Layout(); + // //NativeView.SetNeedsDisplayInRect(NativeView.Bounds); + // //NativeView.LayoutSubtreeIfNeeded(); + // //_table.LayoutSubtreeIfNeeded(); + // //_table.NeedsDisplay = true; + // //NativeView.NeedsDisplay = true; + // return; + // } + ClearHeader(); + //} + + _headerRenderer = Platform.CreateRenderer(headerView); + Platform.SetRenderer(headerView, _headerRenderer); + _table.HeaderView = new CustomNSTableHeaderView(Bounds.Width, _headerRenderer); + + //We need this since the NSSCrollView doesn't know of the new size of our header + //TODO: Find a better solution + (Control as NSScrollView)?.ContentView.ScrollToPoint(new CoreGraphics.CGPoint(0, -_table.HeaderView.Frame.Height)); + } + else if (_headerRenderer != null) + { + ClearHeader(); + } + } + + void ClearHeader() + { + _table.HeaderView = null; + if (_headerRenderer == null) + return; + Platform.DisposeModelAndChildrenRenderers(_headerRenderer.Element); + _headerRenderer = null; + } + + void UpdateItems(NotifyCollectionChangedEventArgs e, int section, bool resetWhenGrouped) + { + var exArgs = e as NotifyCollectionChangedEventArgsEx; + if (exArgs != null) + _dataSource.Counts[section] = exArgs.Count; + + var groupReset = resetWhenGrouped && Element.IsGroupingEnabled; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + + _table.BeginUpdates(); + _table.InsertRows(NSIndexSet.FromArray(Enumerable.Range(e.NewStartingIndex, e.NewItems.Count).ToArray()), + NSTableViewAnimation.SlideUp); + _table.EndUpdates(); + + break; + + case NotifyCollectionChangedAction.Remove: + if (e.OldStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + + _table.BeginUpdates(); + _table.RemoveRows(NSIndexSet.FromArray(Enumerable.Range(e.OldStartingIndex, e.OldItems.Count).ToArray()), + NSTableViewAnimation.SlideDown); + _table.EndUpdates(); + + break; + + case NotifyCollectionChangedAction.Move: + if (e.OldStartingIndex == -1 || e.NewStartingIndex == -1 || groupReset) + goto case NotifyCollectionChangedAction.Reset; + _table.BeginUpdates(); + for (var i = 0; i < e.OldItems.Count; i++) + { + var oldi = e.OldStartingIndex; + var newi = e.NewStartingIndex; + + if (e.NewStartingIndex < e.OldStartingIndex) + { + oldi += i; + newi += i; + } + + _table.MoveRow(oldi, newi); + } + _table.EndUpdates(); + + break; + + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Reset: + _table.ReloadData(); + return; + } + } + + void UpdateRowHeight() + { + var rowHeight = Element.RowHeight; + if (Element.HasUnevenRows && rowHeight == -1) + { + // _table.RowHeight = NoIntrinsicMetric; + } + else + _table.RowHeight = rowHeight <= 0 ? DefaultRowHeight : rowHeight; + } + + //TODO: Implement UpdateIsRefreshing + void UpdateIsRefreshing() + { + } + + //TODO: Implement PullToRefresh + void UpdatePullToRefreshEnabled() + { + } + + //TODO: Implement SeparatorColor + void UpdateSeparatorColor() + { + } + + //TODO: Implement UpdateSeparatorVisibility + void UpdateSeparatorVisibility() + { + } + + //TODO: Implement ScrollTo + void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + } + + //TODO: Implement Footer + void UpdateFooter() + { + } + + class FormsNSTableView : NSTableView + { + //NSTableView doesn't support selection notfications after the items is already selected + //so we do it ourselves by hooking mouse down event + public override void MouseDown(NSEvent theEvent) + { + var clickLocation = ConvertPointFromView(theEvent.LocationInWindow, null); + var clickedRow = GetRow(clickLocation); + + base.MouseDown(theEvent); + if (clickedRow != -1) + (Source as ListViewDataSource)?.OnRowClicked(); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/MasterDetailPageRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/MasterDetailPageRenderer.cs new file mode 100644 index 00000000..8d2fd551 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/MasterDetailPageRenderer.cs @@ -0,0 +1,204 @@ +using System; +using System.ComponentModel; +using System.Linq; +using AppKit; +using CoreGraphics; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class MasterDetailPageRenderer : NSSplitViewController, IVisualElementRenderer, IEffectControlProvider + { + bool _disposed; + EventTracker _events; + VisualElementTracker _tracker; + MasterDetailPage _masterDetailPage; + + IPageController PageController => Element as IPageController; + + void IEffectControlProvider.RegisterEffect(Effect effect) + { + var platformEffect = effect as PlatformEffect; + if (platformEffect != null) + platformEffect.Container = View; + } + + protected MasterDetailPage MasterDetailPage => _masterDetailPage ?? (_masterDetailPage = (MasterDetailPage)Element); + + protected override void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + PageController?.SendDisappearing(); + + if (Element != null) + { + Element.PropertyChanged -= HandlePropertyChanged; + Element = null; + } + + ClearControllers(); + + _tracker?.Dispose(); + _tracker = null; + _events?.Dispose(); + _events = null; + + _disposed = true; + } + base.Dispose(disposing); + } + + public NSViewController ViewController => this; + + public NSView NativeView => View; + + public VisualElement Element { get; private set; } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return NativeView.GetSizeRequest(widthConstraint, heightConstraint); + } + + public void SetElement(VisualElement element) + { + var oldElement = Element; + Element = element; + + UpdateControllers(); + + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + + EffectUtilities.RegisterEffectControlProvider(this, oldElement, element); + } + + public void SetElementSize(Size size) + { + Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); + UpdateChildrenLayout(); + } + + protected virtual void OnElementChanged(VisualElementChangedEventArgs e) + { + if (e.OldElement != null) + e.OldElement.PropertyChanged -= HandlePropertyChanged; + + if (e.NewElement != null) + e.NewElement.PropertyChanged += HandlePropertyChanged; + + ElementChanged?.Invoke(this, e); + } + + protected virtual double MasterWidthPercentage => 0.3; + + public override void ViewWillAppear() + { + UpdateBackground(); + _tracker = new VisualElementTracker(this); + _events = new EventTracker(this); + _events.LoadEvents(NativeView); + UpdateChildrenLayout(); + + base.ViewWillAppear(); + } + + public override CGRect GetEffectiveRect(NSSplitView splitView, CGRect proposedEffectiveRect, CGRect drawnRect, + nint dividerIndex) + { + return CGRect.Empty; + } + + void UpdateChildrenLayout() + { + if (View.Frame.Width == -1) + return; + var width = View.Frame.Width; + var masterWidth = MasterWidthPercentage * width; + if (SplitViewItems.Length > 0) + SplitViewItems[0].MaximumThickness = SplitViewItems[0].MinimumThickness = (nfloat)masterWidth; + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_tracker == null) + return; + + if (e.PropertyName == "Master" || e.PropertyName == "Detail") + UpdateControllers(); + } + + void UpdateControllers() + { + ClearControllers(); + + if (Platform.GetRenderer(MasterDetailPage.Master) == null) + Platform.SetRenderer(MasterDetailPage.Master, Platform.CreateRenderer(MasterDetailPage.Master)); + if (Platform.GetRenderer(MasterDetailPage.Detail) == null) + Platform.SetRenderer(MasterDetailPage.Detail, Platform.CreateRenderer(MasterDetailPage.Detail)); + + AddSplitViewItem(new NSSplitViewItem + { + ViewController = new ViewControllerWrapper(Platform.GetRenderer(MasterDetailPage.Master)) + }); + AddSplitViewItem(new NSSplitViewItem + { + ViewController = new ViewControllerWrapper(Platform.GetRenderer(MasterDetailPage.Detail)) + }); + + UpdateChildrenLayout(); + } + + void ClearControllers() + { + while (SplitViewItems.Length > 0) + { + var splitItem = SplitViewItems.Last(); + var childVisualRenderer = splitItem.ViewController as ViewControllerWrapper; + RemoveSplitViewItem(splitItem); + IVisualElementRenderer render = null; + if (childVisualRenderer.RendererWeakRef.TryGetTarget(out render)) + { + render.Dispose(); + } + childVisualRenderer.Dispose(); + childVisualRenderer = null; + } + } + + //TODO: Implement Background color on MDP + void UpdateBackground() + { + } + + sealed class ViewControllerWrapper : NSViewController + { + internal WeakReference<IVisualElementRenderer> RendererWeakRef; + + public ViewControllerWrapper(IVisualElementRenderer renderer) + { + RendererWeakRef = new WeakReference<IVisualElementRenderer>(renderer); + View = new NSView { WantsLayer = true }; + AddChildViewController(renderer.ViewController); + View.AddSubview(renderer.NativeView); + } + + public override void ViewWillLayout() + { + IVisualElementRenderer renderer; + if (RendererWeakRef.TryGetTarget(out renderer)) + renderer?.Element?.Layout(new Rectangle(0, 0, View.Bounds.Width, View.Bounds.Height)); + base.ViewWillLayout(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && RendererWeakRef != null) + { + RendererWeakRef = null; + } + base.Dispose(disposing); + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/NSPageContainer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/NSPageContainer.cs new file mode 100644 index 00000000..80f409cc --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/NSPageContainer.cs @@ -0,0 +1,17 @@ +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + internal class NSPageContainer : NSObject + { + public NSPageContainer(Page element, int index) + { + Page = element; + Index = index; + } + + public Page Page { get; } + + public int Index { get; set; } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/NavigationPageRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/NavigationPageRenderer.cs new file mode 100644 index 00000000..23d7f856 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/NavigationPageRenderer.cs @@ -0,0 +1,355 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using AppKit; +using CoreAnimation; +using Foundation; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class NavigationPageRenderer : NSViewController, IVisualElementRenderer, IEffectControlProvider + { + bool _disposed; + bool _appeared; + EventTracker _events; + VisualElementTracker _tracker; + Stack<NavigationChildPageWrapper> _currentStack = new Stack<NavigationChildPageWrapper>(); + + IPageController PageController => Element as IPageController; + + IElementController ElementController => Element as IElementController; + + INavigationPageController NavigationController => Element as INavigationPageController; + + void IEffectControlProvider.RegisterEffect(Effect effect) + { + var platformEffect = effect as PlatformEffect; + if (platformEffect != null) + platformEffect.Container = View; + } + + public NavigationPageRenderer() : this(IntPtr.Zero) + { + } + + public NavigationPageRenderer(IntPtr handle) + { + View = new NSView { WantsLayer = true }; + } + + public VisualElement Element { get; private set; } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return NativeView.GetSizeRequest(widthConstraint, heightConstraint); + } + + public NSViewController ViewController => this; + + public NSView NativeView => View; + + public void SetElement(VisualElement element) + { + var oldElement = Element; + Element = element; + + Init(); + + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + + EffectUtilities.RegisterEffectControlProvider(this, oldElement, element); + } + + public void SetElementSize(Size size) + { + Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); + } + + public Task<bool> PopToRootAsync(Page page, bool animated = true) + { + return OnPopToRoot(page, animated); + } + + public Task<bool> PopViewAsync(Page page, bool animated = true) + { + return OnPop(page, animated); + } + + public Task<bool> PushPageAsync(Page page, bool animated = true) + { + return OnPush(page, animated); + } + + protected override void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + if (Element != null) + { + PageController?.SendDisappearing(); + ((Element as IPageContainer<Page>)?.CurrentPage as IPageController)?.SendDisappearing(); + Element.PropertyChanged -= HandlePropertyChanged; + Element = null; + } + + _tracker?.Dispose(); + _tracker = null; + + _events?.Dispose(); + _events = null; + + _disposed = true; + } + base.Dispose(disposing); + } + + public override void ViewDidDisappear() + { + base.ViewDidDisappear(); + if (!_appeared) + return; + Platform.NativeToolbarTracker.TryHide(Element as NavigationPage); + _appeared = false; + PageController?.SendDisappearing(); + } + + public override void ViewDidAppear() + { + base.ViewDidAppear(); + Platform.NativeToolbarTracker.Navigation = (NavigationPage)Element; + if (_appeared) + return; + + _appeared = true; + PageController?.SendAppearing(); + } + + protected virtual void OnElementChanged(VisualElementChangedEventArgs e) + { + if (e.OldElement != null) + e.OldElement.PropertyChanged -= HandlePropertyChanged; + + if (e.NewElement != null) + e.NewElement.PropertyChanged += HandlePropertyChanged; + + ElementChanged?.Invoke(this, e); + } + + protected virtual void ConfigurePageRenderer() + { + View.WantsLayer = true; + } + + protected virtual Task<bool> OnPopToRoot(Page page, bool animated) + { + var renderer = Platform.GetRenderer(page); + if (renderer == null || renderer.ViewController == null) + return Task.FromResult(false); + + Platform.NativeToolbarTracker.UpdateToolBar(); + return Task.FromResult(true); + } + + protected virtual async Task<bool> OnPop(Page page, bool animated) + { + var removed = await PopPageAsync(page, animated); + Platform.NativeToolbarTracker.UpdateToolBar(); + return removed; + } + + protected virtual async Task<bool> OnPush(Page page, bool animated) + { + var shown = await AddPage(page, animated); + Platform.NativeToolbarTracker.UpdateToolBar(); + return shown; + } + + void Init() + { + ConfigurePageRenderer(); + + var navPage = (NavigationPage)Element; + + if (navPage.CurrentPage == null) + throw new InvalidOperationException( + "NavigationPage must have a root Page before being used. Either call PushAsync with a valid Page, or pass a Page to the constructor before usage."); + + Platform.NativeToolbarTracker.Navigation = navPage; + + NavigationController.PushRequested += OnPushRequested; + NavigationController.PopRequested += OnPopRequested; + NavigationController.PopToRootRequested += OnPopToRootRequested; + NavigationController.RemovePageRequested += OnRemovedPageRequested; + NavigationController.InsertPageBeforeRequested += OnInsertPageBeforeRequested; + + navPage.Popped += (sender, e) => Platform.NativeToolbarTracker.UpdateToolBar(); + navPage.PoppedToRoot += (sender, e) => Platform.NativeToolbarTracker.UpdateToolBar(); + + UpdateBarBackgroundColor(); + UpdateBarTextColor(); + + _events = new EventTracker(this); + _events.LoadEvents(NativeView); + _tracker = new VisualElementTracker(this); + + ((INavigationPageController)navPage).Pages.ForEach(async p => await PushPageAsync(p, false)); + + UpdateBackgroundColor(); + } + + IVisualElementRenderer CreateViewControllerForPage(Page page) + { + if (Platform.GetRenderer(page) == null) + Platform.SetRenderer(page, Platform.CreateRenderer(page)); + + var pageRenderer = Platform.GetRenderer(page); + return pageRenderer; + } + + //TODO: Implement InserPageBefore + void InsertPageBefore(Page page, Page before) + { + if (before == null) + throw new ArgumentNullException(nameof(before)); + if (page == null) + throw new ArgumentNullException(nameof(page)); + } + + void OnInsertPageBeforeRequested(object sender, NavigationRequestedEventArgs e) + { + InsertPageBefore(e.Page, e.BeforePage); + } + + void OnPopRequested(object sender, NavigationRequestedEventArgs e) + { + e.Task = PopViewAsync(e.Page, e.Animated); + } + + void OnPopToRootRequested(object sender, NavigationRequestedEventArgs e) + { + e.Task = PopToRootAsync(e.Page, e.Animated); + } + + void OnPushRequested(object sender, NavigationRequestedEventArgs e) + { + e.Task = PushPageAsync(e.Page, e.Animated); + } + + void OnRemovedPageRequested(object sender, NavigationRequestedEventArgs e) + { + RemovePage(e.Page, true); + Platform.NativeToolbarTracker.UpdateToolBar(); + } + + void RemovePage(Page page, bool removeFromStack) + { + (page as IPageController)?.SendDisappearing(); + var target = Platform.GetRenderer(page); + target?.NativeView?.RemoveFromSuperview(); + target?.ViewController?.RemoveFromParentViewController(); + target?.Dispose(); + if (removeFromStack) + { + var newStack = new Stack<NavigationChildPageWrapper>(); + foreach (var stack in _currentStack) + { + if (stack.Page != page) + { + newStack.Push(stack); + } + } + _currentStack = newStack; + } + } + + async Task<bool> PopPageAsync(Page page, bool animated) + { + if (page == null) + throw new ArgumentNullException(nameof(page)); + + var wrapper = _currentStack.Peek(); + if (page != wrapper.Page) + throw new NotSupportedException("Popped page does not appear on top of current navigation stack, please file a bug."); + + _currentStack.Pop(); + (page as IPageController)?.SendDisappearing(); + + var target = Platform.GetRenderer(page); + var previousPage = _currentStack.Peek().Page; + + if (animated) + { + var previousPageRenderer = Platform.GetRenderer(previousPage); + return await this.HandleAsyncAnimation(target.ViewController, previousPageRenderer.ViewController, + NSViewControllerTransitionOptions.SlideBackward, () => Platform.DisposeRendererAndChildren(target), true); + } + + RemovePage(page, false); + return true; + } + + async Task<bool> AddPage(Page page, bool animated) + { + if (page == null) + throw new ArgumentNullException(nameof(page)); + + Page oldPage = null; + if (_currentStack.Count >= 1) + oldPage = _currentStack.Peek().Page; + + _currentStack.Push(new NavigationChildPageWrapper(page)); + + var vc = CreateViewControllerForPage(page); + vc.SetElementSize(new Size(View.Bounds.Width, View.Bounds.Height)); + page.Layout(new Rectangle(0, 0, View.Bounds.Width, View.Frame.Height)); + + if (_currentStack.Count == 1 || !animated) + { + vc.NativeView.WantsLayer = true; + AddChildViewController(vc.ViewController); + View.AddSubview(vc.NativeView); + return true; + } + var vco = Platform.GetRenderer(oldPage); + AddChildViewController(vc.ViewController); + return await this.HandleAsyncAnimation(vco.ViewController, vc.ViewController, + NSViewControllerTransitionOptions.SlideForward, () => (page as IPageController)?.SendAppearing(), true); + } + + void UpdateBackgroundColor() + { + if (View.Layer == null) + return; + var color = Element.BackgroundColor == Color.Default ? Color.White : Element.BackgroundColor; + View.Layer.BackgroundColor = color.ToCGColor(); + } + + void UpdateBarBackgroundColor() + { + Platform.NativeToolbarTracker.UpdateToolBar(); + } + + void UpdateBarTextColor() + { + Platform.NativeToolbarTracker.UpdateToolBar(); + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (_tracker == null) + return; + + if (e.PropertyName == NavigationPage.BarBackgroundColorProperty.PropertyName) + UpdateBarBackgroundColor(); + else if (e.PropertyName == NavigationPage.BarTextColorProperty.PropertyName) + UpdateBarTextColor(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/OpenGLViewRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/OpenGLViewRenderer.cs new file mode 100644 index 00000000..dda6ac50 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/OpenGLViewRenderer.cs @@ -0,0 +1,104 @@ +using System; +using System.ComponentModel; +using CoreVideo; + +namespace Xamarin.Forms.Platform.MacOS +{ + // ReSharper disable once InconsistentNaming + internal class OpenGLViewRenderer : ViewRenderer<OpenGLView, MacOSOpenGLView> + { + CVDisplayLink _displayLink; + + public void Display(object sender, EventArgs eventArgs) + { + if (Element.HasRenderLoop) + return; + SetupRenderLoop(true); + } + + protected override void Dispose(bool disposing) + { + if (_displayLink != null) + { + _displayLink.Dispose(); + _displayLink = null; + + if (Element != null) + ((IOpenGlViewController)Element).DisplayRequested -= Display; + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<OpenGLView> e) + { + if (e.OldElement != null) + ((IOpenGlViewController)e.OldElement).DisplayRequested -= Display; + + if (e.NewElement != null) + { + //var context = new EAGLContext(EAGLRenderingAPI.OpenGLES2); + //var glkView = new GLKView(RectangleF.Empty) { Context = context, DrawableDepthFormat = GLKViewDrawableDepthFormat.Format24, Delegate = new Delegate(e.NewElement) }; + var glkView = new MacOSOpenGLView(); + SetNativeControl(glkView); + + ((IOpenGlViewController)e.NewElement).DisplayRequested += Display; + + SetupRenderLoop(false); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == OpenGLView.HasRenderLoopProperty.PropertyName) + SetupRenderLoop(false); + } + + void SetupRenderLoop(bool oneShot) + { + if (_displayLink != null) + return; + if (!oneShot && !Element.HasRenderLoop) + return; + + _displayLink = new CVDisplayLink(); + + //.Create(() => + //{ + // var control = Control; + // var model = Element; + // if (control != null) + // control.Display(); + // if (control == null || model == null || !model.HasRenderLoop) + // { + // _displayLink.Invalidate(); + // _displayLink.Dispose(); + // _displayLink = null; + // } + //}); + //_displayLink.(NSRunLoop.Current, NSRunLoop.NSDefaultRunLoopMode); + } + + //class Delegate : GLKViewDelegate + //{ + // readonly OpenGLView _model; + + // public Delegate(OpenGLView model) + // { + // _model = model; + // } + + // public override void DrawInRect(GLKView view, RectangleF rect) + // { + // var onDisplay = _model.OnDisplay; + // if (onDisplay == null) + // return; + // onDisplay(rect.ToRectangle()); + // } + //} + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/PageControllerDelegate.cs b/Xamarin.Forms.Platform.MacOS/Renderers/PageControllerDelegate.cs new file mode 100644 index 00000000..d1f40d31 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/PageControllerDelegate.cs @@ -0,0 +1,30 @@ +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + internal class PageControllerDelegate : NSPageControllerDelegate + { + public override string GetIdentifier(NSPageController pageController, NSObject targetObject) + { + return nameof(PageRenderer); + } + + public override NSViewController GetViewController(NSPageController pageController, string identifier) + { + return new PageRenderer(); + } + + public override void PrepareViewController(NSPageController pageController, NSViewController viewController, + NSObject targetObject) + { + var pageContainer = targetObject as NSPageContainer; + var pageRenderer = (viewController as PageRenderer); + if (pageContainer == null || pageRenderer == null) + return; + Page page = pageContainer.Page; + pageRenderer.SetElement(page); + Platform.SetRenderer(page, pageRenderer); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/PageRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/PageRenderer.cs new file mode 100644 index 00000000..e0845d5a --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/PageRenderer.cs @@ -0,0 +1,182 @@ +using System; +using System.ComponentModel; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class PageRenderer : NSViewController, IVisualElementRenderer, IEffectControlProvider + { + bool _init; + bool _appeared; + bool _disposed; + EventTracker _events; + VisualElementPackager _packager; + VisualElementTracker _tracker; + + IPageController PageController => Element as IPageController; + + public PageRenderer() + { + View = new NSView { WantsLayer = true }; + } + + void IEffectControlProvider.RegisterEffect(Effect effect) + { + var platformEffect = effect as PlatformEffect; + if (platformEffect != null) + platformEffect.Container = View; + } + + public VisualElement Element { get; private set; } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return NativeView.GetSizeRequest(widthConstraint, heightConstraint); + } + + public NSView NativeView => _disposed ? null : View; + + public void SetElement(VisualElement element) + { + VisualElement oldElement = Element; + Element = element; + UpdateTitle(); + + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + + if (Element != null && !string.IsNullOrEmpty(Element.AutomationId)) + SetAutomationId(Element.AutomationId); + + EffectUtilities.RegisterEffectControlProvider(this, oldElement, element); + } + + public void SetElementSize(Size size) + { + Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); + } + + public NSViewController ViewController => _disposed ? null : this; + + public override void ViewDidAppear() + { + base.ViewDidAppear(); + + if (_appeared || _disposed) + return; + + _appeared = true; + PageController.SendAppearing(); + } + + public override void ViewDidDisappear() + { + base.ViewDidDisappear(); + + if (!_appeared || _disposed) + return; + + _appeared = false; + PageController.SendDisappearing(); + } + + public override void ViewWillAppear() + { + Init(); + base.ViewWillAppear(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + Element.PropertyChanged -= OnHandlePropertyChanged; + Platform.SetRenderer(Element, null); + if (_appeared) + PageController.SendDisappearing(); + + _appeared = false; + + if (_events != null) + { + _events.Dispose(); + _events = null; + } + + if (_packager != null) + { + _packager.Dispose(); + _packager = null; + } + + if (_tracker != null) + { + _tracker.Dispose(); + _tracker = null; + } + + Element = null; + _disposed = true; + } + + base.Dispose(disposing); + } + + void OnElementChanged(VisualElementChangedEventArgs e) + { + ElementChanged?.Invoke(this, e); + } + + void SetAutomationId(string id) + { + if (NativeView != null) + NativeView.AccessibilityIdentifier = id; + } + + void Init() + { + if (_init) + return; + UpdateBackground(); + + _packager = new VisualElementPackager(this); + _packager.Load(); + + Element.PropertyChanged += OnHandlePropertyChanged; + _tracker = new VisualElementTracker(this); + + _events = new EventTracker(this); + _events.LoadEvents(View); + _init = true; + } + + void OnHandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackground(); + else if (e.PropertyName == Page.BackgroundImageProperty.PropertyName) + UpdateBackground(); + else if (e.PropertyName == Page.TitleProperty.PropertyName) + UpdateTitle(); + } + + void UpdateBackground() + { + string bgImage = ((Page)Element).BackgroundImage; + if (!string.IsNullOrEmpty(bgImage)) + { + View.Layer.BackgroundColor = NSColor.FromPatternImage(NSImage.ImageNamed(bgImage)).CGColor; + return; + } + Color bgColor = Element.BackgroundColor; + View.Layer.BackgroundColor = bgColor.IsDefault ? NSColor.White.CGColor : bgColor.ToCGColor(); + } + + void UpdateTitle() + { + if (!string.IsNullOrWhiteSpace(((Page)Element).Title)) + Title = ((Page)Element).Title; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/PickerRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/PickerRenderer.cs new file mode 100644 index 00000000..ac07bfd2 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/PickerRenderer.cs @@ -0,0 +1,154 @@ +using System; +using AppKit; +using System.ComponentModel; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class PickerRenderer : ViewRenderer<Picker, NSComboBox> + { + bool _disposed; + NSColor _defaultTextColor; + NSColor _defaultBackgroundColor; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<Picker> e) + { + if (e.NewElement != null) + { + if (Control == null) + SetNativeControl(new NSComboBox { Editable = false }); + + _defaultTextColor = Control.TextColor; + _defaultBackgroundColor = Control.BackgroundColor; + + Control.UsesDataSource = true; + Control.DataSource = new ComboDataSource(this); + + Control.SelectionChanged += ComboBoxSelectionChanged; + + UpdatePicker(); + UpdateTextColor(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if (e.PropertyName == Picker.TitleProperty.PropertyName) + UpdatePicker(); + if (e.PropertyName == Picker.SelectedIndexProperty.PropertyName) + UpdatePicker(); + if (e.PropertyName == Picker.TextColorProperty.PropertyName || + e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateTextColor(); + } + + protected override void SetBackgroundColor(Color color) + { + base.SetBackgroundColor(color); + + if (Control == null) + return; + + Control.BackgroundColor = color == Color.Default ? _defaultBackgroundColor : color.ToNSColor(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (!_disposed) + { + _disposed = true; + if (Element != null) + { + //TODO: Implement ObservableList picker source change + //((ObservableList<string>)Element.Items).CollectionChanged -= RowsCollectionChanged; + } + + if (Control != null) + Control.SelectionChanged -= ComboBoxSelectionChanged; + } + } + base.Dispose(disposing); + } + + void ComboBoxSelectionChanged(object sender, EventArgs e) + { + ElementController?.SetValueFromRenderer(Picker.SelectedIndexProperty, (int)Control.SelectedIndex); + } + + void OnEnded(object sender, EventArgs eventArgs) + { + ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + } + + void OnStarted(object sender, EventArgs eventArgs) + { + ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + } + + void RowsCollectionChanged(object sender, EventArgs e) + { + UpdatePicker(); + } + + void UpdatePicker() + { + if (Control == null || Element == null) + return; + + var selectedIndex = Element.SelectedIndex; + var items = Element.Items; + Control.PlaceholderString = Element.Title ?? string.Empty; + Control.ReloadData(); + if (items == null || items.Count == 0 || selectedIndex < 0) + return; + + Control.SelectItem(selectedIndex); + } + + void UpdateTextColor() + { + if (Control == null || Element == null) + return; + + var textColor = Element.TextColor; + + if (textColor.IsDefault || !Element.IsEnabled) + Control.TextColor = _defaultTextColor; + else + Control.TextColor = textColor.ToNSColor(); + } + + class ComboDataSource : NSComboBoxDataSource + { + readonly PickerRenderer _renderer; + + public ComboDataSource(PickerRenderer model) + { + _renderer = model; + } + + public override nint ItemCount(NSComboBox comboBox) + { + return _renderer.Element.Items?.Count ?? 0; + } + + public override NSObject ObjectValueForItem(NSComboBox comboBox, nint index) + { + return new NSString(_renderer.Element.Items[(int)index]); + } + + public override nint IndexOfItem(NSComboBox comboBox, string value) + { + var index = _renderer.Element.Items?.IndexOf(value) ?? -1; + return index; + } + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ProgressBarRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ProgressBarRenderer.cs new file mode 100644 index 00000000..7b3c84ae --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ProgressBarRenderer.cs @@ -0,0 +1,66 @@ +using System.ComponentModel; +using AppKit; +using CoreImage; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class ProgressBarRenderer : ViewRenderer<ProgressBar, NSProgressIndicator> + { + static CIColorPolynomial s_currentColorFilter; + static NSColor s_currentColor; + + protected override void OnElementChanged(ElementChangedEventArgs<ProgressBar> e) + { + if (e.NewElement == null) return; + if (Control == null) + SetNativeControl(new NSProgressIndicator + { + IsDisplayedWhenStopped = true, + Style = NSProgressIndicatorStyle.Bar, + MinValue = 0, + MaxValue = 1 + }); + UpdateProgress(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == ProgressBar.ProgressProperty.PropertyName) + UpdateProgress(); + } + + protected override void SetBackgroundColor(Color color) + { + if (Control == null) + return; + + if (s_currentColorFilter == null && color.IsDefault) + return; + + if (color.IsDefault) + Control.ContentFilters = new CIFilter[0]; + + var newColor = Element.BackgroundColor.ToNSColor(); + if (Equals(s_currentColor, newColor)) + return; + + s_currentColor = newColor; + + s_currentColorFilter = new CIColorPolynomial + { + RedCoefficients = new CIVector(s_currentColor.RedComponent), + BlueCoefficients = new CIVector(s_currentColor.BlueComponent), + GreenCoefficients = new CIVector(s_currentColor.GreenComponent) + }; + + Control.ContentFilters = new CIFilter[] { s_currentColorFilter }; + } + + void UpdateProgress() + { + Control.DoubleValue = Element.Progress; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/ScrollViewRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/ScrollViewRenderer.cs new file mode 100644 index 00000000..c41d1d2b --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/ScrollViewRenderer.cs @@ -0,0 +1,214 @@ +using System; +using System.ComponentModel; +using AppKit; +using RectangleF = CoreGraphics.CGRect; +using ObjCRuntime; +using Foundation; + +// ReSharper disable UnusedMember.Local +// ReSharper disable UnusedParameter.Local + +namespace Xamarin.Forms.Platform.MacOS +{ + public class ScrollViewRenderer : NSScrollView, IVisualElementRenderer + { + EventTracker _events; + VisualElementTracker _tracker; + ScrollToRequestedEventArgs _requestedScroll; + IVisualElementRenderer _contentRenderer; + + public ScrollViewRenderer() : base(RectangleF.Empty) + { + DrawsBackground = false; + ContentView.PostsBoundsChangedNotifications = true; + NSNotificationCenter.DefaultCenter.AddObserver(this, new Selector(nameof(UpdateScrollPosition)), + BoundsChangedNotification, ContentView); + } + + IScrollViewController Controller => Element as IScrollViewController; + + public VisualElement Element { get; private set; } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return NativeView.GetSizeRequest(widthConstraint, heightConstraint, 44, 44); + } + + public NSView NativeView => this; + + public void SetElement(VisualElement element) + { + _requestedScroll = null; + var oldElement = Element; + Element = element; + + if (oldElement != null) + { + oldElement.PropertyChanged -= HandlePropertyChanged; + ((IScrollViewController)oldElement).ScrollToRequested -= OnScrollToRequested; + } + + if (element != null) + { + element.PropertyChanged += HandlePropertyChanged; + ((IScrollViewController)element).ScrollToRequested += OnScrollToRequested; + if (_tracker == null) + { + PackContent(); + + _events = new EventTracker(this); + _events.LoadEvents(this); + + _tracker = new VisualElementTracker(this); + _tracker.NativeControlUpdated += OnNativeControlUpdated; + } + + UpdateContentSize(); + UpdateBackgroundColor(); + + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + } + } + + public void SetElementSize(Size size) + { + Xamarin.Forms.Layout.LayoutChildIntoBoundingRegion(Element, + new Rectangle(Element.X, Element.Y, size.Width, size.Height)); + } + + public NSViewController ViewController => null; + + public override void Layout() + { + base.Layout(); + LayoutSubviews(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_tracker == null) + return; + + SetElement(null); + + _tracker.NativeControlUpdated -= OnNativeControlUpdated; + _tracker.Dispose(); + _tracker = null; + + _events.Dispose(); + _events = null; + + ClearContentRenderer(); + + //NSNotificationCenter.DefaultCenter.RemoveObserver(this, BoundsChangedNotification); + } + + base.Dispose(disposing); + } + + void OnElementChanged(VisualElementChangedEventArgs e) + { + ElementChanged?.Invoke(this, e); + } + + void PackContent() + { + ClearContentRenderer(); + + if (Controller.Children.Count == 0 || !(Controller.Children[0] is VisualElement)) + return; + + var content = (VisualElement)Controller.Children[0]; + if (Platform.GetRenderer(content) == null) + Platform.SetRenderer(content, Platform.CreateRenderer(content)); + + _contentRenderer = Platform.GetRenderer(content); + + DocumentView = _contentRenderer.NativeView; + } + + void LayoutSubviews() + { + if (_requestedScroll != null && Superview != null) + { + var request = _requestedScroll; + _requestedScroll = null; + OnScrollToRequested(this, request); + } + } + + void HandlePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == ScrollView.ContentSizeProperty.PropertyName) + UpdateContentSize(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundColor(); + } + + void HandleScrollAnimationEnded(object sender, EventArgs e) + { + Controller.SendScrollFinished(); + } + + void HandleScrolled(object sender, EventArgs e) + { + UpdateScrollPosition(); + } + + void OnNativeControlUpdated(object sender, EventArgs eventArgs) + { + UpdateContentSize(); + } + + void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e) + { + if (Superview == null) + { + _requestedScroll = e; + return; + } + + Point scrollPoint = (e.Mode == ScrollToMode.Position) + ? new Point(e.ScrollX, Element.Height - e.ScrollY) + : Controller.GetScrollPositionForElement(e.Element as VisualElement, e.Position); + + (DocumentView as NSView)?.ScrollPoint(scrollPoint.ToPointF()); + + Controller.SendScrollFinished(); + } + + void UpdateBackgroundColor() + { + BackgroundColor = Element.BackgroundColor.ToNSColor(Color.Transparent); + } + + void UpdateContentSize() + { + if (_contentRenderer == null) + return; + var contentSize = ((ScrollView)Element).ContentSize.ToSizeF(); + if (!contentSize.IsEmpty) + _contentRenderer.NativeView.Frame = new RectangleF(0, Element.Height - contentSize.Height, contentSize.Width, + contentSize.Height); + } + + [Export(nameof(UpdateScrollPosition))] + void UpdateScrollPosition() + { + var convertedPoint = (DocumentView as NSView)?.ConvertPointFromView(ContentView.Bounds.Location, ContentView); + if (convertedPoint.HasValue) + Controller.SetScrolledPosition(Math.Max(0, convertedPoint.Value.X), Math.Max(0, convertedPoint.Value.Y)); + } + + void ClearContentRenderer() + { + _contentRenderer?.NativeView?.RemoveFromSuperview(); + _contentRenderer?.Dispose(); + _contentRenderer = null; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/SearchBarRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/SearchBarRenderer.cs new file mode 100644 index 00000000..50c7d5fe --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/SearchBarRenderer.cs @@ -0,0 +1,179 @@ +using System; +using System.ComponentModel; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class SearchBarRenderer : ViewRenderer<SearchBar, NSSearchField> + { + NSColor _defaultTextColor; + + IElementController ElementController => Element; + + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (Control != null) + { + Control.Changed -= OnTextChanged; + Control.Cell.CancelButtonCell.Activated -= OnCancelClicked; + Control.Cell.SearchButtonCell.Activated -= OnSearchButtonClicked; + Control.EditingEnded -= OnEditingEnded; + Control.EditingBegan -= OnEditingStarted; + } + } + + base.Dispose(disposing); + } + + protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e) + { + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new NSSearchField { BackgroundColor = NSColor.Clear, DrawsBackground = false }); + + Control.Cell.CancelButtonCell.Activated += OnCancelClicked; + Control.Cell.SearchButtonCell.Activated += OnSearchButtonClicked; + + Control.Changed += OnTextChanged; + Control.EditingBegan += OnEditingStarted; + Control.EditingEnded += OnEditingEnded; + } + + UpdatePlaceholder(); + UpdateText(); + UpdateFont(); + UpdateIsEnabled(); + UpdateCancelButton(); + UpdateAlignment(); + UpdateTextColor(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == SearchBar.PlaceholderProperty.PropertyName || + e.PropertyName == SearchBar.PlaceholderColorProperty.PropertyName) + UpdatePlaceholder(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + { + UpdateIsEnabled(); + UpdateTextColor(); + UpdatePlaceholder(); + } + else if (e.PropertyName == SearchBar.TextColorProperty.PropertyName) + UpdateTextColor(); + else if (e.PropertyName == SearchBar.TextProperty.PropertyName) + UpdateText(); + else if (e.PropertyName == SearchBar.CancelButtonColorProperty.PropertyName) + UpdateCancelButton(); + else if (e.PropertyName == SearchBar.FontAttributesProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == SearchBar.FontFamilyProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == SearchBar.FontSizeProperty.PropertyName) + UpdateFont(); + else if (e.PropertyName == SearchBar.HorizontalTextAlignmentProperty.PropertyName) + UpdateAlignment(); + } + + protected override void SetBackgroundColor(Color color) + { + base.SetBackgroundColor(color); + + if (Control == null) + return; + Control.BackgroundColor = color == Color.Default ? NSColor.Clear : color.ToNSColor(); + + UpdateCancelButton(); + } + + void OnCancelClicked(object sender, EventArgs args) + { + ElementController.SetValueFromRenderer(SearchBar.TextProperty, null); + Control.ResignFirstResponder(); + } + + void OnEditingEnded(object sender, EventArgs e) + { + ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false); + } + + void OnEditingStarted(object sender, EventArgs e) + { + ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); + } + + void OnSearchButtonClicked(object sender, EventArgs e) + { + ((ISearchBarController)Element).OnSearchButtonPressed(); + Control.ResignFirstResponder(); + } + + void OnTextChanged(object sender, EventArgs a) + { + ElementController.SetValueFromRenderer(SearchBar.TextProperty, Control.StringValue); + } + + void UpdateAlignment() + { + Control.Alignment = Element.HorizontalTextAlignment.ToNativeTextAlignment(); + } + + void UpdateCancelButton() + { + var cancelButtonColor = Element.CancelButtonColor; + + if (cancelButtonColor.IsDefault) + { + Control.Cell.CancelButtonCell.Title = ""; + } + else + { + var textWithColor = new NSAttributedString(Control.Cell.CancelButtonCell.Title ?? "", + foregroundColor: cancelButtonColor.ToNSColor()); + Control.Cell.CancelButtonCell.AttributedTitle = textWithColor; + } + } + + void UpdateFont() + { + Control.Font = Element.ToNSFont(); + } + + void UpdateIsEnabled() + { + Control.Enabled = Element.IsEnabled; + } + + void UpdatePlaceholder() + { + var formatted = (FormattedString)Element.Placeholder ?? string.Empty; + var targetColor = Element.PlaceholderColor; + var color = Element.IsEnabled && !targetColor.IsDefault ? targetColor : ColorExtensions.SeventyPercentGrey.ToColor(); + Control.PlaceholderAttributedString = formatted.ToAttributed(Element, color); + } + + void UpdateText() + { + Control.StringValue = Element.Text ?? ""; + UpdateCancelButton(); + } + + void UpdateTextColor() + { + _defaultTextColor = _defaultTextColor ?? Control.TextColor; + var targetColor = Element.TextColor; + + Control.TextColor = targetColor.ToNSColor(_defaultTextColor); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/SliderRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/SliderRenderer.cs new file mode 100644 index 00000000..9b495ded --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/SliderRenderer.cs @@ -0,0 +1,77 @@ +using System; +using SizeF = CoreGraphics.CGSize; +using AppKit; +using System.ComponentModel; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class SliderRenderer : ViewRenderer<Slider, NSSlider> + { + bool _disposed; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<Slider> e) + { + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new NSSlider { Continuous = true }); + Control.Activated += OnControlActivated; + } + + UpdateMaximum(); + UpdateMinimum(); + UpdateValue(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Slider.MaximumProperty.PropertyName) + UpdateMaximum(); + else if (e.PropertyName == Slider.MinimumProperty.PropertyName) + UpdateMinimum(); + else if (e.PropertyName == Slider.ValueProperty.PropertyName) + UpdateValue(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + if (Control != null) + Control.Activated -= OnControlActivated; + } + + base.Dispose(disposing); + } + + void OnControlActivated(object sender, EventArgs eventArgs) + { + ElementController?.SetValueFromRenderer(Slider.ValueProperty, Control.DoubleValue); + } + + void UpdateMaximum() + { + Control.MaxValue = (float)Element.Maximum; + } + + void UpdateMinimum() + { + Control.MinValue = (float)Element.Minimum; + } + + void UpdateValue() + { + if (Math.Abs(Element.Value - Control.DoubleValue) > 0) + Control.DoubleValue = (float)Element.Value; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/StepperRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/StepperRenderer.cs new file mode 100644 index 00000000..f28bf1f7 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/StepperRenderer.cs @@ -0,0 +1,84 @@ +using System; +using System.ComponentModel; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class StepperRenderer : ViewRenderer<Stepper, NSStepper> + { + bool _disposed; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<Stepper> e) + { + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new NSStepper()); + Control.Activated += OnControlActivated; + } + + UpdateMinimum(); + UpdateMaximum(); + UpdateValue(); + UpdateIncrement(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Stepper.MinimumProperty.PropertyName) + UpdateMinimum(); + else if (e.PropertyName == Stepper.MaximumProperty.PropertyName) + UpdateMaximum(); + else if (e.PropertyName == Stepper.ValueProperty.PropertyName) + UpdateValue(); + else if (e.PropertyName == Stepper.IncrementProperty.PropertyName) + UpdateIncrement(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + if (Control != null) + Control.Activated -= OnControlActivated; + } + + base.Dispose(disposing); + } + + void OnControlActivated(object sender, EventArgs e) + { + ElementController?.SetValueFromRenderer(Stepper.ValueProperty, Control.DoubleValue); + } + + void UpdateIncrement() + { + Control.Increment = Element.Increment; + } + + void UpdateMaximum() + { + Control.MaxValue = Element.Maximum; + } + + void UpdateMinimum() + { + Control.MinValue = Element.Minimum; + } + + void UpdateValue() + { + if (Math.Abs(Control.DoubleValue - Element.Value) > 0) + Control.DoubleValue = Element.Value; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/SwitchRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/SwitchRenderer.cs new file mode 100644 index 00000000..c93007e2 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/SwitchRenderer.cs @@ -0,0 +1,61 @@ +using System; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class SwitchRenderer : ViewRenderer<Switch, NSButton> + { + bool _disposed; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<Switch> e) + { + if (e.OldElement != null) + e.OldElement.Toggled -= OnElementToggled; + + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new NSButton { AllowsMixedState = false, Title = string.Empty }); + + Control.SetButtonType(NSButtonType.Switch); + Control.Activated += OnControlActivated; + } + + UpdateState(); + e.NewElement.Toggled += OnElementToggled; + } + + base.OnElementChanged(e); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + if (Control != null) + Control.Activated -= OnControlActivated; + } + + base.Dispose(disposing); + } + + void OnControlActivated(object sender, EventArgs e) + { + ElementController?.SetValueFromRenderer(Switch.IsToggledProperty, Control.State == NSCellStateValue.On); + } + + void OnElementToggled(object sender, EventArgs e) + { + UpdateState(); + } + + void UpdateState() + { + Control.State = Element.IsToggled ? NSCellStateValue.On : NSCellStateValue.Off; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/TabbedPageRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/TabbedPageRenderer.cs new file mode 100644 index 00000000..3aee095d --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/TabbedPageRenderer.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using AppKit; +using CoreGraphics; +using Xamarin.Forms.Internals; +using Xamarin.Forms.PlatformConfiguration.macOSSpecific; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class TabbedPageRenderer : NSTabViewController, IVisualElementRenderer, IEffectControlProvider + { + const float DefaultImageSizeSegmentedButton = 19; + const int TabHolderHeight = 30; + + bool _disposed; + bool _updatingControllers; + bool _barBackgroundColorWasSet; + bool _barTextColorWasSet; + bool _defaultBarTextColorSet; + bool _defaultBarColorSet; + VisualElementTracker _tracker; + bool _loaded; + Size _queuedSize; + + + public VisualElement Element { get; private set; } + + public event EventHandler<VisualElementChangedEventArgs> ElementChanged; + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return NativeView.GetSizeRequest(widthConstraint, heightConstraint); + } + + public NSView NativeView => View; + + public void SetElement(VisualElement element) + { + var oldElement = Element; + Element = element; + + if (oldElement != null) + { + oldElement.PropertyChanged -= OnPropertyChanged; + var tabbedPage = oldElement as TabbedPage; + if (tabbedPage != null) tabbedPage.PagesChanged -= OnPagesChanged; + } + + if (element != null) + { + if (_tracker == null) + { + _tracker = new VisualElementTracker(this); + _tracker.NativeControlUpdated += (sender, e) => UpdateNativeWidget(); + } + } + + OnElementChanged(new VisualElementChangedEventArgs(oldElement, element)); + + ConfigureTabView(); + + OnPagesChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + + Tabbed.PropertyChanged += OnPropertyChanged; + Tabbed.PagesChanged += OnPagesChanged; + + UpdateBarBackgroundColor(); + + UpdateBarTextColor(); + + EffectUtilities.RegisterEffectControlProvider(this, oldElement, element); + } + + IPageController PageController => Element as IPageController; + + IElementController ElementController => Element; + + void IEffectControlProvider.RegisterEffect(Effect effect) + { + var platformEffect = effect as PlatformEffect; + if (platformEffect != null) + platformEffect.Container = View; + } + + public void SetElementSize(Size size) + { + if (_loaded) + Element.Layout(new Rectangle(Element.X, Element.Y, size.Width, size.Height)); + else + _queuedSize = size; + } + + public NSViewController ViewController => this; + + public override void ViewWillLayout() + { + base.ViewWillLayout(); + + if (Element == null) + return; + + if (!Element.Bounds.IsEmpty) + View.Frame = new System.Drawing.RectangleF((float)Element.X, (float)Element.Y, (float)Element.Width, (float)Element.Height); + + var frame = View.Frame; + PageController.ContainerArea = new Rectangle(0, 0, frame.Width, frame.Height - TabHolderHeight); + + if (!_queuedSize.IsZero) + { + Element.Layout(new Rectangle(Element.X, Element.Y, _queuedSize.Width, _queuedSize.Height)); + _queuedSize = Size.Zero; + } + + _loaded = true; + } + + + public override nint SelectedTabViewItemIndex + { + get { return base.SelectedTabViewItemIndex; } + set + { + base.SelectedTabViewItemIndex = value; + if (!_updatingControllers) + UpdateCurrentPage(); + } + } + + public override void ViewDidAppear() + { + PageController.SendAppearing(); + base.ViewDidAppear(); + } + + public override void ViewDidDisappear() + { + base.ViewDidDisappear(); + PageController.SendDisappearing(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + PageController.SendDisappearing(); + Tabbed.PropertyChanged -= OnPropertyChanged; + Tabbed.PagesChanged -= OnPagesChanged; + + if (_tracker != null) + { + _tracker.Dispose(); + _tracker = null; + } + } + + base.Dispose(disposing); + } + + protected virtual void ConfigureTabView() + { + View.WantsLayer = true; + TabView.WantsLayer = true; + TabView.DrawsBackground = false; + var tabStyle = Tabbed.OnThisPlatform().GetTabsStyle(); + switch (tabStyle) + { + case TabsStyle.OnNavigation: + case TabsStyle.Hidden: + TabStyle = NSTabViewControllerTabStyle.Unspecified; + break; + case TabsStyle.Icons: + TabStyle = NSTabViewControllerTabStyle.Toolbar; + break; + case TabsStyle.OnBottom: + TabStyle = NSTabViewControllerTabStyle.SegmentedControlOnBottom; + break; + default: + TabStyle = NSTabViewControllerTabStyle.SegmentedControlOnTop; + break; + } + + TabView.TabViewType = NSTabViewType.NSNoTabsNoBorder; + } + + protected virtual void OnElementChanged(VisualElementChangedEventArgs e) + { + ElementChanged?.Invoke(this, e); + } + + protected virtual NSTabViewItem GetTabViewItem(Page page, IVisualElementRenderer pageRenderer) + { + var tvi = new NSTabViewItem { ViewController = pageRenderer.ViewController, Label = page.Title ?? "" }; + if (!string.IsNullOrEmpty (page.Icon)) { + var image = GetTabViewItemIcon (page.Icon); + if (image != null) + tvi.Image = image; + } + return tvi; + } + + protected virtual NSImage GetTabViewItemIcon(string imageName) + { + var image = NSImage.ImageNamed (imageName); + if(image == null) + image = new NSImage (imageName); + + if (image == null) + return null; + + bool shouldResize = TabStyle == NSTabViewControllerTabStyle.SegmentedControlOnTop || + TabStyle == NSTabViewControllerTabStyle.SegmentedControlOnBottom; + if (shouldResize) + image = image.ResizeTo(new CGSize(DefaultImageSizeSegmentedButton, DefaultImageSizeSegmentedButton)); + return image; + } + + protected virtual void UpdateNativeWidget() + { + TabView.Layout(); + } + + protected TabbedPage Tabbed => (TabbedPage)Element; + + void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == Page.TitleProperty.PropertyName) + { + var page = (Page)sender; + var index = TabbedPage.GetIndex(page); + TabViewItems[index].Label = page.Title; + } + else if (e.PropertyName == Page.IconProperty.PropertyName) + { + var page = (Page)sender; + + var index = TabbedPage.GetIndex(page); + TabViewItems[index].Label = page.Title; + + if (!string.IsNullOrEmpty(page.Icon)) + { + TabViewItems[index].Image = new NSImage(page.Icon); + } + else if (TabViewItems[index].Image != null) + { + TabViewItems[index].Image = new NSImage(); + } + } + } + + void OnPagesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + e.Apply((o, i, c) => SetupPage((Page)o, i), (o, i) => TeardownPage((Page)o), Reset); + + SetControllers(); + + UpdateChildrenOrderIndex(); + + SetSelectedTabViewItem(); + } + + void OnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(TabbedPage.CurrentPage)) + { + var current = Tabbed.CurrentPage; + if (current == null) + return; + + SetSelectedTabViewItem(); + } + else if (e.PropertyName == TabbedPage.BarBackgroundColorProperty.PropertyName) + UpdateBarBackgroundColor(); + else if (e.PropertyName == TabbedPage.BarTextColorProperty.PropertyName) + UpdateBarTextColor(); + } + + void Reset() + { + var i = 0; + foreach (var page in Tabbed.Children) + SetupPage(page, i++); + } + + void SetControllers() + { + _updatingControllers = true; + for (var i = 0; i < ElementController.LogicalChildren.Count; i++) + { + var child = ElementController.LogicalChildren[i]; + var page = child as Page; + if (page == null) + continue; + + var pageRenderer = Platform.GetRenderer(page); + if (pageRenderer != null) + { + pageRenderer.ViewController.Identifier = i.ToString(); + + NSTabViewItem newTvi = GetTabViewItem(page, pageRenderer); + + AddTabViewItem(newTvi); + } + } + _updatingControllers = false; + } + + void SetupPage(Page page, int index) + { + var renderer = Platform.GetRenderer(page); + if (renderer == null) + { + renderer = Platform.CreateRenderer(page); + Platform.SetRenderer(page, renderer); + } + + renderer.ViewController.Identifier = index.ToString(); + + page.PropertyChanged += OnPagePropertyChanged; + } + + void TeardownPage(Page page) + { + page.PropertyChanged -= OnPagePropertyChanged; + + Platform.SetRenderer(page, null); + } + + void SetSelectedTabViewItem() + { + if (Tabbed.CurrentPage == null) + return; + var selectedIndex = TabbedPage.GetIndex(Tabbed.CurrentPage); + SelectedTabViewItemIndex = selectedIndex; + } + + void UpdateChildrenOrderIndex() + { + for (var i = 0; i < TabViewItems.Length; i++) + { + int originalIndex; + if (int.TryParse(TabViewItems[i].ViewController.Identifier, out originalIndex)) + { + var page = PageController.InternalChildren[originalIndex]; + TabbedPage.SetIndex(page as Page, i); + } + } + } + + void UpdateCurrentPage() + { + var count = PageController.InternalChildren.Count; + Tabbed.CurrentPage = SelectedTabViewItemIndex >= 0 && SelectedTabViewItemIndex < count + ? Tabbed.GetPageByIndex((int)SelectedTabViewItemIndex) + : null; + } + + //TODO: Implement UpdateBarBackgroundColor + void UpdateBarBackgroundColor() + { + if (Tabbed == null || TabView == null) + return; + + var barBackgroundColor = Tabbed.BarBackgroundColor; + var isDefaultColor = barBackgroundColor.IsDefault; + + if (isDefaultColor && !_barBackgroundColorWasSet) + return; + + if (!_defaultBarColorSet) + { + //_defaultBarColor = TabView.color; + _defaultBarColorSet = true; + } + + if (!isDefaultColor) + _barBackgroundColorWasSet = true; + } + + //TODO: Implement UpdateBarTextColor + void UpdateBarTextColor() + { + if (Tabbed == null || TabView == null) + return; + + var barTextColor = Tabbed.BarTextColor; + var isDefaultColor = barTextColor.IsDefault; + + if (isDefaultColor && !_barTextColorWasSet) + return; + + if (!_defaultBarTextColorSet) + { + // _defaultBarTextColor = TabBar.TintColor; + _defaultBarTextColorSet = true; + } + + if (!isDefaultColor) + _barTextColorWasSet = true; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/TableViewDataSource.cs b/Xamarin.Forms.Platform.MacOS/Renderers/TableViewDataSource.cs new file mode 100644 index 00000000..90335ee2 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/TableViewDataSource.cs @@ -0,0 +1,131 @@ +using System; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + internal class TableViewDataSource : NSTableViewSource + { + static int s_sectionCount; + + const string HeaderIdentifier = nameof(TextCell); + const string ItemIdentifier = nameof(ViewCell); + + protected ITableViewController Controller => _tableView; + + readonly NSTableView _nsTableView; + readonly TableView _tableView; + + public TableViewDataSource(TableViewRenderer tableViewRenderer) + { + _tableView = tableViewRenderer.Element; + _nsTableView = tableViewRenderer.TableView; + Controller.ModelChanged += (s, e) => { _nsTableView?.ReloadData(); }; + AutomaticallyDeselect = true; + } + + public bool AutomaticallyDeselect { get; set; } + + public override void SelectionDidChange(NSNotification notification) + { + var row = _nsTableView.SelectedRow; + if (row == -1) + return; + + int sectionIndex; + bool isHeader; + int itemIndexInSection; + + GetComputedIndexes(row, out sectionIndex, out itemIndexInSection, out isHeader); + + var cell = Controller.Model.GetCell(sectionIndex, itemIndexInSection); + Controller.Model.RowSelected(cell); + if (AutomaticallyDeselect) + _nsTableView.DeselectRow(row); + } + + public override nint GetRowCount(NSTableView tableView) + { + nint count = 0; + s_sectionCount = Controller.Model.GetSectionCount(); + for (int i = 0; i < s_sectionCount; i++) + { + count += Controller.Model.GetRowCount(i) + 1; + } + + return count; + } + + public override bool ShouldSelectRow(NSTableView tableView, nint row) + { + int sectionIndex; + bool isHeader; + int itemIndexInSection; + + GetComputedIndexes(row, out sectionIndex, out itemIndexInSection, out isHeader); + + return !isHeader; + } + + public override NSView GetViewForItem(NSTableView tableView, NSTableColumn tableColumn, nint row) + { + int sectionIndex; + bool isHeader; + int itemIndexInSection; + + GetComputedIndexes(row, out sectionIndex, out itemIndexInSection, out isHeader); + + string id; + Cell cell; + if (isHeader) + { + id = HeaderIdentifier; + cell = Controller.Model.GetHeaderCell(sectionIndex) ?? + new TextCell { Text = Controller.Model.GetSectionTitle(sectionIndex) }; + } + else + { + id = ItemIdentifier; + cell = Controller.Model.GetCell(sectionIndex, itemIndexInSection); + } + + var nativeCell = CellNSView.GetNativeCell(tableView, cell, id, isHeader); + return nativeCell; + } + + void GetComputedIndexes(nint row, out int sectionIndex, out int itemIndexInSection, out bool isHeader) + { + var totalItems = 0; + isHeader = false; + sectionIndex = 0; + itemIndexInSection = 0; + + for (int i = 0; i < s_sectionCount; i++) + { + var groupCount = Controller.Model.GetRowCount(i); + var itemsInSection = groupCount + 1; + + if (row < totalItems + itemsInSection) + { + sectionIndex = i; + itemIndexInSection = (int)row - totalItems; + isHeader = itemIndexInSection == 0; + if (isHeader) + itemIndexInSection = -1; + else + itemIndexInSection = itemIndexInSection - 1; + break; + } + totalItems += itemsInSection; + } + } + } + + //TODO: Implement Uneven rows + internal class UnEvenTableViewModelRenderer : TableViewDataSource + { + public UnEvenTableViewModelRenderer(TableViewRenderer model) : base(model) + { + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/TableViewRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/TableViewRenderer.cs new file mode 100644 index 00000000..de447dcb --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/TableViewRenderer.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.ComponentModel; +using AppKit; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class TableViewRenderer : ViewRenderer<TableView, NSView> + { + const int DefaultRowHeight = 44; + + internal NSTableView TableView { get; set; } + + public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) + { + return Control.GetSizeRequest(widthConstraint, heightConstraint, DefaultRowHeight, DefaultRowHeight); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + var viewsToLookAt = new Stack<NSView>(Subviews); + while (viewsToLookAt.Count > 0) + { + var view = viewsToLookAt.Pop(); + var viewCellRenderer = view as IVisualElementRenderer; + if (viewCellRenderer != null) + { + viewCellRenderer.Dispose(); + } + else + { + foreach (var child in view.Subviews) + viewsToLookAt.Push(child); + } + } + } + + base.Dispose(disposing); + } + + protected virtual NSTableView CreateNSTableView(TableView list) + { + return new NSTableView().AsListViewLook(); + } + + protected override void OnElementChanged(ElementChangedEventArgs<TableView> e) + { + if (e.NewElement != null) + { + if (Control == null) + { + var scroller = new NSScrollView + { + AutoresizingMask = NSViewResizingMask.HeightSizable | NSViewResizingMask.WidthSizable, + DocumentView = TableView = CreateNSTableView(e.NewElement) + }; + + SetNativeControl(scroller); + } + + SetSource(); + UpdateRowHeight(); + UpdateBackgroundView(); + } + + base.OnElementChanged(e); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == Xamarin.Forms.TableView.RowHeightProperty.PropertyName) + UpdateRowHeight(); + else if (e.PropertyName == Xamarin.Forms.TableView.HasUnevenRowsProperty.PropertyName) + SetSource(); + else if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) + UpdateBackgroundView(); + } + + void SetSource() + { + var modeledView = Element; + TableView.Source = modeledView.HasUnevenRows ? new UnEvenTableViewModelRenderer(this) : new TableViewDataSource(this); + } + + void UpdateBackgroundView() + { + } + + void UpdateRowHeight() + { + var rowHeight = Element.RowHeight; + TableView.RowHeight = rowHeight <= 0 ? DefaultRowHeight : rowHeight; + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/TimePickerRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/TimePickerRenderer.cs new file mode 100644 index 00000000..fc645c46 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/TimePickerRenderer.cs @@ -0,0 +1,104 @@ +using System; +using System.ComponentModel; +using AppKit; +using Foundation; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class TimePickerRenderer : ViewRenderer<TimePicker, NSDatePicker> + { + NSColor _defaultTextColor; + NSColor _defaultBackgroundColor; + bool _disposed; + + IElementController ElementController => Element; + + protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e) + { + base.OnElementChanged(e); + + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new NSDatePicker + { + DatePickerMode = NSDatePickerMode.Single, + TimeZone = new NSTimeZone("UTC"), + DatePickerStyle = NSDatePickerStyle.TextFieldAndStepper, + DatePickerElements = NSDatePickerElementFlags.HourMinuteSecond + }); + + Control.ValidateProposedDateValue += HandleValueChanged; + _defaultTextColor = Control.TextColor; + _defaultBackgroundColor = Control.BackgroundColor; + } + + UpdateTime(); + UpdateTextColor(); + } + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == TimePicker.TimeProperty.PropertyName || + e.PropertyName == TimePicker.FormatProperty.PropertyName) + UpdateTime(); + + if (e.PropertyName == TimePicker.TextColorProperty.PropertyName || + e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateTextColor(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + if (Control != null) + Control.ValidateProposedDateValue -= HandleValueChanged; + + _disposed = true; + } + base.Dispose(disposing); + } + + protected override void SetBackgroundColor(Color color) + { + base.SetBackgroundColor(color); + + if (Control == null) + return; + Control.BackgroundColor = color == Color.Default ? _defaultBackgroundColor : color.ToNSColor(); + } + + void HandleValueChanged(object sender, NSDatePickerValidatorEventArgs e) + { + ElementController?.SetValueFromRenderer(TimePicker.TimeProperty, + Control.DateValue.ToDateTime() - new DateTime(2001, 1, 1)); + } + + void UpdateTime() + { + if (Control == null || Element == null) + return; + var time = new DateTime(2001, 1, 1).Add(Element.Time); + var newDate = time.ToNSDate(); + if (!Equals(Control.DateValue, newDate)) + Control.DateValue = newDate; + } + + void UpdateTextColor() + { + if (Control == null || Element == null) + return; + var textColor = Element.TextColor; + + if (textColor.IsDefault || !Element.IsEnabled) + Control.TextColor = _defaultTextColor; + else + Control.TextColor = textColor.ToNSColor(); + } + } +}
\ No newline at end of file diff --git a/Xamarin.Forms.Platform.MacOS/Renderers/WebViewRenderer.cs b/Xamarin.Forms.Platform.MacOS/Renderers/WebViewRenderer.cs new file mode 100644 index 00000000..8d93c2c7 --- /dev/null +++ b/Xamarin.Forms.Platform.MacOS/Renderers/WebViewRenderer.cs @@ -0,0 +1,149 @@ +using System; +using System.ComponentModel; +using AppKit; +using Foundation; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Platform.MacOS +{ + public class WebViewRenderer : ViewRenderer<WebView, WebKit.WebView>, IWebViewDelegate + { + bool _disposed; + bool _ignoreSourceChanges; + WebNavigationEvent _lastBackForwardEvent; + WebNavigationEvent _lastEvent; + + IElementController ElementController => Element; + + void IWebViewDelegate.LoadHtml(string html, string baseUrl) + { + if (html != null) + Control.MainFrame.LoadHtmlString(html, + baseUrl == null ? new NSUrl(NSBundle.MainBundle.BundlePath, true) : new NSUrl(baseUrl, true)); + } + + void IWebViewDelegate.LoadUrl(string url) + { + Control.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl(url))); + } + + protected override void OnElementChanged(ElementChangedEventArgs<WebView> e) + { + base.OnElementChanged(e); + + if (e.NewElement != null) + { + if (Control == null) + { + SetNativeControl(new WebKit.WebView + { + AutoresizingMask = NSViewResizingMask.WidthSizable, + AutoresizesSubviews = true + }); + Control.OnFinishedLoading += OnNSWebViewFinishedLoad; + Control.OnFailedLoading += OnNSWebViewFailedLoadWithError; + + Element.EvalRequested += OnEvalRequested; + Element.GoBackRequested += OnGoBackRequested; + Element.GoForwardRequested += OnGoForwardRequested; + + Layer.BackgroundColor = NSColor.Clear.CGColor; + } + } + + Load(); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + + if (e.PropertyName == WebView.SourceProperty.PropertyName) + Load(); + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) + { + _disposed = true; + Control.OnFinishedLoading -= OnNSWebViewFinishedLoad; + Control.OnFailedLoading -= OnNSWebViewFailedLoadWithError; + Element.EvalRequested -= OnEvalRequested; + Element.GoBackRequested -= OnGoBackRequested; + Element.GoForwardRequested -= OnGoForwardRequested; + } + base.Dispose(disposing); + } + + void Load() + { + if (_ignoreSourceChanges) + return; + + Element?.Source?.Load(this); + + UpdateCanGoBackForward(); + } + + void UpdateCanGoBackForward() + { + if (Element == null) + return; + Element.CanGoBack = Control.CanGoBack(); + Element.CanGoForward = Control.CanGoForward(); + } + + void OnEvalRequested(object sender, EvalRequested eventArg) + { + Control?.StringByEvaluatingJavaScriptFromString(eventArg?.Script); + } + + void OnGoBackRequested(object sender, EventArgs eventArgs) + { + if (Control.CanGoBack()) + { + _lastBackForwardEvent = WebNavigationEvent.Back; + Control.GoBack(); + } + + UpdateCanGoBackForward(); + } + + void OnGoForwardRequested(object sender, EventArgs eventArgs) + { + if (Control.CanGoForward()) + { + _lastBackForwardEvent = WebNavigationEvent.Forward; + Control.GoForward(); + } + + UpdateCanGoBackForward(); + } + + void OnNSWebViewFailedLoadWithError(object sender, WebKit.WebResourceErrorEventArgs e) + { + _lastEvent = _lastBackForwardEvent; + Element?.SendNavigated(new WebNavigatedEventArgs(_lastEvent, new UrlWebViewSource { Url = Control.MainFrameUrl }, + Control.MainFrameUrl, WebNavigationResult.Failure)); + + UpdateCanGoBackForward(); + } + + void OnNSWebViewFinishedLoad(object sender, WebKit.WebResourceCompletedEventArgs e) + { + if (Control.IsLoading) + return; + + _ignoreSourceChanges = true; + ElementController?.SetValueFromRenderer(WebView.SourceProperty, new UrlWebViewSource { Url = Control.MainFrameUrl }); + _ignoreSourceChanges = false; + + _lastEvent = _lastBackForwardEvent; + Element?.SendNavigated(new WebNavigatedEventArgs(_lastEvent, Element?.Source, Control.MainFrameUrl, + WebNavigationResult.Success)); + + UpdateCanGoBackForward(); + } + } +}
\ No newline at end of file |