Browse Source

Samsung Tizen (#12151)

* implement tizen platform, resolve #6110

* Fix enum types of touch event

* Fix issue of touch tracking.

* #6110 fix ui thread lock

* #6110 send keypad from tizen window

* #6110 fix touch update

* #6110 add text editor for input fields

* #6110 Hide text field

* #6110 Fix issue of text selection and backspace

* #6110 support multi and single line for input

* #6110 temporary set multiline true by default.

* #6110 fix issue of build with latest version and make it compatible

* #6110 by pass double input when softkeyboard showing.

* #6110 adding support for clipboard.

* #6110 add permission helper to tizen platform

* #6110 Implementation of storage provider for tizen

* #6110 send text for key character from hard keyboard

* #6110 Complete the input implementation. soft and hard keyboard

* #6110 remove ElmSharp

* Update Permissions.cs

* #6110 remove open folder for tizen

* #6110 fix the issue of keyboard frezee for tizen

* #6110 add workload to build tizen

* #6110 try to setup tizen workloads for windows

* #6110 change tizen workload install to powershell

* #6110 skip api diff check when package not exists

* #6110 try to fix linux pipeline

* #6110 implement tizen native view

* #6110 add dev analyzer and trimming

* #6110 Migrate to GLES and remove framebuffer

* #6110 Temporary solution to make GL work

* #6110 WIP and add comments

* #6110 optimise gles rendering and timing.

* #6110 fix dependency issue of tizen HarfBuzzSharp

* #6110 patch a fix for clipboard issue

* #6110 Support tizen sdk for old proj type, WIP

* #6110 wip setup app

* #6110 support only net6 for tizen

* #6110 resolve comments

---------

Co-authored-by: Kangho Hur <rookiejava+github@gmail.com>
Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/12987/head
Omid Mafakher 3 years ago
committed by GitHub
parent
commit
3687cb9ecc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      Avalonia.sln
  2. 23
      azure-pipelines.yml
  3. 10
      nukebuild/ApiDiffValidation.cs
  4. 20
      samples/ControlCatalog.Tizen/ControlCatalog.Tizen.csproj
  5. 41
      samples/ControlCatalog.Tizen/EmbedSampleNuiTizen.cs
  6. 23
      samples/ControlCatalog.Tizen/Main.cs
  7. BIN
      samples/ControlCatalog.Tizen/shared/res/Avalonia.png
  8. 23
      samples/ControlCatalog.Tizen/tizen-manifest.xml
  9. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  10. 20
      src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj
  11. 12
      src/Tizen/Avalonia.Tizen/ITizenView.cs
  12. 179
      src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs
  13. 205
      src/Tizen/Avalonia.Tizen/NuiAvaloniaViewTextEditable.cs
  14. 76
      src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs
  15. 73
      src/Tizen/Avalonia.Tizen/NuiGlLayerSurface.cs
  16. 87
      src/Tizen/Avalonia.Tizen/NuiGlPlatform.cs
  17. 102
      src/Tizen/Avalonia.Tizen/NuiKeyboardHandler.cs
  18. 117
      src/Tizen/Avalonia.Tizen/NuiNativeControlHostImpl.cs
  19. 73
      src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs
  20. 99
      src/Tizen/Avalonia.Tizen/NuiTouchHandler.cs
  21. 39
      src/Tizen/Avalonia.Tizen/NuiViewControlHandle.cs
  22. 29
      src/Tizen/Avalonia.Tizen/Platform/Consts.cs
  23. 221
      src/Tizen/Avalonia.Tizen/Platform/Input/TizenKeyboardDevice.cs
  24. 20
      src/Tizen/Avalonia.Tizen/Platform/Interop/Strucs.cs
  25. 110
      src/Tizen/Avalonia.Tizen/Platform/Permissions.cs
  26. 10
      src/Tizen/Avalonia.Tizen/Platform/TizenPlatformSettings.cs
  27. 65
      src/Tizen/Avalonia.Tizen/Stubs.cs
  28. 20
      src/Tizen/Avalonia.Tizen/TizenApplicationExtensions.cs
  29. 35
      src/Tizen/Avalonia.Tizen/TizenPlatform.cs
  30. 44
      src/Tizen/Avalonia.Tizen/TizenRenderTimer.cs
  31. 152
      src/Tizen/Avalonia.Tizen/TizenStorageProvider.cs
  32. 50
      src/Tizen/Avalonia.Tizen/TizenThreadingInterface.cs
  33. 117
      src/Tizen/Avalonia.Tizen/TopLevelImpl.cs

18
Avalonia.sln

@ -273,6 +273,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless.XUnit.Uni
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MobileSandbox.Browser", "samples\MobileSandbox.Browser\MobileSandbox.Browser.csproj", "{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tizen", "Tizen", "{D1300000-7217-4693-8B0F-57CBD5814302}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Tizen", "src\Tizen\Avalonia.Tizen\Avalonia.Tizen.csproj", "{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Tizen", "samples\ControlCatalog.Tizen\ControlCatalog.Tizen.csproj", "{A0B29221-2B6F-4B29-A4D5-2227811B5915}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Metal", "src\Avalonia.Metal\Avalonia.Metal.csproj", "{60B4ED1F-ECFA-453B-8A70-1788261C8355}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks.UnitTest", "tests\Avalonia.Build.Tasks.UnitTest\Avalonia.Build.Tasks.UnitTest.csproj", "{B0FD6A48-FBAB-4676-B36A-DE76B0922B12}"
@ -670,6 +676,16 @@ Global
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8}.Release|Any CPU.Build.0 = Release|Any CPU
{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8}.Release|Any CPU.Build.0 = Release|Any CPU
{A0B29221-2B6F-4B29-A4D5-2227811B5915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0B29221-2B6F-4B29-A4D5-2227811B5915}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0B29221-2B6F-4B29-A4D5-2227811B5915}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{A0B29221-2B6F-4B29-A4D5-2227811B5915}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A0B29221-2B6F-4B29-A4D5-2227811B5915}.Release|Any CPU.Build.0 = Release|Any CPU
{A0B29221-2B6F-4B29-A4D5-2227811B5915}.Release|Any CPU.Deploy.0 = Release|Any CPU
{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60B4ED1F-ECFA-453B-8A70-1788261C8355}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -764,6 +780,8 @@ Global
{2999D79E-3C20-4A90-B651-CA7E0AC92D35} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{F83FC908-A4E3-40DE-B4CF-A4BA1E92CDB3} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{43FCC14E-EEBE-44B3-BCBC-F1C537EECBF8} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{DFFBDBF5-5DBE-47ED-9EAE-D40B75AC99E8} = {D1300000-7217-4693-8B0F-57CBD5814302}
{A0B29221-2B6F-4B29-A4D5-2227811B5915} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{B0FD6A48-FBAB-4676-B36A-DE76B0922B12} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}

23
azure-pipelines.yml

@ -45,6 +45,13 @@ jobs:
script: |
dotnet workload install wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Install Tizen Workload'
inputs:
targetType: 'inline'
script: |
curl -sSL https://raw.githubusercontent.com/Samsung/Tizen.NET/main/workload/scripts/workload-install.sh | sudo bash -s -- -d "/opt/hostedtoolcache/dotnet"
- task: CmdLine@2
displayName: 'Run Build'
inputs:
@ -81,7 +88,14 @@ jobs:
inputs:
script: |
dotnet workload install wasm-tools wasm-experimental
- task: CmdLine@2
displayName: 'Install Tizen Workload'
inputs:
targetType: 'inline'
script: |
curl -sSL https://raw.githubusercontent.com/Samsung/Tizen.NET/main/workload/scripts/workload-install.sh | sudo bash
- task: CmdLine@2
displayName: 'Generate avalonia-native'
inputs:
@ -153,6 +167,13 @@ jobs:
script: |
dotnet workload install android ios wasm-tools wasm-experimental
- task: PowerShell@2
displayName: 'Install Tizen Workload'
inputs:
targetType: 'inline'
script: |
(New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Samsung/Tizen.NET/main/workload/scripts/workload-install.ps1') | Invoke-Expression
- task: CmdLine@2
displayName: 'Install Nuke'
inputs:

10
nukebuild/ApiDiffValidation.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -27,7 +28,10 @@ public static class ApiDiffValidation
Directory.CreateDirectory(suppressionFilesFolder!);
}
await using (var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion))
await using var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion);
if (baselineStream == null)
return;
using (var target = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.Read), ZipArchiveMode.Read))
using (var baseline = new ZipArchive(baselineStream, ZipArchiveMode.Read))
using (Helpers.UseTempDir(out var tempFolder))
@ -139,6 +143,10 @@ public static class ApiDiffValidation
memoryStream.Seek(0, SeekOrigin.Begin);
return memoryStream;
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
return null;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Downloading baseline package for {packageId} {baselineVersion} failed.\r" + ex.Message, ex);

