Browse Source

Working on iOS IME

pull/8963/head
Nikita Tsukanov 4 years ago
parent
commit
006fe37d0d
  1. 5
      samples/MobileSandbox.iOS/AppDelegate.cs
  2. 2
      samples/MobileSandbox.iOS/Info.plist
  3. 2
      samples/MobileSandbox.iOS/MobileSandbox.iOS.csproj
  4. 2
      src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs
  5. 4
      src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs
  6. 34
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  7. 2
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  8. 4
      src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs
  9. 504
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  10. 113
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  11. 40
      src/iOS/Avalonia.iOS/CombinedSpan3.cs
  12. 71
      src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs
  13. 471
      src/iOS/Avalonia.iOS/TextInputResponder.cs

5
samples/MobileSandbox.iOS/AppDelegate.cs

@ -3,6 +3,7 @@ using UIKit;
using Avalonia;
using Avalonia.Controls;
using Avalonia.iOS;
using Avalonia.Logging;
using Avalonia.Media;
namespace MobileSandbox
@ -13,5 +14,9 @@ namespace MobileSandbox
[Register("AppDelegate")]
public partial class AppDelegate : AvaloniaAppDelegate<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
return builder.LogToTrace(LogEventLevel.Debug, "IOSIME");
}
}
}

2
samples/MobileSandbox.iOS/Info.plist

@ -13,7 +13,7 @@
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MinimumOSVersion</key>
<string>10.0</string>
<string>13.0</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>

2
samples/MobileSandbox.iOS/MobileSandbox.iOS.csproj

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<ProvisioningType>manual</ProvisioningType>
<TargetFramework>net6.0-ios</TargetFramework>
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<!-- temporal workaround for our GL interface backend -->
<UseInterpreter>True</UseInterpreter>
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>

2
src/Avalonia.Base/Input/TextInput/ITextInputMethodClient.cs

@ -49,6 +49,8 @@ namespace Avalonia.Input.TextInput
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry
/// </summary>
string? TextAfterCursor { get; }
public void SelectInSurroundingText(int start, int end);
}
public struct TextInputMethodSurroundingText

4
src/Avalonia.Base/Input/TextInput/ITextInputMethodImpl.cs

@ -5,10 +5,6 @@ namespace Avalonia.Input.TextInput
[Unstable]
public interface ITextInputMethodImpl
{
ITextInputMethodClient? Client { get; }
bool IsActive { get; }
void SetClient(ITextInputMethodClient? client);
void SetCursorRect(Rect rect);
void SetOptions(TextInputOptions options);

34
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -51,21 +51,51 @@ namespace Avalonia.Controls
public bool SupportsSurroundingText => true;
public event EventHandler? SurroundingTextChanged { add { } remove { } }
public event EventHandler? SurroundingTextChanged;
public TextInputMethodSurroundingText SurroundingText => new TextInputMethodSurroundingText { Text = _presenter?.Text ?? "", CursorOffset = _presenter?.CaretIndex ?? 0, AnchorOffset = _presenter?.SelectionStart ?? 0};
public TextInputMethodSurroundingText SurroundingText => new()
{
Text = _presenter?.Text ?? "",
CursorOffset = _presenter?.CaretIndex ?? 0,
AnchorOffset = _presenter?.SelectionStart ?? 0
};
public string? TextBeforeCursor => null;
public string? TextAfterCursor => null;
public void SelectInSurroundingText(int start, int end)
{
if(_parent == null)
return;
// TODO: Account for the offset
_parent.SelectionStart = start;
_parent.SelectionEnd = end;
}
private void OnCaretBoundsChanged(object? sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty);
private void OnTextBoxPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == TextBox.TextProperty || e.Property == TextBox.SelectionStartProperty ||
e.Property == TextBox.SelectionEndProperty)
SurroundingTextChanged?.Invoke(this, EventArgs.Empty);
}
public void SetPresenter(TextPresenter? presenter, TextBox? parent)
{
if (_parent != null)
{
_parent.PropertyChanged -= OnTextBoxPropertyChanged;
}
_parent = parent;
if (_parent != null)
{
_parent.PropertyChanged += OnTextBoxPropertyChanged;
}
if (_presenter != null)
{
_presenter.CaretBoundsChanged -= OnCaretBoundsChanged;

2
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-ios</TargetFramework>
<SupportedOSPlatformVersion>10.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup>
<ItemGroup>

4
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@ -20,7 +20,7 @@ namespace Avalonia.iOS
}
}
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseiOS();
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
[Export("window")]
public UIWindow Window { get; set; }
@ -28,7 +28,7 @@ namespace Avalonia.iOS
[Export("application:didFinishLaunchingWithOptions:")]
public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
var builder = AppBuilder.Configure<TApp>();
var builder = AppBuilder.Configure<TApp>().UseiOS();
CustomizeAppBuilder(builder);
var lifetime = new SingleViewLifetime();

