Fix compilation: restore clean RC1 codebase

- Restore clean BindableProperty.Create syntax from RC1 commit
- Remove decompiler artifacts with mangled delegate types
- Add Svg.Skia package reference for icon support
- Fix duplicate type definitions
- Library now compiles successfully (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:
2026-01-01 07:43:44 -05:00
parent 33914bf572
commit 2a4e35cd39
258 changed files with 35256 additions and 49900 deletions

View File

@@ -1,25 +0,0 @@
using System.Collections.Generic;
namespace Microsoft.Maui.Platform.Linux.Services;
internal class _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder
{
private readonly List<char> _chars = new List<char>();
public int Length => _chars.Count;
public void Append(string s)
{
_chars.AddRange(s);
}
public void Clear()
{
_chars.Clear();
}
public override string ToString()
{
return new string(_chars.ToArray());
}
}

View File

@@ -1,52 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public static class AccessibilityServiceFactory
{
private static IAccessibilityService? _instance;
private static readonly object _lock = new object();
public static IAccessibilityService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = CreateService();
}
}
}
return _instance;
}
}
private static IAccessibilityService CreateService()
{
try
{
AtSpi2AccessibilityService atSpi2AccessibilityService = new AtSpi2AccessibilityService();
atSpi2AccessibilityService.Initialize();
return atSpi2AccessibilityService;
}
catch (Exception ex)
{
Console.WriteLine("AccessibilityServiceFactory: Failed to create AT-SPI2 service - " + ex.Message);
return new NullAccessibilityService();
}
}
public static void Reset()
{
lock (_lock)
{
_instance?.Shutdown();
_instance = null;
}
}
}

View File

@@ -1,10 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public class AccessibleAction
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? KeyBinding { get; set; }
}

View File

@@ -1,11 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum AccessibleProperty
{
Name,
Description,
Role,
Value,
Parent,
Children
}

View File

@@ -1,20 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public struct AccessibleRect
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public AccessibleRect(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}

View File

@@ -1,49 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum AccessibleRole
{
Unknown,
Window,
Application,
Panel,
Frame,
Button,
CheckBox,
RadioButton,
ComboBox,
Entry,
Label,
List,
ListItem,
Menu,
MenuBar,
MenuItem,
ScrollBar,
Slider,
SpinButton,
StatusBar,
Tab,
TabPanel,
Text,
ToggleButton,
ToolBar,
ToolTip,
Tree,
TreeItem,
Image,
ProgressBar,
Separator,
Link,
Table,
TableCell,
TableRow,
TableColumnHeader,
TableRowHeader,
PageTab,
PageTabList,
Dialog,
Alert,
Filler,
Icon,
Canvas
}

View File

@@ -1,38 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum AccessibleState
{
Active,
Armed,
Busy,
Checked,
Collapsed,
Defunct,
Editable,
Enabled,
Expandable,
Expanded,
Focusable,
Focused,
Horizontal,
Iconified,
Modal,
MultiLine,
Opaque,
Pressed,
Resizable,
Selectable,
Selected,
Sensitive,
Showing,
SingleLine,
Stale,
Transient,
Vertical,
Visible,
ManagesDescendants,
Indeterminate,
Required,
InvalidEntry,
ReadOnly
}

View File

@@ -1,50 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
[Flags]
public enum AccessibleStates : long
{
None = 0L,
Active = 1L,
Armed = 2L,
Busy = 4L,
Checked = 8L,
Collapsed = 0x10L,
Defunct = 0x20L,
Editable = 0x40L,
Enabled = 0x80L,
Expandable = 0x100L,
Expanded = 0x200L,
Focusable = 0x400L,
Focused = 0x800L,
HasToolTip = 0x1000L,
Horizontal = 0x2000L,
Iconified = 0x4000L,
Modal = 0x8000L,
MultiLine = 0x10000L,
MultiSelectable = 0x20000L,
Opaque = 0x40000L,
Pressed = 0x80000L,
Resizable = 0x100000L,
Selectable = 0x200000L,
Selected = 0x400000L,
Sensitive = 0x800000L,
Showing = 0x1000000L,
SingleLine = 0x2000000L,
Stale = 0x4000000L,
Transient = 0x8000000L,
Vertical = 0x10000000L,
Visible = 0x20000000L,
ManagesDescendants = 0x40000000L,
Indeterminate = 0x80000000L,
Required = 0x100000000L,
Truncated = 0x200000000L,
Animated = 0x400000000L,
InvalidEntry = 0x800000000L,
SupportsAutocompletion = 0x1000000000L,
SelectableText = 0x2000000000L,
IsDefault = 0x4000000000L,
Visited = 0x8000000000L,
ReadOnly = 0x10000000000L
}

View File

@@ -1,7 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum AnnouncementPriority
{
Polite,
Assertive
}

View File

@@ -1,149 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.ApplicationModel;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux app actions implementation using desktop file actions.
/// </summary>
public class AppActionsService : IAppActions
{
private readonly List<AppAction> _actions = new List<AppAction>();
private readonly List<AppAction> _actions = new();
private static readonly string DesktopFilesPath;
private static readonly string DesktopFilesPath;
static AppActionsService()
{
DesktopFilesPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"applications");
}
public bool IsSupported => true;
public bool IsSupported => true;
public event EventHandler<AppActionEventArgs>? AppActionActivated;
public event EventHandler<AppActionEventArgs>? AppActionActivated;
static AppActionsService()
{
DesktopFilesPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "applications");
}
public Task<IEnumerable<AppAction>> GetAsync()
{
return Task.FromResult<IEnumerable<AppAction>>(_actions.AsReadOnly());
}
public Task<IEnumerable<AppAction>> GetAsync()
{
return Task.FromResult((IEnumerable<AppAction>)_actions.AsReadOnly());
}
public Task SetAsync(IEnumerable<AppAction> actions)
{
_actions.Clear();
_actions.AddRange(actions);
public Task SetAsync(IEnumerable<AppAction> actions)
{
_actions.Clear();
_actions.AddRange(actions);
UpdateDesktopActions();
return Task.CompletedTask;
}
// On Linux, app actions can be exposed via .desktop file Actions
// This would require modifying the application's .desktop file
UpdateDesktopActions();
private void UpdateDesktopActions()
{
}
return Task.CompletedTask;
}
public void HandleActionArgument(string actionId)
{
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_003f: Expected O, but got Unknown
AppAction val = ((IEnumerable<AppAction>)_actions).FirstOrDefault((Func<AppAction, bool>)((AppAction a) => a.Id == actionId));
if (val != null)
{
this.AppActionActivated?.Invoke(this, new AppActionEventArgs(val));
}
}
private void UpdateDesktopActions()
{
// Desktop actions are defined in the .desktop file
// Example:
// [Desktop Action new-window]
// Name=New Window
// Exec=myapp --action=new-window
public void CreateDesktopFile(string appName, string execPath, string? iconPath = null)
{
try
{
if (!Directory.Exists(DesktopFilesPath))
{
Directory.CreateDirectory(DesktopFilesPath);
}
string contents = GenerateDesktopFileContent(appName, execPath, iconPath);
string path = Path.Combine(DesktopFilesPath, appName.ToLowerInvariant().Replace(" ", "-") + ".desktop");
File.WriteAllText(path, contents);
File.SetUnixFileMode(path, UnixFileMode.OtherRead | UnixFileMode.GroupRead | UnixFileMode.UserExecute | UnixFileMode.UserWrite | UnixFileMode.UserRead);
}
catch
{
}
}
// For a proper implementation, we would need to:
// 1. Find or create the application's .desktop file
// 2. Add [Desktop Action] sections for each action
// 3. The actions would then appear in the dock/launcher right-click menu
private string GenerateDesktopFileContent(string appName, string execPath, string? iconPath)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine("[Desktop Entry]");
stringBuilder.AppendLine("Type=Application");
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder2);
handler.AppendLiteral("Name=");
handler.AppendFormatted(appName);
stringBuilder3.AppendLine(ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2);
handler.AppendLiteral("Exec=");
handler.AppendFormatted(execPath);
handler.AppendLiteral(" %U");
stringBuilder4.AppendLine(ref handler);
if (!string.IsNullOrEmpty(iconPath) && File.Exists(iconPath))
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder2);
handler.AppendLiteral("Icon=");
handler.AppendFormatted(iconPath);
stringBuilder5.AppendLine(ref handler);
}
stringBuilder.AppendLine("Terminal=false");
stringBuilder.AppendLine("Categories=Utility;");
if (_actions.Count > 0)
{
string value = string.Join(";", _actions.Select((AppAction a) => a.Id));
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder2);
handler.AppendLiteral("Actions=");
handler.AppendFormatted(value);
handler.AppendLiteral(";");
stringBuilder6.AppendLine(ref handler);
stringBuilder.AppendLine();
foreach (AppAction action in _actions)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder7 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(17, 1, stringBuilder2);
handler.AppendLiteral("[Desktop Action ");
handler.AppendFormatted(action.Id);
handler.AppendLiteral("]");
stringBuilder7.AppendLine(ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder8 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(5, 1, stringBuilder2);
handler.AppendLiteral("Name=");
handler.AppendFormatted(action.Title);
stringBuilder8.AppendLine(ref handler);
if (!string.IsNullOrEmpty(action.Subtitle))
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder9 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(8, 1, stringBuilder2);
handler.AppendLiteral("Comment=");
handler.AppendFormatted(action.Subtitle);
stringBuilder9.AppendLine(ref handler);
}
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder10 = stringBuilder2;
handler = new StringBuilder.AppendInterpolatedStringHandler(15, 2, stringBuilder2);
handler.AppendLiteral("Exec=");
handler.AppendFormatted(execPath);
handler.AppendLiteral(" --action=");
handler.AppendFormatted(action.Id);
stringBuilder10.AppendLine(ref handler);
stringBuilder.AppendLine();
}
}
return stringBuilder.ToString();
}
// This is a simplified implementation that logs actions
// A full implementation would require more system integration
}
/// <summary>
/// Call this method to handle command-line action arguments.
/// </summary>
public void HandleActionArgument(string actionId)
{
var action = _actions.FirstOrDefault(a => a.Id == actionId);
if (action != null)
{
AppActionActivated?.Invoke(this, new AppActionEventArgs(action));
}
}
/// <summary>
/// Creates a .desktop file for the application with the defined actions.
/// </summary>
public void CreateDesktopFile(string appName, string execPath, string? iconPath = null)
{
try
{
if (!Directory.Exists(DesktopFilesPath))
{
Directory.CreateDirectory(DesktopFilesPath);
}
var desktopContent = GenerateDesktopFileContent(appName, execPath, iconPath);
var desktopFilePath = Path.Combine(DesktopFilesPath, $"{appName.ToLowerInvariant().Replace(" ", "-")}.desktop");
File.WriteAllText(desktopFilePath, desktopContent);
// Make it executable
File.SetUnixFileMode(desktopFilePath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.OtherRead);
}
catch
{
// Silently fail - desktop file creation is optional
}
}
private string GenerateDesktopFileContent(string appName, string execPath, string? iconPath)
{
var content = new System.Text.StringBuilder();
content.AppendLine("[Desktop Entry]");
content.AppendLine("Type=Application");
content.AppendLine($"Name={appName}");
content.AppendLine($"Exec={execPath} %U");
if (!string.IsNullOrEmpty(iconPath) && File.Exists(iconPath))
{
content.AppendLine($"Icon={iconPath}");
}
content.AppendLine("Terminal=false");
content.AppendLine("Categories=Utility;");
// Add actions list
if (_actions.Count > 0)
{
var actionIds = string.Join(";", _actions.Select(a => a.Id));
content.AppendLine($"Actions={actionIds};");
content.AppendLine();
// Add each action section
foreach (var action in _actions)
{
content.AppendLine($"[Desktop Action {action.Id}]");
content.AppendLine($"Name={action.Title}");
if (!string.IsNullOrEmpty(action.Subtitle))
{
content.AppendLine($"Comment={action.Subtitle}");
}
content.AppendLine($"Exec={execPath} --action={action.Id}");
{
}
content.AppendLine();
}
}
return content.ToString();
}
}

View File

@@ -1,147 +0,0 @@
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)1;
public AppTheme RequestedTheme
{
get
{
//IL_0042: Unknown result type (might be due to invalid IL or missing references)
//IL_0045: Unknown result type (might be due to invalid IL or missing references)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_0039: Unknown result type (might be due to invalid IL or missing references)
//IL_0022: Unknown result type (might be due to invalid IL or missing references)
try
{
string environmentVariable = Environment.GetEnvironmentVariable("GTK_THEME");
if (!string.IsNullOrEmpty(environmentVariable) && environmentVariable.Contains("dark", StringComparison.OrdinalIgnoreCase))
{
return (AppTheme)2;
}
if (GetGnomeColorScheme().Contains("dark", StringComparison.OrdinalIgnoreCase))
{
return (AppTheme)2;
}
return (AppTheme)1;
}
catch
{
return (AppTheme)1;
}
}
}
public AppPackagingModel PackagingModel
{
get
{
if (Environment.GetEnvironmentVariable("FLATPAK_ID") == null)
{
if (Environment.GetEnvironmentVariable("SNAP") == null)
{
if (Environment.GetEnvironmentVariable("APPIMAGE") == null)
{
return (AppPackagingModel)1;
}
return (AppPackagingModel)0;
}
return (AppPackagingModel)0;
}
return (AppPackagingModel)0;
}
}
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
{
}
}
}
}

View File

@@ -1,500 +1,461 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// AT-SPI2 accessibility service implementation.
/// Provides screen reader support through the AT-SPI2 D-Bus interface.
/// </summary>
public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable
{
private IntPtr _connection;
private IntPtr _registry;
private bool _isEnabled;
private bool _disposed;
private IAccessible? _focusedAccessible;
private readonly ConcurrentDictionary<string, IAccessible> _registeredObjects = new ConcurrentDictionary<string, IAccessible>();
private readonly string _applicationName;
private IntPtr _applicationAccessible;
private const int ATSPI_ROLE_UNKNOWN = 0;
private const int ATSPI_ROLE_WINDOW = 22;
private const int ATSPI_ROLE_APPLICATION = 75;
private const int ATSPI_ROLE_PANEL = 25;
private const int ATSPI_ROLE_FRAME = 11;
private const int ATSPI_ROLE_PUSH_BUTTON = 31;
private const int ATSPI_ROLE_CHECK_BOX = 4;
private const int ATSPI_ROLE_RADIO_BUTTON = 33;
private const int ATSPI_ROLE_COMBO_BOX = 6;
private const int ATSPI_ROLE_ENTRY = 24;
private const int ATSPI_ROLE_LABEL = 16;
private const int ATSPI_ROLE_LIST = 17;
private const int ATSPI_ROLE_LIST_ITEM = 18;
private const int ATSPI_ROLE_MENU = 19;
private const int ATSPI_ROLE_MENU_BAR = 20;
private const int ATSPI_ROLE_MENU_ITEM = 21;
private const int ATSPI_ROLE_SCROLL_BAR = 40;
private const int ATSPI_ROLE_SLIDER = 43;
private const int ATSPI_ROLE_SPIN_BUTTON = 44;
private const int ATSPI_ROLE_STATUS_BAR = 46;
private const int ATSPI_ROLE_PAGE_TAB = 26;
private const int ATSPI_ROLE_PAGE_TAB_LIST = 27;
private const int ATSPI_ROLE_TEXT = 49;
private const int ATSPI_ROLE_TOGGLE_BUTTON = 51;
private const int ATSPI_ROLE_TOOL_BAR = 52;
private const int ATSPI_ROLE_TOOL_TIP = 53;
private const int ATSPI_ROLE_TREE = 54;
private const int ATSPI_ROLE_TREE_ITEM = 55;
private const int ATSPI_ROLE_IMAGE = 14;
private const int ATSPI_ROLE_PROGRESS_BAR = 30;
private const int ATSPI_ROLE_SEPARATOR = 42;
private const int ATSPI_ROLE_LINK = 83;
private const int ATSPI_ROLE_TABLE = 47;
private const int ATSPI_ROLE_TABLE_CELL = 48;
private const int ATSPI_ROLE_TABLE_ROW = 89;
private const int ATSPI_ROLE_TABLE_COLUMN_HEADER = 36;
private const int ATSPI_ROLE_TABLE_ROW_HEADER = 37;
private const int ATSPI_ROLE_DIALOG = 8;
private const int ATSPI_ROLE_ALERT = 2;
private const int ATSPI_ROLE_FILLER = 10;
private const int ATSPI_ROLE_ICON = 13;
private const int ATSPI_ROLE_CANVAS = 3;
public bool IsEnabled => _isEnabled;
public AtSpi2AccessibilityService(string applicationName = "MAUI Application")
{
_applicationName = applicationName;
}
public void Initialize()
{
try
{
if (atspi_init() != 0)
{
Console.WriteLine("AtSpi2AccessibilityService: Failed to initialize AT-SPI2");
return;
}
_isEnabled = CheckAccessibilityEnabled();
if (_isEnabled)
{
_registry = atspi_get_desktop(0);
RegisterApplication();
Console.WriteLine("AtSpi2AccessibilityService: Initialized successfully");
}
else
{
Console.WriteLine("AtSpi2AccessibilityService: Accessibility is not enabled");
}
}
catch (Exception ex)
{
Console.WriteLine("AtSpi2AccessibilityService: Initialization failed - " + ex.Message);
}
}
private bool CheckAccessibilityEnabled()
{
try
{
IntPtr intPtr = atspi_get_desktop(0);
if (intPtr != IntPtr.Zero)
{
g_object_unref(intPtr);
return true;
}
}
catch
{
}
return Environment.GetEnvironmentVariable("GTK_A11Y")?.ToLowerInvariant() != "none";
}
private void RegisterApplication()
{
atspi_set_main_context(IntPtr.Zero);
}
public void Register(IAccessible accessible)
{
if (accessible != null)
{
_registeredObjects.TryAdd(accessible.AccessibleId, accessible);
}
}
public void Unregister(IAccessible accessible)
{
if (accessible != null)
{
_registeredObjects.TryRemove(accessible.AccessibleId, out IAccessible _);
}
}
public void NotifyFocusChanged(IAccessible? accessible)
{
_focusedAccessible = accessible;
if (_isEnabled && accessible != null)
{
EmitEvent("focus:", accessible);
}
}
public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property)
{
if (_isEnabled && accessible != null)
{
string text = property switch
{
AccessibleProperty.Name => "object:property-change:accessible-name",
AccessibleProperty.Description => "object:property-change:accessible-description",
AccessibleProperty.Role => "object:property-change:accessible-role",
AccessibleProperty.Value => "object:property-change:accessible-value",
AccessibleProperty.Parent => "object:property-change:accessible-parent",
AccessibleProperty.Children => "object:children-changed",
_ => string.Empty,
};
if (!string.IsNullOrEmpty(text))
{
EmitEvent(text, accessible);
}
}
}
public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value)
{
if (_isEnabled && accessible != null)
{
string text = state.ToString().ToLowerInvariant();
string eventName = "object:state-changed:" + text;
EmitEvent(eventName, accessible, value ? 1 : 0);
}
}
public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite)
{
if (!_isEnabled || string.IsNullOrEmpty(text))
{
return;
}
try
{
Console.WriteLine($"[Accessibility Announcement ({priority})]: {text}");
}
catch (Exception ex)
{
Console.WriteLine("AtSpi2AccessibilityService: Announcement failed - " + ex.Message);
}
}
private void EmitEvent(string eventName, IAccessible accessible, int detail1 = 0, int detail2 = 0)
{
Console.WriteLine($"[AT-SPI2 Event] {eventName}: {accessible.AccessibleName} ({accessible.Role})");
}
public static int GetAtSpiRole(AccessibleRole role)
{
return role switch
{
AccessibleRole.Unknown => 0,
AccessibleRole.Window => 22,
AccessibleRole.Application => 75,
AccessibleRole.Panel => 25,
AccessibleRole.Frame => 11,
AccessibleRole.Button => 31,
AccessibleRole.CheckBox => 4,
AccessibleRole.RadioButton => 33,
AccessibleRole.ComboBox => 6,
AccessibleRole.Entry => 24,
AccessibleRole.Label => 16,
AccessibleRole.List => 17,
AccessibleRole.ListItem => 18,
AccessibleRole.Menu => 19,
AccessibleRole.MenuBar => 20,
AccessibleRole.MenuItem => 21,
AccessibleRole.ScrollBar => 40,
AccessibleRole.Slider => 43,
AccessibleRole.SpinButton => 44,
AccessibleRole.StatusBar => 46,
AccessibleRole.Tab => 26,
AccessibleRole.TabPanel => 27,
AccessibleRole.Text => 49,
AccessibleRole.ToggleButton => 51,
AccessibleRole.ToolBar => 52,
AccessibleRole.ToolTip => 53,
AccessibleRole.Tree => 54,
AccessibleRole.TreeItem => 55,
AccessibleRole.Image => 14,
AccessibleRole.ProgressBar => 30,
AccessibleRole.Separator => 42,
AccessibleRole.Link => 83,
AccessibleRole.Table => 47,
AccessibleRole.TableCell => 48,
AccessibleRole.TableRow => 89,
AccessibleRole.TableColumnHeader => 36,
AccessibleRole.TableRowHeader => 37,
AccessibleRole.PageTab => 26,
AccessibleRole.PageTabList => 27,
AccessibleRole.Dialog => 8,
AccessibleRole.Alert => 2,
AccessibleRole.Filler => 10,
AccessibleRole.Icon => 13,
AccessibleRole.Canvas => 3,
_ => 0,
};
}
public static (uint Low, uint High) GetAtSpiStates(AccessibleStates states)
{
uint num = 0u;
uint num2 = 0u;
if (states.HasFlag(AccessibleStates.Active))
{
num |= 1;
}
if (states.HasFlag(AccessibleStates.Armed))
{
num |= 2;
}
if (states.HasFlag(AccessibleStates.Busy))
{
num |= 4;
}
if (states.HasFlag(AccessibleStates.Checked))
{
num |= 8;
}
if (states.HasFlag(AccessibleStates.Collapsed))
{
num |= 0x10;
}
if (states.HasFlag(AccessibleStates.Defunct))
{
num |= 0x20;
}
if (states.HasFlag(AccessibleStates.Editable))
{
num |= 0x40;
}
if (states.HasFlag(AccessibleStates.Enabled))
{
num |= 0x80;
}
if (states.HasFlag(AccessibleStates.Expandable))
{
num |= 0x100;
}
if (states.HasFlag(AccessibleStates.Expanded))
{
num |= 0x200;
}
if (states.HasFlag(AccessibleStates.Focusable))
{
num |= 0x400;
}
if (states.HasFlag(AccessibleStates.Focused))
{
num |= 0x800;
}
if (states.HasFlag(AccessibleStates.Horizontal))
{
num |= 0x2000;
}
if (states.HasFlag(AccessibleStates.Iconified))
{
num |= 0x4000;
}
if (states.HasFlag(AccessibleStates.Modal))
{
num |= 0x8000;
}
if (states.HasFlag(AccessibleStates.MultiLine))
{
num |= 0x10000;
}
if (states.HasFlag(AccessibleStates.MultiSelectable))
{
num |= 0x20000;
}
if (states.HasFlag(AccessibleStates.Opaque))
{
num |= 0x40000;
}
if (states.HasFlag(AccessibleStates.Pressed))
{
num |= 0x80000;
}
if (states.HasFlag(AccessibleStates.Resizable))
{
num |= 0x100000;
}
if (states.HasFlag(AccessibleStates.Selectable))
{
num |= 0x200000;
}
if (states.HasFlag(AccessibleStates.Selected))
{
num |= 0x400000;
}
if (states.HasFlag(AccessibleStates.Sensitive))
{
num |= 0x800000;
}
if (states.HasFlag(AccessibleStates.Showing))
{
num |= 0x1000000;
}
if (states.HasFlag(AccessibleStates.SingleLine))
{
num |= 0x2000000;
}
if (states.HasFlag(AccessibleStates.Stale))
{
num |= 0x4000000;
}
if (states.HasFlag(AccessibleStates.Transient))
{
num |= 0x8000000;
}
if (states.HasFlag(AccessibleStates.Vertical))
{
num |= 0x10000000;
}
if (states.HasFlag(AccessibleStates.Visible))
{
num |= 0x20000000;
}
if (states.HasFlag(AccessibleStates.ManagesDescendants))
{
num |= 0x40000000;
}
if (states.HasFlag(AccessibleStates.Indeterminate))
{
num |= 0x80000000u;
}
if (states.HasFlag(AccessibleStates.Required))
{
num2 |= 1;
}
if (states.HasFlag(AccessibleStates.Truncated))
{
num2 |= 2;
}
if (states.HasFlag(AccessibleStates.Animated))
{
num2 |= 4;
}
if (states.HasFlag(AccessibleStates.InvalidEntry))
{
num2 |= 8;
}
if (states.HasFlag(AccessibleStates.SupportsAutocompletion))
{
num2 |= 0x10;
}
if (states.HasFlag(AccessibleStates.SelectableText))
{
num2 |= 0x20;
}
if (states.HasFlag(AccessibleStates.IsDefault))
{
num2 |= 0x40;
}
if (states.HasFlag(AccessibleStates.Visited))
{
num2 |= 0x80;
}
if (states.HasFlag(AccessibleStates.ReadOnly))
{
num2 |= 0x400;
}
return (Low: num, High: num2);
}
public void Shutdown()
{
Dispose();
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_registeredObjects.Clear();
if (_applicationAccessible != IntPtr.Zero)
{
g_object_unref(_applicationAccessible);
_applicationAccessible = IntPtr.Zero;
}
if (_registry != IntPtr.Zero)
{
g_object_unref(_registry);
_registry = IntPtr.Zero;
}
atspi_exit();
}
}
[DllImport("libatspi.so.0")]
private static extern int atspi_init();
[DllImport("libatspi.so.0")]
private static extern int atspi_exit();
[DllImport("libatspi.so.0")]
private static extern IntPtr atspi_get_desktop(int i);
[DllImport("libatspi.so.0")]
private static extern void atspi_set_main_context(IntPtr context);
[DllImport("libgobject-2.0.so.0")]
private static extern void g_object_unref(IntPtr obj);
private nint _connection;
private nint _registry;
private bool _isEnabled;
private bool _disposed;
private IAccessible? _focusedAccessible;
private readonly ConcurrentDictionary<string, IAccessible> _registeredObjects = new();
private readonly string _applicationName;
private nint _applicationAccessible;
public bool IsEnabled => _isEnabled;
public AtSpi2AccessibilityService(string applicationName = "MAUI Application")
{
_applicationName = applicationName;
}
public void Initialize()
{
try
{
// Initialize AT-SPI2
int result = atspi_init();
if (result != 0)
{
Console.WriteLine("AtSpi2AccessibilityService: Failed to initialize AT-SPI2");
return;
}
// Check if accessibility is enabled
_isEnabled = CheckAccessibilityEnabled();
if (_isEnabled)
{
// Get the desktop (root accessible)
_registry = atspi_get_desktop(0);
// Register our application
RegisterApplication();
Console.WriteLine("AtSpi2AccessibilityService: Initialized successfully");
}
else
{
Console.WriteLine("AtSpi2AccessibilityService: Accessibility is not enabled");
}
}
catch (Exception ex)
{
Console.WriteLine($"AtSpi2AccessibilityService: Initialization failed - {ex.Message}");
}
}
private bool CheckAccessibilityEnabled()
{
// Check if AT-SPI2 registry is available
try
{
nint desktop = atspi_get_desktop(0);
if (desktop != IntPtr.Zero)
{
g_object_unref(desktop);
return true;
}
}
catch
{
// AT-SPI2 not available
}
// Also check the gsettings key
var enabled = Environment.GetEnvironmentVariable("GTK_A11Y");
return enabled?.ToLowerInvariant() != "none";
}
private void RegisterApplication()
{
// In a full implementation, we would create an AtspiApplication object
// and register it with the AT-SPI2 registry. For now, we set up the basics.
// Set application name
atspi_set_main_context(IntPtr.Zero);
}
public void Register(IAccessible accessible)
{
if (accessible == null) return;
_registeredObjects.TryAdd(accessible.AccessibleId, accessible);
// In a full implementation, we would create an AtspiAccessible object
// and register it with AT-SPI2
}
public void Unregister(IAccessible accessible)
{
if (accessible == null) return;
_registeredObjects.TryRemove(accessible.AccessibleId, out _);
// Clean up AT-SPI2 resources for this accessible
}
public void NotifyFocusChanged(IAccessible? accessible)
{
_focusedAccessible = accessible;
if (!_isEnabled || accessible == null) return;
// Emit focus event through AT-SPI2
EmitEvent("focus:", accessible);
}
public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property)
{
if (!_isEnabled || accessible == null) return;
string eventName = property switch
{
AccessibleProperty.Name => "object:property-change:accessible-name",
AccessibleProperty.Description => "object:property-change:accessible-description",
AccessibleProperty.Role => "object:property-change:accessible-role",
AccessibleProperty.Value => "object:property-change:accessible-value",
AccessibleProperty.Parent => "object:property-change:accessible-parent",
AccessibleProperty.Children => "object:children-changed",
_ => string.Empty
};
if (!string.IsNullOrEmpty(eventName))
{
EmitEvent(eventName, accessible);
}
}
public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value)
{
if (!_isEnabled || accessible == null) return;
string stateName = state.ToString().ToLowerInvariant();
string eventName = $"object:state-changed:{stateName}";
EmitEvent(eventName, accessible, value ? 1 : 0);
}
public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite)
{
if (!_isEnabled || string.IsNullOrEmpty(text)) return;
// Use AT-SPI2 live region to announce text
// Priority maps to: Polite = ATSPI_LIVE_POLITE, Assertive = ATSPI_LIVE_ASSERTIVE
try
{
// In AT-SPI2, announcements are typically done through live regions
// or by emitting "object:announcement" events
// For now, use a simpler approach with the event system
Console.WriteLine($"[Accessibility Announcement ({priority})]: {text}");
}
catch (Exception ex)
{
Console.WriteLine($"AtSpi2AccessibilityService: Announcement failed - {ex.Message}");
}
}
private void EmitEvent(string eventName, IAccessible accessible, int detail1 = 0, int detail2 = 0)
{
// In a full implementation, we would emit the event through D-Bus
// using the org.a11y.atspi.Event interface
// For now, log the event for debugging
Console.WriteLine($"[AT-SPI2 Event] {eventName}: {accessible.AccessibleName} ({accessible.Role})");
}
/// <summary>
/// Gets the AT-SPI2 role value for the given accessible role.
/// </summary>
public static int GetAtSpiRole(AccessibleRole role)
{
return role switch
{
AccessibleRole.Unknown => ATSPI_ROLE_UNKNOWN,
AccessibleRole.Window => ATSPI_ROLE_WINDOW,
AccessibleRole.Application => ATSPI_ROLE_APPLICATION,
AccessibleRole.Panel => ATSPI_ROLE_PANEL,
AccessibleRole.Frame => ATSPI_ROLE_FRAME,
AccessibleRole.Button => ATSPI_ROLE_PUSH_BUTTON,
AccessibleRole.CheckBox => ATSPI_ROLE_CHECK_BOX,
AccessibleRole.RadioButton => ATSPI_ROLE_RADIO_BUTTON,
AccessibleRole.ComboBox => ATSPI_ROLE_COMBO_BOX,
AccessibleRole.Entry => ATSPI_ROLE_ENTRY,
AccessibleRole.Label => ATSPI_ROLE_LABEL,
AccessibleRole.List => ATSPI_ROLE_LIST,
AccessibleRole.ListItem => ATSPI_ROLE_LIST_ITEM,
AccessibleRole.Menu => ATSPI_ROLE_MENU,
AccessibleRole.MenuBar => ATSPI_ROLE_MENU_BAR,
AccessibleRole.MenuItem => ATSPI_ROLE_MENU_ITEM,
AccessibleRole.ScrollBar => ATSPI_ROLE_SCROLL_BAR,
AccessibleRole.Slider => ATSPI_ROLE_SLIDER,
AccessibleRole.SpinButton => ATSPI_ROLE_SPIN_BUTTON,
AccessibleRole.StatusBar => ATSPI_ROLE_STATUS_BAR,
AccessibleRole.Tab => ATSPI_ROLE_PAGE_TAB,
AccessibleRole.TabPanel => ATSPI_ROLE_PAGE_TAB_LIST,
AccessibleRole.Text => ATSPI_ROLE_TEXT,
AccessibleRole.ToggleButton => ATSPI_ROLE_TOGGLE_BUTTON,
AccessibleRole.ToolBar => ATSPI_ROLE_TOOL_BAR,
AccessibleRole.ToolTip => ATSPI_ROLE_TOOL_TIP,
AccessibleRole.Tree => ATSPI_ROLE_TREE,
AccessibleRole.TreeItem => ATSPI_ROLE_TREE_ITEM,
AccessibleRole.Image => ATSPI_ROLE_IMAGE,
AccessibleRole.ProgressBar => ATSPI_ROLE_PROGRESS_BAR,
AccessibleRole.Separator => ATSPI_ROLE_SEPARATOR,
AccessibleRole.Link => ATSPI_ROLE_LINK,
AccessibleRole.Table => ATSPI_ROLE_TABLE,
AccessibleRole.TableCell => ATSPI_ROLE_TABLE_CELL,
AccessibleRole.TableRow => ATSPI_ROLE_TABLE_ROW,
AccessibleRole.TableColumnHeader => ATSPI_ROLE_TABLE_COLUMN_HEADER,
AccessibleRole.TableRowHeader => ATSPI_ROLE_TABLE_ROW_HEADER,
AccessibleRole.PageTab => ATSPI_ROLE_PAGE_TAB,
AccessibleRole.PageTabList => ATSPI_ROLE_PAGE_TAB_LIST,
AccessibleRole.Dialog => ATSPI_ROLE_DIALOG,
AccessibleRole.Alert => ATSPI_ROLE_ALERT,
AccessibleRole.Filler => ATSPI_ROLE_FILLER,
AccessibleRole.Icon => ATSPI_ROLE_ICON,
AccessibleRole.Canvas => ATSPI_ROLE_CANVAS,
_ => ATSPI_ROLE_UNKNOWN
};
}
/// <summary>
/// Converts accessible states to AT-SPI2 state set.
/// </summary>
public static (uint Low, uint High) GetAtSpiStates(AccessibleStates states)
{
uint low = 0;
uint high = 0;
if (states.HasFlag(AccessibleStates.Active)) low |= 1 << 0;
if (states.HasFlag(AccessibleStates.Armed)) low |= 1 << 1;
if (states.HasFlag(AccessibleStates.Busy)) low |= 1 << 2;
if (states.HasFlag(AccessibleStates.Checked)) low |= 1 << 3;
if (states.HasFlag(AccessibleStates.Collapsed)) low |= 1 << 4;
if (states.HasFlag(AccessibleStates.Defunct)) low |= 1 << 5;
if (states.HasFlag(AccessibleStates.Editable)) low |= 1 << 6;
if (states.HasFlag(AccessibleStates.Enabled)) low |= 1 << 7;
if (states.HasFlag(AccessibleStates.Expandable)) low |= 1 << 8;
if (states.HasFlag(AccessibleStates.Expanded)) low |= 1 << 9;
if (states.HasFlag(AccessibleStates.Focusable)) low |= 1 << 10;
if (states.HasFlag(AccessibleStates.Focused)) low |= 1 << 11;
if (states.HasFlag(AccessibleStates.Horizontal)) low |= 1 << 13;
if (states.HasFlag(AccessibleStates.Iconified)) low |= 1 << 14;
if (states.HasFlag(AccessibleStates.Modal)) low |= 1 << 15;
if (states.HasFlag(AccessibleStates.MultiLine)) low |= 1 << 16;
if (states.HasFlag(AccessibleStates.MultiSelectable)) low |= 1 << 17;
if (states.HasFlag(AccessibleStates.Opaque)) low |= 1 << 18;
if (states.HasFlag(AccessibleStates.Pressed)) low |= 1 << 19;
if (states.HasFlag(AccessibleStates.Resizable)) low |= 1 << 20;
if (states.HasFlag(AccessibleStates.Selectable)) low |= 1 << 21;
if (states.HasFlag(AccessibleStates.Selected)) low |= 1 << 22;
if (states.HasFlag(AccessibleStates.Sensitive)) low |= 1 << 23;
if (states.HasFlag(AccessibleStates.Showing)) low |= 1 << 24;
if (states.HasFlag(AccessibleStates.SingleLine)) low |= 1 << 25;
if (states.HasFlag(AccessibleStates.Stale)) low |= 1 << 26;
if (states.HasFlag(AccessibleStates.Transient)) low |= 1 << 27;
if (states.HasFlag(AccessibleStates.Vertical)) low |= 1 << 28;
if (states.HasFlag(AccessibleStates.Visible)) low |= 1 << 29;
if (states.HasFlag(AccessibleStates.ManagesDescendants)) low |= 1 << 30;
if (states.HasFlag(AccessibleStates.Indeterminate)) low |= 1u << 31;
// High bits (states 32+)
if (states.HasFlag(AccessibleStates.Required)) high |= 1 << 0;
if (states.HasFlag(AccessibleStates.Truncated)) high |= 1 << 1;
if (states.HasFlag(AccessibleStates.Animated)) high |= 1 << 2;
if (states.HasFlag(AccessibleStates.InvalidEntry)) high |= 1 << 3;
if (states.HasFlag(AccessibleStates.SupportsAutocompletion)) high |= 1 << 4;
if (states.HasFlag(AccessibleStates.SelectableText)) high |= 1 << 5;
if (states.HasFlag(AccessibleStates.IsDefault)) high |= 1 << 6;
if (states.HasFlag(AccessibleStates.Visited)) high |= 1 << 7;
if (states.HasFlag(AccessibleStates.ReadOnly)) high |= 1 << 10;
return (low, high);
}
public void Shutdown()
{
Dispose();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_registeredObjects.Clear();
if (_applicationAccessible != IntPtr.Zero)
{
g_object_unref(_applicationAccessible);
_applicationAccessible = IntPtr.Zero;
}
if (_registry != IntPtr.Zero)
{
g_object_unref(_registry);
_registry = IntPtr.Zero;
}
// Exit AT-SPI2
atspi_exit();
}
#region AT-SPI2 Role Constants
private const int ATSPI_ROLE_UNKNOWN = 0;
private const int ATSPI_ROLE_WINDOW = 22;
private const int ATSPI_ROLE_APPLICATION = 75;
private const int ATSPI_ROLE_PANEL = 25;
private const int ATSPI_ROLE_FRAME = 11;
private const int ATSPI_ROLE_PUSH_BUTTON = 31;
private const int ATSPI_ROLE_CHECK_BOX = 4;
private const int ATSPI_ROLE_RADIO_BUTTON = 33;
private const int ATSPI_ROLE_COMBO_BOX = 6;
private const int ATSPI_ROLE_ENTRY = 24;
private const int ATSPI_ROLE_LABEL = 16;
private const int ATSPI_ROLE_LIST = 17;
private const int ATSPI_ROLE_LIST_ITEM = 18;
private const int ATSPI_ROLE_MENU = 19;
private const int ATSPI_ROLE_MENU_BAR = 20;
private const int ATSPI_ROLE_MENU_ITEM = 21;
private const int ATSPI_ROLE_SCROLL_BAR = 40;
private const int ATSPI_ROLE_SLIDER = 43;
private const int ATSPI_ROLE_SPIN_BUTTON = 44;
private const int ATSPI_ROLE_STATUS_BAR = 46;
private const int ATSPI_ROLE_PAGE_TAB = 26;
private const int ATSPI_ROLE_PAGE_TAB_LIST = 27;
private const int ATSPI_ROLE_TEXT = 49;
private const int ATSPI_ROLE_TOGGLE_BUTTON = 51;
private const int ATSPI_ROLE_TOOL_BAR = 52;
private const int ATSPI_ROLE_TOOL_TIP = 53;
private const int ATSPI_ROLE_TREE = 54;
private const int ATSPI_ROLE_TREE_ITEM = 55;
private const int ATSPI_ROLE_IMAGE = 14;
private const int ATSPI_ROLE_PROGRESS_BAR = 30;
private const int ATSPI_ROLE_SEPARATOR = 42;
private const int ATSPI_ROLE_LINK = 83;
private const int ATSPI_ROLE_TABLE = 47;
private const int ATSPI_ROLE_TABLE_CELL = 48;
private const int ATSPI_ROLE_TABLE_ROW = 89;
private const int ATSPI_ROLE_TABLE_COLUMN_HEADER = 36;
private const int ATSPI_ROLE_TABLE_ROW_HEADER = 37;
private const int ATSPI_ROLE_DIALOG = 8;
private const int ATSPI_ROLE_ALERT = 2;
private const int ATSPI_ROLE_FILLER = 10;
private const int ATSPI_ROLE_ICON = 13;
private const int ATSPI_ROLE_CANVAS = 3;
#endregion
#region AT-SPI2 Interop
[DllImport("libatspi.so.0")]
private static extern int atspi_init();
[DllImport("libatspi.so.0")]
private static extern int atspi_exit();
[DllImport("libatspi.so.0")]
private static extern nint atspi_get_desktop(int i);
[DllImport("libatspi.so.0")]
private static extern void atspi_set_main_context(nint context);
[DllImport("libgobject-2.0.so.0")]
private static extern void g_object_unref(nint obj);
#endregion
}
/// <summary>
/// Factory for creating accessibility service instances.
/// </summary>
public static class AccessibilityServiceFactory
{
private static IAccessibilityService? _instance;
private static readonly object _lock = new();
/// <summary>
/// Gets the singleton accessibility service instance.
/// </summary>
public static IAccessibilityService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
_instance ??= CreateService();
}
}
return _instance;
}
}
private static IAccessibilityService CreateService()
{
try
{
var service = new AtSpi2AccessibilityService();
service.Initialize();
return service;
}
catch (Exception ex)
{
Console.WriteLine($"AccessibilityServiceFactory: Failed to create AT-SPI2 service - {ex.Message}");
return new NullAccessibilityService();
}
}
/// <summary>
/// Resets the singleton instance.
/// </summary>
public static void Reset()
{
lock (_lock)
{
_instance?.Shutdown();
_instance = null;
}
}
}
/// <summary>
/// Null implementation of accessibility service.
/// </summary>
public class NullAccessibilityService : IAccessibilityService
{
public bool IsEnabled => false;
public void Initialize() { }
public void Register(IAccessible accessible) { }
public void Unregister(IAccessible accessible) { }
public void NotifyFocusChanged(IAccessible? accessible) { }
public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property) { }
public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value) { }
public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite) { }
public void Shutdown() { }
}

View File

@@ -1,68 +1,66 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux browser implementation using xdg-open.
/// </summary>
public class BrowserService : IBrowser
{
public async Task<bool> OpenAsync(string uri)
{
return await OpenAsync(new Uri(uri), (BrowserLaunchMode)0);
}
public async Task<bool> OpenAsync(string uri)
{
return await OpenAsync(new Uri(uri), BrowserLaunchMode.SystemPreferred);
}
public async Task<bool> OpenAsync(string uri, BrowserLaunchMode launchMode)
{
//IL_001e: Unknown result type (might be due to invalid IL or missing references)
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
return await OpenAsync(new Uri(uri), launchMode);
}
public async Task<bool> OpenAsync(string uri, BrowserLaunchMode launchMode)
{
return await OpenAsync(new Uri(uri), launchMode);
}
public async Task<bool> OpenAsync(Uri uri)
{
return await OpenAsync(uri, (BrowserLaunchMode)0);
}
public async Task<bool> OpenAsync(Uri uri)
{
return await OpenAsync(uri, BrowserLaunchMode.SystemPreferred);
}
public async Task<bool> OpenAsync(Uri uri, BrowserLaunchMode launchMode)
{
//IL_001e: Unknown result type (might be due to invalid IL or missing references)
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
return await this.OpenAsync(uri, new BrowserLaunchOptions
{
LaunchMode = launchMode
});
}
public async Task<bool> OpenAsync(Uri uri, BrowserLaunchMode launchMode)
{
return await OpenAsync(uri, new BrowserLaunchOptions { LaunchMode = launchMode });
}
public async Task<bool> OpenAsync(Uri uri, BrowserLaunchOptions options)
{
if (uri == null)
{
throw new ArgumentNullException("uri");
}
try
{
string absoluteUri = uri.AbsoluteUri;
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = "\"" + absoluteUri + "\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return false;
}
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
public async Task<bool> OpenAsync(Uri uri, BrowserLaunchOptions options)
{
if (uri == null)
throw new ArgumentNullException(nameof(uri));
try
{
var uriString = uri.AbsoluteUri;
// Use xdg-open which respects user's default browser
var startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = $"\"{uriString}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null)
return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
}

View File

@@ -1,195 +1,206 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel.DataTransfer;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux clipboard implementation using xclip/xsel command line tools.
/// </summary>
public class ClipboardService : IClipboard
{
private string? _lastSetText;
private string? _lastSetText;
public bool HasText
{
get
{
try
{
return !string.IsNullOrEmpty(GetTextAsync().GetAwaiter().GetResult());
}
catch
{
return false;
}
}
}
public bool HasText
{
get
{
try
{
var result = GetTextAsync().GetAwaiter().GetResult();
return !string.IsNullOrEmpty(result);
}
catch
{
return false;
}
}
}
public event EventHandler<EventArgs>? ClipboardContentChanged;
public event EventHandler<EventArgs>? ClipboardContentChanged;
public async Task<string?> GetTextAsync()
{
string text = await TryGetWithXclip();
if (text != null)
{
return text;
}
return await TryGetWithXsel();
}
public async Task<string?> GetTextAsync()
{
// Try xclip first
var result = await TryGetWithXclip();
if (result != null) return result;
public async Task SetTextAsync(string? text)
{
_lastSetText = text;
if (string.IsNullOrEmpty(text))
{
await ClearClipboard();
return;
}
if (!(await TrySetWithXclip(text)))
{
await TrySetWithXsel(text);
}
this.ClipboardContentChanged?.Invoke(this, EventArgs.Empty);
}
// Try xsel as fallback
return await TryGetWithXsel();
}
private async Task<string?> TryGetWithXclip()
{
_ = 1;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard -o",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return null;
}
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode == 0) ? output : null;
}
catch
{
return null;
}
}
public async Task SetTextAsync(string? text)
{
_lastSetText = text;
private async Task<string?> TryGetWithXsel()
{
_ = 1;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xsel",
Arguments = "--clipboard --output",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return null;
}
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode == 0) ? output : null;
}
catch
{
return null;
}
}
if (string.IsNullOrEmpty(text))
{
await ClearClipboard();
return;
}
private async Task<bool> TrySetWithXclip(string text)
{
_ = 1;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return false;
}
await process.StandardInput.WriteAsync(text);
process.StandardInput.Close();
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
// Try xclip first
var success = await TrySetWithXclip(text);
if (!success)
{
// Try xsel as fallback
await TrySetWithXsel(text);
}
private async Task<bool> TrySetWithXsel(string text)
{
_ = 1;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xsel",
Arguments = "--clipboard --input",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return false;
}
await process.StandardInput.WriteAsync(text);
process.StandardInput.Close();
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
ClipboardContentChanged?.Invoke(this, EventArgs.Empty);
}
private async Task ClearClipboard()
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard",
UseShellExecute = false,
RedirectStandardInput = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
process.StandardInput.Close();
await process.WaitForExitAsync();
}
}
catch
{
}
}
private async Task<string?> TryGetWithXclip()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard -o",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return process.ExitCode == 0 ? output : null;
}
catch
{
return null;
}
}
private async Task<string?> TryGetWithXsel()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xsel",
Arguments = "--clipboard --output",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return process.ExitCode == 0 ? output : null;
}
catch
{
return null;
}
}
private async Task<bool> TrySetWithXclip(string text)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
await process.StandardInput.WriteAsync(text);
process.StandardInput.Close();
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private async Task<bool> TrySetWithXsel(string text)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xsel",
Arguments = "--clipboard --input",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
await process.StandardInput.WriteAsync(text);
process.StandardInput.Close();
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private async Task ClearClipboard()
{
try
{
// Try xclip first
var startInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = "-selection clipboard",
UseShellExecute = false,
RedirectStandardInput = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
process.StandardInput.Close();
await process.WaitForExitAsync();
}
}
catch
{
// Ignore errors when clearing
}
}
}

View File

@@ -1,14 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public class ColorDialogResult
{
public bool Accepted { get; init; }
public float Red { get; init; }
public float Green { get; init; }
public float Blue { get; init; }
public float Alpha { get; init; }
}

View File

@@ -1,172 +0,0 @@
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
{
//IL_0007: Unknown result type (might be due to invalid IL or missing references)
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()
{
//IL_0102: Unknown result type (might be due to invalid IL or missing references)
//IL_0034: Unknown result type (might be due to invalid IL or missing references)
//IL_00f8: Unknown result type (might be due to invalid IL or missing references)
try
{
IEnumerable<NetworkInterface> enumerable = from ni in NetworkInterface.GetAllNetworkInterfaces()
where ni.OperationalStatus == OperationalStatus.Up && ni.NetworkInterfaceType != NetworkInterfaceType.Loopback
select ni;
if (!enumerable.Any())
{
_networkAccess = (NetworkAccess)1;
_connectionProfiles = Enumerable.Empty<ConnectionProfile>();
return;
}
List<ConnectionProfile> list = new List<ConnectionProfile>();
using (IEnumerator<NetworkInterface> enumerator = enumerable.GetEnumerator())
{
while (enumerator.MoveNext())
{
switch (enumerator.Current.NetworkInterfaceType)
{
case NetworkInterfaceType.Ethernet:
case NetworkInterfaceType.FastEthernetT:
case NetworkInterfaceType.FastEthernetFx:
case NetworkInterfaceType.GigabitEthernet:
list.Add((ConnectionProfile)3);
break;
case NetworkInterfaceType.Wireless80211:
list.Add((ConnectionProfile)4);
break;
case NetworkInterfaceType.Ppp:
case NetworkInterfaceType.Slip:
list.Add((ConnectionProfile)2);
break;
default:
list.Add((ConnectionProfile)0);
break;
}
}
}
_connectionProfiles = list.Distinct().ToList();
_networkAccess = (NetworkAccess)(CheckInternetAccess() ? 4 : ((!_connectionProfiles.Any()) ? 1 : 2));
}
catch
{
_networkAccess = (NetworkAccess)0;
_connectionProfiles = (IEnumerable<ConnectionProfile>)(object)new ConnectionProfile[1];
}
}
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)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0019: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Unknown result type (might be due to invalid IL or missing references)
//IL_0046: Unknown result type (might be due to invalid IL or missing references)
//IL_0050: Expected O, but got Unknown
NetworkAccess networkAccess = _networkAccess;
List<ConnectionProfile> first = _connectionProfiles.ToList();
RefreshConnectivity();
if (networkAccess != _networkAccess || !first.SequenceEqual(_connectionProfiles))
{
this.ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles));
}
}
private void OnNetworkAddressChanged(object? sender, EventArgs e)
{
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
//IL_0019: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Unknown result type (might be due to invalid IL or missing references)
//IL_0046: Unknown result type (might be due to invalid IL or missing references)
//IL_0050: Expected O, but got Unknown
NetworkAccess networkAccess = _networkAccess;
List<ConnectionProfile> first = _connectionProfiles.ToList();
RefreshConnectivity();
if (networkAccess != _networkAccess || !first.SequenceEqual(_connectionProfiles))
{
this.ConnectivityChanged?.Invoke(this, new ConnectivityChangedEventArgs(_networkAccess, _connectionProfiles));
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
NetworkChange.NetworkAvailabilityChanged -= OnNetworkAvailabilityChanged;
NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
}
}
}

View File

@@ -1,13 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum DesktopEnvironment
{
Unknown,
GNOME,
KDE,
XFCE,
MATE,
Cinnamon,
LXQt,
LXDE
}

View File

@@ -1,131 +0,0 @@
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
{
//IL_0007: Unknown result type (might be due to invalid IL or missing references)
RefreshDisplayInfo();
return _mainDisplayInfo;
}
}
public event EventHandler<DisplayInfoChangedEventArgs>? MainDisplayInfoChanged;
public DeviceDisplayService()
{
RefreshDisplayInfo();
}
private void RefreshDisplayInfo()
{
//IL_0097: Unknown result type (might be due to invalid IL or missing references)
//IL_009c: Unknown result type (might be due to invalid IL or missing references)
//IL_0067: Unknown result type (might be due to invalid IL or missing references)
//IL_006c: Unknown result type (might be due to invalid IL or missing references)
//IL_0038: Unknown result type (might be due to invalid IL or missing references)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
try
{
IntPtr intPtr = GdkNative.gdk_screen_get_default();
if (intPtr != IntPtr.Zero)
{
int num = GdkNative.gdk_screen_get_width(intPtr);
int num2 = GdkNative.gdk_screen_get_height(intPtr);
double scaleFactor = GetScaleFactor();
_mainDisplayInfo = new DisplayInfo((double)num, (double)num2, scaleFactor, (DisplayOrientation)((num <= num2) ? 1 : 2), (DisplayRotation)1, GetRefreshRate());
}
else
{
_mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, (DisplayOrientation)2, (DisplayRotation)1, 60f);
}
}
catch
{
_mainDisplayInfo = new DisplayInfo(1920.0, 1080.0, 1.0, (DisplayOrientation)2, (DisplayRotation)1, 60f);
}
}
private double GetScaleFactor()
{
string environmentVariable = Environment.GetEnvironmentVariable("GDK_SCALE");
if (!string.IsNullOrEmpty(environmentVariable) && double.TryParse(environmentVariable, out var result))
{
return result;
}
string environmentVariable2 = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR");
if (!string.IsNullOrEmpty(environmentVariable2) && double.TryParse(environmentVariable2, out result))
{
return result;
}
return 1.0;
}
private float GetRefreshRate()
{
return 60f;
}
private void SetScreenSaverInhibit(bool inhibit)
{
try
{
string value = (inhibit ? "suspend" : "resume");
IntPtr intPtr = LinuxApplication.Current?.MainWindow?.Handle ?? IntPtr.Zero;
if (intPtr != IntPtr.Zero)
{
long value2 = intPtr.ToInt64();
Process.Start(new ProcessStartInfo
{
FileName = "xdg-screensaver",
Arguments = $"{value} {value2}",
UseShellExecute = false,
CreateNoWindow = true
});
}
}
catch
{
}
}
public void OnDisplayInfoChanged()
{
//IL_0013: Unknown result type (might be due to invalid IL or missing references)
//IL_0018: Unknown result type (might be due to invalid IL or missing references)
//IL_0022: Expected O, but got Unknown
RefreshDisplayInfo();
this.MainDisplayInfoChanged?.Invoke(this, new DisplayInfoChangedEventArgs(_mainDisplayInfo));
}
}

View File

@@ -1,93 +0,0 @@
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)1;
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";
}
}
}
}

View File

@@ -1,100 +1,274 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Window;
using Microsoft.Maui.Platform.Linux.Rendering;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Supported display server types.
/// </summary>
public enum DisplayServerType
{
Auto,
X11,
Wayland
}
/// <summary>
/// Factory for creating display server connections.
/// Supports X11 and Wayland display servers.
/// </summary>
public static class DisplayServerFactory
{
private static DisplayServerType? _cachedServerType;
private static DisplayServerType? _cachedServerType;
public static DisplayServerType DetectDisplayServer()
{
if (_cachedServerType.HasValue)
{
return _cachedServerType.Value;
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")))
{
string? environmentVariable = Environment.GetEnvironmentVariable("DISPLAY");
string environmentVariable2 = Environment.GetEnvironmentVariable("MAUI_PREFER_X11");
if (!string.IsNullOrEmpty(environmentVariable) && !string.IsNullOrEmpty(environmentVariable2))
{
Console.WriteLine("[DisplayServer] XWayland detected, using X11 backend (MAUI_PREFER_X11 set)");
_cachedServerType = DisplayServerType.X11;
return DisplayServerType.X11;
}
Console.WriteLine("[DisplayServer] Wayland display detected");
_cachedServerType = DisplayServerType.Wayland;
return DisplayServerType.Wayland;
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY")))
{
Console.WriteLine("[DisplayServer] X11 display detected");
_cachedServerType = DisplayServerType.X11;
return DisplayServerType.X11;
}
Console.WriteLine("[DisplayServer] No display server detected, defaulting to X11");
_cachedServerType = DisplayServerType.X11;
return DisplayServerType.X11;
}
/// <summary>
/// Detects the current display server type.
/// </summary>
public static DisplayServerType DetectDisplayServer()
{
if (_cachedServerType.HasValue)
return _cachedServerType.Value;
public static IDisplayWindow CreateWindow(string title, int width, int height, DisplayServerType serverType = DisplayServerType.Auto)
{
if (serverType == DisplayServerType.Auto)
{
serverType = DetectDisplayServer();
}
return serverType switch
{
DisplayServerType.X11 => CreateX11Window(title, width, height),
DisplayServerType.Wayland => CreateWaylandWindow(title, width, height),
_ => CreateX11Window(title, width, height),
};
}
// Check for Wayland first (modern default)
var waylandDisplay = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY");
if (!string.IsNullOrEmpty(waylandDisplay))
{
// Check if XWayland is available - prefer it for now until native Wayland is fully tested
var xDisplay = Environment.GetEnvironmentVariable("DISPLAY");
var preferX11 = Environment.GetEnvironmentVariable("MAUI_PREFER_X11");
if (!string.IsNullOrEmpty(xDisplay) && !string.IsNullOrEmpty(preferX11))
{
Console.WriteLine("[DisplayServer] XWayland detected, using X11 backend (MAUI_PREFER_X11 set)");
_cachedServerType = DisplayServerType.X11;
return DisplayServerType.X11;
}
private static IDisplayWindow CreateX11Window(string title, int width, int height)
{
try
{
Console.WriteLine($"[DisplayServer] Creating X11 window: {title} ({width}x{height})");
return new X11DisplayWindow(title, width, height);
}
catch (Exception ex)
{
Console.WriteLine("[DisplayServer] Failed to create X11 window: " + ex.Message);
throw;
}
}
Console.WriteLine("[DisplayServer] Wayland display detected");
_cachedServerType = DisplayServerType.Wayland;
return DisplayServerType.Wayland;
}
private static IDisplayWindow CreateWaylandWindow(string title, int width, int height)
{
try
{
Console.WriteLine($"[DisplayServer] Creating Wayland window: {title} ({width}x{height})");
return new WaylandDisplayWindow(title, width, height);
}
catch (Exception ex)
{
Console.WriteLine("[DisplayServer] Failed to create Wayland window: " + ex.Message);
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY")))
{
Console.WriteLine("[DisplayServer] Falling back to X11 (XWayland)");
return CreateX11Window(title, width, height);
}
throw;
}
}
// Fall back to X11
var x11Display = Environment.GetEnvironmentVariable("DISPLAY");
if (!string.IsNullOrEmpty(x11Display))
{
Console.WriteLine("[DisplayServer] X11 display detected");
_cachedServerType = DisplayServerType.X11;
return DisplayServerType.X11;
}
public static string GetDisplayServerName(DisplayServerType serverType = DisplayServerType.Auto)
{
if (serverType == DisplayServerType.Auto)
{
serverType = DetectDisplayServer();
}
return serverType switch
{
DisplayServerType.X11 => "X11",
DisplayServerType.Wayland => "Wayland",
_ => "Unknown",
};
}
// Default to X11 and let it fail if not available
Console.WriteLine("[DisplayServer] No display server detected, defaulting to X11");
_cachedServerType = DisplayServerType.X11;
return DisplayServerType.X11;
}
/// <summary>
/// Creates a window for the specified or detected display server.
/// </summary>
public static IDisplayWindow CreateWindow(string title, int width, int height, DisplayServerType serverType = DisplayServerType.Auto)
{
if (serverType == DisplayServerType.Auto)
{
serverType = DetectDisplayServer();
}
return serverType switch
{
DisplayServerType.X11 => CreateX11Window(title, width, height),
DisplayServerType.Wayland => CreateWaylandWindow(title, width, height),
_ => CreateX11Window(title, width, height)
};
}
private static IDisplayWindow CreateX11Window(string title, int width, int height)
{
try
{
Console.WriteLine($"[DisplayServer] Creating X11 window: {title} ({width}x{height})");
return new X11DisplayWindow(title, width, height);
}
catch (Exception ex)
{
Console.WriteLine($"[DisplayServer] Failed to create X11 window: {ex.Message}");
throw;
}
}
private static IDisplayWindow CreateWaylandWindow(string title, int width, int height)
{
try
{
Console.WriteLine($"[DisplayServer] Creating Wayland window: {title} ({width}x{height})");
return new WaylandDisplayWindow(title, width, height);
}
catch (Exception ex)
{
Console.WriteLine($"[DisplayServer] Failed to create Wayland window: {ex.Message}");
// Try to fall back to X11 via XWayland
var xDisplay = Environment.GetEnvironmentVariable("DISPLAY");
if (!string.IsNullOrEmpty(xDisplay))
{
Console.WriteLine("[DisplayServer] Falling back to X11 (XWayland)");
return CreateX11Window(title, width, height);
}
throw;
}
}
/// <summary>
/// Gets a human-readable name for the display server.
/// </summary>
public static string GetDisplayServerName(DisplayServerType serverType = DisplayServerType.Auto)
{
if (serverType == DisplayServerType.Auto)
serverType = DetectDisplayServer();
return serverType switch
{
DisplayServerType.X11 => "X11",
DisplayServerType.Wayland => "Wayland",
_ => "Unknown"
};
}
}
/// <summary>
/// Common interface for display server windows.
/// </summary>
public interface IDisplayWindow : IDisposable
{
int Width { get; }
int Height { get; }
bool IsRunning { get; }
void Show();
void Hide();
void SetTitle(string title);
void Resize(int width, int height);
void ProcessEvents();
void Stop();
event EventHandler<KeyEventArgs>? KeyDown;
event EventHandler<KeyEventArgs>? KeyUp;
event EventHandler<TextInputEventArgs>? TextInput;
event EventHandler<PointerEventArgs>? PointerMoved;
event EventHandler<PointerEventArgs>? PointerPressed;
event EventHandler<PointerEventArgs>? PointerReleased;
event EventHandler<ScrollEventArgs>? Scroll;
event EventHandler? Exposed;
event EventHandler<(int Width, int Height)>? Resized;
event EventHandler? CloseRequested;
}
/// <summary>
/// X11 display window wrapper implementing the common interface.
/// </summary>
public class X11DisplayWindow : IDisplayWindow
{
private readonly X11Window _window;
public int Width => _window.Width;
public int Height => _window.Height;
public bool IsRunning => _window.IsRunning;
public event EventHandler<KeyEventArgs>? KeyDown;
public event EventHandler<KeyEventArgs>? KeyUp;
public event EventHandler<TextInputEventArgs>? TextInput;
public event EventHandler<PointerEventArgs>? PointerMoved;
public event EventHandler<PointerEventArgs>? PointerPressed;
public event EventHandler<PointerEventArgs>? PointerReleased;
public event EventHandler<ScrollEventArgs>? Scroll;
public event EventHandler? Exposed;
public event EventHandler<(int Width, int Height)>? Resized;
public event EventHandler? CloseRequested;
public X11DisplayWindow(string title, int width, int height)
{
_window = new X11Window(title, width, height);
_window.KeyDown += (s, e) => KeyDown?.Invoke(this, e);
_window.KeyUp += (s, e) => KeyUp?.Invoke(this, e);
_window.TextInput += (s, e) => TextInput?.Invoke(this, e);
_window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e);
_window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e);
_window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e);
_window.Scroll += (s, e) => Scroll?.Invoke(this, e);
_window.Exposed += (s, e) => Exposed?.Invoke(this, e);
_window.Resized += (s, e) => Resized?.Invoke(this, e);
_window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e);
}
public void Show() => _window.Show();
public void Hide() => _window.Hide();
public void SetTitle(string title) => _window.SetTitle(title);
public void Resize(int width, int height) => _window.Resize(width, height);
public void ProcessEvents() => _window.ProcessEvents();
public void Stop() => _window.Stop();
public void Dispose() => _window.Dispose();
}
/// <summary>
/// Wayland display window wrapper implementing IDisplayWindow.
/// Uses the full WaylandWindow implementation with xdg-shell protocol.
/// </summary>
public class WaylandDisplayWindow : IDisplayWindow
{
private readonly WaylandWindow _window;
public int Width => _window.Width;
public int Height => _window.Height;
public bool IsRunning => _window.IsRunning;
/// <summary>
/// Gets the pixel data pointer for rendering.
/// </summary>
public IntPtr PixelData => _window.PixelData;
/// <summary>
/// Gets the stride (bytes per row) of the pixel buffer.
/// </summary>
public int Stride => _window.Stride;
public event EventHandler<KeyEventArgs>? KeyDown;
public event EventHandler<KeyEventArgs>? KeyUp;
public event EventHandler<TextInputEventArgs>? TextInput;
public event EventHandler<PointerEventArgs>? PointerMoved;
public event EventHandler<PointerEventArgs>? PointerPressed;
public event EventHandler<PointerEventArgs>? PointerReleased;
public event EventHandler<ScrollEventArgs>? Scroll;
public event EventHandler? Exposed;
public event EventHandler<(int Width, int Height)>? Resized;
public event EventHandler? CloseRequested;
public WaylandDisplayWindow(string title, int width, int height)
{
_window = new WaylandWindow(title, width, height);
// Wire up events
_window.KeyDown += (s, e) => KeyDown?.Invoke(this, e);
_window.KeyUp += (s, e) => KeyUp?.Invoke(this, e);
_window.TextInput += (s, e) => TextInput?.Invoke(this, e);
_window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e);
_window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e);
_window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e);
_window.Scroll += (s, e) => Scroll?.Invoke(this, e);
_window.Exposed += (s, e) => Exposed?.Invoke(this, e);
_window.Resized += (s, e) => Resized?.Invoke(this, e);
_window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e);
}
public void Show() => _window.Show();
public void Hide() => _window.Hide();
public void SetTitle(string title) => _window.SetTitle(title);
public void Resize(int width, int height) => _window.Resize(width, height);
public void ProcessEvents() => _window.ProcessEvents();
public void Stop() => _window.Stop();
public void CommitFrame() => _window.CommitFrame();
public void Dispose() => _window.Dispose();
}

View File

@@ -1,8 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum DisplayServerType
{
Auto,
X11,
Wayland
}

View File

@@ -1,9 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum DragAction
{
None,
Copy,
Move,
Link
}

View File

@@ -1,16 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class DragData
{
public IntPtr SourceWindow { get; set; }
public IntPtr[] SupportedTypes { get; set; } = Array.Empty<IntPtr>();
public string? Text { get; set; }
public string[]? FilePaths { get; set; }
public object? Data { get; set; }
}

View File

@@ -1,361 +1,516 @@
using System;
using System.Collections.Generic;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Provides drag and drop functionality using the X11 XDND protocol.
/// </summary>
public class DragDropService : IDisposable
{
private struct XClientMessageEvent
{
public int type;
public ulong serial;
public bool send_event;
public IntPtr display;
public IntPtr window;
public IntPtr message_type;
public int format;
public IntPtr data0;
public IntPtr data1;
public IntPtr data2;
public IntPtr data3;
public IntPtr data4;
}
private IntPtr _display;
private IntPtr _window;
private bool _isDragging;
private DragData? _currentDragData;
private IntPtr _dragSource;
private IntPtr _dragTarget;
private bool _disposed;
private IntPtr _xdndAware;
private IntPtr _xdndEnter;
private IntPtr _xdndPosition;
private IntPtr _xdndStatus;
private IntPtr _xdndLeave;
private IntPtr _xdndDrop;
private IntPtr _xdndFinished;
private IntPtr _xdndSelection;
private IntPtr _xdndActionCopy;
private IntPtr _xdndActionMove;
private IntPtr _xdndActionLink;
private IntPtr _xdndTypeList;
private IntPtr _textPlain;
private IntPtr _textUri;
private IntPtr _applicationOctetStream;
private const int ClientMessage = 33;
private const int PropModeReplace = 0;
private static readonly IntPtr XA_ATOM = (IntPtr)4;
public bool IsDragging => _isDragging;
public event EventHandler<DragEventArgs>? DragEnter;
public event EventHandler<DragEventArgs>? DragOver;
public event EventHandler? DragLeave;
public event EventHandler<DropEventArgs>? Drop;
public void Initialize(IntPtr display, IntPtr window)
{
_display = display;
_window = window;
InitializeAtoms();
SetXdndAware();
}
private void InitializeAtoms()
{
_xdndAware = XInternAtom(_display, "XdndAware", onlyIfExists: false);
_xdndEnter = XInternAtom(_display, "XdndEnter", onlyIfExists: false);
_xdndPosition = XInternAtom(_display, "XdndPosition", onlyIfExists: false);
_xdndStatus = XInternAtom(_display, "XdndStatus", onlyIfExists: false);
_xdndLeave = XInternAtom(_display, "XdndLeave", onlyIfExists: false);
_xdndDrop = XInternAtom(_display, "XdndDrop", onlyIfExists: false);
_xdndFinished = XInternAtom(_display, "XdndFinished", onlyIfExists: false);
_xdndSelection = XInternAtom(_display, "XdndSelection", onlyIfExists: false);
_xdndActionCopy = XInternAtom(_display, "XdndActionCopy", onlyIfExists: false);
_xdndActionMove = XInternAtom(_display, "XdndActionMove", onlyIfExists: false);
_xdndActionLink = XInternAtom(_display, "XdndActionLink", onlyIfExists: false);
_xdndTypeList = XInternAtom(_display, "XdndTypeList", onlyIfExists: false);
_textPlain = XInternAtom(_display, "text/plain", onlyIfExists: false);
_textUri = XInternAtom(_display, "text/uri-list", onlyIfExists: false);
_applicationOctetStream = XInternAtom(_display, "application/octet-stream", onlyIfExists: false);
}
private void SetXdndAware()
{
int data = 5;
XChangeProperty(_display, _window, _xdndAware, XA_ATOM, 32, 0, ref data, 1);
}
public bool ProcessClientMessage(IntPtr messageType, IntPtr[] data)
{
if (messageType == _xdndEnter)
{
return HandleXdndEnter(data);
}
if (messageType == _xdndPosition)
{
return HandleXdndPosition(data);
}
if (messageType == _xdndLeave)
{
return HandleXdndLeave(data);
}
if (messageType == _xdndDrop)
{
return HandleXdndDrop(data);
}
return false;
}
private bool HandleXdndEnter(IntPtr[] data)
{
_dragSource = data[0];
_ = data[1];
bool num = ((nint)data[1] & 1) != 0;
List<IntPtr> list = new List<IntPtr>();
if (num)
{
list = GetTypeList(_dragSource);
}
else
{
for (int i = 2; i < 5; i++)
{
if (data[i] != IntPtr.Zero)
{
list.Add(data[i]);
}
}
}
_currentDragData = new DragData
{
SourceWindow = _dragSource,
SupportedTypes = list.ToArray()
};
this.DragEnter?.Invoke(this, new DragEventArgs(_currentDragData, 0, 0));
return true;
}
private bool HandleXdndPosition(IntPtr[] data)
{
if (_currentDragData == null)
{
return false;
}
int x = (int)(((nint)data[2] >> 16) & 0xFFFF);
int y = (int)((nint)data[2] & 0xFFFF);
IntPtr atom = data[4];
DragEventArgs e = new DragEventArgs(_currentDragData, x, y)
{
AllowedAction = GetDragAction(atom)
};
this.DragOver?.Invoke(this, e);
SendXdndStatus(e.Accepted, e.AcceptedAction);
return true;
}
private bool HandleXdndLeave(IntPtr[] data)
{
_currentDragData = null;
_dragSource = IntPtr.Zero;
this.DragLeave?.Invoke(this, EventArgs.Empty);
return true;
}
private bool HandleXdndDrop(IntPtr[] data)
{
if (_currentDragData == null)
{
return false;
}
uint timestamp = (uint)(nint)data[2];
string droppedData = RequestDropData(timestamp);
DropEventArgs e = new DropEventArgs(_currentDragData, droppedData);
this.Drop?.Invoke(this, e);
SendXdndFinished(e.Handled);
_currentDragData = null;
_dragSource = IntPtr.Zero;
return true;
}
private List<IntPtr> GetTypeList(IntPtr window)
{
List<IntPtr> list = new List<IntPtr>();
if (XGetWindowProperty(_display, window, _xdndTypeList, 0L, 1024L, delete: false, XA_ATOM, out var _, out var _, out var nitems, out var _, out var data) == 0 && data != IntPtr.Zero)
{
for (int i = 0; i < (int)(nint)nitems; i++)
{
IntPtr item = Marshal.ReadIntPtr(data, i * IntPtr.Size);
list.Add(item);
}
XFree(data);
}
return list;
}
private void SendXdndStatus(bool accepted, DragAction action)
{
XClientMessageEvent xevent = new XClientMessageEvent
{
type = 33,
window = _dragSource,
message_type = _xdndStatus,
format = 32
};
xevent.data0 = _window;
xevent.data1 = (IntPtr)(accepted ? 1 : 0);
xevent.data2 = (IntPtr)0;
xevent.data3 = (IntPtr)0;
xevent.data4 = GetActionAtom(action);
XSendEvent(_display, _dragSource, propagate: false, 0L, ref xevent);
XFlush(_display);
}
private void SendXdndFinished(bool accepted)
{
XClientMessageEvent xevent = new XClientMessageEvent
{
type = 33,
window = _dragSource,
message_type = _xdndFinished,
format = 32
};
xevent.data0 = _window;
xevent.data1 = (IntPtr)(accepted ? 1 : 0);
xevent.data2 = (accepted ? _xdndActionCopy : IntPtr.Zero);
XSendEvent(_display, _dragSource, propagate: false, 0L, ref xevent);
XFlush(_display);
}
private string? RequestDropData(uint timestamp)
{
IntPtr target = _textPlain;
if (_currentDragData != null)
{
IntPtr[] supportedTypes = _currentDragData.SupportedTypes;
for (int i = 0; i < supportedTypes.Length; i++)
{
if (supportedTypes[i] == _textUri)
{
target = _textUri;
break;
}
}
}
XConvertSelection(_display, _xdndSelection, target, _xdndSelection, _window, timestamp);
XFlush(_display);
return null;
}
private DragAction GetDragAction(IntPtr atom)
{
if (atom == _xdndActionCopy)
{
return DragAction.Copy;
}
if (atom == _xdndActionMove)
{
return DragAction.Move;
}
if (atom == _xdndActionLink)
{
return DragAction.Link;
}
return DragAction.None;
}
private IntPtr GetActionAtom(DragAction action)
{
return action switch
{
DragAction.Copy => _xdndActionCopy,
DragAction.Move => _xdndActionMove,
DragAction.Link => _xdndActionLink,
_ => IntPtr.Zero,
};
}
public void StartDrag(DragData data)
{
if (!_isDragging)
{
_isDragging = true;
_currentDragData = data;
}
}
public void CancelDrag()
{
_isDragging = false;
_currentDragData = null;
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
}
}
[DllImport("libX11.so.6")]
private static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists);
[DllImport("libX11.so.6")]
private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, int mode, ref int data, int nelements);
[DllImport("libX11.so.6")]
private static extern int XGetWindowProperty(IntPtr display, IntPtr window, IntPtr property, long offset, long length, bool delete, IntPtr reqType, out IntPtr actualType, out int actualFormat, out IntPtr nitems, out IntPtr bytesAfter, out IntPtr data);
[DllImport("libX11.so.6")]
private static extern int XSendEvent(IntPtr display, IntPtr window, bool propagate, long eventMask, ref XClientMessageEvent xevent);
[DllImport("libX11.so.6")]
private static extern int XConvertSelection(IntPtr display, IntPtr selection, IntPtr target, IntPtr property, IntPtr requestor, uint time);
[DllImport("libX11.so.6")]
private static extern void XFree(IntPtr ptr);
[DllImport("libX11.so.6")]
private static extern void XFlush(IntPtr display);
private nint _display;
private nint _window;
private bool _isDragging;
private DragData? _currentDragData;
private nint _dragSource;
private nint _dragTarget;
private bool _disposed;
// XDND atoms
private nint _xdndAware;
private nint _xdndEnter;
private nint _xdndPosition;
private nint _xdndStatus;
private nint _xdndLeave;
private nint _xdndDrop;
private nint _xdndFinished;
private nint _xdndSelection;
private nint _xdndActionCopy;
private nint _xdndActionMove;
private nint _xdndActionLink;
private nint _xdndTypeList;
// Common MIME types
private nint _textPlain;
private nint _textUri;
private nint _applicationOctetStream;
/// <summary>
/// Gets whether a drag operation is in progress.
/// </summary>
public bool IsDragging => _isDragging;
/// <summary>
/// Event raised when a drag enters the window.
/// </summary>
public event EventHandler<DragEventArgs>? DragEnter;
/// <summary>
/// Event raised when dragging over the window.
/// </summary>
public event EventHandler<DragEventArgs>? DragOver;
/// <summary>
/// Event raised when a drag leaves the window.
/// </summary>
public event EventHandler? DragLeave;
/// <summary>
/// Event raised when a drop occurs.
/// </summary>
public event EventHandler<DropEventArgs>? Drop;
/// <summary>
/// Initializes the drag drop service for the specified window.
/// </summary>
public void Initialize(nint display, nint window)
{
_display = display;
_window = window;
InitializeAtoms();
SetXdndAware();
}
private void InitializeAtoms()
{
_xdndAware = XInternAtom(_display, "XdndAware", false);
_xdndEnter = XInternAtom(_display, "XdndEnter", false);
_xdndPosition = XInternAtom(_display, "XdndPosition", false);
_xdndStatus = XInternAtom(_display, "XdndStatus", false);
_xdndLeave = XInternAtom(_display, "XdndLeave", false);
_xdndDrop = XInternAtom(_display, "XdndDrop", false);
_xdndFinished = XInternAtom(_display, "XdndFinished", false);
_xdndSelection = XInternAtom(_display, "XdndSelection", false);
_xdndActionCopy = XInternAtom(_display, "XdndActionCopy", false);
_xdndActionMove = XInternAtom(_display, "XdndActionMove", false);
_xdndActionLink = XInternAtom(_display, "XdndActionLink", false);
_xdndTypeList = XInternAtom(_display, "XdndTypeList", false);
_textPlain = XInternAtom(_display, "text/plain", false);
_textUri = XInternAtom(_display, "text/uri-list", false);
_applicationOctetStream = XInternAtom(_display, "application/octet-stream", false);
}
private void SetXdndAware()
{
// Set XdndAware property to indicate we support XDND version 5
int version = 5;
XChangeProperty(_display, _window, _xdndAware, XA_ATOM, 32,
PropModeReplace, ref version, 1);
}
/// <summary>
/// Processes an X11 client message for drag and drop.
/// </summary>
public bool ProcessClientMessage(nint messageType, nint[] data)
{
if (messageType == _xdndEnter)
{
return HandleXdndEnter(data);
}
else if (messageType == _xdndPosition)
{
return HandleXdndPosition(data);
}
else if (messageType == _xdndLeave)
{
return HandleXdndLeave(data);
}
else if (messageType == _xdndDrop)
{
return HandleXdndDrop(data);
}
return false;
}
private bool HandleXdndEnter(nint[] data)
{
_dragSource = data[0];
int version = (int)((data[1] >> 24) & 0xFF);
bool hasTypeList = (data[1] & 1) != 0;
var types = new List<nint>();
if (hasTypeList)
{
// Get types from XdndTypeList property
types = GetTypeList(_dragSource);
}
else
{
// Types are in the message
for (int i = 2; i < 5; i++)
{
if (data[i] != IntPtr.Zero)
{
types.Add(data[i]);
}
}
}
_currentDragData = new DragData
{
SourceWindow = _dragSource,
SupportedTypes = types.ToArray()
};
DragEnter?.Invoke(this, new DragEventArgs(_currentDragData, 0, 0));
return true;
}
private bool HandleXdndPosition(nint[] data)
{
if (_currentDragData == null) return false;
int x = (int)((data[2] >> 16) & 0xFFFF);
int y = (int)(data[2] & 0xFFFF);
nint action = data[4];
var eventArgs = new DragEventArgs(_currentDragData, x, y)
{
AllowedAction = GetDragAction(action)
};
DragOver?.Invoke(this, eventArgs);
// Send XdndStatus reply
SendXdndStatus(eventArgs.Accepted, eventArgs.AcceptedAction);
return true;
}
private bool HandleXdndLeave(nint[] data)
{
_currentDragData = null;
_dragSource = IntPtr.Zero;
DragLeave?.Invoke(this, EventArgs.Empty);
return true;
}
private bool HandleXdndDrop(nint[] data)
{
if (_currentDragData == null) return false;
uint timestamp = (uint)data[2];
// Request the data
string? droppedData = RequestDropData(timestamp);
var eventArgs = new DropEventArgs(_currentDragData, droppedData);
Drop?.Invoke(this, eventArgs);
// Send XdndFinished
SendXdndFinished(eventArgs.Handled);
_currentDragData = null;
_dragSource = IntPtr.Zero;
return true;
}
private List<nint> GetTypeList(nint window)
{
var types = new List<nint>();
nint actualType;
int actualFormat;
nint nitems, bytesAfter;
nint data;
int result = XGetWindowProperty(_display, window, _xdndTypeList, 0, 1024, false,
XA_ATOM, out actualType, out actualFormat, out nitems, out bytesAfter, out data);
if (result == 0 && data != IntPtr.Zero)
{
for (int i = 0; i < (int)nitems; i++)
{
nint atom = Marshal.ReadIntPtr(data, i * IntPtr.Size);
types.Add(atom);
}
XFree(data);
}
return types;
}
private void SendXdndStatus(bool accepted, DragAction action)
{
var ev = new XClientMessageEvent
{
type = ClientMessage,
window = _dragSource,
message_type = _xdndStatus,
format = 32
};
ev.data0 = _window;
ev.data1 = accepted ? 1 : 0;
ev.data2 = 0; // x, y of rectangle
ev.data3 = 0; // width, height of rectangle
ev.data4 = GetActionAtom(action);
XSendEvent(_display, _dragSource, false, 0, ref ev);
XFlush(_display);
}
private void SendXdndFinished(bool accepted)
{
var ev = new XClientMessageEvent
{
type = ClientMessage,
window = _dragSource,
message_type = _xdndFinished,
format = 32
};
ev.data0 = _window;
ev.data1 = accepted ? 1 : 0;
ev.data2 = accepted ? _xdndActionCopy : IntPtr.Zero;
XSendEvent(_display, _dragSource, false, 0, ref ev);
XFlush(_display);
}
private string? RequestDropData(uint timestamp)
{
// Convert selection to get the data
nint targetType = _textPlain;
// Check if text/uri-list is available
if (_currentDragData != null)
{
foreach (var type in _currentDragData.SupportedTypes)
{
if (type == _textUri)
{
targetType = _textUri;
break;
}
}
}
// Request selection conversion
XConvertSelection(_display, _xdndSelection, targetType, _xdndSelection, _window, timestamp);
XFlush(_display);
// In a real implementation, we would wait for SelectionNotify event
// and then get the data. For simplicity, we return null here.
// The actual data retrieval requires an event loop integration.
return null;
}
private DragAction GetDragAction(nint atom)
{
if (atom == _xdndActionCopy) return DragAction.Copy;
if (atom == _xdndActionMove) return DragAction.Move;
if (atom == _xdndActionLink) return DragAction.Link;
return DragAction.None;
}
private nint GetActionAtom(DragAction action)
{
return action switch
{
DragAction.Copy => _xdndActionCopy,
DragAction.Move => _xdndActionMove,
DragAction.Link => _xdndActionLink,
_ => IntPtr.Zero
};
}
/// <summary>
/// Starts a drag operation.
/// </summary>
public void StartDrag(DragData data)
{
if (_isDragging) return;
_isDragging = true;
_currentDragData = data;
// Set the drag cursor and initiate the drag
// This requires integration with the X11 event loop
}
/// <summary>
/// Cancels the current drag operation.
/// </summary>
public void CancelDrag()
{
_isDragging = false;
_currentDragData = null;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
}
#region X11 Interop
private const int ClientMessage = 33;
private const int PropModeReplace = 0;
private static readonly nint XA_ATOM = (nint)4;
[StructLayout(LayoutKind.Sequential)]
private struct XClientMessageEvent
{
public int type;
public ulong serial;
public bool send_event;
public nint display;
public nint window;
public nint message_type;
public int format;
public nint data0;
public nint data1;
public nint data2;
public nint data3;
public nint data4;
}
[DllImport("libX11.so.6")]
private static extern nint XInternAtom(nint display, string atomName, bool onlyIfExists);
[DllImport("libX11.so.6")]
private static extern int XChangeProperty(nint display, nint window, nint property, nint type,
int format, int mode, ref int data, int nelements);
[DllImport("libX11.so.6")]
private static extern int XGetWindowProperty(nint display, nint window, nint property,
long offset, long length, bool delete, nint reqType,
out nint actualType, out int actualFormat, out nint nitems, out nint bytesAfter, out nint data);
[DllImport("libX11.so.6")]
private static extern int XSendEvent(nint display, nint window, bool propagate, long eventMask, ref XClientMessageEvent xevent);
[DllImport("libX11.so.6")]
private static extern int XConvertSelection(nint display, nint selection, nint target, nint property, nint requestor, uint time);
[DllImport("libX11.so.6")]
private static extern void XFree(nint ptr);
[DllImport("libX11.so.6")]
private static extern void XFlush(nint display);
#endregion
}
/// <summary>
/// Contains data for a drag operation.
/// </summary>
public class DragData
{
/// <summary>
/// Gets or sets the source window.
/// </summary>
public nint SourceWindow { get; set; }
/// <summary>
/// Gets or sets the supported MIME types.
/// </summary>
public nint[] SupportedTypes { get; set; } = Array.Empty<nint>();
/// <summary>
/// Gets or sets the text data.
/// </summary>
public string? Text { get; set; }
/// <summary>
/// Gets or sets the file paths.
/// </summary>
public string[]? FilePaths { get; set; }
/// <summary>
/// Gets or sets custom data.
/// </summary>
public object? Data { get; set; }
}
/// <summary>
/// Event args for drag events.
/// </summary>
public class DragEventArgs : EventArgs
{
/// <summary>
/// Gets the drag data.
/// </summary>
public DragData Data { get; }
/// <summary>
/// Gets the X coordinate.
/// </summary>
public int X { get; }
/// <summary>
/// Gets the Y coordinate.
/// </summary>
public int Y { get; }
/// <summary>
/// Gets or sets whether the drop is accepted.
/// </summary>
public bool Accepted { get; set; }
/// <summary>
/// Gets or sets the allowed action.
/// </summary>
public DragAction AllowedAction { get; set; }
/// <summary>
/// Gets or sets the accepted action.
/// </summary>
public DragAction AcceptedAction { get; set; } = DragAction.Copy;
public DragEventArgs(DragData data, int x, int y)
{
Data = data;
X = x;
Y = y;
}
}
/// <summary>
/// Event args for drop events.
/// </summary>
public class DropEventArgs : EventArgs
{
/// <summary>
/// Gets the drag data.
/// </summary>
public DragData Data { get; }
/// <summary>
/// Gets the dropped data as string.
/// </summary>
public string? DroppedData { get; }
/// <summary>
/// Gets or sets whether the drop was handled.
/// </summary>
public bool Handled { get; set; }
public DropEventArgs(DragData data, string? droppedData)
{
Data = data;
DroppedData = droppedData;
}
}
/// <summary>
/// Drag action types.
/// </summary>
public enum DragAction
{
None,
Copy,
Move,
Link
}

View File

@@ -1,25 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class DragEventArgs : EventArgs
{
public DragData Data { get; }
public int X { get; }
public int Y { get; }
public bool Accepted { get; set; }
public DragAction AllowedAction { get; set; }
public DragAction AcceptedAction { get; set; } = DragAction.Copy;
public DragEventArgs(DragData data, int x, int y)
{
Data = data;
X = x;
Y = y;
}
}

View File

@@ -1,18 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class DropEventArgs : EventArgs
{
public DragData Data { get; }
public string? DroppedData { get; }
public bool Handled { get; set; }
public DropEventArgs(DragData data, string? droppedData)
{
Data = data;
DroppedData = droppedData;
}
}

View File

@@ -1,98 +1,113 @@
using System;
using System.Collections.Generic;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel.Communication;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux email implementation using mailto: URI.
/// </summary>
public class EmailService : IEmail
{
public bool IsComposeSupported => true;
public bool IsComposeSupported => true;
public async Task ComposeAsync()
{
await ComposeAsync(new EmailMessage());
}
public async Task ComposeAsync()
{
await ComposeAsync(new EmailMessage());
}
public async Task ComposeAsync(string subject, string body, params string[] to)
{
EmailMessage val = new EmailMessage
{
Subject = subject,
Body = body
};
if (to != null && to.Length != 0)
{
val.To = new List<string>(to);
}
await ComposeAsync(val);
}
public async Task ComposeAsync(string subject, string body, params string[] to)
{
var message = new EmailMessage
{
Subject = subject,
Body = body
};
public async Task ComposeAsync(EmailMessage? message)
{
if (message == null)
{
throw new ArgumentNullException("message");
}
string text = BuildMailtoUri(message);
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = "\"" + text + "\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception innerException)
{
throw new InvalidOperationException("Failed to open email client", innerException);
}
}
if (to != null && to.Length > 0)
{
message.To = new List<string>(to);
}
private static string BuildMailtoUri(EmailMessage? message)
{
StringBuilder stringBuilder = new StringBuilder("mailto:");
List<string> to = message.To;
if (to != null && to.Count > 0)
{
stringBuilder.Append(string.Join(",", message.To.Select(Uri.EscapeDataString)));
}
List<string> list = new List<string>();
if (!string.IsNullOrEmpty(message.Subject))
{
list.Add("subject=" + Uri.EscapeDataString(message.Subject));
}
if (!string.IsNullOrEmpty(message.Body))
{
list.Add("body=" + Uri.EscapeDataString(message.Body));
}
List<string> cc = message.Cc;
if (cc != null && cc.Count > 0)
{
list.Add("cc=" + string.Join(",", message.Cc.Select(Uri.EscapeDataString)));
}
List<string> bcc = message.Bcc;
if (bcc != null && bcc.Count > 0)
{
list.Add("bcc=" + string.Join(",", message.Bcc.Select(Uri.EscapeDataString)));
}
if (list.Count > 0)
{
stringBuilder.Append('?');
stringBuilder.Append(string.Join("&", list));
}
return stringBuilder.ToString();
}
await ComposeAsync(message);
}
public async Task ComposeAsync(EmailMessage? message)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
var mailto = BuildMailtoUri(message);
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = $"\"{mailto}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to open email client", ex);
}
}
private static string BuildMailtoUri(EmailMessage? message)
{
var sb = new StringBuilder("mailto:");
// Add recipients
if (message.To?.Count > 0)
{
sb.Append(string.Join(",", message.To.Select(Uri.EscapeDataString)));
}
var queryParams = new List<string>();
// Add subject
if (!string.IsNullOrEmpty(message.Subject))
{
queryParams.Add($"subject={Uri.EscapeDataString(message.Subject)}");
}
// Add body
if (!string.IsNullOrEmpty(message.Body))
{
queryParams.Add($"body={Uri.EscapeDataString(message.Body)}");
}
// Add CC
if (message.Cc?.Count > 0)
{
queryParams.Add($"cc={string.Join(",", message.Cc.Select(Uri.EscapeDataString))}");
}
// Add BCC
if (message.Bcc?.Count > 0)
{
queryParams.Add($"bcc={string.Join(",", message.Bcc.Select(Uri.EscapeDataString))}");
}
if (queryParams.Count > 0)
{
sb.Append('?');
sb.Append(string.Join("&", queryParams));
}
return sb.ToString();
}
}

View File

@@ -1,329 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Microsoft.Maui.Platform.Linux.Services;
public class Fcitx5InputMethodService : IInputMethodService, IDisposable
{
private IInputContext? _currentContext;
private string _preEditText = string.Empty;
private int _preEditCursorPosition;
private bool _isActive;
private bool _disposed;
private Process? _dBusMonitor;
private string? _inputContextPath;
public bool IsActive => _isActive;
public string PreEditText => _preEditText;
public int PreEditCursorPosition => _preEditCursorPosition;
public event EventHandler<TextCommittedEventArgs>? TextCommitted;
public event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
public event EventHandler? PreEditEnded;
public void Initialize(IntPtr windowHandle)
{
try
{
string text = RunDBusCommand("call --session --dest org.fcitx.Fcitx5 --object-path /org/freedesktop/portal/inputmethod --method org.fcitx.Fcitx.InputMethod1.CreateInputContext \"maui-linux\" \"\"");
if (!string.IsNullOrEmpty(text) && text.Contains("/"))
{
int num = text.IndexOf("'/");
int num2 = text.IndexOf("'", num + 1);
if (num >= 0 && num2 > num)
{
_inputContextPath = text.Substring(num + 1, num2 - num - 1);
Console.WriteLine("Fcitx5InputMethodService: Created context at " + _inputContextPath);
StartMonitoring();
}
}
else
{
Console.WriteLine("Fcitx5InputMethodService: Failed to create input context");
}
}
catch (Exception ex)
{
Console.WriteLine("Fcitx5InputMethodService: Initialization failed - " + ex.Message);
}
}
private void StartMonitoring()
{
if (string.IsNullOrEmpty(_inputContextPath))
{
return;
}
Task.Run(async delegate
{
_ = 2;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "dbus-monitor",
Arguments = "--session \"path='" + _inputContextPath + "'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
_dBusMonitor = Process.Start(startInfo);
if (_dBusMonitor != null)
{
StreamReader reader = _dBusMonitor.StandardOutput;
while (!_disposed && !_dBusMonitor.HasExited)
{
string text = await reader.ReadLineAsync();
if (text == null)
{
break;
}
if (text.Contains("CommitString"))
{
await ProcessCommitSignal(reader);
}
else if (text.Contains("UpdatePreedit"))
{
await ProcessPreeditSignal(reader);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine("Fcitx5InputMethodService: Monitor error - " + ex.Message);
}
});
}
private async Task ProcessCommitSignal(StreamReader reader)
{
try
{
for (int i = 0; i < 5; i++)
{
string text = await reader.ReadLineAsync();
if (text == null)
{
break;
}
if (text.Contains("string"))
{
Match match = Regex.Match(text, "string\\s+\"([^\"]*)\"");
if (match.Success)
{
string value = match.Groups[1].Value;
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
this.TextCommitted?.Invoke(this, new TextCommittedEventArgs(value));
_currentContext?.OnTextCommitted(value);
break;
}
}
}
}
catch
{
}
}
private async Task ProcessPreeditSignal(StreamReader reader)
{
try
{
for (int i = 0; i < 10; i++)
{
string text = await reader.ReadLineAsync();
if (text == null)
{
break;
}
if (text.Contains("string"))
{
Match match = Regex.Match(text, "string\\s+\"([^\"]*)\"");
if (match.Success)
{
_preEditText = match.Groups[1].Value;
_isActive = !string.IsNullOrEmpty(_preEditText);
this.PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, new List<PreEditAttribute>()));
_currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition);
break;
}
}
}
}
catch
{
}
}
public void SetFocus(IInputContext? context)
{
_currentContext = context;
if (!string.IsNullOrEmpty(_inputContextPath))
{
if (context != null)
{
RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.FocusIn");
}
else
{
RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.FocusOut");
}
}
}
public void SetCursorLocation(int x, int y, int width, int height)
{
if (!string.IsNullOrEmpty(_inputContextPath))
{
RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.SetCursorRect {x} {y} {width} {height}");
}
}
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown)
{
if (string.IsNullOrEmpty(_inputContextPath))
{
return false;
}
uint num = ConvertModifiers(modifiers);
if (!isKeyDown)
{
num |= 0x40000000;
}
return RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.ProcessKeyEvent {keyCode} {keyCode} {num} {(isKeyDown ? "true" : "false")} 0")?.Contains("true") ?? false;
}
private uint ConvertModifiers(KeyModifiers modifiers)
{
uint num = 0u;
if (modifiers.HasFlag(KeyModifiers.Shift))
{
num |= 1;
}
if (modifiers.HasFlag(KeyModifiers.CapsLock))
{
num |= 2;
}
if (modifiers.HasFlag(KeyModifiers.Control))
{
num |= 4;
}
if (modifiers.HasFlag(KeyModifiers.Alt))
{
num |= 8;
}
if (modifiers.HasFlag(KeyModifiers.Super))
{
num |= 0x40;
}
return num;
}
public void Reset()
{
if (!string.IsNullOrEmpty(_inputContextPath))
{
RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.Reset");
}
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
this.PreEditEnded?.Invoke(this, EventArgs.Empty);
_currentContext?.OnPreEditEnded();
}
public void Shutdown()
{
Dispose();
}
private string? RunDBusCommand(string args)
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "gdbus",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
if (process == null)
{
return null;
}
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return result;
}
catch
{
return null;
}
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
try
{
_dBusMonitor?.Kill();
_dBusMonitor?.Dispose();
}
catch
{
}
if (!string.IsNullOrEmpty(_inputContextPath))
{
RunDBusCommand($"call --session --dest org.fcitx.Fcitx5 --object-path {_inputContextPath} --method org.fcitx.Fcitx.InputContext1.Destroy");
}
}
}
public static bool IsAvailable()
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "gdbus",
Arguments = "introspect --session --dest org.fcitx.Fcitx5 --object-path /org/freedesktop/portal/inputmethod",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
if (process == null)
{
return false;
}
process.WaitForExit(1000);
return process.ExitCode == 0;
}
catch
{
return false;
}
}
}

View File

@@ -1,22 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class FileDialogResult
{
public bool Accepted { get; init; }
public string[] SelectedFiles { get; init; } = Array.Empty<string>();
public string? SelectedFile
{
get
{
if (SelectedFiles.Length == 0)
{
return null;
}
return SelectedFiles[0];
}
}
}

View File

@@ -1,213 +1,212 @@
using System;
using System.Collections.Generic;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux file picker implementation using zenity or kdialog.
/// </summary>
public class FilePickerService : IFilePicker
{
private enum DialogTool
{
None,
Zenity,
Kdialog
}
private enum DialogTool
{
None,
Zenity,
Kdialog
}
private static DialogTool? _availableTool;
private static DialogTool? _availableTool;
private static DialogTool GetAvailableTool()
{
if (_availableTool.HasValue)
{
return _availableTool.Value;
}
if (IsToolAvailable("zenity"))
{
_availableTool = DialogTool.Zenity;
return DialogTool.Zenity;
}
if (IsToolAvailable("kdialog"))
{
_availableTool = DialogTool.Kdialog;
return DialogTool.Kdialog;
}
_availableTool = DialogTool.None;
return DialogTool.None;
}
private static DialogTool GetAvailableTool()
{
if (_availableTool.HasValue)
return _availableTool.Value;
private static bool IsToolAvailable(string tool)
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "which",
Arguments = tool,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
});
process?.WaitForExit(1000);
return process != null && process.ExitCode == 0;
}
catch
{
return false;
}
}
// Check for zenity first (GNOME/GTK)
if (IsToolAvailable("zenity"))
{
_availableTool = DialogTool.Zenity;
return DialogTool.Zenity;
}
public Task<FileResult?> PickAsync(PickOptions? options = null)
{
return PickInternalAsync(options, multiple: false);
}
// Check for kdialog (KDE)
if (IsToolAvailable("kdialog"))
{
_availableTool = DialogTool.Kdialog;
return DialogTool.Kdialog;
}
public Task<IEnumerable<FileResult>> PickMultipleAsync(PickOptions? options = null)
{
return PickMultipleInternalAsync(options);
}
_availableTool = DialogTool.None;
return DialogTool.None;
}
private async Task<FileResult?> PickInternalAsync(PickOptions? options, bool multiple)
{
return (await PickMultipleInternalAsync(options, multiple)).FirstOrDefault();
}
private static bool IsToolAvailable(string tool)
{
try
{
var psi = new ProcessStartInfo
{
FileName = "which",
Arguments = tool,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
private Task<IEnumerable<FileResult>> PickMultipleInternalAsync(PickOptions? options, bool multiple = true)
{
return Task.Run(delegate
{
DialogTool availableTool = GetAvailableTool();
string arguments;
switch (availableTool)
{
case DialogTool.None:
{
Console.WriteLine("No file dialog available. Please enter file path:");
string text = Console.ReadLine();
if (!string.IsNullOrEmpty(text) && File.Exists(text))
{
return (IEnumerable<FileResult>)(object)new LinuxFileResult[1]
{
new LinuxFileResult(text)
};
}
return Array.Empty<FileResult>();
}
case DialogTool.Zenity:
arguments = BuildZenityArguments(options, multiple);
break;
default:
arguments = BuildKdialogArguments(options, multiple);
break;
}
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = ((availableTool == DialogTool.Zenity) ? "zenity" : "kdialog"),
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
using Process process = Process.Start(startInfo);
if (process == null)
{
return Array.Empty<FileResult>();
}
string text2 = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
if (process.ExitCode != 0 || string.IsNullOrEmpty(text2))
{
return Array.Empty<FileResult>();
}
char separator = ((availableTool == DialogTool.Zenity) ? '|' : '\n');
return (from p in text2.Split(separator, StringSplitOptions.RemoveEmptyEntries).Where(File.Exists)
select (FileResult)(object)new LinuxFileResult(p)).ToArray();
}
catch
{
return Array.Empty<FileResult>();
}
});
}
using var process = Process.Start(psi);
process?.WaitForExit(1000);
return process?.ExitCode == 0;
}
catch
{
return false;
}
}
private string BuildZenityArguments(PickOptions? options, bool multiple)
{
StringBuilder stringBuilder = new StringBuilder("--file-selection");
if (multiple)
{
stringBuilder.Append(" --multiple --separator='|'");
}
if (!string.IsNullOrEmpty((options != null) ? options.PickerTitle : null))
{
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2);
handler.AppendLiteral(" --title=\"");
handler.AppendFormatted(EscapeArgument(options.PickerTitle));
handler.AppendLiteral("\"");
stringBuilder3.Append(ref handler);
}
if (((options != null) ? options.FileTypes : null) != null)
{
foreach (string item in options.FileTypes.Value)
{
string value = (item.StartsWith(".") ? item : ("." + item));
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2);
handler.AppendLiteral(" --file-filter='*");
handler.AppendFormatted(value);
handler.AppendLiteral("'");
stringBuilder4.Append(ref handler);
}
}
return stringBuilder.ToString();
}
public Task<FileResult?> PickAsync(PickOptions? options = null)
{
return PickInternalAsync(options, false);
}
private string BuildKdialogArguments(PickOptions? options, bool multiple)
{
StringBuilder stringBuilder = new StringBuilder("--getopenfilename");
if (multiple)
{
stringBuilder.Insert(0, "--multiple ");
}
stringBuilder.Append(" .");
if (((options != null) ? options.FileTypes : null) != null)
{
string value = string.Join(" ", options.FileTypes.Value.Select((string e) => (!e.StartsWith(".")) ? ("*." + e) : ("*" + e)));
if (!string.IsNullOrEmpty(value))
{
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder2);
handler.AppendLiteral(" \"");
handler.AppendFormatted(value);
handler.AppendLiteral("\"");
stringBuilder3.Append(ref handler);
}
}
if (!string.IsNullOrEmpty((options != null) ? options.PickerTitle : null))
{
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder2);
handler.AppendLiteral(" --title \"");
handler.AppendFormatted(EscapeArgument(options.PickerTitle));
handler.AppendLiteral("\"");
stringBuilder4.Append(ref handler);
}
return stringBuilder.ToString();
}
public Task<IEnumerable<FileResult>> PickMultipleAsync(PickOptions? options = null)
{
return PickMultipleInternalAsync(options);
}
private static string EscapeArgument(string arg)
{
return arg.Replace("\"", "\\\"").Replace("'", "\\'");
}
private async Task<FileResult?> PickInternalAsync(PickOptions? options, bool multiple)
{
var results = await PickMultipleInternalAsync(options, multiple);
return results.FirstOrDefault();
}
private Task<IEnumerable<FileResult>> PickMultipleInternalAsync(PickOptions? options, bool multiple = true)
{
return Task.Run<IEnumerable<FileResult>>(() =>
{
var tool = GetAvailableTool();
if (tool == DialogTool.None)
{
// Fall back to console path input
Console.WriteLine("No file dialog available. Please enter file path:");
var path = Console.ReadLine();
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
return new[] { new LinuxFileResult(path) };
}
return Array.Empty<FileResult>();
}
string arguments;
if (tool == DialogTool.Zenity)
{
arguments = BuildZenityArguments(options, multiple);
}
else
{
arguments = BuildKdialogArguments(options, multiple);
}
var psi = new ProcessStartInfo
{
FileName = tool == DialogTool.Zenity ? "zenity" : "kdialog",
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
using var process = Process.Start(psi);
if (process == null)
return Array.Empty<FileResult>();
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
if (process.ExitCode != 0 || string.IsNullOrEmpty(output))
return Array.Empty<FileResult>();
// Parse output (paths separated by | for zenity, newlines for kdialog)
var separator = tool == DialogTool.Zenity ? '|' : '\n';
var paths = output.Split(separator, StringSplitOptions.RemoveEmptyEntries);
return paths
.Where(File.Exists)
.Select(p => (FileResult)new LinuxFileResult(p))
.ToArray();
}
catch
{
return Array.Empty<FileResult>();
}
});
}
private string BuildZenityArguments(PickOptions? options, bool multiple)
{
var sb = new StringBuilder("--file-selection");
if (multiple)
sb.Append(" --multiple --separator='|'");
if (!string.IsNullOrEmpty(options?.PickerTitle))
sb.Append($" --title=\"{EscapeArgument(options.PickerTitle)}\"");
if (options?.FileTypes != null)
{
foreach (var ext in options.FileTypes.Value)
{
var extension = ext.StartsWith(".") ? ext : $".{ext}";
sb.Append($" --file-filter='*{extension}'");
}
}
return sb.ToString();
}
private string BuildKdialogArguments(PickOptions? options, bool multiple)
{
var sb = new StringBuilder("--getopenfilename");
if (multiple)
sb.Insert(0, "--multiple ");
sb.Append(" .");
if (options?.FileTypes != null)
{
var extensions = string.Join(" ", options.FileTypes.Value.Select(e =>
e.StartsWith(".") ? $"*{e}" : $"*.{e}"));
if (!string.IsNullOrEmpty(extensions))
{
sb.Append($" \"{extensions}\"");
}
}
if (!string.IsNullOrEmpty(options?.PickerTitle))
sb.Append($" --title \"{EscapeArgument(options.PickerTitle)}\"");
return sb.ToString();
}
private static string EscapeArgument(string arg)
{
return arg.Replace("\"", "\\\"").Replace("'", "\\'");
}
}
/// <summary>
/// Linux-specific FileResult implementation.
/// </summary>
internal class LinuxFileResult : FileResult
{
public LinuxFileResult(string fullPath) : base(fullPath)
{
}
}

View File

@@ -1,8 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public class FolderPickerOptions
{
public string? Title { get; set; }
public string? InitialDirectory { get; set; }
}

View File

@@ -1,13 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public class FolderPickerResult
{
public FolderResult? Folder { get; }
public bool WasSuccessful => Folder != null;
public FolderPickerResult(FolderResult? folder)
{
Folder = folder;
}
}

View File

@@ -1,121 +1,129 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux folder picker utility using zenity or kdialog.
/// This is a standalone service as MAUI core does not define IFolderPicker.
/// </summary>
public class FolderPickerService
{
public async Task<string?> PickFolderAsync(string? initialDirectory = null, CancellationToken cancellationToken = default(CancellationToken))
{
_ = 1;
try
{
string text = await TryZenityFolderPicker(initialDirectory, cancellationToken);
if (text != null)
{
return text;
}
text = await TryKdialogFolderPicker(initialDirectory, cancellationToken);
if (text != null)
{
return text;
}
return null;
}
catch (OperationCanceledException)
{
return null;
}
catch
{
return null;
}
}
public async Task<string?> PickFolderAsync(string? initialDirectory = null, CancellationToken cancellationToken = default)
{
try
{
// Try zenity first (GNOME)
var result = await TryZenityFolderPicker(initialDirectory, cancellationToken);
if (result != null)
{
return result;
}
private async Task<string?> TryZenityFolderPicker(string? initialDirectory, CancellationToken cancellationToken)
{
_ = 1;
try
{
string text = "--file-selection --directory";
if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory))
{
text = text + " --filename=\"" + initialDirectory + "/\"";
}
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = text,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return null;
}
string output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
{
string text2 = output.Trim();
if (Directory.Exists(text2))
{
return text2;
}
}
return null;
}
catch
{
return null;
}
}
// Fall back to kdialog (KDE)
result = await TryKdialogFolderPicker(initialDirectory, cancellationToken);
if (result != null)
{
return result;
}
private async Task<string?> TryKdialogFolderPicker(string? initialDirectory, CancellationToken cancellationToken)
{
_ = 1;
try
{
string text = "--getexistingdirectory";
if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory))
{
text = text + " \"" + initialDirectory + "\"";
}
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "kdialog",
Arguments = text,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return null;
}
string output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
{
string text2 = output.Trim();
if (Directory.Exists(text2))
{
return text2;
}
}
return null;
}
catch
{
return null;
}
}
return null;
}
catch (OperationCanceledException)
{
return null;
}
catch
{
return null;
}
}
private async Task<string?> TryZenityFolderPicker(string? initialDirectory, CancellationToken cancellationToken)
{
try
{
var args = "--file-selection --directory";
if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory))
{
args += $" --filename=\"{initialDirectory}/\"";
}
var startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
{
var path = output.Trim();
if (Directory.Exists(path))
{
return path;
}
}
return null;
}
catch
{
return null;
}
}
private async Task<string?> TryKdialogFolderPicker(string? initialDirectory, CancellationToken cancellationToken)
{
try
{
var args = "--getexistingdirectory";
if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory))
{
args += $" \"{initialDirectory}\"";
}
var startInfo = new ProcessStartInfo
{
FileName = "kdialog",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return null;
var output = await process.StandardOutput.ReadToEndAsync(cancellationToken);
await process.WaitForExitAsync(cancellationToken);
if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output))
{
var path = output.Trim();
if (Directory.Exists(path))
{
return path;
}
}
return null;
}
catch
{
return null;
}
}
}

View File

@@ -1,15 +0,0 @@
using System.IO;
namespace Microsoft.Maui.Platform.Linux.Services;
public class FolderResult
{
public string Path { get; }
public string Name => System.IO.Path.GetFileName(Path) ?? Path;
public FolderResult(string path)
{
Path = path;
}
}

View File

@@ -1,186 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
public class FontFallbackManager
{
private static FontFallbackManager? _instance;
private static readonly object _lock = new object();
private readonly string[] _fallbackFonts = new string[27]
{
"Noto Sans", "DejaVu Sans", "Liberation Sans", "FreeSans", "Noto Color Emoji", "Noto Emoji", "Symbola", "Segoe UI Emoji", "Noto Sans CJK SC", "Noto Sans CJK TC",
"Noto Sans CJK JP", "Noto Sans CJK KR", "WenQuanYi Micro Hei", "WenQuanYi Zen Hei", "Droid Sans Fallback", "Noto Sans Arabic", "Noto Naskh Arabic", "DejaVu Sans", "Noto Sans Devanagari", "Noto Sans Tamil",
"Noto Sans Bengali", "Noto Sans Telugu", "Noto Sans Thai", "Loma", "Noto Sans Hebrew", "Sans", "sans-serif"
};
private readonly Dictionary<string, SKTypeface?> _typefaceCache = new Dictionary<string, SKTypeface>();
private readonly Dictionary<(int codepoint, string preferredFont), SKTypeface?> _glyphCache = new Dictionary<(int, string), SKTypeface>();
public static FontFallbackManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new FontFallbackManager();
}
}
}
return _instance;
}
}
private FontFallbackManager()
{
foreach (string item in _fallbackFonts.Take(10))
{
GetCachedTypeface(item);
}
}
public SKTypeface GetTypefaceForCodepoint(int codepoint, SKTypeface preferred)
{
(int, string) key = (codepoint, preferred.FamilyName);
if (_glyphCache.TryGetValue(key, out SKTypeface value))
{
return value ?? preferred;
}
if (TypefaceContainsGlyph(preferred, codepoint))
{
_glyphCache[key] = preferred;
return preferred;
}
string[] fallbackFonts = _fallbackFonts;
foreach (string fontFamily in fallbackFonts)
{
SKTypeface cachedTypeface = GetCachedTypeface(fontFamily);
if (cachedTypeface != null && TypefaceContainsGlyph(cachedTypeface, codepoint))
{
_glyphCache[key] = cachedTypeface;
return cachedTypeface;
}
}
_glyphCache[key] = null;
return preferred;
}
public SKTypeface GetTypefaceForText(string text, SKTypeface preferred)
{
if (string.IsNullOrEmpty(text))
{
return preferred;
}
foreach (Rune item in text.EnumerateRunes())
{
if (item.Value > 127)
{
return GetTypefaceForCodepoint(item.Value, preferred);
}
}
return preferred;
}
public List<TextRun> ShapeTextWithFallback(string text, SKTypeface preferred)
{
List<TextRun> list = new List<TextRun>();
if (string.IsNullOrEmpty(text))
{
return list;
}
_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2 = new _003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder();
SKTypeface val = null;
int startIndex = 0;
int num = 0;
foreach (Rune item in text.EnumerateRunes())
{
SKTypeface typefaceForCodepoint = GetTypefaceForCodepoint(item.Value, preferred);
if (val == null)
{
val = typefaceForCodepoint;
}
else if (typefaceForCodepoint.FamilyName != val.FamilyName)
{
if (_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Length > 0)
{
list.Add(new TextRun(_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.ToString(), val, startIndex));
}
_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Clear();
val = typefaceForCodepoint;
startIndex = num;
}
_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Append(item.ToString());
num += item.Utf16SequenceLength;
}
if (_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.Length > 0 && val != null)
{
list.Add(new TextRun(_003CFontFallbackManager_003EFAC9D2911A2850E174CCA7662C668F37C2FBBA325CAF5C11AFE3FA59C16CC64ED__StringBuilder2.ToString(), val, startIndex));
}
return list;
}
public bool IsFontAvailable(string fontFamily)
{
SKTypeface cachedTypeface = GetCachedTypeface(fontFamily);
if (cachedTypeface != null)
{
return cachedTypeface.FamilyName.Equals(fontFamily, StringComparison.OrdinalIgnoreCase);
}
return false;
}
public IEnumerable<string> GetAvailableFallbackFonts()
{
string[] fallbackFonts = _fallbackFonts;
foreach (string text in fallbackFonts)
{
if (IsFontAvailable(text))
{
yield return text;
}
}
}
private SKTypeface? GetCachedTypeface(string fontFamily)
{
if (_typefaceCache.TryGetValue(fontFamily, out SKTypeface value))
{
return value;
}
SKTypeface val = SKTypeface.FromFamilyName(fontFamily);
if (val != null && !val.FamilyName.Equals(fontFamily, StringComparison.OrdinalIgnoreCase))
{
val = null;
}
_typefaceCache[fontFamily] = val;
return val;
}
private bool TypefaceContainsGlyph(SKTypeface typeface, int codepoint)
{
//IL_0010: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Expected O, but got Unknown
SKFont val = new SKFont(typeface, 12f, 1f, 0f);
try
{
ushort[] array = new ushort[1];
string text = char.ConvertFromUtf32(codepoint);
val.GetGlyphs(text, (Span<ushort>)array);
return array[0] != 0;
}
finally
{
((IDisposable)val)?.Dispose();
}
}
}

View File

@@ -1,296 +1,393 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Concurrent;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Provides global hotkey registration and handling using X11.
/// </summary>
public class GlobalHotkeyService : IDisposable
{
[StructLayout(LayoutKind.Explicit)]
private struct XEvent
{
[FieldOffset(0)]
public int type;
private nint _display;
private nint _rootWindow;
private readonly ConcurrentDictionary<int, HotkeyRegistration> _registrations = new();
private int _nextId = 1;
private bool _disposed;
private Thread? _eventThread;
private bool _isListening;
[FieldOffset(0)]
public XKeyEvent KeyEvent;
}
/// <summary>
/// Event raised when a registered hotkey is pressed.
/// </summary>
public event EventHandler<HotkeyEventArgs>? HotkeyPressed;
private struct XKeyEvent
{
public int type;
/// <summary>
/// Initializes the global hotkey service.
/// </summary>
public void Initialize()
{
_display = XOpenDisplay(IntPtr.Zero);
if (_display == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to open X display");
}
public ulong serial;
_rootWindow = XDefaultRootWindow(_display);
public bool send_event;
// Start listening for hotkeys in background
_isListening = true;
_eventThread = new Thread(ListenForHotkeys)
{
IsBackground = true,
Name = "GlobalHotkeyListener"
};
_eventThread.Start();
}
public IntPtr display;
/// <summary>
/// Registers a global hotkey.
/// </summary>
/// <param name="key">The key code.</param>
/// <param name="modifiers">The modifier keys.</param>
/// <returns>A registration ID that can be used to unregister.</returns>
public int Register(HotkeyKey key, HotkeyModifiers modifiers)
{
if (_display == IntPtr.Zero)
{
throw new InvalidOperationException("Service not initialized");
}
public IntPtr window;
int keyCode = XKeysymToKeycode(_display, (nint)key);
if (keyCode == 0)
{
throw new ArgumentException($"Invalid key: {key}");
}
public IntPtr root;
uint modifierMask = GetModifierMask(modifiers);
public IntPtr subwindow;
// Register for all modifier combinations (with/without NumLock, CapsLock)
uint[] masks = GetModifierCombinations(modifierMask);
public ulong time;
foreach (var mask in masks)
{
int result = XGrabKey(_display, keyCode, mask, _rootWindow, true, GrabModeAsync, GrabModeAsync);
if (result == 0)
{
Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}");
}
}
public int x;
int id = _nextId++;
_registrations[id] = new HotkeyRegistration
{
Id = id,
KeyCode = keyCode,
Modifiers = modifierMask,
Key = key,
ModifierKeys = modifiers
};
public int y;
XFlush(_display);
return id;
}
public int x_root;
/// <summary>
/// Unregisters a global hotkey.
/// </summary>
/// <param name="id">The registration ID.</param>
public void Unregister(int id)
{
if (_registrations.TryRemove(id, out var registration))
{
uint[] masks = GetModifierCombinations(registration.Modifiers);
public int y_root;
foreach (var mask in masks)
{
XUngrabKey(_display, registration.KeyCode, mask, _rootWindow);
}
public uint state;
XFlush(_display);
}
}
public int keycode;
/// <summary>
/// Unregisters all global hotkeys.
/// </summary>
public void UnregisterAll()
{
foreach (var id in _registrations.Keys.ToList())
{
Unregister(id);
}
}
public bool same_screen;
}
private void ListenForHotkeys()
{
while (_isListening && _display != IntPtr.Zero)
{
try
{
if (XPending(_display) > 0)
{
var xevent = new XEvent();
XNextEvent(_display, ref xevent);
private class HotkeyRegistration
{
public int Id { get; set; }
if (xevent.type == KeyPress)
{
var keyEvent = xevent.KeyEvent;
ProcessKeyEvent(keyEvent.keycode, keyEvent.state);
}
}
else
{
Thread.Sleep(10);
}
}
catch (Exception ex)
{
Console.WriteLine($"GlobalHotkeyService error: {ex.Message}");
}
}
}
public int KeyCode { get; set; }
private void ProcessKeyEvent(int keyCode, uint state)
{
// Remove NumLock and CapsLock from state for comparison
uint cleanState = state & ~(NumLockMask | CapsLockMask | ScrollLockMask);
public uint Modifiers { get; set; }
foreach (var registration in _registrations.Values)
{
if (registration.KeyCode == keyCode &&
(registration.Modifiers == cleanState ||
registration.Modifiers == (cleanState & ~Mod2Mask))) // Mod2 is often NumLock
{
OnHotkeyPressed(registration);
break;
}
}
}
public HotkeyKey Key { get; set; }
private void OnHotkeyPressed(HotkeyRegistration registration)
{
HotkeyPressed?.Invoke(this, new HotkeyEventArgs(
registration.Id,
registration.Key,
registration.ModifierKeys));
}
public HotkeyModifiers ModifierKeys { get; set; }
}
private uint GetModifierMask(HotkeyModifiers modifiers)
{
uint mask = 0;
if (modifiers.HasFlag(HotkeyModifiers.Shift)) mask |= ShiftMask;
if (modifiers.HasFlag(HotkeyModifiers.Control)) mask |= ControlMask;
if (modifiers.HasFlag(HotkeyModifiers.Alt)) mask |= Mod1Mask;
if (modifiers.HasFlag(HotkeyModifiers.Super)) mask |= Mod4Mask;
return mask;
}
private IntPtr _display;
private uint[] GetModifierCombinations(uint baseMask)
{
// Include combinations with NumLock and CapsLock
return new uint[]
{
baseMask,
baseMask | NumLockMask,
baseMask | CapsLockMask,
baseMask | NumLockMask | CapsLockMask
};
}
private IntPtr _rootWindow;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
private readonly ConcurrentDictionary<int, HotkeyRegistration> _registrations = new ConcurrentDictionary<int, HotkeyRegistration>();
_isListening = false;
private int _nextId = 1;
UnregisterAll();
private bool _disposed;
if (_display != IntPtr.Zero)
{
XCloseDisplay(_display);
_display = IntPtr.Zero;
}
}
private Thread? _eventThread;
#region X11 Interop
private bool _isListening;
private const int KeyPress = 2;
private const int GrabModeAsync = 1;
private const int KeyPress = 2;
private const uint ShiftMask = 1 << 0;
private const uint LockMask = 1 << 1; // CapsLock
private const uint ControlMask = 1 << 2;
private const uint Mod1Mask = 1 << 3; // Alt
private const uint Mod2Mask = 1 << 4; // NumLock
private const uint Mod4Mask = 1 << 6; // Super
private const int GrabModeAsync = 1;
private const uint NumLockMask = Mod2Mask;
private const uint CapsLockMask = LockMask;
private const uint ScrollLockMask = 0; // Usually not used
private const uint ShiftMask = 1u;
[StructLayout(LayoutKind.Explicit)]
private struct XEvent
{
[FieldOffset(0)] public int type;
[FieldOffset(0)] public XKeyEvent KeyEvent;
}
private const uint LockMask = 2u;
[StructLayout(LayoutKind.Sequential)]
private struct XKeyEvent
{
public int type;
public ulong serial;
public bool send_event;
public nint display;
public nint window;
public nint root;
public nint subwindow;
public ulong time;
public int x, y;
public int x_root, y_root;
public uint state;
public int keycode;
public bool same_screen;
}
private const uint ControlMask = 4u;
[DllImport("libX11.so.6")]
private static extern nint XOpenDisplay(nint display);
private const uint Mod1Mask = 8u;
[DllImport("libX11.so.6")]
private static extern void XCloseDisplay(nint display);
private const uint Mod2Mask = 16u;
[DllImport("libX11.so.6")]
private static extern nint XDefaultRootWindow(nint display);
private const uint Mod4Mask = 64u;
[DllImport("libX11.so.6")]
private static extern int XKeysymToKeycode(nint display, nint keysym);
private const uint NumLockMask = 16u;
[DllImport("libX11.so.6")]
private static extern int XGrabKey(nint display, int keycode, uint modifiers, nint grabWindow,
bool ownerEvents, int pointerMode, int keyboardMode);
private const uint CapsLockMask = 2u;
[DllImport("libX11.so.6")]
private static extern int XUngrabKey(nint display, int keycode, uint modifiers, nint grabWindow);
private const uint ScrollLockMask = 0u;
[DllImport("libX11.so.6")]
private static extern int XPending(nint display);
public event EventHandler<HotkeyEventArgs>? HotkeyPressed;
[DllImport("libX11.so.6")]
private static extern int XNextEvent(nint display, ref XEvent xevent);
public void Initialize()
{
_display = XOpenDisplay(IntPtr.Zero);
if (_display == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to open X display");
}
_rootWindow = XDefaultRootWindow(_display);
_isListening = true;
_eventThread = new Thread(ListenForHotkeys)
{
IsBackground = true,
Name = "GlobalHotkeyListener"
};
_eventThread.Start();
}
[DllImport("libX11.so.6")]
private static extern void XFlush(nint display);
public int Register(HotkeyKey key, HotkeyModifiers modifiers)
{
if (_display == IntPtr.Zero)
{
throw new InvalidOperationException("Service not initialized");
}
int num = XKeysymToKeycode(_display, (nint)key);
if (num == 0)
{
throw new ArgumentException($"Invalid key: {key}");
}
uint modifierMask = GetModifierMask(modifiers);
uint[] modifierCombinations = GetModifierCombinations(modifierMask);
foreach (uint modifiers2 in modifierCombinations)
{
if (XGrabKey(_display, num, modifiers2, _rootWindow, ownerEvents: true, 1, 1) == 0)
{
Console.WriteLine($"Failed to grab key {key} with modifiers {modifiers}");
}
}
int num2 = _nextId++;
_registrations[num2] = new HotkeyRegistration
{
Id = num2,
KeyCode = num,
Modifiers = modifierMask,
Key = key,
ModifierKeys = modifiers
};
XFlush(_display);
return num2;
}
#endregion
public void Unregister(int id)
{
if (_registrations.TryRemove(id, out HotkeyRegistration value))
{
uint[] modifierCombinations = GetModifierCombinations(value.Modifiers);
foreach (uint modifiers in modifierCombinations)
{
XUngrabKey(_display, value.KeyCode, modifiers, _rootWindow);
}
XFlush(_display);
}
}
public void UnregisterAll()
{
foreach (int item in _registrations.Keys.ToList())
{
Unregister(item);
}
}
private void ListenForHotkeys()
{
while (_isListening && _display != IntPtr.Zero)
{
try
{
if (XPending(_display) > 0)
{
XEvent xevent = default(XEvent);
XNextEvent(_display, ref xevent);
if (xevent.type == 2)
{
XKeyEvent keyEvent = xevent.KeyEvent;
ProcessKeyEvent(keyEvent.keycode, keyEvent.state);
}
}
else
{
Thread.Sleep(10);
}
}
catch (Exception ex)
{
Console.WriteLine("GlobalHotkeyService error: " + ex.Message);
}
}
}
private void ProcessKeyEvent(int keyCode, uint state)
{
uint num = state & 0xFFFFFFEDu;
foreach (HotkeyRegistration value in _registrations.Values)
{
if (value.KeyCode == keyCode && (value.Modifiers == num || value.Modifiers == (num & 0xFFFFFFEFu)))
{
OnHotkeyPressed(value);
break;
}
}
}
private void OnHotkeyPressed(HotkeyRegistration registration)
{
this.HotkeyPressed?.Invoke(this, new HotkeyEventArgs(registration.Id, registration.Key, registration.ModifierKeys));
}
private uint GetModifierMask(HotkeyModifiers modifiers)
{
uint num = 0u;
if (modifiers.HasFlag(HotkeyModifiers.Shift))
{
num |= 1;
}
if (modifiers.HasFlag(HotkeyModifiers.Control))
{
num |= 4;
}
if (modifiers.HasFlag(HotkeyModifiers.Alt))
{
num |= 8;
}
if (modifiers.HasFlag(HotkeyModifiers.Super))
{
num |= 0x40;
}
return num;
}
private uint[] GetModifierCombinations(uint baseMask)
{
return new uint[4]
{
baseMask,
baseMask | 0x10,
baseMask | 2,
baseMask | 0x10 | 2
};
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
_isListening = false;
UnregisterAll();
if (_display != IntPtr.Zero)
{
XCloseDisplay(_display);
_display = IntPtr.Zero;
}
}
}
[DllImport("libX11.so.6")]
private static extern IntPtr XOpenDisplay(IntPtr display);
[DllImport("libX11.so.6")]
private static extern void XCloseDisplay(IntPtr display);
[DllImport("libX11.so.6")]
private static extern IntPtr XDefaultRootWindow(IntPtr display);
[DllImport("libX11.so.6")]
private static extern int XKeysymToKeycode(IntPtr display, IntPtr keysym);
[DllImport("libX11.so.6")]
private static extern int XGrabKey(IntPtr display, int keycode, uint modifiers, IntPtr grabWindow, bool ownerEvents, int pointerMode, int keyboardMode);
[DllImport("libX11.so.6")]
private static extern int XUngrabKey(IntPtr display, int keycode, uint modifiers, IntPtr grabWindow);
[DllImport("libX11.so.6")]
private static extern int XPending(IntPtr display);
[DllImport("libX11.so.6")]
private static extern int XNextEvent(IntPtr display, ref XEvent xevent);
[DllImport("libX11.so.6")]
private static extern void XFlush(IntPtr display);
private class HotkeyRegistration
{
public int Id { get; set; }
public int KeyCode { get; set; }
public uint Modifiers { get; set; }
public HotkeyKey Key { get; set; }
public HotkeyModifiers ModifierKeys { get; set; }
}
}
/// <summary>
/// Event args for hotkey pressed events.
/// </summary>
public class HotkeyEventArgs : EventArgs
{
/// <summary>
/// Gets the registration ID.
/// </summary>
public int Id { get; }
/// <summary>
/// Gets the key.
/// </summary>
public HotkeyKey Key { get; }
/// <summary>
/// Gets the modifier keys.
/// </summary>
public HotkeyModifiers Modifiers { get; }
public HotkeyEventArgs(int id, HotkeyKey key, HotkeyModifiers modifiers)
{
Id = id;
Key = key;
Modifiers = modifiers;
}
}
/// <summary>
/// Hotkey modifier keys.
/// </summary>
[Flags]
public enum HotkeyModifiers
{
None = 0,
Shift = 1 << 0,
Control = 1 << 1,
Alt = 1 << 2,
Super = 1 << 3
}
/// <summary>
/// Hotkey keys (X11 keysyms).
/// </summary>
public enum HotkeyKey : uint
{
// Letters
A = 0x61, B = 0x62, C = 0x63, D = 0x64, E = 0x65,
F = 0x66, G = 0x67, H = 0x68, I = 0x69, J = 0x6A,
K = 0x6B, L = 0x6C, M = 0x6D, N = 0x6E, O = 0x6F,
P = 0x70, Q = 0x71, R = 0x72, S = 0x73, T = 0x74,
U = 0x75, V = 0x76, W = 0x77, X = 0x78, Y = 0x79,
Z = 0x7A,
// Numbers
D0 = 0x30, D1 = 0x31, D2 = 0x32, D3 = 0x33, D4 = 0x34,
D5 = 0x35, D6 = 0x36, D7 = 0x37, D8 = 0x38, D9 = 0x39,
// Function keys
F1 = 0xFFBE, F2 = 0xFFBF, F3 = 0xFFC0, F4 = 0xFFC1,
F5 = 0xFFC2, F6 = 0xFFC3, F7 = 0xFFC4, F8 = 0xFFC5,
F9 = 0xFFC6, F10 = 0xFFC7, F11 = 0xFFC8, F12 = 0xFFC9,
// Special keys
Escape = 0xFF1B,
Tab = 0xFF09,
Return = 0xFF0D,
Space = 0x20,
BackSpace = 0xFF08,
Delete = 0xFFFF,
Insert = 0xFF63,
Home = 0xFF50,
End = 0xFF57,
PageUp = 0xFF55,
PageDown = 0xFF56,
// Arrow keys
Left = 0xFF51,
Up = 0xFF52,
Right = 0xFF53,
Down = 0xFF54,
// Media keys
AudioPlay = 0x1008FF14,
AudioStop = 0x1008FF15,
AudioPrev = 0x1008FF16,
AudioNext = 0x1008FF17,
AudioMute = 0x1008FF12,
AudioRaiseVolume = 0x1008FF13,
AudioLowerVolume = 0x1008FF11,
// Print screen
Print = 0xFF61
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum GtkButtonsType
{
None,
Ok,
Close,
Cancel,
YesNo,
OkCancel
}

View File

@@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Maui.Platform.Linux.Native;
namespace Microsoft.Maui.Platform.Linux.Services;
public static class GtkContextMenuService
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void ActivateCallback(IntPtr menuItem, IntPtr userData);
private static readonly List<ActivateCallback> _callbacks = new List<ActivateCallback>();
private static readonly List<Action> _actions = new List<Action>();
public static void ShowContextMenu(List<GtkMenuItem> items)
{
if (items == null || items.Count == 0)
{
return;
}
_callbacks.Clear();
_actions.Clear();
IntPtr intPtr = GtkNative.gtk_menu_new();
if (intPtr == IntPtr.Zero)
{
Console.WriteLine("[GtkContextMenuService] Failed to create GTK menu");
return;
}
foreach (GtkMenuItem item in items)
{
IntPtr intPtr2;
if (item.IsSeparator)
{
intPtr2 = GtkNative.gtk_separator_menu_item_new();
}
else
{
intPtr2 = GtkNative.gtk_menu_item_new_with_label(item.Text);
GtkNative.gtk_widget_set_sensitive(intPtr2, item.IsEnabled);
if (item.IsEnabled && item.Action != null)
{
Action action = item.Action;
_actions.Add(action);
int actionIndex = _actions.Count - 1;
ActivateCallback activateCallback = delegate
{
Console.WriteLine("[GtkContextMenuService] Menu item activated: " + item.Text);
_actions[actionIndex]?.Invoke();
};
_callbacks.Add(activateCallback);
GtkNative.g_signal_connect_data(intPtr2, "activate", Marshal.GetFunctionPointerForDelegate(activateCallback), IntPtr.Zero, IntPtr.Zero, 0);
}
}
GtkNative.gtk_menu_shell_append(intPtr, intPtr2);
GtkNative.gtk_widget_show(intPtr2);
}
GtkNative.gtk_widget_show(intPtr);
IntPtr intPtr3 = GtkNative.gtk_get_current_event();
GtkNative.gtk_menu_popup_at_pointer(intPtr, intPtr3);
if (intPtr3 != IntPtr.Zero)
{
GtkNative.gdk_event_free(intPtr3);
}
Console.WriteLine($"[GtkContextMenuService] Showed GTK menu with {items.Count} items");
}
}

View File

@@ -1,9 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum GtkFileChooserAction
{
Open,
Save,
SelectFolder,
CreateFolder
}

View File

@@ -1,56 +0,0 @@
using System;
using Microsoft.Maui.Platform.Linux.Handlers;
using Microsoft.Maui.Platform.Linux.Window;
namespace Microsoft.Maui.Platform.Linux.Services;
public class GtkHostService
{
private static GtkHostService? _instance;
private GtkHostWindow? _hostWindow;
private GtkWebViewManager? _webViewManager;
public static GtkHostService Instance => _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);
this.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;
}
}

View File

@@ -1,32 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
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;
}
}

View File

@@ -1,10 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum GtkMessageType
{
Info,
Warning,
Question,
Error,
Other
}

View File

@@ -1,16 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum GtkResponseType
{
None = -1,
Reject = -2,
Accept = -3,
DeleteEvent = -4,
Ok = -5,
Cancel = -6,
Close = -7,
Yes = -8,
No = -9,
Apply = -10,
Help = -11
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,387 +1,524 @@
using System;
using System.Diagnostics;
using System.IO;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Provides HiDPI and display scaling detection for Linux.
/// </summary>
public class HiDpiService
{
private const float DefaultDpi = 96f;
private const float DefaultDpi = 96f;
private float _scaleFactor = 1.0f;
private float _dpi = DefaultDpi;
private bool _initialized;
private float _scaleFactor = 1f;
/// <summary>
/// Gets the current scale factor.
/// </summary>
public float ScaleFactor => _scaleFactor;
private float _dpi = 96f;
/// <summary>
/// Gets the current DPI.
/// </summary>
public float Dpi => _dpi;
private bool _initialized;
/// <summary>
/// Event raised when scale factor changes.
/// </summary>
public event EventHandler<ScaleChangedEventArgs>? ScaleChanged;
public float ScaleFactor => _scaleFactor;
/// <summary>
/// Initializes the HiDPI detection service.
/// </summary>
public void Initialize()
{
if (_initialized) return;
_initialized = true;
public float Dpi => _dpi;
DetectScaleFactor();
}
public event EventHandler<ScaleChangedEventArgs>? ScaleChanged;
/// <summary>
/// Detects the current scale factor using multiple methods.
/// </summary>
public void DetectScaleFactor()
{
float scale = 1.0f;
float dpi = DefaultDpi;
public void Initialize()
{
if (!_initialized)
{
_initialized = true;
DetectScaleFactor();
}
}
// Try multiple detection methods in order of preference
if (TryGetEnvironmentScale(out float envScale))
{
scale = envScale;
}
else if (TryGetGnomeScale(out float gnomeScale, out float gnomeDpi))
{
scale = gnomeScale;
dpi = gnomeDpi;
}
else if (TryGetKdeScale(out float kdeScale))
{
scale = kdeScale;
}
else if (TryGetX11Scale(out float x11Scale, out float x11Dpi))
{
scale = x11Scale;
dpi = x11Dpi;
}
else if (TryGetXrandrScale(out float xrandrScale))
{
scale = xrandrScale;
}
public void DetectScaleFactor()
{
float scale = 1f;
float dpi = 96f;
float scale3;
float dpi2;
float scale4;
float scale5;
float dpi3;
float scale6;
if (TryGetEnvironmentScale(out var scale2))
{
scale = scale2;
}
else if (TryGetGnomeScale(out scale3, out dpi2))
{
scale = scale3;
dpi = dpi2;
}
else if (TryGetKdeScale(out scale4))
{
scale = scale4;
}
else if (TryGetX11Scale(out scale5, out dpi3))
{
scale = scale5;
dpi = dpi3;
}
else if (TryGetXrandrScale(out scale6))
{
scale = scale6;
}
UpdateScale(scale, dpi);
}
UpdateScale(scale, dpi);
}
private void UpdateScale(float scale, float dpi)
{
if (Math.Abs(_scaleFactor - scale) > 0.01f || Math.Abs(_dpi - dpi) > 0.01f)
{
float scaleFactor = _scaleFactor;
_scaleFactor = scale;
_dpi = dpi;
this.ScaleChanged?.Invoke(this, new ScaleChangedEventArgs(scaleFactor, scale, dpi));
}
}
private void UpdateScale(float scale, float dpi)
{
if (Math.Abs(_scaleFactor - scale) > 0.01f || Math.Abs(_dpi - dpi) > 0.01f)
{
var oldScale = _scaleFactor;
_scaleFactor = scale;
_dpi = dpi;
ScaleChanged?.Invoke(this, new ScaleChangedEventArgs(oldScale, scale, dpi));
}
}
private static bool TryGetEnvironmentScale(out float scale)
{
scale = 1f;
string environmentVariable = Environment.GetEnvironmentVariable("GDK_SCALE");
if (!string.IsNullOrEmpty(environmentVariable) && float.TryParse(environmentVariable, out var result))
{
scale = result;
return true;
}
string environmentVariable2 = Environment.GetEnvironmentVariable("GDK_DPI_SCALE");
if (!string.IsNullOrEmpty(environmentVariable2) && float.TryParse(environmentVariable2, out var result2))
{
scale = result2;
return true;
}
string environmentVariable3 = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR");
if (!string.IsNullOrEmpty(environmentVariable3) && float.TryParse(environmentVariable3, out var result3))
{
scale = result3;
return true;
}
string environmentVariable4 = Environment.GetEnvironmentVariable("QT_SCREEN_SCALE_FACTORS");
if (!string.IsNullOrEmpty(environmentVariable4))
{
string text = environmentVariable4.Split(';')[0];
if (text.Contains('='))
{
text = text.Split('=')[1];
}
if (float.TryParse(text, out var result4))
{
scale = result4;
return true;
}
}
return false;
}
/// <summary>
/// Gets scale from environment variables.
/// </summary>
private static bool TryGetEnvironmentScale(out float scale)
{
scale = 1.0f;
private static bool TryGetGnomeScale(out float scale, out float dpi)
{
scale = 1f;
dpi = 96f;
try
{
string text = RunCommand("gsettings", "get org.gnome.desktop.interface scaling-factor");
if (!string.IsNullOrEmpty(text))
{
Match match = Regex.Match(text, "uint32\\s+(\\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out var result) && result > 0)
{
scale = result;
}
}
text = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor");
if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result2) && result2 > 0.5f)
{
scale = Math.Max(scale, result2);
}
text = RunCommand("gsettings", "get org.gnome.mutter experimental-features");
if (text != null && text.Contains("scale-monitor-framebuffer"))
{
text = RunCommand("gdbus", "call --session --dest org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.GetCurrentState");
if (text != null)
{
Match match2 = Regex.Match(text, "'scale':\\s*<(\\d+\\.?\\d*)>");
if (match2.Success && float.TryParse(match2.Groups[1].Value, out var result3))
{
scale = result3;
}
}
}
return scale > 1f || Math.Abs(scale - 1f) < 0.01f;
}
catch
{
return false;
}
}
// GDK_SCALE (GTK3/4)
var gdkScale = Environment.GetEnvironmentVariable("GDK_SCALE");
if (!string.IsNullOrEmpty(gdkScale) && float.TryParse(gdkScale, out float gdk))
{
scale = gdk;
return true;
}
private static bool TryGetKdeScale(out float scale)
{
scale = 1f;
try
{
string text = RunCommand("kreadconfig5", "--file kdeglobals --group KScreen --key ScaleFactor");
if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result) && result > 0f)
{
scale = result;
return true;
}
text = RunCommand("kreadconfig6", "--file kdeglobals --group KScreen --key ScaleFactor");
if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result2) && result2 > 0f)
{
scale = result2;
return true;
}
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals");
if (File.Exists(path))
{
string[] array = File.ReadAllLines(path);
bool flag = false;
string[] array2 = array;
foreach (string text2 in array2)
{
if (text2.Trim() == "[KScreen]")
{
flag = true;
continue;
}
if (flag && text2.StartsWith("["))
{
break;
}
if (flag && text2.StartsWith("ScaleFactor=") && float.TryParse(text2.Substring("ScaleFactor=".Length), out var result3))
{
scale = result3;
return true;
}
}
}
return false;
}
catch
{
return false;
}
}
// GDK_DPI_SCALE (GTK3/4)
var gdkDpiScale = Environment.GetEnvironmentVariable("GDK_DPI_SCALE");
if (!string.IsNullOrEmpty(gdkDpiScale) && float.TryParse(gdkDpiScale, out float gdkDpi))
{
scale = gdkDpi;
return true;
}
private bool TryGetX11Scale(out float scale, out float dpi)
{
scale = 1f;
dpi = 96f;
try
{
string text = RunCommand("xrdb", "-query");
if (!string.IsNullOrEmpty(text))
{
Match match = Regex.Match(text, "Xft\\.dpi:\\s*(\\d+)");
if (match.Success && float.TryParse(match.Groups[1].Value, out var result))
{
dpi = result;
scale = result / 96f;
return true;
}
}
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".Xresources");
if (File.Exists(path))
{
Match match2 = Regex.Match(File.ReadAllText(path), "Xft\\.dpi:\\s*(\\d+)");
if (match2.Success && float.TryParse(match2.Groups[1].Value, out var result2))
{
dpi = result2;
scale = result2 / 96f;
return true;
}
}
return TryGetX11DpiDirect(out scale, out dpi);
}
catch
{
return false;
}
}
// QT_SCALE_FACTOR
var qtScale = Environment.GetEnvironmentVariable("QT_SCALE_FACTOR");
if (!string.IsNullOrEmpty(qtScale) && float.TryParse(qtScale, out float qt))
{
scale = qt;
return true;
}
private bool TryGetX11DpiDirect(out float scale, out float dpi)
{
scale = 1f;
dpi = 96f;
try
{
IntPtr intPtr = XOpenDisplay(IntPtr.Zero);
if (intPtr == IntPtr.Zero)
{
return false;
}
try
{
int screen = XDefaultScreen(intPtr);
int num = XDisplayWidthMM(intPtr, screen);
int num2 = XDisplayHeightMM(intPtr, screen);
int num3 = XDisplayWidth(intPtr, screen);
int num4 = XDisplayHeight(intPtr, screen);
if (num > 0 && num2 > 0)
{
float num5 = (float)num3 * 25.4f / (float)num;
float num6 = (float)num4 * 25.4f / (float)num2;
dpi = (num5 + num6) / 2f;
scale = dpi / 96f;
return true;
}
return false;
}
finally
{
XCloseDisplay(intPtr);
}
}
catch
{
return false;
}
}
// QT_SCREEN_SCALE_FACTORS (can be per-screen)
var qtScreenScales = Environment.GetEnvironmentVariable("QT_SCREEN_SCALE_FACTORS");
if (!string.IsNullOrEmpty(qtScreenScales))
{
// Format: "screen1=1.5;screen2=2.0" or just "1.5"
var first = qtScreenScales.Split(';')[0];
if (first.Contains('='))
{
first = first.Split('=')[1];
}
if (float.TryParse(first, out float qtScreen))
{
scale = qtScreen;
return true;
}
}
private static bool TryGetXrandrScale(out float scale)
{
scale = 1f;
try
{
string text = RunCommand("xrandr", "--query");
if (string.IsNullOrEmpty(text))
{
return false;
}
string[] array = text.Split('\n');
foreach (string text2 in array)
{
if (text2.Contains("connected") && !text2.Contains("disconnected"))
{
Match match = Regex.Match(text2, "(\\d+)x(\\d+)\\+\\d+\\+\\d+");
Match match2 = Regex.Match(text2, "(\\d+)mm x (\\d+)mm");
if (match.Success && match2.Success && int.TryParse(match.Groups[1].Value, out var result) && int.TryParse(match2.Groups[1].Value, out var result2) && result2 > 0)
{
float num = (float)result * 25.4f / (float)result2;
scale = num / 96f;
return true;
}
}
}
return false;
}
catch
{
return false;
}
}
return false;
}
private static string? RunCommand(string command, string arguments)
{
try
{
using Process process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return result;
}
catch
{
return null;
}
}
/// <summary>
/// Gets scale from GNOME settings.
/// </summary>
private static bool TryGetGnomeScale(out float scale, out float dpi)
{
scale = 1.0f;
dpi = DefaultDpi;
public float ToPhysicalPixels(float logicalPixels)
{
return logicalPixels * _scaleFactor;
}
try
{
// Try gsettings for GNOME
var result = RunCommand("gsettings", "get org.gnome.desktop.interface scaling-factor");
if (!string.IsNullOrEmpty(result))
{
var match = Regex.Match(result, @"uint32\s+(\d+)");
if (match.Success && int.TryParse(match.Groups[1].Value, out int gnomeScale))
{
if (gnomeScale > 0)
{
scale = gnomeScale;
}
}
}
public float ToLogicalPixels(float physicalPixels)
{
return physicalPixels / _scaleFactor;
}
// Also check text-scaling-factor for fractional scaling
result = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor");
if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float textScale))
{
if (textScale > 0.5f)
{
scale = Math.Max(scale, textScale);
}
}
public float GetFontScaleFactor()
{
try
{
string text = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor");
if (!string.IsNullOrEmpty(text) && float.TryParse(text.Trim(), out var result))
{
return result;
}
}
catch
{
}
return _scaleFactor;
}
// Check for GNOME 40+ experimental fractional scaling
result = RunCommand("gsettings", "get org.gnome.mutter experimental-features");
if (result != null && result.Contains("scale-monitor-framebuffer"))
{
// Fractional scaling is enabled, try to get actual scale
result = RunCommand("gdbus", "call --session --dest org.gnome.Mutter.DisplayConfig --object-path /org/gnome/Mutter/DisplayConfig --method org.gnome.Mutter.DisplayConfig.GetCurrentState");
if (result != null)
{
// Parse for scale value
var scaleMatch = Regex.Match(result, @"'scale':\s*<(\d+\.?\d*)>");
if (scaleMatch.Success && float.TryParse(scaleMatch.Groups[1].Value, out float mutterScale))
{
scale = mutterScale;
}
}
}
[DllImport("libX11.so.6")]
private static extern IntPtr XOpenDisplay(IntPtr display);
return scale > 1.0f || Math.Abs(scale - 1.0f) < 0.01f;
}
catch
{
return false;
}
}
[DllImport("libX11.so.6")]
private static extern void XCloseDisplay(IntPtr display);
/// <summary>
/// Gets scale from KDE settings.
/// </summary>
private static bool TryGetKdeScale(out float scale)
{
scale = 1.0f;
[DllImport("libX11.so.6")]
private static extern int XDefaultScreen(IntPtr display);
try
{
// Try kreadconfig5 for KDE Plasma 5
var result = RunCommand("kreadconfig5", "--file kdeglobals --group KScreen --key ScaleFactor");
if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float kdeScale))
{
if (kdeScale > 0)
{
scale = kdeScale;
return true;
}
}
[DllImport("libX11.so.6")]
private static extern int XDisplayWidth(IntPtr display, int screen);
// Try KDE Plasma 6
result = RunCommand("kreadconfig6", "--file kdeglobals --group KScreen --key ScaleFactor");
if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float kde6Scale))
{
if (kde6Scale > 0)
{
scale = kde6Scale;
return true;
}
}
[DllImport("libX11.so.6")]
private static extern int XDisplayHeight(IntPtr display, int screen);
// Check kdeglobals config file directly
var configPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "kdeglobals");
[DllImport("libX11.so.6")]
private static extern int XDisplayWidthMM(IntPtr display, int screen);
if (File.Exists(configPath))
{
var lines = File.ReadAllLines(configPath);
bool inKScreenSection = false;
foreach (var line in lines)
{
if (line.Trim() == "[KScreen]")
{
inKScreenSection = true;
continue;
}
if (inKScreenSection && line.StartsWith("["))
{
break;
}
if (inKScreenSection && line.StartsWith("ScaleFactor="))
{
var value = line.Substring("ScaleFactor=".Length);
if (float.TryParse(value, out float fileScale))
{
scale = fileScale;
return true;
}
}
}
}
[DllImport("libX11.so.6")]
private static extern int XDisplayHeightMM(IntPtr display, int screen);
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Gets scale from X11 Xresources.
/// </summary>
private bool TryGetX11Scale(out float scale, out float dpi)
{
scale = 1.0f;
dpi = DefaultDpi;
try
{
// Try xrdb query
var result = RunCommand("xrdb", "-query");
if (!string.IsNullOrEmpty(result))
{
// Look for Xft.dpi
var match = Regex.Match(result, @"Xft\.dpi:\s*(\d+)");
if (match.Success && float.TryParse(match.Groups[1].Value, out float xftDpi))
{
dpi = xftDpi;
scale = xftDpi / DefaultDpi;
return true;
}
}
// Try reading .Xresources directly
var xresourcesPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".Xresources");
if (File.Exists(xresourcesPath))
{
var content = File.ReadAllText(xresourcesPath);
var match = Regex.Match(content, @"Xft\.dpi:\s*(\d+)");
if (match.Success && float.TryParse(match.Groups[1].Value, out float fileDpi))
{
dpi = fileDpi;
scale = fileDpi / DefaultDpi;
return true;
}
}
// Try X11 directly
return TryGetX11DpiDirect(out scale, out dpi);
}
catch
{
return false;
}
}
/// <summary>
/// Gets DPI directly from X11 server.
/// </summary>
private bool TryGetX11DpiDirect(out float scale, out float dpi)
{
scale = 1.0f;
dpi = DefaultDpi;
try
{
var display = XOpenDisplay(IntPtr.Zero);
if (display == IntPtr.Zero) return false;
try
{
int screen = XDefaultScreen(display);
// Get physical dimensions
int widthMm = XDisplayWidthMM(display, screen);
int heightMm = XDisplayHeightMM(display, screen);
int widthPx = XDisplayWidth(display, screen);
int heightPx = XDisplayHeight(display, screen);
if (widthMm > 0 && heightMm > 0)
{
float dpiX = widthPx * 25.4f / widthMm;
float dpiY = heightPx * 25.4f / heightMm;
dpi = (dpiX + dpiY) / 2;
scale = dpi / DefaultDpi;
return true;
}
return false;
}
finally
{
XCloseDisplay(display);
}
}
catch
{
return false;
}
}
/// <summary>
/// Gets scale from xrandr output.
/// </summary>
private static bool TryGetXrandrScale(out float scale)
{
scale = 1.0f;
try
{
var result = RunCommand("xrandr", "--query");
if (string.IsNullOrEmpty(result)) return false;
// Look for connected displays with scaling
// Format: "eDP-1 connected primary 2560x1440+0+0 (normal left inverted right x axis y axis) 309mm x 174mm"
var lines = result.Split('\n');
foreach (var line in lines)
{
if (!line.Contains("connected") || line.Contains("disconnected")) continue;
// Try to find resolution and physical size
var resMatch = Regex.Match(line, @"(\d+)x(\d+)\+\d+\+\d+");
var mmMatch = Regex.Match(line, @"(\d+)mm x (\d+)mm");
if (resMatch.Success && mmMatch.Success)
{
if (int.TryParse(resMatch.Groups[1].Value, out int widthPx) &&
int.TryParse(mmMatch.Groups[1].Value, out int widthMm) &&
widthMm > 0)
{
float dpi = widthPx * 25.4f / widthMm;
scale = dpi / DefaultDpi;
return true;
}
}
}
return false;
}
catch
{
return false;
}
}
private static string? RunCommand(string command, string arguments)
{
try
{
using var process = new System.Diagnostics.Process();
process.StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return output;
}
catch
{
return null;
}
}
/// <summary>
/// Converts logical pixels to physical pixels.
/// </summary>
public float ToPhysicalPixels(float logicalPixels)
{
return logicalPixels * _scaleFactor;
}
/// <summary>
/// Converts physical pixels to logical pixels.
/// </summary>
public float ToLogicalPixels(float physicalPixels)
{
return physicalPixels / _scaleFactor;
}
/// <summary>
/// Gets the recommended font scale factor.
/// </summary>
public float GetFontScaleFactor()
{
// Some desktop environments use a separate text scaling factor
try
{
var result = RunCommand("gsettings", "get org.gnome.desktop.interface text-scaling-factor");
if (!string.IsNullOrEmpty(result) && float.TryParse(result.Trim(), out float textScale))
{
return textScale;
}
}
catch { }
return _scaleFactor;
}
#region X11 Interop
[DllImport("libX11.so.6")]
private static extern nint XOpenDisplay(nint display);
[DllImport("libX11.so.6")]
private static extern void XCloseDisplay(nint display);
[DllImport("libX11.so.6")]
private static extern int XDefaultScreen(nint display);
[DllImport("libX11.so.6")]
private static extern int XDisplayWidth(nint display, int screen);
[DllImport("libX11.so.6")]
private static extern int XDisplayHeight(nint display, int screen);
[DllImport("libX11.so.6")]
private static extern int XDisplayWidthMM(nint display, int screen);
[DllImport("libX11.so.6")]
private static extern int XDisplayHeightMM(nint display, int screen);
#endregion
}
/// <summary>
/// Event args for scale change events.
/// </summary>
public class ScaleChangedEventArgs : EventArgs
{
/// <summary>
/// Gets the old scale factor.
/// </summary>
public float OldScale { get; }
/// <summary>
/// Gets the new scale factor.
/// </summary>
public float NewScale { get; }
/// <summary>
/// Gets the new DPI.
/// </summary>
public float NewDpi { get; }
public ScaleChangedEventArgs(float oldScale, float newScale, float newDpi)
{
OldScale = oldScale;
NewScale = newScale;
NewDpi = newDpi;
}
}

View File

@@ -1,16 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class HighContrastChangedEventArgs : EventArgs
{
public bool IsEnabled { get; }
public HighContrastTheme Theme { get; }
public HighContrastChangedEventArgs(bool isEnabled, HighContrastTheme theme)
{
IsEnabled = isEnabled;
Theme = theme;
}
}

View File

@@ -1,32 +0,0 @@
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
public class HighContrastColors
{
public SKColor Background { get; set; }
public SKColor Foreground { get; set; }
public SKColor Accent { get; set; }
public SKColor Border { get; set; }
public SKColor Error { get; set; }
public SKColor Success { get; set; }
public SKColor Warning { get; set; }
public SKColor Link { get; set; }
public SKColor LinkVisited { get; set; }
public SKColor Selection { get; set; }
public SKColor SelectionText { get; set; }
public SKColor DisabledText { get; set; }
public SKColor DisabledBackground { get; set; }
}

View File

@@ -1,342 +1,402 @@
using System;
using System.Diagnostics;
using System.IO;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Provides high contrast mode detection and theme support for accessibility.
/// </summary>
public class HighContrastService
{
private bool _isHighContrastEnabled;
private bool _isHighContrastEnabled;
private HighContrastTheme _currentTheme = HighContrastTheme.None;
private bool _initialized;
private HighContrastTheme _currentTheme;
/// <summary>
/// Gets whether high contrast mode is enabled.
/// </summary>
public bool IsHighContrastEnabled => _isHighContrastEnabled;
private bool _initialized;
/// <summary>
/// Gets the current high contrast theme.
/// </summary>
public HighContrastTheme CurrentTheme => _currentTheme;
public bool IsHighContrastEnabled => _isHighContrastEnabled;
/// <summary>
/// Event raised when high contrast mode changes.
/// </summary>
public event EventHandler<HighContrastChangedEventArgs>? HighContrastChanged;
public HighContrastTheme CurrentTheme => _currentTheme;
/// <summary>
/// Initializes the high contrast service.
/// </summary>
public void Initialize()
{
if (_initialized) return;
_initialized = true;
public event EventHandler<HighContrastChangedEventArgs>? HighContrastChanged;
DetectHighContrast();
}
public void Initialize()
{
if (!_initialized)
{
_initialized = true;
DetectHighContrast();
}
}
/// <summary>
/// Detects current high contrast mode settings.
/// </summary>
public void DetectHighContrast()
{
bool isEnabled = false;
var theme = HighContrastTheme.None;
public void DetectHighContrast()
{
bool isEnabled = false;
HighContrastTheme theme = HighContrastTheme.None;
bool isEnabled3;
string themeName2;
bool isEnabled4;
string themeName3;
bool isEnabled5;
if (TryGetGnomeHighContrast(out bool isEnabled2, out string themeName))
{
isEnabled = isEnabled2;
if (isEnabled2)
{
theme = ParseThemeName(themeName);
}
}
else if (TryGetKdeHighContrast(out isEnabled3, out themeName2))
{
isEnabled = isEnabled3;
if (isEnabled3)
{
theme = ParseThemeName(themeName2);
}
}
else if (TryGetGtkHighContrast(out isEnabled4, out themeName3))
{
isEnabled = isEnabled4;
if (isEnabled4)
{
theme = ParseThemeName(themeName3);
}
}
else if (TryGetEnvironmentHighContrast(out isEnabled5))
{
isEnabled = isEnabled5;
theme = HighContrastTheme.WhiteOnBlack;
}
UpdateHighContrast(isEnabled, theme);
}
// Try GNOME settings
if (TryGetGnomeHighContrast(out bool gnomeEnabled, out string? gnomeTheme))
{
isEnabled = gnomeEnabled;
if (gnomeEnabled)
{
theme = ParseThemeName(gnomeTheme);
}
}
// Try KDE settings
else if (TryGetKdeHighContrast(out bool kdeEnabled, out string? kdeTheme))
{
isEnabled = kdeEnabled;
if (kdeEnabled)
{
theme = ParseThemeName(kdeTheme);
}
}
// Try GTK settings
else if (TryGetGtkHighContrast(out bool gtkEnabled, out string? gtkTheme))
{
isEnabled = gtkEnabled;
if (gtkEnabled)
{
theme = ParseThemeName(gtkTheme);
}
}
// Check environment variables
else if (TryGetEnvironmentHighContrast(out bool envEnabled))
{
isEnabled = envEnabled;
theme = HighContrastTheme.WhiteOnBlack; // Default
}
private void UpdateHighContrast(bool isEnabled, HighContrastTheme theme)
{
if (_isHighContrastEnabled != isEnabled || _currentTheme != theme)
{
_isHighContrastEnabled = isEnabled;
_currentTheme = theme;
this.HighContrastChanged?.Invoke(this, new HighContrastChangedEventArgs(isEnabled, theme));
}
}
UpdateHighContrast(isEnabled, theme);
}
private static bool TryGetGnomeHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
string text = RunCommand("gsettings", "get org.gnome.desktop.a11y.interface high-contrast");
if (!string.IsNullOrEmpty(text))
{
isEnabled = text.Trim().ToLower() == "true";
}
text = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme");
if (!string.IsNullOrEmpty(text))
{
themeName = text.Trim().Trim('\'');
if (!isEnabled && themeName != null)
{
string text2 = themeName.ToLower();
isEnabled = text2.Contains("highcontrast") || text2.Contains("high-contrast") || text2.Contains("hc");
}
}
return true;
}
catch
{
return false;
}
}
private void UpdateHighContrast(bool isEnabled, HighContrastTheme theme)
{
if (_isHighContrastEnabled != isEnabled || _currentTheme != theme)
{
_isHighContrastEnabled = isEnabled;
_currentTheme = theme;
HighContrastChanged?.Invoke(this, new HighContrastChangedEventArgs(isEnabled, theme));
}
}
private static bool TryGetKdeHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals");
if (!File.Exists(path))
{
return false;
}
string[] array = File.ReadAllLines(path);
foreach (string text in array)
{
if (text.StartsWith("ColorScheme="))
{
themeName = text.Substring("ColorScheme=".Length);
string text2 = themeName.ToLower();
isEnabled = text2.Contains("highcontrast") || text2.Contains("high-contrast") || text2.Contains("breeze-high-contrast");
return true;
}
}
return false;
}
catch
{
return false;
}
}
private static bool TryGetGnomeHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
private static bool TryGetGtkHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gtk-3.0", "settings.ini");
if (!File.Exists(path))
{
path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gtk-4.0", "settings.ini");
}
if (!File.Exists(path))
{
return false;
}
string[] array = File.ReadAllLines(path);
foreach (string text in array)
{
if (text.StartsWith("gtk-theme-name="))
{
themeName = text.Substring("gtk-theme-name=".Length);
string text2 = themeName.ToLower();
isEnabled = text2.Contains("highcontrast") || text2.Contains("high-contrast");
return true;
}
}
return false;
}
catch
{
return false;
}
}
try
{
// Check if high contrast is enabled via gsettings
var result = RunCommand("gsettings", "get org.gnome.desktop.a11y.interface high-contrast");
if (!string.IsNullOrEmpty(result))
{
isEnabled = result.Trim().ToLower() == "true";
}
private static bool TryGetEnvironmentHighContrast(out bool isEnabled)
{
isEnabled = false;
string environmentVariable = Environment.GetEnvironmentVariable("GTK_THEME");
if (!string.IsNullOrEmpty(environmentVariable))
{
string text = environmentVariable.ToLower();
isEnabled = text.Contains("highcontrast") || text.Contains("high-contrast");
if (isEnabled)
{
return true;
}
}
string environmentVariable2 = Environment.GetEnvironmentVariable("GTK_A11Y");
if (!(environmentVariable2?.ToLower() == "atspi"))
{
_ = environmentVariable2 == "1";
}
return isEnabled;
}
// Get the current GTK theme
result = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme");
if (!string.IsNullOrEmpty(result))
{
themeName = result.Trim().Trim('\'');
private static HighContrastTheme ParseThemeName(string? themeName)
{
if (string.IsNullOrEmpty(themeName))
{
return HighContrastTheme.WhiteOnBlack;
}
string text = themeName.ToLower();
if (text.Contains("inverse") || text.Contains("dark") || text.Contains("white-on-black"))
{
return HighContrastTheme.WhiteOnBlack;
}
if (text.Contains("light") || text.Contains("black-on-white"))
{
return HighContrastTheme.BlackOnWhite;
}
return HighContrastTheme.WhiteOnBlack;
}
// Check if theme name indicates high contrast
if (!isEnabled && themeName != null)
{
var lowerTheme = themeName.ToLower();
isEnabled = lowerTheme.Contains("highcontrast") ||
lowerTheme.Contains("high-contrast") ||
lowerTheme.Contains("hc");
}
}
public HighContrastColors GetColors()
{
//IL_001d: Unknown result type (might be due to invalid IL or missing references)
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_0049: Unknown result type (might be due to invalid IL or missing references)
//IL_005d: Unknown result type (might be due to invalid IL or missing references)
//IL_0071: Unknown result type (might be due to invalid IL or missing references)
//IL_007c: Unknown result type (might be due to invalid IL or missing references)
//IL_0093: Unknown result type (might be due to invalid IL or missing references)
//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
//IL_00c0: Unknown result type (might be due to invalid IL or missing references)
//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
//IL_00e5: Unknown result type (might be due to invalid IL or missing references)
//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
//IL_010c: Unknown result type (might be due to invalid IL or missing references)
//IL_0117: Unknown result type (might be due to invalid IL or missing references)
//IL_0129: Unknown result type (might be due to invalid IL or missing references)
//IL_0134: Unknown result type (might be due to invalid IL or missing references)
//IL_0146: Unknown result type (might be due to invalid IL or missing references)
//IL_0158: Unknown result type (might be due to invalid IL or missing references)
//IL_016b: Unknown result type (might be due to invalid IL or missing references)
//IL_017d: Unknown result type (might be due to invalid IL or missing references)
//IL_018d: Unknown result type (might be due to invalid IL or missing references)
//IL_01a0: Unknown result type (might be due to invalid IL or missing references)
//IL_01ab: Unknown result type (might be due to invalid IL or missing references)
//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
//IL_01d6: Unknown result type (might be due to invalid IL or missing references)
return _currentTheme switch
{
HighContrastTheme.WhiteOnBlack => new HighContrastColors
{
Background = SKColors.Black,
Foreground = SKColors.White,
Accent = new SKColor((byte)0, byte.MaxValue, byte.MaxValue),
Border = SKColors.White,
Error = new SKColor(byte.MaxValue, (byte)100, (byte)100),
Success = new SKColor((byte)100, byte.MaxValue, (byte)100),
Warning = SKColors.Yellow,
Link = new SKColor((byte)100, (byte)200, byte.MaxValue),
LinkVisited = new SKColor((byte)200, (byte)150, byte.MaxValue),
Selection = new SKColor((byte)0, (byte)120, (byte)215),
SelectionText = SKColors.White,
DisabledText = new SKColor((byte)160, (byte)160, (byte)160),
DisabledBackground = new SKColor((byte)40, (byte)40, (byte)40)
},
HighContrastTheme.BlackOnWhite => new HighContrastColors
{
Background = SKColors.White,
Foreground = SKColors.Black,
Accent = new SKColor((byte)0, (byte)0, (byte)200),
Border = SKColors.Black,
Error = new SKColor((byte)180, (byte)0, (byte)0),
Success = new SKColor((byte)0, (byte)130, (byte)0),
Warning = new SKColor((byte)180, (byte)120, (byte)0),
Link = new SKColor((byte)0, (byte)0, (byte)180),
LinkVisited = new SKColor((byte)80, (byte)0, (byte)120),
Selection = new SKColor((byte)0, (byte)120, (byte)215),
SelectionText = SKColors.White,
DisabledText = new SKColor((byte)100, (byte)100, (byte)100),
DisabledBackground = new SKColor((byte)220, (byte)220, (byte)220)
},
_ => GetDefaultColors(),
};
}
return true;
}
catch
{
return false;
}
}
private static HighContrastColors GetDefaultColors()
{
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_0048: Unknown result type (might be due to invalid IL or missing references)
//IL_005c: Unknown result type (might be due to invalid IL or missing references)
//IL_0070: Unknown result type (might be due to invalid IL or missing references)
//IL_0086: Unknown result type (might be due to invalid IL or missing references)
//IL_009d: Unknown result type (might be due to invalid IL or missing references)
//IL_00b4: Unknown result type (might be due to invalid IL or missing references)
//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
//IL_00d6: Unknown result type (might be due to invalid IL or missing references)
//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
//IL_010a: Unknown result type (might be due to invalid IL or missing references)
return new HighContrastColors
{
Background = SKColors.White,
Foreground = new SKColor((byte)33, (byte)33, (byte)33),
Accent = new SKColor((byte)33, (byte)150, (byte)243),
Border = new SKColor((byte)200, (byte)200, (byte)200),
Error = new SKColor((byte)244, (byte)67, (byte)54),
Success = new SKColor((byte)76, (byte)175, (byte)80),
Warning = new SKColor(byte.MaxValue, (byte)152, (byte)0),
Link = new SKColor((byte)33, (byte)150, (byte)243),
LinkVisited = new SKColor((byte)156, (byte)39, (byte)176),
Selection = new SKColor((byte)33, (byte)150, (byte)243),
SelectionText = SKColors.White,
DisabledText = new SKColor((byte)158, (byte)158, (byte)158),
DisabledBackground = new SKColor((byte)238, (byte)238, (byte)238)
};
}
private static bool TryGetKdeHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
public void ForceHighContrast(bool enabled, HighContrastTheme theme = HighContrastTheme.WhiteOnBlack)
{
UpdateHighContrast(enabled, theme);
}
try
{
// Check kdeglobals for color scheme
var configPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "kdeglobals");
private static string? RunCommand(string command, string arguments)
{
try
{
using Process process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return result;
}
catch
{
return null;
}
}
if (!File.Exists(configPath)) return false;
var lines = File.ReadAllLines(configPath);
foreach (var line in lines)
{
if (line.StartsWith("ColorScheme="))
{
themeName = line.Substring("ColorScheme=".Length);
var lowerTheme = themeName.ToLower();
isEnabled = lowerTheme.Contains("highcontrast") ||
lowerTheme.Contains("high-contrast") ||
lowerTheme.Contains("breeze-high-contrast");
return true;
}
}
return false;
}
catch
{
return false;
}
}
private static bool TryGetGtkHighContrast(out bool isEnabled, out string? themeName)
{
isEnabled = false;
themeName = null;
try
{
// Check GTK settings.ini
var gtkConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "gtk-3.0", "settings.ini");
if (!File.Exists(gtkConfigPath))
{
gtkConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config", "gtk-4.0", "settings.ini");
}
if (!File.Exists(gtkConfigPath)) return false;
var lines = File.ReadAllLines(gtkConfigPath);
foreach (var line in lines)
{
if (line.StartsWith("gtk-theme-name="))
{
themeName = line.Substring("gtk-theme-name=".Length);
var lowerTheme = themeName.ToLower();
isEnabled = lowerTheme.Contains("highcontrast") ||
lowerTheme.Contains("high-contrast");
return true;
}
}
return false;
}
catch
{
return false;
}
}
private static bool TryGetEnvironmentHighContrast(out bool isEnabled)
{
isEnabled = false;
// Check GTK_THEME environment variable
var gtkTheme = Environment.GetEnvironmentVariable("GTK_THEME");
if (!string.IsNullOrEmpty(gtkTheme))
{
var lower = gtkTheme.ToLower();
isEnabled = lower.Contains("highcontrast") || lower.Contains("high-contrast");
if (isEnabled) return true;
}
// Check accessibility-related env vars
var forceA11y = Environment.GetEnvironmentVariable("GTK_A11Y");
if (forceA11y?.ToLower() == "atspi" || forceA11y == "1")
{
// A11y is forced, but doesn't necessarily mean high contrast
}
return isEnabled;
}
private static HighContrastTheme ParseThemeName(string? themeName)
{
if (string.IsNullOrEmpty(themeName))
return HighContrastTheme.WhiteOnBlack;
var lower = themeName.ToLower();
if (lower.Contains("inverse") || lower.Contains("dark") || lower.Contains("white-on-black"))
return HighContrastTheme.WhiteOnBlack;
if (lower.Contains("light") || lower.Contains("black-on-white"))
return HighContrastTheme.BlackOnWhite;
// Default to white on black (more common high contrast choice)
return HighContrastTheme.WhiteOnBlack;
}
/// <summary>
/// Gets the appropriate colors for the current high contrast theme.
/// </summary>
public HighContrastColors GetColors()
{
return _currentTheme switch
{
HighContrastTheme.WhiteOnBlack => new HighContrastColors
{
Background = SKColors.Black,
Foreground = SKColors.White,
Accent = new SKColor(0, 255, 255), // Cyan
Border = SKColors.White,
Error = new SKColor(255, 100, 100),
Success = new SKColor(100, 255, 100),
Warning = SKColors.Yellow,
Link = new SKColor(100, 200, 255),
LinkVisited = new SKColor(200, 150, 255),
Selection = new SKColor(0, 120, 215),
SelectionText = SKColors.White,
DisabledText = new SKColor(160, 160, 160),
DisabledBackground = new SKColor(40, 40, 40)
},
HighContrastTheme.BlackOnWhite => new HighContrastColors
{
Background = SKColors.White,
Foreground = SKColors.Black,
Accent = new SKColor(0, 0, 200), // Dark blue
Border = SKColors.Black,
Error = new SKColor(180, 0, 0),
Success = new SKColor(0, 130, 0),
Warning = new SKColor(180, 120, 0),
Link = new SKColor(0, 0, 180),
LinkVisited = new SKColor(80, 0, 120),
Selection = new SKColor(0, 120, 215),
SelectionText = SKColors.White,
DisabledText = new SKColor(100, 100, 100),
DisabledBackground = new SKColor(220, 220, 220)
},
_ => GetDefaultColors()
};
}
private static HighContrastColors GetDefaultColors()
{
return new HighContrastColors
{
Background = SKColors.White,
Foreground = new SKColor(33, 33, 33),
Accent = new SKColor(33, 150, 243),
Border = new SKColor(200, 200, 200),
Error = new SKColor(244, 67, 54),
Success = new SKColor(76, 175, 80),
Warning = new SKColor(255, 152, 0),
Link = new SKColor(33, 150, 243),
LinkVisited = new SKColor(156, 39, 176),
Selection = new SKColor(33, 150, 243),
SelectionText = SKColors.White,
DisabledText = new SKColor(158, 158, 158),
DisabledBackground = new SKColor(238, 238, 238)
};
}
/// <summary>
/// Forces a specific high contrast mode (for testing or user preference override).
/// </summary>
public void ForceHighContrast(bool enabled, HighContrastTheme theme = HighContrastTheme.WhiteOnBlack)
{
UpdateHighContrast(enabled, theme);
}
private static string? RunCommand(string command, string arguments)
{
try
{
using var process = new System.Diagnostics.Process();
process.StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return output;
}
catch
{
return null;
}
}
}
/// <summary>
/// High contrast theme types.
/// </summary>
public enum HighContrastTheme
{
None,
WhiteOnBlack,
BlackOnWhite
}
/// <summary>
/// Color palette for high contrast mode.
/// </summary>
public class HighContrastColors
{
public SKColor Background { get; set; }
public SKColor Foreground { get; set; }
public SKColor Accent { get; set; }
public SKColor Border { get; set; }
public SKColor Error { get; set; }
public SKColor Success { get; set; }
public SKColor Warning { get; set; }
public SKColor Link { get; set; }
public SKColor LinkVisited { get; set; }
public SKColor Selection { get; set; }
public SKColor SelectionText { get; set; }
public SKColor DisabledText { get; set; }
public SKColor DisabledBackground { get; set; }
}
/// <summary>
/// Event args for high contrast mode changes.
/// </summary>
public class HighContrastChangedEventArgs : EventArgs
{
/// <summary>
/// Gets whether high contrast mode is enabled.
/// </summary>
public bool IsEnabled { get; }
/// <summary>
/// Gets the current theme.
/// </summary>
public HighContrastTheme Theme { get; }
public HighContrastChangedEventArgs(bool isEnabled, HighContrastTheme theme)
{
IsEnabled = isEnabled;
Theme = theme;
}
}

View File

@@ -1,8 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum HighContrastTheme
{
None,
WhiteOnBlack,
BlackOnWhite
}

View File

@@ -1,19 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class HotkeyEventArgs : EventArgs
{
public int Id { get; }
public HotkeyKey Key { get; }
public HotkeyModifiers Modifiers { get; }
public HotkeyEventArgs(int id, HotkeyKey key, HotkeyModifiers modifiers)
{
Id = id;
Key = key;
Modifiers = modifiers;
}
}

View File

@@ -1,76 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum HotkeyKey : uint
{
A = 97u,
B = 98u,
C = 99u,
D = 100u,
E = 101u,
F = 102u,
G = 103u,
H = 104u,
I = 105u,
J = 106u,
K = 107u,
L = 108u,
M = 109u,
N = 110u,
O = 111u,
P = 112u,
Q = 113u,
R = 114u,
S = 115u,
T = 116u,
U = 117u,
V = 118u,
W = 119u,
X = 120u,
Y = 121u,
Z = 122u,
D0 = 48u,
D1 = 49u,
D2 = 50u,
D3 = 51u,
D4 = 52u,
D5 = 53u,
D6 = 54u,
D7 = 55u,
D8 = 56u,
D9 = 57u,
F1 = 65470u,
F2 = 65471u,
F3 = 65472u,
F4 = 65473u,
F5 = 65474u,
F6 = 65475u,
F7 = 65476u,
F8 = 65477u,
F9 = 65478u,
F10 = 65479u,
F11 = 65480u,
F12 = 65481u,
Escape = 65307u,
Tab = 65289u,
Return = 65293u,
Space = 32u,
BackSpace = 65288u,
Delete = 65535u,
Insert = 65379u,
Home = 65360u,
End = 65367u,
PageUp = 65365u,
PageDown = 65366u,
Left = 65361u,
Up = 65362u,
Right = 65363u,
Down = 65364u,
AudioPlay = 269025044u,
AudioStop = 269025045u,
AudioPrev = 269025046u,
AudioNext = 269025047u,
AudioMute = 269025042u,
AudioRaiseVolume = 269025043u,
AudioLowerVolume = 269025041u,
Print = 65377u
}

View File

@@ -1,13 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
[Flags]
public enum HotkeyModifiers
{
None = 0,
Shift = 1,
Control = 2,
Alt = 4,
Super = 8
}

View File

@@ -1,22 +1,436 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Interface for accessibility services using AT-SPI2.
/// Provides screen reader support on Linux.
/// </summary>
public interface IAccessibilityService
{
bool IsEnabled { get; }
/// <summary>
/// Gets whether accessibility is enabled.
/// </summary>
bool IsEnabled { get; }
void Initialize();
/// <summary>
/// Initializes the accessibility service.
/// </summary>
void Initialize();
void Register(IAccessible accessible);
/// <summary>
/// Registers an accessible object.
/// </summary>
/// <param name="accessible">The accessible object to register.</param>
void Register(IAccessible accessible);
void Unregister(IAccessible accessible);
/// <summary>
/// Unregisters an accessible object.
/// </summary>
/// <param name="accessible">The accessible object to unregister.</param>
void Unregister(IAccessible accessible);
void NotifyFocusChanged(IAccessible? accessible);
/// <summary>
/// Notifies that focus has changed.
/// </summary>
/// <param name="accessible">The newly focused accessible object.</param>
void NotifyFocusChanged(IAccessible? accessible);
void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property);
/// <summary>
/// Notifies that a property has changed.
/// </summary>
/// <param name="accessible">The accessible object.</param>
/// <param name="property">The property that changed.</param>
void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property);
void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value);
/// <summary>
/// Notifies that an accessible's state has changed.
/// </summary>
/// <param name="accessible">The accessible object.</param>
/// <param name="state">The state that changed.</param>
/// <param name="value">The new value of the state.</param>
void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value);
void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite);
/// <summary>
/// Announces text to the screen reader.
/// </summary>
/// <param name="text">The text to announce.</param>
/// <param name="priority">The announcement priority.</param>
void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite);
void Shutdown();
/// <summary>
/// Shuts down the accessibility service.
/// </summary>
void Shutdown();
}
/// <summary>
/// Interface for accessible objects.
/// </summary>
public interface IAccessible
{
/// <summary>
/// Gets the unique identifier for this accessible.
/// </summary>
string AccessibleId { get; }
/// <summary>
/// Gets the accessible name (label for screen readers).
/// </summary>
string AccessibleName { get; }
/// <summary>
/// Gets the accessible description (additional context).
/// </summary>
string AccessibleDescription { get; }
/// <summary>
/// Gets the accessible role.
/// </summary>
AccessibleRole Role { get; }
/// <summary>
/// Gets the accessible states.
/// </summary>
AccessibleStates States { get; }
/// <summary>
/// Gets the parent accessible.
/// </summary>
IAccessible? Parent { get; }
/// <summary>
/// Gets the child accessibles.
/// </summary>
IReadOnlyList<IAccessible> Children { get; }
/// <summary>
/// Gets the bounding rectangle in screen coordinates.
/// </summary>
AccessibleRect Bounds { get; }
/// <summary>
/// Gets the available actions.
/// </summary>
IReadOnlyList<AccessibleAction> Actions { get; }
/// <summary>
/// Performs an action.
/// </summary>
/// <param name="actionName">The name of the action to perform.</param>
/// <returns>True if the action was performed.</returns>
bool DoAction(string actionName);
/// <summary>
/// Gets the accessible value (for sliders, progress bars, etc.).
/// </summary>
double? Value { get; }
/// <summary>
/// Gets the minimum value.
/// </summary>
double? MinValue { get; }
/// <summary>
/// Gets the maximum value.
/// </summary>
double? MaxValue { get; }
/// <summary>
/// Sets the accessible value.
/// </summary>
bool SetValue(double value);
}
/// <summary>
/// Interface for accessible text components.
/// </summary>
public interface IAccessibleText : IAccessible
{
/// <summary>
/// Gets the text content.
/// </summary>
string Text { get; }
/// <summary>
/// Gets the caret offset.
/// </summary>
int CaretOffset { get; }
/// <summary>
/// Gets the number of selections.
/// </summary>
int SelectionCount { get; }
/// <summary>
/// Gets the selection at the specified index.
/// </summary>
(int Start, int End) GetSelection(int index);
/// <summary>
/// Sets the selection.
/// </summary>
bool SetSelection(int index, int start, int end);
/// <summary>
/// Gets the character at the specified offset.
/// </summary>
char GetCharacterAtOffset(int offset);
/// <summary>
/// Gets the text in the specified range.
/// </summary>
string GetTextInRange(int start, int end);
/// <summary>
/// Gets the bounds of the character at the specified offset.
/// </summary>
AccessibleRect GetCharacterBounds(int offset);
}
/// <summary>
/// Interface for editable text components.
/// </summary>
public interface IAccessibleEditableText : IAccessibleText
{
/// <summary>
/// Sets the text content.
/// </summary>
bool SetText(string text);
/// <summary>
/// Inserts text at the specified position.
/// </summary>
bool InsertText(int position, string text);
/// <summary>
/// Deletes text in the specified range.
/// </summary>
bool DeleteText(int start, int end);
/// <summary>
/// Copies text to clipboard.
/// </summary>
bool CopyText(int start, int end);
/// <summary>
/// Cuts text to clipboard.
/// </summary>
bool CutText(int start, int end);
/// <summary>
/// Pastes text from clipboard.
/// </summary>
bool PasteText(int position);
}
/// <summary>
/// Accessible roles (based on AT-SPI2 roles).
/// </summary>
public enum AccessibleRole
{
Unknown,
Window,
Application,
Panel,
Frame,
Button,
CheckBox,
RadioButton,
ComboBox,
Entry,
Label,
List,
ListItem,
Menu,
MenuBar,
MenuItem,
ScrollBar,
Slider,
SpinButton,
StatusBar,
Tab,
TabPanel,
Text,
ToggleButton,
ToolBar,
ToolTip,
Tree,
TreeItem,
Image,
ProgressBar,
Separator,
Link,
Table,
TableCell,
TableRow,
TableColumnHeader,
TableRowHeader,
PageTab,
PageTabList,
Dialog,
Alert,
Filler,
Icon,
Canvas
}
/// <summary>
/// Accessible states.
/// </summary>
[Flags]
public enum AccessibleStates : long
{
None = 0,
Active = 1L << 0,
Armed = 1L << 1,
Busy = 1L << 2,
Checked = 1L << 3,
Collapsed = 1L << 4,
Defunct = 1L << 5,
Editable = 1L << 6,
Enabled = 1L << 7,
Expandable = 1L << 8,
Expanded = 1L << 9,
Focusable = 1L << 10,
Focused = 1L << 11,
HasToolTip = 1L << 12,
Horizontal = 1L << 13,
Iconified = 1L << 14,
Modal = 1L << 15,
MultiLine = 1L << 16,
MultiSelectable = 1L << 17,
Opaque = 1L << 18,
Pressed = 1L << 19,
Resizable = 1L << 20,
Selectable = 1L << 21,
Selected = 1L << 22,
Sensitive = 1L << 23,
Showing = 1L << 24,
SingleLine = 1L << 25,
Stale = 1L << 26,
Transient = 1L << 27,
Vertical = 1L << 28,
Visible = 1L << 29,
ManagesDescendants = 1L << 30,
Indeterminate = 1L << 31,
Required = 1L << 32,
Truncated = 1L << 33,
Animated = 1L << 34,
InvalidEntry = 1L << 35,
SupportsAutocompletion = 1L << 36,
SelectableText = 1L << 37,
IsDefault = 1L << 38,
Visited = 1L << 39,
ReadOnly = 1L << 40
}
/// <summary>
/// Accessible state enumeration for notifications.
/// </summary>
public enum AccessibleState
{
Active,
Armed,
Busy,
Checked,
Collapsed,
Defunct,
Editable,
Enabled,
Expandable,
Expanded,
Focusable,
Focused,
Horizontal,
Iconified,
Modal,
MultiLine,
Opaque,
Pressed,
Resizable,
Selectable,
Selected,
Sensitive,
Showing,
SingleLine,
Stale,
Transient,
Vertical,
Visible,
ManagesDescendants,
Indeterminate,
Required,
InvalidEntry,
ReadOnly
}
/// <summary>
/// Accessible property for notifications.
/// </summary>
public enum AccessibleProperty
{
Name,
Description,
Role,
Value,
Parent,
Children
}
/// <summary>
/// Announcement priority.
/// </summary>
public enum AnnouncementPriority
{
/// <summary>
/// Low priority - can be interrupted.
/// </summary>
Polite,
/// <summary>
/// High priority - interrupts current speech.
/// </summary>
Assertive
}
/// <summary>
/// Represents an accessible action.
/// </summary>
public class AccessibleAction
{
/// <summary>
/// The action name.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The action description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// The keyboard shortcut for this action.
/// </summary>
public string? KeyBinding { get; set; }
}
/// <summary>
/// Represents a rectangle in accessible coordinates.
/// </summary>
public struct AccessibleRect
{
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public AccessibleRect(int x, int y, int width, int height)
{
X = x;
Y = y;
Width = width;
Height = height;
}
}

View File

@@ -1,34 +0,0 @@
using System.Collections.Generic;
namespace Microsoft.Maui.Platform.Linux.Services;
public interface IAccessible
{
string AccessibleId { get; }
string AccessibleName { get; }
string AccessibleDescription { get; }
AccessibleRole Role { get; }
AccessibleStates States { get; }
IAccessible? Parent { get; }
IReadOnlyList<IAccessible> Children { get; }
AccessibleRect Bounds { get; }
IReadOnlyList<AccessibleAction> Actions { get; }
double? Value { get; }
double? MinValue { get; }
double? MaxValue { get; }
bool DoAction(string actionName);
bool SetValue(double value);
}

View File

@@ -1,16 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public interface IAccessibleEditableText : IAccessibleText, IAccessible
{
bool SetText(string text);
bool InsertText(int position, string text);
bool DeleteText(int start, int end);
bool CopyText(int start, int end);
bool CutText(int start, int end);
bool PasteText(int position);
}

View File

@@ -1,20 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public interface IAccessibleText : IAccessible
{
string Text { get; }
int CaretOffset { get; }
int SelectionCount { get; }
(int Start, int End) GetSelection(int index);
bool SetSelection(int index, int start, int end);
char GetCharacterAtOffset(int offset);
string GetTextInRange(int start, int end);
AccessibleRect GetCharacterBounds(int offset);
}

View File

@@ -1,382 +1,379 @@
using System;
using System.Collections.Generic;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// IBus Input Method service using D-Bus interface.
/// Provides modern IME support on Linux desktops.
/// </summary>
public class IBusInputMethodService : IInputMethodService, IDisposable
{
private delegate void IBusCommitTextCallback(IntPtr context, IntPtr text, IntPtr userData);
private nint _bus;
private nint _context;
private IInputContext? _currentContext;
private string _preEditText = string.Empty;
private int _preEditCursorPosition;
private bool _isActive;
private bool _disposed;
// Callback delegates (prevent GC)
private IBusCommitTextCallback? _commitCallback;
private IBusUpdatePreeditTextCallback? _preeditCallback;
private IBusShowPreeditTextCallback? _showPreeditCallback;
private IBusHidePreeditTextCallback? _hidePreeditCallback;
public bool IsActive => _isActive;
public string PreEditText => _preEditText;
public int PreEditCursorPosition => _preEditCursorPosition;
public event EventHandler<TextCommittedEventArgs>? TextCommitted;
public event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
public event EventHandler? PreEditEnded;
public void Initialize(nint windowHandle)
{
try
{
// Initialize IBus
ibus_init();
// Get the IBus bus connection
_bus = ibus_bus_new();
if (_bus == IntPtr.Zero)
{
Console.WriteLine("IBusInputMethodService: Failed to connect to IBus");
return;
}
// Check if IBus is connected
if (!ibus_bus_is_connected(_bus))
{
Console.WriteLine("IBusInputMethodService: IBus not connected");
return;
}
// Create input context
_context = ibus_bus_create_input_context(_bus, "maui-linux");
if (_context == IntPtr.Zero)
{
Console.WriteLine("IBusInputMethodService: Failed to create input context");
return;
}
// Set capabilities
uint capabilities = IBUS_CAP_PREEDIT_TEXT | IBUS_CAP_FOCUS | IBUS_CAP_SURROUNDING_TEXT;
ibus_input_context_set_capabilities(_context, capabilities);
// Connect signals
ConnectSignals();
Console.WriteLine("IBusInputMethodService: Initialized successfully");
}
catch (Exception ex)
{
Console.WriteLine($"IBusInputMethodService: Initialization failed - {ex.Message}");
}
}
private void ConnectSignals()
{
if (_context == IntPtr.Zero) return;
// Set up callbacks for IBus signals
_commitCallback = OnCommitText;
_preeditCallback = OnUpdatePreeditText;
_showPreeditCallback = OnShowPreeditText;
_hidePreeditCallback = OnHidePreeditText;
// Connect to commit-text signal
g_signal_connect(_context, "commit-text",
Marshal.GetFunctionPointerForDelegate(_commitCallback), IntPtr.Zero);
// Connect to update-preedit-text signal
g_signal_connect(_context, "update-preedit-text",
Marshal.GetFunctionPointerForDelegate(_preeditCallback), IntPtr.Zero);
// Connect to show-preedit-text signal
g_signal_connect(_context, "show-preedit-text",
Marshal.GetFunctionPointerForDelegate(_showPreeditCallback), IntPtr.Zero);
// Connect to hide-preedit-text signal
g_signal_connect(_context, "hide-preedit-text",
Marshal.GetFunctionPointerForDelegate(_hidePreeditCallback), IntPtr.Zero);
}
private void OnCommitText(nint context, nint text, nint userData)
{
if (text == IntPtr.Zero) return;
string committedText = GetIBusTextString(text);
if (!string.IsNullOrEmpty(committedText))
{
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
TextCommitted?.Invoke(this, new TextCommittedEventArgs(committedText));
_currentContext?.OnTextCommitted(committedText);
}
}
private void OnUpdatePreeditText(nint context, nint text, uint cursorPos, bool visible, nint userData)
{
if (!visible)
{
OnHidePreeditText(context, userData);
return;
}
_isActive = true;
_preEditText = text != IntPtr.Zero ? GetIBusTextString(text) : string.Empty;
_preEditCursorPosition = (int)cursorPos;
var attributes = GetPreeditAttributes(text);
PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, attributes));
_currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition);
}
private void OnShowPreeditText(nint context, nint userData)
{
_isActive = true;
}
private void OnHidePreeditText(nint context, nint userData)
{
_isActive = false;
_preEditText = string.Empty;
_preEditCursorPosition = 0;
PreEditEnded?.Invoke(this, EventArgs.Empty);
_currentContext?.OnPreEditEnded();
}
private string GetIBusTextString(nint ibusText)
{
if (ibusText == IntPtr.Zero) return string.Empty;
nint textPtr = ibus_text_get_text(ibusText);
if (textPtr == IntPtr.Zero) return string.Empty;
return Marshal.PtrToStringUTF8(textPtr) ?? string.Empty;
}
private List<PreEditAttribute> GetPreeditAttributes(nint ibusText)
{
var attributes = new List<PreEditAttribute>();
if (ibusText == IntPtr.Zero) return attributes;
nint attrList = ibus_text_get_attributes(ibusText);
if (attrList == IntPtr.Zero) return attributes;
uint count = ibus_attr_list_size(attrList);
for (uint i = 0; i < count; i++)
{
nint attr = ibus_attr_list_get(attrList, i);
if (attr == IntPtr.Zero) continue;
var type = ibus_attribute_get_attr_type(attr);
var start = ibus_attribute_get_start_index(attr);
var end = ibus_attribute_get_end_index(attr);
attributes.Add(new PreEditAttribute
{
Start = (int)start,
Length = (int)(end - start),
Type = ConvertAttributeType(type)
});
}
return attributes;
}
private PreEditAttributeType ConvertAttributeType(uint ibusType)
{
return ibusType switch
{
IBUS_ATTR_TYPE_UNDERLINE => PreEditAttributeType.Underline,
IBUS_ATTR_TYPE_FOREGROUND => PreEditAttributeType.Highlighted,
IBUS_ATTR_TYPE_BACKGROUND => PreEditAttributeType.Reverse,
_ => PreEditAttributeType.None
};
}
public void SetFocus(IInputContext? context)
{
_currentContext = context;
if (_context != IntPtr.Zero)
{
if (context != null)
{
ibus_input_context_focus_in(_context);
}
else
{
ibus_input_context_focus_out(_context);
}
}
}
public void SetCursorLocation(int x, int y, int width, int height)
{
if (_context == IntPtr.Zero) return;
ibus_input_context_set_cursor_location(_context, x, y, width, height);
}
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown)
{
if (_context == IntPtr.Zero) return false;
uint state = ConvertModifiers(modifiers);
if (!isKeyDown)
{
state |= IBUS_RELEASE_MASK;
}
return ibus_input_context_process_key_event(_context, keyCode, keyCode, state);
}
private uint ConvertModifiers(KeyModifiers modifiers)
{
uint state = 0;
if (modifiers.HasFlag(KeyModifiers.Shift)) state |= IBUS_SHIFT_MASK;
if (modifiers.HasFlag(KeyModifiers.Control)) state |= IBUS_CONTROL_MASK;
if (modifiers.HasFlag(KeyModifiers.Alt)) state |= IBUS_MOD1_MASK;
if (modifiers.HasFlag(KeyModifiers.Super)) state |= IBUS_SUPER_MASK;
if (modifiers.HasFlag(KeyModifiers.CapsLock)) state |= IBUS_LOCK_MASK;
return state;
}
public void Reset()
{
if (_context != IntPtr.Zero)
{
ibus_input_context_reset(_context);
}
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
PreEditEnded?.Invoke(this, EventArgs.Empty);
_currentContext?.OnPreEditEnded();
}
public void Shutdown()
{
Dispose();
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_context != IntPtr.Zero)
{
ibus_input_context_focus_out(_context);
g_object_unref(_context);
_context = IntPtr.Zero;
}
if (_bus != IntPtr.Zero)
{
g_object_unref(_bus);
_bus = IntPtr.Zero;
}
}
#region IBus Constants
private const uint IBUS_CAP_PREEDIT_TEXT = 1 << 0;
private const uint IBUS_CAP_FOCUS = 1 << 3;
private const uint IBUS_CAP_SURROUNDING_TEXT = 1 << 5;
private const uint IBUS_SHIFT_MASK = 1 << 0;
private const uint IBUS_LOCK_MASK = 1 << 1;
private const uint IBUS_CONTROL_MASK = 1 << 2;
private const uint IBUS_MOD1_MASK = 1 << 3;
private const uint IBUS_SUPER_MASK = 1 << 26;
private const uint IBUS_RELEASE_MASK = 1 << 30;
private const uint IBUS_ATTR_TYPE_UNDERLINE = 1;
private const uint IBUS_ATTR_TYPE_FOREGROUND = 2;
private const uint IBUS_ATTR_TYPE_BACKGROUND = 3;
#endregion
#region IBus Interop
private delegate void IBusCommitTextCallback(nint context, nint text, nint userData);
private delegate void IBusUpdatePreeditTextCallback(nint context, nint text, uint cursorPos, bool visible, nint userData);
private delegate void IBusShowPreeditTextCallback(nint context, nint userData);
private delegate void IBusHidePreeditTextCallback(nint context, nint userData);
private delegate void IBusUpdatePreeditTextCallback(IntPtr context, IntPtr text, uint cursorPos, bool visible, IntPtr userData);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_init();
private delegate void IBusShowPreeditTextCallback(IntPtr context, IntPtr userData);
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_bus_new();
private delegate void IBusHidePreeditTextCallback(IntPtr context, IntPtr userData);
[DllImport("libibus-1.0.so.5")]
private static extern bool ibus_bus_is_connected(nint bus);
private IntPtr _bus;
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_bus_create_input_context(nint bus, string clientName);
private IntPtr _context;
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_set_capabilities(nint context, uint capabilities);
private IInputContext? _currentContext;
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_focus_in(nint context);
private string _preEditText = string.Empty;
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_focus_out(nint context);
private int _preEditCursorPosition;
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_reset(nint context);
private bool _isActive;
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_set_cursor_location(nint context, int x, int y, int w, int h);
private bool _disposed;
[DllImport("libibus-1.0.so.5")]
private static extern bool ibus_input_context_process_key_event(nint context, uint keyval, uint keycode, uint state);
private IBusCommitTextCallback? _commitCallback;
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_text_get_text(nint text);
private IBusUpdatePreeditTextCallback? _preeditCallback;
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_text_get_attributes(nint text);
private IBusShowPreeditTextCallback? _showPreeditCallback;
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attr_list_size(nint attrList);
private IBusHidePreeditTextCallback? _hidePreeditCallback;
[DllImport("libibus-1.0.so.5")]
private static extern nint ibus_attr_list_get(nint attrList, uint index);
private const uint IBUS_CAP_PREEDIT_TEXT = 1u;
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_attr_type(nint attr);
private const uint IBUS_CAP_FOCUS = 8u;
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_start_index(nint attr);
private const uint IBUS_CAP_SURROUNDING_TEXT = 32u;
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_end_index(nint attr);
private const uint IBUS_SHIFT_MASK = 1u;
[DllImport("libgobject-2.0.so.0")]
private static extern void g_object_unref(nint obj);
private const uint IBUS_LOCK_MASK = 2u;
[DllImport("libgobject-2.0.so.0")]
private static extern ulong g_signal_connect(nint instance, string signal, nint handler, nint data);
private const uint IBUS_CONTROL_MASK = 4u;
private const uint IBUS_MOD1_MASK = 8u;
private const uint IBUS_SUPER_MASK = 67108864u;
private const uint IBUS_RELEASE_MASK = 1073741824u;
private const uint IBUS_ATTR_TYPE_UNDERLINE = 1u;
private const uint IBUS_ATTR_TYPE_FOREGROUND = 2u;
private const uint IBUS_ATTR_TYPE_BACKGROUND = 3u;
public bool IsActive => _isActive;
public string PreEditText => _preEditText;
public int PreEditCursorPosition => _preEditCursorPosition;
public event EventHandler<TextCommittedEventArgs>? TextCommitted;
public event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
public event EventHandler? PreEditEnded;
public void Initialize(IntPtr windowHandle)
{
try
{
ibus_init();
_bus = ibus_bus_new();
if (_bus == IntPtr.Zero)
{
Console.WriteLine("IBusInputMethodService: Failed to connect to IBus");
return;
}
if (!ibus_bus_is_connected(_bus))
{
Console.WriteLine("IBusInputMethodService: IBus not connected");
return;
}
_context = ibus_bus_create_input_context(_bus, "maui-linux");
if (_context == IntPtr.Zero)
{
Console.WriteLine("IBusInputMethodService: Failed to create input context");
return;
}
uint capabilities = 41u;
ibus_input_context_set_capabilities(_context, capabilities);
ConnectSignals();
Console.WriteLine("IBusInputMethodService: Initialized successfully");
}
catch (Exception ex)
{
Console.WriteLine("IBusInputMethodService: Initialization failed - " + ex.Message);
}
}
private void ConnectSignals()
{
if (_context != IntPtr.Zero)
{
_commitCallback = OnCommitText;
_preeditCallback = OnUpdatePreeditText;
_showPreeditCallback = OnShowPreeditText;
_hidePreeditCallback = OnHidePreeditText;
g_signal_connect(_context, "commit-text", Marshal.GetFunctionPointerForDelegate(_commitCallback), IntPtr.Zero);
g_signal_connect(_context, "update-preedit-text", Marshal.GetFunctionPointerForDelegate(_preeditCallback), IntPtr.Zero);
g_signal_connect(_context, "show-preedit-text", Marshal.GetFunctionPointerForDelegate(_showPreeditCallback), IntPtr.Zero);
g_signal_connect(_context, "hide-preedit-text", Marshal.GetFunctionPointerForDelegate(_hidePreeditCallback), IntPtr.Zero);
}
}
private void OnCommitText(IntPtr context, IntPtr text, IntPtr userData)
{
if (text != IntPtr.Zero)
{
string iBusTextString = GetIBusTextString(text);
if (!string.IsNullOrEmpty(iBusTextString))
{
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
this.TextCommitted?.Invoke(this, new TextCommittedEventArgs(iBusTextString));
_currentContext?.OnTextCommitted(iBusTextString);
}
}
}
private void OnUpdatePreeditText(IntPtr context, IntPtr text, uint cursorPos, bool visible, IntPtr userData)
{
if (!visible)
{
OnHidePreeditText(context, userData);
return;
}
_isActive = true;
_preEditText = ((text != IntPtr.Zero) ? GetIBusTextString(text) : string.Empty);
_preEditCursorPosition = (int)cursorPos;
List<PreEditAttribute> preeditAttributes = GetPreeditAttributes(text);
this.PreEditChanged?.Invoke(this, new PreEditChangedEventArgs(_preEditText, _preEditCursorPosition, preeditAttributes));
_currentContext?.OnPreEditChanged(_preEditText, _preEditCursorPosition);
}
private void OnShowPreeditText(IntPtr context, IntPtr userData)
{
_isActive = true;
}
private void OnHidePreeditText(IntPtr context, IntPtr userData)
{
_isActive = false;
_preEditText = string.Empty;
_preEditCursorPosition = 0;
this.PreEditEnded?.Invoke(this, EventArgs.Empty);
_currentContext?.OnPreEditEnded();
}
private string GetIBusTextString(IntPtr ibusText)
{
if (ibusText == IntPtr.Zero)
{
return string.Empty;
}
IntPtr intPtr = ibus_text_get_text(ibusText);
if (intPtr == IntPtr.Zero)
{
return string.Empty;
}
return Marshal.PtrToStringUTF8(intPtr) ?? string.Empty;
}
private List<PreEditAttribute> GetPreeditAttributes(IntPtr ibusText)
{
List<PreEditAttribute> list = new List<PreEditAttribute>();
if (ibusText == IntPtr.Zero)
{
return list;
}
IntPtr intPtr = ibus_text_get_attributes(ibusText);
if (intPtr == IntPtr.Zero)
{
return list;
}
uint num = ibus_attr_list_size(intPtr);
for (uint num2 = 0u; num2 < num; num2++)
{
IntPtr intPtr2 = ibus_attr_list_get(intPtr, num2);
if (intPtr2 != IntPtr.Zero)
{
uint ibusType = ibus_attribute_get_attr_type(intPtr2);
uint num3 = ibus_attribute_get_start_index(intPtr2);
uint num4 = ibus_attribute_get_end_index(intPtr2);
list.Add(new PreEditAttribute
{
Start = (int)num3,
Length = (int)(num4 - num3),
Type = ConvertAttributeType(ibusType)
});
}
}
return list;
}
private PreEditAttributeType ConvertAttributeType(uint ibusType)
{
return ibusType switch
{
1u => PreEditAttributeType.Underline,
2u => PreEditAttributeType.Highlighted,
3u => PreEditAttributeType.Reverse,
_ => PreEditAttributeType.None,
};
}
public void SetFocus(IInputContext? context)
{
_currentContext = context;
if (_context != IntPtr.Zero)
{
if (context != null)
{
ibus_input_context_focus_in(_context);
}
else
{
ibus_input_context_focus_out(_context);
}
}
}
public void SetCursorLocation(int x, int y, int width, int height)
{
if (_context != IntPtr.Zero)
{
ibus_input_context_set_cursor_location(_context, x, y, width, height);
}
}
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown)
{
if (_context == IntPtr.Zero)
{
return false;
}
uint num = ConvertModifiers(modifiers);
if (!isKeyDown)
{
num |= 0x40000000;
}
return ibus_input_context_process_key_event(_context, keyCode, keyCode, num);
}
private uint ConvertModifiers(KeyModifiers modifiers)
{
uint num = 0u;
if (modifiers.HasFlag(KeyModifiers.Shift))
{
num |= 1;
}
if (modifiers.HasFlag(KeyModifiers.Control))
{
num |= 4;
}
if (modifiers.HasFlag(KeyModifiers.Alt))
{
num |= 8;
}
if (modifiers.HasFlag(KeyModifiers.Super))
{
num |= 0x4000000;
}
if (modifiers.HasFlag(KeyModifiers.CapsLock))
{
num |= 2;
}
return num;
}
public void Reset()
{
if (_context != IntPtr.Zero)
{
ibus_input_context_reset(_context);
}
_preEditText = string.Empty;
_preEditCursorPosition = 0;
_isActive = false;
this.PreEditEnded?.Invoke(this, EventArgs.Empty);
_currentContext?.OnPreEditEnded();
}
public void Shutdown()
{
Dispose();
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
if (_context != IntPtr.Zero)
{
ibus_input_context_focus_out(_context);
g_object_unref(_context);
_context = IntPtr.Zero;
}
if (_bus != IntPtr.Zero)
{
g_object_unref(_bus);
_bus = IntPtr.Zero;
}
}
}
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_init();
[DllImport("libibus-1.0.so.5")]
private static extern IntPtr ibus_bus_new();
[DllImport("libibus-1.0.so.5")]
private static extern bool ibus_bus_is_connected(IntPtr bus);
[DllImport("libibus-1.0.so.5")]
private static extern IntPtr ibus_bus_create_input_context(IntPtr bus, string clientName);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_set_capabilities(IntPtr context, uint capabilities);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_focus_in(IntPtr context);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_focus_out(IntPtr context);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_reset(IntPtr context);
[DllImport("libibus-1.0.so.5")]
private static extern void ibus_input_context_set_cursor_location(IntPtr context, int x, int y, int w, int h);
[DllImport("libibus-1.0.so.5")]
private static extern bool ibus_input_context_process_key_event(IntPtr context, uint keyval, uint keycode, uint state);
[DllImport("libibus-1.0.so.5")]
private static extern IntPtr ibus_text_get_text(IntPtr text);
[DllImport("libibus-1.0.so.5")]
private static extern IntPtr ibus_text_get_attributes(IntPtr text);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attr_list_size(IntPtr attrList);
[DllImport("libibus-1.0.so.5")]
private static extern IntPtr ibus_attr_list_get(IntPtr attrList, uint index);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_attr_type(IntPtr attr);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_start_index(IntPtr attr);
[DllImport("libibus-1.0.so.5")]
private static extern uint ibus_attribute_get_end_index(IntPtr attr);
[DllImport("libgobject-2.0.so.0")]
private static extern void g_object_unref(IntPtr obj);
[DllImport("libgobject-2.0.so.0")]
private static extern ulong g_signal_connect(IntPtr instance, string signal, IntPtr handler, IntPtr data);
#endregion
}

View File

@@ -1,44 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public interface IDisplayWindow : IDisposable
{
int Width { get; }
int Height { get; }
bool IsRunning { get; }
event EventHandler<KeyEventArgs>? KeyDown;
event EventHandler<KeyEventArgs>? KeyUp;
event EventHandler<TextInputEventArgs>? TextInput;
event EventHandler<PointerEventArgs>? PointerMoved;
event EventHandler<PointerEventArgs>? PointerPressed;
event EventHandler<PointerEventArgs>? PointerReleased;
event EventHandler<ScrollEventArgs>? Scroll;
event EventHandler? Exposed;
event EventHandler<(int Width, int Height)>? Resized;
event EventHandler? CloseRequested;
void Show();
void Hide();
void SetTitle(string title);
void Resize(int width, int height);
void ProcessEvents();
void Stop();
}

View File

@@ -1,18 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public interface IInputContext
{
string Text { get; set; }
int CursorPosition { get; set; }
int SelectionStart { get; }
int SelectionLength { get; }
void OnTextCommitted(string text);
void OnPreEditChanged(string preEditText, int cursorPosition);
void OnPreEditEnded();
}

View File

@@ -1,30 +1,231 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Interface for Input Method Editor (IME) services.
/// Provides support for complex text input methods like CJK languages.
/// </summary>
public interface IInputMethodService
{
bool IsActive { get; }
/// <summary>
/// Gets whether IME is currently active.
/// </summary>
bool IsActive { get; }
string PreEditText { get; }
/// <summary>
/// Gets the current pre-edit (composition) text.
/// </summary>
string PreEditText { get; }
int PreEditCursorPosition { get; }
/// <summary>
/// Gets the cursor position within the pre-edit text.
/// </summary>
int PreEditCursorPosition { get; }
event EventHandler<TextCommittedEventArgs>? TextCommitted;
/// <summary>
/// Initializes the IME service for the given window.
/// </summary>
/// <param name="windowHandle">The native window handle.</param>
void Initialize(nint windowHandle);
event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
/// <summary>
/// Sets focus to the specified input context.
/// </summary>
/// <param name="context">The input context to focus.</param>
void SetFocus(IInputContext? context);
event EventHandler? PreEditEnded;
/// <summary>
/// Sets the cursor location for candidate window positioning.
/// </summary>
/// <param name="x">X coordinate in screen space.</param>
/// <param name="y">Y coordinate in screen space.</param>
/// <param name="width">Width of the cursor area.</param>
/// <param name="height">Height of the cursor area.</param>
void SetCursorLocation(int x, int y, int width, int height);
void Initialize(IntPtr windowHandle);
/// <summary>
/// Processes a key event through the IME.
/// </summary>
/// <param name="keyCode">The key code.</param>
/// <param name="modifiers">Key modifiers.</param>
/// <param name="isKeyDown">True for key press, false for key release.</param>
/// <returns>True if the IME handled the event.</returns>
bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown);
void SetFocus(IInputContext? context);
/// <summary>
/// Resets the IME state, canceling any composition.
/// </summary>
void Reset();
void SetCursorLocation(int x, int y, int width, int height);
/// <summary>
/// Shuts down the IME service.
/// </summary>
void Shutdown();
bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown);
/// <summary>
/// Event raised when text is committed from IME.
/// </summary>
event EventHandler<TextCommittedEventArgs>? TextCommitted;
void Reset();
/// <summary>
/// Event raised when pre-edit (composition) text changes.
/// </summary>
event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
void Shutdown();
/// <summary>
/// Event raised when pre-edit is completed or cancelled.
/// </summary>
event EventHandler? PreEditEnded;
}
/// <summary>
/// Represents an input context that can receive IME input.
/// </summary>
public interface IInputContext
{
/// <summary>
/// Gets or sets the current text content.
/// </summary>
string Text { get; set; }
/// <summary>
/// Gets or sets the cursor position.
/// </summary>
int CursorPosition { get; set; }
/// <summary>
/// Gets the selection start position.
/// </summary>
int SelectionStart { get; }
/// <summary>
/// Gets the selection length.
/// </summary>
int SelectionLength { get; }
/// <summary>
/// Called when text is committed from the IME.
/// </summary>
/// <param name="text">The committed text.</param>
void OnTextCommitted(string text);
/// <summary>
/// Called when pre-edit text changes.
/// </summary>
/// <param name="preEditText">The current pre-edit text.</param>
/// <param name="cursorPosition">Cursor position within pre-edit text.</param>
void OnPreEditChanged(string preEditText, int cursorPosition);
/// <summary>
/// Called when pre-edit mode ends.
/// </summary>
void OnPreEditEnded();
}
/// <summary>
/// Event args for text committed events.
/// </summary>
public class TextCommittedEventArgs : EventArgs
{
/// <summary>
/// The committed text.
/// </summary>
public string Text { get; }
public TextCommittedEventArgs(string text)
{
Text = text;
}
}
/// <summary>
/// Event args for pre-edit changed events.
/// </summary>
public class PreEditChangedEventArgs : EventArgs
{
/// <summary>
/// The current pre-edit text.
/// </summary>
public string PreEditText { get; }
/// <summary>
/// Cursor position within the pre-edit text.
/// </summary>
public int CursorPosition { get; }
/// <summary>
/// Formatting attributes for the pre-edit text.
/// </summary>
public IReadOnlyList<PreEditAttribute> Attributes { get; }
public PreEditChangedEventArgs(string preEditText, int cursorPosition, IReadOnlyList<PreEditAttribute>? attributes = null)
{
PreEditText = preEditText;
CursorPosition = cursorPosition;
Attributes = attributes ?? Array.Empty<PreEditAttribute>();
}
}
/// <summary>
/// Represents formatting for a portion of pre-edit text.
/// </summary>
public class PreEditAttribute
{
/// <summary>
/// Start position in the pre-edit text.
/// </summary>
public int Start { get; set; }
/// <summary>
/// Length of the attributed range.
/// </summary>
public int Length { get; set; }
/// <summary>
/// The attribute type.
/// </summary>
public PreEditAttributeType Type { get; set; }
}
/// <summary>
/// Types of pre-edit text attributes.
/// </summary>
public enum PreEditAttributeType
{
/// <summary>
/// Normal text (no special formatting).
/// </summary>
None,
/// <summary>
/// Underlined text (typical for composition).
/// </summary>
Underline,
/// <summary>
/// Highlighted/selected text.
/// </summary>
Highlighted,
/// <summary>
/// Reverse video (selected clause in some IMEs).
/// </summary>
Reverse
}
/// <summary>
/// Key modifiers for IME processing.
/// </summary>
[Flags]
public enum KeyModifiers
{
None = 0,
Shift = 1 << 0,
Control = 1 << 1,
Alt = 1 << 2,
Super = 1 << 3,
CapsLock = 1 << 4,
NumLock = 1 << 5
}

View File

@@ -1,154 +1,172 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Factory for creating the appropriate Input Method service.
/// Automatically selects IBus or XIM based on availability.
/// </summary>
public static class InputMethodServiceFactory
{
private static IInputMethodService? _instance;
private static IInputMethodService? _instance;
private static readonly object _lock = new();
private static readonly object _lock = new object();
/// <summary>
/// Gets the singleton input method service instance.
/// </summary>
public static IInputMethodService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
_instance ??= CreateService();
}
}
return _instance;
}
}
public static IInputMethodService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = CreateService();
}
}
}
return _instance;
}
}
/// <summary>
/// Creates the most appropriate input method service for the current environment.
/// </summary>
public static IInputMethodService CreateService()
{
// Check environment variable for user preference
var imePreference = Environment.GetEnvironmentVariable("MAUI_INPUT_METHOD");
public static IInputMethodService CreateService()
{
string environmentVariable = Environment.GetEnvironmentVariable("MAUI_INPUT_METHOD");
if (!string.IsNullOrEmpty(environmentVariable))
{
switch (environmentVariable.ToLowerInvariant())
{
case "ibus":
return CreateIBusService();
case "fcitx":
case "fcitx5":
return CreateFcitx5Service();
case "xim":
return CreateXIMService();
case "none":
return new NullInputMethodService();
default:
return CreateAutoService();
}
}
return CreateAutoService();
}
if (!string.IsNullOrEmpty(imePreference))
{
return imePreference.ToLowerInvariant() switch
{
"ibus" => CreateIBusService(),
"xim" => CreateXIMService(),
"none" => new NullInputMethodService(),
_ => CreateAutoService()
};
}
private static IInputMethodService CreateAutoService()
{
string obj = Environment.GetEnvironmentVariable("GTK_IM_MODULE")?.ToLowerInvariant();
if (obj != null && obj.Contains("fcitx") && Fcitx5InputMethodService.IsAvailable())
{
Console.WriteLine("InputMethodServiceFactory: Using Fcitx5");
return CreateFcitx5Service();
}
if (IsIBusAvailable())
{
Console.WriteLine("InputMethodServiceFactory: Using IBus");
return CreateIBusService();
}
if (Fcitx5InputMethodService.IsAvailable())
{
Console.WriteLine("InputMethodServiceFactory: Using Fcitx5");
return CreateFcitx5Service();
}
if (IsXIMAvailable())
{
Console.WriteLine("InputMethodServiceFactory: Using XIM");
return CreateXIMService();
}
Console.WriteLine("InputMethodServiceFactory: No IME available, using null service");
return new NullInputMethodService();
}
return CreateAutoService();
}
private static IInputMethodService CreateIBusService()
{
try
{
return new IBusInputMethodService();
}
catch (Exception ex)
{
Console.WriteLine("InputMethodServiceFactory: Failed to create IBus service - " + ex.Message);
return new NullInputMethodService();
}
}
private static IInputMethodService CreateAutoService()
{
// Try IBus first (most common on modern Linux)
if (IsIBusAvailable())
{
Console.WriteLine("InputMethodServiceFactory: Using IBus");
return CreateIBusService();
}
private static IInputMethodService CreateFcitx5Service()
{
try
{
return new Fcitx5InputMethodService();
}
catch (Exception ex)
{
Console.WriteLine("InputMethodServiceFactory: Failed to create Fcitx5 service - " + ex.Message);
return new NullInputMethodService();
}
}
// Fall back to XIM
if (IsXIMAvailable())
{
Console.WriteLine("InputMethodServiceFactory: Using XIM");
return CreateXIMService();
}
private static IInputMethodService CreateXIMService()
{
try
{
return new X11InputMethodService();
}
catch (Exception ex)
{
Console.WriteLine("InputMethodServiceFactory: Failed to create XIM service - " + ex.Message);
return new NullInputMethodService();
}
}
// No IME available
Console.WriteLine("InputMethodServiceFactory: No IME available, using null service");
return new NullInputMethodService();
}
private static bool IsIBusAvailable()
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("IBUS_ADDRESS")))
{
return true;
}
try
{
NativeLibrary.Free(NativeLibrary.Load("libibus-1.0.so.5"));
return true;
}
catch
{
return false;
}
}
private static IInputMethodService CreateIBusService()
{
try
{
return new IBusInputMethodService();
}
catch (Exception ex)
{
Console.WriteLine($"InputMethodServiceFactory: Failed to create IBus service - {ex.Message}");
return new NullInputMethodService();
}
}
private static bool IsXIMAvailable()
{
string environmentVariable = Environment.GetEnvironmentVariable("XMODIFIERS");
if (!string.IsNullOrEmpty(environmentVariable) && environmentVariable.Contains("@im="))
{
return true;
}
return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DISPLAY"));
}
private static IInputMethodService CreateXIMService()
{
try
{
return new X11InputMethodService();
}
catch (Exception ex)
{
Console.WriteLine($"InputMethodServiceFactory: Failed to create XIM service - {ex.Message}");
return new NullInputMethodService();
}
}
public static void Reset()
{
lock (_lock)
{
_instance?.Shutdown();
_instance = null;
}
}
private static bool IsIBusAvailable()
{
// Check if IBus daemon is running
var ibusAddress = Environment.GetEnvironmentVariable("IBUS_ADDRESS");
if (!string.IsNullOrEmpty(ibusAddress))
{
return true;
}
// Try to load IBus library
try
{
var handle = NativeLibrary.Load("libibus-1.0.so.5");
NativeLibrary.Free(handle);
return true;
}
catch
{
return false;
}
}
private static bool IsXIMAvailable()
{
// Check XMODIFIERS environment variable
var xmodifiers = Environment.GetEnvironmentVariable("XMODIFIERS");
if (!string.IsNullOrEmpty(xmodifiers) && xmodifiers.Contains("@im="))
{
return true;
}
// Check if running under X11
var display = Environment.GetEnvironmentVariable("DISPLAY");
return !string.IsNullOrEmpty(display);
}
/// <summary>
/// Resets the singleton instance (useful for testing).
/// </summary>
public static void Reset()
{
lock (_lock)
{
_instance?.Shutdown();
_instance = null;
}
}
}
/// <summary>
/// Null implementation of IInputMethodService for when no IME is available.
/// </summary>
public class NullInputMethodService : IInputMethodService
{
public bool IsActive => false;
public string PreEditText => string.Empty;
public int PreEditCursorPosition => 0;
public event EventHandler<TextCommittedEventArgs>? TextCommitted;
public event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
public event EventHandler? PreEditEnded;
public void Initialize(nint windowHandle) { }
public void SetFocus(IInputContext? context) { }
public void SetCursorLocation(int x, int y, int width, int height) { }
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown) => false;
public void Reset() { }
public void Shutdown() { }
}

View File

@@ -1,15 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
[Flags]
public enum KeyModifiers
{
None = 0,
Shift = 1,
Control = 2,
Alt = 4,
Super = 8,
CapsLock = 0x10,
NumLock = 0x20
}

View File

@@ -1,77 +1,85 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux launcher service for opening URLs and files.
/// </summary>
public class LauncherService : ILauncher
{
public Task<bool> CanOpenAsync(Uri uri)
{
return Task.FromResult(result: true);
}
public Task<bool> CanOpenAsync(Uri uri)
{
// On Linux, we can generally open any URI using xdg-open
return Task.FromResult(true);
}
public Task<bool> OpenAsync(Uri uri)
{
return Task.Run(delegate
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = uri.ToString(),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
if (process == null)
{
return false;
}
return true;
}
catch
{
return false;
}
});
}
public Task<bool> OpenAsync(Uri uri)
{
return Task.Run(() =>
{
try
{
var psi = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = uri.ToString(),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
public Task<bool> OpenAsync(OpenFileRequest request)
{
if (request.File == null)
{
return Task.FromResult(result: false);
}
return Task.Run(delegate
{
try
{
string fullPath = ((FileBase)request.File).FullPath;
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = "\"" + fullPath + "\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
});
return process != null;
}
catch
{
return false;
}
});
}
using var process = Process.Start(psi);
if (process == null)
return false;
public Task<bool> TryOpenAsync(Uri uri)
{
return OpenAsync(uri);
}
// Don't wait for the process to exit - xdg-open may spawn another process
return true;
}
catch
{
return false;
}
});
}
public Task<bool> OpenAsync(OpenFileRequest request)
{
if (request.File == null)
return Task.FromResult(false);
return Task.Run(() =>
{
try
{
var filePath = request.File.FullPath;
var psi = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = $"\"{filePath}\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
return process != null;
}
catch
{
return false;
}
});
}
public Task<bool> TryOpenAsync(Uri uri)
{
return OpenAsync(uri);
}
}

View File

@@ -1,11 +0,0 @@
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
internal class LinuxFileResult : FileResult
{
public LinuxFileResult(string fullPath)
: base(fullPath)
{
}
}

View File

@@ -1,109 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Internals;
[assembly: Dependency(typeof(Microsoft.Maui.Platform.Linux.Services.LinuxResourcesProvider))]
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Provides system resources for the Linux platform.
/// </summary>
internal sealed class LinuxResourcesProvider : ISystemResourcesProvider
{
private ResourceDictionary? _dictionary;
private ResourceDictionary? _dictionary;
public IResourceDictionary GetSystemResources()
{
if (_dictionary == null)
{
_dictionary = CreateResourceDictionary();
}
return (IResourceDictionary)(object)_dictionary;
}
public IResourceDictionary GetSystemResources()
{
_dictionary ??= CreateResourceDictionary();
return _dictionary;
}
private ResourceDictionary CreateResourceDictionary()
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0005: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_001f: Expected O, but got Unknown
//IL_001f: Unknown result type (might be due to invalid IL or missing references)
//IL_002f: Unknown result type (might be due to invalid IL or missing references)
//IL_003f: Unknown result type (might be due to invalid IL or missing references)
//IL_004f: Unknown result type (might be due to invalid IL or missing references)
//IL_005f: Unknown result type (might be due to invalid IL or missing references)
//IL_0069: Expected O, but got Unknown
//IL_0069: Unknown result type (might be due to invalid IL or missing references)
//IL_007a: Expected O, but got Unknown
return new ResourceDictionary
{
[Styles.BodyStyleKey] = (object)new Style(typeof(Label)),
[Styles.TitleStyleKey] = CreateTitleStyle(),
[Styles.SubtitleStyleKey] = CreateSubtitleStyle(),
[Styles.CaptionStyleKey] = CreateCaptionStyle(),
[Styles.ListItemTextStyleKey] = (object)new Style(typeof(Label)),
[Styles.ListItemDetailTextStyleKey] = CreateCaptionStyle()
};
}
private ResourceDictionary CreateResourceDictionary()
{
var dictionary = new ResourceDictionary();
private static Style CreateTitleStyle()
{
//IL_000a: Unknown result type (might be due to invalid IL or missing references)
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Expected O, but got Unknown
//IL_003f: Expected O, but got Unknown
return new Style(typeof(Label))
{
Setters =
{
new Setter
{
Property = Label.FontSizeProperty,
Value = 24.0
}
}
};
}
// Add default styles
dictionary[Device.Styles.BodyStyleKey] = new Style(typeof(Label));
dictionary[Device.Styles.TitleStyleKey] = CreateTitleStyle();
dictionary[Device.Styles.SubtitleStyleKey] = CreateSubtitleStyle();
dictionary[Device.Styles.CaptionStyleKey] = CreateCaptionStyle();
dictionary[Device.Styles.ListItemTextStyleKey] = new Style(typeof(Label));
dictionary[Device.Styles.ListItemDetailTextStyleKey] = CreateCaptionStyle();
private static Style CreateSubtitleStyle()
{
//IL_000a: Unknown result type (might be due to invalid IL or missing references)
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Expected O, but got Unknown
//IL_003f: Expected O, but got Unknown
return new Style(typeof(Label))
{
Setters =
{
new Setter
{
Property = Label.FontSizeProperty,
Value = 18.0
}
}
};
}
return dictionary;
}
private static Style CreateCaptionStyle()
{
//IL_000a: Unknown result type (might be due to invalid IL or missing references)
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_001a: Unknown result type (might be due to invalid IL or missing references)
//IL_0025: Unknown result type (might be due to invalid IL or missing references)
//IL_003e: Expected O, but got Unknown
//IL_003f: Expected O, but got Unknown
return new Style(typeof(Label))
{
Setters =
{
new Setter
{
Property = Label.FontSizeProperty,
Value = 12.0
}
}
};
}
private static Style CreateTitleStyle() => new(typeof(Label))
{
Setters = { new Setter { Property = Label.FontSizeProperty, Value = 24.0 } }
};
private static Style CreateSubtitleStyle() => new(typeof(Label))
{
Setters = { new Setter { Property = Label.FontSizeProperty, Value = 18.0 } }
};
private static Style CreateCaptionStyle() => new(typeof(Label))
{
Setters = { new Setter { Property = Label.FontSizeProperty, Value = 12.0 } }
};
}

View File

@@ -1,184 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using SkiaSharp;
using Svg.Skia;
namespace Microsoft.Maui.Platform.Linux.Services;
public static class MauiIconGenerator
{
private const int DefaultIconSize = 256;
public static string? GenerateIcon(string metaFilePath)
{
//IL_0095: Unknown result type (might be due to invalid IL or missing references)
//IL_008c: Unknown result type (might be due to invalid IL or missing references)
//IL_009a: Unknown result type (might be due to invalid IL or missing references)
//IL_0123: Unknown result type (might be due to invalid IL or missing references)
//IL_0165: Unknown result type (might be due to invalid IL or missing references)
//IL_017c: Unknown result type (might be due to invalid IL or missing references)
//IL_018e: Unknown result type (might be due to invalid IL or missing references)
//IL_0195: Expected O, but got Unknown
//IL_01b5: Unknown result type (might be due to invalid IL or missing references)
//IL_01ba: Unknown result type (might be due to invalid IL or missing references)
if (!File.Exists(metaFilePath))
{
Console.WriteLine("[MauiIconGenerator] Metadata file not found: " + metaFilePath);
return null;
}
try
{
string? path = Path.GetDirectoryName(metaFilePath) ?? "";
Dictionary<string, string> dictionary = ParseMetadata(File.ReadAllText(metaFilePath));
Path.Combine(path, "appicon_bg.svg");
string text = Path.Combine(path, "appicon_fg.svg");
string text2 = Path.Combine(path, "appicon.png");
string value;
int result;
int num = ((dictionary.TryGetValue("Size", out value) && int.TryParse(value, out result)) ? result : 256);
string value2;
SKColor val = (dictionary.TryGetValue("Color", out value2) ? ParseColor(value2) : SKColors.Purple);
string value3;
float result2;
float num2 = ((dictionary.TryGetValue("Scale", out value3) && float.TryParse(value3, out result2)) ? result2 : 0.65f);
Console.WriteLine($"[MauiIconGenerator] Generating {num}x{num} icon");
Console.WriteLine($"[MauiIconGenerator] Color: {val}");
Console.WriteLine($"[MauiIconGenerator] Scale: {num2}");
SKSurface val2 = SKSurface.Create(new SKImageInfo(num, num, (SKColorType)4, (SKAlphaType)2));
try
{
SKCanvas canvas = val2.Canvas;
canvas.Clear(val);
if (File.Exists(text))
{
SKSvg val3 = new SKSvg();
try
{
if (val3.Load(text) != null && val3.Picture != null)
{
SKRect cullRect = val3.Picture.CullRect;
float num3 = (float)num * num2 / Math.Max(((SKRect)(ref cullRect)).Width, ((SKRect)(ref cullRect)).Height);
float num4 = ((float)num - ((SKRect)(ref cullRect)).Width * num3) / 2f;
float num5 = ((float)num - ((SKRect)(ref cullRect)).Height * num3) / 2f;
canvas.Save();
canvas.Translate(num4, num5);
canvas.Scale(num3);
canvas.DrawPicture(val3.Picture, (SKPaint)null);
canvas.Restore();
}
}
finally
{
((IDisposable)val3)?.Dispose();
}
}
SKImage val4 = val2.Snapshot();
try
{
SKData val5 = val4.Encode((SKEncodedImageFormat)4, 100);
try
{
using FileStream fileStream = File.OpenWrite(text2);
val5.SaveTo((Stream)fileStream);
Console.WriteLine("[MauiIconGenerator] Generated: " + text2);
return text2;
}
finally
{
((IDisposable)val5)?.Dispose();
}
}
finally
{
((IDisposable)val4)?.Dispose();
}
}
finally
{
((IDisposable)val2)?.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine("[MauiIconGenerator] Error: " + ex.Message);
return null;
}
}
private static Dictionary<string, string> ParseMetadata(string content)
{
Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string[] array = content.Split(new char[2] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < array.Length; i++)
{
string[] array2 = array[i].Split('=', 2);
if (array2.Length == 2)
{
dictionary[array2[0].Trim()] = array2[1].Trim();
}
}
return dictionary;
}
private static SKColor ParseColor(string colorStr)
{
//IL_0008: Unknown result type (might be due to invalid IL or missing references)
//IL_0246: Unknown result type (might be due to invalid IL or missing references)
//IL_024b: Unknown result type (might be due to invalid IL or missing references)
//IL_024d: Unknown result type (might be due to invalid IL or missing references)
//IL_0207: Unknown result type (might be due to invalid IL or missing references)
//IL_020c: Unknown result type (might be due to invalid IL or missing references)
//IL_0219: Unknown result type (might be due to invalid IL or missing references)
//IL_021e: Unknown result type (might be due to invalid IL or missing references)
//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
//IL_023d: Unknown result type (might be due to invalid IL or missing references)
//IL_0242: Unknown result type (might be due to invalid IL or missing references)
//IL_022b: Unknown result type (might be due to invalid IL or missing references)
//IL_0230: Unknown result type (might be due to invalid IL or missing references)
//IL_0119: Unknown result type (might be due to invalid IL or missing references)
//IL_0210: Unknown result type (might be due to invalid IL or missing references)
//IL_0215: Unknown result type (might be due to invalid IL or missing references)
//IL_0222: Unknown result type (might be due to invalid IL or missing references)
//IL_0227: Unknown result type (might be due to invalid IL or missing references)
//IL_0234: Unknown result type (might be due to invalid IL or missing references)
//IL_0239: Unknown result type (might be due to invalid IL or missing references)
if (string.IsNullOrEmpty(colorStr))
{
return SKColors.Purple;
}
colorStr = colorStr.Trim();
if (colorStr.StartsWith("#"))
{
string text = colorStr.Substring(1);
if (text.Length == 3)
{
text = $"{text[0]}{text[0]}{text[1]}{text[1]}{text[2]}{text[2]}";
}
uint result2;
if (text.Length == 6)
{
if (uint.TryParse(text, NumberStyles.HexNumber, null, out var result))
{
return new SKColor((byte)((result >> 16) & 0xFF), (byte)((result >> 8) & 0xFF), (byte)(result & 0xFF));
}
}
else if (text.Length == 8 && uint.TryParse(text, NumberStyles.HexNumber, null, out result2))
{
return new SKColor((byte)((result2 >> 16) & 0xFF), (byte)((result2 >> 8) & 0xFF), (byte)(result2 & 0xFF), (byte)((result2 >> 24) & 0xFF));
}
}
return (SKColor)(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,
});
}
}

View File

@@ -1,23 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class NotificationAction
{
public string Key { get; set; } = "";
public string Label { get; set; } = "";
public Action? Callback { get; set; }
public NotificationAction()
{
}
public NotificationAction(string key, string label, Action? callback = null)
{
Key = key;
Label = label;
Callback = callback;
}
}

View File

@@ -1,19 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class NotificationActionEventArgs : EventArgs
{
public uint NotificationId { get; }
public string ActionKey { get; }
public string? Tag { get; }
public NotificationActionEventArgs(uint notificationId, string actionKey, string? tag)
{
NotificationId = notificationId;
ActionKey = actionKey;
Tag = tag;
}
}

View File

@@ -1,9 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum NotificationCloseReason
{
Expired = 1,
Dismissed,
Closed,
Undefined
}

View File

@@ -1,19 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class NotificationClosedEventArgs : EventArgs
{
public uint NotificationId { get; }
public NotificationCloseReason Reason { get; }
public string? Tag { get; }
public NotificationClosedEventArgs(uint notificationId, NotificationCloseReason reason, string? tag)
{
NotificationId = notificationId;
Reason = reason;
Tag = tag;
}
}

View File

@@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
namespace Microsoft.Maui.Platform.Linux.Services;
internal class NotificationContext
{
public string? Tag { get; set; }
public Dictionary<string, Action?>? ActionCallbacks { get; set; }
}

View File

@@ -1,24 +0,0 @@
using System.Collections.Generic;
namespace Microsoft.Maui.Platform.Linux.Services;
public class NotificationOptions
{
public string Title { get; set; } = "";
public string Message { get; set; } = "";
public string? IconPath { get; set; }
public string? IconName { get; set; }
public NotificationUrgency Urgency { get; set; } = NotificationUrgency.Normal;
public int ExpireTimeMs { get; set; } = 5000;
public string? Category { get; set; }
public bool IsTransient { get; set; }
public Dictionary<string, string>? Actions { get; set; }
}

View File

@@ -1,370 +1,211 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux notification service using notify-send (libnotify).
/// </summary>
public class NotificationService
{
private readonly string _appName;
private readonly string _appName;
private readonly string? _defaultIconPath;
private readonly string? _defaultIconPath;
public NotificationService(string appName = "MAUI Application", string? defaultIconPath = null)
{
_appName = appName;
_defaultIconPath = defaultIconPath;
}
private readonly ConcurrentDictionary<uint, NotificationContext> _activeNotifications = new ConcurrentDictionary<uint, NotificationContext>();
/// <summary>
/// Shows a simple notification.
/// </summary>
public async Task ShowAsync(string title, string message)
{
await ShowAsync(new NotificationOptions
{
Title = title,
Message = message
});
}
private static uint _notificationIdCounter = 1u;
/// <summary>
/// Shows a notification with options.
/// </summary>
public async Task ShowAsync(NotificationOptions options)
{
try
{
var args = BuildNotifyArgs(options);
private Process? _dBusMonitor;
var startInfo = new ProcessStartInfo
{
FileName = "notify-send",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
private bool _monitoringActions;
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
// Fall back to zenity notification
await TryZenityNotification(options);
}
}
public event EventHandler<NotificationActionEventArgs>? ActionInvoked;
private string BuildNotifyArgs(NotificationOptions options)
{
var args = new List<string>();
public event EventHandler<NotificationClosedEventArgs>? NotificationClosed;
// App name
args.Add($"--app-name=\"{EscapeArg(_appName)}\"");
public NotificationService(string appName = "MAUI Application", string? defaultIconPath = null)
{
_appName = appName;
_defaultIconPath = defaultIconPath;
}
// Urgency
args.Add($"--urgency={options.Urgency.ToString().ToLower()}");
public void StartActionMonitoring()
{
if (!_monitoringActions)
{
_monitoringActions = true;
Task.Run((Func<Task?>)MonitorNotificationSignals);
}
}
// Expire time (milliseconds, 0 = never expire)
if (options.ExpireTimeMs > 0)
{
args.Add($"--expire-time={options.ExpireTimeMs}");
}
public void StopActionMonitoring()
{
_monitoringActions = false;
try
{
_dBusMonitor?.Kill();
_dBusMonitor?.Dispose();
_dBusMonitor = null;
}
catch
{
}
}
// Icon
var icon = options.IconPath ?? _defaultIconPath;
if (!string.IsNullOrEmpty(icon))
{
args.Add($"--icon=\"{EscapeArg(icon)}\"");
}
else if (!string.IsNullOrEmpty(options.IconName))
{
args.Add($"--icon={options.IconName}");
}
private async Task MonitorNotificationSignals()
{
_ = 2;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "dbus-monitor",
Arguments = "--session \"interface='org.freedesktop.Notifications'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
_dBusMonitor = Process.Start(startInfo);
if (_dBusMonitor == null)
{
return;
}
StreamReader reader = _dBusMonitor.StandardOutput;
StringBuilder buffer = new StringBuilder();
while (_monitoringActions && !_dBusMonitor.HasExited)
{
string text = await reader.ReadLineAsync();
if (text != null)
{
buffer.AppendLine(text);
if (text.Contains("ActionInvoked"))
{
await ProcessActionInvoked(reader);
}
else if (text.Contains("NotificationClosed"))
{
await ProcessNotificationClosed(reader);
}
continue;
}
break;
}
}
catch (Exception ex)
{
Console.WriteLine("[NotificationService] D-Bus monitor error: " + ex.Message);
}
}
// Category
if (!string.IsNullOrEmpty(options.Category))
{
args.Add($"--category={options.Category}");
}
private async Task ProcessActionInvoked(StreamReader reader)
{
try
{
uint notificationId = 0u;
string actionKey = null;
for (int i = 0; i < 10; i++)
{
string text = await reader.ReadLineAsync();
if (text == null)
{
break;
}
if (text.Contains("uint32"))
{
Match match = Regex.Match(text, "uint32\\s+(\\d+)");
if (match.Success)
{
notificationId = uint.Parse(match.Groups[1].Value);
}
}
else if (text.Contains("string"))
{
Match match2 = Regex.Match(text, "string\\s+\"([^\"]*)\"");
if (match2.Success && actionKey == null)
{
actionKey = match2.Groups[1].Value;
}
}
if (notificationId != 0 && actionKey != null)
{
break;
}
}
if (notificationId != 0 && actionKey != null && _activeNotifications.TryGetValue(notificationId, out NotificationContext value))
{
Action value2 = default(Action);
if (value.ActionCallbacks?.TryGetValue(actionKey, out value2) ?? false)
{
value2?.Invoke();
}
this.ActionInvoked?.Invoke(this, new NotificationActionEventArgs(notificationId, actionKey, value.Tag));
}
}
catch
{
}
}
// Hint for transient notifications
if (options.IsTransient)
{
args.Add("--hint=int:transient:1");
}
private async Task ProcessNotificationClosed(StreamReader reader)
{
try
{
uint notificationId = 0u;
uint reason = 0u;
for (int i = 0; i < 5; i++)
{
string text = await reader.ReadLineAsync();
if (text == null)
{
break;
}
if (!text.Contains("uint32"))
{
continue;
}
Match match = Regex.Match(text, "uint32\\s+(\\d+)");
if (match.Success)
{
if (notificationId == 0)
{
notificationId = uint.Parse(match.Groups[1].Value);
}
else
{
reason = uint.Parse(match.Groups[1].Value);
}
}
}
if (notificationId != 0)
{
_activeNotifications.TryRemove(notificationId, out NotificationContext value);
this.NotificationClosed?.Invoke(this, new NotificationClosedEventArgs(notificationId, (NotificationCloseReason)reason, value?.Tag));
}
}
catch
{
}
}
// Actions (if supported)
if (options.Actions?.Count > 0)
{
foreach (var action in options.Actions)
{
args.Add($"--action=\"{action.Key}={EscapeArg(action.Value)}\"");
}
}
public async Task ShowAsync(string title, string message)
{
await ShowAsync(new NotificationOptions
{
Title = title,
Message = message
});
}
// Title and message
args.Add($"\"{EscapeArg(options.Title)}\"");
args.Add($"\"{EscapeArg(options.Message)}\"");
public async Task<uint> ShowWithActionsAsync(string title, string message, IEnumerable<NotificationAction> actions, string? tag = null)
{
uint notificationId = _notificationIdCounter++;
NotificationContext value = new NotificationContext
{
Tag = tag,
ActionCallbacks = actions.ToDictionary((NotificationAction a) => a.Key, (NotificationAction a) => a.Callback)
};
_activeNotifications[notificationId] = value;
Dictionary<string, string> actions2 = actions.ToDictionary((NotificationAction a) => a.Key, (NotificationAction a) => a.Label);
await ShowAsync(new NotificationOptions
{
Title = title,
Message = message,
Actions = actions2
});
return notificationId;
}
return string.Join(" ", args);
}
public async Task CancelAsync(uint notificationId)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "gdbus",
Arguments = $"call --session --dest org.freedesktop.Notifications --object-path /org/freedesktop/Notifications --method org.freedesktop.Notifications.CloseNotification {notificationId}",
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
_activeNotifications.TryRemove(notificationId, out NotificationContext _);
}
catch
{
}
}
private async Task TryZenityNotification(NotificationOptions options)
{
try
{
var iconArg = "";
if (!string.IsNullOrEmpty(options.IconPath))
{
iconArg = $"--window-icon=\"{options.IconPath}\"";
}
public async Task ShowAsync(NotificationOptions options)
{
try
{
string arguments = BuildNotifyArgs(options);
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "notify-send",
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
_ = ex;
await TryZenityNotification(options);
}
}
var typeArg = options.Urgency == NotificationUrgency.Critical ? "--error" : "--info";
private string BuildNotifyArgs(NotificationOptions options)
{
List<string> list = new List<string>();
list.Add("--app-name=\"" + EscapeArg(_appName) + "\"");
list.Add("--urgency=" + options.Urgency.ToString().ToLower());
if (options.ExpireTimeMs > 0)
{
list.Add($"--expire-time={options.ExpireTimeMs}");
}
string text = options.IconPath ?? _defaultIconPath;
if (!string.IsNullOrEmpty(text))
{
list.Add("--icon=\"" + EscapeArg(text) + "\"");
}
else if (!string.IsNullOrEmpty(options.IconName))
{
list.Add("--icon=" + options.IconName);
}
if (!string.IsNullOrEmpty(options.Category))
{
list.Add("--category=" + options.Category);
}
if (options.IsTransient)
{
list.Add("--hint=int:transient:1");
}
Dictionary<string, string>? actions = options.Actions;
if (actions != null && actions.Count > 0)
{
foreach (KeyValuePair<string, string> action in options.Actions)
{
list.Add($"--action=\"{action.Key}={EscapeArg(action.Value)}\"");
}
}
list.Add("\"" + EscapeArg(options.Title) + "\"");
list.Add("\"" + EscapeArg(options.Message) + "\"");
return string.Join(" ", list);
}
var startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = $"{typeArg} {iconArg} --title=\"{EscapeArg(options.Title)}\" --text=\"{EscapeArg(options.Message)}\" --timeout=5",
UseShellExecute = false,
CreateNoWindow = true
};
private async Task TryZenityNotification(NotificationOptions options)
{
try
{
string value = "";
if (!string.IsNullOrEmpty(options.IconPath))
{
value = "--window-icon=\"" + options.IconPath + "\"";
}
string value2 = ((options.Urgency == NotificationUrgency.Critical) ? "--error" : "--info");
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = $"{value2} {value} --title=\"{EscapeArg(options.Title)}\" --text=\"{EscapeArg(options.Message)}\" --timeout=5",
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch
{
}
}
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch
{
// Silently fail if no notification method available
}
}
public static bool IsAvailable()
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "which",
Arguments = "notify-send",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
});
if (process == null)
{
return false;
}
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if notifications are available on this system.
/// </summary>
public static bool IsAvailable()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "which",
Arguments = "notify-send",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
private static string EscapeArg(string arg)
{
return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? "";
}
using var process = Process.Start(startInfo);
if (process == null) return false;
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static string EscapeArg(string arg)
{
return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? "";
}
}
/// <summary>
/// Options for displaying a notification.
/// </summary>
public class NotificationOptions
{
public string Title { get; set; } = "";
public string Message { get; set; } = "";
public string? IconPath { get; set; }
public string? IconName { get; set; } // Standard icon name like "dialog-information"
public NotificationUrgency Urgency { get; set; } = NotificationUrgency.Normal;
public int ExpireTimeMs { get; set; } = 5000; // 5 seconds default
public string? Category { get; set; } // e.g., "email", "im", "transfer"
public bool IsTransient { get; set; }
public Dictionary<string, string>? Actions { get; set; }
}
/// <summary>
/// Notification urgency level.
/// </summary>
public enum NotificationUrgency
{
Low,
Normal,
Critical
}

View File

@@ -1,8 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum NotificationUrgency
{
Low,
Normal,
Critical
}

View File

@@ -1,38 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public class NullAccessibilityService : IAccessibilityService
{
public bool IsEnabled => false;
public void Initialize()
{
}
public void Register(IAccessible accessible)
{
}
public void Unregister(IAccessible accessible)
{
}
public void NotifyFocusChanged(IAccessible? accessible)
{
}
public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property)
{
}
public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value)
{
}
public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite)
{
}
public void Shutdown()
{
}
}

View File

@@ -1,43 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class NullInputMethodService : IInputMethodService
{
public bool IsActive => false;
public string PreEditText => string.Empty;
public int PreEditCursorPosition => 0;
public event EventHandler<TextCommittedEventArgs>? TextCommitted;
public event EventHandler<PreEditChangedEventArgs>? PreEditChanged;
public event EventHandler? PreEditEnded;
public void Initialize(IntPtr windowHandle)
{
}
public void SetFocus(IInputContext? context)
{
}
public void SetCursorLocation(int x, int y, int width, int height)
{
}
public bool ProcessKeyEvent(uint keyCode, KeyModifiers modifiers, bool isKeyDown)
{
return false;
}
public void Reset()
{
}
public void Shutdown()
{
}
}

View File

@@ -1,384 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
public class PortalFilePickerService : IFilePicker
{
private bool _portalAvailable = true;
private string? _fallbackTool;
public PortalFilePickerService()
{
DetectAvailableTools();
}
private void DetectAvailableTools()
{
_portalAvailable = CheckPortalAvailable();
if (!_portalAvailable)
{
if (IsCommandAvailable("zenity"))
{
_fallbackTool = "zenity";
}
else if (IsCommandAvailable("kdialog"))
{
_fallbackTool = "kdialog";
}
else if (IsCommandAvailable("yad"))
{
_fallbackTool = "yad";
}
}
}
private bool CheckPortalAvailable()
{
try
{
return RunCommand("busctl", "--user list | grep -q org.freedesktop.portal.Desktop && echo yes").Trim() == "yes";
}
catch
{
return false;
}
}
private bool IsCommandAvailable(string command)
{
try
{
return !string.IsNullOrWhiteSpace(RunCommand("which", command));
}
catch
{
return false;
}
}
public async Task<FileResult?> PickAsync(PickOptions? options = null)
{
if (options == null)
{
options = new PickOptions();
}
return (await PickFilesAsync(options, allowMultiple: false)).FirstOrDefault();
}
public async Task<IEnumerable<FileResult>> PickMultipleAsync(PickOptions? options = null)
{
if (options == null)
{
options = new PickOptions();
}
return await PickFilesAsync(options, allowMultiple: true);
}
private async Task<IEnumerable<FileResult>> PickFilesAsync(PickOptions options, bool allowMultiple)
{
if (_portalAvailable)
{
return await PickWithPortalAsync(options, allowMultiple);
}
if (_fallbackTool != null)
{
return await PickWithFallbackAsync(options, allowMultiple);
}
Console.WriteLine("[FilePickerService] No file picker available (install xdg-desktop-portal, zenity, or kdialog)");
return Enumerable.Empty<FileResult>();
}
private async Task<IEnumerable<FileResult>> PickWithPortalAsync(PickOptions options, bool allowMultiple)
{
IEnumerable<FileResult> result = default(IEnumerable<FileResult>);
object obj;
int num;
try
{
string text = BuildPortalFilterArgs(options.FileTypes);
string value = (allowMultiple ? "true" : "false");
string input = options.PickerTitle ?? "Open File";
StringBuilder args = new StringBuilder();
args.Append("call --session ");
args.Append("--dest org.freedesktop.portal.Desktop ");
args.Append("--object-path /org/freedesktop/portal/desktop ");
args.Append("--method org.freedesktop.portal.FileChooser.OpenFile ");
args.Append("\"\" ");
StringBuilder stringBuilder = args;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(3, 1, stringBuilder);
handler.AppendLiteral("\"");
handler.AppendFormatted(EscapeForShell(input));
handler.AppendLiteral("\" ");
stringBuilder2.Append(ref handler);
args.Append("@a{sv} {");
stringBuilder = args;
StringBuilder stringBuilder3 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(14, 1, stringBuilder);
handler.AppendLiteral("'multiple': <");
handler.AppendFormatted(value);
handler.AppendLiteral(">");
stringBuilder3.Append(ref handler);
if (text != null)
{
stringBuilder = args;
StringBuilder stringBuilder4 = stringBuilder;
handler = new StringBuilder.AppendInterpolatedStringHandler(15, 1, stringBuilder);
handler.AppendLiteral(", 'filters': <");
handler.AppendFormatted(text);
handler.AppendLiteral(">");
stringBuilder4.Append(ref handler);
}
args.Append("}");
if (string.IsNullOrEmpty(ParseRequestPath(await Task.Run(() => RunCommand("gdbus", args.ToString())))))
{
result = Enumerable.Empty<FileResult>();
return result;
}
await Task.Delay(100);
if (_fallbackTool != null)
{
result = await PickWithFallbackAsync(options, allowMultiple);
return result;
}
result = Enumerable.Empty<FileResult>();
return result;
}
catch (Exception ex)
{
obj = ex;
num = 1;
}
if (num != 1)
{
return result;
}
Exception ex2 = (Exception)obj;
Console.WriteLine("[FilePickerService] Portal error: " + ex2.Message);
if (_fallbackTool != null)
{
return await PickWithFallbackAsync(options, allowMultiple);
}
return Enumerable.Empty<FileResult>();
}
private async Task<IEnumerable<FileResult>> PickWithFallbackAsync(PickOptions options, bool allowMultiple)
{
return _fallbackTool switch
{
"zenity" => await PickWithZenityAsync(options, allowMultiple),
"kdialog" => await PickWithKdialogAsync(options, allowMultiple),
"yad" => await PickWithYadAsync(options, allowMultiple),
_ => Enumerable.Empty<FileResult>(),
};
}
private async Task<IEnumerable<FileResult>> PickWithZenityAsync(PickOptions options, bool allowMultiple)
{
StringBuilder args = new StringBuilder();
args.Append("--file-selection ");
if (!string.IsNullOrEmpty(options.PickerTitle))
{
StringBuilder stringBuilder = args;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
handler.AppendLiteral("--title=\"");
handler.AppendFormatted(EscapeForShell(options.PickerTitle));
handler.AppendLiteral("\" ");
stringBuilder2.Append(ref handler);
}
if (allowMultiple)
{
args.Append("--multiple --separator=\"|\" ");
}
List<string> extensionsFromFileType = GetExtensionsFromFileType(options.FileTypes);
if (extensionsFromFileType.Count > 0)
{
string value = string.Join(" ", extensionsFromFileType.Select((string e) => "*" + e));
StringBuilder stringBuilder = args;
StringBuilder stringBuilder3 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder);
handler.AppendLiteral("--file-filter=\"Files | ");
handler.AppendFormatted(value);
handler.AppendLiteral("\" ");
stringBuilder3.Append(ref handler);
}
string text = await Task.Run(() => RunCommand("zenity", args.ToString()));
if (string.IsNullOrWhiteSpace(text))
{
return Enumerable.Empty<FileResult>();
}
return ((IEnumerable<string>)text.Trim().Split('|', StringSplitOptions.RemoveEmptyEntries)).Select((Func<string, FileResult>)((string f) => new FileResult(f.Trim()))).ToList();
}
private async Task<IEnumerable<FileResult>> PickWithKdialogAsync(PickOptions options, bool allowMultiple)
{
StringBuilder args = new StringBuilder();
args.Append("--getopenfilename ");
args.Append(". ");
List<string> extensionsFromFileType = GetExtensionsFromFileType(options.FileTypes);
if (extensionsFromFileType.Count > 0)
{
string value = string.Join(" ", extensionsFromFileType.Select((string e) => "*" + e));
StringBuilder stringBuilder = args;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
handler.AppendLiteral("\"Files (");
handler.AppendFormatted(value);
handler.AppendLiteral(")\" ");
stringBuilder2.Append(ref handler);
}
if (!string.IsNullOrEmpty(options.PickerTitle))
{
StringBuilder stringBuilder = args;
StringBuilder stringBuilder3 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
handler.AppendLiteral("--title \"");
handler.AppendFormatted(EscapeForShell(options.PickerTitle));
handler.AppendLiteral("\" ");
stringBuilder3.Append(ref handler);
}
if (allowMultiple)
{
args.Append("--multiple --separate-output ");
}
string text = await Task.Run(() => RunCommand("kdialog", args.ToString()));
if (string.IsNullOrWhiteSpace(text))
{
return Enumerable.Empty<FileResult>();
}
return ((IEnumerable<string>)text.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries)).Select((Func<string, FileResult>)((string f) => new FileResult(f.Trim()))).ToList();
}
private async Task<IEnumerable<FileResult>> PickWithYadAsync(PickOptions options, bool allowMultiple)
{
StringBuilder args = new StringBuilder();
args.Append("--file ");
if (!string.IsNullOrEmpty(options.PickerTitle))
{
StringBuilder stringBuilder = args;
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
handler.AppendLiteral("--title=\"");
handler.AppendFormatted(EscapeForShell(options.PickerTitle));
handler.AppendLiteral("\" ");
stringBuilder2.Append(ref handler);
}
if (allowMultiple)
{
args.Append("--multiple --separator=\"|\" ");
}
List<string> extensionsFromFileType = GetExtensionsFromFileType(options.FileTypes);
if (extensionsFromFileType.Count > 0)
{
string value = string.Join(" ", extensionsFromFileType.Select((string e) => "*" + e));
StringBuilder stringBuilder = args;
StringBuilder stringBuilder3 = stringBuilder;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder);
handler.AppendLiteral("--file-filter=\"Files | ");
handler.AppendFormatted(value);
handler.AppendLiteral("\" ");
stringBuilder3.Append(ref handler);
}
string text = await Task.Run(() => RunCommand("yad", args.ToString()));
if (string.IsNullOrWhiteSpace(text))
{
return Enumerable.Empty<FileResult>();
}
return ((IEnumerable<string>)text.Trim().Split('|', StringSplitOptions.RemoveEmptyEntries)).Select((Func<string, FileResult>)((string f) => new FileResult(f.Trim()))).ToList();
}
private List<string> GetExtensionsFromFileType(FilePickerFileType? fileType)
{
List<string> list = new List<string>();
if (fileType == null)
{
return list;
}
try
{
IEnumerable<string> value = fileType.Value;
if (value == null)
{
return list;
}
foreach (string item2 in value)
{
if (item2.StartsWith(".") || (!item2.Contains('/') && !item2.Contains('*')))
{
string item = (item2.StartsWith(".") ? item2 : ("." + item2));
if (!list.Contains(item))
{
list.Add(item);
}
}
}
}
catch
{
}
return list;
}
private string? BuildPortalFilterArgs(FilePickerFileType? fileType)
{
List<string> extensionsFromFileType = GetExtensionsFromFileType(fileType);
if (extensionsFromFileType.Count == 0)
{
return null;
}
string text = string.Join(", ", extensionsFromFileType.Select((string e) => "(uint32 0, '*" + e + "')"));
return "[('Files', [" + text + "])]";
}
private string? ParseRequestPath(string output)
{
int num = output.IndexOf("'/");
int num2 = output.IndexOf("',", num);
if (num >= 0 && num2 > num)
{
return output.Substring(num + 1, num2 - num - 1);
}
return null;
}
private string EscapeForShell(string input)
{
return input.Replace("\"", "\\\"").Replace("'", "\\'");
}
private string RunCommand(string command, string arguments)
{
try
{
using Process process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit(30000);
return result;
}
catch (Exception ex)
{
Console.WriteLine("[FilePickerService] Command error: " + ex.Message);
return "";
}
}
}

View File

@@ -1,76 +0,0 @@
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Maui.Platform.Linux.Services;
public class PortalFolderPickerService
{
public async Task<FolderPickerResult> PickAsync(FolderPickerOptions? options = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (options == null)
{
options = new FolderPickerOptions();
}
string text = null;
if (IsCommandAvailable("zenity"))
{
string args = "--file-selection --directory --title=\"" + (options.Title ?? "Select Folder") + "\"";
text = await Task.Run(() => RunCommand("zenity", args)?.Trim());
}
else if (IsCommandAvailable("kdialog"))
{
string args2 = "--getexistingdirectory . --title \"" + (options.Title ?? "Select Folder") + "\"";
text = await Task.Run(() => RunCommand("kdialog", args2)?.Trim());
}
if (!string.IsNullOrEmpty(text) && Directory.Exists(text))
{
return new FolderPickerResult(new FolderResult(text));
}
return new FolderPickerResult(null);
}
public async Task<FolderPickerResult> PickAsync(CancellationToken cancellationToken = default(CancellationToken))
{
return await PickAsync(null, cancellationToken);
}
private bool IsCommandAvailable(string command)
{
try
{
return !string.IsNullOrWhiteSpace(RunCommand("which", command));
}
catch
{
return false;
}
}
private string? RunCommand(string command, string arguments)
{
try
{
using Process process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit(30000);
return result;
}
catch
{
return null;
}
}
}

View File

@@ -1,10 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public class PreEditAttribute
{
public int Start { get; set; }
public int Length { get; set; }
public PreEditAttributeType Type { get; set; }
}

View File

@@ -1,9 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum PreEditAttributeType
{
None,
Underline,
Highlighted,
Reverse
}

View File

@@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
namespace Microsoft.Maui.Platform.Linux.Services;
public class PreEditChangedEventArgs : EventArgs
{
public string PreEditText { get; }
public int CursorPosition { get; }
public IReadOnlyList<PreEditAttribute> Attributes { get; }
public PreEditChangedEventArgs(string preEditText, int cursorPosition, IReadOnlyList<PreEditAttribute>? attributes = null)
{
PreEditText = preEditText;
CursorPosition = cursorPosition;
Attributes = attributes ?? Array.Empty<PreEditAttribute>();
}
}

View File

@@ -1,195 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Storage;
using MauiAppInfo = Microsoft.Maui.ApplicationModel.AppInfo;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux preferences implementation using JSON file storage.
/// Follows XDG Base Directory Specification.
/// </summary>
public class PreferencesService : IPreferences
{
private readonly string _preferencesPath;
private readonly string _preferencesPath;
private readonly object _lock = new();
private Dictionary<string, Dictionary<string, object?>> _preferences = new();
private bool _loaded;
private readonly object _lock = new object();
public PreferencesService()
{
// Use XDG config directory
var configHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
if (string.IsNullOrEmpty(configHome))
{
configHome = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
}
private Dictionary<string, Dictionary<string, object?>> _preferences = new Dictionary<string, Dictionary<string, object>>();
var appName = MauiAppInfo.Current?.Name ?? "MauiApp";
var appDir = Path.Combine(configHome, appName);
Directory.CreateDirectory(appDir);
private bool _loaded;
_preferencesPath = Path.Combine(appDir, "preferences.json");
}
public PreferencesService()
{
string text = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
if (string.IsNullOrEmpty(text))
{
text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
}
IAppInfo current = AppInfo.Current;
string path = ((current != null) ? current.Name : null) ?? "MauiApp";
string text2 = Path.Combine(text, path);
Directory.CreateDirectory(text2);
_preferencesPath = Path.Combine(text2, "preferences.json");
}
private void EnsureLoaded()
{
if (_loaded) return;
private void EnsureLoaded()
{
if (_loaded)
{
return;
}
lock (_lock)
{
if (_loaded)
{
return;
}
try
{
if (File.Exists(_preferencesPath))
{
string json = File.ReadAllText(_preferencesPath);
_preferences = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json) ?? new Dictionary<string, Dictionary<string, object>>();
}
}
catch
{
_preferences = new Dictionary<string, Dictionary<string, object>>();
}
_loaded = true;
}
}
lock (_lock)
{
if (_loaded) return;
private void Save()
{
lock (_lock)
{
try
{
string contents = JsonSerializer.Serialize(_preferences, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_preferencesPath, contents);
}
catch
{
}
}
}
try
{
if (File.Exists(_preferencesPath))
{
var json = File.ReadAllText(_preferencesPath);
_preferences = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object?>>>(json)
?? new();
}
}
catch
{
_preferences = new();
}
private Dictionary<string, object?> GetContainer(string? sharedName)
{
string key = sharedName ?? "__default__";
EnsureLoaded();
if (!_preferences.TryGetValue(key, out Dictionary<string, object> value))
{
value = new Dictionary<string, object>();
_preferences[key] = value;
}
return value;
}
_loaded = true;
}
}
public bool ContainsKey(string key, string? sharedName = null)
{
return GetContainer(sharedName).ContainsKey(key);
}
private void Save()
{
lock (_lock)
{
try
{
var json = JsonSerializer.Serialize(_preferences, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_preferencesPath, json);
}
catch
{
// Silently fail save operations
}
}
}
public void Remove(string key, string? sharedName = null)
{
lock (_lock)
{
if (GetContainer(sharedName).Remove(key))
{
Save();
}
}
}
private Dictionary<string, object?> GetContainer(string? sharedName)
{
var key = sharedName ?? "__default__";
public void Clear(string? sharedName = null)
{
lock (_lock)
{
GetContainer(sharedName).Clear();
Save();
}
}
EnsureLoaded();
public void Set<T>(string key, T value, string? sharedName = null)
{
lock (_lock)
{
GetContainer(sharedName)[key] = value;
Save();
}
}
if (!_preferences.TryGetValue(key, out var container))
{
container = new Dictionary<string, object?>();
_preferences[key] = container;
}
public T Get<T>(string key, T defaultValue, string? sharedName = null)
{
if (!GetContainer(sharedName).TryGetValue(key, out object value))
{
return defaultValue;
}
if (value == null)
{
return defaultValue;
}
try
{
if (value is JsonElement element)
{
return ConvertJsonElement(element, defaultValue);
}
if (value is T result)
{
return result;
}
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
return container;
}
private T ConvertJsonElement<T>(JsonElement element, T defaultValue)
{
Type typeFromHandle = typeof(T);
try
{
if (typeFromHandle == typeof(string))
{
return (T)(object)element.GetString();
}
if (typeFromHandle == typeof(int))
{
return (T)(object)element.GetInt32();
}
if (typeFromHandle == typeof(long))
{
return (T)(object)element.GetInt64();
}
if (typeFromHandle == typeof(float))
{
return (T)(object)element.GetSingle();
}
if (typeFromHandle == typeof(double))
{
return (T)(object)element.GetDouble();
}
if (typeFromHandle == typeof(bool))
{
return (T)(object)element.GetBoolean();
}
if (typeFromHandle == typeof(DateTime))
{
return (T)(object)element.GetDateTime();
}
T val = element.Deserialize<T>();
return (T)((val != null) ? ((object)val) : ((object)defaultValue));
}
catch
{
return defaultValue;
}
}
public bool ContainsKey(string key, string? sharedName = null)
{
var container = GetContainer(sharedName);
return container.ContainsKey(key);
}
public void Remove(string key, string? sharedName = null)
{
lock (_lock)
{
var container = GetContainer(sharedName);
if (container.Remove(key))
{
Save();
}
}
}
public void Clear(string? sharedName = null)
{
lock (_lock)
{
var container = GetContainer(sharedName);
container.Clear();
Save();
}
}
public void Set<T>(string key, T value, string? sharedName = null)
{
lock (_lock)
{
var container = GetContainer(sharedName);
container[key] = value;
Save();
}
}
public T Get<T>(string key, T defaultValue, string? sharedName = null)
{
var container = GetContainer(sharedName);
if (!container.TryGetValue(key, out var value))
return defaultValue;
if (value == null)
return defaultValue;
try
{
// Handle JsonElement conversion (from deserialization)
if (value is JsonElement element)
{
return ConvertJsonElement<T>(element, defaultValue);
}
// Direct conversion
if (value is T typedValue)
return typedValue;
// Try Convert.ChangeType for primitive types
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
private T ConvertJsonElement<T>(JsonElement element, T defaultValue)
{
var targetType = typeof(T);
try
{
if (targetType == typeof(string))
return (T)(object)element.GetString()!;
if (targetType == typeof(int))
return (T)(object)element.GetInt32();
if (targetType == typeof(long))
return (T)(object)element.GetInt64();
if (targetType == typeof(float))
return (T)(object)element.GetSingle();
if (targetType == typeof(double))
return (T)(object)element.GetDouble();
if (targetType == typeof(bool))
return (T)(object)element.GetBoolean();
if (targetType == typeof(DateTime))
return (T)(object)element.GetDateTime();
// For complex types, deserialize
return element.Deserialize<T>() ?? defaultValue;
}
catch
{
return defaultValue;
}
}
}

View File

@@ -1,19 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class ScaleChangedEventArgs : EventArgs
{
public float OldScale { get; }
public float NewScale { get; }
public float NewDpi { get; }
public ScaleChangedEventArgs(float oldScale, float newScale, float newDpi)
{
OldScale = oldScale;
NewScale = newScale;
NewDpi = newDpi;
}
}

View File

@@ -1,304 +1,359 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux secure storage implementation using secret-tool (libsecret) or encrypted file fallback.
/// </summary>
public class SecureStorageService : ISecureStorage
{
private const string ServiceName = "maui-secure-storage";
private const string ServiceName = "maui-secure-storage";
private const string FallbackDirectory = ".maui-secure";
private readonly string _fallbackPath;
private readonly bool _useSecretService;
private const string FallbackDirectory = ".maui-secure";
public SecureStorageService()
{
_fallbackPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
FallbackDirectory);
_useSecretService = CheckSecretServiceAvailable();
}
private readonly string _fallbackPath;
private bool CheckSecretServiceAvailable()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "which",
Arguments = "secret-tool",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
private readonly bool _useSecretService;
using var process = Process.Start(startInfo);
if (process == null) return false;
public SecureStorageService()
{
_fallbackPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".maui-secure");
_useSecretService = CheckSecretServiceAvailable();
}
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private bool CheckSecretServiceAvailable()
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "which",
Arguments = "secret-tool",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
});
if (process == null)
{
return false;
}
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
public Task<string?> GetAsync(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
public Task<string?> GetAsync(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
if (_useSecretService)
{
return GetFromSecretServiceAsync(key);
}
return GetFromFallbackAsync(key);
}
if (_useSecretService)
{
return GetFromSecretServiceAsync(key);
}
else
{
return GetFromFallbackAsync(key);
}
}
public Task SetAsync(string key, string value)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
if (_useSecretService)
{
return SetInSecretServiceAsync(key, value);
}
return SetInFallbackAsync(key, value);
}
public Task SetAsync(string key, string value)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
public bool Remove(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException("key");
}
if (_useSecretService)
{
return RemoveFromSecretService(key);
}
return RemoveFromFallback(key);
}
if (_useSecretService)
{
return SetInSecretServiceAsync(key, value);
}
else
{
return SetInFallbackAsync(key, value);
}
}
public void RemoveAll()
{
if (!_useSecretService && Directory.Exists(_fallbackPath))
{
Directory.Delete(_fallbackPath, recursive: true);
}
}
public bool Remove(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
private async Task<string?> GetFromSecretServiceAsync(string key)
{
_ = 1;
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "secret-tool",
Arguments = "lookup service maui-secure-storage key " + EscapeArg(key),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
return null;
}
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
return output.TrimEnd('\n');
}
return null;
}
catch
{
return null;
}
}
if (_useSecretService)
{
return RemoveFromSecretService(key);
}
else
{
return RemoveFromFallback(key);
}
}
private async Task SetInSecretServiceAsync(string key, string value)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "secret-tool",
Arguments = $"store --label=\"{EscapeArg(key)}\" service {"maui-secure-storage"} key {EscapeArg(key)}",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process == null)
{
throw new InvalidOperationException("Failed to start secret-tool");
}
await process.StandardInput.WriteAsync(value);
process.StandardInput.Close();
await process.WaitForExitAsync();
if (process.ExitCode != 0)
{
throw new InvalidOperationException("Failed to store secret: " + await process.StandardError.ReadToEndAsync());
}
}
catch (Exception ex) when (!(ex is InvalidOperationException))
{
await SetInFallbackAsync(key, value);
}
}
public void RemoveAll()
{
if (_useSecretService)
{
// Cannot easily remove all from secret service without knowing all keys
// This would require additional tracking
}
else
{
if (Directory.Exists(_fallbackPath))
{
Directory.Delete(_fallbackPath, true);
}
}
}
private bool RemoveFromSecretService(string key)
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "secret-tool",
Arguments = "clear service maui-secure-storage key " + EscapeArg(key),
UseShellExecute = false,
CreateNoWindow = true
});
if (process == null)
{
return false;
}
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
#region Secret Service (libsecret)
private async Task<string?> GetFromFallbackAsync(string key)
{
string fallbackFilePath = GetFallbackFilePath(key);
if (!File.Exists(fallbackFilePath))
{
return null;
}
try
{
return DecryptData(await File.ReadAllBytesAsync(fallbackFilePath));
}
catch
{
return null;
}
}
private async Task<string?> GetFromSecretServiceAsync(string key)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "secret-tool",
Arguments = $"lookup service {ServiceName} key {EscapeArg(key)}",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
private async Task SetInFallbackAsync(string key, string value)
{
EnsureFallbackDirectory();
string filePath = GetFallbackFilePath(key);
byte[] bytes = EncryptData(value);
await File.WriteAllBytesAsync(filePath, bytes);
File.SetUnixFileMode(filePath, UnixFileMode.UserWrite | UnixFileMode.UserRead);
}
using var process = Process.Start(startInfo);
if (process == null) return null;
private bool RemoveFromFallback(string key)
{
string fallbackFilePath = GetFallbackFilePath(key);
if (File.Exists(fallbackFilePath))
{
File.Delete(fallbackFilePath);
return true;
}
return false;
}
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
private string GetFallbackFilePath(string key)
{
using SHA256 sHA = SHA256.Create();
string path = Convert.ToHexString(sHA.ComputeHash(Encoding.UTF8.GetBytes(key))).ToLowerInvariant();
return Path.Combine(_fallbackPath, path);
}
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
{
return output.TrimEnd('\n');
}
private void EnsureFallbackDirectory()
{
if (!Directory.Exists(_fallbackPath))
{
Directory.CreateDirectory(_fallbackPath);
File.SetUnixFileMode(_fallbackPath, UnixFileMode.UserExecute | UnixFileMode.UserWrite | UnixFileMode.UserRead);
}
}
return null;
}
catch
{
return null;
}
}
private byte[] EncryptData(string data)
{
byte[] machineKey = GetMachineKey();
using Aes aes = Aes.Create();
aes.Key = machineKey;
aes.GenerateIV();
using ICryptoTransform cryptoTransform = aes.CreateEncryptor();
byte[] bytes = Encoding.UTF8.GetBytes(data);
byte[] array = cryptoTransform.TransformFinalBlock(bytes, 0, bytes.Length);
byte[] array2 = new byte[aes.IV.Length + array.Length];
Buffer.BlockCopy(aes.IV, 0, array2, 0, aes.IV.Length);
Buffer.BlockCopy(array, 0, array2, aes.IV.Length, array.Length);
return array2;
}
private async Task SetInSecretServiceAsync(string key, string value)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "secret-tool",
Arguments = $"store --label=\"{EscapeArg(key)}\" service {ServiceName} key {EscapeArg(key)}",
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
private string DecryptData(byte[] encryptedData)
{
byte[] machineKey = GetMachineKey();
using Aes aes = Aes.Create();
aes.Key = machineKey;
byte[] array = new byte[aes.BlockSize / 8];
Buffer.BlockCopy(encryptedData, 0, array, 0, array.Length);
aes.IV = array;
byte[] array2 = new byte[encryptedData.Length - array.Length];
Buffer.BlockCopy(encryptedData, array.Length, array2, 0, array2.Length);
using ICryptoTransform cryptoTransform = aes.CreateDecryptor();
byte[] bytes = cryptoTransform.TransformFinalBlock(array2, 0, array2.Length);
return Encoding.UTF8.GetString(bytes);
}
using var process = Process.Start(startInfo);
if (process == null)
throw new InvalidOperationException("Failed to start secret-tool");
private byte[] GetMachineKey()
{
string machineId = GetMachineId();
string userName = Environment.UserName;
string s = $"{machineId}:{userName}:{"maui-secure-storage"}";
using SHA256 sHA = SHA256.Create();
return sHA.ComputeHash(Encoding.UTF8.GetBytes(s));
}
await process.StandardInput.WriteAsync(value);
process.StandardInput.Close();
private string GetMachineId()
{
try
{
if (File.Exists("/etc/machine-id"))
{
return File.ReadAllText("/etc/machine-id").Trim();
}
if (File.Exists("/var/lib/dbus/machine-id"))
{
return File.ReadAllText("/var/lib/dbus/machine-id").Trim();
}
return Environment.MachineName;
}
catch
{
return Environment.MachineName;
}
}
await process.WaitForExitAsync();
private static string EscapeArg(string arg)
{
return arg.Replace("\"", "\\\"").Replace("'", "\\'");
}
if (process.ExitCode != 0)
{
var error = await process.StandardError.ReadToEndAsync();
throw new InvalidOperationException($"Failed to store secret: {error}");
}
}
catch (Exception ex) when (ex is not InvalidOperationException)
{
// Fall back to file storage
await SetInFallbackAsync(key, value);
}
}
private bool RemoveFromSecretService(string key)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "secret-tool",
Arguments = $"clear service {ServiceName} key {EscapeArg(key)}",
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
#endregion
#region Fallback Encrypted Storage
private async Task<string?> GetFromFallbackAsync(string key)
{
var filePath = GetFallbackFilePath(key);
if (!File.Exists(filePath))
return null;
try
{
var encryptedData = await File.ReadAllBytesAsync(filePath);
return DecryptData(encryptedData);
}
catch
{
return null;
}
}
private async Task SetInFallbackAsync(string key, string value)
{
EnsureFallbackDirectory();
var filePath = GetFallbackFilePath(key);
var encryptedData = EncryptData(value);
await File.WriteAllBytesAsync(filePath, encryptedData);
// Set restrictive permissions
File.SetUnixFileMode(filePath, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
private bool RemoveFromFallback(string key)
{
var filePath = GetFallbackFilePath(key);
if (File.Exists(filePath))
{
File.Delete(filePath);
return true;
}
return false;
}
private string GetFallbackFilePath(string key)
{
// Hash the key to create a safe filename
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(key));
var fileName = Convert.ToHexString(hash).ToLowerInvariant();
return Path.Combine(_fallbackPath, fileName);
}
private void EnsureFallbackDirectory()
{
if (!Directory.Exists(_fallbackPath))
{
Directory.CreateDirectory(_fallbackPath);
// Set restrictive permissions on the directory
File.SetUnixFileMode(_fallbackPath,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
}
}
private byte[] EncryptData(string data)
{
// Use a machine-specific key derived from machine ID
var key = GetMachineKey();
using var aes = Aes.Create();
aes.Key = key;
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
var plainBytes = Encoding.UTF8.GetBytes(data);
var encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
// Prepend IV to encrypted data
var result = new byte[aes.IV.Length + encryptedBytes.Length];
Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length);
Buffer.BlockCopy(encryptedBytes, 0, result, aes.IV.Length, encryptedBytes.Length);
return result;
}
private string DecryptData(byte[] encryptedData)
{
var key = GetMachineKey();
using var aes = Aes.Create();
aes.Key = key;
// Extract IV from beginning of data
var iv = new byte[aes.BlockSize / 8];
Buffer.BlockCopy(encryptedData, 0, iv, 0, iv.Length);
aes.IV = iv;
var cipherText = new byte[encryptedData.Length - iv.Length];
Buffer.BlockCopy(encryptedData, iv.Length, cipherText, 0, cipherText.Length);
using var decryptor = aes.CreateDecryptor();
var plainBytes = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
return Encoding.UTF8.GetString(plainBytes);
}
private byte[] GetMachineKey()
{
// Derive a key from machine-id and user
var machineId = GetMachineId();
var user = Environment.UserName;
var combined = $"{machineId}:{user}:{ServiceName}";
using var sha256 = SHA256.Create();
return sha256.ComputeHash(Encoding.UTF8.GetBytes(combined));
}
private string GetMachineId()
{
try
{
// Try /etc/machine-id first (systemd)
if (File.Exists("/etc/machine-id"))
{
return File.ReadAllText("/etc/machine-id").Trim();
}
// Try /var/lib/dbus/machine-id (older systems)
if (File.Exists("/var/lib/dbus/machine-id"))
{
return File.ReadAllText("/var/lib/dbus/machine-id").Trim();
}
// Fallback to hostname
return Environment.MachineName;
}
catch
{
return Environment.MachineName;
}
}
#endregion
private static string EscapeArg(string arg)
{
return arg.Replace("\"", "\\\"").Replace("'", "\\'");
}
}

View File

@@ -1,139 +1,147 @@
using System;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel.DataTransfer;
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux share implementation using xdg-open and portal APIs.
/// </summary>
public class ShareService : IShare
{
public async Task RequestAsync(ShareTextRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (!string.IsNullOrEmpty(request.Uri))
{
await OpenUrlAsync(request.Uri);
}
else if (!string.IsNullOrEmpty(request.Text))
{
string text = Uri.EscapeDataString(request.Subject ?? "");
string text2 = Uri.EscapeDataString(request.Text ?? "");
string url = "mailto:?subject=" + text + "&body=" + text2;
await OpenUrlAsync(url);
}
}
public async Task RequestAsync(ShareTextRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
public async Task RequestAsync(ShareFileRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.File == null)
{
throw new ArgumentException("File is required", "request");
}
await ShareFileAsync(((FileBase)request.File).FullPath);
}
// On Linux, we can use mailto: for text sharing or write to a temp file
if (!string.IsNullOrEmpty(request.Uri))
{
// Share as URL
await OpenUrlAsync(request.Uri);
}
else if (!string.IsNullOrEmpty(request.Text))
{
// Try to use email for text sharing
var subject = Uri.EscapeDataString(request.Subject ?? "");
var body = Uri.EscapeDataString(request.Text ?? "");
var mailto = $"mailto:?subject={subject}&body={body}";
await OpenUrlAsync(mailto);
}
}
public async Task RequestAsync(ShareMultipleFilesRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.Files == null || !request.Files.Any())
{
throw new ArgumentException("Files are required", "request");
}
foreach (ShareFile file in request.Files)
{
await ShareFileAsync(((FileBase)file).FullPath);
}
}
public async Task RequestAsync(ShareFileRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
private async Task OpenUrlAsync(string url)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = "\"" + url + "\"",
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception innerException)
{
throw new InvalidOperationException("Failed to open URL for sharing", innerException);
}
}
if (request.File == null)
throw new ArgumentException("File is required", nameof(request));
private async Task ShareFileAsync(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException("File not found for sharing", filePath);
}
try
{
if (await TryPortalShareAsync(filePath))
{
return;
}
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = "\"" + Path.GetDirectoryName(filePath) + "\"",
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception innerException)
{
throw new InvalidOperationException("Failed to share file", innerException);
}
}
await ShareFileAsync(request.File.FullPath);
}
private async Task<bool> TryPortalShareAsync(string filePath)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = $"--info --text=\"File ready to share:\\n{Path.GetFileName(filePath)}\\n\\nPath: {filePath}\" --title=\"Share File\"",
UseShellExecute = false,
CreateNoWindow = true
};
using Process process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
return true;
}
return false;
}
catch
{
return false;
}
}
public async Task RequestAsync(ShareMultipleFilesRequest request)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (request.Files == null || !request.Files.Any())
throw new ArgumentException("Files are required", nameof(request));
// Share files one by one or use file manager
foreach (var file in request.Files)
{
await ShareFileAsync(file.FullPath);
}
}
private async Task OpenUrlAsync(string url)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = $"\"{url}\"",
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to open URL for sharing", ex);
}
}
private async Task ShareFileAsync(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException("File not found for sharing", filePath);
try
{
// Try to use the portal API via gdbus for proper share dialog
var portalResult = await TryPortalShareAsync(filePath);
if (portalResult)
return;
// Fall back to opening with default file manager
var startInfo = new ProcessStartInfo
{
FileName = "xdg-open",
Arguments = $"\"{Path.GetDirectoryName(filePath)}\"",
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to share file", ex);
}
}
private async Task<bool> TryPortalShareAsync(string filePath)
{
try
{
// Try freedesktop portal for proper share dialog
// This would use org.freedesktop.portal.FileChooser or similar
// For now, we'll use zenity --info as a fallback notification
var startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = $"--info --text=\"File ready to share:\\n{Path.GetFileName(filePath)}\\n\\nPath: {filePath}\" --title=\"Share File\"",
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
return true;
}
return false;
}
catch
{
return false;
}
}
}

View File

@@ -1,26 +0,0 @@
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
public class SystemColors
{
public SKColor Background { get; init; }
public SKColor Surface { get; init; }
public SKColor Primary { get; init; }
public SKColor OnPrimary { get; init; }
public SKColor Text { get; init; }
public SKColor TextSecondary { get; init; }
public SKColor Border { get; init; }
public SKColor Divider { get; init; }
public SKColor Error { get; init; }
public SKColor Success { get; init; }
}

View File

@@ -1,7 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum SystemTheme
{
Light,
Dark
}

View File

@@ -1,448 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
public class SystemThemeService
{
private static SystemThemeService? _instance;
private static readonly object _lock = new object();
private FileSystemWatcher? _settingsWatcher;
public static SystemThemeService Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new SystemThemeService();
}
}
}
return _instance;
}
}
public SystemTheme CurrentTheme { get; private set; }
public SKColor AccentColor { get; private set; } = new SKColor((byte)33, (byte)150, (byte)243);
public DesktopEnvironment Desktop { get; private set; }
public SystemColors Colors { get; private set; }
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
private SystemThemeService()
{
//IL_000d: Unknown result type (might be due to invalid IL or missing references)
//IL_0012: Unknown result type (might be due to invalid IL or missing references)
DetectDesktopEnvironment();
DetectTheme();
UpdateColors();
SetupWatcher();
}
private void DetectDesktopEnvironment()
{
string text = Environment.GetEnvironmentVariable("XDG_CURRENT_DESKTOP")?.ToLowerInvariant() ?? "";
string text2 = Environment.GetEnvironmentVariable("DESKTOP_SESSION")?.ToLowerInvariant() ?? "";
if (text.Contains("gnome") || text2.Contains("gnome"))
{
Desktop = DesktopEnvironment.GNOME;
}
else if (text.Contains("kde") || text.Contains("plasma") || text2.Contains("plasma"))
{
Desktop = DesktopEnvironment.KDE;
}
else if (text.Contains("xfce") || text2.Contains("xfce"))
{
Desktop = DesktopEnvironment.XFCE;
}
else if (text.Contains("mate") || text2.Contains("mate"))
{
Desktop = DesktopEnvironment.MATE;
}
else if (text.Contains("cinnamon") || text2.Contains("cinnamon"))
{
Desktop = DesktopEnvironment.Cinnamon;
}
else if (text.Contains("lxqt"))
{
Desktop = DesktopEnvironment.LXQt;
}
else if (text.Contains("lxde"))
{
Desktop = DesktopEnvironment.LXDE;
}
else
{
Desktop = DesktopEnvironment.Unknown;
}
}
private void DetectTheme()
{
//IL_0071: Unknown result type (might be due to invalid IL or missing references)
//IL_0076: Unknown result type (might be due to invalid IL or missing references)
//IL_0095: Unknown result type (might be due to invalid IL or missing references)
//IL_007a: Unknown result type (might be due to invalid IL or missing references)
//IL_007f: Unknown result type (might be due to invalid IL or missing references)
//IL_008e: Unknown result type (might be due to invalid IL or missing references)
//IL_0093: Unknown result type (might be due to invalid IL or missing references)
CurrentTheme = (Desktop switch
{
DesktopEnvironment.GNOME => DetectGnomeTheme(),
DesktopEnvironment.KDE => DetectKdeTheme(),
DesktopEnvironment.XFCE => DetectXfceTheme(),
DesktopEnvironment.Cinnamon => DetectCinnamonTheme(),
_ => DetectGtkTheme(),
}).GetValueOrDefault();
AccentColor = (SKColor)(Desktop switch
{
DesktopEnvironment.GNOME => GetGnomeAccentColor(),
DesktopEnvironment.KDE => GetKdeAccentColor(),
_ => new SKColor((byte)33, (byte)150, (byte)243),
});
}
private SystemTheme? DetectGnomeTheme()
{
try
{
string text = RunCommand("gsettings", "get org.gnome.desktop.interface color-scheme");
if (text.Contains("prefer-dark"))
{
return SystemTheme.Dark;
}
if (text.Contains("prefer-light") || text.Contains("default"))
{
return SystemTheme.Light;
}
text = RunCommand("gsettings", "get org.gnome.desktop.interface gtk-theme");
if (text.ToLowerInvariant().Contains("dark"))
{
return SystemTheme.Dark;
}
}
catch
{
}
return null;
}
private SystemTheme? DetectKdeTheme()
{
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals");
if (File.Exists(path))
{
string text = File.ReadAllText(path);
if (text.Contains("BreezeDark", StringComparison.OrdinalIgnoreCase) || text.Contains("Dark", StringComparison.OrdinalIgnoreCase))
{
return SystemTheme.Dark;
}
}
}
catch
{
}
return null;
}
private SystemTheme? DetectXfceTheme()
{
try
{
if (RunCommand("xfconf-query", "-c xsettings -p /Net/ThemeName").ToLowerInvariant().Contains("dark"))
{
return SystemTheme.Dark;
}
}
catch
{
}
return DetectGtkTheme();
}
private SystemTheme? DetectCinnamonTheme()
{
try
{
if (RunCommand("gsettings", "get org.cinnamon.desktop.interface gtk-theme").ToLowerInvariant().Contains("dark"))
{
return SystemTheme.Dark;
}
}
catch
{
}
return null;
}
private SystemTheme? DetectGtkTheme()
{
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gtk-3.0", "settings.ini");
if (File.Exists(path))
{
string[] array = File.ReadAllText(path).Split('\n');
foreach (string text in array)
{
if (text.StartsWith("gtk-theme-name=", StringComparison.OrdinalIgnoreCase) && text.Substring("gtk-theme-name=".Length).Trim().Contains("dark", StringComparison.OrdinalIgnoreCase))
{
return SystemTheme.Dark;
}
if (text.StartsWith("gtk-application-prefer-dark-theme=", StringComparison.OrdinalIgnoreCase))
{
string text2 = text.Substring("gtk-application-prefer-dark-theme=".Length).Trim();
if (text2 == "1" || text2.Equals("true", StringComparison.OrdinalIgnoreCase))
{
return SystemTheme.Dark;
}
}
}
}
}
catch
{
}
return null;
}
private SKColor GetGnomeAccentColor()
{
//IL_021e: Unknown result type (might be due to invalid IL or missing references)
//IL_0223: Unknown result type (might be due to invalid IL or missing references)
//IL_0206: Unknown result type (might be due to invalid IL or missing references)
//IL_020b: Unknown result type (might be due to invalid IL or missing references)
//IL_0227: Unknown result type (might be due to invalid IL or missing references)
//IL_020c: Unknown result type (might be due to invalid IL or missing references)
//IL_020d: Unknown result type (might be due to invalid IL or missing references)
//IL_01bc: Unknown result type (might be due to invalid IL or missing references)
//IL_01c1: Unknown result type (might be due to invalid IL or missing references)
//IL_015c: Unknown result type (might be due to invalid IL or missing references)
//IL_0161: Unknown result type (might be due to invalid IL or missing references)
//IL_0187: Unknown result type (might be due to invalid IL or missing references)
//IL_018c: Unknown result type (might be due to invalid IL or missing references)
//IL_01ab: Unknown result type (might be due to invalid IL or missing references)
//IL_01b0: Unknown result type (might be due to invalid IL or missing references)
//IL_01d0: Unknown result type (might be due to invalid IL or missing references)
//IL_01d5: Unknown result type (might be due to invalid IL or missing references)
//IL_01f2: Unknown result type (might be due to invalid IL or missing references)
//IL_01f7: Unknown result type (might be due to invalid IL or missing references)
//IL_01e4: Unknown result type (might be due to invalid IL or missing references)
//IL_01e9: Unknown result type (might be due to invalid IL or missing references)
//IL_0173: Unknown result type (might be due to invalid IL or missing references)
//IL_0178: Unknown result type (might be due to invalid IL or missing references)
//IL_019b: Unknown result type (might be due to invalid IL or missing references)
//IL_01a0: Unknown result type (might be due to invalid IL or missing references)
try
{
return (SKColor)(RunCommand("gsettings", "get org.gnome.desktop.interface accent-color").Trim().Trim('\'') switch
{
"blue" => new SKColor((byte)53, (byte)132, (byte)228),
"teal" => new SKColor((byte)42, (byte)195, (byte)222),
"green" => new SKColor((byte)58, (byte)148, (byte)74),
"yellow" => new SKColor((byte)246, (byte)211, (byte)45),
"orange" => new SKColor(byte.MaxValue, (byte)120, (byte)0),
"red" => new SKColor((byte)224, (byte)27, (byte)36),
"pink" => new SKColor((byte)214, (byte)86, (byte)140),
"purple" => new SKColor((byte)145, (byte)65, (byte)172),
"slate" => new SKColor((byte)94, (byte)92, (byte)100),
_ => new SKColor((byte)33, (byte)150, (byte)243),
});
}
catch
{
return new SKColor((byte)33, (byte)150, (byte)243);
}
}
private SKColor GetKdeAccentColor()
{
//IL_00e8: Unknown result type (might be due to invalid IL or missing references)
//IL_00c1: Unknown result type (might be due to invalid IL or missing references)
//IL_00c6: Unknown result type (might be due to invalid IL or missing references)
//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kdeglobals");
if (File.Exists(path))
{
string[] array = File.ReadAllText(path).Split('\n');
bool flag = false;
string[] array2 = array;
foreach (string text in array2)
{
if (text.StartsWith("[Colors:Header]"))
{
flag = true;
continue;
}
if (text.StartsWith("[") && flag)
{
break;
}
if (flag && text.StartsWith("BackgroundNormal="))
{
string[] array3 = text.Substring("BackgroundNormal=".Length).Split(',');
if (array3.Length >= 3 && byte.TryParse(array3[0], out var result) && byte.TryParse(array3[1], out var result2) && byte.TryParse(array3[2], out var result3))
{
return new SKColor(result, result2, result3);
}
}
}
}
}
catch
{
}
return new SKColor((byte)33, (byte)150, (byte)243);
}
private void UpdateColors()
{
//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
//IL_0101: Unknown result type (might be due to invalid IL or missing references)
//IL_011b: Unknown result type (might be due to invalid IL or missing references)
//IL_0135: Unknown result type (might be due to invalid IL or missing references)
//IL_0146: Unknown result type (might be due to invalid IL or missing references)
//IL_0157: Unknown result type (might be due to invalid IL or missing references)
//IL_016b: Unknown result type (might be due to invalid IL or missing references)
//IL_0185: Unknown result type (might be due to invalid IL or missing references)
//IL_0022: Unknown result type (might be due to invalid IL or missing references)
//IL_002d: Unknown result type (might be due to invalid IL or missing references)
//IL_0039: Unknown result type (might be due to invalid IL or missing references)
//IL_0044: Unknown result type (might be due to invalid IL or missing references)
//IL_0055: Unknown result type (might be due to invalid IL or missing references)
//IL_0066: Unknown result type (might be due to invalid IL or missing references)
//IL_0080: Unknown result type (might be due to invalid IL or missing references)
//IL_009a: Unknown result type (might be due to invalid IL or missing references)
//IL_00ad: Unknown result type (might be due to invalid IL or missing references)
//IL_00be: Unknown result type (might be due to invalid IL or missing references)
Colors = ((CurrentTheme == SystemTheme.Dark) ? new SystemColors
{
Background = new SKColor((byte)30, (byte)30, (byte)30),
Surface = new SKColor((byte)45, (byte)45, (byte)45),
Primary = AccentColor,
OnPrimary = SKColors.White,
Text = new SKColor((byte)240, (byte)240, (byte)240),
TextSecondary = new SKColor((byte)160, (byte)160, (byte)160),
Border = new SKColor((byte)64, (byte)64, (byte)64),
Divider = new SKColor((byte)58, (byte)58, (byte)58),
Error = new SKColor((byte)207, (byte)102, (byte)121),
Success = new SKColor((byte)129, (byte)201, (byte)149)
} : new SystemColors
{
Background = new SKColor((byte)250, (byte)250, (byte)250),
Surface = SKColors.White,
Primary = AccentColor,
OnPrimary = SKColors.White,
Text = new SKColor((byte)33, (byte)33, (byte)33),
TextSecondary = new SKColor((byte)117, (byte)117, (byte)117),
Border = new SKColor((byte)224, (byte)224, (byte)224),
Divider = new SKColor((byte)238, (byte)238, (byte)238),
Error = new SKColor((byte)176, (byte)0, (byte)32),
Success = new SKColor((byte)46, (byte)125, (byte)50)
});
}
private void SetupWatcher()
{
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
if (Directory.Exists(path))
{
_settingsWatcher = new FileSystemWatcher(path)
{
NotifyFilter = NotifyFilters.LastWrite,
IncludeSubdirectories = true,
EnableRaisingEvents = true
};
_settingsWatcher.Changed += OnSettingsChanged;
}
}
catch
{
}
}
private void OnSettingsChanged(object sender, FileSystemEventArgs e)
{
string? name = e.Name;
if (name == null || !name.Contains("kdeglobals"))
{
string? name2 = e.Name;
if (name2 == null || !name2.Contains("gtk"))
{
string? name3 = e.Name;
if (name3 == null || !name3.Contains("settings"))
{
return;
}
}
}
Task.Delay(500).ContinueWith(delegate
{
SystemTheme currentTheme = CurrentTheme;
DetectTheme();
UpdateColors();
if (currentTheme != CurrentTheme)
{
this.ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme));
}
});
}
private string RunCommand(string command, string arguments)
{
try
{
using Process process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = command,
Arguments = arguments,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit(1000);
return result;
}
catch
{
return "";
}
}
public void RefreshTheme()
{
SystemTheme currentTheme = CurrentTheme;
DetectTheme();
UpdateColors();
if (currentTheme != CurrentTheme)
{
this.ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme));
}
}
}

View File

@@ -1,263 +1,282 @@
using System;
using System.Collections.Generic;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux system tray service using various backends.
/// Supports yad, zenity, or native D-Bus StatusNotifierItem.
/// </summary>
public class SystemTrayService : IDisposable
{
private Process? _trayProcess;
private Process? _trayProcess;
private readonly string _appName;
private string? _iconPath;
private string? _tooltip;
private readonly List<TrayMenuItem> _menuItems = new();
private bool _isVisible;
private bool _disposed;
private readonly string _appName;
public event EventHandler? Clicked;
public event EventHandler<string>? MenuItemClicked;
private string? _iconPath;
public SystemTrayService(string appName)
{
_appName = appName;
}
private string? _tooltip;
/// <summary>
/// Gets or sets the tray icon path.
/// </summary>
public string? IconPath
{
get => _iconPath;
set
{
_iconPath = value;
if (_isVisible) UpdateTray();
}
}
private readonly List<TrayMenuItem> _menuItems = new List<TrayMenuItem>();
/// <summary>
/// Gets or sets the tooltip text.
/// </summary>
public string? Tooltip
{
get => _tooltip;
set
{
_tooltip = value;
if (_isVisible) UpdateTray();
}
}
private bool _isVisible;
/// <summary>
/// Gets the menu items.
/// </summary>
public IList<TrayMenuItem> MenuItems => _menuItems;
private bool _disposed;
/// <summary>
/// Shows the system tray icon.
/// </summary>
public async Task ShowAsync()
{
if (_isVisible) return;
public string? IconPath
{
get
{
return _iconPath;
}
set
{
_iconPath = value;
if (_isVisible)
{
UpdateTray();
}
}
}
// Try yad first (most feature-complete)
if (await TryYadTray())
{
_isVisible = true;
return;
}
public string? Tooltip
{
get
{
return _tooltip;
}
set
{
_tooltip = value;
if (_isVisible)
{
UpdateTray();
}
}
}
// Fall back to a simple approach
_isVisible = true;
}
public IList<TrayMenuItem> MenuItems => _menuItems;
/// <summary>
/// Hides the system tray icon.
/// </summary>
public void Hide()
{
if (!_isVisible) return;
public event EventHandler? Clicked;
_trayProcess?.Kill();
_trayProcess?.Dispose();
_trayProcess = null;
_isVisible = false;
}
public event EventHandler<string>? MenuItemClicked;
/// <summary>
/// Updates the tray icon and menu.
/// </summary>
public void UpdateTray()
{
if (!_isVisible) return;
public SystemTrayService(string appName)
{
_appName = appName;
}
// Restart tray with new settings
Hide();
_ = ShowAsync();
}
public async Task ShowAsync()
{
if (!_isVisible)
{
await TryYadTray();
_isVisible = true;
}
}
private async Task<bool> TryYadTray()
{
try
{
var args = BuildYadArgs();
public void Hide()
{
if (_isVisible)
{
_trayProcess?.Kill();
_trayProcess?.Dispose();
_trayProcess = null;
_isVisible = false;
}
}
var startInfo = new ProcessStartInfo
{
FileName = "yad",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
public void UpdateTray()
{
if (_isVisible)
{
Hide();
ShowAsync();
}
}
_trayProcess = Process.Start(startInfo);
if (_trayProcess == null) return false;
private async Task<bool> TryYadTray()
{
try
{
string arguments = BuildYadArgs();
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "yad",
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
_trayProcess = Process.Start(startInfo);
if (_trayProcess == null)
{
return false;
}
Task.Run(async delegate
{
try
{
while (!_trayProcess.HasExited)
{
string text = await _trayProcess.StandardOutput.ReadLineAsync();
if (!string.IsNullOrEmpty(text))
{
HandleTrayOutput(text);
}
}
}
catch
{
}
});
return true;
}
catch
{
return false;
}
}
// Start reading output for menu clicks
_ = Task.Run(async () =>
{
try
{
while (!_trayProcess.HasExited)
{
var line = await _trayProcess.StandardOutput.ReadLineAsync();
if (!string.IsNullOrEmpty(line))
{
HandleTrayOutput(line);
}
}
}
catch { }
});
private string BuildYadArgs()
{
List<string> list = new List<string> { "--notification", "--listen" };
if (!string.IsNullOrEmpty(_iconPath) && File.Exists(_iconPath))
{
list.Add("--image=\"" + _iconPath + "\"");
}
else
{
list.Add("--image=application-x-executable");
}
if (!string.IsNullOrEmpty(_tooltip))
{
list.Add("--text=\"" + EscapeArg(_tooltip) + "\"");
}
if (_menuItems.Count > 0)
{
string text = string.Join("!", _menuItems.Select(delegate(TrayMenuItem m)
{
object obj;
if (!m.IsSeparator)
{
obj = EscapeArg(m.Text);
if (obj == null)
{
return "";
}
}
else
{
obj = "---";
}
return (string)obj;
}));
list.Add("--menu=\"" + text + "\"");
}
list.Add("--command=\"echo clicked\"");
return string.Join(" ", list);
}
return true;
}
catch
{
return false;
}
}
private void HandleTrayOutput(string output)
{
if (output == "clicked")
{
this.Clicked?.Invoke(this, EventArgs.Empty);
return;
}
TrayMenuItem trayMenuItem = _menuItems.FirstOrDefault((TrayMenuItem m) => m.Text == output);
if (trayMenuItem != null)
{
trayMenuItem.Action?.Invoke();
this.MenuItemClicked?.Invoke(this, output);
}
}
private string BuildYadArgs()
{
var args = new List<string>
{
"--notification",
"--listen"
};
public void AddMenuItem(string text, Action? action = null)
{
_menuItems.Add(new TrayMenuItem
{
Text = text,
Action = action
});
}
if (!string.IsNullOrEmpty(_iconPath) && File.Exists(_iconPath))
{
args.Add($"--image=\"{_iconPath}\"");
}
else
{
args.Add("--image=application-x-executable");
}
public void AddSeparator()
{
_menuItems.Add(new TrayMenuItem
{
IsSeparator = true
});
}
if (!string.IsNullOrEmpty(_tooltip))
{
args.Add($"--text=\"{EscapeArg(_tooltip)}\"");
}
public void ClearMenuItems()
{
_menuItems.Clear();
}
// Build menu
if (_menuItems.Count > 0)
{
var menuStr = string.Join("!", _menuItems.Select(m =>
m.IsSeparator ? "---" : $"{EscapeArg(m.Text)}"));
args.Add($"--menu=\"{menuStr}\"");
}
public static bool IsAvailable()
{
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = "which",
Arguments = "yad",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
});
if (process == null)
{
return false;
}
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
args.Add("--command=\"echo clicked\"");
private static string EscapeArg(string arg)
{
return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("!", "\\!") ?? "";
}
return string.Join(" ", args);
}
public void Dispose()
{
if (!_disposed)
{
_disposed = true;
Hide();
GC.SuppressFinalize(this);
}
}
private void HandleTrayOutput(string output)
{
if (output == "clicked")
{
Clicked?.Invoke(this, EventArgs.Empty);
}
else
{
// Menu item clicked
var menuItem = _menuItems.FirstOrDefault(m => m.Text == output);
if (menuItem != null)
{
menuItem.Action?.Invoke();
MenuItemClicked?.Invoke(this, output);
}
}
}
~SystemTrayService()
{
Dispose();
}
/// <summary>
/// Adds a menu item to the tray context menu.
/// </summary>
public void AddMenuItem(string text, Action? action = null)
{
_menuItems.Add(new TrayMenuItem { Text = text, Action = action });
}
/// <summary>
/// Adds a separator to the tray context menu.
/// </summary>
public void AddSeparator()
{
_menuItems.Add(new TrayMenuItem { IsSeparator = true });
}
/// <summary>
/// Clears all menu items.
/// </summary>
public void ClearMenuItems()
{
_menuItems.Clear();
}
/// <summary>
/// Checks if system tray is available on this system.
/// </summary>
public static bool IsAvailable()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "which",
Arguments = "yad",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null) return false;
process.WaitForExit();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
private static string EscapeArg(string arg)
{
return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("!", "\\!") ?? "";
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Hide();
GC.SuppressFinalize(this);
}
~SystemTrayService()
{
Dispose();
}
}
/// <summary>
/// Represents a tray menu item.
/// </summary>
public class TrayMenuItem
{
public string Text { get; set; } = "";
public Action? Action { get; set; }
public bool IsSeparator { get; set; }
public bool IsEnabled { get; set; } = true;
public string? IconPath { get; set; }
}

View File

@@ -1,13 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class TextCommittedEventArgs : EventArgs
{
public string Text { get; }
public TextCommittedEventArgs(string text)
{
Text = text;
}
}

View File

@@ -1,19 +0,0 @@
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Services;
public class TextRun
{
public string Text { get; }
public SKTypeface Typeface { get; }
public int StartIndex { get; }
public TextRun(string text, SKTypeface typeface, int startIndex)
{
Text = text;
Typeface = typeface;
StartIndex = startIndex;
}
}

View File

@@ -1,13 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class ThemeChangedEventArgs : EventArgs
{
public SystemTheme NewTheme { get; }
public ThemeChangedEventArgs(SystemTheme newTheme)
{
NewTheme = newTheme;
}
}

View File

@@ -1,16 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class TrayMenuItem
{
public string Text { get; set; } = "";
public Action? Action { get; set; }
public bool IsSeparator { get; set; }
public bool IsEnabled { get; set; } = true;
public string? IconPath { get; set; }
}

View File

@@ -1,250 +1,251 @@
using System;
using System.Collections.Generic;
using System.IO;
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using System.Text.Json;
using Microsoft.Maui.ApplicationModel;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux version tracking implementation.
/// </summary>
public class VersionTrackingService : IVersionTracking
{
private class VersionTrackingData
{
public string? CurrentVersion { get; set; }
private const string VersionTrackingFile = ".maui-version-tracking.json";
private readonly string _trackingFilePath;
private VersionTrackingData _data;
private bool _isInitialized;
public string? CurrentBuild { get; set; }
public VersionTrackingService()
{
_trackingFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
VersionTrackingFile);
_data = new VersionTrackingData();
}
public string? PreviousVersion { get; set; }
private void EnsureInitialized()
{
if (_isInitialized) return;
_isInitialized = true;
public string? PreviousBuild { get; set; }
LoadTrackingData();
UpdateTrackingData();
}
public string? FirstInstalledVersion { get; set; }
private void LoadTrackingData()
{
try
{
if (File.Exists(_trackingFilePath))
{
var json = File.ReadAllText(_trackingFilePath);
_data = JsonSerializer.Deserialize<VersionTrackingData>(json) ?? new VersionTrackingData();
}
}
catch
{
_data = new VersionTrackingData();
}
}
public string? FirstInstalledBuild { get; set; }
private void UpdateTrackingData()
{
var currentVersion = CurrentVersion;
var currentBuild = CurrentBuild;
public List<string> VersionHistory { get; set; } = new List<string>();
// Check if this is a new version
if (_data.PreviousVersion != currentVersion || _data.PreviousBuild != currentBuild)
{
// Store previous version info
if (!string.IsNullOrEmpty(_data.CurrentVersion))
{
_data.PreviousVersion = _data.CurrentVersion;
_data.PreviousBuild = _data.CurrentBuild;
}
public List<string> BuildHistory { get; set; } = new List<string>();
_data.CurrentVersion = currentVersion;
_data.CurrentBuild = currentBuild;
public bool IsFirstLaunchEver { get; set; }
// Add to version history
if (!_data.VersionHistory.Contains(currentVersion))
{
_data.VersionHistory.Add(currentVersion);
}
public bool IsFirstLaunchForCurrentVersion { get; set; }
// Add to build history
if (!_data.BuildHistory.Contains(currentBuild))
{
_data.BuildHistory.Add(currentBuild);
}
}
public bool IsFirstLaunchForCurrentBuild { get; set; }
}
// Track first launch
if (_data.FirstInstalledVersion == null)
{
_data.FirstInstalledVersion = currentVersion;
_data.FirstInstalledBuild = currentBuild;
_data.IsFirstLaunchEver = true;
}
else
{
_data.IsFirstLaunchEver = false;
}
private const string VersionTrackingFile = ".maui-version-tracking.json";
// Check if first launch for current version
_data.IsFirstLaunchForCurrentVersion = _data.PreviousVersion != currentVersion;
_data.IsFirstLaunchForCurrentBuild = _data.PreviousBuild != currentBuild;
private readonly string _trackingFilePath;
SaveTrackingData();
}
private VersionTrackingData _data;
private void SaveTrackingData()
{
try
{
var directory = Path.GetDirectoryName(_trackingFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
private bool _isInitialized;
var json = JsonSerializer.Serialize(_data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_trackingFilePath, json);
}
catch
{
// Silently fail if we can't save
}
}
public bool IsFirstLaunchEver
{
get
{
EnsureInitialized();
return _data.IsFirstLaunchEver;
}
}
public bool IsFirstLaunchEver
{
get
{
EnsureInitialized();
return _data.IsFirstLaunchEver;
}
}
public bool IsFirstLaunchForCurrentVersion
{
get
{
EnsureInitialized();
return _data.IsFirstLaunchForCurrentVersion;
}
}
public bool IsFirstLaunchForCurrentVersion
{
get
{
EnsureInitialized();
return _data.IsFirstLaunchForCurrentVersion;
}
}
public bool IsFirstLaunchForCurrentBuild
{
get
{
EnsureInitialized();
return _data.IsFirstLaunchForCurrentBuild;
}
}
public bool IsFirstLaunchForCurrentBuild
{
get
{
EnsureInitialized();
return _data.IsFirstLaunchForCurrentBuild;
}
}
public string CurrentVersion => GetAssemblyVersion();
public string CurrentVersion => GetAssemblyVersion();
public string CurrentBuild => GetAssemblyBuild();
public string CurrentBuild => GetAssemblyBuild();
public string? PreviousVersion
{
get
{
EnsureInitialized();
return _data.PreviousVersion;
}
}
public string? PreviousVersion
{
get
{
EnsureInitialized();
return _data.PreviousVersion;
}
}
public string? PreviousBuild
{
get
{
EnsureInitialized();
return _data.PreviousBuild;
}
}
public string? PreviousBuild
{
get
{
EnsureInitialized();
return _data.PreviousBuild;
}
}
public string? FirstInstalledVersion
{
get
{
EnsureInitialized();
return _data.FirstInstalledVersion;
}
}
public string? FirstInstalledVersion
{
get
{
EnsureInitialized();
return _data.FirstInstalledVersion;
}
}
public string? FirstInstalledBuild
{
get
{
EnsureInitialized();
return _data.FirstInstalledBuild;
}
}
public string? FirstInstalledBuild
{
get
{
EnsureInitialized();
return _data.FirstInstalledBuild;
}
}
public IReadOnlyList<string> VersionHistory
{
get
{
EnsureInitialized();
return _data.VersionHistory.AsReadOnly();
}
}
public IReadOnlyList<string> VersionHistory
{
get
{
EnsureInitialized();
return _data.VersionHistory.AsReadOnly();
}
}
public IReadOnlyList<string> BuildHistory
{
get
{
EnsureInitialized();
return _data.BuildHistory.AsReadOnly();
}
}
public IReadOnlyList<string> BuildHistory
{
get
{
EnsureInitialized();
return _data.BuildHistory.AsReadOnly();
}
}
public bool IsFirstLaunchForVersion(string version)
{
EnsureInitialized();
return !_data.VersionHistory.Contains(version);
}
public VersionTrackingService()
{
_trackingFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".maui-version-tracking.json");
_data = new VersionTrackingData();
}
public bool IsFirstLaunchForBuild(string build)
{
EnsureInitialized();
return !_data.BuildHistory.Contains(build);
}
private void EnsureInitialized()
{
if (!_isInitialized)
{
_isInitialized = true;
LoadTrackingData();
UpdateTrackingData();
}
}
public void Track()
{
EnsureInitialized();
}
private void LoadTrackingData()
{
try
{
if (File.Exists(_trackingFilePath))
{
string json = File.ReadAllText(_trackingFilePath);
_data = JsonSerializer.Deserialize<VersionTrackingData>(json) ?? new VersionTrackingData();
}
}
catch
{
_data = new VersionTrackingData();
}
}
private static string GetAssemblyVersion()
{
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
var version = assembly.GetName().Version;
return version != null ? $"{version.Major}.{version.Minor}.{version.Build}" : "1.0.0";
}
private void UpdateTrackingData()
{
string currentVersion = CurrentVersion;
string currentBuild = CurrentBuild;
if (_data.PreviousVersion != currentVersion || _data.PreviousBuild != currentBuild)
{
if (!string.IsNullOrEmpty(_data.CurrentVersion))
{
_data.PreviousVersion = _data.CurrentVersion;
_data.PreviousBuild = _data.CurrentBuild;
}
_data.CurrentVersion = currentVersion;
_data.CurrentBuild = currentBuild;
if (!_data.VersionHistory.Contains(currentVersion))
{
_data.VersionHistory.Add(currentVersion);
}
if (!_data.BuildHistory.Contains(currentBuild))
{
_data.BuildHistory.Add(currentBuild);
}
}
if (_data.FirstInstalledVersion == null)
{
_data.FirstInstalledVersion = currentVersion;
_data.FirstInstalledBuild = currentBuild;
_data.IsFirstLaunchEver = true;
}
else
{
_data.IsFirstLaunchEver = false;
}
_data.IsFirstLaunchForCurrentVersion = _data.PreviousVersion != currentVersion;
_data.IsFirstLaunchForCurrentBuild = _data.PreviousBuild != currentBuild;
SaveTrackingData();
}
private static string GetAssemblyBuild()
{
var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
var version = assembly.GetName().Version;
return version?.Revision.ToString() ?? "0";
}
private void SaveTrackingData()
{
try
{
string directoryName = Path.GetDirectoryName(_trackingFilePath);
if (!string.IsNullOrEmpty(directoryName) && !Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
}
string contents = JsonSerializer.Serialize(_data, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_trackingFilePath, contents);
}
catch
{
}
}
public bool IsFirstLaunchForVersion(string version)
{
EnsureInitialized();
return !_data.VersionHistory.Contains(version);
}
public bool IsFirstLaunchForBuild(string build)
{
EnsureInitialized();
return !_data.BuildHistory.Contains(build);
}
public void Track()
{
EnsureInitialized();
}
private static string GetAssemblyVersion()
{
Version version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName().Version;
if (!(version != null))
{
return "1.0.0";
}
return $"{version.Major}.{version.Minor}.{version.Build}";
}
private static string GetAssemblyBuild()
{
return (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName().Version?.Revision.ToString() ?? "0";
}
private class VersionTrackingData
{
public string? CurrentVersion { get; set; }
public string? CurrentBuild { get; set; }
public string? PreviousVersion { get; set; }
public string? PreviousBuild { get; set; }
public string? FirstInstalledVersion { get; set; }
public string? FirstInstalledBuild { get; set; }
public List<string> VersionHistory { get; set; } = new();
public List<string> BuildHistory { get; set; } = new();
public bool IsFirstLaunchEver { get; set; }
public bool IsFirstLaunchForCurrentVersion { get; set; }
public bool IsFirstLaunchForCurrentBuild { get; set; }
}
}

View File

@@ -1,9 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum VideoAccelerationApi
{
Auto,
VaApi,
Vdpau,
Software
}

View File

@@ -1,44 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public class VideoFrame : IDisposable
{
private bool _disposed;
private Action? _releaseCallback;
public int Width { get; init; }
public int Height { get; init; }
public IntPtr DataY { get; init; }
public IntPtr DataU { get; init; }
public IntPtr DataV { get; init; }
public int StrideY { get; init; }
public int StrideU { get; init; }
public int StrideV { get; init; }
public long Timestamp { get; init; }
public bool IsKeyFrame { get; init; }
internal void SetReleaseCallback(Action callback)
{
_releaseCallback = callback;
}
public void Dispose()
{
if (!_disposed)
{
_releaseCallback?.Invoke();
_disposed = true;
}
}
}

View File

@@ -1,14 +0,0 @@
namespace Microsoft.Maui.Platform.Linux.Services;
public enum VideoProfile
{
H264Baseline,
H264Main,
H264High,
H265Main,
H265Main10,
Vp8,
Vp9Profile0,
Vp9Profile2,
Av1Main
}

View File

@@ -1,63 +0,0 @@
using System;
namespace Microsoft.Maui.Platform.Linux.Services;
public static class VirtualizationExtensions
{
public static (int first, int last) CalculateVisibleRange(float scrollOffset, float viewportHeight, float itemHeight, float itemSpacing, int totalItems)
{
if (totalItems == 0)
{
return (first: -1, last: -1);
}
float num = itemHeight + itemSpacing;
int item = Math.Max(0, (int)(scrollOffset / num));
int item2 = Math.Min(totalItems - 1, (int)((scrollOffset + viewportHeight) / num) + 1);
return (first: item, last: item2);
}
public static (int first, int last) CalculateVisibleRangeVariable(float scrollOffset, float viewportHeight, Func<int, float> getItemHeight, float itemSpacing, int totalItems)
{
if (totalItems == 0)
{
return (first: -1, last: -1);
}
int num = 0;
float num2 = 0f;
for (int i = 0; i < totalItems; i++)
{
float num3 = getItemHeight(i);
if (num2 + num3 > scrollOffset)
{
num = i;
break;
}
num2 += num3 + itemSpacing;
}
int item = num;
float num4 = scrollOffset + viewportHeight;
for (int j = num; j < totalItems; j++)
{
float num5 = getItemHeight(j);
if (num2 > num4)
{
break;
}
item = j;
num2 += num5 + itemSpacing;
}
return (first: num, last: item);
}
public static (int firstRow, int lastRow) CalculateVisibleGridRange(float scrollOffset, float viewportHeight, float rowHeight, float rowSpacing, int totalRows)
{
if (totalRows == 0)
{
return (firstRow: -1, lastRow: -1);
}
float num = rowHeight + rowSpacing;
int item = Math.Max(0, (int)(scrollOffset / num));
int item2 = Math.Min(totalRows - 1, (int)((scrollOffset + viewportHeight) / num) + 1);
return (firstRow: item, lastRow: item2);
}
}

View File

@@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Maui.Platform.Linux.Services;
public class VirtualizationManager<T> where T : SkiaView
{
private readonly Dictionary<int, T> _activeViews = new Dictionary<int, T>();
private readonly Queue<T> _recyclePool = new Queue<T>();
private readonly Func<T> _viewFactory;
private readonly Action<T>? _viewRecycler;
private readonly int _maxPoolSize;
private int _firstVisibleIndex = -1;
private int _lastVisibleIndex = -1;
public int ActiveViewCount => _activeViews.Count;
public int PooledViewCount => _recyclePool.Count;
public (int First, int Last) VisibleRange => (First: _firstVisibleIndex, Last: _lastVisibleIndex);
public VirtualizationManager(Func<T> viewFactory, Action<T>? viewRecycler = null, int maxPoolSize = 20)
{
_viewFactory = viewFactory ?? throw new ArgumentNullException("viewFactory");
_viewRecycler = viewRecycler;
_maxPoolSize = maxPoolSize;
}
public void UpdateVisibleRange(int firstVisible, int lastVisible)
{
if (firstVisible == _firstVisibleIndex && lastVisible == _lastVisibleIndex)
{
return;
}
List<int> list = new List<int>();
foreach (KeyValuePair<int, T> activeView in _activeViews)
{
if (activeView.Key < firstVisible || activeView.Key > lastVisible)
{
list.Add(activeView.Key);
}
}
foreach (int item in list)
{
RecycleView(item);
}
_firstVisibleIndex = firstVisible;
_lastVisibleIndex = lastVisible;
}
public T GetOrCreateView(int index, Action<T> bindData)
{
if (_activeViews.TryGetValue(index, out var value))
{
return value;
}
T val = ((_recyclePool.Count <= 0) ? _viewFactory() : _recyclePool.Dequeue());
bindData(val);
_activeViews[index] = val;
return val;
}
public T? GetActiveView(int index)
{
if (!_activeViews.TryGetValue(index, out var value))
{
return null;
}
return value;
}
private void RecycleView(int index)
{
if (_activeViews.TryGetValue(index, out var value))
{
_activeViews.Remove(index);
_viewRecycler?.Invoke(value);
if (_recyclePool.Count < _maxPoolSize)
{
_recyclePool.Enqueue(value);
}
else
{
value.Dispose();
}
}
}
public void Clear()
{
foreach (T value in _activeViews.Values)
{
value.Dispose();
}
_activeViews.Clear();
while (_recyclePool.Count > 0)
{
_recyclePool.Dequeue().Dispose();
}
_firstVisibleIndex = -1;
_lastVisibleIndex = -1;
}
public void RemoveItem(int index)
{
RecycleView(index);
foreach (KeyValuePair<int, T> item in (from kvp in _activeViews
where kvp.Key > index
orderby kvp.Key
select kvp).ToList())
{
_activeViews.Remove(item.Key);
_activeViews[item.Key - 1] = item.Value;
}
}
public void InsertItem(int index)
{
foreach (KeyValuePair<int, T> item in (from kvp in _activeViews
where kvp.Key >= index
orderby kvp.Key descending
select kvp).ToList())
{
_activeViews.Remove(item.Key);
_activeViews[item.Key + 1] = item.Value;
}
}
}

Some files were not shown because too many files have changed in this diff Show More