20
samples/ControlCatalog.Tizen/ControlCatalog.Tizen.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0-tizen</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<TizenSharedResource Remove="shared\res\Avalonia.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Tizen\Avalonia.Tizen\Avalonia.Tizen.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="lib\" />
<Folder Include="res\" />
</ItemGroup>
</Project>

41
samples/ControlCatalog.Tizen/EmbedSampleNuiTizen.cs

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Platform;
using Avalonia.Tizen;
using ControlCatalog.Pages;
using Tizen.NUI.BaseComponents;
using Tizen.NUI.Components;
using Tizen.Pims.Contacts.ContactsViews;
namespace ControlCatalog.Tizen;
public class EmbedSampleNuiTizen : INativeDemoControl
{
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault)
{
if (isSecond)
{
var webView = new WebView();
webView.LoadUrl("https://avaloniaui.net/");
return new NuiViewControlHandle(webView);
}
else
{
var clickCount = 0;
var button = new Button
{
Text = "Hello world"
};
button.Clicked += (sender, e) =>
{
clickCount++;
button.Text = $"Click count {clickCount}";
};
return new NuiViewControlHandle(button);
}
}
}

23
samples/ControlCatalog.Tizen/Main.cs

@ -0,0 +1,23 @@
using System;
using Avalonia;
using Avalonia.Tizen;
using ElmSharp;
using SkiaSharp;
using Tizen.Applications;
namespace ControlCatalog.Tizen;
class Program : NuiTizenApplication<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) =>
base.CustomizeAppBuilder(builder).AfterSetup(_ =>
{
Pages.EmbedSample.Implementation = new EmbedSampleNuiTizen();
});
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}

BIN
samples/ControlCatalog.Tizen/shared/res/Avalonia.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

23
samples/ControlCatalog.Tizen/tizen-manifest.xml

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.avalonia.control-catalog" version="1.0.0" api-version="7.0" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="com.avalonia.control-catalog" exec="ControlCatalog.Tizen.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" hw-acceleration="on" launch_mode="single">
<label>Avalonia</label>
<icon>Avalonia.png</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
<splash-screens />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/appdir.shareddata</privilege>
<privilege>http://tizen.org/privilege/appmanager.launch</privilege>
<privilege>http://tizen.org/privilege/externalstorage</privilege>
<privilege>http://tizen.org/privilege/externalstorage.appdata</privilege>
<privilege>http://tizen.org/privilege/internet</privilege>
<privilege>http://tizen.org/privilege/network.get</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
<feature>http://tizen.org/feature/opengles.surfaceless_context</feature>
<feature>http://tizen.org/feature/opengles.version.2_0</feature>
</manifest>

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -54,6 +54,7 @@
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.iOS, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Tizen, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Dialogs, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Diagnostics, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LinuxFramebuffer, PublicKey=$(AvaloniaPublicKey)" />

20
src/Tizen/Avalonia.Tizen/Avalonia.Tizen.csproj

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net6.0-tizen</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
<PackageReference Include="HarfBuzzSharp.NativeAssets.Tizen" Version="2.8.2.3" />
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />
</Project>

12
src/Tizen/Avalonia.Tizen/ITizenView.cs

@ -0,0 +1,12 @@
using Avalonia.Controls.Platform;
using Avalonia.Input;
namespace Avalonia.Tizen;
internal interface ITizenView
{
Size ClientSize { get; }
double Scaling { get; }
IInputRoot InputRoot { get; set; }
INativeControlHostImpl NativeControlHost { get; }
}

179
src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs

@ -0,0 +1,179 @@
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading;
using Tizen.NUI;
using Tizen.NUI.BaseComponents;
namespace Avalonia.Tizen;
/// <summary>
/// Avalonia View for Tizen NUI controls
/// </summary>
public class NuiAvaloniaView : GLView, ITizenView, ITextInputMethodImpl
{
private readonly NuiKeyboardHandler _keyboardHandler;
private readonly NuiTouchHandler _touchHandler;
private readonly NuiAvaloniaViewTextEditable _textEditor;
private TizenRenderTimer? _renderTimer;
private TopLevelImpl _topLevelImpl;
private EmbeddableControlRoot _topLevel;
private TouchDevice _device = new();
private ServerCompositionTarget _compositionTargetServer;
public IInputRoot InputRoot { get; set; }
public INativeControlHostImpl NativeControlHost { get; }
internal TopLevelImpl TopLevelImpl => _topLevelImpl;
public double Scaling => 1;
public Size ClientSize => new(Size.Width, Size.Height);
public Control? Content
{
get => _topLevel.Content as Control;
set => _topLevel.Content = value;
}
internal NuiAvaloniaViewTextEditable TextEditor => _textEditor;
internal NuiKeyboardHandler KeyboardHandler => _keyboardHandler;
#region Setup
public event Action OnSurfaceInit;
public NuiAvaloniaView() : base(ColorFormat.RGBA8888)
{
RenderingMode = GLRenderingMode.OnDemand;
SetGraphicsConfig(true, true, 0, GLESVersion.Version30);
RegisterGLCallbacks(GlInit, GlRenderFrame, GlTerminate);
_textEditor = new NuiAvaloniaViewTextEditable(this);
_keyboardHandler = new NuiKeyboardHandler(this);
_touchHandler = new NuiTouchHandler(this);
NativeControlHost = new NuiNativeControlHostImpl(this);
Layout = new CustomLayout
{
SizeUpdated = OnResized
};
TouchEvent += OnTouchEvent;
WheelEvent += OnWheelEvent;
}
private void GlInit()
{
OnSurfaceInit?.Invoke();
}
private int GlRenderFrame()
{
if (_renderTimer == null || _topLevel == null)
return 0;
var rev = _compositionTargetServer.Revision;
_renderTimer.ManualTick();
return rev == _compositionTargetServer.Revision ? 0 : 1;
}
private void GlTerminate()
{
}
internal void Initialise()
{
_topLevelImpl = new TopLevelImpl(this, new[] { new NuiGlLayerSurface(this) });
_topLevelImpl.Compositor.AfterCommit += RenderOnce;
TizenPlatform.ThreadingInterface.TickExecuted += RenderOnce;
_topLevel = new(_topLevelImpl);
_topLevel.Prepare();
_topLevel.StartRendering();
_compositionTargetServer = ((CompositingRenderer)((IRenderRoot)_topLevel).Renderer).CompositionTarget.Server;
_renderTimer = (TizenRenderTimer)AvaloniaLocator.Current.GetRequiredService<IRenderTimer>();
_renderTimer.RenderTick += RenderOnce;
OnResized();
}
#endregion
#region Resize and layout
private class CustomLayout : AbsoluteLayout
{
float _width;
float _height;
public Action? SizeUpdated { get; set; }
protected override void OnLayout(bool changed, LayoutLength left, LayoutLength top, LayoutLength right, LayoutLength bottom)
{
var sizeChanged = _width != Owner.SizeWidth || _height != Owner.SizeHeight;
_width = Owner.SizeWidth;
_height = Owner.SizeHeight;
if (sizeChanged)
{
SizeUpdated?.Invoke();
}
base.OnLayout(changed, left, top, right, bottom);
}
}
protected void OnResized()
{
if (Size.Width == 0 || Size.Height == 0)
return;
_topLevelImpl?.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout);
}
#endregion
#region Event handlers
private bool OnTouchEvent(object source, TouchEventArgs e)
{
_touchHandler?.Handle(e);
return true;
}
private bool OnWheelEvent(object source, WheelEventArgs e)
{
_touchHandler?.Handle(e);
return true;
}
public void SetClient(TextInputMethodClient? client)
{
_textEditor.SetClient(client);
}
public void SetCursorRect(Rect rect)
{
}
public void SetOptions(TextInputOptions options) =>
_textEditor.SetOptions(options);
#endregion
protected override void Dispose(bool disposing)
{
if (disposing)
{
_topLevel.StopRendering();
_topLevel.Dispose();
_topLevelImpl.Dispose();
_device.Dispose();
}
base.Dispose(disposing);
}
}

205
src/Tizen/Avalonia.Tizen/NuiAvaloniaViewTextEditable.cs