504
src/iOS/Avalonia.iOS/AvaloniaView.Text.cs

@ -1,500 +1,54 @@
using System;
using System.Runtime.InteropServices;
using Foundation;
using ObjCRuntime;
#nullable enable
using Avalonia.Input.TextInput;
using Avalonia.Input;
using Avalonia.Input.Raw;
using CoreGraphics;
using JetBrains.Annotations;
using UIKit;
namespace Avalonia.iOS;
#nullable enable
[Adopts("UITextInput")]
[Adopts("UITextInputTraits")]
[Adopts("UIKeyInput")]
public class AvaloniaResponder : UIResponder, IUITextInput
public partial class AvaloniaView
{
private class AvaloniaTextRange : UITextRange
{
private readonly NSRange _range;
public AvaloniaTextRange(NSRange range)
{
_range = range;
}
public override AvaloniaTextPosition Start => new AvaloniaTextPosition((int)_range.Location);
public override AvaloniaTextPosition End => new AvaloniaTextPosition((int)(_range.Location + _range.Length));
public NSRange Range => _range;
public override bool IsEmpty => _range.Length == 0;
}
private class AvaloniaTextPosition : UITextPosition
{
public AvaloniaTextPosition(nint index)
{
Index = index;
}
public nint Index { get; }
//[Export("inputDelegate")]
//public UITextInputDelegate InputDelegate { get; set; }
[Export("foo")]
public void Foo()
{
}
}
private UIResponder _nextResponder;
public AvaloniaResponder(AvaloniaView view, ITextInputMethodClient client)
{
_nextResponder = view;
_client = client;
_tokenizer = new UITextInputStringTokenizer(this);
}
public override UIResponder NextResponder => _nextResponder;
public override bool CanPerform(Selector action, NSObject? withSender)
{
return base.CanPerform(action, withSender);
}
private string _markedText = "";
private ITextInputMethodClient? _client;
private NSDictionary? _markedTextStyle;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer;
public ITextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
public override bool CanResignFirstResponder => true;
public override bool CanBecomeFirstResponder => true;
public override UIEditingInteractionConfiguration EditingInteractionConfiguration =>
UIEditingInteractionConfiguration.Default;
public override NSString TextInputContextIdentifier => new NSString("Test");
public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode;
[DllImport("/usr/lib/libobjc.dylib")]
extern static void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg);
private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:");
private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:");
private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:");
private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:");
private void ClientOnCursorRectangleChanged(object? sender, EventArgs e)
{
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle);
}
private void ClientOnSurroundingTextChanged(object? sender, EventArgs e)
{
objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle);
}
// Traits (Optional)
[Export("autocapitalizationType")] public UITextAutocapitalizationType AutocapitalizationType { get; private set; }
[Export("autocorrectionType")] public UITextAutocorrectionType AutocorrectionType { get; private set; }
[Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default;
[Export("keyboardAppearance")]
public UIKeyboardAppearance KeyboardAppearance { get; private set; } = UIKeyboardAppearance.Default;
[Export("returnKeyType")] public UIReturnKeyType ReturnKeyType { get; set; }
[Export("enablesReturnKeyAutomatically")] public bool EnablesReturnKeyAutomatically { get; set; }
[Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; }
[Export("spellCheckingType")]
public UITextSpellCheckingType SpellCheckingType { get; set; } = UITextSpellCheckingType.Default;
[Export("textContentType")]
public NSString TextContentType { get; set; }
[Export("smartQuotesType")]
public UITextSmartQuotesType SmartQuotesType { get; set; } = UITextSmartQuotesType.Default;
[Export("smartDashesType")]
public UITextSmartDashesType SmartDashesType { get; set; } = UITextSmartDashesType.Default;
[Export("smartInsertDeleteType")]
public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default;
[Export("passwordRules")]
public UITextInputPasswordRules PasswordRules { get; set; }
void IUIKeyInput.InsertText(string text)
{
if (_client == null)
{
return;
}
if (text == "\n")
{
// emulate return key released.
}
switch (ReturnKeyType)
{
case UIReturnKeyType.Done:
case UIReturnKeyType.Search:
case UIReturnKeyType.Go:
case UIReturnKeyType.Send:
ResignFirstResponder();
return;
}
// TODO replace this with _client.SetCommitText?
if (KeyboardDevice.Instance is { })
{
/*_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
0, InputRoot, text));*/
}
}
void IUIKeyInput.DeleteBackward()
{
if (KeyboardDevice.Instance is { })
{
// TODO: pass this through IME infrastructure instead of emulating a backspace press
/*_topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None));
_topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance,
0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None));*/
}
}
bool IUIKeyInput.HasText => true;
string IUITextInput.TextInRange(UITextRange range)
{
var text = _client.SurroundingText.Text;
if (!string.IsNullOrWhiteSpace(_markedText))
{
// todo check this combining _marked text with surrounding text.
int cursorPos = _client.SurroundingText.CursorOffset;
text = text[.. cursorPos] + _markedText + text[cursorPos ..];
}
var start = (int)(range.Start as AvaloniaTextPosition).Index;
var end = (int)(range.End as AvaloniaTextPosition).Index;
var result = text[start .. end];
return result;
}
void IUITextInput.ReplaceText(UITextRange range, string text)
{
((IUITextInput)this).SelectedTextRange = range;
// todo _client.SetCommitText(text);
if (KeyboardDevice.Instance is { })
{
/*_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
0, InputRoot, text));*/
}
}
void IUITextInput.SetMarkedText(string markedText, NSRange selectedRange)
{
_markedText = markedText;
// todo check this... seems correct
_client.SetPreeditText(markedText);
}
void IUITextInput.UnmarkText()
{
if (string.IsNullOrWhiteSpace(_markedText))
return;
// todo _client.CommitString (_markedText);
_markedText = "";
}
public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition)
{
if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t)
{
// todo check calculation.
var range = new NSRange(Math.Min(f.Index, t.Index), Math.Abs(t.Index - f.Index));
return new AvaloniaTextRange(range);
}
return null;
//throw new Exception();
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset)
{
if (fromPosition is AvaloniaTextPosition indexedPosition)
{
var end = indexedPosition.Index + offset;
// Verify position is valid in document.
//if (end > self.text.length || end < 0) {
// return nil;
//}
return new AvaloniaTextPosition(end);
}
return null;
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection, nint offset)
{
if (fromPosition is AvaloniaTextPosition f)
{
var newPosition = f.Index;
switch (inDirection)
{
case UITextLayoutDirection.Left:
newPosition -= offset;
break;
case UITextLayoutDirection.Right:
newPosition += offset;
break;
}
if (newPosition < 0)
{
newPosition = 0;
}
if (newPosition > _client.SurroundingText.Text.Length)
{
newPosition = _client.SurroundingText.Text.Length;
}
return new AvaloniaTextPosition(newPosition);
}
throw new Exception();
}
NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second)
{
if (first is AvaloniaTextPosition f && second is AvaloniaTextPosition s)
{
if (f.Index < s.Index)
return NSComparisonResult.Ascending;
if (f.Index > s.Index)
return NSComparisonResult.Descending;
return NSComparisonResult.Same;
}
throw new Exception();
}
nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition)
{
if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t)
{
return t.Index - f.Index;
}
throw new Exception();
}
UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction)
{
if (range is AvaloniaTextRange indexedRange)
{
nint position = 0;
switch (direction)
{
case UITextLayoutDirection.Up:
case UITextLayoutDirection.Left:
position = indexedRange.Range.Location;
break;
case UITextLayoutDirection.Down:
case UITextLayoutDirection.Right:
position = indexedRange.Range.Location + indexedRange.Range.Length;
break;
}
return new AvaloniaTextPosition(position);
}
throw new Exception();
}
UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction)
{
if (byExtendingPosition is AvaloniaTextPosition pos)
{
NSRange result = new NSRange();
switch (direction)
{
case UITextLayoutDirection.Up:
case UITextLayoutDirection.Left:
result = new NSRange(pos.Index - 1, 1);
break;
case UITextLayoutDirection.Right:
case UITextLayoutDirection.Down:
result = new NSRange(pos.Index, 1);
break;
}
return new AvaloniaTextRange(result);
}
private const string ImeLog = "IOSIME";
private Rect _cursorRect;
private TextInputOptions? _options;
throw new Exception();
}
NSWritingDirection IUITextInput.GetBaseWritingDirection(UITextPosition forPosition,
UITextStorageDirection direction)
{
return NSWritingDirection.LeftToRight;
// todo query and retyrn RTL.
}
void IUITextInput.SetBaseWritingDirectionforRange(NSWritingDirection writingDirection, UITextRange range)
{
// todo ? ignore?
}
CGRect IUITextInput.GetFirstRectForRange(UITextRange range)
{
if (_client == null)
return CGRect.Empty;
if (!string.IsNullOrWhiteSpace(_markedText))
{
return CGRect.Empty;
}
if (range is AvaloniaTextRange r)
{
// todo add ime apis to get cursor rect.
throw new NotImplementedException();
}
throw new Exception();
}
CGRect IUITextInput.GetCaretRectForPosition(UITextPosition? position)
{
var rect = _client.CursorRectangle;
return new CGRect(rect.X, rect.Y, rect.Width, rect.Height);
}
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point)
{
// TODO HitTest text?
throw new System.NotImplementedException();
}
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange)
{
// TODO HitTest text?
throw new System.NotImplementedException();
}
UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point)
{
return null;
}
UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range)
private static UIResponder? CurrentAvaloniaResponder { get; set; }
public override bool BecomeFirstResponder()
{
return null;
var res = base.BecomeFirstResponder();
if (res)
CurrentAvaloniaResponder = this;
return res;
}
[Export("textStylingAtPosition:inDirection:")]
public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction)
public override bool ResignFirstResponder()
{
return null;
var res = base.ResignFirstResponder();
if (res && ReferenceEquals(CurrentAvaloniaResponder, this))
CurrentAvaloniaResponder = null;
return res;
}
UITextRange? IUITextInput.SelectedTextRange
{
get
{
return new AvaloniaTextRange(new NSRange(
(nint)Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
(nint)Math.Abs(_client.SurroundingText.CursorOffset - _client.SurroundingText.AnchorOffset)));
}
set
{
throw new NotImplementedException();
}
}
private bool IsDrivingText => CurrentAvaloniaResponder is TextInputResponder t && ReferenceEquals(t.NextResponder, this);
NSDictionary? IUITextInput.MarkedTextStyle
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
get => _markedTextStyle;
set => _markedTextStyle = value;
}
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
_client = client;
if (_client == null && IsDrivingText)
BecomeFirstResponder();
UITextPosition IUITextInput.EndOfDocument
{
get
if (_client is { })
{
return new AvaloniaTextPosition(_client.SurroundingText.Text.Length + _markedText.Length);
new TextInputResponder(this, _client).BecomeFirstResponder();
}
}
void ITextInputMethodImpl.SetCursorRect(Rect rect) => _cursorRect = rect;
public NSObject? WeakInputDelegate
{
get;
set;
}
NSObject IUITextInput.WeakTokenizer => _tokenizer;
void ITextInputMethodImpl.SetOptions(TextInputOptions options) => _options = options;
UITextRange IUITextInput.MarkedTextRange
void ITextInputMethodImpl.Reset()
{
get
{
if (_client == null || string.IsNullOrWhiteSpace(_markedText))
{
return null;
}
// todo
return new AvaloniaTextRange(new NSRange(
(nint)Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
(nint)Math.Abs(_client.SurroundingText.CursorOffset - _client.SurroundingText.AnchorOffset)));
}
if (IsDrivingText)
BecomeFirstResponder();
}
}

