committed by
GitHub
127 changed files with 3031 additions and 573 deletions
@ -0,0 +1,31 @@ |
|||
--- |
|||
name: Bug report |
|||
about: Create a report to help us improve |
|||
title: '' |
|||
labels: '' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Describe the bug** |
|||
A clear and concise description of what the bug is. |
|||
|
|||
**To Reproduce** |
|||
Steps to reproduce the behavior: |
|||
1. Go to '...' |
|||
2. Click on '....' |
|||
3. Scroll down to '....' |
|||
4. See error |
|||
|
|||
**Expected behavior** |
|||
A clear and concise description of what you expected to happen. |
|||
|
|||
**Screenshots** |
|||
If applicable, add screenshots to help explain your problem. |
|||
|
|||
**Desktop (please complete the following information):** |
|||
- OS: [e.g. Windows, Mac, Linux (State distribution)] |
|||
- Version [e.g. 0.10.0-rc1 or 0.9.12] |
|||
|
|||
**Additional context** |
|||
Add any other context about the problem here. |
|||
@ -0,0 +1,20 @@ |
|||
--- |
|||
name: Feature request |
|||
about: Suggest an idea for this project |
|||
title: '' |
|||
labels: '' |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Is your feature request related to a problem? Please describe.** |
|||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
|||
|
|||
**Describe the solution you'd like** |
|||
A clear and concise description of what you want to happen. |
|||
|
|||
**Describe alternatives you've considered** |
|||
A clear and concise description of any alternative solutions or features you've considered. |
|||
|
|||
**Additional context** |
|||
Add any other context or screenshots about the feature request here. |
|||
@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="HarfBuzzSharp" Version="2.6.1.6" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="HarfBuzzSharp.NativeAssets.Linux" 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.7" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -1,5 +1,26 @@ |
|||
<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> |
|||
<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> |
|||
</Project> |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
<?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"> |
|||
<uses-sdk /> |
|||
<uses-sdk android:targetSdkVersion="29" /> |
|||
<application android:label="ControlCatalog.Android"></application> |
|||
</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"?> |
|||
<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> |
|||
<uses-permission android:name="android.permission.INTERNET" /> |
|||
</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(); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue