Browse Source

Merge branch 'master' into fix/DataGridBorderIssue

pull/10565/head
Max Katz 3 years ago
committed by GitHub
parent
commit
f8621e9963
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      azure-pipelines-integrationtests.yml
  2. 12
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  3. 46
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.h
  4. 41
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm
  5. 20
      native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h
  6. 6
      native/Avalonia.Native/src/OSX/AvnView.h
  7. 85
      native/Avalonia.Native/src/OSX/AvnView.mm
  8. 4
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  9. 10
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  10. 2
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  11. 2
      src/Avalonia.Base/Styling/DescendentSelector.cs
  12. 27
      src/Avalonia.Base/Styling/OrSelector.cs
  13. 9
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  14. 64
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  15. 2
      src/Avalonia.Controls/TextBox.cs
  16. 58
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  17. 118
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  18. 11
      src/Avalonia.Native/WindowImpl.cs
  19. 17
      src/Avalonia.Native/avn.idl
  20. 22
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  21. 75
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  22. 1
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  23. 15
      tests/Avalonia.Benchmarks/Properties/launchSettings.json
  24. 48
      tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs
  25. 19
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

1
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

12
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -44,6 +44,9 @@
5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.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 */; };
@ -97,6 +100,9 @@
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; sourceTree = "<group>"; };
8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethodDelegate.h; sourceTree = "<group>"; };
8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethod.h; sourceTree = "<group>"; };
8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnTextInputMethod.mm; sourceTree = "<group>"; };
AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
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; };
@ -143,6 +149,9 @@
isa = PBXGroup;
children = (
855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */,
8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */,
8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */,
8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */,
BC11A5BC2608D58F0017BAD0 /* automation.h */,
BC11A5BD2608D58F0017BAD0 /* automation.mm */,
1A1852DB23E05814008F0DED /* deadlock.mm */,
@ -213,6 +222,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 */,
@ -293,6 +304,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.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 */,

46
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 <Foundation/Foundation.h>
#include "com.h"
#include "comimpl.h"
#include "avalonia-native.h"
#import "AvnTextInputMethodDelegate.h"
class AvnTextInputMethod: public virtual ComObject, public virtual IAvnTextInputMethod{
private:
id<AvnTextInputMethodDelegate> _inputMethodDelegate;
public:
FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP()
INTERFACE_MAP_ENTRY(IAvnTextInputMethod, IID_IAvnTextInputMethod)
END_INTERFACE_MAP()
virtual ~AvnTextInputMethod();
AvnTextInputMethod(id<AvnTextInputMethodDelegate> 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<IAvnTextInputMethodClient> Client;
};
#endif /* AvnTextInputMethod_h */

41
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<AvnTextInputMethodDelegate> 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];
}

20
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 */

6
native/Avalonia.Native/src/OSX/AvnView.h

@ -5,8 +5,6 @@
#pragma once
#import <Foundation/Foundation.h>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#include "common.h"
#include "WindowImpl.h"
@ -14,7 +12,7 @@
@class AvnAccessibilityElement;
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination, AvnTextInputMethodDelegate>
-(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
@end

85
native/Avalonia.Native/src/OSX/AvnView.mm

@ -12,6 +12,7 @@
{
ComPtr<WindowBaseImpl> _parent;
NSTrackingArea* _area;
NSMutableAttributedString* _markedText;
bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed;
AvnInputModifiers _modifierState;
NSEvent* _lastMouseDownEvent;
@ -20,6 +21,9 @@
NSObject<IRenderTarget>* _renderTarget;
AvnPlatformResizeReason _resizeReason;
AvnAccessibilityElement* _accessibilityChild;
NSRect _cursorRect;
NSMutableString* _text;
NSRange _selection;
}
- (void)onClosed
@ -518,7 +522,7 @@
- (void)keyDown:(NSEvent *)event
{
[self keyboardEvent:event withType:KeyDown];
[[self inputContext] handleEvent:event];
_lastKeyHandled = [[self inputContext] handleEvent:event];
[super keyDown:event];
}
@ -557,27 +561,50 @@
- (BOOL)hasMarkedText
{
return _lastKeyHandled;
return [_markedText length] > 0;
}
- (NSRange)markedRange
{
if([_markedText length] > 0)
return NSMakeRange(0, [_markedText length] - 1);
return NSMakeRange(NSNotFound, 0);
}
- (NSRange)selectedRange
{
return NSMakeRange(NSNotFound, 0);
return _selection;
}
- (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);
[[self inputContext] discardMarkedText];
}
- (NSArray<NSString *> *)validAttributesForMarkedText
@ -587,30 +614,38 @@
- (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
{
return 0;
return NSNotFound;
}
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
{
CGRect result = { 0 };
return result;
if(!_parent->InputMethod->IsActive()){
return NSZeroRect;
}
return _cursorRect;
}
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
@ -715,4 +750,28 @@
return [[self accessibilityChild] accessibilityFocusedUIElement];
}
- (void) setText:(NSString *)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{
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

4
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<AvnWindowProtocol> GetWindowProtocol ();
virtual void BringToFront ();
virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override;
protected:
virtual NSWindowStyleMask CalculateStyleMask() = 0;
@ -130,6 +133,7 @@ public:
NSObject <IRenderTarget> *renderTarget;
NSWindow * Window;
ComPtr<IAvnWindowBaseEvents> BaseEvents;
ComPtr<AvnTextInputMethod> InputMethod;
AvnView *View;
};