113
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -26,7 +26,6 @@ namespace Avalonia.iOS
private EmbeddableControlRoot _topLevel;
private TouchHandler _touches;
private ITextInputMethodClient _client;
private bool _isActive;
public AvaloniaView()
{
@ -155,117 +154,5 @@ namespace Avalonia.iOS
get => (Control)_topLevel.Content;
set => _topLevel.Content = value;
}
ITextInputMethodClient ITextInputMethodImpl.Client => _client;
bool ITextInputMethodImpl.IsActive => _isActive;
private AvaloniaResponder _currentResponder;
void ITextInputMethodImpl.SetClient(ITextInputMethodClient client)
{
if (_client != null)
{
//_client.CursorRectangleChanged -= ClientOnCursorRectangleChanged;
}
_client = client;
if (_client is { })
{
if (_currentResponder != null && _currentResponder.IsFirstResponder)
{
_currentResponder.ResignFirstResponder();
_currentResponder = null;
}
_currentResponder = new AvaloniaResponder(this, _client);
//_client.CursorRectangleChanged += ClientOnCursorRectangleChanged;
//_client.SurroundingTextChanged += ClientOnSurroundingTextChanged;
_currentResponder.BecomeFirstResponder();
var x = _currentResponder.IsFirstResponder;
}
else
{
_currentResponder.ResignFirstResponder();
}
}
void ITextInputMethodImpl.SetCursorRect(Rect rect)
{
// maybe this will be cursor / selection rect?
}
void ITextInputMethodImpl.SetOptions(TextInputOptions options)
{
/*IsSecureEntry = false;
switch (options.ContentType)
{
case TextInputContentType.Normal:
KeyboardType = UIKeyboardType.Default;
break;
case TextInputContentType.Alpha:
KeyboardType = UIKeyboardType.AsciiCapable;
break;
case TextInputContentType.Digits:
KeyboardType = UIKeyboardType.PhonePad;
break;
case TextInputContentType.Pin:
KeyboardType = UIKeyboardType.NumberPad;
IsSecureEntry = true;
break;
case TextInputContentType.Number:
KeyboardType = UIKeyboardType.PhonePad;
break;
case TextInputContentType.Email:
KeyboardType = UIKeyboardType.EmailAddress;
break;
case TextInputContentType.Url:
KeyboardType = UIKeyboardType.Url;
break;
case TextInputContentType.Name:
KeyboardType = UIKeyboardType.NamePhonePad;
break;
case TextInputContentType.Password:
KeyboardType = UIKeyboardType.Default;
IsSecureEntry = true;
break;
case TextInputContentType.Social:
KeyboardType = UIKeyboardType.Twitter;
break;
case TextInputContentType.Search:
KeyboardType = UIKeyboardType.WebSearch;
break;
}
if (options.IsSensitive)
{
IsSecureEntry = true;
}
ReturnKeyType = (UIReturnKeyType)options.ReturnKeyType;
AutocorrectionType = UITextAutocorrectionType.Yes;
SpellCheckingType = UITextSpellCheckingType.Yes;
KeyboardAppearance = UIKeyboardAppearance.Alert;*/
}
void ITextInputMethodImpl.Reset()
{
_currentResponder?.ResignFirstResponder();
}
}
}

