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:
142
Services/AppInfoService.cs
Normal file
142
Services/AppInfoService.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
public class AppInfoService : IAppInfo
|
||||
{
|
||||
private static readonly Lazy<AppInfoService> _instance = new Lazy<AppInfoService>(() => new AppInfoService());
|
||||
|
||||
private readonly Assembly _entryAssembly;
|
||||
|
||||
private readonly string _packageName;
|
||||
|
||||
private readonly string _name;
|
||||
|
||||
private readonly string _versionString;
|
||||
|
||||
private readonly Version _version;
|
||||
|
||||
private readonly string _buildString;
|
||||
|
||||
public static AppInfoService Instance => _instance.Value;
|
||||
|
||||
public string PackageName => _packageName;
|
||||
|
||||
public string Name => _name;
|
||||
|
||||
public string VersionString => _versionString;
|
||||
|
||||
public Version Version => _version;
|
||||
|
||||
public string BuildString => _buildString;
|
||||
|
||||
public LayoutDirection RequestedLayoutDirection => LayoutDirection.LeftToRight;
|
||||
|
||||
public AppTheme RequestedTheme
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
string environmentVariable = Environment.GetEnvironmentVariable("GTK_THEME");
|
||||
if (!string.IsNullOrEmpty(environmentVariable) && environmentVariable.Contains("dark", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return AppTheme.Dark;
|
||||
}
|
||||
if (GetGnomeColorScheme().Contains("dark", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return AppTheme.Dark;
|
||||
}
|
||||
return AppTheme.Light;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return AppTheme.Light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AppPackagingModel PackagingModel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("FLATPAK_ID") != null)
|
||||
{
|
||||
return AppPackagingModel.Packaged;
|
||||
}
|
||||
if (Environment.GetEnvironmentVariable("SNAP") != null)
|
||||
{
|
||||
return AppPackagingModel.Packaged;
|
||||
}
|
||||
if (Environment.GetEnvironmentVariable("APPIMAGE") != null)
|
||||
{
|
||||
return AppPackagingModel.Packaged;
|
||||
}
|
||||
return AppPackagingModel.Unpackaged;
|
||||
}
|
||||
}
|
||||
|
||||
public AppInfoService()
|
||||
{
|
||||
_entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
||||
_packageName = _entryAssembly.GetName().Name ?? "Unknown";
|
||||
_name = _entryAssembly.GetCustomAttribute<AssemblyTitleAttribute>()?.Title ?? _packageName;
|
||||
_versionString = (_version = _entryAssembly.GetName().Version ?? new Version(1, 0)).ToString();
|
||||
_buildString = _entryAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? _versionString;
|
||||
}
|
||||
|
||||
private string GetGnomeColorScheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
using Process? process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "gsettings",
|
||||
Arguments = "get org.gnome.desktop.interface color-scheme",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
if (process != null)
|
||||
{
|
||||
string text = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit(1000);
|
||||
return text.Trim().Trim('\'');
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void ShowSettingsUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "gnome-control-center",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "xdg-open",
|
||||
Arguments = "x-settings:",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
Services/ConnectivityService.cs
Normal file
170
Services/ConnectivityService.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.Maui.Networking;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
public class ConnectivityService : IConnectivity, IDisposable
|
||||
{
|
||||
private static readonly Lazy<ConnectivityService> _instance = new Lazy<ConnectivityService>(() => new ConnectivityService());
|
||||
|
||||
private NetworkAccess _networkAccess;
|
||||
|
||||
private IEnumerable<ConnectionProfile> _connectionProfiles;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public static ConnectivityService Instance => _instance.Value;
|
||||
|
||||
public NetworkAccess NetworkAccess
|
||||
{
|
||||
get
|
||||
{
|
||||
RefreshConnectivity();
|
||||
return _networkAccess;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ConnectionProfile> ConnectionProfiles
|
||||
{
|
||||
get
|
||||
{
|
||||
RefreshConnectivity();
|
||||
return _connectionProfiles;
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<ConnectivityChangedEventArgs>? ConnectivityChanged;
|
||||
|
||||
public ConnectivityService()
|
||||
{
|
||||
_connectionProfiles = new List<ConnectionProfile>();
|
||||
RefreshConnectivity();
|
||||
NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged;
|
||||
NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
|
||||
}
|
||||
|
||||
private void RefreshConnectivity()
|
||||
{
|
||||
try
|
||||
{
|
||||
IEnumerable<NetworkInterface> activeInterfaces = from ni in NetworkInterface.GetAllNetworkInterfaces()
|
||||
where ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback
|
||||
select ni;
|
||||
|
||||
if (!activeInterfaces.Any())
|
||||
{
|
||||
_networkAccess = NetworkAccess.None;
|
||||
_connectionProfiles = Enumerable.Empty<ConnectionProfile>();
|
||||
return;
|
||||
}
|
||||
|
||||
List<ConnectionProfile> profiles = new List<ConnectionProfile>();
|
||||
foreach (var networkInterface in activeInterfaces)
|
||||
{
|
||||
switch (networkInterface.NetworkInterfaceType)
|
||||
{
|
||||
case NetworkInterfaceType.Ethernet:
|
||||
case NetworkInterfaceType.FastEthernetT:
|
||||
case NetworkInterfaceType.FastEthernetFx:
|
||||
case NetworkInterfaceType.GigabitEthernet:
|
||||
profiles.Add(ConnectionProfile.Ethernet);
|
||||
break;
|
||||
case NetworkInterfaceType.Wireless80211:
|
||||
profiles.Add(ConnectionProfile.WiFi);
|
||||
break;
|
||||
case NetworkInterfaceType.Ppp:
|
||||
case NetworkInterfaceType.Slip:
|
||||
profiles.Add(ConnectionProfile.Cellular);
|
||||
break;
|
||||
default:
|
||||
profiles.Add(ConnectionProfile.Unknown);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_connectionProfiles = profiles.Distinct().ToList();
|
||||
|
||||
if (CheckInternetAccess())
|
||||
{
|
||||
_networkAccess = NetworkAccess.Internet;
|
||||
}
|
||||
else if (_connectionProfiles.Any())
|
||||
{
|
||||
_networkAccess = NetworkAccess.Local;
|
||||
}
|
||||
else
|
||||
{
|
||||
_networkAccess = NetworkAccess.None;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_networkAccess = NetworkAccess.Unknown;
|
||||
_connectionProfiles = new ConnectionProfile[] { ConnectionProfile.Unknown };
|
||||
}
|
||||
}
|
||||
|
||||
private bool CheckInternetAccess()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Dns.GetHostEntry("dns.google").AddressList.Length != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (NetworkInterface item in from n in NetworkInterface.GetAllNetworkInterfaces()
|
||||
where n.OperationalStatus == OperationalStatus.Up
|
||||
select n)
|
||||
{
|
||||
if (item.GetIPProperties().GatewayAddresses.Any((GatewayIPAddressInformation g) => g.Address.AddressFamily == AddressFamily.InterNetwork))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
|
||||
{
|
||||
NetworkAccess previousAccess = _networkAccess;
|
||||
List<ConnectionProfile> previousProfiles = _connectionProfiles.ToList();
|
||||
RefreshConnectivity();
|
||||
if (previousAccess != _networkAccess || !previousProfiles.SequenceEqual(_connectionProfiles))
|
||||
{
|
||||
ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNetworkAddressChanged(object? sender, EventArgs e)
|
||||
{
|
||||
NetworkAccess previousAccess = _networkAccess;
|
||||
List<ConnectionProfile> previousProfiles = _connectionProfiles.ToList();
|
||||
RefreshConnectivity();
|
||||
if (previousAccess != _networkAccess || !previousProfiles.SequenceEqual(_connectionProfiles))
|
||||
{
|
||||
ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
|
||||
NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Services/DeviceDisplayService.cs
Normal file
124
Services/DeviceDisplayService.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Maui.Devices;
|
||||
using Microsoft.Maui.Platform.Linux.Native;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
public class DeviceDisplayService : IDeviceDisplay
|
||||
{
|
||||
private static readonly Lazy<DeviceDisplayService> _instance = new Lazy<DeviceDisplayService>(() => new DeviceDisplayService());
|
||||
|
||||
private DisplayInfo _mainDisplayInfo;
|
||||
|
||||
private bool _keepScreenOn;
|
||||
|
||||
public static DeviceDisplayService Instance => _instance.Value;
|
||||
|
||||
public bool KeepScreenOn
|
||||
{
|
||||
get
|
||||
{
|
||||
return _keepScreenOn;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_keepScreenOn != value)
|
||||
{
|
||||
_keepScreenOn = value;
|
||||
SetScreenSaverInhibit(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DisplayInfo MainDisplayInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
RefreshDisplayInfo();
|
||||
return _mainDisplayInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<DisplayInfoChangedEventArgs>? MainDisplayInfoChanged;
|
||||
|
||||
public DeviceDisplayService()
|
||||
{
|
||||
RefreshDisplayInfo();
|
||||
}
|
||||
|
||||
private void RefreshDisplayInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
IntPtr screen = GdkNative.gdk_screen_get_default();
|
||||
if (screen != IntPtr.Zero)
|
||||
{
|
||||
int width = GdkNative.gdk_screen_get_width(screen);
|
||||
int height = GdkNative.gdk_screen_get_height(screen);
|
||||
double scaleFactor = GetScaleFactor();
|
||||
DisplayOrientation orientation = (width <= height) ? DisplayOrientation.Portrait : DisplayOrientation.Landscape;
|
||||
_mainDisplayInfo = new DisplayInfo(width, height, scaleFactor, orientation, DisplayRotation.Rotation0, GetRefreshRate());
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, DisplayOrientation.Landscape, DisplayRotation.Rotation0, 60f);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, DisplayOrientation.Landscape, DisplayRotation.Rotation0, 60f);
|
||||
}
|
||||
}
|
||||
|
||||
private double GetScaleFactor()
|
||||
{
|
||||
string gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE");
|
||||
if (!string.IsNullOrEmpty(gdkScale) && double.TryParse(gdkScale, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
string qtScale = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR");
|
||||
if (!string.IsNullOrEmpty(qtScale) && double.TryParse(qtScale, out result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
private float GetRefreshRate()
|
||||
{
|
||||
return 60f;
|
||||
}
|
||||
|
||||
private void SetScreenSaverInhibit(bool inhibit)
|
||||
{
|
||||
try
|
||||
{
|
||||
string action = inhibit ? "suspend" : "resume";
|
||||
IntPtr windowHandle = LinuxApplication.Current?.MainWindow?.Handle ?? IntPtr.Zero;
|
||||
if (windowHandle != IntPtr.Zero)
|
||||
{
|
||||
long windowId = windowHandle.ToInt64();
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "xdg-screensaver",
|
||||
Arguments = $"{action} {windowId}",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDisplayInfoChanged()
|
||||
{
|
||||
RefreshDisplayInfo();
|
||||
MainDisplayInfoChanged?.Invoke(this, new DisplayInfoChangedEventArgs(_mainDisplayInfo));
|
||||
}
|
||||
}
|
||||
93
Services/DeviceInfoService.cs
Normal file
93
Services/DeviceInfoService.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Maui.Devices;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
public class DeviceInfoService : IDeviceInfo
|
||||
{
|
||||
private static readonly Lazy<DeviceInfoService> _instance = new Lazy<DeviceInfoService>(() => new DeviceInfoService());
|
||||
|
||||
private string? _model;
|
||||
|
||||
private string? _manufacturer;
|
||||
|
||||
private string? _name;
|
||||
|
||||
private string? _versionString;
|
||||
|
||||
public static DeviceInfoService Instance => _instance.Value;
|
||||
|
||||
public string Model => _model ?? "Linux Desktop";
|
||||
|
||||
public string Manufacturer => _manufacturer ?? "Unknown";
|
||||
|
||||
public string Name => _name ?? Environment.MachineName;
|
||||
|
||||
public string VersionString => _versionString ?? Environment.OSVersion.VersionString;
|
||||
|
||||
public Version Version
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.Version.TryParse(Environment.OSVersion.Version.ToString(), out Version? result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return new Version(1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public DevicePlatform Platform => DevicePlatform.Create("Linux");
|
||||
|
||||
public DeviceIdiom Idiom => DeviceIdiom.Desktop;
|
||||
|
||||
public DeviceType DeviceType => DeviceType.Physical;
|
||||
|
||||
public DeviceInfoService()
|
||||
{
|
||||
LoadDeviceInfo();
|
||||
}
|
||||
|
||||
private void LoadDeviceInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists("/sys/class/dmi/id/product_name"))
|
||||
{
|
||||
_model = File.ReadAllText("/sys/class/dmi/id/product_name").Trim();
|
||||
}
|
||||
if (File.Exists("/sys/class/dmi/id/sys_vendor"))
|
||||
{
|
||||
_manufacturer = File.ReadAllText("/sys/class/dmi/id/sys_vendor").Trim();
|
||||
}
|
||||
_name = Environment.MachineName;
|
||||
_versionString = Environment.OSVersion.VersionString;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (_model == null)
|
||||
{
|
||||
_model = "Linux Desktop";
|
||||
}
|
||||
if (_manufacturer == null)
|
||||
{
|
||||
_manufacturer = "Unknown";
|
||||
}
|
||||
if (_name == null)
|
||||
{
|
||||
_name = "localhost";
|
||||
}
|
||||
if (_versionString == null)
|
||||
{
|
||||
_versionString = "Linux";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Services/GtkContextMenuService.cs
Normal file
90
Services/GtkContextMenuService.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Maui.Platform.Linux.Native;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for displaying native GTK context menus in MAUI applications.
|
||||
/// Provides popup menu functionality with action callbacks.
|
||||
/// </summary>
|
||||
public static class GtkContextMenuService
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void ActivateCallback(IntPtr menuItem, IntPtr userData);
|
||||
|
||||
// Keep references to prevent garbage collection
|
||||
private static readonly List<ActivateCallback> _callbacks = new();
|
||||
private static readonly List<Action> _actions = new();
|
||||
|
||||
public static void ShowContextMenu(List<GtkMenuItem> items)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_callbacks.Clear();
|
||||
_actions.Clear();
|
||||
|
||||
IntPtr menu = GtkNative.gtk_menu_new();
|
||||
if (menu == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("[GtkContextMenuService] Failed to create GTK menu");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
IntPtr menuItem;
|
||||
|
||||
if (item.IsSeparator)
|
||||
{
|
||||
menuItem = GtkNative.gtk_separator_menu_item_new();
|
||||
}
|
||||
else
|
||||
{
|
||||
menuItem = GtkNative.gtk_menu_item_new_with_label(item.Text);
|
||||
GtkNative.gtk_widget_set_sensitive(menuItem, item.IsEnabled);
|
||||
|
||||
if (item.IsEnabled && item.Action != null)
|
||||
{
|
||||
var action = item.Action;
|
||||
_actions.Add(action);
|
||||
int actionIndex = _actions.Count - 1;
|
||||
|
||||
ActivateCallback callback = delegate
|
||||
{
|
||||
Console.WriteLine("[GtkContextMenuService] Menu item activated: " + item.Text);
|
||||
_actions[actionIndex]?.Invoke();
|
||||
};
|
||||
_callbacks.Add(callback);
|
||||
|
||||
GtkNative.g_signal_connect_data(
|
||||
menuItem,
|
||||
"activate",
|
||||
Marshal.GetFunctionPointerForDelegate(callback),
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
GtkNative.gtk_menu_shell_append(menu, menuItem);
|
||||
GtkNative.gtk_widget_show(menuItem);
|
||||
}
|
||||
|
||||
GtkNative.gtk_widget_show(menu);
|
||||
|
||||
IntPtr currentEvent = GtkNative.gtk_get_current_event();
|
||||
GtkNative.gtk_menu_popup_at_pointer(menu, currentEvent);
|
||||
|
||||
if (currentEvent != IntPtr.Zero)
|
||||
{
|
||||
GtkNative.gdk_event_free(currentEvent);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[GtkContextMenuService] Showed GTK menu with {items.Count} items");
|
||||
}
|
||||
}
|
||||
56
Services/GtkHostService.cs
Normal file
56
Services/GtkHostService.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Microsoft.Maui.Platform.Linux.Handlers;
|
||||
using Microsoft.Maui.Platform.Linux.Window;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Singleton service that manages the GTK host window and WebView manager.
|
||||
/// Provides centralized access to the GTK infrastructure for MAUI applications.
|
||||
/// </summary>
|
||||
public class GtkHostService
|
||||
{
|
||||
private static GtkHostService? _instance;
|
||||
private GtkHostWindow? _hostWindow;
|
||||
private GtkWebViewManager? _webViewManager;
|
||||
|
||||
public static GtkHostService Instance => _instance ??= new GtkHostService();
|
||||
|
||||
public GtkHostWindow? HostWindow => _hostWindow;
|
||||
public GtkWebViewManager? WebViewManager => _webViewManager;
|
||||
public bool IsInitialized => _hostWindow != null;
|
||||
|
||||
public event EventHandler<GtkHostWindow>? HostWindowCreated;
|
||||
|
||||
public void Initialize(string title, int width, int height)
|
||||
{
|
||||
if (_hostWindow == null)
|
||||
{
|
||||
_hostWindow = new GtkHostWindow(title, width, height);
|
||||
_webViewManager = new GtkWebViewManager(_hostWindow);
|
||||
HostWindowCreated?.Invoke(this, _hostWindow);
|
||||
}
|
||||
}
|
||||
|
||||
public GtkHostWindow GetOrCreateHostWindow(string title = "MAUI Application", int width = 800, int height = 600)
|
||||
{
|
||||
if (_hostWindow == null)
|
||||
{
|
||||
Initialize(title, width, height);
|
||||
}
|
||||
return _hostWindow!;
|
||||
}
|
||||
|
||||
public void SetWindowIcon(string iconPath)
|
||||
{
|
||||
_hostWindow?.SetIcon(iconPath);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_webViewManager?.Clear();
|
||||
_webViewManager = null;
|
||||
_hostWindow?.Dispose();
|
||||
_hostWindow = null;
|
||||
}
|
||||
}
|
||||
32
Services/GtkMenuItem.cs
Normal file
32
Services/GtkMenuItem.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a menu item for use with GtkContextMenuService.
|
||||
/// </summary>
|
||||
public class GtkMenuItem
|
||||
{
|
||||
public string Text { get; }
|
||||
public Action? Action { get; }
|
||||
public bool IsEnabled { get; }
|
||||
public bool IsSeparator { get; }
|
||||
|
||||
public static GtkMenuItem Separator => new GtkMenuItem();
|
||||
|
||||
public GtkMenuItem(string text, Action? action, bool isEnabled = true)
|
||||
{
|
||||
Text = text;
|
||||
Action = action;
|
||||
IsEnabled = isEnabled;
|
||||
IsSeparator = false;
|
||||
}
|
||||
|
||||
private GtkMenuItem()
|
||||
{
|
||||
Text = "";
|
||||
Action = null;
|
||||
IsEnabled = false;
|
||||
IsSeparator = true;
|
||||
}
|
||||
}
|
||||
158
Services/MauiIconGenerator.cs
Normal file
158
Services/MauiIconGenerator.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Microsoft.Maui.Platform.Linux.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Generates application icons from MAUI icon metadata.
|
||||
/// Creates PNG icons suitable for use as window icons on Linux.
|
||||
/// Note: SVG overlay support requires Svg.Skia package (optional).
|
||||
/// </summary>
|
||||
public static class MauiIconGenerator
|
||||
{
|
||||
private const int DefaultIconSize = 256;
|
||||
|
||||
public static string? GenerateIcon(string metaFilePath)
|
||||
{
|
||||
if (!File.Exists(metaFilePath))
|
||||
{
|
||||
Console.WriteLine("[MauiIconGenerator] Metadata file not found: " + metaFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string path = Path.GetDirectoryName(metaFilePath) ?? "";
|
||||
var metadata = ParseMetadata(File.ReadAllText(metaFilePath));
|
||||
|
||||
string outputPath = Path.Combine(path, "appicon.png");
|
||||
|
||||
int size = metadata.TryGetValue("Size", out var sizeStr) && int.TryParse(sizeStr, out var sizeVal)
|
||||
? sizeVal
|
||||
: DefaultIconSize;
|
||||
|
||||
SKColor color = metadata.TryGetValue("Color", out var colorStr)
|
||||
? ParseColor(colorStr)
|
||||
: SKColors.Purple;
|
||||
|
||||
Console.WriteLine($"[MauiIconGenerator] Generating {size}x{size} icon");
|
||||
Console.WriteLine($"[MauiIconGenerator] Color: {color}");
|
||||
|
||||
using var surface = SKSurface.Create(new SKImageInfo(size, size, SKColorType.Bgra8888, SKAlphaType.Premul));
|
||||
var canvas = surface.Canvas;
|
||||
|
||||
// Draw background with rounded corners
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
float cornerRadius = size * 0.2f;
|
||||
using var paint = new SKPaint { Color = color, IsAntialias = true };
|
||||
canvas.DrawRoundRect(new SKRoundRect(new SKRect(0, 0, size, size), cornerRadius), paint);
|
||||
|
||||
// Try to load PNG foreground as fallback (appicon_fg.png)
|
||||
string fgPngPath = Path.Combine(path, "appicon_fg.png");
|
||||
if (File.Exists(fgPngPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fgBitmap = SKBitmap.Decode(fgPngPath);
|
||||
if (fgBitmap != null)
|
||||
{
|
||||
float scale = size * 0.65f / Math.Max(fgBitmap.Width, fgBitmap.Height);
|
||||
float fgWidth = fgBitmap.Width * scale;
|
||||
float fgHeight = fgBitmap.Height * scale;
|
||||
float offsetX = (size - fgWidth) / 2f;
|
||||
float offsetY = (size - fgHeight) / 2f;
|
||||
|
||||
var dstRect = new SKRect(offsetX, offsetY, offsetX + fgWidth, offsetY + fgHeight);
|
||||
canvas.DrawBitmap(fgBitmap, dstRect);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[MauiIconGenerator] Failed to load foreground PNG: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
using var image = surface.Snapshot();
|
||||
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
|
||||
using var fileStream = File.OpenWrite(outputPath);
|
||||
data.SaveTo(fileStream);
|
||||
|
||||
Console.WriteLine("[MauiIconGenerator] Generated: " + outputPath);
|
||||
return outputPath;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("[MauiIconGenerator] Error: " + ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ParseMetadata(string content)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var parts = line.Split('=', 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
result[parts[0].Trim()] = parts[1].Trim();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SKColor ParseColor(string colorStr)
|
||||
{
|
||||
if (string.IsNullOrEmpty(colorStr))
|
||||
{
|
||||
return SKColors.Purple;
|
||||
}
|
||||
|
||||
colorStr = colorStr.Trim();
|
||||
|
||||
if (colorStr.StartsWith("#"))
|
||||
{
|
||||
string hex = colorStr.Substring(1);
|
||||
|
||||
// Expand 3-digit hex to 6-digit
|
||||
if (hex.Length == 3)
|
||||
{
|
||||
hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}";
|
||||
}
|
||||
|
||||
if (hex.Length == 6 && uint.TryParse(hex, NumberStyles.HexNumber, null, out var rgb))
|
||||
{
|
||||
return new SKColor(
|
||||
(byte)((rgb >> 16) & 0xFF),
|
||||
(byte)((rgb >> 8) & 0xFF),
|
||||
(byte)(rgb & 0xFF));
|
||||
}
|
||||
|
||||
if (hex.Length == 8 && uint.TryParse(hex, NumberStyles.HexNumber, null, out var argb))
|
||||
{
|
||||
return new SKColor(
|
||||
(byte)((argb >> 16) & 0xFF),
|
||||
(byte)((argb >> 8) & 0xFF),
|
||||
(byte)(argb & 0xFF),
|
||||
(byte)((argb >> 24) & 0xFF));
|
||||
}
|
||||
}
|
||||
|
||||
return colorStr.ToLowerInvariant() switch
|
||||
{
|
||||
"red" => SKColors.Red,
|
||||
"green" => SKColors.Green,
|
||||
"blue" => SKColors.Blue,
|
||||
"purple" => SKColors.Purple,
|
||||
"orange" => SKColors.Orange,
|
||||
"white" => SKColors.White,
|
||||
"black" => SKColors.Black,
|
||||
_ => SKColors.Purple,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user