Update with recovered code from VM binaries (Jan 1)

Recovered from decompiled OpenMaui.Controls.Linux.dll:
- SkiaShell.cs: FlyoutHeader, FlyoutFooter, scroll support (918 -> 1325 lines)
- X11Window.cs: Cursor support (XCreateFontCursor, XDefineCursor)
- All handlers with dark mode support
- All services with latest implementations
- LinuxApplication with theme change handling
This commit is contained in:
2026-01-01 06:22:48 -05:00
parent 1e84c6168a
commit 1f096c38dc
254 changed files with 49359 additions and 38457 deletions

View File

@@ -1,537 +1,370 @@
// 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.Text;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
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) or D-Bus directly.
/// Supports interactive notifications with action callbacks.
/// </summary>
public class NotificationService
{
private readonly string _appName;
private readonly string? _defaultIconPath;
private readonly ConcurrentDictionary<uint, NotificationContext> _activeNotifications = new();
private static uint _notificationIdCounter = 1;
private Process? _dBusMonitor;
private bool _monitoringActions;
private readonly string _appName;
/// <summary>
/// Event raised when a notification action is invoked.
/// </summary>
public event EventHandler<NotificationActionEventArgs>? ActionInvoked;
private readonly string? _defaultIconPath;
/// <summary>
/// Event raised when a notification is closed.
/// </summary>
public event EventHandler<NotificationClosedEventArgs>? NotificationClosed;
private readonly ConcurrentDictionary<uint, NotificationContext> _activeNotifications = new ConcurrentDictionary<uint, NotificationContext>();
public NotificationService(string appName = "MAUI Application", string? defaultIconPath = null)
{
_appName = appName;
_defaultIconPath = defaultIconPath;
}
private static uint _notificationIdCounter = 1u;
/// <summary>
/// Starts monitoring for notification action callbacks via D-Bus.
/// Call this once at application startup if you want to receive action callbacks.
/// </summary>
public void StartActionMonitoring()
{
if (_monitoringActions) return;
_monitoringActions = true;
private Process? _dBusMonitor;
// Start D-Bus monitor for notification signals
Task.Run(MonitorNotificationSignals);
}
private bool _monitoringActions;
/// <summary>
/// Stops monitoring for notification action callbacks.
/// </summary>
public void StopActionMonitoring()
{
_monitoringActions = false;
try
{
_dBusMonitor?.Kill();
_dBusMonitor?.Dispose();
_dBusMonitor = null;
}
catch { }
}
public event EventHandler<NotificationActionEventArgs>? ActionInvoked;
private async Task MonitorNotificationSignals()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "dbus-monitor",
Arguments = "--session \"interface='org.freedesktop.Notifications'\"",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
public event EventHandler<NotificationClosedEventArgs>? NotificationClosed;
_dBusMonitor = Process.Start(startInfo);
if (_dBusMonitor == null) return;
public NotificationService(string appName = "MAUI Application", string? defaultIconPath = null)
{
_appName = appName;
_defaultIconPath = defaultIconPath;
}
var reader = _dBusMonitor.StandardOutput;
var buffer = new StringBuilder();
public void StartActionMonitoring()
{
if (!_monitoringActions)
{
_monitoringActions = true;
Task.Run((Func<Task?>)MonitorNotificationSignals);
}
}
while (_monitoringActions && !_dBusMonitor.HasExited)
{
var line = await reader.ReadLineAsync();
if (line == null) break;
public void StopActionMonitoring()
{
_monitoringActions = false;
try
{
_dBusMonitor?.Kill();
_dBusMonitor?.Dispose();
_dBusMonitor = null;
}
catch
{
}
}
buffer.AppendLine(line);
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);
}
}
// Look for ActionInvoked or NotificationClosed signals
if (line.Contains("ActionInvoked"))
{
await ProcessActionInvoked(reader);
}
else if (line.Contains("NotificationClosed"))
{
await ProcessNotificationClosed(reader);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[NotificationService] D-Bus monitor error: {ex.Message}");
}
}
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
{
}
}
private async Task ProcessActionInvoked(StreamReader reader)
{
try
{
// Read the signal data (notification id and action key)
uint notificationId = 0;
string? actionKey = null;
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
{
}
}
for (int i = 0; i < 10; i++) // Read a few lines to get the data
{
var line = await reader.ReadLineAsync();
if (line == null) break;
public async Task ShowAsync(string title, string message)
{
await ShowAsync(new NotificationOptions
{
Title = title,
Message = message
});
}
if (line.Contains("uint32"))
{
var idMatch = System.Text.RegularExpressions.Regex.Match(line, @"uint32\s+(\d+)");
if (idMatch.Success)
{
notificationId = uint.Parse(idMatch.Groups[1].Value);
}
}
else if (line.Contains("string"))
{
var strMatch = System.Text.RegularExpressions.Regex.Match(line, @"string\s+""([^""]*)""");
if (strMatch.Success && actionKey == null)
{
actionKey = strMatch.Groups[1].Value;
}
}
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;
}
if (notificationId > 0 && actionKey != null) break;
}
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
{
}
}
if (notificationId > 0 && actionKey != null)
{
if (_activeNotifications.TryGetValue(notificationId, out var context))
{
// Invoke callback if registered
if (context.ActionCallbacks?.TryGetValue(actionKey, out var callback) == true)
{
callback?.Invoke();
}
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);
}
}
ActionInvoked?.Invoke(this, new NotificationActionEventArgs(notificationId, actionKey, context.Tag));
}
}
}
catch { }
}
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);
}
private async Task ProcessNotificationClosed(StreamReader reader)
{
try
{
uint notificationId = 0;
uint reason = 0;
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
{
}
}
for (int i = 0; i < 5; i++)
{
var line = await reader.ReadLineAsync();
if (line == null) break;
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;
}
}
if (line.Contains("uint32"))
{
var match = System.Text.RegularExpressions.Regex.Match(line, @"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 var context);
NotificationClosed?.Invoke(this, new NotificationClosedEventArgs(
notificationId,
(NotificationCloseReason)reason,
context?.Tag));
}
}
catch { }
}
/// <summary>
/// Shows a simple notification.
/// </summary>
public async Task ShowAsync(string title, string message)
{
await ShowAsync(new NotificationOptions
{
Title = title,
Message = message
});
}
/// <summary>
/// Shows a notification with action buttons and callbacks.
/// </summary>
/// <param name="title">Notification title.</param>
/// <param name="message">Notification message.</param>
/// <param name="actions">List of action buttons with callbacks.</param>
/// <param name="tag">Optional tag to identify the notification in events.</param>
/// <returns>The notification ID.</returns>
public async Task<uint> ShowWithActionsAsync(
string title,
string message,
IEnumerable<NotificationAction> actions,
string? tag = null)
{
var notificationId = _notificationIdCounter++;
// Store context for callbacks
var context = new NotificationContext
{
Tag = tag,
ActionCallbacks = actions.ToDictionary(a => a.Key, a => a.Callback)
};
_activeNotifications[notificationId] = context;
// Build actions dictionary for options
var actionDict = actions.ToDictionary(a => a.Key, a => a.Label);
await ShowAsync(new NotificationOptions
{
Title = title,
Message = message,
Actions = actionDict
});
return notificationId;
}
/// <summary>
/// Cancels/closes an active notification.
/// </summary>
public async Task CancelAsync(uint notificationId)
{
try
{
// Use gdbus to close the notification
var 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 var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
_activeNotifications.TryRemove(notificationId, out _);
}
catch { }
}
/// <summary>
/// Shows a notification with options.
/// </summary>
public async Task ShowAsync(NotificationOptions options)
{
try
{
var args = BuildNotifyArgs(options);
var startInfo = new ProcessStartInfo
{
FileName = "notify-send",
Arguments = args,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch (Exception ex)
{
// Fall back to zenity notification
await TryZenityNotification(options);
}
}
private string BuildNotifyArgs(NotificationOptions options)
{
var args = new List<string>();
// App name
args.Add($"--app-name=\"{EscapeArg(_appName)}\"");
// Urgency
args.Add($"--urgency={options.Urgency.ToString().ToLower()}");
// Expire time (milliseconds, 0 = never expire)
if (options.ExpireTimeMs > 0)
{
args.Add($"--expire-time={options.ExpireTimeMs}");
}
// 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}");
}
// Category
if (!string.IsNullOrEmpty(options.Category))
{
args.Add($"--category={options.Category}");
}
// Hint for transient notifications
if (options.IsTransient)
{
args.Add("--hint=int:transient:1");
}
// Actions (if supported)
if (options.Actions?.Count > 0)
{
foreach (var action in options.Actions)
{
args.Add($"--action=\"{action.Key}={EscapeArg(action.Value)}\"");
}
}
// Title and message
args.Add($"\"{EscapeArg(options.Title)}\"");
args.Add($"\"{EscapeArg(options.Message)}\"");
return string.Join(" ", args);
}
private async Task TryZenityNotification(NotificationOptions options)
{
try
{
var iconArg = "";
if (!string.IsNullOrEmpty(options.IconPath))
{
iconArg = $"--window-icon=\"{options.IconPath}\"";
}
var typeArg = options.Urgency == NotificationUrgency.Critical ? "--error" : "--info";
var startInfo = new ProcessStartInfo
{
FileName = "zenity",
Arguments = $"{typeArg} {iconArg} --title=\"{EscapeArg(options.Title)}\" --text=\"{EscapeArg(options.Message)}\" --timeout=5",
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
}
}
catch
{
// Silently fail if no notification method available
}
}
/// <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
};
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
}
/// <summary>
/// Reason a notification was closed.
/// </summary>
public enum NotificationCloseReason
{
Expired = 1,
Dismissed = 2,
Closed = 3,
Undefined = 4
}
/// <summary>
/// Internal context for tracking active notifications.
/// </summary>
internal class NotificationContext
{
public string? Tag { get; set; }
public Dictionary<string, Action?>? ActionCallbacks { get; set; }
}
/// <summary>
/// Event args for notification action events.
/// </summary>
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;
}
}
/// <summary>
/// Event args for notification closed events.
/// </summary>
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;
}
}
/// <summary>
/// Defines an action button for a notification.
/// </summary>
public class NotificationAction
{
/// <summary>
/// Internal action key (not displayed).
/// </summary>
public string Key { get; set; } = "";
/// <summary>
/// Display label for the action button.
/// </summary>
public string Label { get; set; } = "";
/// <summary>
/// Callback to invoke when the action is clicked.
/// </summary>
public Action? Callback { get; set; }
public NotificationAction() { }
public NotificationAction(string key, string label, Action? callback = null)
{
Key = key;
Label = label;
Callback = callback;
}
private static string EscapeArg(string arg)
{
return arg?.Replace("\\", "\\\\").Replace("\"", "\\\"") ?? "";
}
}