40
src/iOS/Avalonia.iOS/CombinedSpan3.cs

@ -0,0 +1,40 @@
using System;
using Avalonia.Controls.Documents;
namespace Avalonia.iOS;
internal ref struct CombinedSpan3<T>
{
public ReadOnlySpan<T> Span1, Span2, Span3;
public CombinedSpan3(ReadOnlySpan<T> span1, ReadOnlySpan<T> span2, ReadOnlySpan<T> span3)
{
Span1 = span1;
Span2 = span2;
Span3 = span3;
}
public int Length => Span1.Length + Span2.Length + Span3.Length;
void CopyFromSpan(ReadOnlySpan<T> from, ref int offset, ref Span<T> to)
{
if(to.Length == 0)
return;
if (offset < from.Length)
{
var copyNow = Math.Min(from.Length - offset, to.Length);
from.Slice(offset, copyNow).CopyTo(to);
to = to.Slice(copyNow);
offset = 0;
}
else
offset -= from.Length;
}
public void CopyTo(Span<T> to, int offset)
{
CopyFromSpan(Span1, ref offset, ref to);
CopyFromSpan(Span2, ref offset, ref to);
CopyFromSpan(Span3, ref offset, ref to);
}
}

71
src/iOS/Avalonia.iOS/TextInputResponder.Properties.cs