@ -0,0 +1,205 @@
using Avalonia.Input.TextInput;
using Tizen.NUI;
using Tizen.NUI.BaseComponents;
using Window = Tizen.NUI.Window;
namespace Avalonia.Tizen;
internal class NuiAvaloniaViewTextEditable
{
private readonly NuiAvaloniaView _avaloniaView;
private INuiTextInput TextInput => _multiline ? _multiLineTextInput : _singleLineTextInput;
private readonly NuiSingleLineTextInput _singleLineTextInput;
private readonly NuiMultiLineTextInput _multiLineTextInput;
private bool _updating;
private bool _keyboardPresented;
private bool _multiline;
private TextInputMethodClient? _client;
public bool IsActive => _client != null && _keyboardPresented;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public NuiAvaloniaViewTextEditable(NuiAvaloniaView avaloniaView)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
_avaloniaView = avaloniaView;
_singleLineTextInput = new NuiSingleLineTextInput
{
HeightResizePolicy = ResizePolicyType.Fixed,
WidthResizePolicy = ResizePolicyType.Fixed,
Size = new(1, 1),
Position = new Position(-1000, -1000),
FontSizeScale = 0.1f,
};
_multiLineTextInput = new NuiMultiLineTextInput
{
HeightResizePolicy = ResizePolicyType.Fixed,
WidthResizePolicy = ResizePolicyType.Fixed,
Size = new(1, 1),
Position = new Position(-1000, -1000),
FontSizeScale = 0.1f,
};
SetupTextInput(_singleLineTextInput);
SetupTextInput(_multiLineTextInput);
}
private void SetupTextInput(INuiTextInput input)
{
input.Hide();
input.GetInputMethodContext().StatusChanged += OnStatusChanged;
input.GetInputMethodContext().EventReceived += OnEventReceived;
}
private InputMethodContext.CallbackData OnEventReceived(object source, InputMethodContext.EventReceivedEventArgs e)
{
switch (e.EventData.EventName)
{
case InputMethodContext.EventType.Preedit:
_client?.SetPreeditText(e.EventData.PredictiveString);
break;
case InputMethodContext.EventType.Commit:
_client?.SetPreeditText(null);
_avaloniaView.TopLevelImpl.TextInput(e.EventData.PredictiveString);
break;
}
return new InputMethodContext.CallbackData();
}
private void OnStatusChanged(object? sender, InputMethodContext.StatusChangedEventArgs e)
{
_keyboardPresented = e.StatusChanged;
if (!_keyboardPresented)
DettachAndHide();
}
internal void SetClient(TextInputMethodClient? client)
{
if (client == null || !_keyboardPresented)
DettachAndHide();
if (client != null)
AttachAndShow(client);
}
internal void SetOptions(TextInputOptions options)
{
//TODO: This should be revert when Avalonia used Multiline property
_multiline = true;
//if (_multiline != options.Multiline)
//{
// DettachAndHide();
// _multiline = options.Multiline;
//}
TextInput.Sensitive = options.IsSensitive;
}
private void AttachAndShow(TextInputMethodClient client)
{
_updating = true;
try
{
TextInput.Text = client.SurroundingText;
TextInput.PrimaryCursorPosition = client.Selection.Start;
Window.Instance.GetDefaultLayer().Add((View)TextInput);
TextInput.Show();
TextInput.EnableSelection = true;
var inputContext = TextInput.GetInputMethodContext();
inputContext.Activate();
inputContext.ShowInputPanel();
inputContext.RestoreAfterFocusLost();
_client = client;
client.TextViewVisualChanged += OnTextViewVisualChanged;
client.SurroundingTextChanged += OnSurroundingTextChanged;
client.SelectionChanged += OnClientSelectionChanged;
TextInput.SelectWholeText();
OnClientSelectionChanged(this, EventArgs.Empty);
}
finally { _updating = false; }
}
private void OnClientSelectionChanged(object? sender, EventArgs e) => InvokeUpdate(client =>
{
if (client.Selection.End == 0 || client.Selection.Start == client.Selection.End)
TextInput.PrimaryCursorPosition = client.Selection.Start;
else
TextInput.SelectText(client.Selection.Start, client.Selection.End);
});
private void OnSurroundingTextChanged(object? sender, EventArgs e) => InvokeUpdate(client =>
{
TextInput.Text = client.SurroundingText;
TextInput.GetInputMethodContext().SetSurroundingText(client.SurroundingText);
OnClientSelectionChanged(sender, e);
});
private void OnTextViewVisualChanged(object? sender, EventArgs e) => InvokeUpdate(client =>
{
TextInput.Text = client.SurroundingText;
});
private void DettachAndHide()
{
if (IsActive)
{
_client!.TextViewVisualChanged -= OnTextViewVisualChanged;
_client!.SurroundingTextChanged -= OnSurroundingTextChanged;
_client!.SelectionChanged -= OnClientSelectionChanged;
}
if (Window.Instance.GetDefaultLayer().Children.Contains((View)TextInput))
Window.Instance.GetDefaultLayer().Remove((View)TextInput);
TextInput.Hide();
var inputContext = TextInput.GetInputMethodContext();
inputContext.Deactivate();
inputContext.HideInputPanel();
}
private void InvokeUpdate(Action<TextInputMethodClient> action)
{
if (_updating || !IsActive)
return;
_updating = true;
try
{
action(_client!);
}
finally { _updating = false; }
}
}
internal interface INuiTextInput
{
string Text { get; set; }
int PrimaryCursorPosition { get; set; }
bool EnableSelection { get; set; }
bool Sensitive { get; set; }
int SelectedTextStart { get; }
int SelectedTextEnd { get; }
void Show();
InputMethodContext GetInputMethodContext();
void Hide();
void SelectText(int selectedTextStart, int value);
void SelectWholeText();
}
public class NuiMultiLineTextInput : TextEditor, INuiTextInput
{
}
public class NuiSingleLineTextInput : TextField, INuiTextInput
{
}

76
src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs

@ -0,0 +1,76 @@
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Threading;
using Tizen.NUI;
using Tizen.NUI.BaseComponents;
namespace Avalonia.Tizen;
internal class NuiClipboardImpl : IClipboard
{
private TextEditor _textEditor;
public NuiClipboardImpl()
{
_textEditor = new TextEditor()
{
HeightResizePolicy = ResizePolicyType.Fixed,
WidthResizePolicy = ResizePolicyType.Fixed,
Position = new Position(-1000, -1000),
Size = new(1, 1)
};
Window.Instance.GetDefaultLayer().Add(_textEditor);
_textEditor.LowerToBottom();
}
public Task ClearAsync() =>
SetTextAsync("");
public Task<string?> GetTextAsync()
{
_textEditor.Show();
_textEditor.Text = "";
//The solution suggested by Samsung, The method PasteTo will execute async and need delay
TextUtils.PasteTo(_textEditor);
return Task.Run<string?>(async () =>
{
await Task.Delay(10);
return await Dispatcher.UIThread.InvokeAsync(() =>
{
_textEditor.Hide();
return _textEditor.Text;
});
});
}
public Task SetTextAsync(string? text)
{
_textEditor.Show();
_textEditor.Text = text;
//The solution suggested by Samsung, The method SelectWholeText will execute async and need delay
_textEditor.SelectWholeText();
return Task.Run(async () =>
{
await Task.Delay(10);
await Dispatcher.UIThread.InvokeAsync(() =>
{
TextUtils.CopyToClipboard(_textEditor);
_textEditor.Hide();
});
});
}
public Task<object?> GetDataAsync(string format) =>
throw new PlatformNotSupportedException();
public Task SetDataObjectAsync(IDataObject data) =>
throw new PlatformNotSupportedException();
public Task<string[]> GetFormatsAsync() =>
throw new PlatformNotSupportedException();
}

73
src/Tizen/Avalonia.Tizen/NuiGlLayerSurface.cs

@ -0,0 +1,73 @@
using Avalonia.OpenGL;
using Avalonia.OpenGL.Surfaces;
namespace Avalonia.Tizen;
internal class NuiGlLayerSurface : IGlPlatformSurface
{
private readonly NuiAvaloniaView _nuiAvaloniaView;
public NuiGlLayerSurface(NuiAvaloniaView nuiAvaloniaView)
{
_nuiAvaloniaView = nuiAvaloniaView;
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context)
{
var ctx = TizenPlatform.GlPlatform.Context;
if (ctx != context)
throw new InvalidOperationException("Platform surface is only usable with tha main context");
using (ctx.MakeCurrent())
{
return new RenderTarget(ctx, _nuiAvaloniaView);
}
}
class RenderTarget : IGlPlatformSurfaceRenderTarget
{
private readonly GlContext _ctx;
private readonly NuiAvaloniaView _nuiAvaloniaView;
public RenderTarget(GlContext ctx, NuiAvaloniaView nuiAvaloniaView)
{
_ctx = ctx;
_nuiAvaloniaView = nuiAvaloniaView;
}
public void Dispose()
{
}
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var restoreContext = _ctx.MakeCurrent();
return new RenderSession(_ctx, restoreContext, _nuiAvaloniaView);
}
}
class RenderSession : IGlPlatformSurfaceRenderingSession
{
private readonly GlContext _ctx;
private readonly IDisposable _restoreContext;
public RenderSession(GlContext ctx, IDisposable restoreContext, NuiAvaloniaView nuiAvaloniaView)
{
_ctx = ctx;
_restoreContext = restoreContext;
Size = new PixelSize((int)nuiAvaloniaView.Size.Width, (int)nuiAvaloniaView.Size.Height);
Scaling = nuiAvaloniaView.Scaling;
Context = ctx;
}
public void Dispose()
{
_ctx.GlInterface.Finish();
_restoreContext.Dispose();
}
public IGlContext Context { get; }
public PixelSize Size { get; }
public double Scaling { get; }
public bool IsYFlipped { get; }
}
}

