Browse Source
* 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
committed by
GitHub
33 changed files with 2112 additions and 2 deletions
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 16 KiB |
@ -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> |
|||
@ -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> |
|||
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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"; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using System; |
|||
using Avalonia.Platform; |
|||
using Tizen.NUI; |
|||
|
|||
namespace Avalonia.Tizen.Platform; |
|||
|
|||
internal class TizenPlatformSettings : DefaultPlatformSettings |
|||
{ |
|||
|
|||
} |
|||
@ -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() { } |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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>()); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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…
Reference in new issue