@ -0,0 +1,71 @@
#nullable enable
using Avalonia.Input.TextInput;
using Foundation;
using UIKit;
namespace Avalonia.iOS;
partial class AvaloniaView
{
partial class TextInputResponder
{
[Export("autocapitalizationType")]
public UITextAutocapitalizationType AutocapitalizationType { get; private set; }
[Export("autocorrectionType")]
public UITextAutocorrectionType AutocorrectionType => UITextAutocorrectionType.Yes;
[Export("keyboardType")]
public UIKeyboardType KeyboardType =>
_view._options == null ?
UIKeyboardType.Default :
_view._options.ContentType switch
{
TextInputContentType.Alpha => UIKeyboardType.AsciiCapable,
TextInputContentType.Digits or TextInputContentType.Number => UIKeyboardType.NumberPad,
TextInputContentType.Pin => UIKeyboardType.NumberPad,
TextInputContentType.Email => UIKeyboardType.EmailAddress,
TextInputContentType.Url => UIKeyboardType.Url,
TextInputContentType.Name => UIKeyboardType.NamePhonePad,
TextInputContentType.Social => UIKeyboardType.Twitter,
TextInputContentType.Search => UIKeyboardType.WebSearch,
_ => UIKeyboardType.Default
};
[Export("keyboardAppearance")]
public UIKeyboardAppearance KeyboardAppearance => UIKeyboardAppearance.Alert;
[Export("returnKeyType")] public UIReturnKeyType ReturnKeyType => UIReturnKeyType.Default;
[Export("enablesReturnKeyAutomatically")]
public bool EnablesReturnKeyAutomatically { get; set; }
[Export("isSecureTextEntry")] public bool IsSecureEntry =>
_view._options?.ContentType is TextInputContentType.Password or TextInputContentType.Pin
|| (_view._options?.IsSensitive ?? false);
[Export("spellCheckingType")] public UITextSpellCheckingType SpellCheckingType => UITextSpellCheckingType.Yes;
[Export("textContentType")] public NSString TextContentType { get; set; } = new NSString("text/plain");
[Export("smartQuotesType")]
public UITextSmartQuotesType SmartQuotesType { get; set; } = UITextSmartQuotesType.Default;
[Export("smartDashesType")]
public UITextSmartDashesType SmartDashesType { get; set; } = UITextSmartDashesType.Default;
[Export("smartInsertDeleteType")]
public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default;
[Export("passwordRules")] public UITextInputPasswordRules PasswordRules { get; set; } = null!;
public NSObject? WeakInputDelegate
{
get;
set;
}
NSObject IUITextInput.WeakTokenizer => _tokenizer;
}
}