87
src/Tizen/Avalonia.Tizen/NuiGlPlatform.cs

@ -0,0 +1,87 @@
using Avalonia.Compatibility;
using Avalonia.OpenGL;
using Avalonia.Platform;
namespace Avalonia.Tizen;
internal class NuiGlPlatform : IPlatformGraphics
{
public IPlatformGraphicsContext GetSharedContext() => Context;
public bool UsesSharedContext => true;
public IPlatformGraphicsContext CreateContext() => throw new NotSupportedException();
public GlContext Context { get; }
public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0);
public NuiGlPlatform()
{
const string library = "/usr/lib/driver/libGLESv2.so";
var libGl = NativeLibraryEx.Load(library);
if (libGl == IntPtr.Zero)
throw new OpenGlException("Unable to load " + library);
var iface = new GlInterface(GlVersion, proc =>
{
if (NativeLibraryEx.TryGetExport(libGl, proc, out var address))
return address;
return default;
});
Context = new(iface);
}
}
class GlContext : IGlContext
{
public GlContext(GlInterface glInterface)
{
GlInterface = glInterface;
}
public void Dispose()
{
}
public IDisposable MakeCurrent()
{
return this;
}
public bool IsLost => false;
public IDisposable EnsureCurrent()
{
return MakeCurrent();
}
public bool IsSharedWith(IGlContext context) => true;
public bool CanCreateSharedContext => true;
public IGlContext CreateSharedContext(IEnumerable<GlVersion> preferredVersions = null)
{
return this;
}
public GlVersion Version => new GlVersion(GlProfileType.OpenGLES, 3, 0);
public GlInterface GlInterface { get; }
public int SampleCount
{
get
{
GlInterface.GetIntegerv(GlConsts.GL_SAMPLES, out var samples);
return samples;
}
}
public int StencilSize
{
get
{
GlInterface.GetIntegerv(GlConsts.GL_STENCIL_BITS, out var stencil);
return stencil;
}
}
public object TryGetFeature(Type featureType) => null;
public IntPtr GetProcAddress(string procName)
{
return GlInterface.GetProcAddress(procName);
}
}

102
src/Tizen/Avalonia.Tizen/NuiKeyboardHandler.cs

@ -0,0 +1,102 @@
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Logging;
using Avalonia.Tizen.Platform.Input;
using Tizen.NUI;
using Key = Tizen.NUI.Key;
namespace Avalonia.Tizen;
internal class NuiKeyboardHandler
{
private const string LogKey = "TIZENHKEY";
private readonly NuiAvaloniaView _view;
public NuiKeyboardHandler(NuiAvaloniaView view)
{
_view = view;
}
public void Handle(Window.KeyEventArgs e)
{
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Key fired {text}", e.Key.KeyPressedName);
if (_view.TextEditor.IsActive)
return;
if (ShouldSendKeyEvent(e, out var keyCode))
{
var mapped = TizenKeyboardDevice.ConvertKey(keyCode);
if (mapped == Input.Key.None)
return;
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Triggering key event {text}", e.Key.KeyString);
SendKeyEvent(e, mapped);
}
else if (e.Key.State == Key.StateType.Up)
{
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Triggering text input {text}", e.Key.KeyString);
_view.TopLevelImpl.TextInput(e.Key.KeyString);
}
}
private void SendKeyEvent(Window.KeyEventArgs e, Input.Key mapped)
{
var type = GetKeyEventType(e);
var modifiers = GetModifierKey(e);
_view.TopLevelImpl.Input?.Invoke(
new RawKeyEventArgs(
KeyboardDevice.Instance!,
e.Key.Time,
_view.InputRoot,
type,
mapped,
modifiers));
}
private bool ShouldSendKeyEvent(Window.KeyEventArgs e, out global::Tizen.Uix.InputMethod.KeyCode keyCode)
{
if (string.IsNullOrEmpty(e.Key.KeyString) ||
e.Key.KeyPressedName is "Delete" or "BackSpace" ||
e.Key.IsCtrlModifier() ||
e.Key.IsAltModifier())
{
if (Enum.TryParse(e.Key.KeyPressedName, true, out keyCode) ||
Enum.TryParse($"Keypad{e.Key.KeyPressedName}", true, out keyCode))
return true;
}
keyCode = 0;
return false;
}
private RawKeyEventType GetKeyEventType(Window.KeyEventArgs ev)
{
switch (ev.Key.State)
{
case Key.StateType.Down:
return RawKeyEventType.KeyDown;
case Key.StateType.Up:
return RawKeyEventType.KeyUp;
default:
throw new ArgumentOutOfRangeException();
}
}
private RawInputModifiers GetModifierKey(Window.KeyEventArgs ev)
{
var modifiers = RawInputModifiers.None;
if (ev.Key.IsShiftModifier())
modifiers |= RawInputModifiers.Shift;
if (ev.Key.IsAltModifier())
modifiers |= RawInputModifiers.Alt;
if (ev.Key.IsCtrlModifier())
modifiers |= RawInputModifiers.Control;
return modifiers;
}
}

117
src/Tizen/Avalonia.Tizen/NuiNativeControlHostImpl.cs

@ -0,0 +1,117 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Tizen.NUI.BaseComponents;
namespace Avalonia.Tizen;
internal class NuiNativeControlHostImpl : INativeControlHostImpl
{
private readonly NuiAvaloniaView _avaloniaView;
public NuiNativeControlHostImpl(NuiAvaloniaView avaloniaView)
{
_avaloniaView = avaloniaView;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) =>
new NuiViewControlHandle(new View());
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create)
{
var parent = new NuiViewControlHandle(_avaloniaView);
NativeControlAttachment? attachment = null;
try
{
var child = create(parent);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
attachment = new NativeControlAttachment(child);
#pragma warning restore IDE0017 // Simplify object initialization
attachment.AttachedTo = this;
return attachment;
}
catch
{
attachment?.Dispose();
throw;
}
}
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) =>
new NativeControlAttachment(handle);
public bool IsCompatibleWith(IPlatformHandle handle) =>
handle.HandleDescriptor == NuiViewControlHandle.ViewDescriptor;
private class NativeControlAttachment : INativeControlHostControlTopLevelAttachment
{
private IPlatformHandle? _child;
private View? _view;
private NuiNativeControlHostImpl? _attachedTo;
public NativeControlAttachment(IPlatformHandle child)
{
_child = child;
_view = (child as NuiViewControlHandle)?.View;
}
[MemberNotNull(nameof(_view))]
private void CheckDisposed()
{
if (_view == null)
throw new ObjectDisposedException(nameof(NativeControlAttachment));
}
public void Dispose()
{
_view?.Unparent();
_child = null;
_attachedTo = null;
_view?.Dispose();
_view = null;
}
public INativeControlHostImpl? AttachedTo
{
get => _attachedTo;
set
{
CheckDisposed();
_attachedTo = (NuiNativeControlHostImpl?)value;
if (_attachedTo == null)
{
_view.Unparent();
}
else
{
_attachedTo._avaloniaView.Add(_view);
}
}
}
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NuiNativeControlHostImpl;
public void HideWithSize(Size size)
{
CheckDisposed();
if (_attachedTo == null)
return;
_view.Hide();
_view.Size = new global::Tizen.NUI.Size(MathF.Max(1f, (float)size.Width), Math.Max(1f, (float)size.Height));
}
public void ShowInBounds(Rect bounds)
{
CheckDisposed();
if (_attachedTo == null)
throw new InvalidOperationException("The control isn't currently attached to a toplevel");
_view.Size = new global::Tizen.NUI.Size(MathF.Max(1f, (float)bounds.Width), Math.Max(1f, (float)bounds.Height));
_view.Position = new global::Tizen.NUI.Position((float)bounds.X, (float)bounds.Y);
_view.Show();
}
}
}

73
src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs

@ -0,0 +1,73 @@
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Tizen.NUI;
using Window = Tizen.NUI.Window;
using Avalonia.Logging;
using Avalonia.Threading;
namespace Avalonia.Tizen;
public class NuiTizenApplication<TApp> : NUIApplication
where TApp : Application, new()
{
private const string LogKey = "TIZENAPP";
private SingleViewLifetime? _lifetime;
private class SingleViewLifetime : ISingleViewApplicationLifetime
{
public NuiAvaloniaView View { get; }
public SingleViewLifetime(NuiAvaloniaView view)
{
View = view;
}
public Control MainView
{
get => View.Content;
set => View.Content = value;
}
}
protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
protected override void OnCreate()
{
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Creating application");
base.OnCreate();
#pragma warning disable CS8601 // Possible null reference assignment.
TizenThreadingInterface.MainloopContext = SynchronizationContext.Current;
#pragma warning restore CS8601 // Possible null reference assignment.
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Setup view");
_lifetime = new SingleViewLifetime(new NuiAvaloniaView());
_lifetime.View.HeightResizePolicy = ResizePolicyType.FillToParent;
_lifetime.View.WidthResizePolicy = ResizePolicyType.FillToParent;
_lifetime.View.OnSurfaceInit += ContinueSetupApplication;
Window.Instance.RenderingBehavior = RenderingBehaviorType.Continuously;
Window.Instance.GetDefaultLayer().Add(_lifetime.View);
Window.Instance.KeyEvent += (s, e) => _lifetime?.View?.KeyboardHandler.Handle(e);
}
private void ContinueSetupApplication()
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "App builder");
var builder = AppBuilder.Configure<TApp>().UseTizen();
TizenThreadingInterface.MainloopContext.Post(_ =>
{
CustomizeAppBuilder(builder);
builder.AfterSetup(_ => _lifetime.View.Initialise());
Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Setup lifetime");
builder.SetupWithLifetime(_lifetime);
}, null);
}
}

99
src/Tizen/Avalonia.Tizen/NuiTouchHandler.cs

@ -0,0 +1,99 @@
using Avalonia.Input;
using Avalonia.Input.Raw;
using Tizen.NUI;
using static Tizen.NUI.BaseComponents.View;
namespace Avalonia.Tizen;
internal class NuiTouchHandler
{
private readonly NuiAvaloniaView _view;
public TouchDevice _device = new TouchDevice();
public NuiTouchHandler(NuiAvaloniaView view)
{
_view = view;
}
private IInputRoot InputRoot => _view.InputRoot;
private static uint _nextTouchPointId = 1;
private List<uint> _knownTouches = new();
public void Handle(TouchEventArgs e)
{
var count = e.Touch.GetPointCount();
for (var i = 0u; i < count; i++)
{
uint id;
if (_knownTouches.Count > i)
{
id = _knownTouches[(int)i];
}
else
{
unchecked
{
id = _nextTouchPointId++;
}
_knownTouches.Add(id);
}
var point = e.Touch.GetLocalPosition(i);
var state = e.Touch.GetState(i);
var timestamp = e.Touch.GetTime();
var avaloniaState = state switch
{
PointStateType.Down => RawPointerEventType.TouchBegin,
PointStateType.Up => RawPointerEventType.TouchEnd,
PointStateType.Motion => RawPointerEventType.TouchUpdate,
PointStateType.Interrupted => RawPointerEventType.TouchCancel,
_ => RawPointerEventType.TouchUpdate
};
var touchEvent = new RawTouchEventArgs(
_device,
timestamp,
InputRoot,
avaloniaState,
new Point(point.X, point.Y),
RawInputModifiers.None,
id);
_view.TopLevelImpl.Input?.Invoke(touchEvent);
if (state is PointStateType.Up or PointStateType.Interrupted)
{
_knownTouches.Remove(id);
}
}
}
public void Handle(WheelEventArgs e)
{
var mouseWheelEvent = new RawMouseWheelEventArgs(
_device,
e.Wheel.TimeStamp,
InputRoot,
new Point(e.Wheel.Point.X, e.Wheel.Point.Y),
new Vector(
e.Wheel.Direction == 1 ? e.Wheel.Z : 0,
e.Wheel.Direction == 0 ? e.Wheel.Z : 0),
GetModifierKey(e));
_view.TopLevelImpl.Input?.Invoke(mouseWheelEvent);
}
private RawInputModifiers GetModifierKey(WheelEventArgs ev)
{
var modifiers = RawInputModifiers.None;
if (ev.Wheel.IsShiftModifier())
modifiers |= RawInputModifiers.Shift;
if (ev.Wheel.IsAltModifier())
modifiers |= RawInputModifiers.Alt;
if (ev.Wheel.IsCtrlModifier())
modifiers |= RawInputModifiers.Control;
return modifiers;
}
}

39
src/Tizen/Avalonia.Tizen/NuiViewControlHandle.cs

@ -0,0 +1,39 @@
using Avalonia.Controls.Platform;
using Tizen.NUI.BaseComponents;
namespace Avalonia.Tizen;
/// <summary>
/// Tizen Nui native view handle for native view attachment
/// </summary>
public class NuiViewControlHandle : INativeControlHostDestroyableControlHandle
{
internal const string ViewDescriptor = "NuiView";
/// <summary>
/// Create handle with native view
/// </summary>
/// <param name="view">NUI Tizen native view to attach</param>
public NuiViewControlHandle(View view)
{
View = view;
}
/// <summary>
/// NUI Tizen View
/// </summary>
public View View { get; set; }
/// <summary>
/// NUI Tizen not supporting handle
/// </summary>
/// <exception cref="NotSupportedException"></exception>
public IntPtr Handle => throw new NotSupportedException();
/// <summary>
/// Return `ViewDescriptor` all the time
/// </summary>
public string? HandleDescriptor => ViewDescriptor;
/// <summary>
/// Dispose Tizen View when it call
/// </summary>
public void Destroy() => View.Dispose();
}

29
src/Tizen/Avalonia.Tizen/Platform/Consts.cs

@ -0,0 +1,29 @@
namespace Avalonia.Tizen.Platform;
internal static class Consts
{
public const int DpiX = 96;
public const int DpiY = 96;
public static readonly Vector Dpi = new Vector(DpiX, DpiY);
public const string VertexShader =
"attribute mediump vec2 aPosition;\n" +
"varying mediump vec2 vTexCoord;\n" +
"uniform highp mat4 uMvpMatrix;\n" +
"uniform mediump vec3 uSize;\n" +
"varying mediump vec2 sTexCoordRect;\n" +
"void main()\n" +
"{\n" +
" gl_Position = uMvpMatrix * vec4(aPosition * uSize.xy, 0.0, 1.0);\n" +
" vTexCoord = aPosition + vec2(0.5);\n" +
"}\n";
public const string FragmentShader =
"#extension GL_OES_EGL_image_external:require\n" +
"uniform lowp vec4 uColor;\n" +
"varying mediump vec2 vTexCoord;\n" +
"uniform samplerExternalOES sTexture;\n" +
"void main()\n" +
"{\n" +
" gl_FragColor = texture2D(sTexture, vTexCoord) * uColor;\n" +
"}\n";
}

221
src/Tizen/Avalonia.Tizen/Platform/Input/TizenKeyboardDevice.cs

@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Tizen.Uix.InputMethod;
namespace Avalonia.Tizen.Platform.Input;
internal class TizenKeyboardDevice: KeyboardDevice, IKeyboardDevice
{
private static readonly Dictionary<KeyCode, Key> KeyDic = new Dictionary<KeyCode, Key>
{
// { KeyCode.Cancel?, Key.Cancel },
{ KeyCode.BackSpace, Key.Back },
{ KeyCode.Tab, Key.Tab },
// { KeyCode.Linefeed?, Key.LineFeed },
{ KeyCode.Clear, Key.Clear },
{ KeyCode.Return, Key.Return },
{ KeyCode.Pause, Key.Pause },
{ KeyCode.CapsLock, Key.CapsLock },
//{ KeyCode.?, Key.HangulMode }
//{ KeyCode.?, Key.JunjaMode }
//{ KeyCode.?, Key.FinalMode }
//{ KeyCode.?, Key.KanjiMode }
{ KeyCode.Escape, Key.Escape },
//{ KeyCode.?, Key.ImeConvert }
//{ KeyCode.?, Key.ImeNonConvert }
//{ KeyCode.?, Key.ImeAccept }
//{ KeyCode.?, Key.ImeModeChange }
{ KeyCode.Space, Key.Space },
{ KeyCode.Page_Up, Key.Prior },
{ KeyCode.Page_Down, Key.PageDown },
{ KeyCode.End, Key.End },
{ KeyCode.Home, Key.Home },
{ KeyCode.Left, Key.Left },
{ KeyCode.Up, Key.Up },
{ KeyCode.Right, Key.Right },
{ KeyCode.Down, Key.Down },
// { KeyCode.ButtonSelect?, Key.Select },
// { KeyCode.print?, Key.Print },
//{ KeyCode.execute?, Key.Execute },
//{ KeyCode.snap?, Key.Snapshot }
{ KeyCode.Insert, Key.Insert },
{ KeyCode.Delete, Key.Delete },
{ KeyCode.Help, Key.Help },
{ KeyCode.Keypad0, Key.D0 },
{ KeyCode.Keypad1, Key.D1 },
{ KeyCode.Keypad2, Key.D2 },
{ KeyCode.Keypad3, Key.D3 },
{ KeyCode.Keypad4, Key.D4 },
{ KeyCode.Keypad5, Key.D5 },
{ KeyCode.Keypad6, Key.D6 },
{ KeyCode.Keypad7, Key.D7 },
{ KeyCode.Keypad8, Key.D8 },
{ KeyCode.Keypad9, Key.D9 },
{ KeyCode.KeypadA, Key.A },
{ KeyCode.KeypadB, Key.B },
{ KeyCode.KeypadC, Key.C },
{ KeyCode.KeypadD, Key.D },
{ KeyCode.KeypadE, Key.E },
{ KeyCode.KeypadF, Key.F },
{ KeyCode.KeypadG, Key.G },
{ KeyCode.KeypadH, Key.H },
{ KeyCode.KeypadI, Key.I },
{ KeyCode.KeypadJ, Key.J },
{ KeyCode.KeypadK, Key.K },
{ KeyCode.KeypadL, Key.L },
{ KeyCode.KeypadM, Key.M },
{ KeyCode.KeypadN, Key.N },
{ KeyCode.KeypadO, Key.O },
{ KeyCode.KeypadP, Key.P },
{ KeyCode.KeypadQ, Key.Q },
{ KeyCode.KeypadR, Key.R },
{ KeyCode.KeypadS, Key.S },
{ KeyCode.KeypadT, Key.T },
{ KeyCode.KeypadU, Key.U },
{ KeyCode.KeypadV, Key.V },
{ KeyCode.KeypadW, Key.W },
{ KeyCode.KeypadX, Key.X },
{ KeyCode.KeypadY, Key.Y },
{ KeyCode.KeypadZ, Key.Z },
{ KeyCode.Keypada, Key.A },
{ KeyCode.Keypadb, Key.B },
{ KeyCode.Keypadc, Key.C },
{ KeyCode.Keypadd, Key.D },
{ KeyCode.Keypade, Key.E },
{ KeyCode.Keypadf, Key.F },
{ KeyCode.Keypadg, Key.G },
{ KeyCode.Keypadh, Key.H },
{ KeyCode.Keypadi, Key.I },
{ KeyCode.Keypadj, Key.J },
{ KeyCode.Keypadk, Key.K },
{ KeyCode.Keypadl, Key.L },
{ KeyCode.Keypadm, Key.M },
{ KeyCode.Keypadn, Key.N },
{ KeyCode.Keypado, Key.O },
{ KeyCode.Keypadp, Key.P },
{ KeyCode.Keypadq, Key.Q },
{ KeyCode.Keypadr, Key.R },
{ KeyCode.Keypads, Key.S },
{ KeyCode.Keypadt, Key.T },
{ KeyCode.Keypadu, Key.U },
{ KeyCode.Keypadv, Key.V },
{ KeyCode.Keypadw, Key.W },
{ KeyCode.Keypadx, Key.X },
{ KeyCode.Keypady, Key.Y },
{ KeyCode.Keypadz, Key.Z },
//{ KeyCode.?, Key.LWin }
//{ KeyCode.?, Key.RWin }
//{ KeyCode.?, Key.Apps }
//{ KeyCode.Sle, Key.Sleep },
{ KeyCode.KP0, Key.NumPad0 },
{ KeyCode.KP1, Key.NumPad1 },
{ KeyCode.KP2, Key.NumPad2 },
{ KeyCode.KP3, Key.NumPad3 },
{ KeyCode.KP4, Key.NumPad4 },
{ KeyCode.KP5, Key.NumPad5 },
{ KeyCode.KP6, Key.NumPad6 },
{ KeyCode.KP7, Key.NumPad7 },
{ KeyCode.KP8, Key.NumPad8 },
{ KeyCode.KP9, Key.NumPad9 },
{ KeyCode.KPMultiply, Key.Multiply },
{ KeyCode.KPAdd, Key.Add },
{ KeyCode.KPSeparator, Key.Separator },
{ KeyCode.KPSubtract, Key.Subtract },
{ KeyCode.KPDecimal, Key.Decimal },
{ KeyCode.KPDivide, Key.Divide },
{ KeyCode.F1, Key.F1 },
{ KeyCode.F2, Key.F2 },
{ KeyCode.F3, Key.F3 },
{ KeyCode.F4, Key.F4 },
{ KeyCode.F5, Key.F5 },
{ KeyCode.F6, Key.F6 },
{ KeyCode.F7, Key.F7 },
{ KeyCode.F8, Key.F8 },
{ KeyCode.F9, Key.F9 },
{ KeyCode.F10, Key.F10 },
{ KeyCode.F11, Key.F11 },
{ KeyCode.F12, Key.F12 },
//{ KeyCode.f13, Key.F13 },
//{ KeyCode.F14, Key.F14 },
//{ KeyCode.L5, Key.F15 },
//{ KeyCode.F16, Key.F16 },
//{ KeyCode.F17, Key.F17 },
//{ KeyCode.L8, Key.F18 },
//{ KeyCode.L9, Key.F19 },
//{ KeyCode.L10, Key.F20 },
//{ KeyCode.R1, Key.F21 },
//{ KeyCode.R2, Key.F22 },
//{ KeyCode.F23, Key.F23 },
//{ KeyCode.R4, Key.F24 },
{ KeyCode.Num_Lock, Key.NumLock },
{ KeyCode.ScrollLock, Key.Scroll },
{ KeyCode.ShiftL, Key.LeftShift },
{ KeyCode.ShiftR, Key.RightShift },
{ KeyCode.ControlL, Key.LeftCtrl },
{ KeyCode.ControlR, Key.RightCtrl },
{ KeyCode.AltL, Key.LeftAlt },
{ KeyCode.AltR, Key.RightAlt },
//{ KeyCode.?, Key.BrowserBack }
//{ KeyCode.?, Key.BrowserForward }
//{ KeyCode.?, Key.BrowserRefresh }
//{ KeyCode.?, Key.BrowserStop }
//{ KeyCode.?, Key.BrowserSearch }
//{ KeyCode.?, Key.BrowserFavorites }
//{ KeyCode.?, Key.BrowserHome }
//{ KeyCode.?, Key.VolumeMute }
//{ KeyCode.VolumeDown, Key.VolumeDown },
//{ KeyCode.VolumeUp, Key.VolumeUp },
//{ KeyCode.MediaNext, Key.MediaNextTrack },
//{ KeyCode.MediaPrevious, Key.MediaPreviousTrack },
//{ KeyCode.MediaStop, Key.MediaStop },
//{ KeyCode.MediaPlayPause, Key.MediaPlayPause },
//{ KeyCode.?, Key.LaunchMail }
//{ KeyCode.?, Key.SelectMedia }
//{ KeyCode.?, Key.LaunchApplication1 }
//{ KeyCode.?, Key.LaunchApplication2 }
{ KeyCode.Semicolon, Key.OemSemicolon },
{ KeyCode.Plus, Key.OemPlus },
{ KeyCode.Comma, Key.OemComma },
{ KeyCode.Minus, Key.OemMinus },
{ KeyCode.Period, Key.OemPeriod },
//{ KeyCode.?, Key.Oem2 }
{ KeyCode.Grave, Key.OemTilde },
//{ KeyCode.?, Key.AbntC1 }
//{ KeyCode.?, Key.AbntC2 }
//{ KeyCode.?, Key.OemPipe }
{ KeyCode.Apostrophe, Key.OemQuotes },
{ KeyCode.Slash, Key.OemQuestion },
{ KeyCode.BraceLeft, Key.OemOpenBrackets },
{ KeyCode.BracketRight, Key.OemCloseBrackets },
//{ KeyCode.?, Key.Oem7 }
//{ KeyCode.?, Key.Oem8 }
//{ KeyCode.?, Key.Oem102 }
//{ KeyCode.?, Key.ImeProcessed }
//{ KeyCode.?, Key.System }
//{ KeyCode.?, Key.OemAttn }
//{ KeyCode.?, Key.OemFinish }
//{ KeyCode.?, Key.DbeHiragana }
//{ KeyCode.?, Key.OemAuto }
//{ KeyCode.?, Key.DbeDbcsChar }
//{ KeyCode.?, Key.OemBackTab }
//{ KeyCode.?, Key.Attn }
//{ KeyCode.?, Key.DbeEnterWordRegisterMode }
//{ KeyCode.?, Key.DbeEnterImeConfigureMode }
//{ KeyCode.?, Key.EraseEof }
//{ KeyCode.MediaPlay, Key.Play },
//{ KeyCode.?, Key.Zoom }
//{ KeyCode.?, Key.NoName }
//{ KeyCode.?, Key.DbeEnterDialogConversionMode }
//{ KeyCode.?, Key.OemClear }
//{ KeyCode.?, Key.DeadCharProcessed }
{ KeyCode.Backslash, Key.OemBackslash }
};
internal static Key ConvertKey(KeyCode key)
{
return KeyDic.TryGetValue(key, out var result) ? result : Key.None;
}
}