10
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -15,6 +15,7 @@
#import "WindowProtocol.h"
#import "WindowInterfaces.h"
#include "WindowBaseImpl.h"
#include "AvnTextInputMethod.h"
WindowBaseImpl::~WindowBaseImpl() {
@ -29,6 +30,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 };
@ -605,6 +607,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

2
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@ -190,7 +190,7 @@
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=RightEdgeAlignedBottom">
<Button Content="Placement=RightEdgeAlignedTop">
<Button.Flyout>
<Flyout Placement="RightEdgeAlignedTop">
<Panel Width="100" Height="100">

2
src/Avalonia.Base/Styling/DescendentSelector.cs

@ -13,7 +13,7 @@ namespace Avalonia.Styling
public DescendantSelector(Selector? parent)
{
_parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
_parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceded by a selector.");
}
/// <inheritdoc/>

27
src/Avalonia.Base/Styling/OrSelector.cs

@ -10,7 +10,7 @@ namespace Avalonia.Styling
/// <summary>
/// The OR style selector.
/// </summary>
internal class OrSelector : Selector
internal sealed class OrSelector : Selector
{
private readonly IReadOnlyList<Selector> _selectors;
private string? _selectorString;
@ -42,18 +42,7 @@ namespace Avalonia.Styling
public override bool IsCombinator => false;
/// <inheritdoc/>
public override Type? TargetType
{
get
{
if (_targetType == null)
{
_targetType = EvaluateTargetType();
}
return _targetType;
}
}
public override Type? TargetType => _targetType ??= EvaluateTargetType();
/// <inheritdoc/>
public override string ToString(Style? owner)
@ -71,7 +60,9 @@ namespace Avalonia.Styling
var activators = new OrActivatorBuilder();
var neverThisInstance = false;
for (var i = 0; i < _selectors.Count; i++)
var count = _selectors.Count;
for (var i = 0; i < count; i++)
{
var match = _selectors[i].Match(control, parent, subscribe);
@ -108,7 +99,9 @@ namespace Avalonia.Styling
internal override void ValidateNestingSelector(bool inControlTheme)
{
for (var i = 0; i < _selectors.Count; i++)
var count = _selectors.Count;
for (var i = 0; i < count; i++)
{
_selectors[i].ValidateNestingSelector(inControlTheme);
}
@ -118,7 +111,9 @@ namespace Avalonia.Styling
{
Type? result = null;
for (var i = 0; i < _selectors.Count; i++)
var count = _selectors.Count;
for (var i = 0; i < count; i++)
{
var selector = _selectors[i];
if (selector.TargetType == null)

9
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -11,7 +11,7 @@ namespace Avalonia.Styling
/// A selector that matches the common case of a type and/or name followed by a collection of
/// style classes and pseudoclasses.
/// </summary>
internal class TypeNameAndClassSelector : Selector
internal sealed class TypeNameAndClassSelector : Selector
{
private readonly Selector? _previous;
private List<string>? _classes;
@ -85,12 +85,7 @@ namespace Avalonia.Styling
/// <inheritdoc/>
public override string ToString(Style? owner)
{
if (_selectorString == null)
{
_selectorString = BuildSelectorString(owner);
}
return _selectorString;
return _selectorString ??= BuildSelectorString(owner);
}
/// <inheritdoc/>

64
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<TextPresenter, string?>(
nameof(Text),
o => o.Text,
(o, v) => o.Text = v);
(o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
/// <summary>
/// Defines the <see cref="PreeditText"/> 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,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;
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
@ -551,7 +535,7 @@ namespace Avalonia.Controls.Presenters
{
TextLayout result;
var text = GetText();
var text = _text;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
@ -564,7 +548,7 @@ namespace Avalonia.Controls.Presenters
var foreground = Foreground;
if(_compositionRegion != null)
if (_compositionRegion != null)
{
var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
new GenericTextRunProperties(typeface, FontSize,
@ -851,7 +835,7 @@ namespace Avalonia.Controls.Presenters
CaretChanged();
}
private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true)
internal void UpdateCaret(CharacterHit characterHit, bool notify = true)
{
_lastCharacterHit = characterHit;
@ -879,7 +863,7 @@ namespace Avalonia.Controls.Presenters
CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
}
if (updateCaretIndex)
if (notify)
{
SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
}
@ -899,35 +883,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);
var textLine = TextLayout.TextLines[lineIndex];
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1));
return characterHit;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@ -935,11 +890,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):

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

58
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@ -1,11 +1,11 @@
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;
using Avalonia.VisualTree;
using static System.Net.Mime.MediaTypeNames;
namespace Avalonia.Controls
{
@ -77,7 +77,7 @@ namespace Avalonia.Controls
{
get => _textEditable; set
{
if(_textEditable != null)
if (_textEditable != null)
{
_textEditable.TextChanged -= TextEditable_TextChanged;
_textEditable.SelectionChanged -= TextEditable_SelectionChanged;
@ -86,7 +86,7 @@ namespace Avalonia.Controls
_textEditable = value;
if(_textEditable != null)
if (_textEditable != null)
{
_textEditable.TextChanged += TextEditable_TextChanged;
_textEditable.SelectionChanged += TextEditable_SelectionChanged;
@ -112,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;
@ -159,14 +159,51 @@ 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)
if (_presenter == null || _parent == null)
{
return;
}
_presenter.PreeditText = text;
if (_presenterText is null)
{
_presenterText = _parent.Text ?? "";
_compositionStart = _parent.CaretIndex;
}
var text = GetText(preeditText);
_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);
return text;
}
public void SetComposingRegion(TextRange? region)
@ -175,6 +212,7 @@ namespace Avalonia.Controls
{
return;
}
_presenter.CompositionRegion = region;
}
@ -256,9 +294,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;
}

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

11
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;
@ -19,6 +20,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 +35,8 @@ namespace Avalonia.Native
}
_nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory);
_inputMethod = new AvaloniaNativeTextInputMethod(_native);
}
class WindowEvents : WindowBaseEvents, IAvnWindowEvents
@ -67,7 +71,7 @@ namespace Avalonia.Native
}
}
public IAvnWindow Native => _native;
public new IAvnWindow Native => _native;
public void CanResize(bool value)
{
@ -229,6 +233,11 @@ namespace Avalonia.Native
public override object TryGetFeature(Type featureType)
{
if(featureType == typeof(ITextInputMethodImpl))
{
return _inputMethod;
}
if (featureType == typeof(ITopLevelNativeMenuExporter))
{
return _nativeMenuExporter;

17
src/Avalonia.Native/avn.idl

@ -546,6 +546,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]
@ -612,6 +613,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
{

22
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);
@ -269,28 +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();
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()

75
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,25 +717,80 @@ namespace Avalonia.Win32
}
case WindowsMessage.WM_IME_COMPOSITION:
{
Imm32InputMethod.Current.CompositionChanged();
var previousComposition = Imm32InputMethod.Current.Composition;
var flags = (GCS)ToInt32(lParam);
var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR);
Imm32InputMethod.Current.CompositionChanged(currentComposition);
switch (flags)
{
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:
{
// Chinese IME sends WM_CHAR after composition has finished.
break;
}
case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
{
// Japanese IME sends WM_CHAR after composition has finished.
break;
}
}
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.Composition = null;
if (Imm32InputMethod.Current.IsActive)
{
Imm32InputMethod.Current.Client.SetPreeditText(null);
}
Imm32InputMethod.Current.IsComposing = true;
return IntPtr.Zero;
case WindowsMessage.WM_IME_ENDCOMPOSITION:
Imm32InputMethod.Current.IsComposing = false;
break;
{
var currentComposition = Imm32InputMethod.Current.Composition;
//In case composition has not been comitted yet we need to do that here.
if (!string.IsNullOrEmpty(currentComposition))
{
e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, currentComposition);
}
//Cleanup composition state.
Imm32InputMethod.Current.IsComposing = false;
Imm32InputMethod.Current.Composition = null;
if (Imm32InputMethod.Current.IsActive)
{
Imm32InputMethod.Current.Client.SetPreeditText(null);
}
break;
}
case WindowsMessage.WM_GETOBJECT:
if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control)
{

1
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -15,6 +15,7 @@
<ItemGroup>
<Compile Link="Compiler\XamlX\filename" Include="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/*.cs" />
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/obj/**/*.cs" />
<Compile Remove="../../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/src/XamlX/**/SreTypeSystem.cs" />
<Compile Include="..\..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
</ItemGroup>

15
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"
}
}
}

48
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<Calendar>(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;
}
}

19
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(),

Loading…
Cancel
Save