|
|
|
@ -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); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|