committed by
GitHub
91 changed files with 2478 additions and 393 deletions
@ -1,6 +1,6 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.6" /> |
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.7" /> |
||||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.6" /> |
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.6.1.7" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -1,5 +1,26 @@ |
|||||
<Project> |
<Project> |
||||
|
<PropertyGroup> |
||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl> |
||||
|
<IncludeSymbols>false</IncludeSymbols> |
||||
|
<EmbedUntrackedSources>true</EmbedUntrackedSources> |
||||
|
<DebugType>embedded</DebugType> |
||||
|
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'"> |
||||
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'"> |
||||
|
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> |
||||
|
</PropertyGroup> |
||||
|
|
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SourceLink.Create.CommandLine" Version="2.8.3" PrivateAssets="All" /> |
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<!-- Workaround for https://github.com/dotnet/sdk/issues/11105 --> |
||||
|
<ItemGroup> |
||||
|
<SourceRoot Include="$(NuGetPackageRoot)" Condition="'$(NuGetPackageRoot)' != ''" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -1,5 +1,5 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="ControlCatalog.Android" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
||||
<uses-sdk /> |
<uses-sdk android:targetSdkVersion="29" /> |
||||
<application android:label="ControlCatalog.Android"></application> |
<application android:label="ControlCatalog.Android"></application> |
||||
</manifest> |
</manifest> |
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@ -0,0 +1,13 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
|
||||
|
<item> |
||||
|
<color android:color="@color/splash_background"/> |
||||
|
</item> |
||||
|
|
||||
|
<item android:drawable="@drawable/icon" |
||||
|
android:width="120dp" |
||||
|
android:height="120dp" |
||||
|
android:gravity="center" /> |
||||
|
|
||||
|
</layer-list> |
||||
@ -1,13 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
android:orientation="vertical" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="match_parent" |
|
||||
> |
|
||||
<Button |
|
||||
android:id="@+id/MyButton" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="wrap_content" |
|
||||
android:text="@string/Hello" |
|
||||
/> |
|
||||
</LinearLayout> |
|
||||
@ -1,5 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<resources> |
|
||||
<string name="Hello">Hello World, Click Me!</string> |
|
||||
<string name="ApplicationName">ControlCatalog.Android</string> |
|
||||
</resources> |
|
||||
@ -0,0 +1,4 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<resources> |
||||
|
<color name="splash_background">#FFFFFF</color> |
||||
|
</resources> |
||||
@ -0,0 +1,17 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8" ?> |
||||
|
<resources> |
||||
|
|
||||
|
<style name="MyTheme"> |
||||
|
</style> |
||||
|
|
||||
|
<style name="MyTheme.NoActionBar"> |
||||
|
<item name="android:windowActionBar">false</item> |
||||
|
<item name="android:windowNoTitle">true</item> |
||||
|
</style> |
||||
|
|
||||
|
<style name="MyTheme.Splash" parent ="MyTheme.NoActionBar"> |
||||
|
<item name="android:windowBackground">@drawable/splash_screen</item> |
||||
|
<item name="android:windowContentOverlay">@null</item> |
||||
|
</style> |
||||
|
|
||||
|
</resources> |
||||
@ -0,0 +1,32 @@ |
|||||
|
using Android.App; |
||||
|
using Android.Content; |
||||
|
using Android.OS; |
||||
|
using Application = Android.App.Application; |
||||
|
|
||||
|
using Avalonia; |
||||
|
|
||||
|
namespace ControlCatalog.Android |
||||
|
{ |
||||
|
[Activity(Theme = "@style/MyTheme.Splash", MainLauncher = true, NoHistory = true)] |
||||
|
public class SplashActivity : Activity |
||||
|
{ |
||||
|
protected override void OnCreate(Bundle savedInstanceState) |
||||
|
{ |
||||
|
base.OnCreate(savedInstanceState); |
||||
|
} |
||||
|
|
||||
|
protected override void OnResume() |
||||
|
{ |
||||
|
base.OnResume(); |
||||
|
|
||||
|
if (Avalonia.Application.Current == null) |
||||
|
{ |
||||
|
AppBuilder.Configure<App>() |
||||
|
.UseAndroid() |
||||
|
.SetupWithoutStarting(); |
||||
|
} |
||||
|
|
||||
|
StartActivity(new Intent(Application.Context, typeof(MainActivity))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Binary file not shown.
@ -0,0 +1,32 @@ |
|||||
|
using System.Linq; |
||||
|
|
||||
|
using Avalonia.OpenGL.Egl; |
||||
|
using Avalonia.OpenGL.Surfaces; |
||||
|
|
||||
|
namespace Avalonia.Android.OpenGL |
||||
|
{ |
||||
|
internal sealed class GlPlatformSurface : EglGlPlatformSurfaceBase |
||||
|
{ |
||||
|
private readonly EglPlatformOpenGlInterface _egl; |
||||
|
private readonly IEglWindowGlPlatformSurfaceInfo _info; |
||||
|
|
||||
|
private GlPlatformSurface(EglPlatformOpenGlInterface egl, IEglWindowGlPlatformSurfaceInfo info) |
||||
|
{ |
||||
|
_egl = egl; |
||||
|
_info = info; |
||||
|
} |
||||
|
|
||||
|
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() => |
||||
|
new GlRenderTarget(_egl, _info, _egl.CreateWindowSurface(_info.Handle)); |
||||
|
|
||||
|
public static GlPlatformSurface TryCreate(IEglWindowGlPlatformSurfaceInfo info) |
||||
|
{ |
||||
|
if (EglPlatformOpenGlInterface.TryCreate() is EglPlatformOpenGlInterface egl) |
||||
|
{ |
||||
|
return new GlPlatformSurface(egl, info); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
using Avalonia.OpenGL.Egl; |
||||
|
using Avalonia.OpenGL.Surfaces; |
||||
|
|
||||
|
namespace Avalonia.Android.OpenGL |
||||
|
{ |
||||
|
internal sealed class GlRenderTarget : EglPlatformSurfaceRenderTargetBase |
||||
|
{ |
||||
|
private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info; |
||||
|
private readonly EglSurface _surface; |
||||
|
|
||||
|
public GlRenderTarget( |
||||
|
EglPlatformOpenGlInterface egl, |
||||
|
EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo info, |
||||
|
EglSurface surface) |
||||
|
: base(egl) |
||||
|
{ |
||||
|
_info = info; |
||||
|
_surface = surface; |
||||
|
} |
||||
|
|
||||
|
public override IGlPlatformSurfaceRenderingSession BeginDraw() => BeginDraw(_surface, _info); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using Avalonia.Controls.Platform.Surfaces; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Android.Platform.SkiaPlatform |
||||
|
{ |
||||
|
internal sealed class FramebufferManager : IFramebufferPlatformSurface |
||||
|
{ |
||||
|
private readonly TopLevelImpl _topLevel; |
||||
|
|
||||
|
public FramebufferManager(TopLevelImpl topLevel) |
||||
|
{ |
||||
|
_topLevel = topLevel; |
||||
|
} |
||||
|
|
||||
|
public ILockedFramebuffer Lock() => new AndroidFramebuffer(_topLevel.InternalView.Holder.Surface); |
||||
|
} |
||||
|
} |
||||
@ -1,60 +0,0 @@ |
|||||
#pragma warning disable 1591
|
|
||||
//------------------------------------------------------------------------------
|
|
||||
// <auto-generated>
|
|
||||
// This code was generated by a tool.
|
|
||||
// Runtime Version:4.0.30319.42000
|
|
||||
//
|
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||
// the code is regenerated.
|
|
||||
// </auto-generated>
|
|
||||
//------------------------------------------------------------------------------
|
|
||||
|
|
||||
[assembly: global::Android.Runtime.ResourceDesignerAttribute("Avalonia.Android.Resource", IsApplication=false)] |
|
||||
|
|
||||
namespace Avalonia.Android |
|
||||
{ |
|
||||
|
|
||||
|
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "1.0.0.0")] |
|
||||
public partial class Resource |
|
||||
{ |
|
||||
|
|
||||
static Resource() |
|
||||
{ |
|
||||
global::Android.Runtime.ResourceIdManager.UpdateIdValues(); |
|
||||
} |
|
||||
|
|
||||
public partial class Attribute |
|
||||
{ |
|
||||
|
|
||||
static Attribute() |
|
||||
{ |
|
||||
global::Android.Runtime.ResourceIdManager.UpdateIdValues(); |
|
||||
} |
|
||||
|
|
||||
private Attribute() |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public partial class String |
|
||||
{ |
|
||||
|
|
||||
// aapt resource value: 0x7f020001
|
|
||||
public static int ApplicationName = 2130837505; |
|
||||
|
|
||||
// aapt resource value: 0x7f020000
|
|
||||
public static int Hello = 2130837504; |
|
||||
|
|
||||
static String() |
|
||||
{ |
|
||||
global::Android.Runtime.ResourceIdManager.UpdateIdValues(); |
|
||||
} |
|
||||
|
|
||||
private String() |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
#pragma warning restore 1591
|
|
||||
@ -1,6 +1,6 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="Avalonia.AndroidTestApplication" android:versionCode="1" android:versionName="1.0" android:installLocation="auto"> |
||||
<uses-sdk /> |
<uses-sdk android:targetSdkVersion="29" /> |
||||
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application> |
<application android:label="Avalonia.AndroidTestApplication" android:icon="@drawable/Icon" android:hardwareAccelerated="true"></application> |
||||
<uses-permission android:name="android.permission.INTERNET" /> |
<uses-permission android:name="android.permission.INTERNET" /> |
||||
</manifest> |
</manifest> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Controls.Platform |
||||
|
{ |
||||
|
public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl |
||||
|
{ |
||||
|
public ITextInputMethodImpl TextInputMethod { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls.Presenters; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
internal class TextBoxTextInputMethodClient : ITextInputMethodClient |
||||
|
{ |
||||
|
private TextPresenter _presenter; |
||||
|
private IDisposable _subscription; |
||||
|
public Rect CursorRectangle => _presenter?.GetCursorRectangle() ?? default; |
||||
|
public event EventHandler CursorRectangleChanged; |
||||
|
public IVisual TextViewVisual => _presenter; |
||||
|
public event EventHandler TextViewVisualChanged; |
||||
|
public bool SupportsPreedit => false; |
||||
|
public void SetPreeditText(string text) => throw new NotSupportedException(); |
||||
|
|
||||
|
public bool SupportsSurroundingText => false; |
||||
|
public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); |
||||
|
public event EventHandler SurroundingTextChanged; |
||||
|
public string TextBeforeCursor => null; |
||||
|
public string TextAfterCursor => null; |
||||
|
|
||||
|
private void OnCaretIndexChanged(int index) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); |
||||
|
|
||||
|
public void SetPresenter(TextPresenter presenter) |
||||
|
{ |
||||
|
_subscription?.Dispose(); |
||||
|
_subscription = null; |
||||
|
_presenter = presenter; |
||||
|
if (_presenter != null) |
||||
|
{ |
||||
|
_subscription = _presenter.GetObservable(TextPresenter.CaretIndexProperty) |
||||
|
.Subscribe(OnCaretIndexChanged); |
||||
|
} |
||||
|
TextViewVisualChanged?.Invoke(this, EventArgs.Empty); |
||||
|
CursorRectangleChanged?.Invoke(this, EventArgs.Empty); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,110 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop |
||||
|
{ |
||||
|
class DBusCallQueue |
||||
|
{ |
||||
|
private readonly Func<Exception, Task> _errorHandler; |
||||
|
|
||||
|
class Item |
||||
|
{ |
||||
|
public Func<Task> Callback; |
||||
|
public Action<Exception> OnFinish; |
||||
|
} |
||||
|
private Queue<Item> _q = new Queue<Item>(); |
||||
|
private bool _processing; |
||||
|
|
||||
|
public DBusCallQueue(Func<Exception, Task> errorHandler) |
||||
|
{ |
||||
|
_errorHandler = errorHandler; |
||||
|
} |
||||
|
|
||||
|
public void Enqueue(Func<Task> cb) |
||||
|
{ |
||||
|
_q.Enqueue(new Item |
||||
|
{ |
||||
|
Callback = cb |
||||
|
}); |
||||
|
Process(); |
||||
|
} |
||||
|
|
||||
|
public Task EnqueueAsync(Func<Task> cb) |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); |
||||
|
_q.Enqueue(new Item |
||||
|
{ |
||||
|
Callback = cb, |
||||
|
OnFinish = e => |
||||
|
{ |
||||
|
if (e == null) |
||||
|
tcs.TrySetResult(0); |
||||
|
else |
||||
|
tcs.TrySetException(e); |
||||
|
} |
||||
|
}); |
||||
|
Process(); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
|
||||
|
public Task<T> EnqueueAsync<T>(Func<Task<T>> cb) |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously); |
||||
|
_q.Enqueue(new Item |
||||
|
{ |
||||
|
Callback = async () => |
||||
|
{ |
||||
|
var res = await cb(); |
||||
|
tcs.TrySetResult(res); |
||||
|
}, |
||||
|
OnFinish = e => |
||||
|
{ |
||||
|
if (e != null) |
||||
|
tcs.TrySetException(e); |
||||
|
} |
||||
|
}); |
||||
|
Process(); |
||||
|
return tcs.Task; |
||||
|
} |
||||
|
|
||||
|
async void Process() |
||||
|
{ |
||||
|
if(_processing) |
||||
|
return; |
||||
|
_processing = true; |
||||
|
try |
||||
|
{ |
||||
|
while (_q.Count > 0) |
||||
|
{ |
||||
|
var item = _q.Dequeue(); |
||||
|
try |
||||
|
{ |
||||
|
await item.Callback(); |
||||
|
item.OnFinish?.Invoke(null); |
||||
|
} |
||||
|
catch(Exception e) |
||||
|
{ |
||||
|
if (item.OnFinish != null) |
||||
|
item.OnFinish(e); |
||||
|
else |
||||
|
await _errorHandler(e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_processing = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void FailAll() |
||||
|
{ |
||||
|
while (_q.Count>0) |
||||
|
{ |
||||
|
var item = _q.Dequeue(); |
||||
|
item.OnFinish?.Invoke(new OperationCanceledException()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,288 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.FreeDesktop.DBusIme.Fcitx; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Logging; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme |
||||
|
{ |
||||
|
internal class DBusInputMethodFactory<T> : IX11InputMethodFactory where T : ITextInputMethodImpl, IX11InputMethodControl |
||||
|
{ |
||||
|
private readonly Func<IntPtr, T> _factory; |
||||
|
|
||||
|
public DBusInputMethodFactory(Func<IntPtr, T> factory) |
||||
|
{ |
||||
|
_factory = factory; |
||||
|
} |
||||
|
|
||||
|
public (ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid) |
||||
|
{ |
||||
|
var im = _factory(xid); |
||||
|
return (im, im); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal abstract class DBusTextInputMethodBase : IX11InputMethodControl, ITextInputMethodImpl |
||||
|
{ |
||||
|
private List<IDisposable> _disposables = new List<IDisposable>(); |
||||
|
private Queue<string> _onlineNamesQueue = new Queue<string>(); |
||||
|
protected Connection Connection { get; } |
||||
|
private readonly string[] _knownNames; |
||||
|
private bool _connecting; |
||||
|
private string _currentName; |
||||
|
private DBusCallQueue _queue; |
||||
|
private bool _controlActive, _windowActive; |
||||
|
private bool? _imeActive; |
||||
|
private Rect _logicalRect; |
||||
|
private PixelRect? _lastReportedRect; |
||||
|
private double _scaling = 1; |
||||
|
private PixelPoint _windowPosition; |
||||
|
|
||||
|
protected bool IsConnected => _currentName != null; |
||||
|
|
||||
|
public DBusTextInputMethodBase(Connection connection, params string[] knownNames) |
||||
|
{ |
||||
|
_queue = new DBusCallQueue(QueueOnError); |
||||
|
Connection = connection; |
||||
|
_knownNames = knownNames; |
||||
|
Watch(); |
||||
|
} |
||||
|
|
||||
|
async void Watch() |
||||
|
{ |
||||
|
foreach (var name in _knownNames) |
||||
|
_disposables.Add(await Connection.ResolveServiceOwnerAsync(name, OnNameChange)); |
||||
|
} |
||||
|
|
||||
|
protected abstract Task<bool> Connect(string name); |
||||
|
|
||||
|
protected string GetAppName() => |
||||
|
Application.Current.Name ?? Assembly.GetEntryAssembly()?.GetName()?.Name ?? "Avalonia"; |
||||
|
|
||||
|
private async void OnNameChange(ServiceOwnerChangedEventArgs args) |
||||
|
{ |
||||
|
if (args.NewOwner != null && _currentName == null) |
||||
|
{ |
||||
|
_onlineNamesQueue.Enqueue(args.ServiceName); |
||||
|
if(!_connecting) |
||||
|
{ |
||||
|
_connecting = true; |
||||
|
try |
||||
|
{ |
||||
|
while (_onlineNamesQueue.Count > 0) |
||||
|
{ |
||||
|
var name = _onlineNamesQueue.Dequeue(); |
||||
|
try |
||||
|
{ |
||||
|
if (await Connect(name)) |
||||
|
{ |
||||
|
_onlineNamesQueue.Clear(); |
||||
|
_currentName = name; |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Error, "IME") |
||||
|
?.Log(this, "Unable to create IME input context:\n" + e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_connecting = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// IME has crashed
|
||||
|
if (args.NewOwner == null && args.ServiceName == _currentName) |
||||
|
{ |
||||
|
_currentName = null; |
||||
|
foreach(var s in _disposables) |
||||
|
s.Dispose(); |
||||
|
_disposables.Clear(); |
||||
|
|
||||
|
OnDisconnected(); |
||||
|
Reset(); |
||||
|
|
||||
|
// Watch again
|
||||
|
Watch(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected virtual Task Disconnect() |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected virtual void OnDisconnected() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
protected virtual void Reset() |
||||
|
{ |
||||
|
_lastReportedRect = null; |
||||
|
_imeActive = null; |
||||
|
} |
||||
|
|
||||
|
async Task QueueOnError(Exception e) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Error, "IME") |
||||
|
?.Log(this, "Error:\n" + e); |
||||
|
try |
||||
|
{ |
||||
|
await Disconnect(); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Logger.TryGet(LogEventLevel.Error, "IME") |
||||
|
?.Log(this, "Error while destroying the context:\n" + ex); |
||||
|
} |
||||
|
OnDisconnected(); |
||||
|
_currentName = null; |
||||
|
} |
||||
|
|
||||
|
protected void Enqueue(Func<Task> cb) => _queue.Enqueue(cb); |
||||
|
|
||||
|
protected void AddDisposable(IDisposable d) => _disposables.Add(d); |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
foreach(var d in _disposables) |
||||
|
d.Dispose(); |
||||
|
_disposables.Clear(); |
||||
|
try |
||||
|
{ |
||||
|
Disconnect().ContinueWith(_ => { }); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// fire and forget
|
||||
|
} |
||||
|
_currentName = null; |
||||
|
} |
||||
|
|
||||
|
protected abstract Task SetCursorRectCore(PixelRect rect); |
||||
|
protected abstract Task SetActiveCore(bool active); |
||||
|
protected abstract Task ResetContextCore(); |
||||
|
protected abstract Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode); |
||||
|
|
||||
|
void UpdateActive() |
||||
|
{ |
||||
|
_queue.Enqueue(async () => |
||||
|
{ |
||||
|
if(!IsConnected) |
||||
|
return; |
||||
|
|
||||
|
var active = _windowActive && _controlActive; |
||||
|
if (active != _imeActive) |
||||
|
{ |
||||
|
_imeActive = active; |
||||
|
await SetActiveCore(active); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
void IX11InputMethodControl.SetWindowActive(bool active) |
||||
|
{ |
||||
|
_windowActive = active; |
||||
|
UpdateActive(); |
||||
|
} |
||||
|
|
||||
|
void ITextInputMethodImpl.SetActive(bool active) |
||||
|
{ |
||||
|
_controlActive = active; |
||||
|
UpdateActive(); |
||||
|
} |
||||
|
|
||||
|
bool IX11InputMethodControl.IsEnabled => IsConnected && _imeActive == true; |
||||
|
|
||||
|
async ValueTask<bool> IX11InputMethodControl.HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return await _queue.EnqueueAsync(async () => await HandleKeyCore(args, keyVal, keyCode)); |
||||
|
} |
||||
|
// Disconnected
|
||||
|
catch (OperationCanceledException) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
// Error, disconnect
|
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
await QueueOnError(e); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Action<string> _onCommit; |
||||
|
event Action<string> IX11InputMethodControl.Commit |
||||
|
{ |
||||
|
add => _onCommit += value; |
||||
|
remove => _onCommit -= value; |
||||
|
} |
||||
|
|
||||
|
protected void FireCommit(string s) => _onCommit?.Invoke(s); |
||||
|
|
||||
|
private Action<X11InputMethodForwardedKey> _onForward; |
||||
|
event Action<X11InputMethodForwardedKey> IX11InputMethodControl.ForwardKey |
||||
|
{ |
||||
|
add => _onForward += value; |
||||
|
remove => _onForward -= value; |
||||
|
} |
||||
|
|
||||
|
protected void FireForward(X11InputMethodForwardedKey k) => _onForward?.Invoke(k); |
||||
|
|
||||
|
void UpdateCursorRect() |
||||
|
{ |
||||
|
_queue.Enqueue(async () => |
||||
|
{ |
||||
|
if(!IsConnected) |
||||
|
return; |
||||
|
var cursorRect = PixelRect.FromRect(_logicalRect, _scaling); |
||||
|
cursorRect = cursorRect.Translate(_windowPosition); |
||||
|
if (cursorRect != _lastReportedRect) |
||||
|
{ |
||||
|
_lastReportedRect = cursorRect; |
||||
|
await SetCursorRectCore(cursorRect); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
void IX11InputMethodControl.UpdateWindowInfo(PixelPoint position, double scaling) |
||||
|
{ |
||||
|
_windowPosition = position; |
||||
|
_scaling = scaling; |
||||
|
UpdateCursorRect(); |
||||
|
} |
||||
|
|
||||
|
void ITextInputMethodImpl.SetCursorRect(Rect rect) |
||||
|
{ |
||||
|
_logicalRect = rect; |
||||
|
UpdateCursorRect(); |
||||
|
} |
||||
|
|
||||
|
public abstract void SetOptions(TextInputOptionsQueryEventArgs options); |
||||
|
|
||||
|
void ITextInputMethodImpl.Reset() |
||||
|
{ |
||||
|
Reset(); |
||||
|
_queue.Enqueue(async () => |
||||
|
{ |
||||
|
if (!IsConnected) |
||||
|
return; |
||||
|
await ResetContextCore(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
[assembly: InternalsVisibleTo(Tmds.DBus.Connection.DynamicAssemblyName)] |
||||
|
namespace Avalonia.FreeDesktop.DBusIme.Fcitx |
||||
|
{ |
||||
|
[DBusInterface("org.fcitx.Fcitx.InputMethod")] |
||||
|
interface IFcitxInputMethod : IDBusObject |
||||
|
{ |
||||
|
Task<(int icid, bool enable, uint keyval1, uint state1, uint keyval2, uint state2)> CreateICv3Async( |
||||
|
string Appname, int Pid); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[DBusInterface("org.fcitx.Fcitx.InputContext")] |
||||
|
interface IFcitxInputContext : IDBusObject |
||||
|
{ |
||||
|
Task EnableICAsync(); |
||||
|
Task CloseICAsync(); |
||||
|
Task FocusInAsync(); |
||||
|
Task FocusOutAsync(); |
||||
|
Task ResetAsync(); |
||||
|
Task MouseEventAsync(int X); |
||||
|
Task SetCursorLocationAsync(int X, int Y); |
||||
|
Task SetCursorRectAsync(int X, int Y, int W, int H); |
||||
|
Task SetCapacityAsync(uint Caps); |
||||
|
Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); |
||||
|
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); |
||||
|
Task DestroyICAsync(); |
||||
|
Task<int> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, int Type, uint Time); |
||||
|
Task<IDisposable> WatchEnableIMAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchCloseIMAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdatePreeditAsync(Action<(string str, int cursorpos)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdateClientSideUIAsync(Action<(string auxup, string auxdown, string preedit, string candidateword, string imname, int cursorpos)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null); |
||||
|
} |
||||
|
|
||||
|
[DBusInterface("org.fcitx.Fcitx.InputContext1")] |
||||
|
interface IFcitxInputContext1 : IDBusObject |
||||
|
{ |
||||
|
Task FocusInAsync(); |
||||
|
Task FocusOutAsync(); |
||||
|
Task ResetAsync(); |
||||
|
Task SetCursorRectAsync(int X, int Y, int W, int H); |
||||
|
Task SetCapabilityAsync(ulong Caps); |
||||
|
Task SetSurroundingTextAsync(string Text, uint Cursor, uint Anchor); |
||||
|
Task SetSurroundingTextPositionAsync(uint Cursor, uint Anchor); |
||||
|
Task DestroyICAsync(); |
||||
|
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State, bool Type, uint Time); |
||||
|
Task<IDisposable> WatchCommitStringAsync(Action<string> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchCurrentIMAsync(Action<(string name, string uniqueName, string langCode)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdateFormattedPreeditAsync(Action<((string, int)[] str, int cursorpos)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, bool type)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchar)> handler, Action<Exception> onError = null); |
||||
|
} |
||||
|
|
||||
|
[DBusInterface("org.fcitx.Fcitx.InputMethod1")] |
||||
|
interface IFcitxInputMethod1 : IDBusObject |
||||
|
{ |
||||
|
Task<(ObjectPath path, byte[] data)> CreateInputContextAsync((string, string)[] arg0); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme.Fcitx |
||||
|
{ |
||||
|
enum FcitxKeyEventType |
||||
|
{ |
||||
|
FCITX_PRESS_KEY, |
||||
|
FCITX_RELEASE_KEY |
||||
|
}; |
||||
|
|
||||
|
[Flags] |
||||
|
enum FcitxCapabilityFlags |
||||
|
{ |
||||
|
CAPACITY_NONE = 0, |
||||
|
CAPACITY_CLIENT_SIDE_UI = (1 << 0), |
||||
|
CAPACITY_PREEDIT = (1 << 1), |
||||
|
CAPACITY_CLIENT_SIDE_CONTROL_STATE = (1 << 2), |
||||
|
CAPACITY_PASSWORD = (1 << 3), |
||||
|
CAPACITY_FORMATTED_PREEDIT = (1 << 4), |
||||
|
CAPACITY_CLIENT_UNFOCUS_COMMIT = (1 << 5), |
||||
|
CAPACITY_SURROUNDING_TEXT = (1 << 6), |
||||
|
CAPACITY_EMAIL = (1 << 7), |
||||
|
CAPACITY_DIGIT = (1 << 8), |
||||
|
CAPACITY_UPPERCASE = (1 << 9), |
||||
|
CAPACITY_LOWERCASE = (1 << 10), |
||||
|
CAPACITY_NOAUTOUPPERCASE = (1 << 11), |
||||
|
CAPACITY_URL = (1 << 12), |
||||
|
CAPACITY_DIALABLE = (1 << 13), |
||||
|
CAPACITY_NUMBER = (1 << 14), |
||||
|
CAPACITY_NO_ON_SCREEN_KEYBOARD = (1 << 15), |
||||
|
CAPACITY_SPELLCHECK = (1 << 16), |
||||
|
CAPACITY_NO_SPELLCHECK = (1 << 17), |
||||
|
CAPACITY_WORD_COMPLETION = (1 << 18), |
||||
|
CAPACITY_UPPERCASE_WORDS = (1 << 19), |
||||
|
CAPACITY_UPPERCASE_SENTENCES = (1 << 20), |
||||
|
CAPACITY_ALPHA = (1 << 21), |
||||
|
CAPACITY_NAME = (1 << 22), |
||||
|
CAPACITY_GET_IM_INFO_ON_FOCUS = (1 << 23), |
||||
|
CAPACITY_RELATIVE_CURSOR_RECT = (1 << 24), |
||||
|
}; |
||||
|
|
||||
|
[Flags] |
||||
|
enum FcitxKeyState |
||||
|
{ |
||||
|
FcitxKeyState_None = 0, |
||||
|
FcitxKeyState_Shift = 1 << 0, |
||||
|
FcitxKeyState_CapsLock = 1 << 1, |
||||
|
FcitxKeyState_Ctrl = 1 << 2, |
||||
|
FcitxKeyState_Alt = 1 << 3, |
||||
|
FcitxKeyState_Alt_Shift = FcitxKeyState_Alt | FcitxKeyState_Shift, |
||||
|
FcitxKeyState_Ctrl_Shift = FcitxKeyState_Ctrl | FcitxKeyState_Shift, |
||||
|
FcitxKeyState_Ctrl_Alt = FcitxKeyState_Ctrl | FcitxKeyState_Alt, |
||||
|
|
||||
|
FcitxKeyState_Ctrl_Alt_Shift = |
||||
|
FcitxKeyState_Ctrl | FcitxKeyState_Alt | FcitxKeyState_Shift, |
||||
|
FcitxKeyState_NumLock = 1 << 4, |
||||
|
FcitxKeyState_Super = 1 << 6, |
||||
|
FcitxKeyState_ScrollLock = 1 << 7, |
||||
|
FcitxKeyState_MousePressed = 1 << 8, |
||||
|
FcitxKeyState_HandledMask = 1 << 24, |
||||
|
FcitxKeyState_IgnoredMask = 1 << 25, |
||||
|
FcitxKeyState_Super2 = 1 << 26, |
||||
|
FcitxKeyState_Hyper = 1 << 27, |
||||
|
FcitxKeyState_Meta = 1 << 28, |
||||
|
FcitxKeyState_UsedMask = 0x5c001fff |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme.Fcitx |
||||
|
{ |
||||
|
internal class FcitxICWrapper |
||||
|
{ |
||||
|
private readonly IFcitxInputContext1 _modern; |
||||
|
private readonly IFcitxInputContext _old; |
||||
|
|
||||
|
public FcitxICWrapper(IFcitxInputContext old) |
||||
|
{ |
||||
|
_old = old; |
||||
|
} |
||||
|
|
||||
|
public FcitxICWrapper(IFcitxInputContext1 modern) |
||||
|
{ |
||||
|
_modern = modern; |
||||
|
} |
||||
|
|
||||
|
public Task FocusInAsync() => _old?.FocusInAsync() ?? _modern.FocusInAsync(); |
||||
|
|
||||
|
public Task FocusOutAsync() => _old?.FocusOutAsync() ?? _modern.FocusOutAsync(); |
||||
|
|
||||
|
public Task ResetAsync() => _old?.ResetAsync() ?? _modern.ResetAsync(); |
||||
|
|
||||
|
public Task SetCursorRectAsync(int x, int y, int w, int h) => |
||||
|
_old?.SetCursorRectAsync(x, y, w, h) ?? _modern.SetCursorRectAsync(x, y, w, h); |
||||
|
public Task DestroyICAsync() => _old?.DestroyICAsync() ?? _modern.DestroyICAsync(); |
||||
|
|
||||
|
public async Task<bool> ProcessKeyEventAsync(uint keyVal, uint keyCode, uint state, int type, uint time) |
||||
|
{ |
||||
|
if(_old!=null) |
||||
|
return await _old.ProcessKeyEventAsync(keyVal, keyCode, state, type, time) != 0; |
||||
|
return await _modern.ProcessKeyEventAsync(keyVal, keyCode, state, type > 0, time); |
||||
|
} |
||||
|
|
||||
|
public Task<IDisposable> WatchCommitStringAsync(Action<string> handler) => |
||||
|
_old?.WatchCommitStringAsync(handler) ?? _modern.WatchCommitStringAsync(handler); |
||||
|
|
||||
|
public Task<IDisposable> WatchForwardKeyAsync(Action<(uint keyval, uint state, int type)> handler) |
||||
|
{ |
||||
|
return _old?.WatchForwardKeyAsync(handler) |
||||
|
?? _modern.WatchForwardKeyAsync(ev => |
||||
|
handler((ev.keyval, ev.state, ev.type ? 1 : 0))); |
||||
|
} |
||||
|
|
||||
|
public Task SetCapacityAsync(uint flags) => |
||||
|
_old?.SetCapacityAsync(flags) ?? _modern.SetCapabilityAsync(flags); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,149 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using System.Reactive.Concurrency; |
||||
|
using System.Reflection; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme.Fcitx |
||||
|
{ |
||||
|
internal class FcitxX11TextInputMethod : DBusTextInputMethodBase |
||||
|
{ |
||||
|
private FcitxICWrapper _context; |
||||
|
private FcitxCapabilityFlags? _lastReportedFlags; |
||||
|
|
||||
|
public FcitxX11TextInputMethod(Connection connection) : base(connection, |
||||
|
"org.fcitx.Fcitx", |
||||
|
"org.freedesktop.portal.Fcitx" |
||||
|
) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
protected override async Task<bool> Connect(string name) |
||||
|
{ |
||||
|
if (name == "org.fcitx.Fcitx") |
||||
|
{ |
||||
|
var method = Connection.CreateProxy<IFcitxInputMethod>(name, "/inputmethod"); |
||||
|
var resp = await method.CreateICv3Async(GetAppName(), |
||||
|
Process.GetCurrentProcess().Id); |
||||
|
|
||||
|
var proxy = Connection.CreateProxy<IFcitxInputContext>(name, |
||||
|
"/inputcontext_" + resp.icid); |
||||
|
|
||||
|
_context = new FcitxICWrapper(proxy); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var method = Connection.CreateProxy<IFcitxInputMethod1>(name, "/inputmethod"); |
||||
|
var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); |
||||
|
var proxy = Connection.CreateProxy<IFcitxInputContext1>(name, resp.path); |
||||
|
_context = new FcitxICWrapper(proxy); |
||||
|
} |
||||
|
|
||||
|
AddDisposable(await _context.WatchCommitStringAsync(OnCommitString)); |
||||
|
AddDisposable(await _context.WatchForwardKeyAsync(OnForward)); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
protected override Task Disconnect() => _context.DestroyICAsync(); |
||||
|
|
||||
|
protected override void OnDisconnected() => _context = null; |
||||
|
|
||||
|
protected override void Reset() |
||||
|
{ |
||||
|
_lastReportedFlags = null; |
||||
|
base.Reset(); |
||||
|
} |
||||
|
|
||||
|
protected override Task SetCursorRectCore(PixelRect cursorRect) => |
||||
|
_context.SetCursorRectAsync(cursorRect.X, cursorRect.Y, Math.Max(1, cursorRect.Width), |
||||
|
Math.Max(1, cursorRect.Height)); |
||||
|
|
||||
|
protected override Task SetActiveCore(bool active) |
||||
|
{ |
||||
|
if (active) |
||||
|
return _context.FocusInAsync(); |
||||
|
else |
||||
|
return _context.FocusOutAsync(); |
||||
|
} |
||||
|
|
||||
|
protected override Task ResetContextCore() => _context.ResetAsync(); |
||||
|
|
||||
|
protected override async Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) |
||||
|
{ |
||||
|
FcitxKeyState state = default; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) |
||||
|
state |= FcitxKeyState.FcitxKeyState_Ctrl; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) |
||||
|
state |= FcitxKeyState.FcitxKeyState_Alt; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) |
||||
|
state |= FcitxKeyState.FcitxKeyState_Shift; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) |
||||
|
state |= FcitxKeyState.FcitxKeyState_Super; |
||||
|
|
||||
|
var type = args.Type == RawKeyEventType.KeyDown ? |
||||
|
FcitxKeyEventType.FCITX_PRESS_KEY : |
||||
|
FcitxKeyEventType.FCITX_RELEASE_KEY; |
||||
|
|
||||
|
return await _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state, (int)type, |
||||
|
(uint)args.Timestamp).ConfigureAwait(false); |
||||
|
} |
||||
|
|
||||
|
public override void SetOptions(TextInputOptionsQueryEventArgs options) => |
||||
|
Enqueue(async () => |
||||
|
{ |
||||
|
if(_context == null) |
||||
|
return; |
||||
|
FcitxCapabilityFlags flags = default; |
||||
|
if (options.Lowercase) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_LOWERCASE; |
||||
|
if (options.Uppercase) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_UPPERCASE; |
||||
|
if (!options.AutoCapitalization) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_NOAUTOUPPERCASE; |
||||
|
if (options.ContentType == TextInputContentType.Email) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_EMAIL; |
||||
|
else if (options.ContentType == TextInputContentType.Number) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_NUMBER; |
||||
|
else if (options.ContentType == TextInputContentType.Password) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_PASSWORD; |
||||
|
else if (options.ContentType == TextInputContentType.Phone) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_DIALABLE; |
||||
|
else if (options.ContentType == TextInputContentType.Url) |
||||
|
flags |= FcitxCapabilityFlags.CAPACITY_URL; |
||||
|
if (flags != _lastReportedFlags) |
||||
|
{ |
||||
|
_lastReportedFlags = flags; |
||||
|
await _context.SetCapacityAsync((uint)flags); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
private void OnForward((uint keyval, uint state, int type) ev) |
||||
|
{ |
||||
|
var state = (FcitxKeyState)ev.state; |
||||
|
KeyModifiers mods = default; |
||||
|
if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Ctrl)) |
||||
|
mods |= KeyModifiers.Control; |
||||
|
if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Alt)) |
||||
|
mods |= KeyModifiers.Alt; |
||||
|
if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Shift)) |
||||
|
mods |= KeyModifiers.Shift; |
||||
|
if (state.HasFlagCustom(FcitxKeyState.FcitxKeyState_Super)) |
||||
|
mods |= KeyModifiers.Meta; |
||||
|
FireForward(new X11InputMethodForwardedKey |
||||
|
{ |
||||
|
Modifiers = mods, |
||||
|
KeyVal = (int)ev.keyval, |
||||
|
Type = ev.type == (int)FcitxKeyEventType.FCITX_PRESS_KEY ? |
||||
|
RawKeyEventType.KeyDown : |
||||
|
RawKeyEventType.KeyUp |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private void OnCommitString(string s) => FireCommit(s); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)] |
||||
|
namespace Avalonia.FreeDesktop.DBusIme.IBus |
||||
|
{ |
||||
|
[DBusInterface("org.freedesktop.IBus.InputContext")] |
||||
|
interface IIBusInputContext : IDBusObject |
||||
|
{ |
||||
|
Task<bool> ProcessKeyEventAsync(uint Keyval, uint Keycode, uint State); |
||||
|
Task SetCursorLocationAsync(int X, int Y, int W, int H); |
||||
|
Task FocusInAsync(); |
||||
|
Task FocusOutAsync(); |
||||
|
Task ResetAsync(); |
||||
|
Task SetCapabilitiesAsync(uint Caps); |
||||
|
Task PropertyActivateAsync(string Name, int State); |
||||
|
Task SetEngineAsync(string Name); |
||||
|
Task<object> GetEngineAsync(); |
||||
|
Task DestroyAsync(); |
||||
|
Task SetSurroundingTextAsync(object Text, uint CursorPos, uint AnchorPos); |
||||
|
Task<IDisposable> WatchCommitTextAsync(Action<object> cb, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchForwardKeyEventAsync(Action<(uint keyval, uint keycode, uint state)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchRequireSurroundingTextAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchDeleteSurroundingTextAsync(Action<(int offset, uint nchars)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdatePreeditTextAsync(Action<(object text, uint cursorPos, bool visible)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchShowPreeditTextAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchHidePreeditTextAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdateAuxiliaryTextAsync(Action<(object text, bool visible)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchShowAuxiliaryTextAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchHideAuxiliaryTextAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdateLookupTableAsync(Action<(object table, bool visible)> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchShowLookupTableAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchHideLookupTableAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchPageUpLookupTableAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchPageDownLookupTableAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchCursorUpLookupTableAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchCursorDownLookupTableAsync(Action handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchRegisterPropertiesAsync(Action<object> handler, Action<Exception> onError = null); |
||||
|
Task<IDisposable> WatchUpdatePropertyAsync(Action<object> handler, Action<Exception> onError = null); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
[DBusInterface("org.freedesktop.IBus.Portal")] |
||||
|
interface IIBusPortal : IDBusObject |
||||
|
{ |
||||
|
Task<ObjectPath> CreateInputContextAsync(string Name); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme.IBus |
||||
|
{ |
||||
|
[Flags] |
||||
|
internal enum IBusModifierMask |
||||
|
{ |
||||
|
ShiftMask = 1 << 0, |
||||
|
LockMask = 1 << 1, |
||||
|
ControlMask = 1 << 2, |
||||
|
Mod1Mask = 1 << 3, |
||||
|
Mod2Mask = 1 << 4, |
||||
|
Mod3Mask = 1 << 5, |
||||
|
Mod4Mask = 1 << 6, |
||||
|
Mod5Mask = 1 << 7, |
||||
|
Button1Mask = 1 << 8, |
||||
|
Button2Mask = 1 << 9, |
||||
|
Button3Mask = 1 << 10, |
||||
|
Button4Mask = 1 << 11, |
||||
|
Button5Mask = 1 << 12, |
||||
|
|
||||
|
HandledMask = 1 << 24, |
||||
|
ForwardMask = 1 << 25, |
||||
|
IgnoredMask = ForwardMask, |
||||
|
|
||||
|
SuperMask = 1 << 26, |
||||
|
HyperMask = 1 << 27, |
||||
|
MetaMask = 1 << 28, |
||||
|
|
||||
|
ReleaseMask = 1 << 30, |
||||
|
|
||||
|
ModifierMask = 0x5c001fff |
||||
|
} |
||||
|
|
||||
|
[Flags] |
||||
|
internal enum IBusCapability |
||||
|
{ |
||||
|
CapPreeditText = 1 << 0, |
||||
|
CapAuxiliaryText = 1 << 1, |
||||
|
CapLookupTable = 1 << 2, |
||||
|
CapFocus = 1 << 3, |
||||
|
CapProperty = 1 << 4, |
||||
|
CapSurroundingText = 1 << 5, |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme.IBus |
||||
|
{ |
||||
|
internal class IBusX11TextInputMethod : DBusTextInputMethodBase |
||||
|
{ |
||||
|
private IIBusInputContext _context; |
||||
|
|
||||
|
public IBusX11TextInputMethod(Connection connection) : base(connection, |
||||
|
"org.freedesktop.portal.IBus") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task<bool> Connect(string name) |
||||
|
{ |
||||
|
var path = |
||||
|
await Connection.CreateProxy<IIBusPortal>(name, "/org/freedesktop/IBus") |
||||
|
.CreateInputContextAsync(GetAppName()); |
||||
|
|
||||
|
_context = Connection.CreateProxy<IIBusInputContext>(name, path); |
||||
|
AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); |
||||
|
AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); |
||||
|
Enqueue(() => _context.SetCapabilitiesAsync((uint)IBusCapability.CapFocus)); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private void OnForwardKey((uint keyval, uint keycode, uint state) k) |
||||
|
{ |
||||
|
var state = (IBusModifierMask)k.state; |
||||
|
KeyModifiers mods = default; |
||||
|
if (state.HasFlagCustom(IBusModifierMask.ControlMask)) |
||||
|
mods |= KeyModifiers.Control; |
||||
|
if (state.HasFlagCustom(IBusModifierMask.Mod1Mask)) |
||||
|
mods |= KeyModifiers.Alt; |
||||
|
if (state.HasFlagCustom(IBusModifierMask.ShiftMask)) |
||||
|
mods |= KeyModifiers.Shift; |
||||
|
if (state.HasFlagCustom(IBusModifierMask.Mod4Mask)) |
||||
|
mods |= KeyModifiers.Meta; |
||||
|
FireForward(new X11InputMethodForwardedKey |
||||
|
{ |
||||
|
KeyVal = (int)k.keyval, |
||||
|
Type = state.HasFlagCustom(IBusModifierMask.ReleaseMask) ? RawKeyEventType.KeyUp : RawKeyEventType.KeyDown, |
||||
|
Modifiers = mods |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void OnCommitText(object wtf) |
||||
|
{ |
||||
|
// Hello darkness, my old friend
|
||||
|
var prop = wtf.GetType().GetField("Item3"); |
||||
|
if (prop != null) |
||||
|
{ |
||||
|
var text = (string)prop.GetValue(wtf); |
||||
|
if (!string.IsNullOrEmpty(text)) |
||||
|
FireCommit(text); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override Task Disconnect() => _context.DestroyAsync(); |
||||
|
|
||||
|
protected override void OnDisconnected() |
||||
|
{ |
||||
|
_context = null; |
||||
|
base.OnDisconnected(); |
||||
|
} |
||||
|
|
||||
|
protected override Task SetCursorRectCore(PixelRect rect) |
||||
|
=> _context.SetCursorLocationAsync(rect.X, rect.Y, rect.Width, rect.Height); |
||||
|
|
||||
|
protected override Task SetActiveCore(bool active) |
||||
|
=> active ? _context.FocusInAsync() : _context.FocusOutAsync(); |
||||
|
|
||||
|
protected override Task ResetContextCore() |
||||
|
=> _context.ResetAsync(); |
||||
|
|
||||
|
protected override Task<bool> HandleKeyCore(RawKeyEventArgs args, int keyVal, int keyCode) |
||||
|
{ |
||||
|
IBusModifierMask state = default; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Control)) |
||||
|
state |= IBusModifierMask.ControlMask; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Alt)) |
||||
|
state |= IBusModifierMask.Mod1Mask; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Shift)) |
||||
|
state |= IBusModifierMask.ShiftMask; |
||||
|
if (args.Modifiers.HasFlagCustom(RawInputModifiers.Meta)) |
||||
|
state |= IBusModifierMask.Mod4Mask; |
||||
|
|
||||
|
if (args.Type == RawKeyEventType.KeyUp) |
||||
|
state |= IBusModifierMask.ReleaseMask; |
||||
|
|
||||
|
return _context.ProcessKeyEventAsync((uint)keyVal, (uint)keyCode, (uint)state); |
||||
|
} |
||||
|
|
||||
|
public override void SetOptions(TextInputOptionsQueryEventArgs options) |
||||
|
{ |
||||
|
// No-op, because ibus
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.FreeDesktop.DBusIme.Fcitx; |
||||
|
using Avalonia.FreeDesktop.DBusIme.IBus; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop.DBusIme |
||||
|
{ |
||||
|
public class X11DBusImeHelper |
||||
|
{ |
||||
|
private static readonly Dictionary<string, Func<Connection, IX11InputMethodFactory>> KnownMethods = |
||||
|
new Dictionary<string, Func<Connection, IX11InputMethodFactory>> |
||||
|
{ |
||||
|
["fcitx"] = conn => |
||||
|
new DBusInputMethodFactory<FcitxX11TextInputMethod>(_ => new FcitxX11TextInputMethod(conn)), |
||||
|
["ibus"] = conn => |
||||
|
new DBusInputMethodFactory<IBusX11TextInputMethod>(_ => new IBusX11TextInputMethod(conn)) |
||||
|
}; |
||||
|
|
||||
|
static Func<Connection, IX11InputMethodFactory> DetectInputMethod() |
||||
|
{ |
||||
|
foreach (var name in new[] { "AVALONIA_IM_MODULE", "GTK_IM_MODULE", "QT_IM_MODULE" }) |
||||
|
{ |
||||
|
var value = Environment.GetEnvironmentVariable(name); |
||||
|
|
||||
|
if (value == "none") |
||||
|
return null; |
||||
|
|
||||
|
if (value != null && KnownMethods.TryGetValue(value, out var factory)) |
||||
|
return factory; |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public static bool DetectAndRegister() |
||||
|
{ |
||||
|
var factory = DetectInputMethod(); |
||||
|
if (factory != null) |
||||
|
{ |
||||
|
var conn = DBusHelper.TryInitialize(); |
||||
|
if (conn != null) |
||||
|
{ |
||||
|
AvaloniaLocator.CurrentMutable.Bind<IX11InputMethodFactory>().ToConstant(factory(conn)); |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop |
||||
|
{ |
||||
|
public interface IX11InputMethodFactory |
||||
|
{ |
||||
|
(ITextInputMethodImpl method, IX11InputMethodControl control) CreateClient(IntPtr xid); |
||||
|
} |
||||
|
|
||||
|
public struct X11InputMethodForwardedKey |
||||
|
{ |
||||
|
public int KeyVal { get; set; } |
||||
|
public KeyModifiers Modifiers { get; set; } |
||||
|
public RawKeyEventType Type { get; set; } |
||||
|
} |
||||
|
|
||||
|
public interface IX11InputMethodControl : IDisposable |
||||
|
{ |
||||
|
void SetWindowActive(bool active); |
||||
|
bool IsEnabled { get; } |
||||
|
ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode); |
||||
|
event Action<string> Commit; |
||||
|
event Action<X11InputMethodForwardedKey> ForwardKey; |
||||
|
|
||||
|
void UpdateWindowInfo(PixelPoint position, double scaling); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
using System; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
public interface ITextInputMethodClient |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The cursor rectangle relative to the TextViewVisual
|
||||
|
/// </summary>
|
||||
|
Rect CursorRectangle { get; } |
||||
|
/// <summary>
|
||||
|
/// Should be fired when cursor rectangle is changed inside the TextViewVisual
|
||||
|
/// </summary>
|
||||
|
event EventHandler CursorRectangleChanged; |
||||
|
/// <summary>
|
||||
|
/// The visual that's showing the text
|
||||
|
/// </summary>
|
||||
|
IVisual TextViewVisual { get; } |
||||
|
/// <summary>
|
||||
|
/// Should be fired when text-hosting visual is changed
|
||||
|
/// </summary>
|
||||
|
event EventHandler TextViewVisualChanged; |
||||
|
/// <summary>
|
||||
|
/// Indicates if TextViewVisual is capable of displaying non-commited input on the cursor position
|
||||
|
/// </summary>
|
||||
|
bool SupportsPreedit { get; } |
||||
|
/// <summary>
|
||||
|
/// Sets the non-commited input string
|
||||
|
/// </summary>
|
||||
|
void SetPreeditText(string text); |
||||
|
/// <summary>
|
||||
|
/// Indicates if text input client is capable of providing the text around the cursor
|
||||
|
/// </summary>
|
||||
|
bool SupportsSurroundingText { get; } |
||||
|
/// <summary>
|
||||
|
/// Returns the text around the cursor, usually the current paragraph, the cursor position inside that text and selection start position
|
||||
|
/// </summary>
|
||||
|
TextInputMethodSurroundingText SurroundingText { get; } |
||||
|
/// <summary>
|
||||
|
/// Should be fired when surrounding text changed
|
||||
|
/// </summary>
|
||||
|
event EventHandler SurroundingTextChanged; |
||||
|
/// <summary>
|
||||
|
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the beginning of the text entry
|
||||
|
/// </summary>
|
||||
|
string TextBeforeCursor { get; } |
||||
|
/// <summary>
|
||||
|
/// Returns the text before the cursor. Must return a non-empty string if cursor is not at the end of the text entry
|
||||
|
/// </summary>
|
||||
|
string TextAfterCursor { get; } |
||||
|
} |
||||
|
|
||||
|
public struct TextInputMethodSurroundingText |
||||
|
{ |
||||
|
public string Text { get; set; } |
||||
|
public int CursorOffset { get; set; } |
||||
|
public int AnchorOffset { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
public interface ITextInputMethodImpl |
||||
|
{ |
||||
|
void SetActive(bool active); |
||||
|
void SetCursorRect(Rect rect); |
||||
|
void SetOptions(TextInputOptionsQueryEventArgs options); |
||||
|
void Reset(); |
||||
|
} |
||||
|
|
||||
|
public interface ITextInputMethodRoot : IInputRoot |
||||
|
{ |
||||
|
ITextInputMethodImpl InputMethod { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,101 @@ |
|||||
|
using System; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
internal class TextInputMethodManager |
||||
|
{ |
||||
|
private ITextInputMethodImpl? _im; |
||||
|
private IInputElement? _focusedElement; |
||||
|
private ITextInputMethodClient? _client; |
||||
|
private readonly TransformTrackingHelper _transformTracker = new TransformTrackingHelper(); |
||||
|
|
||||
|
public TextInputMethodManager() => _transformTracker.MatrixChanged += UpdateCursorRect; |
||||
|
|
||||
|
private ITextInputMethodClient? Client |
||||
|
{ |
||||
|
get => _client; |
||||
|
set |
||||
|
{ |
||||
|
if(_client == value) |
||||
|
return; |
||||
|
if (_client != null) |
||||
|
{ |
||||
|
_client.CursorRectangleChanged -= OnCursorRectangleChanged; |
||||
|
_client.TextViewVisualChanged -= OnTextViewVisualChanged; |
||||
|
} |
||||
|
|
||||
|
_client = value; |
||||
|
|
||||
|
if (_client != null) |
||||
|
{ |
||||
|
_client.CursorRectangleChanged += OnCursorRectangleChanged; |
||||
|
_client.TextViewVisualChanged += OnTextViewVisualChanged; |
||||
|
var optionsQuery = new TextInputOptionsQueryEventArgs |
||||
|
{ |
||||
|
RoutedEvent = InputElement.TextInputOptionsQueryEvent |
||||
|
}; |
||||
|
_focusedElement?.RaiseEvent(optionsQuery); |
||||
|
_im?.Reset(); |
||||
|
_im?.SetOptions(optionsQuery); |
||||
|
_transformTracker?.SetVisual(_client?.TextViewVisual); |
||||
|
UpdateCursorRect(); |
||||
|
_im?.SetActive(true); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_im?.SetActive(false); |
||||
|
_transformTracker.SetVisual(null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnTextViewVisualChanged(object sender, EventArgs e) |
||||
|
=> _transformTracker.SetVisual(_client?.TextViewVisual); |
||||
|
|
||||
|
private void UpdateCursorRect() |
||||
|
{ |
||||
|
if (_im == null || _client == null || _focusedElement?.VisualRoot == null) |
||||
|
return; |
||||
|
var transform = _focusedElement.TransformToVisual(_focusedElement.VisualRoot); |
||||
|
if (transform == null) |
||||
|
_im.SetCursorRect(default); |
||||
|
else |
||||
|
_im.SetCursorRect(_client.CursorRectangle.TransformToAABB(transform.Value)); |
||||
|
} |
||||
|
|
||||
|
private void OnCursorRectangleChanged(object sender, EventArgs e) |
||||
|
{ |
||||
|
if (sender == _client) |
||||
|
UpdateCursorRect(); |
||||
|
} |
||||
|
|
||||
|
public void SetFocusedElement(IInputElement? element) |
||||
|
{ |
||||
|
if(_focusedElement == element) |
||||
|
return; |
||||
|
_focusedElement = element; |
||||
|
|
||||
|
var inputMethod = (element?.VisualRoot as ITextInputMethodRoot)?.InputMethod; |
||||
|
if(_im != inputMethod) |
||||
|
_im?.SetActive(false); |
||||
|
|
||||
|
_im = inputMethod; |
||||
|
|
||||
|
if (_focusedElement == null || _im == null) |
||||
|
{ |
||||
|
Client = null; |
||||
|
_im?.SetActive(false); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var clientQuery = new TextInputMethodClientRequestedEventArgs |
||||
|
{ |
||||
|
RoutedEvent = InputElement.TextInputMethodClientRequestedEvent |
||||
|
}; |
||||
|
|
||||
|
_focusedElement.RaiseEvent(clientQuery); |
||||
|
Client = clientQuery.Client; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
public enum TextInputContentType |
||||
|
{ |
||||
|
Normal = 0, |
||||
|
Email = 1, |
||||
|
Phone = 2, |
||||
|
Number = 3, |
||||
|
Url = 4, |
||||
|
Password = 5 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
public class TextInputMethodClientRequestedEventArgs : RoutedEventArgs |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Set this property to a valid text input client to enable input method interaction
|
||||
|
/// </summary>
|
||||
|
public ITextInputMethodClient? Client { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
public class TextInputOptionsQueryEventArgs : RoutedEventArgs |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The content type (mostly for determining the shape of the virtual keyboard)
|
||||
|
/// </summary>
|
||||
|
public TextInputContentType ContentType { get; set; } |
||||
|
/// <summary>
|
||||
|
/// Text is multiline
|
||||
|
/// </summary>
|
||||
|
public bool Multiline { get; set; } |
||||
|
/// <summary>
|
||||
|
/// Text is in lower case
|
||||
|
/// </summary>
|
||||
|
public bool Lowercase { get; set; } |
||||
|
/// <summary>
|
||||
|
/// Text is in upper case
|
||||
|
/// </summary>
|
||||
|
public bool Uppercase { get; set; } |
||||
|
/// <summary>
|
||||
|
/// Automatically capitalize letters at the start of the sentence
|
||||
|
/// </summary>
|
||||
|
public bool AutoCapitalization { get; set; } |
||||
|
/// <summary>
|
||||
|
/// Text contains sensitive data like card numbers and should not be stored
|
||||
|
/// </summary>
|
||||
|
public bool IsSensitive { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Input.TextInput |
||||
|
{ |
||||
|
class TransformTrackingHelper : IDisposable |
||||
|
{ |
||||
|
private IVisual? _visual; |
||||
|
private bool _queuedForUpdate; |
||||
|
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler; |
||||
|
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>(); |
||||
|
|
||||
|
public TransformTrackingHelper() |
||||
|
{ |
||||
|
_propertyChangedHandler = PropertyChangedHandler; |
||||
|
} |
||||
|
|
||||
|
public void SetVisual(IVisual? visual) |
||||
|
{ |
||||
|
Dispose(); |
||||
|
_visual = visual; |
||||
|
if (visual != null) |
||||
|
{ |
||||
|
visual.AttachedToVisualTree += OnAttachedToVisualTree; |
||||
|
visual.DetachedFromVisualTree -= OnDetachedFromVisualTree; |
||||
|
if (visual.IsAttachedToVisualTree) |
||||
|
SubscribeToParents(); |
||||
|
UpdateMatrix(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Matrix? Matrix { get; private set; } |
||||
|
public event Action? MatrixChanged; |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if(_visual == null) |
||||
|
return; |
||||
|
UnsubscribeFromParents(); |
||||
|
_visual.AttachedToVisualTree -= OnAttachedToVisualTree; |
||||
|
_visual.DetachedFromVisualTree -= OnDetachedFromVisualTree; |
||||
|
_visual = null; |
||||
|
} |
||||
|
|
||||
|
private void SubscribeToParents() |
||||
|
{ |
||||
|
var visual = _visual; |
||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
|
// false positive
|
||||
|
while (visual != null) |
||||
|
{ |
||||
|
if (visual is Visual v) |
||||
|
{ |
||||
|
v.PropertyChanged += _propertyChangedHandler; |
||||
|
_propertyChangedSubscriptions.Add(v); |
||||
|
} |
||||
|
|
||||
|
visual = visual.VisualParent; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void UnsubscribeFromParents() |
||||
|
{ |
||||
|
foreach (var v in _propertyChangedSubscriptions) |
||||
|
v.PropertyChanged -= _propertyChangedHandler; |
||||
|
_propertyChangedSubscriptions.Clear(); |
||||
|
} |
||||
|
|
||||
|
void UpdateMatrix() |
||||
|
{ |
||||
|
Matrix? matrix = null; |
||||
|
if (_visual != null && _visual.VisualRoot != null) |
||||
|
matrix = _visual.TransformToVisual(_visual.VisualRoot); |
||||
|
if (Matrix != matrix) |
||||
|
{ |
||||
|
Matrix = matrix; |
||||
|
MatrixChanged?.Invoke(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) |
||||
|
{ |
||||
|
SubscribeToParents(); |
||||
|
UpdateMatrix(); |
||||
|
} |
||||
|
|
||||
|
private void EnqueueForUpdate() |
||||
|
{ |
||||
|
if(_queuedForUpdate) |
||||
|
return; |
||||
|
_queuedForUpdate = true; |
||||
|
Dispatcher.UIThread.Post(UpdateMatrix, DispatcherPriority.Render); |
||||
|
} |
||||
|
|
||||
|
private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.IsEffectiveValueChange && e.Property == Visual.BoundsProperty) |
||||
|
EnqueueForUpdate(); |
||||
|
} |
||||
|
|
||||
|
private void OnDetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs visualTreeAttachmentEventArgs) |
||||
|
{ |
||||
|
UnsubscribeFromParents(); |
||||
|
UpdateMatrix(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,208 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Text; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.FreeDesktop; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Platform.Interop; |
||||
|
using static Avalonia.X11.XLib; |
||||
|
|
||||
|
namespace Avalonia.X11 |
||||
|
{ |
||||
|
partial class X11Window |
||||
|
{ |
||||
|
private ITextInputMethodImpl _ime; |
||||
|
private IX11InputMethodControl _imeControl; |
||||
|
private bool _processingIme; |
||||
|
|
||||
|
private Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue = |
||||
|
new Queue<(RawKeyEventArgs args, XEvent xev, int keyVal, int keyCode)>(); |
||||
|
|
||||
|
unsafe void CreateIC() |
||||
|
{ |
||||
|
if (_x11.HasXim) |
||||
|
{ |
||||
|
XGetIMValues(_x11.Xim, XNames.XNQueryInputStyle, out var supported_styles, IntPtr.Zero); |
||||
|
for (var c = 0; c < supported_styles->count_styles; c++) |
||||
|
{ |
||||
|
var style = (XIMProperties)supported_styles->supported_styles[c]; |
||||
|
if ((int)(style & XIMProperties.XIMPreeditPosition) != 0 |
||||
|
&& ((int)(style & XIMProperties.XIMStatusNothing) != 0)) |
||||
|
{ |
||||
|
XPoint spot = default; |
||||
|
XRectangle area = default; |
||||
|
|
||||
|
|
||||
|
//using var areaS = new Utf8Buffer("area");
|
||||
|
using var spotS = new Utf8Buffer("spotLocation"); |
||||
|
using var fontS = new Utf8Buffer("fontSet"); |
||||
|
|
||||
|
var list = XVaCreateNestedList(0, |
||||
|
//areaS, &area,
|
||||
|
spotS, &spot, |
||||
|
fontS, _x11.DefaultFontSet, |
||||
|
IntPtr.Zero); |
||||
|
_xic = XCreateIC(_x11.Xim, |
||||
|
XNames.XNClientWindow, _handle, |
||||
|
XNames.XNFocusWindow, _handle, |
||||
|
XNames.XNInputStyle, new IntPtr((int)style), |
||||
|
XNames.XNResourceName, _platform.Options.WmClass, |
||||
|
XNames.XNResourceClass, _platform.Options.WmClass, |
||||
|
XNames.XNPreeditAttributes, list, |
||||
|
IntPtr.Zero); |
||||
|
|
||||
|
XFree(list); |
||||
|
|
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
XFree(new IntPtr(supported_styles)); |
||||
|
} |
||||
|
|
||||
|
if (_xic == IntPtr.Zero) |
||||
|
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, |
||||
|
new IntPtr((int)(XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing)), |
||||
|
XNames.XNClientWindow, _handle, XNames.XNFocusWindow, _handle, IntPtr.Zero); |
||||
|
} |
||||
|
|
||||
|
void InitializeIme() |
||||
|
{ |
||||
|
var ime = AvaloniaLocator.Current.GetService<IX11InputMethodFactory>()?.CreateClient(_handle); |
||||
|
if (ime == null && _x11.HasXim) |
||||
|
{ |
||||
|
var xim = new XimInputMethod(this); |
||||
|
ime = (xim, xim); |
||||
|
} |
||||
|
if (ime != null) |
||||
|
{ |
||||
|
(_ime, _imeControl) = ime.Value; |
||||
|
_imeControl.Commit += s => |
||||
|
ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(), |
||||
|
_inputRoot, s)); |
||||
|
_imeControl.ForwardKey += ev => |
||||
|
{ |
||||
|
ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(), |
||||
|
_inputRoot, ev.Type, X11KeyTransform.ConvertKey((X11Key)ev.KeyVal), |
||||
|
(RawInputModifiers)ev.Modifiers)); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void UpdateImePosition() => _imeControl?.UpdateWindowInfo(Position, RenderScaling); |
||||
|
|
||||
|
void HandleKeyEvent(ref XEvent ev) |
||||
|
{ |
||||
|
var index = ev.KeyEvent.state.HasFlag(XModifierMask.ShiftMask); |
||||
|
|
||||
|
// We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
|
||||
|
var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32(); |
||||
|
|
||||
|
// Manually switch the Shift index for the keypad,
|
||||
|
// there should be a proper way to do this
|
||||
|
if (ev.KeyEvent.state.HasFlag(XModifierMask.Mod2Mask) |
||||
|
&& key > X11Key.Num_Lock && key <= X11Key.KP_9) |
||||
|
key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32(); |
||||
|
|
||||
|
var filtered = ScheduleKeyInput(new RawKeyEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, |
||||
|
ev.type == XEventName.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, |
||||
|
X11KeyTransform.ConvertKey(key), TranslateModifiers(ev.KeyEvent.state)), ref ev, (int)key, ev.KeyEvent.keycode); |
||||
|
|
||||
|
if (ev.type == XEventName.KeyPress && !filtered) |
||||
|
TriggerClassicTextInputEvent(ref ev); |
||||
|
} |
||||
|
|
||||
|
void TriggerClassicTextInputEvent(ref XEvent ev) |
||||
|
{ |
||||
|
var text = TranslateEventToString(ref ev); |
||||
|
if (text != null) |
||||
|
ScheduleInput( |
||||
|
new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), |
||||
|
ref ev); |
||||
|
} |
||||
|
|
||||
|
private const int ImeBufferSize = 64 * 1024; |
||||
|
[ThreadStatic] private static IntPtr ImeBuffer; |
||||
|
|
||||
|
unsafe string TranslateEventToString(ref XEvent ev) |
||||
|
{ |
||||
|
if (ImeBuffer == IntPtr.Zero) |
||||
|
ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize); |
||||
|
|
||||
|
var len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), ImeBufferSize, |
||||
|
out _, out var istatus); |
||||
|
var status = (XLookupStatus)istatus; |
||||
|
|
||||
|
if (len == 0) |
||||
|
return null; |
||||
|
|
||||
|
string text; |
||||
|
if (status == XLookupStatus.XBufferOverflow) |
||||
|
return null; |
||||
|
else |
||||
|
text = Encoding.UTF8.GetString((byte*)ImeBuffer.ToPointer(), len); |
||||
|
|
||||
|
if (text == null) |
||||
|
return null; |
||||
|
|
||||
|
if (text.Length == 1) |
||||
|
{ |
||||
|
if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
return text; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
bool ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode) |
||||
|
{ |
||||
|
_x11.LastActivityTimestamp = xev.ButtonEvent.time; |
||||
|
if (_imeControl != null && _imeControl.IsEnabled) |
||||
|
{ |
||||
|
if (FilterIme(args, xev, keyval, keycode)) |
||||
|
return true; |
||||
|
} |
||||
|
ScheduleInput(args); |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
bool FilterIme(RawKeyEventArgs args, XEvent xev, int keyval, int keycode) |
||||
|
{ |
||||
|
if (_ime == null) |
||||
|
return false; |
||||
|
_imeQueue.Enqueue((args, xev, keyval, keycode)); |
||||
|
if (!_processingIme) |
||||
|
ProcessNextImeEvent(); |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
async void ProcessNextImeEvent() |
||||
|
{ |
||||
|
if(_processingIme) |
||||
|
return; |
||||
|
_processingIme = true; |
||||
|
try |
||||
|
{ |
||||
|
while (_imeQueue.Count != 0) |
||||
|
{ |
||||
|
var ev = _imeQueue.Dequeue(); |
||||
|
if (_imeControl == null || !await _imeControl.HandleEventAsync(ev.args, ev.keyval, ev.keycode)) |
||||
|
{ |
||||
|
ScheduleInput(ev.args); |
||||
|
if (ev.args.Type == RawKeyEventType.KeyDown) |
||||
|
TriggerClassicTextInputEvent(ref ev.xev); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_processingIme = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,121 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.FreeDesktop; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Input.TextInput; |
||||
|
using Avalonia.Platform.Interop; |
||||
|
using Avalonia.Threading; |
||||
|
using static Avalonia.X11.XLib; |
||||
|
namespace Avalonia.X11 |
||||
|
{ |
||||
|
partial class X11Window |
||||
|
{ |
||||
|
|
||||
|
class XimInputMethod : ITextInputMethodImpl, IX11InputMethodControl |
||||
|
{ |
||||
|
private readonly X11Window _parent; |
||||
|
private bool _controlActive, _windowActive, _imeActive; |
||||
|
private Rect? _queuedCursorRect; |
||||
|
|
||||
|
public XimInputMethod(X11Window parent) |
||||
|
{ |
||||
|
_parent = parent; |
||||
|
} |
||||
|
|
||||
|
public void SetCursorRect(Rect rect) |
||||
|
{ |
||||
|
var needEnqueue = _queuedCursorRect == null; |
||||
|
_queuedCursorRect = rect; |
||||
|
if(needEnqueue) |
||||
|
Dispatcher.UIThread.Post(() => |
||||
|
{ |
||||
|
if(_queuedCursorRect == null) |
||||
|
return; |
||||
|
var rc = _queuedCursorRect.Value; |
||||
|
_queuedCursorRect = null; |
||||
|
|
||||
|
if (_parent._xic == IntPtr.Zero) |
||||
|
return; |
||||
|
|
||||
|
rect *= _parent._scaling; |
||||
|
|
||||
|
var pt = new XPoint |
||||
|
{ |
||||
|
X = (short)Math.Min(Math.Max(rect.X, short.MinValue), short.MaxValue), |
||||
|
Y = (short)Math.Min(Math.Max(rect.Y + rect.Height, short.MinValue), short.MaxValue) |
||||
|
}; |
||||
|
|
||||
|
using var spotLoc = new Utf8Buffer(XNames.XNSpotLocation); |
||||
|
var list = XVaCreateNestedList(0, spotLoc, ref pt, IntPtr.Zero); |
||||
|
XSetICValues(_parent._xic, XNames.XNPreeditAttributes, list, IntPtr.Zero); |
||||
|
XFree(list); |
||||
|
}, DispatcherPriority.Background); |
||||
|
} |
||||
|
|
||||
|
public void SetWindowActive(bool active) |
||||
|
{ |
||||
|
_windowActive = active; |
||||
|
UpdateActive(); |
||||
|
} |
||||
|
|
||||
|
public void SetActive(bool active) |
||||
|
{ |
||||
|
_controlActive = active; |
||||
|
UpdateActive(); |
||||
|
} |
||||
|
|
||||
|
private void UpdateActive() |
||||
|
{ |
||||
|
var active = _windowActive && _controlActive; |
||||
|
if(_parent._xic == IntPtr.Zero) |
||||
|
return; |
||||
|
if (active != _imeActive) |
||||
|
{ |
||||
|
_imeActive = active; |
||||
|
if (active) |
||||
|
{ |
||||
|
Reset(); |
||||
|
XSetICFocus(_parent._xic); |
||||
|
} |
||||
|
else |
||||
|
XUnsetICFocus(_parent._xic); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void UpdateWindowInfo(PixelPoint position, double scaling) |
||||
|
{ |
||||
|
// No-op
|
||||
|
} |
||||
|
|
||||
|
public void SetOptions(TextInputOptionsQueryEventArgs options) |
||||
|
{ |
||||
|
// No-op
|
||||
|
} |
||||
|
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
if(_parent._xic == IntPtr.Zero) |
||||
|
return; |
||||
|
|
||||
|
var data = XmbResetIC(_parent._xic); |
||||
|
if (data != IntPtr.Zero) |
||||
|
XFree(data); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
// No-op
|
||||
|
} |
||||
|
|
||||
|
public bool IsEnabled => false; |
||||
|
|
||||
|
public ValueTask<bool> HandleEventAsync(RawKeyEventArgs args, int keyVal, int keyCode) => |
||||
|
new ValueTask<bool>(false); |
||||
|
|
||||
|
public event Action<string> Commit; |
||||
|
public event Action<X11InputMethodForwardedKey> ForwardKey; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue