diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 41d1534f8d..048cc9a6b0 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -43,6 +43,9 @@ 523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; }; 5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; }; 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; }; + 8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */; }; + 8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */; }; + 8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */; }; AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; }; AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; }; AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; }; @@ -95,6 +98,9 @@ 5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = ""; }; 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = ""; }; 5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = ""; }; + 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethodDelegate.h; sourceTree = ""; }; + 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethod.h; sourceTree = ""; }; + 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnTextInputMethod.mm; sourceTree = ""; }; AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; }; AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -140,6 +146,9 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, + 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, + 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */, BC11A5BC2608D58F0017BAD0 /* automation.h */, BC11A5BD2608D58F0017BAD0 /* automation.mm */, 1A1852DB23E05814008F0DED /* deadlock.mm */, @@ -210,6 +219,8 @@ 1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */, 183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */, 18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */, + 8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */, + 8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */, 18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */, 18391ED5F611FF62C45F196D /* AvnView.h in Headers */, 18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */, @@ -289,6 +300,7 @@ BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */, 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, + 8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, 1AFD334123E03C4F0042899B /* controlhost.mm in Sources */, 1A465D10246AB61600C5858B /* dnd.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/AvnTextInputMethod.h b/native/Avalonia.Native/src/OSX/AvnTextInputMethod.h new file mode 100644 index 0000000000..4e5116ee71 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnTextInputMethod.h @@ -0,0 +1,46 @@ +// +// AvnTextInputMethod.h +// Avalonia.Native.OSX +// +// Created by Benedikt Stebner on 22.11.22. +// Copyright © 2022 Avalonia. All rights reserved. +// + +#ifndef AvnTextInputMethod_h +#define AvnTextInputMethod_h + +#import + +#include "com.h" +#include "comimpl.h" +#include "avalonia-native.h" +#import "AvnTextInputMethodDelegate.h" + +class AvnTextInputMethod: public virtual ComObject, public virtual IAvnTextInputMethod{ +private: + id _inputMethodDelegate; +public: + FORWARD_IUNKNOWN() + + BEGIN_INTERFACE_MAP() + INTERFACE_MAP_ENTRY(IAvnTextInputMethod, IID_IAvnTextInputMethod) + END_INTERFACE_MAP() + + virtual ~AvnTextInputMethod(); + + AvnTextInputMethod(id inputMethodDelegate); + + bool IsActive (); + + HRESULT SetClient (IAvnTextInputMethodClient* client) override; + + virtual void Reset () override; + + virtual void SetCursorRect (AvnRect rect) override; + + virtual void SetSurroundingText (char* text, int anchorOffset, int cursorOffset) override; + +public: + ComPtr Client; +}; +#endif /* AvnTextInputMethod_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm b/native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm new file mode 100644 index 0000000000..8c3ae080fa --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm @@ -0,0 +1,41 @@ +// +// AvnTextInputMethod.mm +// Avalonia.Native.OSX +// +// Created by Benedikt Stebner on 23.11.22. +// Copyright © 2022 Avalonia. All rights reserved. +// + +#include "AvnTextInputMethod.h" + +AvnTextInputMethod::~AvnTextInputMethod() { + Client = nullptr; +} + +AvnTextInputMethod::AvnTextInputMethod(id inputMethodDelegate) { + _inputMethodDelegate = inputMethodDelegate; +} + +bool AvnTextInputMethod::IsActive() { + return Client != nullptr; +} + +HRESULT AvnTextInputMethod::SetClient(IAvnTextInputMethodClient *client) { + START_COM_CALL; + + Client = client; + + return S_OK; +} + +void AvnTextInputMethod::Reset() { +} + +void AvnTextInputMethod::SetSurroundingText(char* text, int anchorOffset, int cursorOffset) { + [_inputMethodDelegate setText:[NSString stringWithUTF8String:text]]; + [_inputMethodDelegate setSelection: anchorOffset : cursorOffset]; +} + +void AvnTextInputMethod::SetCursorRect(AvnRect rect) { + [_inputMethodDelegate setCursorRect: rect]; +} diff --git a/native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h b/native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h new file mode 100644 index 0000000000..9f321ca595 --- /dev/null +++ b/native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h @@ -0,0 +1,20 @@ +// +// AvnTextInputMethodHost.h +// Avalonia.Native.OSX +// +// Created by Benedikt Stebner on 24.11.22. +// Copyright © 2022 Avalonia. All rights reserved. +// + +#ifndef AvnTextInputMethodHost_h +#define AvnTextInputMethodHost_h + +@protocol AvnTextInputMethodDelegate +@required +-(void) setText:(NSString* _Nonnull) text; +-(void) setCursorRect:(AvnRect) cursorRect; +-(void) setSelection: (int) start : (int) end; + +@end + +#endif /* AvnTextInputMethodHost_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnView.h b/native/Avalonia.Native/src/OSX/AvnView.h index 86a68d34c5..256caa70e9 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.h +++ b/native/Avalonia.Native/src/OSX/AvnView.h @@ -5,8 +5,6 @@ #pragma once #import - -#import #import #include "common.h" #include "WindowImpl.h" @@ -14,7 +12,7 @@ @class AvnAccessibilityElement; -@interface AvnView : NSView +@interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; @@ -24,4 +22,4 @@ -(AvnPlatformResizeReason) getResizeReason; -(void) setResizeReason:(AvnPlatformResizeReason)reason; + (AvnPoint)toAvnPoint:(CGPoint)p; -@end \ No newline at end of file +@end diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 4ae6ad5a00..11155afb2b 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -12,6 +12,7 @@ { ComPtr _parent; NSTrackingArea* _area; + NSMutableAttributedString* _markedText; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; @@ -20,6 +21,9 @@ NSObject* _renderTarget; AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; + AvnRect _cursorRect; + NSString* _text; + NSRange _selection; } - (void)onClosed @@ -560,11 +564,13 @@ - (BOOL)hasMarkedText { - return _lastKeyHandled; + return [_markedText length] > 0; } - (NSRange)markedRange { + if([_markedText length] > 0) + return NSMakeRange(0, [_markedText length] - 1); return NSMakeRange(NSNotFound, 0); } @@ -575,12 +581,31 @@ - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { - + if([string isKindOfClass:[NSAttributedString class]]) + { + _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + } + else + { + _markedText = [[NSMutableAttributedString alloc] initWithString:string]; + } + + if(!_parent->InputMethod->IsActive()){ + return; + } + + _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]); } - (void)unmarkText { - + [[_markedText mutableString] setString:@""]; + + if(!_parent->InputMethod->IsActive()){ + return; + } + + _parent->InputMethod->Client->SetPreeditText(nullptr); } - (NSArray *)validAttributesForMarkedText @@ -606,14 +631,16 @@ - (NSUInteger)characterIndexForPoint:(NSPoint)point { - return 0; + return NSNotFound; } - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - CGRect result = { 0 }; - - return result; + if(!_parent->InputMethod->IsActive()){ + return NSZeroRect; + } + + return ToNSRect(_cursorRect); } - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info @@ -718,4 +745,16 @@ return [[self accessibilityChild] accessibilityFocusedUIElement]; } +- (void) setText:(NSString *)text{ + _text = text; +} + +- (void) setSelection:(int)start :(int)end{ + _selection = NSMakeRange(start, end - start); +} + +- (void) setCursorRect:(AvnRect)rect{ + _cursorRect = rect; +} + @end diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h index 4c2758f6c6..b2a9d32480 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.h +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.h @@ -8,6 +8,7 @@ #include "rendertarget.h" #include "INSWindowHolder.h" +#include "AvnTextInputMethod.h" @class AutoFitContentView; @class AvnMenu; @@ -103,6 +104,8 @@ BEGIN_INTERFACE_MAP() id GetWindowProtocol (); virtual void BringToFront (); + + virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override; protected: virtual NSWindowStyleMask GetStyle(); @@ -131,6 +134,7 @@ public: NSObject *renderTarget; NSWindow * Window; ComPtr BaseEvents; + ComPtr InputMethod; AvnView *View; }; diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 038e9a048c..9a6aa6ed2c 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -14,6 +14,7 @@ #import "WindowProtocol.h" #import "WindowInterfaces.h" #include "WindowBaseImpl.h" +#include "AvnTextInputMethod.h" WindowBaseImpl::~WindowBaseImpl() { @@ -28,6 +29,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl, _glContext = gl; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl]; View = [[AvnView alloc] initWithParent:this]; + InputMethod = new AvnTextInputMethod(View); StandardContainer = [[AutoFitContentView new] initWithContent:View]; lastPositionSet = { 0, 0 }; @@ -612,6 +614,14 @@ void WindowBaseImpl::BringToFront() // do nothing. } +HRESULT WindowBaseImpl::GetInputMethod(IAvnTextInputMethod **retOut) { + START_COM_CALL; + + *retOut = InputMethod; + + return S_OK; +} + extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl) { @autoreleasepool diff --git a/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs new file mode 100644 index 0000000000..b216970af3 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs @@ -0,0 +1,118 @@ +using System; +using Avalonia.Input.TextInput; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable + { + private ITextInputMethodClient _client; + private IAvnTextInputMethodClient _nativeClient; + private readonly IAvnTextInputMethod _inputMethod; + + public AvaloniaNativeTextInputMethod(IAvnWindowBase nativeWindow) + { + _inputMethod = nativeWindow.InputMethod; + } + + public void Dispose() + { + _inputMethod.Dispose(); + _nativeClient?.Dispose(); + } + + public void Reset() + { + _inputMethod.Reset(); + } + + public void SetClient(ITextInputMethodClient client) + { + if (_client is { SupportsSurroundingText: true }) + { + _client.SurroundingTextChanged -= OnSurroundingTextChanged; + _client.CursorRectangleChanged -= OnCursorRectangleChanged; + + _nativeClient?.Dispose(); + } + + _nativeClient = null; + _client = client; + + if (client != null) + { + _nativeClient = new AvnTextInputMethodClient(client); + + OnSurroundingTextChanged(this, EventArgs.Empty); + OnCursorRectangleChanged(this, EventArgs.Empty); + + _client.SurroundingTextChanged += OnSurroundingTextChanged; + _client.CursorRectangleChanged += OnCursorRectangleChanged; + } + + _inputMethod.SetClient(_nativeClient); + } + + private void OnCursorRectangleChanged(object sender, EventArgs e) + { + if (_client == null) + { + return; + } + + _inputMethod.SetCursorRect(_client.CursorRectangle.ToAvnRect()); + } + + private void OnSurroundingTextChanged(object sender, EventArgs e) + { + if (_client == null) + { + return; + } + + var surroundingText = _client.SurroundingText; + + _inputMethod.SetSurroundingText( + surroundingText.Text, + surroundingText.AnchorOffset, + surroundingText.CursorOffset + ); + } + + public void SetCursorRect(Rect rect) + { + _inputMethod.SetCursorRect(rect.ToAvnRect()); + } + + public void SetOptions(TextInputOptions options) + { + + } + + private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient + { + private readonly ITextInputMethodClient _client; + + public AvnTextInputMethodClient(ITextInputMethodClient client) + { + _client = client; + } + + public void SetPreeditText(string preeditText) + { + if (_client.SupportsPreedit) + { + _client.SetPreeditText(preeditText); + } + } + + public void SelectInSurroundingText(int start, int end) + { + if (_client.SupportsSurroundingText) + { + _client.SelectInSurroundingText(start, end); + } + } + } + } +} diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index f27d94b61a..7de3523e5f 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -19,6 +19,7 @@ namespace Avalonia.Native private double _extendTitleBarHeight = -1; private DoubleClickHelper _doubleClickHelper; private readonly ITopLevelNativeMenuExporter _nativeMenuExporter; + private readonly AvaloniaNativeTextInputMethod _inputMethod; internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts, AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature) @@ -33,6 +34,8 @@ namespace Avalonia.Native } _nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory); + + _inputMethod = new AvaloniaNativeTextInputMethod(_native); } class WindowEvents : WindowBaseEvents, IAvnWindowEvents @@ -89,6 +92,8 @@ namespace Avalonia.Native _native.SetTitle(title ?? ""); } + public ITextInputMethodImpl TextInputMethod => _inputMethod; + public WindowState WindowState { get => (WindowState)_native.WindowState; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index a062fdc61d..b4e5fe683c 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -545,6 +545,7 @@ interface IAvnWindowBase : IUnknown IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle); HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode); HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode); + HRESULT GetInputMethod(IAvnTextInputMethod **ppv); } [uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits] @@ -611,6 +612,22 @@ interface IAvnWindowEvents : IAvnWindowBaseEvents void GotInputWhenDisabled(); } +[uuid(f2079145-a2d9-42b8-a85e-2732e3c2b055)] +interface IAvnTextInputMethodClient : IUnknown +{ + void SetPreeditText(char* preeditText); + void SelectInSurroundingText(int start, int length); +} + +[uuid(1382a29f-e260-4c7a-b83f-c99fc72e27c2)] +interface IAvnTextInputMethod : IUnknown +{ + HRESULT SetClient(IAvnTextInputMethodClient* client); + void Reset(); + void SetCursorRect(AvnRect rect); + void SetSurroundingText(char* text, int anchorOffset, int cursorOffset); +} + [uuid(e34ae0f8-18b4-48a3-b09d-2e6b19a3cf5e)] interface IAvnMacOptions : IUnknown {