From decf863bc9020eb6534ac95234c03dc72f4777af Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Feb 2023 16:38:35 +0100 Subject: [PATCH 01/15] Initial macOS IME integration --- .../project.pbxproj | 12 ++ .../src/OSX/AvnTextInputMethod.h | 46 +++++++ .../src/OSX/AvnTextInputMethod.mm | 41 ++++++ .../src/OSX/AvnTextInputMethodDelegate.h | 20 +++ native/Avalonia.Native/src/OSX/AvnView.h | 6 +- native/Avalonia.Native/src/OSX/AvnView.mm | 53 ++++++-- .../Avalonia.Native/src/OSX/WindowBaseImpl.h | 4 + .../Avalonia.Native/src/OSX/WindowBaseImpl.mm | 10 ++ .../AvaloniaNativeTextInputMethod.cs | 118 ++++++++++++++++++ src/Avalonia.Native/WindowImpl.cs | 5 + src/Avalonia.Native/avn.idl | 17 +++ 11 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/AvnTextInputMethod.h create mode 100644 native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm create mode 100644 native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h create mode 100644 src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs 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 { From d69f4a86dc6e6b3dc3031eca000e6488a8a03a19 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Feb 2023 16:40:57 +0100 Subject: [PATCH 02/15] Add missing using --- src/Avalonia.Native/WindowImpl.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 7de3523e5f..d086799b6f 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; using Avalonia.Native.Interop; using Avalonia.OpenGL; using Avalonia.Platform; From 0fc25da0dc57fadae99169df57c5b04442c52cc8 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 12:35:13 +0100 Subject: [PATCH 03/15] [TextBox] MoveEnd now correctly positions the caret before the line break --- src/Avalonia.Controls/TextBox.cs | 2 +- .../TextBoxTests.cs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 78caf350b7..ac1b5cd417 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1736,7 +1736,7 @@ namespace Avalonia.Controls var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); var textLine = textLines[lineIndex]; - var textPosition = textLine.FirstTextSourceIndex + textLine.Length; + var textPosition = textLine.FirstTextSourceIndex + textLine.Length - textLine.NewLineLength; _presenter.MoveCaretToTextPosition(textPosition, true); } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 531a2869cd..3d8f823621 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1058,6 +1058,25 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Should_Move_Caret_To_EndOfLine() + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "AB\nAB" + }; + + tb.Measure(Size.Infinity); + + RaiseKeyEvent(tb, Key.End, KeyModifiers.Shift); + + Assert.Equal(2, tb.CaretIndex); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), From cb1d30648295fbd55bcbdc577bc8dbfe834b92ee Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 28 Feb 2023 15:35:43 +0100 Subject: [PATCH 04/15] Rework preedit handling --- .../Presenters/TextPresenter.cs | 33 ++++--------------- .../TextBoxTextInputMethodClient.cs | 6 ++-- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bb6b03d59a..14d8270647 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -564,7 +564,7 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; - if(_compositionRegion != null) + if (_compositionRegion != null) { var preeditHighlight = new ValueSpan(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0, new GenericTextRunProperties(typeface, FontSize, @@ -851,7 +851,7 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true) + private void UpdateCaret(CharacterHit characterHit, bool notify = true) { _lastCharacterHit = characterHit; @@ -879,10 +879,14 @@ namespace Avalonia.Controls.Presenters CaretBoundsChanged?.Invoke(this, EventArgs.Empty); } - if (updateCaretIndex) + if (notify) { SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } + else + { + _caretIndex = caretIndex; + } } internal Rect GetCursorRectangle() @@ -899,24 +903,6 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - protected void OnPreeditTextChanged(string? oldValue, string? newValue) - { - InvalidateTextLayout(); - - if (string.IsNullOrEmpty(newValue)) - { - UpdateCaret(_lastCharacterHit); - } - else - { - var textPosition = _caretIndex + newValue?.Length ?? 0; - - var characterHit = GetCharacterHitFromTextPosition(textPosition); - - UpdateCaret(characterHit, false); - } - } - private CharacterHit GetCharacterHitFromTextPosition(int textPosition) { var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true); @@ -935,11 +921,6 @@ namespace Avalonia.Controls.Presenters switch (change.Property.Name) { case nameof(PreeditText): - { - OnPreeditTextChanged(change.OldValue as string, change.NewValue as string); - break; - } - case nameof(CompositionRegion): case nameof(Foreground): case nameof(FontSize): diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 3a28836a99..347cac13ef 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -4,8 +4,6 @@ using Avalonia.Input.TextInput; using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.Utilities; -using Avalonia.VisualTree; -using static System.Net.Mime.MediaTypeNames; namespace Avalonia.Controls { @@ -161,11 +159,13 @@ namespace Avalonia.Controls public void SetPreeditText(string? text) { - if (_presenter == null) + if (_presenter == null || _parent == null) { return; } + _presenter.CaretIndex = _parent.CaretIndex; + _presenter.PreeditText = text; } From 397867602e4d718c0012fe613f4d005a9e2e1b56 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 11:29:27 +0100 Subject: [PATCH 05/15] Properly sync preedit --- .../Presenters/TextPresenter.cs | 50 ++++++----------- .../TextBoxTextInputMethodClient.cs | 56 ++++++++++++++++--- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 3 +- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 14d8270647..2e6f1a0f2d 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -10,6 +10,7 @@ using Avalonia.Layout; using Avalonia.Media.Immutable; using Avalonia.Controls.Documents; using Avalonia.Input.TextInput; +using Avalonia.Data; namespace Avalonia.Controls.Presenters { @@ -52,7 +53,7 @@ namespace Avalonia.Controls.Presenters AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, - (o, v) => o.Text = v); + (o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay); /// /// Defines the property. @@ -107,7 +108,7 @@ namespace Avalonia.Controls.Presenters private int _selectionStart; private int _selectionEnd; private bool _caretBlink; - private string? _text; + internal string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -526,22 +527,22 @@ namespace Avalonia.Controls.Presenters } } - private string? GetText() - { - if (!string.IsNullOrEmpty(_preeditText)) - { - if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length) - { - return _preeditText; - } + //private string? GetText() + //{ + // if (!string.IsNullOrEmpty(_preeditText)) + // { + // if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length) + // { + // return _preeditText; + // } - var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex); + // var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex); - return text; - } + // return text; + // } - return _text; - } + // return _text; + //} /// /// Creates the used to render the text. @@ -551,7 +552,7 @@ namespace Avalonia.Controls.Presenters { TextLayout result; - var text = GetText(); + var text = _text; var typeface = new Typeface(FontFamily, FontStyle, FontWeight); @@ -851,7 +852,7 @@ namespace Avalonia.Controls.Presenters CaretChanged(); } - private void UpdateCaret(CharacterHit characterHit, bool notify = true) + internal void UpdateCaret(CharacterHit characterHit, bool notify = true) { _lastCharacterHit = characterHit; @@ -883,10 +884,6 @@ namespace Avalonia.Controls.Presenters { SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex); } - else - { - _caretIndex = caretIndex; - } } internal Rect GetCursorRectangle() @@ -903,17 +900,6 @@ namespace Avalonia.Controls.Presenters _caretTimer.Tick -= CaretTimerTick; } - private CharacterHit GetCharacterHitFromTextPosition(int textPosition) - { - var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true); - - var textLine = TextLayout.TextLines[lineIndex]; - - var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1)); - - return characterHit; - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 347cac13ef..6f8f3f3cab 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics; using Avalonia.Controls.Presenters; using Avalonia.Input.TextInput; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Threading; using Avalonia.Utilities; @@ -75,7 +77,7 @@ namespace Avalonia.Controls { get => _textEditable; set { - if(_textEditable != null) + if (_textEditable != null) { _textEditable.TextChanged -= TextEditable_TextChanged; _textEditable.SelectionChanged -= TextEditable_SelectionChanged; @@ -84,7 +86,7 @@ namespace Avalonia.Controls _textEditable = value; - if(_textEditable != null) + if (_textEditable != null) { _textEditable.TextChanged += TextEditable_TextChanged; _textEditable.SelectionChanged += TextEditable_SelectionChanged; @@ -110,7 +112,7 @@ namespace Avalonia.Controls private void TextEditable_SelectionChanged(object? sender, EventArgs e) { - if(_parent != null && _textEditable != null) + if (_parent != null && _textEditable != null) { _parent.SelectionStart = _textEditable.SelectionStart; _parent.SelectionEnd = _textEditable.SelectionEnd; @@ -157,16 +159,53 @@ namespace Avalonia.Controls public event EventHandler? SurroundingTextChanged; - public void SetPreeditText(string? text) + private string? _presenterText; + private int _compositionStart; + + public void SetPreeditText(string? preeditText) { if (_presenter == null || _parent == null) { return; } - _presenter.CaretIndex = _parent.CaretIndex; + if (_presenterText is null) + { + _presenterText = _parent.Text ?? ""; + _compositionStart = _parent.CaretIndex; + } + + var text = GetText(preeditText); + + Debug.WriteLine(text); + + _presenter._text = text; + + _presenter.PreeditText = preeditText; + + _presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false); + + if (string.IsNullOrEmpty(preeditText)) + { + _presenterText = null; + } + } + + private string? GetText(string? preeditText) + { + if (string.IsNullOrEmpty(preeditText)) + { + return _presenterText; + } + + if (string.IsNullOrEmpty(_presenterText)) + { + return preeditText; + } + + var text = _presenterText.Substring(0, _compositionStart) + preeditText + _presenterText.Substring(_compositionStart); - _presenter.PreeditText = text; + return text; } public void SetComposingRegion(TextRange? region) @@ -175,6 +214,7 @@ namespace Avalonia.Controls { return; } + _presenter.CompositionRegion = region; } @@ -256,9 +296,9 @@ namespace Avalonia.Controls } } - if(e.Property == TextBox.TextProperty) + if (e.Property == TextBox.TextProperty) { - if(_textEditable != null) + if (_textEditable != null) { _textEditable.Text = (string?)e.NewValue; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index e8d2d8f0c5..1cc9d1fa2f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -713,13 +713,14 @@ namespace Avalonia.Win32 break; } + case WindowsMessage.WM_IME_SELECT: + break; case WindowsMessage.WM_IME_CHAR: case WindowsMessage.WM_IME_COMPOSITIONFULL: case WindowsMessage.WM_IME_CONTROL: case WindowsMessage.WM_IME_KEYDOWN: case WindowsMessage.WM_IME_KEYUP: case WindowsMessage.WM_IME_NOTIFY: - case WindowsMessage.WM_IME_SELECT: break; case WindowsMessage.WM_IME_STARTCOMPOSITION: Imm32InputMethod.Current.IsComposing = true; From 56727d00a0a4f018a2409e90863b627ddb36492a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 16:02:46 +0100 Subject: [PATCH 06/15] More adjustments --- .../Avalonia.Win32/Input/Imm32InputMethod.cs | 4 ++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 44 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 41417dd950..a777aa81b7 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -31,6 +31,8 @@ namespace Avalonia.Win32.Input public bool ShowCompositionWindow => false; + public string? Composition { get; internal set; } + public void CreateCaret() { _caretManager.TryCreate(Hwnd); @@ -283,6 +285,8 @@ namespace Avalonia.Win32.Input var composition = GetCompositionString(); + Composition = composition; + Client.SetPreeditText(composition); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 1cc9d1fa2f..6e12b9c4fd 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Avalonia.Automation.Peers; @@ -10,6 +11,7 @@ using Avalonia.Platform; using Avalonia.Threading; using Avalonia.Win32.Automation; using Avalonia.Win32.Input; +using Avalonia.Win32.Interop; using Avalonia.Win32.Interop.Automation; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -181,11 +183,17 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_CHAR: { + if (Imm32InputMethod.Current.IsComposing) + { + break; + } + // Ignore control chars and chars that were handled in WM_KEYDOWN. if (ToInt32(wParam) >= 32 && !_ignoreWmChar) { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, - new string((char)ToInt32(wParam), 1)); + var text = new string((char)ToInt32(wParam), 1); + + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); } break; @@ -709,8 +717,18 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_IME_COMPOSITION: { + var previousComposition = Imm32InputMethod.Current.Composition; + Imm32InputMethod.Current.CompositionChanged(); + //For Korean IME we need preserve commited text. + if (ToInt32(lParam) == (uint)GCS.GCS_RESULTSTR && ToInt32(wParam) >= 32) + { + Imm32InputMethod.Current.Composition = previousComposition; + + _ignoreWmChar = true; + } + break; } case WindowsMessage.WM_IME_SELECT: @@ -726,9 +744,27 @@ namespace Avalonia.Win32 Imm32InputMethod.Current.IsComposing = true; return IntPtr.Zero; case WindowsMessage.WM_IME_ENDCOMPOSITION: - Imm32InputMethod.Current.IsComposing = false; - break; + { + //In case composition has not been comitted yet we need to do that here. + if (!string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) + { + var text = Imm32InputMethod.Current.Composition; + + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); + + Imm32InputMethod.Current.Composition = null; + } + + //Cleanup composition state. + Imm32InputMethod.Current.IsComposing = false; + if (Imm32InputMethod.Current.IsActive) + { + Imm32InputMethod.Current.Client.SetPreeditText(null); + } + + break; + } case WindowsMessage.WM_GETOBJECT: if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control) { From d5cfc06f72f5baca45a1f6448d822e956e21683c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Mar 2023 16:04:58 +0100 Subject: [PATCH 07/15] Remove redundant code --- .../Presenters/TextPresenter.cs | 17 ----------------- .../TextBoxTextInputMethodClient.cs | 2 -- 2 files changed, 19 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 2e6f1a0f2d..089a2c1168 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -527,23 +527,6 @@ namespace Avalonia.Controls.Presenters } } - //private string? GetText() - //{ - // if (!string.IsNullOrEmpty(_preeditText)) - // { - // if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length) - // { - // return _preeditText; - // } - - // var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex); - - // return text; - // } - - // return _text; - //} - /// /// Creates the used to render the text. /// diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 6f8f3f3cab..deace4084c 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -177,8 +177,6 @@ namespace Avalonia.Controls var text = GetText(preeditText); - Debug.WriteLine(text); - _presenter._text = text; _presenter.PreeditText = preeditText; From 8457fba5a315ba18bffd27c74f8122e28c14ea8e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Mar 2023 06:11:39 +0100 Subject: [PATCH 08/15] More adjustments --- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 6e12b9c4fd..eaab0d5559 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -721,12 +721,40 @@ namespace Avalonia.Win32 Imm32InputMethod.Current.CompositionChanged(); - //For Korean IME we need preserve commited text. - if (ToInt32(lParam) == (uint)GCS.GCS_RESULTSTR && ToInt32(wParam) >= 32) + var gcs = (GCS)ToInt32(lParam); + + switch (gcs) { - Imm32InputMethod.Current.Composition = previousComposition; + case GCS.GCS_RESULTSTR: + { + if(ToInt32(wParam) >= 32) + { + Imm32InputMethod.Current.Composition = previousComposition; + + _ignoreWmChar = true; + } + break; + } + case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: + { + if (string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) + { + var c = (char)ToInt32(wParam); + + Imm32InputMethod.Current.Composition = new string(c, 1); - _ignoreWmChar = true; + _ignoreWmChar = true; + } + break; + } + case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: + { + Imm32InputMethod.Current.Composition = previousComposition; + + _ignoreWmChar = true; + + break; + } } break; @@ -741,6 +769,13 @@ namespace Avalonia.Win32 case WindowsMessage.WM_IME_NOTIFY: break; case WindowsMessage.WM_IME_STARTCOMPOSITION: + Imm32InputMethod.Current.Composition = null; + + if (Imm32InputMethod.Current.IsActive) + { + Imm32InputMethod.Current.Client.SetPreeditText(null); + } + Imm32InputMethod.Current.IsComposing = true; return IntPtr.Zero; case WindowsMessage.WM_IME_ENDCOMPOSITION: @@ -751,12 +786,11 @@ namespace Avalonia.Win32 var text = Imm32InputMethod.Current.Composition; e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); - - Imm32InputMethod.Current.Composition = null; } //Cleanup composition state. Imm32InputMethod.Current.IsComposing = false; + Imm32InputMethod.Current.Composition = null; if (Imm32InputMethod.Current.IsActive) { From 26263cd7adb4224990fc8c3ad2496524c03714b5 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Mar 2023 12:20:51 +0100 Subject: [PATCH 09/15] Fix cursor rect --- native/Avalonia.Native/src/OSX/AvnView.mm | 42 +++++++++++++++++------ src/Avalonia.Native/WindowImpl.cs | 9 +++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index 11155afb2b..ee8dbcedc1 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -21,8 +21,8 @@ NSObject* _renderTarget; AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; - AvnRect _cursorRect; - NSString* _text; + NSRect _cursorRect; + NSMutableString* _text; NSRange _selection; } @@ -525,7 +525,7 @@ - (void)keyDown:(NSEvent *)event { [self keyboardEvent:event withType:KeyDown]; - [[self inputContext] handleEvent:event]; + _lastKeyHandled = [[self inputContext] handleEvent:event]; [super keyDown:event]; } @@ -576,7 +576,7 @@ - (NSRange)selectedRange { - return NSMakeRange(NSNotFound, 0); + return _selection; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange @@ -606,6 +606,8 @@ } _parent->InputMethod->Client->SetPreeditText(nullptr); + + [[self inputContext] discardMarkedText]; } - (NSArray *)validAttributesForMarkedText @@ -615,18 +617,24 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - return [NSAttributedString new]; + return nullptr; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - if(!_lastKeyHandled) - { + //[_text replaceCharactersInRange:replacementRange withString:string]; + + [self unmarkText]; + + //if(!_lastKeyHandled) + //{ if(_parent != nullptr) { _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); } - } + //} + + [[self inputContext] invalidateCharacterCoordinates]; } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -640,7 +648,7 @@ return NSZeroRect; } - return ToNSRect(_cursorRect); + return _cursorRect; } - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info @@ -746,15 +754,27 @@ } - (void) setText:(NSString *)text{ - _text = text; + [_text setString:text]; + + [[self inputContext] discardMarkedText]; } - (void) setSelection:(int)start :(int)end{ _selection = NSMakeRange(start, end - start); + + [[self inputContext] invalidateCharacterCoordinates]; } - (void) setCursorRect:(AvnRect)rect{ - _cursorRect = rect; + NSRect cursorRect = ToNSRect(rect); + NSRect windowRectOnScreen = [[self window] convertRectToScreen:self.frame]; + + windowRectOnScreen.size = cursorRect.size; + windowRectOnScreen.origin = NSMakePoint(windowRectOnScreen.origin.x + cursorRect.origin.x, windowRectOnScreen.origin.y + self.frame.size.height - cursorRect.origin.y - cursorRect.size.height); + + _cursorRect = windowRectOnScreen; + + [[self inputContext] invalidateCharacterCoordinates]; } @end diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index cbfebbbec4..5d0e6a2d18 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -71,7 +71,7 @@ namespace Avalonia.Native } } - public IAvnWindow Native => _native; + public new IAvnWindow Native => _native; public void CanResize(bool value) { @@ -93,8 +93,6 @@ namespace Avalonia.Native _native.SetTitle(title ?? ""); } - public ITextInputMethodImpl TextInputMethod => _inputMethod; - public WindowState WindowState { get => (WindowState)_native.WindowState; @@ -235,6 +233,11 @@ namespace Avalonia.Native public override object TryGetFeature(Type featureType) { + if(featureType == typeof(ITextInputMethodImpl)) + { + return _inputMethod; + } + if (featureType == typeof(ITopLevelNativeMenuExporter)) { return _nativeMenuExporter; From 87e0ff2a51a884d47eb888885b1cb3731149b7c7 Mon Sep 17 00:00:00 2001 From: tkefauver Date: Sat, 4 Mar 2023 14:58:19 -0500 Subject: [PATCH 10/15] Flyout sample label fix --- samples/ControlCatalog/Pages/FlyoutsPage.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 54aa9d1b67..36e2e8f4f8 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -190,7 +190,7 @@ - - internal class TypeNameAndClassSelector : Selector + internal sealed class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; private List? _classes; @@ -85,12 +85,7 @@ namespace Avalonia.Styling /// public override string ToString(Style? owner) { - if (_selectorString == null) - { - _selectorString = BuildSelectorString(owner); - } - - return _selectorString; + return _selectorString ??= BuildSelectorString(owner); } /// diff --git a/tests/Avalonia.Benchmarks/Properties/launchSettings.json b/tests/Avalonia.Benchmarks/Properties/launchSettings.json new file mode 100644 index 0000000000..619d635ebf --- /dev/null +++ b/tests/Avalonia.Benchmarks/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "Avalonia.Benchmarks": { + "commandName": "Project" + }, + "Avalonia.Benchmarks (in-process)": { + "commandName": "Project", + "commandLineArgs": "--inprocess" + }, + "Avalonia.Benchmarks (debug)": { + "commandName": "Project", + "commandLineArgs": "--debug" + } + } +} diff --git a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs index 0ac96c1103..11bc5ce35f 100644 --- a/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls; +using System; +using Avalonia.Controls; using Avalonia.Styling; using BenchmarkDotNet.Attributes; @@ -11,6 +12,8 @@ namespace Avalonia.Benchmarks.Styling private readonly Calendar _matchingControl; private readonly Selector _isCalendarSelector; private readonly Selector _classSelector; + private readonly Selector _orSelectorTwo; + private readonly Selector _orSelectorFive; public SelectorBenchmark() { @@ -23,6 +26,14 @@ namespace Avalonia.Benchmarks.Styling _isCalendarSelector = Selectors.Is(null); _classSelector = Selectors.Class(null, className); + + _orSelectorTwo = Selectors.Or(new AlwaysMatchSelector(), new AlwaysMatchSelector()); + _orSelectorFive = Selectors.Or( + new AlwaysMatchSelector(), + new AlwaysMatchSelector(), + new AlwaysMatchSelector(), + new AlwaysMatchSelector(), + new AlwaysMatchSelector()); } [Benchmark] @@ -48,5 +59,40 @@ namespace Avalonia.Benchmarks.Styling { return _classSelector.Match(_matchingControl); } + + [Benchmark] + public SelectorMatch OrSelector_One_Match() + { + return _orSelectorTwo.Match(_matchingControl); + } + + [Benchmark] + public SelectorMatch OrSelector_Five_Match() + { + return _orSelectorFive.Match(_matchingControl); + } + } + + internal class AlwaysMatchSelector : Selector + { + public override bool InTemplate => false; + + public override bool IsCombinator => false; + + public override Type TargetType => null; + + public override string ToString(Style owner) + { + return "Always"; + } + + protected override SelectorMatch Evaluate(StyledElement control, IStyle parent, bool subscribe) + { + return SelectorMatch.AlwaysThisType; + } + + protected override Selector MovePrevious() => null; + + protected override Selector MovePreviousOrParent() => null; } } From c5189bd6f660c55ca95b04d08c855963c2a05a8f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Mar 2023 17:10:14 +0100 Subject: [PATCH 13/15] Trz to fix Japanese IME --- .../Avalonia.Win32/Input/Imm32InputMethod.cs | 22 +++++++++---------- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 21 +++++++++--------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index a777aa81b7..db650db4b0 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -271,30 +271,28 @@ namespace Avalonia.Win32.Input // we're skipping this. not usable on windows } - public void CompositionChanged() + public void CompositionChanged(string? composition) { - if (!IsComposing) - { - return; - } + Composition = composition; - if(!IsActive || !Client.SupportsPreedit) + if (!IsActive || !Client.SupportsPreedit) { return; } - var composition = GetCompositionString(); - - Composition = composition; - Client.SetPreeditText(composition); } - private string? GetCompositionString() + public string? GetCompositionString(GCS flag) { + if (!IsComposing) + { + return null; + } + var himc = ImmGetContext(Hwnd); - return ImmGetCompositionString(himc, GCS.GCS_COMPSTR); + return ImmGetCompositionString(himc, flag); } ~Imm32InputMethod() diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index eaab0d5559..0ffbed0ee7 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -719,11 +719,13 @@ namespace Avalonia.Win32 { var previousComposition = Imm32InputMethod.Current.Composition; - Imm32InputMethod.Current.CompositionChanged(); + var flags = (GCS)ToInt32(lParam); - var gcs = (GCS)ToInt32(lParam); + var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR); - switch (gcs) + Imm32InputMethod.Current.CompositionChanged(currentComposition); + + switch (flags) { case GCS.GCS_RESULTSTR: { @@ -749,10 +751,7 @@ namespace Avalonia.Win32 } case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: { - Imm32InputMethod.Current.Composition = previousComposition; - - _ignoreWmChar = true; - + // Japanese IME sends WM_CHAR after composition has finished. break; } } @@ -780,12 +779,12 @@ namespace Avalonia.Win32 return IntPtr.Zero; case WindowsMessage.WM_IME_ENDCOMPOSITION: { + var currentComposition = Imm32InputMethod.Current.Composition; + //In case composition has not been comitted yet we need to do that here. - if (!string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) + if (!string.IsNullOrEmpty(currentComposition)) { - var text = Imm32InputMethod.Current.Composition; - - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text); + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, currentComposition); } //Cleanup composition state. From 5ea3027f98c7ccf5456c31c1d51afa14f1c4ab82 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 6 Mar 2023 17:14:13 +0100 Subject: [PATCH 14/15] Do the same for Chinese IME --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0ffbed0ee7..2a40eb9f5b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -739,14 +739,7 @@ namespace Avalonia.Win32 } case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: { - if (string.IsNullOrEmpty(Imm32InputMethod.Current.Composition)) - { - var c = (char)ToInt32(wParam); - - Imm32InputMethod.Current.Composition = new string(c, 1); - - _ignoreWmChar = true; - } + // Chinese IME sends WM_CHAR after composition has finished. break; } case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE: From 989e45428a9fd9443751dd59ca689b05729629cc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 6 Mar 2023 18:34:04 +0000 Subject: [PATCH 15/15] always kill testmanagerd --- azure-pipelines-integrationtests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index dec94a44d5..939d09c959 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -24,6 +24,7 @@ jobs: fi sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node + pkill testmanagerd appium > appium.out & pkill IntegrationTestApp ./build.sh CompileNative