|
using System;
using System.ComponentModel;
using CoreGraphics;
using Foundation;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using ObjCRuntime;
using UIKit;
using Specifics = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Entry;
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
{
[System.Obsolete(Compatibility.Hosting.MauiAppBuilderExtensions.UseMapperInstead)]
public class EntryRenderer : EntryRendererBase<UITextField>
{
[Preserve(Conditional = true)]
public EntryRenderer()
{
Frame = new CGRect(0, 20, 320, 40);
}
protected override UITextField CreateNativeControl()
{
var textField = new UITextField(CGRect.Empty);
textField.BorderStyle = UITextBorderStyle.RoundedRect;
textField.ClipsToBounds = true;
return textField;
}
}
[System.Obsolete(Compatibility.Hosting.MauiAppBuilderExtensions.UseMapperInstead)]
public abstract class EntryRendererBase<TControl> : ViewRenderer<Entry, TControl>
where TControl : UITextField
{
UIColor _defaultTextColor;
// 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
readonly Color _defaultPlaceholderColor = Maui.Platform.ColorExtensions.SeventyPercentGrey.ToColor();
UIColor _defaultCursorColor;
bool _useLegacyColorManagement;
bool _disposed;
IDisposable _selectedTextRangeObserver;
bool _nativeSelectionIsUpdating;
bool _cursorPositionChangePending;
bool _selectionLengthChangePending;
static readonly int baseHeight = 30;
static CGSize initialSize = CGSize.Empty;
public EntryRendererBase()
{
}
public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
{
var baseResult = base.GetDesiredSize(widthConstraint, heightConstraint);
if (Forms.IsiOS11OrNewer)
return baseResult;
NSString testString = new NSString("Tj");
var testSize = testString.GetSizeUsingAttributes(new UIStringAttributes { Font = Control.Font });
double height = baseHeight + testSize.Height - initialSize.Height;
height = Math.Round(height);
return new SizeRequest(new Size(baseResult.Request.Width, height));
}
IElementController ElementController => Element as IElementController;
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
_disposed = true;
if (disposing)
{
_defaultTextColor = null;
if (Control != null)
{
_defaultCursorColor = Control.TintColor;
Control.EditingDidBegin -= OnEditingBegan;
Control.EditingChanged -= OnEditingChanged;
Control.EditingDidEnd -= OnEditingEnded;
Control.ShouldChangeCharacters -= ShouldChangeCharacters;
_selectedTextRangeObserver?.Dispose();
}
}
base.Dispose(disposing);
}
abstract protected override TControl CreateNativeControl();
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
return;
if (Control == null)
{
var textField = CreateNativeControl();
SetNativeControl(textField);
// Cache the default text color
_defaultTextColor = textField.TextColor;
_useLegacyColorManagement = e.NewElement.UseLegacyColorManagement();
textField.EditingChanged += OnEditingChanged;
textField.ShouldReturn = OnShouldReturn;
textField.EditingDidBegin += OnEditingBegan;
textField.EditingDidEnd += OnEditingEnded;
textField.ShouldChangeCharacters += ShouldChangeCharacters;
_selectedTextRangeObserver = textField.AddObserver("selectedTextRange", NSKeyValueObservingOptions.New, UpdateCursorFromControl);
}
// When we set the control text, it triggers the UpdateCursorFromControl event, which updates CursorPosition and SelectionLength;
// These one-time-use variables will let us initialize a CursorPosition and SelectionLength via ctor/xaml/etc.
_cursorPositionChangePending = Element.IsSet(Entry.CursorPositionProperty);
_selectionLengthChangePending = Element.IsSet(Entry.SelectionLengthProperty);
// Font needs to be set before Text and Placeholder so that they layout correctly when set
UpdateFont();
UpdatePlaceholder();
UpdatePassword();
UpdateText();
UpdateCharacterSpacing();
UpdateColor();
UpdateKeyboard();
UpdateHorizontalTextAlignment();
UpdateVerticalTextAlignment();
UpdateAdjustsFontSizeToFitWidth();
UpdateMaxLength();
UpdateReturnType();
if (_cursorPositionChangePending || _selectionLengthChangePending)
UpdateCursorSelection();
UpdateCursorColor();
UpdateIsReadOnly();
if (Element.ClearButtonVisibility != ClearButtonVisibility.Never)
UpdateClearButtonVisibility();
}
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.IsOneOf(Entry.TextProperty, Entry.TextTransformProperty))
{
UpdateText();
UpdateCharacterSpacing();
}
else if (e.PropertyName == Entry.TextColorProperty.PropertyName)
UpdateColor();
else if (e.PropertyName == Entry.CharacterSpacingProperty.PropertyName)
UpdateCharacterSpacing();
else if (e.PropertyName == Microsoft.Maui.Controls.InputView.KeyboardProperty.PropertyName)
UpdateKeyboard();
else if (e.PropertyName == Microsoft.Maui.Controls.InputView.IsSpellCheckEnabledProperty.PropertyName)
UpdateKeyboard();
else if (e.PropertyName == Entry.IsTextPredictionEnabledProperty.PropertyName)
UpdateKeyboard();
else if (e.PropertyName == Entry.HorizontalTextAlignmentProperty.PropertyName)
UpdateHorizontalTextAlignment();
else if (e.PropertyName == Entry.VerticalTextAlignmentProperty.PropertyName)
UpdateVerticalTextAlignment();
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();
}
else if (e.PropertyName == Specifics.AdjustsFontSizeToFitWidthProperty.PropertyName)
UpdateAdjustsFontSizeToFitWidth();
else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName)
UpdateHorizontalTextAlignment();
else if (e.PropertyName == Microsoft.Maui.Controls.InputView.MaxLengthProperty.PropertyName)
UpdateMaxLength();
else if (e.PropertyName == Entry.ReturnTypeProperty.PropertyName)
UpdateReturnType();
else if (e.PropertyName == Entry.CursorPositionProperty.PropertyName)
UpdateCursorSelection();
else if (e.PropertyName == Entry.SelectionLengthProperty.PropertyName)
UpdateCursorSelection();
else if (e.PropertyName == Specifics.CursorColorProperty.PropertyName)
UpdateCursorColor();
else if (e.PropertyName == Microsoft.Maui.Controls.InputView.IsReadOnlyProperty.PropertyName)
UpdateIsReadOnly();
else if (e.PropertyName == Entry.ClearButtonVisibilityProperty.PropertyName)
UpdateClearButtonVisibility();
base.OnElementPropertyChanged(sender, e);
}
[PortHandler("Pending to port setting the IsFocused property")]
void OnEditingBegan(object sender, EventArgs e)
{
if (!_cursorPositionChangePending && !_selectionLengthChangePending)
UpdateCursorFromControl(null);
else
UpdateCursorSelection();
ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true);
}
[PortHandler("Ported Text setter")]
void OnEditingChanged(object sender, EventArgs eventArgs)
{
ElementController.SetValueFromRenderer(Entry.TextProperty, Control.Text);
UpdateCursorFromControl(null);
}
[PortHandler("Pending to port setting the IsFocused property")]
void OnEditingEnded(object sender, EventArgs e)
{
// Typing aid changes don't always raise EditingChanged event
// Normalizing nulls to string.Empty allows us to ensure that a change from null to "" doesn't result in a change event.
// While technically this is a difference it serves no functional good.
var controlText = Control.Text ?? string.Empty;
var entryText = Element.Text ?? string.Empty;
if (controlText != entryText)
{
ElementController.SetValueFromRenderer(Entry.TextProperty, controlText);
}
ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false);
}
[PortHandler("Still pending the code related to Focus.")]
protected virtual bool OnShouldReturn(UITextField view)
{
Control.ResignFirstResponder();
((IEntryController)Element).SendCompleted();
return false;
}
[PortHandler]
void UpdateHorizontalTextAlignment()
{
Control.TextAlignment = Element.HorizontalTextAlignment.ToPlatformTextAlignment(((IVisualElementController)Element).EffectiveFlowDirection);
}
[PortHandler]
void UpdateVerticalTextAlignment()
{
Control.VerticalAlignment = Element.VerticalTextAlignment.ToPlatformTextAlignment();
}
[PortHandler]
protected virtual void UpdateColor()
{
var textColor = Element.TextColor;
if (_useLegacyColorManagement)
{
Control.TextColor = textColor == null || !Element.IsEnabled ? _defaultTextColor : textColor.ToPlatform();
}
else
{
Control.TextColor = textColor == null ? _defaultTextColor : textColor.ToPlatform();
}
}
[PortHandler]
void UpdateAdjustsFontSizeToFitWidth()
{
Control.AdjustsFontSizeToFitWidth = Element.OnThisPlatform().AdjustsFontSizeToFitWidth();
}
[PortHandler]
protected virtual void UpdateFont()
{
if (initialSize == CGSize.Empty)
{
NSString testString = new NSString("Tj");
#pragma warning disable CA1416, CA1422 // TODO: API has [UnsupportedOSPlatform("ios7.0")]
initialSize = testString.StringSize(Control.Font);
#pragma warning restore CA1416, CA1422
}
Control.Font = Element.ToUIFont();
}
void UpdateKeyboard()
{
var keyboard = Element.Keyboard;
Control.ApplyKeyboard(keyboard);
if (!(keyboard is CustomKeyboard))
{
if (Element.IsSet(Microsoft.Maui.Controls.InputView.IsSpellCheckEnabledProperty))
{
if (!Element.IsSpellCheckEnabled)
{
Control.SpellCheckingType = UITextSpellCheckingType.No;
}
}
if (Element.IsSet(Microsoft.Maui.Controls.Entry.IsTextPredictionEnabledProperty))
{
if (!Element.IsTextPredictionEnabled)
{
Control.AutocorrectionType = UITextAutocorrectionType.No;
}
}
}
Control.ReloadInputViews();
}
[PortHandler]
void UpdatePassword()
{
if (Element.IsPassword && Control.IsFirstResponder)
{
Control.Enabled = false;
Control.SecureTextEntry = true;
Control.Enabled = Element.IsEnabled;
Control.BecomeFirstResponder();
}
else
Control.SecureTextEntry = Element.IsPassword;
}
[PortHandler]
protected virtual void UpdatePlaceholder()
{
var formatted = (FormattedString)Element.Placeholder;
if (formatted == null)
return;
var targetColor = Element.PlaceholderColor;
if (_useLegacyColorManagement)
{
var color = targetColor == null || !Element.IsEnabled ? _defaultPlaceholderColor : targetColor;
UpdateAttributedPlaceholder(formatted.ToNSAttributedString(Element.RequireFontManager(), defaultColor: color));
}
else
{
// Using VSM color management; take whatever is in Element.PlaceholderColor
var color = targetColor ?? _defaultPlaceholderColor;
UpdateAttributedPlaceholder(formatted.ToNSAttributedString(Element.RequireFontManager(), defaultColor: color));
}
UpdateAttributedPlaceholder(Control.AttributedPlaceholder.WithCharacterSpacing(Element.CharacterSpacing));
}
protected virtual void UpdateAttributedPlaceholder(NSAttributedString nsAttributedString) =>
Control.AttributedPlaceholder = nsAttributedString;
[PortHandler]
void UpdateText()
{
var text = Element.UpdateFormsText(Element.Text, Element.TextTransform);
// ReSharper disable once RedundantCheckBeforeAssignment
if (Control.Text != text)
Control.Text = text;
}
[PortHandler("Partially ported ...")]
void UpdateCharacterSpacing()
{
var textAttr = Control.AttributedText.WithCharacterSpacing(Element.CharacterSpacing);
if (textAttr != null)
Control.AttributedText = textAttr;
var placeHolder = Control.AttributedPlaceholder.WithCharacterSpacing(Element.CharacterSpacing);
if (placeHolder != null)
UpdateAttributedPlaceholder(placeHolder);
}
[PortHandler]
void UpdateMaxLength()
{
var currentControlText = Control.Text;
if (currentControlText.Length > Element.MaxLength)
Control.Text = currentControlText.Substring(0, Element.MaxLength);
}
[PortHandler]
bool ShouldChangeCharacters(UITextField textField, NSRange range, string replacementString)
{
var newLength = textField?.Text?.Length + replacementString?.Length - range.Length;
return newLength <= Element?.MaxLength;
}
[PortHandler]
void UpdateReturnType()
{
if (Control == null || Element == null)
return;
Control.ReturnKeyType = Element.ReturnType.ToUIReturnKeyType();
}
void UpdateCursorFromControl(NSObservedChange obj)
{
if (_nativeSelectionIsUpdating || Control == null || Element == null)
return;
var currentSelection = Control.SelectedTextRange;
if (currentSelection != null)
{
if (!_cursorPositionChangePending)
{
int newCursorPosition = (int)Control.GetOffsetFromPosition(Control.BeginningOfDocument, currentSelection.Start);
if (newCursorPosition != Element.CursorPosition)
SetCursorPositionFromRenderer(newCursorPosition);
}
if (!_selectionLengthChangePending)
{
int selectionLength = (int)Control.GetOffsetFromPosition(currentSelection.Start, currentSelection.End);
if (selectionLength != Element.SelectionLength)
SetSelectionLengthFromRenderer(selectionLength);
}
}
}
void UpdateCursorSelection()
{
if (_nativeSelectionIsUpdating || Control == null || Element == null)
return;
_cursorPositionChangePending = _selectionLengthChangePending = true;
// If this is run from the ctor, the control is likely too early in its lifecycle to be first responder yet.
// Anything done here will have no effect, so we'll skip this work until later.
// We'll try again when the control does become first responder later OnEditingBegan
if (Control.BecomeFirstResponder())
{
try
{
int cursorPosition = Element.CursorPosition;
UITextPosition start = GetSelectionStart(cursorPosition, out int startOffset);
UITextPosition end = GetSelectionEnd(cursorPosition, start, startOffset);
Control.SelectedTextRange = Control.GetTextRange(start, end);
}
catch (Exception ex)
{
Forms.MauiContext?.CreateLogger<EntryRenderer>()?.LogWarning(ex, "Failed to set Control.SelectedTextRange from CursorPosition/SelectionLength");
}
finally
{
_cursorPositionChangePending = _selectionLengthChangePending = false;
}
}
}
UITextPosition GetSelectionEnd(int cursorPosition, UITextPosition start, int startOffset)
{
UITextPosition end = start;
int endOffset = startOffset;
int selectionLength = Element.SelectionLength;
if (Element.IsSet(Entry.SelectionLengthProperty))
{
end = Control.GetPosition(start, Math.Max(startOffset, Math.Min(Control.Text.Length - cursorPosition, selectionLength))) ?? start;
endOffset = Math.Max(startOffset, (int)Control.GetOffsetFromPosition(Control.BeginningOfDocument, end));
}
int newSelectionLength = Math.Max(0, endOffset - startOffset);
if (newSelectionLength != selectionLength)
SetSelectionLengthFromRenderer(newSelectionLength);
return end;
}
UITextPosition GetSelectionStart(int cursorPosition, out int startOffset)
{
UITextPosition start = Control.EndOfDocument;
startOffset = Control.Text.Length;
if (Element.IsSet(Entry.CursorPositionProperty))
{
start = Control.GetPosition(Control.BeginningOfDocument, cursorPosition) ?? Control.EndOfDocument;
startOffset = Math.Max(0, (int)Control.GetOffsetFromPosition(Control.BeginningOfDocument, start));
}
if (startOffset != cursorPosition)
SetCursorPositionFromRenderer(startOffset);
return start;
}
[PortHandler]
void UpdateCursorColor()
{
var control = Control;
if (control == null || Element == null)
return;
if (Element.IsSet(Specifics.CursorColorProperty))
{
var color = Element.OnThisPlatform().GetCursorColor();
if (color == null)
control.TintColor = _defaultCursorColor;
else
control.TintColor = color.ToPlatform();
}
}
void SetCursorPositionFromRenderer(int start)
{
try
{
_nativeSelectionIsUpdating = true;
ElementController?.SetValueFromRenderer(Entry.CursorPositionProperty, start);
}
catch (Exception ex)
{
Forms.MauiContext?.CreateLogger<EntryRenderer>()?.LogWarning(ex, "FFailed to set CursorPosition from renderer");
}
finally
{
_nativeSelectionIsUpdating = false;
}
}
void SetSelectionLengthFromRenderer(int selectionLength)
{
try
{
_nativeSelectionIsUpdating = true;
ElementController?.SetValueFromRenderer(Entry.SelectionLengthProperty, selectionLength);
}
catch (Exception ex)
{
Forms.MauiContext?.CreateLogger<EntryRenderer>()?.LogWarning(ex, "Failed to set SelectionLength from renderer");
}
finally
{
_nativeSelectionIsUpdating = false;
}
}
[PortHandler]
void UpdateIsReadOnly()
{
Control.UserInteractionEnabled = !Element.IsReadOnly;
}
[PortHandler]
void UpdateClearButtonVisibility()
{
Control.ClearButtonMode = Element.ClearButtonVisibility == ClearButtonVisibility.WhileEditing ? UITextFieldViewMode.WhileEditing : UITextFieldViewMode.Never;
}
}
}
|