471
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -0,0 +1,471 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Foundation;
using ObjCRuntime;
using Avalonia.Input.TextInput;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Logging;
using CoreGraphics;
using UIKit;
// ReSharper disable InconsistentNaming
// ReSharper disable StringLiteralTypo
namespace Avalonia.iOS;
#nullable enable
partial class AvaloniaView
{
[Adopts("UITextInput")]
[Adopts("UITextInputTraits")]
[Adopts("UIKeyInput")]
partial class TextInputResponder : UIResponder, IUITextInput
{
private class AvaloniaTextRange : UITextRange, INSCopying
{
private UITextPosition? _start;
private UITextPosition? _end;
public int StartIndex { get; }
public int EndIndex { get; }
public AvaloniaTextRange(int startIndex, int endIndex)
{
if (startIndex < 0)
throw new ArgumentOutOfRangeException(nameof(startIndex));
if (endIndex < startIndex)
throw new ArgumentOutOfRangeException(nameof(endIndex));
StartIndex = startIndex;
EndIndex = endIndex;
}
public override bool IsEmpty => StartIndex == EndIndex;
public override UITextPosition Start => _start ??= new AvaloniaTextPosition(StartIndex);
public override UITextPosition End => _end ??= new AvaloniaTextPosition(EndIndex);
public NSObject Copy(NSZone? zone)
{
return new AvaloniaTextRange(StartIndex, EndIndex);
}
}
private class AvaloniaTextPosition : UITextPosition, INSCopying
{
public AvaloniaTextPosition(int index)
{
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index));
Index = index;
}
public int Index { get; }
public NSObject Copy(NSZone? zone) => new AvaloniaTextPosition(Index);
}
public TextInputResponder(AvaloniaView view, ITextInputMethodClient client)
{
_view = view;
NextResponder = view;
_client = client;
_tokenizer = new UITextInputStringTokenizer(this);
}
public override UIResponder NextResponder { get; }
private readonly ITextInputMethodClient _client;
private int _inSurroundingTextUpdateEvent;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer;
public ITextInputMethodClient? Client => _client;
public override bool CanResignFirstResponder => true;
public override bool CanBecomeFirstResponder => true;
public override UIEditingInteractionConfiguration EditingInteractionConfiguration =>
UIEditingInteractionConfiguration.Default;
public override NSString TextInputContextIdentifier => new NSString(Guid.NewGuid().ToString());
public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode;
[DllImport("/usr/lib/libobjc.dylib")]
private static extern void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg);
private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:");
private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:");
private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:");
private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:");
private readonly AvaloniaView _view;
private string? _markedText;
private void SurroundingTextChanged(object? sender, EventArgs e)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "SurroundingTextChanged");
if (WeakInputDelegate == null)
return;
_inSurroundingTextUpdateEvent++;
try
{
objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle);
objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle);
}
finally
{
_inSurroundingTextUpdateEvent--;
}
}
private void KeyPress(Key ev)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", ev);
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
RawKeyEventType.KeyDown, ev, RawInputModifiers.None));
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
RawKeyEventType.KeyUp, ev, RawInputModifiers.None));
}
private void TextInput(string text)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering text input {text}", text);
_view._topLevelImpl.Input(new RawTextInputEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, text));
}
void IUIKeyInput.InsertText(string text)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.InsertText {text}", text);
if (text == "\n")
{
KeyPress(Key.Enter);
return;
}
TextInput(text);
}
void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back);
bool IUIKeyInput.HasText => true;
string IUITextInput.TextInRange(UITextRange range)
{
var r = (AvaloniaTextRange)range;
var s = _client.SurroundingText;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.TextInRange {start} {end}", r.StartIndex, r.EndIndex);
string result = "";
if(string.IsNullOrEmpty(_markedText))
result = s.Text[r.StartIndex .. r.EndIndex];
else
{
var span = new CombinedSpan3<char>(s.Text.AsSpan().Slice(0, s.CursorOffset),
_markedText,
s.Text.AsSpan().Slice(s.CursorOffset));
var buf = new char[r.EndIndex - r.StartIndex];
span.CopyTo(buf, r.StartIndex);
result = new string(buf);
}
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "result: {res}", result);
return result;
}
void IUITextInput.ReplaceText(UITextRange range, string text)
{
var r = (AvaloniaTextRange)range;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUIKeyInput.ReplaceText {start} {end} {text}", r.StartIndex, r.EndIndex, text);
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex);
TextInput(text);
}
void IUITextInput.SetMarkedText(string markedText, NSRange selectedRange)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUIKeyInput.SetMarkedText {start} {len} {text}", selectedRange.Location,
selectedRange.Location, markedText);
_markedText = markedText;
_client.SetPreeditText(markedText);
}
void IUITextInput.UnmarkText()
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.UnmarkText");
if(_markedText == null)
return;
var commitString = _markedText;
_markedText = null;
_client.SetPreeditText(null);
if (string.IsNullOrWhiteSpace(commitString))
return;
TextInput(commitString);
}
public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition)
{
var f = (AvaloniaTextPosition)fromPosition;
var t = (AvaloniaTextPosition)toPosition;
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "IUIKeyInput.GetTextRange {start} {end}", f.Index, t.Index);
return new AvaloniaTextRange(f.Index, t.Index);
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset)
{
var pos = (AvaloniaTextPosition)fromPosition;
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, "IUIKeyInput.GetPosition {start} {offset}", pos.Index, (int)offset);
var res = GetPositionCore(pos, offset);
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index));
return res!;
}
private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition pos, nint offset)
{
var end = pos.Index + (int)offset;
if (end < 0)
return null!;
if (end > DocumentLength)
return null;
return new AvaloniaTextPosition(end);
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection,
nint offset)
{
var pos = (AvaloniaTextPosition)fromPosition;
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, "IUIKeyInput.GetPosition {start} {direction} {offset}", pos.Index, inDirection, (int)offset);
var res = GetPositionCore(pos, inDirection, offset);
Logger.TryGet(LogEventLevel.Debug, ImeLog)
?.Log(null, $"res: " + (res == null ? "null" : (int)res.Index));
return res!;
}
private AvaloniaTextPosition? GetPositionCore(AvaloniaTextPosition fromPosition, UITextLayoutDirection inDirection,
nint offset)
{
var f = (AvaloniaTextPosition)fromPosition;
var newPosition = f.Index;
switch (inDirection)
{
case UITextLayoutDirection.Left:
newPosition -= (int)offset;
break;
case UITextLayoutDirection.Right:
newPosition += (int)offset;
break;
}
if (newPosition < 0)
return null!;
if (newPosition > DocumentLength)
return null!;
return new AvaloniaTextPosition(newPosition);
}
NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second)
{
var f = (AvaloniaTextPosition)first;
var s = (AvaloniaTextPosition)second;
if (f.Index < s.Index)
return NSComparisonResult.Ascending;
if (f.Index > s.Index)
return NSComparisonResult.Descending;
return NSComparisonResult.Same;
}
nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition)
{
var f = (AvaloniaTextPosition)fromPosition;
var t = (AvaloniaTextPosition)toPosition;
return t.Index - f.Index;
}
UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction)
{
var r = (AvaloniaTextRange)range;
if (direction is UITextLayoutDirection.Right or UITextLayoutDirection.Down)
return r.End;
return r.Start;
}
UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction)
{
var p = (AvaloniaTextPosition)byExtendingPosition;
if (direction is UITextLayoutDirection.Left or UITextLayoutDirection.Up)
return new AvaloniaTextRange(0, p.Index);
return new AvaloniaTextRange(p.Index, DocumentLength);
}
NSWritingDirection IUITextInput.GetBaseWritingDirection(UITextPosition forPosition,
UITextStorageDirection direction)
{
return NSWritingDirection.LeftToRight;
// todo query and retyrn RTL.
}
void IUITextInput.SetBaseWritingDirectionforRange(NSWritingDirection writingDirection, UITextRange range)
{
// todo ? ignore?
}
CGRect IUITextInput.GetFirstRectForRange(UITextRange range)
{
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetFirstRectForRange");
// TODO: Query from the input client
var r = _view._cursorRect;
return new CGRect(r.Left, r.Top, r.Width, r.Height);
}
CGRect IUITextInput.GetCaretRectForPosition(UITextPosition? position)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetCaretRectForPosition");
var rect = _client.CursorRectangle;
return new CGRect(rect.X, rect.Y, rect.Width, rect.Height);
}
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetClosestPositionToPoint");
return new AvaloniaTextPosition(0);
}
UITextPosition IUITextInput.GetClosestPositionToPoint(CGPoint point, UITextRange withinRange)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetClosestPositionToPoint");
return new AvaloniaTextPosition(0);
}
UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetCharacterRangeAtPoint");
return new AvaloniaTextRange(0, 0);
}
UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range)
{
// TODO: Query from the input client
Logger.TryGet(LogEventLevel.Debug, ImeLog)?
.Log(null, "IUITextInput:GetSelectionRect");
return new UITextSelectionRect[0];
}
[Export("textStylingAtPosition:inDirection:")]
public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction)
{
return null!;
}
UITextRange? IUITextInput.SelectedTextRange
{
get
{
return new AvaloniaTextRange(
Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset));
}
set
{
if (_inSurroundingTextUpdateEvent > 0)
return;
if (value == null)
_client.SelectInSurroundingText(_client.SurroundingText.CursorOffset,
_client.SurroundingText.CursorOffset);
else
{
var r = (AvaloniaTextRange)value;
_client.SelectInSurroundingText(r.StartIndex, r.EndIndex);
}
}
}
NSDictionary? IUITextInput.MarkedTextStyle
{
get => null;
set {}
}
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
private int DocumentLength => _client.SurroundingText.Text.Length + (_markedText?.Length ?? 0);
UITextPosition IUITextInput.EndOfDocument => new AvaloniaTextPosition(DocumentLength);
UITextRange IUITextInput.MarkedTextRange
{
get
{
if (string.IsNullOrWhiteSpace(_markedText))
return null!;
return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset + _markedText.Length);
}
}
public override bool BecomeFirstResponder()
{
var res = base.BecomeFirstResponder();
if (res)
{
Logger.TryGet(LogEventLevel.Debug, "IOSIME")
?.Log(null, "Became first responder");
_client.SurroundingTextChanged += SurroundingTextChanged;
CurrentAvaloniaResponder = this;
}
return res;
}
public override bool ResignFirstResponder()
{
var res = base.ResignFirstResponder();
if (res && ReferenceEquals(CurrentAvaloniaResponder, this))
{
Logger.TryGet(LogEventLevel.Debug, "IOSIME")
?.Log(null, "Resigned first responder");
_client.SurroundingTextChanged -= SurroundingTextChanged;
CurrentAvaloniaResponder = null;
}
return res;
}
}
}
Loading…
Cancel
Save