Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure: - Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer - Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit) - Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget - Update LinuxApplication with GTK mode, theme handling, and icon support - Fix duplicate LinuxDispatcher in LinuxMauiContext Handlers: - Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView - Add FlexLayoutHandler and GestureManager - Update multiple handlers with ToViewHandler fix and missing mappers - Add MauiHandlerExtensions with ToViewHandler extension method Views: - Add SkiaContextMenu with hover, keyboard, and dark theme support - Add LinuxDialogService with context menu management - Add SkiaFlexLayout for flex container support - Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer - Update SkiaWebView with SetMainWindow, ProcessGtkEvents - Update SkiaImage with LoadFromBitmap method Services: - Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService - Add GtkHostService, GtkContextMenuService, MauiIconGenerator Window: - Add CursorType enum and GtkHostWindow - Update X11Window with SetIcon, SetCursor methods Build: SUCCESS (0 errors) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Maui.Animations;
|
||||
using Microsoft.Maui.Dispatching;
|
||||
using Microsoft.Maui.Platform;
|
||||
using Microsoft.Maui.Platform.Linux.Dispatching;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
@@ -82,125 +83,6 @@ public class ScopedLinuxMauiContext : IMauiContext
|
||||
public IMauiHandlersFactory Handlers => _parent.Handlers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linux dispatcher for UI thread operations.
|
||||
/// </summary>
|
||||
internal class LinuxDispatcher : IDispatcher
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly Queue<Action> _queue = new();
|
||||
private bool _isDispatching;
|
||||
|
||||
public bool IsDispatchRequired => false; // Linux uses single-threaded event loop
|
||||
|
||||
public IDispatcherTimer CreateTimer()
|
||||
{
|
||||
return new LinuxDispatcherTimer();
|
||||
}
|
||||
|
||||
public bool Dispatch(Action action)
|
||||
{
|
||||
if (action == null)
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_queue.Enqueue(action);
|
||||
}
|
||||
|
||||
ProcessQueue();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DispatchDelayed(TimeSpan delay, Action action)
|
||||
{
|
||||
if (action == null)
|
||||
return false;
|
||||
|
||||
Task.Delay(delay).ContinueWith(_ => Dispatch(action));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ProcessQueue()
|
||||
{
|
||||
if (_isDispatching)
|
||||
return;
|
||||
|
||||
_isDispatching = true;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
Action? action;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_queue.Count == 0)
|
||||
break;
|
||||
action = _queue.Dequeue();
|
||||
}
|
||||
action?.Invoke();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isDispatching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linux dispatcher timer implementation.
|
||||
/// </summary>
|
||||
internal class LinuxDispatcherTimer : IDispatcherTimer
|
||||
{
|
||||
private Timer? _timer;
|
||||
private TimeSpan _interval = TimeSpan.FromMilliseconds(16); // ~60fps default
|
||||
private bool _isRunning;
|
||||
private bool _isRepeating = true;
|
||||
|
||||
public TimeSpan Interval
|
||||
{
|
||||
get => _interval;
|
||||
set => _interval = value;
|
||||
}
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
|
||||
public bool IsRepeating
|
||||
{
|
||||
get => _isRepeating;
|
||||
set => _isRepeating = value;
|
||||
}
|
||||
|
||||
public event EventHandler? Tick;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_isRunning)
|
||||
return;
|
||||
|
||||
_isRunning = true;
|
||||
_timer = new Timer(OnTimerCallback, null, _interval, _isRepeating ? _interval : Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
private void OnTimerCallback(object? state)
|
||||
{
|
||||
Tick?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (!_isRepeating)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Linux animation manager.
|
||||
/// </summary>
|
||||
|
||||
@@ -476,22 +476,3 @@ public class LinuxViewRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for MAUI handler creation.
|
||||
/// </summary>
|
||||
public static class MauiHandlerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a handler for the view and returns it.
|
||||
/// </summary>
|
||||
public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext)
|
||||
{
|
||||
var handler = mauiContext.Handlers.GetHandler(element.GetType());
|
||||
if (handler != null)
|
||||
{
|
||||
handler.SetMauiContext(mauiContext);
|
||||
handler.SetVirtualView(element);
|
||||
}
|
||||
return handler!;
|
||||
}
|
||||
}
|
||||
|
||||
123
Hosting/MauiHandlerExtensions.cs
Normal file
123
Hosting/MauiHandlerExtensions.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Hosting;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for creating MAUI handlers on Linux.
|
||||
/// Maps MAUI types to Linux-specific handlers with fallback to MAUI defaults.
|
||||
/// </summary>
|
||||
public static class MauiHandlerExtensions
|
||||
{
|
||||
private static readonly Dictionary<Type, Func<IElementHandler>> LinuxHandlerMap = new Dictionary<Type, Func<IElementHandler>>
|
||||
{
|
||||
[typeof(Button)] = () => new TextButtonHandler(),
|
||||
[typeof(Label)] = () => new LabelHandler(),
|
||||
[typeof(Entry)] = () => new EntryHandler(),
|
||||
[typeof(Editor)] = () => new EditorHandler(),
|
||||
[typeof(CheckBox)] = () => new CheckBoxHandler(),
|
||||
[typeof(Switch)] = () => new SwitchHandler(),
|
||||
[typeof(Slider)] = () => new SliderHandler(),
|
||||
[typeof(Stepper)] = () => new StepperHandler(),
|
||||
[typeof(ProgressBar)] = () => new ProgressBarHandler(),
|
||||
[typeof(ActivityIndicator)] = () => new ActivityIndicatorHandler(),
|
||||
[typeof(Picker)] = () => new PickerHandler(),
|
||||
[typeof(DatePicker)] = () => new DatePickerHandler(),
|
||||
[typeof(TimePicker)] = () => new TimePickerHandler(),
|
||||
[typeof(SearchBar)] = () => new SearchBarHandler(),
|
||||
[typeof(RadioButton)] = () => new RadioButtonHandler(),
|
||||
[typeof(WebView)] = () => new WebViewHandler(),
|
||||
[typeof(Image)] = () => new ImageHandler(),
|
||||
[typeof(ImageButton)] = () => new ImageButtonHandler(),
|
||||
[typeof(BoxView)] = () => new BoxViewHandler(),
|
||||
[typeof(Frame)] = () => new FrameHandler(),
|
||||
[typeof(Border)] = () => new BorderHandler(),
|
||||
[typeof(ContentView)] = () => new BorderHandler(),
|
||||
[typeof(ScrollView)] = () => new ScrollViewHandler(),
|
||||
[typeof(Grid)] = () => new GridHandler(),
|
||||
[typeof(StackLayout)] = () => new StackLayoutHandler(),
|
||||
[typeof(VerticalStackLayout)] = () => new StackLayoutHandler(),
|
||||
[typeof(HorizontalStackLayout)] = () => new StackLayoutHandler(),
|
||||
[typeof(AbsoluteLayout)] = () => new LayoutHandler(),
|
||||
[typeof(FlexLayout)] = () => new FlexLayoutHandler(),
|
||||
[typeof(CollectionView)] = () => new CollectionViewHandler(),
|
||||
[typeof(ListView)] = () => new CollectionViewHandler(),
|
||||
[typeof(Page)] = () => new PageHandler(),
|
||||
[typeof(ContentPage)] = () => new ContentPageHandler(),
|
||||
[typeof(NavigationPage)] = () => new NavigationPageHandler(),
|
||||
[typeof(Shell)] = () => new ShellHandler(),
|
||||
[typeof(FlyoutPage)] = () => new FlyoutPageHandler(),
|
||||
[typeof(TabbedPage)] = () => new TabbedPageHandler(),
|
||||
[typeof(Application)] = () => new ApplicationHandler(),
|
||||
[typeof(Microsoft.Maui.Controls.Window)] = () => new WindowHandler(),
|
||||
[typeof(GraphicsView)] = () => new GraphicsViewHandler()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates an element handler for the given element.
|
||||
/// </summary>
|
||||
public static IElementHandler ToHandler(this IElement element, IMauiContext mauiContext)
|
||||
{
|
||||
return CreateHandler(element, mauiContext)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a view handler for the given view.
|
||||
/// </summary>
|
||||
public static IViewHandler? ToViewHandler(this IView view, IMauiContext mauiContext)
|
||||
{
|
||||
var handler = CreateHandler((IElement)view, mauiContext);
|
||||
return handler as IViewHandler;
|
||||
}
|
||||
|
||||
private static IElementHandler? CreateHandler(IElement element, IMauiContext mauiContext)
|
||||
{
|
||||
Type type = element.GetType();
|
||||
IElementHandler? handler = null;
|
||||
|
||||
// First, try exact type match
|
||||
if (LinuxHandlerMap.TryGetValue(type, out Func<IElementHandler>? factory))
|
||||
{
|
||||
handler = factory();
|
||||
Console.WriteLine($"[ToHandler] Using Linux handler for {type.Name}: {handler.GetType().Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to find a base type match
|
||||
Type? bestMatch = null;
|
||||
Func<IElementHandler>? bestFactory = null;
|
||||
|
||||
foreach (var kvp in LinuxHandlerMap)
|
||||
{
|
||||
if (kvp.Key.IsAssignableFrom(type) && (bestMatch == null || bestMatch.IsAssignableFrom(kvp.Key)))
|
||||
{
|
||||
bestMatch = kvp.Key;
|
||||
bestFactory = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestFactory != null)
|
||||
{
|
||||
handler = bestFactory();
|
||||
Console.WriteLine($"[ToHandler] Using Linux handler (via base {bestMatch!.Name}) for {type.Name}: {handler.GetType().Name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to MAUI's default handler
|
||||
if (handler == null)
|
||||
{
|
||||
handler = mauiContext.Handlers.GetHandler(type);
|
||||
Console.WriteLine($"[ToHandler] Using MAUI handler for {type.Name}: {handler?.GetType().Name ?? "null"}");
|
||||
}
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
handler.SetMauiContext(mauiContext);
|
||||
handler.SetVirtualView(element);
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user