Browse Source

add implementation of advanced ios ime api.

feature/ImePreeditText
Dan Walmsley 3 years ago
parent
commit
71ed1b03c1
  1. 424
      src/iOS/Avalonia.iOS/AvaloniaView.Text.cs
  2. 1
      src/iOS/Avalonia.iOS/AvaloniaView.cs

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

@ -1,75 +1,66 @@
using System;
using Foundation;
using ObjCRuntime;
using Avalonia.Input.TextInput;
using Avalonia.Input;
using Avalonia.Input.Raw;
using CoreGraphics;
using UIKit;
namespace Avalonia.iOS;
#nullable enable
[Adopts("UITextInput")]
[Adopts("UITextInputTraits")]
[Adopts("UIKeyInput")]
public partial class AvaloniaView : ITextInputMethodImpl
public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput
{
private IUITextInputDelegate _inputDelegate;
private ITextInputMethodClient? _client;
class TextInputHandler : UITextInputDelegate
private class AvaloniaTextRange : UITextRange
{
}
public ITextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
public override bool CanResignFirstResponder => true;
public override bool CanBecomeFirstResponder => true;
private readonly AvaloniaTextPosition _start;
private readonly AvaloniaTextPosition _end;
[Export("hasText")]
public bool HasText
{
get
public AvaloniaTextRange(int start, int end)
{
if (Client is { } && Client.SupportsSurroundingText &&
Client.SurroundingText.Text.Length > 0)
{
return true;
}
return false;
_start = new AvaloniaTextPosition(start);
_end = new AvaloniaTextPosition(end);
}
}
[Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default;
public override AvaloniaTextPosition Start => _start;
[Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; }
public override AvaloniaTextPosition End => _end;
}
[Export("insertText:")]
public void InsertText(string text)
private class AvaloniaTextPosition : UITextPosition
{
if (KeyboardDevice.Instance is { })
public AvaloniaTextPosition(int offset)
{
_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance,
0, InputRoot, text));
Offset = offset;
}
public int Offset { get; }
}
public IUITextInputDelegate InputDelegate => _inputDelegate;
private string _markedText = "";
private readonly IUITextInputDelegate _inputDelegate;
private ITextInputMethodClient? _client;
private NSDictionary? _markedTextStyle;
private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0);
private readonly UITextInputStringTokenizer _tokenizer;
[Export("deleteBackward")]
public void DeleteBackward()
private class TextInputHandler : UITextInputDelegate
{
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));
}
}
public ITextInputMethodClient? Client => _client;
public bool IsActive => _client != null;
public override bool CanResignFirstResponder => true;
public override bool CanBecomeFirstResponder => true;
void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client)
{
_client = client;
@ -86,36 +77,36 @@ public partial class AvaloniaView : ITextInputMethodImpl
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;
@ -123,20 +114,20 @@ public partial class AvaloniaView : ITextInputMethodImpl
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;
@ -148,8 +139,335 @@ public partial class AvaloniaView : ITextInputMethodImpl
}
}
void ITextInputMethodImpl.Reset()
{
ResignFirstResponder();
}
// Traits (Optional)
[Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default;
[Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; }
[Export("returnKeyType")] public UIReturnKeyType ReturnKeyType { 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 = (range.Start as AvaloniaTextPosition).Offset;
int end = (range.End as AvaloniaTextPosition).Offset;
return text[start .. end];
}
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 = "";
}
UITextRange IUITextInput.GetTextRange(UITextPosition fromPosition, UITextPosition toPosition)
{
if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t)
{
// todo check calculation.
return new AvaloniaTextRange(f.Offset, t.Offset);
}
throw new Exception();
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset)
{
if (fromPosition is AvaloniaTextPosition f)
{
var position = f.Offset;
int posPlusIndex = position + (int)offset;
var length = _client.SurroundingText.Text.Length;
if (posPlusIndex < 0 || posPlusIndex > length)
{
return null;
}
return new AvaloniaTextPosition(posPlusIndex);
}
throw new Exception();
}
UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection, nint offset)
{
if (fromPosition is AvaloniaTextPosition f)
{
var pos = f.Offset;
switch (inDirection)
{
case UITextLayoutDirection.Left:
return new AvaloniaTextPosition(pos - (int)offset);
case UITextLayoutDirection.Right:
return new AvaloniaTextPosition(pos + (int)offset);
default:
return fromPosition;
}
}
throw new Exception();
}
NSComparisonResult IUITextInput.ComparePosition(UITextPosition first, UITextPosition second)
{
if (first is AvaloniaTextPosition f && second is AvaloniaTextPosition s)
{
if (f.Offset > s.Offset)
return NSComparisonResult.Ascending;
if (f.Offset < s.Offset)
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.Offset - f.Offset;
}
throw new Exception();
}
UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction)
{
if (range is AvaloniaTextRange r)
{
switch (direction)
{
case UITextLayoutDirection.Right:
return r.End;
default:
return r.Start;
}
}
throw new Exception();
}
UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction)
{
if (byExtendingPosition is AvaloniaTextPosition p)
{
switch (direction)
{
case UITextLayoutDirection.Left:
return new AvaloniaTextRange(0, p.Offset);
default:
// todo check this.
return new AvaloniaTextRange(p.Offset, _client.SurroundingText.Text.Length);
}
}
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)
{
// TODO check if needed, hittest?
return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset);
}
UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range)
{
// todo?
return Array.Empty<UITextSelectionRect>();
}
UITextRange? IUITextInput.SelectedTextRange
{
get
{
return new AvaloniaTextRange(
Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset),
Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset));
}
set
{
throw new NotImplementedException();
}
}
NSDictionary? IUITextInput.MarkedTextStyle
{
get => _markedTextStyle;
set => _markedTextStyle = value;
}
UITextPosition IUITextInput.BeginningOfDocument => _beginningOfDocument;
UITextPosition IUITextInput.EndOfDocument
{
get
{
return new AvaloniaTextPosition(_client.SurroundingText.Text.Length + _markedText.Length);
}
}
NSObject? IUITextInput.WeakInputDelegate
{
get => _inputDelegate as TextInputHandler;
set => throw new NotSupportedException();
}
NSObject IUITextInput.WeakTokenizer => _tokenizer;
UITextRange IUITextInput.MarkedTextRange
{
get
{
if (string.IsNullOrWhiteSpace(_markedText))
{
return null;
}
return new AvaloniaTextRange(0, _markedText.Length);
}
}
}

1
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -32,6 +32,7 @@ namespace Avalonia.iOS
_touches = new TouchHandler(this, _topLevelImpl);
_topLevel = new EmbeddableControlRoot(_topLevelImpl);
_inputDelegate = new TextInputHandler();
_tokenizer = new UITextInputStringTokenizer(this);
_topLevel.Prepare();

Loading…
Cancel
Save