20
src/Tizen/Avalonia.Tizen/Platform/Interop/Strucs.cs

@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Avalonia.Tizen.Platform.Interop;
struct TexturedQuadVertex
{
public Vec2 position;
};
[StructLayout(LayoutKind.Sequential)]
struct Vec2
{
float x;
float y;
public Vec2(float xIn, float yIn)
{
x = xIn;
y = yIn;
}
}

110
src/Tizen/Avalonia.Tizen/Platform/Permissions.cs

@ -0,0 +1,110 @@
using System.Security;
using System.Security.Permissions;
using Tizen.Applications;
using Tizen.Security;
namespace Avalonia.Tizen.Platform;
internal class Permissions
{
public record Privilege (string Path, bool IsRuntime);
public static readonly Privilege InternetPrivilege = new("http://tizen.org/privilege/internet", false);
public static readonly Privilege NetworkPrivilege = new("http://tizen.org/privilege/network.get", false);
public static readonly Privilege CameraPrivilege = new("http://tizen.org/privilege/camera", false);
public static readonly Privilege ContactReadPrivilege = new("http://tizen.org/privilege/contact.read", true);
public static readonly Privilege ContactWritePrivilege = new("http://tizen.org/privilege/contact.write", true);
public static readonly Privilege LedPrivilege = new("http://tizen.org/privilege/led", false);
public static readonly Privilege AppManagerLaunchPrivilege = new("http://tizen.org/privilege/appmanager.launch", false);
public static readonly Privilege LocationPrivilege = new("http://tizen.org/privilege/location", true);
public static readonly Privilege MapServicePrivilege = new("http://tizen.org/privilege/mapservice", false);
public static readonly Privilege MediaStoragePrivilege = new("http://tizen.org/privilege/mediastorage", true);
public static readonly Privilege RecorderPrivilege = new("http://tizen.org/privilege/recorder", false);
public static readonly Privilege HapticPrivilege = new("http://tizen.org/privilege/haptic", false);
public static readonly Privilege[] NetworkPrivileges = { InternetPrivilege, NetworkPrivilege };
public static readonly Privilege[] MapsPrivileges = { InternetPrivilege, MapServicePrivilege, NetworkPrivilege };
public static Package CurrentPackage
{
get
{
var packageId = global::Tizen.Applications.Application.Current.ApplicationInfo.PackageId;
return PackageManager.GetPackage(packageId);
}
}
public static bool IsPrivilegeDeclared(string tizenPrivilege)
{
var tizenPrivileges = tizenPrivilege;
if (tizenPrivileges == null || !tizenPrivileges.Any())
return false;
var package = CurrentPackage;
if (!package.Privileges.Contains(tizenPrivilege))
return false;
return true;
}
public static void EnsureDeclared(params Privilege[] requiredPrivileges)
{
if (requiredPrivileges?.Any() != true)
return;
foreach (var (tizenPrivilege, isRuntime) in requiredPrivileges)
{
if (!IsPrivilegeDeclared(tizenPrivilege))
throw new SecurityException($"You need to declare the privilege: `{tizenPrivilege}` in your tizen-manifest.xml");
}
}
public static Task<bool> CheckPrivilegeAsync(params Privilege[] requiredPrivileges) => CheckPrivilegeAsync(requiredPrivileges, false);
public static Task<bool> RequestPrivilegeAsync(params Privilege[] requiredPrivileges) => CheckPrivilegeAsync(requiredPrivileges, true);
private static async Task<bool> CheckPrivilegeAsync(Privilege[] requiredPrivileges, bool ask)
{
var ret = global::Tizen.System.Information.TryGetValue("http://tizen.org/feature/profile", out string profile);
if (!ret || (ret && (!profile.Equals("mobile") || !profile.Equals("wearable"))))
{
return true;
}
if (requiredPrivileges == null || !requiredPrivileges.Any())
return true;
EnsureDeclared();
var tizenPrivileges = requiredPrivileges.Where(p => p.IsRuntime);
foreach (var (tizenPrivilege, isRuntime) in tizenPrivileges)
{
var checkResult = PrivacyPrivilegeManager.CheckPermission(tizenPrivilege);
if (checkResult == CheckResult.Ask)
{
if (ask)
{
var tcs = new TaskCompletionSource<bool>();
PrivacyPrivilegeManager.GetResponseContext(tizenPrivilege)
.TryGetTarget(out var context);
void OnResponseFetched(object sender, RequestResponseEventArgs e)
{
tcs.TrySetResult(e.result == RequestResult.AllowForever);
}
context.ResponseFetched += OnResponseFetched;
PrivacyPrivilegeManager.RequestPermission(tizenPrivilege);
var result = await tcs.Task;
context.ResponseFetched -= OnResponseFetched;
if (result)
continue;
}
return false;
}
else if (checkResult == CheckResult.Deny)
{
return false;
}
}
return true;
}
}

10
src/Tizen/Avalonia.Tizen/Platform/TizenPlatformSettings.cs

@ -0,0 +1,10 @@
using System;
using Avalonia.Platform;
using Tizen.NUI;
namespace Avalonia.Tizen.Platform;
internal class TizenPlatformSettings : DefaultPlatformSettings
{
}

65
src/Tizen/Avalonia.Tizen/Stubs.cs

@ -0,0 +1,65 @@
using Avalonia.Input;
using Avalonia.Platform;
namespace Avalonia.Tizen;
internal class WindowingPlatformStub : IWindowingPlatform
{
public IWindowImpl CreateWindow() => throw new NotSupportedException();
public IWindowImpl CreateEmbeddableWindow() => throw new NotSupportedException();
public ITrayIconImpl CreateTrayIcon() => null;
}
internal class PlatformIconLoaderStub : IPlatformIconLoader
{
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
{
using (var stream = new MemoryStream())
{
bitmap.Save(stream);
return LoadIcon(stream);
}
}
public IWindowIconImpl LoadIcon(Stream stream)
{
var ms = new MemoryStream();
stream.CopyTo(ms);
return new IconStub(ms);
}
public IWindowIconImpl LoadIcon(string fileName)
{
using (var file = File.Open(fileName, FileMode.Open))
return LoadIcon(file);
}
}
internal class IconStub : IWindowIconImpl
{
private readonly MemoryStream _ms;
public IconStub(MemoryStream stream)
{
_ms = stream;
}
public void Save(Stream outputStream)
{
_ms.Position = 0;
_ms.CopyTo(outputStream);
}
}
internal class CursorFactoryStub : ICursorFactory
{
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) => new CursorImplStub();
ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) => new CursorImplStub();
private class CursorImplStub : ICursorImpl
{
public void Dispose() { }
}
}

20
src/Tizen/Avalonia.Tizen/TizenApplicationExtensions.cs

@ -0,0 +1,20 @@
namespace Avalonia.Tizen;
/// <summary>
/// Extension to setup app builder with tizen backend
/// </summary>
public static class TizenApplicationExtensions
{
/// <summary>
/// Use tizen builder to setup tizen sub system
/// </summary>
/// <param name="builder">Avalonia App Builder</param>
/// <returns>Return same builder</returns>
public static AppBuilder UseTizen(this AppBuilder builder)
{
return builder
.UseWindowingSubsystem(TizenPlatform.Initialize, "Tizen")
.UseStandardRuntimePlatformSubsystem()
.UseSkia();
}
}

35
src/Tizen/Avalonia.Tizen/TizenPlatform.cs

@ -0,0 +1,35 @@
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Tizen.Platform.Input;
using Avalonia.Tizen.Platform;
using Avalonia.Threading;
namespace Avalonia.Tizen;
internal class TizenPlatform
{
public static readonly TizenPlatform Instance = new();
internal static NuiGlPlatform GlPlatform { get; set; }
internal static Compositor Compositor { get; private set; }
internal static TizenThreadingInterface ThreadingInterface { get; private set; } = new();
public static void Initialize()
{
AvaloniaLocator.CurrentMutable
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IKeyboardDevice>().ToSingleton<TizenKeyboardDevice>()
.Bind<IPlatformSettings>().ToSingleton<TizenPlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(ThreadingInterface)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoaderStub>()
.Bind<IRenderTimer>().ToConstant(new TizenRenderTimer())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IPlatformGraphics>().ToConstant(GlPlatform = new NuiGlPlatform());
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}
}

44
src/Tizen/Avalonia.Tizen/TizenRenderTimer.cs

@ -0,0 +1,44 @@
using System.Diagnostics;
using Avalonia.Rendering;
using Tizen.Account.AccountManager;
using Tizen.System;
namespace Avalonia.Tizen;
internal class TizenRenderTimer : IRenderTimer
{
private readonly Stopwatch _st = Stopwatch.StartNew();
private Timer _timer;
public bool RunsInBackground => true;
public event Action<TimeSpan>? Tick;
public event Action? RenderTick;
public TizenRenderTimer()
{
_timer = new Timer(TimerTick, null, 16, 16);
Display.StateChanged += Display_StateChanged;
}
private void TimerTick(object? state)
{
RenderTick?.Invoke();
}
private void Display_StateChanged(object? sender, DisplayStateChangedEventArgs e)
{
if (e.State == DisplayState.Off)
{
_timer.Change(Timeout.Infinite, Timeout.Infinite);
}
else
{
_timer.Change(16, 16);
}
}
internal void ManualTick()
{
Tick?.Invoke(_st.Elapsed);
}
}

152
src/Tizen/Avalonia.Tizen/TizenStorageProvider.cs

@ -0,0 +1,152 @@
using System.Security;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Tizen.Platform;
using Tizen.Applications;
namespace Avalonia.Tizen;
internal class TizenStorageProvider : IStorageProvider
{
public bool CanOpen => true;
public bool CanSave => false;
public bool CanPickFolder => false;
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
{
return Task.FromException<IStorageBookmarkFile?>(
new PlatformNotSupportedException("Bookmark is not supported by Tizen"));
}
private static async Task CheckPermission()
{
Permissions.EnsureDeclared(Permissions.AppManagerLaunchPrivilege);
if (await Permissions.RequestPrivilegeAsync(Permissions.MediaStoragePrivilege) == false)
{
throw new SecurityException("Application doesn't have storage permission.");
}
}
public async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
await CheckPermission();
var tcs = new TaskCompletionSource<IReadOnlyList<IStorageFile>>();
#pragma warning disable CS8603 // Possible null reference return.
var fileType = options.FileTypeFilter?
.Where(w => w.MimeTypes != null)
.SelectMany(s => s.MimeTypes);
#pragma warning restore CS8603 // Possible null reference return.
var appControl = new AppControl
{
Operation = AppControlOperations.Pick,
Mime = fileType?.Any() == true
? fileType.Aggregate((o, n) => o + ";" + n)
: "*/*"
};
appControl.ExtraData.Add(AppControlData.SectionMode, options.AllowMultiple ? "multiple" : "single");
if (options.SuggestedStartLocation?.Path is { } startupPath)
appControl.ExtraData.Add(AppControlData.Path, startupPath.ToString());
appControl.LaunchMode = AppControlLaunchMode.Single;
var fileResults = new List<IStorageFile>();
AppControl.SendLaunchRequest(appControl, (_, reply, result) =>
{
if (result == AppControlReplyResult.Succeeded)
{
if (reply.ExtraData.Count() > 0)
{
var selectedFiles = reply.ExtraData.Get<IEnumerable<string>>(AppControlData.Selected).ToList();
fileResults.AddRange(selectedFiles.Select(f => new BclStorageFile(new(f))));
}
}
tcs.TrySetResult(fileResults);
});
return await tcs.Task;
}
public Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
return Task.FromException<IReadOnlyList<IStorageFolder>>(
new PlatformNotSupportedException("Open folder is not supported by Tizen"));
}
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{
return Task.FromException<IStorageBookmarkFolder?>(
new PlatformNotSupportedException("Open folder is not supported by Tize"));
}
public Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromException<IStorageFile?>(
new PlatformNotSupportedException("Save file picker is not supported by Tizen"));
}
public async Task<IStorageFile?> TryGetFileFromPathAsync(Uri filePath)
{
await CheckPermission();
if (filePath is not { IsAbsoluteUri: true, Scheme: "file" })
{
throw new ArgumentException("File path is expected to be an absolute link with \"file\" scheme.");
}
var path = Path.Combine(global::Tizen.Applications.Application.Current.DirectoryInfo.Resource, filePath.AbsolutePath);
var file = new FileInfo(path);
if (!file.Exists)
{
return null;
}
return new BclStorageFile(file);
}
public async Task<IStorageFolder?> TryGetFolderFromPathAsync(Uri folderPath)
{
if (folderPath is null)
{
throw new ArgumentNullException(nameof(folderPath));
}
await CheckPermission();
if (folderPath is not { IsAbsoluteUri: true, Scheme: "file" })
{
throw new ArgumentException("File path is expected to be an absolute link with \"file\" scheme.");
}
var path = Path.Combine(global::Tizen.Applications.Application.Current.DirectoryInfo.Resource, folderPath.AbsolutePath);
var directory = new System.IO.DirectoryInfo(path);
if (!directory.Exists)
return null;
return new BclStorageFolder(directory);
}
public Task<IStorageFolder?> TryGetWellKnownFolderAsync(WellKnownFolder wellKnownFolder)
{
var folder = wellKnownFolder switch
{
WellKnownFolder.Desktop => null,
WellKnownFolder.Documents => global::Tizen.Applications.Application.Current.DirectoryInfo.Data,
WellKnownFolder.Downloads => global::Tizen.Applications.Application.Current.DirectoryInfo.SharedData,
WellKnownFolder.Music => null,
WellKnownFolder.Pictures => null,
WellKnownFolder.Videos => null,
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null),
};
if (folder == null)
return Task.FromResult<IStorageFolder?>(null);
var storageFolder = new BclStorageFolder(new System.IO.DirectoryInfo(folder));
return Task.FromResult<IStorageFolder?>(storageFolder);
}
}

50
src/Tizen/Avalonia.Tizen/TizenThreadingInterface.cs

@ -0,0 +1,50 @@
using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.Tizen;
internal class TizenThreadingInterface : IPlatformThreadingInterface
{
internal event Action? TickExecuted;
private bool _signaled;
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
internal static SynchronizationContext MainloopContext { get; set; }
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return new Timer(_ =>
{
EnsureInvokeOnMainThread(tick);
}, null, interval, interval);
}
private void EnsureInvokeOnMainThread(Action action)
{
if (SynchronizationContext.Current != null)
action();
else
MainloopContext.Post(static arg => ((Action)arg!).Invoke(), action);
}
public void Signal(DispatcherPriority prio)
{
if (_signaled)
return;
_signaled = true;
var interval = TimeSpan.FromMilliseconds(1);
IDisposable? disp = null;
disp = new Timer(_ =>
{
_signaled = false;
disp?.Dispose();
EnsureInvokeOnMainThread(() => Signaled?.Invoke(prio));
}, null, interval, interval);
}
public bool CurrentThreadIsLoopThread => SynchronizationContext.Current != null;
public event Action<DispatcherPriority?>? Signaled;
}

117
src/Tizen/Avalonia.Tizen/TopLevelImpl.cs

@ -0,0 +1,117 @@
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition;
using Avalonia.Tizen.Platform.Input;
namespace Avalonia.Tizen;
internal class TopLevelImpl : ITopLevelImpl
{
private readonly ITizenView _view;
private readonly NuiClipboardImpl _clipboard;
private readonly IStorageProvider _storageProvider;
public TopLevelImpl(ITizenView view, IEnumerable<object> surfaces)
{
_view = view;
Surfaces = surfaces;
_storageProvider = new TizenStorageProvider();
_clipboard = new NuiClipboardImpl();
}
public Size ClientSize => _view.ClientSize;
public Size? FrameSize => null;
public double RenderScaling => 1;
public IEnumerable<object> Surfaces { get; set; }
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }
public Action<Size, WindowResizeReason>? Resized { get; set; }
public Action<double>? ScalingChanged { get; set; }
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public Compositor Compositor => TizenPlatform.Compositor;
public Action? Closed { get; set; }
public Action? LostFocus { get; set; }
public WindowTransparencyLevel TransparencyLevel { get; set; }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels();
public IPopupImpl? CreatePopup()
{
return null;
}
public void Dispose()
{
//
}
public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y);
public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y);
public void SetCursor(ICursorImpl? cursor)
{
//
}
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
//
}
public void SetInputRoot(IInputRoot inputRoot)
{
_view.InputRoot = inputRoot;
}
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels)
{
//
}
public object? TryGetFeature(Type featureType)
{
if (featureType == typeof(IStorageProvider))
{
return _storageProvider;
}
if (featureType == typeof(ITextInputMethodImpl))
{
return _view;
}
if (featureType == typeof(INativeControlHostImpl))
{
return _view.NativeControlHost;
}
if (featureType == typeof(IClipboard))
{
return _clipboard;
}
return null;
}
internal void TextInput(string text)
{
if (Input == null) return;
var args = new RawTextInputEventArgs(TizenKeyboardDevice.Instance, (ulong)DateTime.Now.Ticks, _view.InputRoot, text);
Input(args);
}
}
Loading…
Cancel
Save