More fixes
This commit is contained in:
@@ -241,6 +241,8 @@ public partial class EntryHandler : ViewHandler<IEntry, SkiaEntry>
|
|||||||
if (entry is Entry ve && ve.BackgroundColor != null)
|
if (entry is Entry ve && ve.BackgroundColor != null)
|
||||||
{
|
{
|
||||||
handler.PlatformView.EntryBackgroundColor = ve.BackgroundColor;
|
handler.PlatformView.EntryBackgroundColor = ve.BackgroundColor;
|
||||||
|
// Also set base BackgroundColor so SkiaView.DrawBackground() respects transparency
|
||||||
|
handler.PlatformView.BackgroundColor = ve.BackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using Microsoft.Maui.Handlers;
|
using Microsoft.Maui.Handlers;
|
||||||
|
using Microsoft.Maui.Platform;
|
||||||
using Microsoft.Maui.Platform.Linux.Native;
|
using Microsoft.Maui.Platform.Linux.Native;
|
||||||
using Microsoft.Maui.Platform.Linux.Services;
|
using Microsoft.Maui.Platform.Linux.Services;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
@@ -52,6 +53,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
|||||||
{
|
{
|
||||||
_platformWebView.NavigationStarted += OnNavigationStarted;
|
_platformWebView.NavigationStarted += OnNavigationStarted;
|
||||||
_platformWebView.NavigationCompleted += OnNavigationCompleted;
|
_platformWebView.NavigationCompleted += OnNavigationCompleted;
|
||||||
|
_platformWebView.ScriptDialogRequested += OnScriptDialogRequested;
|
||||||
}
|
}
|
||||||
Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready");
|
Console.WriteLine("[GtkWebViewHandler] ConnectHandler - WebView ready");
|
||||||
}
|
}
|
||||||
@@ -62,6 +64,7 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
|||||||
{
|
{
|
||||||
_platformWebView.NavigationStarted -= OnNavigationStarted;
|
_platformWebView.NavigationStarted -= OnNavigationStarted;
|
||||||
_platformWebView.NavigationCompleted -= OnNavigationCompleted;
|
_platformWebView.NavigationCompleted -= OnNavigationCompleted;
|
||||||
|
_platformWebView.ScriptDialogRequested -= OnScriptDialogRequested;
|
||||||
UnregisterFromHost();
|
UnregisterFromHost();
|
||||||
_platformWebView.Dispose();
|
_platformWebView.Dispose();
|
||||||
_platformWebView = null;
|
_platformWebView = null;
|
||||||
@@ -69,6 +72,35 @@ public class GtkWebViewHandler : ViewHandler<IWebView, GtkWebViewProxy>
|
|||||||
base.DisconnectHandler(platformView);
|
base.DisconnectHandler(platformView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnScriptDialogRequested(object? sender,
|
||||||
|
(ScriptDialogType Type, string Message, Action<bool> Callback) e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Script dialog requested: type={e.Type}, message={e.Message}");
|
||||||
|
|
||||||
|
string title = e.Type switch
|
||||||
|
{
|
||||||
|
ScriptDialogType.Alert => "Alert",
|
||||||
|
ScriptDialogType.Confirm => "Confirm",
|
||||||
|
ScriptDialogType.Prompt => "Prompt",
|
||||||
|
_ => "Message"
|
||||||
|
};
|
||||||
|
|
||||||
|
string? acceptButton = e.Type == ScriptDialogType.Alert ? "OK" : "OK";
|
||||||
|
string? cancelButton = e.Type == ScriptDialogType.Alert ? null : "Cancel";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = await LinuxDialogService.ShowAlertAsync(title, e.Message, acceptButton, cancelButton);
|
||||||
|
e.Callback(result);
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Dialog result: {result}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewHandler] Error showing dialog: {ex.Message}");
|
||||||
|
e.Callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnNavigationStarted(object? sender, string uri)
|
private void OnNavigationStarted(object? sender, string uri)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[GtkWebViewHandler] Navigation started: {uri}");
|
Console.WriteLine($"[GtkWebViewHandler] Navigation started: {uri}");
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.Maui.Platform.Linux.Native;
|
using Microsoft.Maui.Platform.Linux.Native;
|
||||||
|
using Microsoft.Maui.Platform.Linux.Services;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
namespace Microsoft.Maui.Platform.Linux.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of JavaScript dialog.
|
||||||
|
/// </summary>
|
||||||
|
public enum ScriptDialogType
|
||||||
|
{
|
||||||
|
Alert = 0,
|
||||||
|
Confirm = 1,
|
||||||
|
Prompt = 2,
|
||||||
|
BeforeUnloadConfirm = 3
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// GTK-based WebView platform view using WebKitGTK.
|
/// GTK-based WebView platform view using WebKitGTK.
|
||||||
/// Provides web browsing capabilities within MAUI applications.
|
/// Provides web browsing capabilities within MAUI applications.
|
||||||
@@ -13,7 +25,10 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
|||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private string? _currentUri;
|
private string? _currentUri;
|
||||||
private ulong _loadChangedSignalId;
|
private ulong _loadChangedSignalId;
|
||||||
|
private ulong _scriptDialogSignalId;
|
||||||
private WebKitNative.LoadChangedCallback? _loadChangedCallback;
|
private WebKitNative.LoadChangedCallback? _loadChangedCallback;
|
||||||
|
private WebKitNative.ScriptDialogCallback? _scriptDialogCallback;
|
||||||
|
private EventHandler<Microsoft.Maui.Controls.AppThemeChangedEventArgs>? _themeChangedHandler;
|
||||||
|
|
||||||
public IntPtr Widget => _widget;
|
public IntPtr Widget => _widget;
|
||||||
public string? CurrentUri => _currentUri;
|
public string? CurrentUri => _currentUri;
|
||||||
@@ -21,6 +36,7 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
|||||||
public event EventHandler<string>? NavigationStarted;
|
public event EventHandler<string>? NavigationStarted;
|
||||||
public event EventHandler<(string Url, bool Success)>? NavigationCompleted;
|
public event EventHandler<(string Url, bool Success)>? NavigationCompleted;
|
||||||
public event EventHandler<string>? TitleChanged;
|
public event EventHandler<string>? TitleChanged;
|
||||||
|
public event EventHandler<(ScriptDialogType Type, string Message, Action<bool> Callback)>? ScriptDialogRequested;
|
||||||
|
|
||||||
public GtkWebViewPlatformView()
|
public GtkWebViewPlatformView()
|
||||||
{
|
{
|
||||||
@@ -36,9 +52,231 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
|||||||
WebKitNative.ConfigureSettings(_widget);
|
WebKitNative.ConfigureSettings(_widget);
|
||||||
_loadChangedCallback = OnLoadChanged;
|
_loadChangedCallback = OnLoadChanged;
|
||||||
_loadChangedSignalId = WebKitNative.ConnectLoadChanged(_widget, _loadChangedCallback);
|
_loadChangedSignalId = WebKitNative.ConnectLoadChanged(_widget, _loadChangedCallback);
|
||||||
|
|
||||||
|
// Connect to script-dialog signal to intercept JavaScript alerts/confirms/prompts
|
||||||
|
_scriptDialogCallback = OnScriptDialog;
|
||||||
|
_scriptDialogSignalId = WebKitNative.ConnectScriptDialog(_widget, _scriptDialogCallback);
|
||||||
|
|
||||||
|
// Set initial background color based on theme
|
||||||
|
UpdateBackgroundForTheme();
|
||||||
|
|
||||||
|
// Subscribe to theme changes to update background color
|
||||||
|
_themeChangedHandler = (sender, args) =>
|
||||||
|
{
|
||||||
|
GLibNative.IdleAdd(() =>
|
||||||
|
{
|
||||||
|
UpdateBackgroundForTheme();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (Microsoft.Maui.Controls.Application.Current != null)
|
||||||
|
{
|
||||||
|
Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged += _themeChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine("[GtkWebViewPlatformView] Created WebKitWebView widget");
|
Console.WriteLine("[GtkWebViewPlatformView] Created WebKitWebView widget");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the WebView background color based on the current app theme.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateBackgroundForTheme()
|
||||||
|
{
|
||||||
|
if (_widget == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
var isDark = Microsoft.Maui.Controls.Application.Current?.RequestedTheme == Microsoft.Maui.ApplicationModel.AppTheme.Dark;
|
||||||
|
if (isDark)
|
||||||
|
{
|
||||||
|
// Dark theme: use a dark gray background
|
||||||
|
WebKitNative.SetBackgroundColor(_widget, 0.12, 0.12, 0.12, 1.0); // #1E1E1E
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Light theme: use white background
|
||||||
|
WebKitNative.SetBackgroundColor(_widget, 1.0, 1.0, 1.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool OnScriptDialog(IntPtr webView, IntPtr dialog, IntPtr userData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var webkitDialogType = WebKitNative.GetScriptDialogType(dialog);
|
||||||
|
var dialogType = (ScriptDialogType)(int)webkitDialogType;
|
||||||
|
var message = WebKitNative.GetScriptDialogMessage(dialog) ?? "";
|
||||||
|
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Script dialog: type={dialogType}, message={message}");
|
||||||
|
|
||||||
|
// Get the parent window for proper modal behavior
|
||||||
|
IntPtr parentWindow = GtkHostService.Instance.HostWindow?.Window ?? IntPtr.Zero;
|
||||||
|
|
||||||
|
// Handle prompt dialogs specially - they need a text entry
|
||||||
|
if (dialogType == ScriptDialogType.Prompt)
|
||||||
|
{
|
||||||
|
return HandlePromptDialog(dialog, message, parentWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine dialog type and buttons based on JavaScript dialog type
|
||||||
|
int messageType = GtkNative.GTK_MESSAGE_INFO;
|
||||||
|
int buttons = GtkNative.GTK_BUTTONS_OK;
|
||||||
|
|
||||||
|
switch (dialogType)
|
||||||
|
{
|
||||||
|
case ScriptDialogType.Alert:
|
||||||
|
messageType = GtkNative.GTK_MESSAGE_INFO;
|
||||||
|
buttons = GtkNative.GTK_BUTTONS_OK;
|
||||||
|
break;
|
||||||
|
case ScriptDialogType.Confirm:
|
||||||
|
case ScriptDialogType.BeforeUnloadConfirm:
|
||||||
|
messageType = GtkNative.GTK_MESSAGE_QUESTION;
|
||||||
|
buttons = GtkNative.GTK_BUTTONS_OK_CANCEL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and show native GTK message dialog
|
||||||
|
IntPtr gtkDialog = GtkNative.gtk_message_dialog_new(
|
||||||
|
parentWindow,
|
||||||
|
GtkNative.GTK_DIALOG_MODAL | GtkNative.GTK_DIALOG_DESTROY_WITH_PARENT,
|
||||||
|
messageType,
|
||||||
|
buttons,
|
||||||
|
message,
|
||||||
|
IntPtr.Zero);
|
||||||
|
|
||||||
|
if (gtkDialog != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
// Set dialog title based on type
|
||||||
|
string title = dialogType switch
|
||||||
|
{
|
||||||
|
ScriptDialogType.Alert => "Alert",
|
||||||
|
ScriptDialogType.Confirm => "Confirm",
|
||||||
|
ScriptDialogType.BeforeUnloadConfirm => "Leave Page?",
|
||||||
|
_ => "Message"
|
||||||
|
};
|
||||||
|
GtkNative.gtk_window_set_title(gtkDialog, title);
|
||||||
|
|
||||||
|
// Make dialog modal to parent if we have a parent
|
||||||
|
if (parentWindow != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
GtkNative.gtk_window_set_transient_for(gtkDialog, parentWindow);
|
||||||
|
GtkNative.gtk_window_set_modal(gtkDialog, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the dialog synchronously - this blocks until user responds
|
||||||
|
int response = GtkNative.gtk_dialog_run(gtkDialog);
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Dialog response: {response}");
|
||||||
|
|
||||||
|
// Set the confirmed state for confirm dialogs
|
||||||
|
if (dialogType == ScriptDialogType.Confirm || dialogType == ScriptDialogType.BeforeUnloadConfirm)
|
||||||
|
{
|
||||||
|
bool confirmed = response == GtkNative.GTK_RESPONSE_OK || response == GtkNative.GTK_RESPONSE_YES;
|
||||||
|
WebKitNative.SetScriptDialogConfirmed(dialog, confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
GtkNative.gtk_widget_destroy(gtkDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true to indicate we handled the dialog (prevents WebKitGTK's default)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Error in OnScriptDialog: {ex.Message}");
|
||||||
|
// Return false on error to let WebKitGTK try its default handling
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HandlePromptDialog(IntPtr webkitDialog, string message, IntPtr parentWindow)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get the default text for the prompt
|
||||||
|
string? defaultText = WebKitNative.GetScriptDialogPromptDefaultText(webkitDialog) ?? "";
|
||||||
|
|
||||||
|
// Create a custom dialog with OK/Cancel buttons
|
||||||
|
IntPtr gtkDialog = GtkNative.gtk_dialog_new_with_buttons(
|
||||||
|
"Prompt",
|
||||||
|
parentWindow,
|
||||||
|
GtkNative.GTK_DIALOG_MODAL | GtkNative.GTK_DIALOG_DESTROY_WITH_PARENT,
|
||||||
|
"_Cancel",
|
||||||
|
GtkNative.GTK_RESPONSE_CANCEL,
|
||||||
|
"_OK",
|
||||||
|
GtkNative.GTK_RESPONSE_OK,
|
||||||
|
IntPtr.Zero);
|
||||||
|
|
||||||
|
if (gtkDialog == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[GtkWebViewPlatformView] Failed to create prompt dialog");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the content area
|
||||||
|
IntPtr contentArea = GtkNative.gtk_dialog_get_content_area(gtkDialog);
|
||||||
|
|
||||||
|
// Create a vertical box for the content
|
||||||
|
IntPtr vbox = GtkNative.gtk_box_new(GtkNative.GTK_ORIENTATION_VERTICAL, 10);
|
||||||
|
GtkNative.gtk_widget_set_margin_start(vbox, 12);
|
||||||
|
GtkNative.gtk_widget_set_margin_end(vbox, 12);
|
||||||
|
GtkNative.gtk_widget_set_margin_top(vbox, 12);
|
||||||
|
GtkNative.gtk_widget_set_margin_bottom(vbox, 12);
|
||||||
|
|
||||||
|
// Add the message label
|
||||||
|
IntPtr label = GtkNative.gtk_label_new(message);
|
||||||
|
GtkNative.gtk_box_pack_start(vbox, label, false, false, 0);
|
||||||
|
|
||||||
|
// Add the text entry
|
||||||
|
IntPtr entry = GtkNative.gtk_entry_new();
|
||||||
|
GtkNative.gtk_entry_set_text(entry, defaultText);
|
||||||
|
GtkNative.gtk_box_pack_start(vbox, entry, false, false, 0);
|
||||||
|
|
||||||
|
// Add the vbox to content area
|
||||||
|
GtkNative.gtk_box_pack_start(contentArea, vbox, true, true, 0);
|
||||||
|
|
||||||
|
// Make dialog modal
|
||||||
|
if (parentWindow != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
GtkNative.gtk_window_set_transient_for(gtkDialog, parentWindow);
|
||||||
|
GtkNative.gtk_window_set_modal(gtkDialog, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show all widgets
|
||||||
|
GtkNative.gtk_widget_show_all(gtkDialog);
|
||||||
|
|
||||||
|
// Run the dialog
|
||||||
|
int response = GtkNative.gtk_dialog_run(gtkDialog);
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Prompt dialog response: {response}");
|
||||||
|
|
||||||
|
if (response == GtkNative.GTK_RESPONSE_OK)
|
||||||
|
{
|
||||||
|
// Get the text from the entry
|
||||||
|
IntPtr textPtr = GtkNative.gtk_entry_get_text(entry);
|
||||||
|
string? enteredText = textPtr != IntPtr.Zero
|
||||||
|
? System.Runtime.InteropServices.Marshal.PtrToStringUTF8(textPtr)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Prompt text: {enteredText}");
|
||||||
|
|
||||||
|
// Set the prompt response
|
||||||
|
WebKitNative.SetScriptDialogPromptText(webkitDialog, enteredText ?? "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// User cancelled - for prompts, not confirming means returning null
|
||||||
|
// WebKit handles this by not calling prompt_set_text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
GtkNative.gtk_widget_destroy(gtkDialog);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GtkWebViewPlatformView] Error in HandlePromptDialog: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData)
|
private void OnLoadChanged(IntPtr webView, int loadEvent, IntPtr userData)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -153,12 +391,22 @@ public sealed class GtkWebViewPlatformView : IDisposable
|
|||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
{
|
{
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
||||||
|
// Unsubscribe from theme changes
|
||||||
|
if (_themeChangedHandler != null && Microsoft.Maui.Controls.Application.Current != null)
|
||||||
|
{
|
||||||
|
Microsoft.Maui.Controls.Application.Current.RequestedThemeChanged -= _themeChangedHandler;
|
||||||
|
_themeChangedHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (_widget != IntPtr.Zero)
|
if (_widget != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
WebKitNative.DisconnectLoadChanged(_widget);
|
WebKitNative.DisconnectLoadChanged(_widget);
|
||||||
|
WebKitNative.DisconnectScriptDialog(_widget);
|
||||||
}
|
}
|
||||||
_widget = IntPtr.Zero;
|
_widget = IntPtr.Zero;
|
||||||
_loadChangedCallback = null;
|
_loadChangedCallback = null;
|
||||||
|
_scriptDialogCallback = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
["BackgroundColor"] = MapBackgroundColor,
|
["BackgroundColor"] = MapBackgroundColor,
|
||||||
[nameof(IView.Width)] = MapWidth,
|
[nameof(IView.Width)] = MapWidth,
|
||||||
[nameof(IView.Height)] = MapHeight,
|
[nameof(IView.Height)] = MapHeight,
|
||||||
|
["VerticalOptions"] = MapVerticalOptions,
|
||||||
|
["HorizontalOptions"] = MapHorizontalOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
public static CommandMapper<IImageButton, ImageButtonHandler> CommandMapper = new(ViewHandler.ViewCommandMapper)
|
||||||
@@ -184,6 +186,26 @@ public partial class ImageButtonHandler : ViewHandler<IImageButton, SkiaImageBut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void MapVerticalOptions(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn)
|
||||||
|
{
|
||||||
|
handler.PlatformView.VerticalOptions = imgBtn.VerticalOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MapHorizontalOptions(ImageButtonHandler handler, IImageButton imageButton)
|
||||||
|
{
|
||||||
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
|
if (imageButton is Microsoft.Maui.Controls.ImageButton imgBtn)
|
||||||
|
{
|
||||||
|
handler.PlatformView.HorizontalOptions = imgBtn.HorizontalOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Image source loading helper
|
// Image source loading helper
|
||||||
private ImageSourceServiceResultManager _sourceLoader = null!;
|
private ImageSourceServiceResultManager _sourceLoader = null!;
|
||||||
|
|
||||||
|
|||||||
@@ -48,11 +48,13 @@ public partial class ShellHandler : ViewHandler<Shell, SkiaShell>
|
|||||||
|
|
||||||
protected override SkiaShell CreatePlatformView()
|
protected override SkiaShell CreatePlatformView()
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("[ShellHandler] CreatePlatformView - creating SkiaShell");
|
||||||
return new SkiaShell();
|
return new SkiaShell();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ConnectHandler(SkiaShell platformView)
|
protected override void ConnectHandler(SkiaShell platformView)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine("[ShellHandler] ConnectHandler - connecting to SkiaShell");
|
||||||
base.ConnectHandler(platformView);
|
base.ConnectHandler(platformView);
|
||||||
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
|
platformView.FlyoutIsPresentedChanged += OnFlyoutIsPresentedChanged;
|
||||||
platformView.Navigated += OnNavigated;
|
platformView.Navigated += OnNavigated;
|
||||||
|
|||||||
@@ -81,13 +81,20 @@ public partial class WindowHandler : ElementHandler<IWindow, SkiaWindow>
|
|||||||
|
|
||||||
public static void MapContent(WindowHandler handler, IWindow window)
|
public static void MapContent(WindowHandler handler, IWindow window)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - PlatformView={handler.PlatformView != null}");
|
||||||
if (handler.PlatformView is null) return;
|
if (handler.PlatformView is null) return;
|
||||||
|
|
||||||
var content = window.Content;
|
var content = window.Content;
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - content type={content?.GetType().Name}, handler={content?.Handler?.GetType().Name}");
|
||||||
if (content?.Handler?.PlatformView is SkiaView skiaContent)
|
if (content?.Handler?.PlatformView is SkiaView skiaContent)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - setting SkiaView content: {skiaContent.GetType().Name}");
|
||||||
handler.PlatformView.Content = skiaContent;
|
handler.PlatformView.Content = skiaContent;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[WindowHandler] MapContent - content has no SkiaView! Handler={content?.Handler}, PlatformView={content?.Handler?.PlatformView}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void MapX(WindowHandler handler, IWindow window)
|
public static void MapX(WindowHandler handler, IWindow window)
|
||||||
|
|||||||
@@ -164,18 +164,11 @@ public class LinuxViewRenderer
|
|||||||
page.Handler?.DisconnectHandler();
|
page.Handler?.DisconnectHandler();
|
||||||
var handler = page.ToHandler(_mauiContext);
|
var handler = page.ToHandler(_mauiContext);
|
||||||
|
|
||||||
|
// The handler's property mappers (e.g., ContentPageHandler.MapContent)
|
||||||
|
// already set up the content and child handlers - no need to re-render here.
|
||||||
|
// Re-rendering would disconnect the existing handler hierarchy.
|
||||||
if (handler.PlatformView is SkiaView skiaPage)
|
if (handler.PlatformView is SkiaView skiaPage)
|
||||||
{
|
{
|
||||||
// For ContentPage, render the content
|
|
||||||
if (page is ContentPage contentPage && contentPage.Content != null)
|
|
||||||
{
|
|
||||||
var contentView = RenderView(contentPage.Content);
|
|
||||||
if (skiaPage is SkiaPage sp && contentView != null)
|
|
||||||
{
|
|
||||||
sp.Content = contentView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return skiaPage;
|
return skiaPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ public class LinuxApplication : IDisposable
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Requests a redraw of the application.
|
/// Requests a redraw of the application.
|
||||||
|
/// Thread-safe - will marshal to GTK thread if needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void RequestRedraw()
|
public static void RequestRedraw()
|
||||||
{
|
{
|
||||||
@@ -117,6 +118,27 @@ public class LinuxApplication : IDisposable
|
|||||||
if (_isRedrawing)
|
if (_isRedrawing)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Check if we're on the GTK thread
|
||||||
|
int currentThread = Environment.CurrentManagedThreadId;
|
||||||
|
if (_gtkThreadId != 0 && currentThread != _gtkThreadId)
|
||||||
|
{
|
||||||
|
// We're on a background thread - use IdleAdd to marshal to GTK thread
|
||||||
|
GLibNative.IdleAdd(() =>
|
||||||
|
{
|
||||||
|
RequestRedrawInternal();
|
||||||
|
return false; // Don't repeat
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestRedrawInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RequestRedrawInternal()
|
||||||
|
{
|
||||||
|
if (_isRedrawing)
|
||||||
|
return;
|
||||||
|
|
||||||
_isRedrawing = true;
|
_isRedrawing = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -197,7 +219,18 @@ public class LinuxApplication : IDisposable
|
|||||||
Current = this;
|
Current = this;
|
||||||
|
|
||||||
// Set up dialog service invalidation callback
|
// Set up dialog service invalidation callback
|
||||||
LinuxDialogService.SetInvalidateCallback(() => _renderingEngine?.InvalidateAll());
|
// This callback will work for both GTK and X11 modes
|
||||||
|
LinuxDialogService.SetInvalidateCallback(() =>
|
||||||
|
{
|
||||||
|
if (_useGtk)
|
||||||
|
{
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_renderingEngine?.InvalidateAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -265,6 +298,20 @@ public class LinuxApplication : IDisposable
|
|||||||
currentProperty.SetValue(null, mauiApplication);
|
currentProperty.SetValue(null, mauiApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set initial theme based on system theme
|
||||||
|
var systemTheme = SystemThemeService.Instance.CurrentTheme;
|
||||||
|
Console.WriteLine($"[LinuxApplication] System theme detected at startup: {systemTheme}");
|
||||||
|
if (systemTheme == SystemTheme.Dark)
|
||||||
|
{
|
||||||
|
mauiApplication.UserAppTheme = AppTheme.Dark;
|
||||||
|
Console.WriteLine("[LinuxApplication] Set initial UserAppTheme to Dark based on system theme");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mauiApplication.UserAppTheme = AppTheme.Light;
|
||||||
|
Console.WriteLine("[LinuxApplication] Set initial UserAppTheme to Light based on system theme");
|
||||||
|
}
|
||||||
|
|
||||||
// Handle user-initiated theme changes
|
// Handle user-initiated theme changes
|
||||||
((BindableObject)mauiApplication).PropertyChanged += (s, e) =>
|
((BindableObject)mauiApplication).PropertyChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
@@ -272,23 +319,93 @@ public class LinuxApplication : IDisposable
|
|||||||
{
|
{
|
||||||
Console.WriteLine($"[LinuxApplication] User theme changed to: {mauiApplication.UserAppTheme}");
|
Console.WriteLine($"[LinuxApplication] User theme changed to: {mauiApplication.UserAppTheme}");
|
||||||
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
||||||
|
|
||||||
|
// Force re-render the entire page to pick up theme changes
|
||||||
|
linuxApp.RefreshPageForThemeChange();
|
||||||
|
|
||||||
|
// Invalidate to redraw - use correct method based on mode
|
||||||
|
if (linuxApp._useGtk)
|
||||||
|
{
|
||||||
|
linuxApp._gtkWindow?.RequestRedraw();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
linuxApp._renderingEngine?.InvalidateAll();
|
linuxApp._renderingEngine?.InvalidateAll();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle system theme changes (e.g., GNOME/KDE dark mode toggle)
|
// Handle system theme changes (e.g., GNOME/KDE dark mode toggle)
|
||||||
SystemThemeService.Instance.ThemeChanged += (s, e) =>
|
SystemThemeService.Instance.ThemeChanged += (s, e) =>
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[LinuxApplication] System theme changed to: {e.NewTheme}");
|
Console.WriteLine($"[LinuxApplication] System theme changed to: {e.NewTheme}");
|
||||||
// Notify MAUI framework that system theme changed
|
|
||||||
// This will cause AppThemeBinding to re-evaluate
|
// Update MAUI's UserAppTheme to match system theme
|
||||||
|
// This will trigger the PropertyChanged handler which does the refresh
|
||||||
|
var newAppTheme = e.NewTheme == SystemTheme.Dark ? AppTheme.Dark : AppTheme.Light;
|
||||||
|
if (mauiApplication.UserAppTheme != newAppTheme)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] Setting UserAppTheme to {newAppTheme} to match system");
|
||||||
|
mauiApplication.UserAppTheme = newAppTheme;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If UserAppTheme didn't change (user manually set it), still refresh
|
||||||
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
LinuxViewRenderer.CurrentSkiaShell?.RefreshTheme();
|
||||||
|
linuxApp.RefreshPageForThemeChange();
|
||||||
|
if (linuxApp._useGtk)
|
||||||
|
{
|
||||||
|
linuxApp._gtkWindow?.RequestRedraw();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
linuxApp._renderingEngine?.InvalidateAll();
|
linuxApp._renderingEngine?.InvalidateAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (mauiApplication.MainPage != null)
|
// Get the main page - prefer CreateWindow() over deprecated MainPage
|
||||||
|
Page? mainPage = null;
|
||||||
|
|
||||||
|
// Try CreateWindow() first (the modern MAUI pattern)
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var mainPage = mauiApplication.MainPage;
|
// CreateWindow is protected, use reflection
|
||||||
|
var createWindowMethod = typeof(Application).GetMethod("CreateWindow",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
|
||||||
|
null, new[] { typeof(IActivationState) }, null);
|
||||||
|
|
||||||
|
if (createWindowMethod != null)
|
||||||
|
{
|
||||||
|
var mauiWindow = createWindowMethod.Invoke(mauiApplication, new object?[] { null }) as Microsoft.Maui.Controls.Window;
|
||||||
|
if (mauiWindow != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] Got Window from CreateWindow: {mauiWindow.GetType().Name}");
|
||||||
|
mainPage = mauiWindow.Page;
|
||||||
|
Console.WriteLine($"[LinuxApplication] Window.Page: {mainPage?.GetType().Name}");
|
||||||
|
|
||||||
|
// Add to windows list
|
||||||
|
var windowsField = typeof(Application).GetField("_windows",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
|
var windowsList = windowsField?.GetValue(mauiApplication) as List<Microsoft.Maui.Controls.Window>;
|
||||||
|
if (windowsList != null && !windowsList.Contains(mauiWindow))
|
||||||
|
{
|
||||||
|
windowsList.Add(mauiWindow);
|
||||||
|
mauiWindow.Parent = mauiApplication;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] CreateWindow failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to deprecated MainPage if CreateWindow didn't work
|
||||||
|
if (mainPage == null && mauiApplication.MainPage != null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] Falling back to MainPage: {mauiApplication.MainPage.GetType().Name}");
|
||||||
|
mainPage = mauiApplication.MainPage;
|
||||||
|
|
||||||
var windowsField = typeof(Application).GetField("_windows",
|
var windowsField = typeof(Application).GetField("_windows",
|
||||||
BindingFlags.NonPublic | BindingFlags.Instance);
|
BindingFlags.NonPublic | BindingFlags.Instance);
|
||||||
@@ -304,7 +421,10 @@ public class LinuxApplication : IDisposable
|
|||||||
{
|
{
|
||||||
windowsList[0].Page = mainPage;
|
windowsList[0].Page = mainPage;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainPage != null)
|
||||||
|
{
|
||||||
var renderer = new LinuxViewRenderer(mauiContext);
|
var renderer = new LinuxViewRenderer(mauiContext);
|
||||||
rootView = renderer.RenderPage(mainPage);
|
rootView = renderer.RenderPage(mainPage);
|
||||||
|
|
||||||
@@ -547,6 +667,133 @@ public class LinuxApplication : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces all views to refresh their theme-dependent properties.
|
||||||
|
/// This is needed because AppThemeBinding may not automatically trigger
|
||||||
|
/// property mappers on all platforms.
|
||||||
|
/// </summary>
|
||||||
|
private void RefreshPageForThemeChange()
|
||||||
|
{
|
||||||
|
Console.WriteLine("[LinuxApplication] RefreshPageForThemeChange - forcing property updates");
|
||||||
|
|
||||||
|
// First, try to trigger MAUI's RequestedThemeChanged event using reflection
|
||||||
|
// This ensures AppThemeBinding bindings re-evaluate
|
||||||
|
TriggerMauiThemeChanged();
|
||||||
|
|
||||||
|
if (_rootView == null) return;
|
||||||
|
|
||||||
|
// Traverse the visual tree and force theme-dependent properties to update
|
||||||
|
RefreshViewTheme(_rootView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers MAUI's internal RequestedThemeChanged event to force AppThemeBinding updates.
|
||||||
|
/// </summary>
|
||||||
|
private void TriggerMauiThemeChanged()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var app = Application.Current;
|
||||||
|
if (app == null) return;
|
||||||
|
|
||||||
|
var currentTheme = app.UserAppTheme;
|
||||||
|
Console.WriteLine($"[LinuxApplication] Triggering theme changed event for: {currentTheme}");
|
||||||
|
|
||||||
|
// Try to find and invoke the RequestedThemeChanged event
|
||||||
|
var eventField = typeof(Application).GetField("RequestedThemeChanged",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||||
|
|
||||||
|
if (eventField != null)
|
||||||
|
{
|
||||||
|
var eventDelegate = eventField.GetValue(app) as MulticastDelegate;
|
||||||
|
if (eventDelegate != null)
|
||||||
|
{
|
||||||
|
var args = new AppThemeChangedEventArgs(currentTheme);
|
||||||
|
foreach (var handler in eventDelegate.GetInvocationList())
|
||||||
|
{
|
||||||
|
handler.DynamicInvoke(app, args);
|
||||||
|
}
|
||||||
|
Console.WriteLine("[LinuxApplication] Successfully invoked RequestedThemeChanged handlers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Try alternative approach - trigger OnPropertyChanged for RequestedTheme
|
||||||
|
var onPropertyChangedMethod = typeof(BindableObject).GetMethod("OnPropertyChanged",
|
||||||
|
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
|
||||||
|
null, new[] { typeof(string) }, null);
|
||||||
|
|
||||||
|
if (onPropertyChangedMethod != null)
|
||||||
|
{
|
||||||
|
onPropertyChangedMethod.Invoke(app, new object[] { "RequestedTheme" });
|
||||||
|
Console.WriteLine("[LinuxApplication] Triggered OnPropertyChanged for RequestedTheme");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] Error triggering theme changed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshViewTheme(SkiaView view)
|
||||||
|
{
|
||||||
|
// Get the associated MAUI view and handler
|
||||||
|
var mauiView = view.MauiView;
|
||||||
|
var handler = mauiView?.Handler;
|
||||||
|
|
||||||
|
if (handler != null && mauiView != null)
|
||||||
|
{
|
||||||
|
// Force key properties to be re-mapped
|
||||||
|
// This ensures theme-dependent bindings are re-evaluated
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Background/BackgroundColor
|
||||||
|
handler.UpdateValue(nameof(IView.Background));
|
||||||
|
|
||||||
|
// For ImageButton, force Source to be re-mapped
|
||||||
|
if (mauiView is Microsoft.Maui.Controls.ImageButton)
|
||||||
|
{
|
||||||
|
handler.UpdateValue(nameof(IImageSourcePart.Source));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Image, force Source to be re-mapped
|
||||||
|
if (mauiView is Microsoft.Maui.Controls.Image)
|
||||||
|
{
|
||||||
|
handler.UpdateValue(nameof(IImageSourcePart.Source));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For views with text colors
|
||||||
|
if (mauiView is ITextStyle)
|
||||||
|
{
|
||||||
|
handler.UpdateValue(nameof(ITextStyle.TextColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Entry/Editor placeholder colors
|
||||||
|
if (mauiView is IPlaceholder)
|
||||||
|
{
|
||||||
|
handler.UpdateValue(nameof(IPlaceholder.PlaceholderColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Border stroke
|
||||||
|
if (mauiView is IBorderStroke)
|
||||||
|
{
|
||||||
|
handler.UpdateValue(nameof(IBorderStroke.Stroke));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[LinuxApplication] Error refreshing theme for {mauiView.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively process children
|
||||||
|
foreach (var child in view.Children)
|
||||||
|
{
|
||||||
|
RefreshViewTheme(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateAnimations()
|
private void UpdateAnimations()
|
||||||
{
|
{
|
||||||
// Update cursor blink for entry controls
|
// Update cursor blink for entry controls
|
||||||
@@ -802,6 +1049,16 @@ public class LinuxApplication : IDisposable
|
|||||||
string buttonName = e.Button == 1 ? "Left" : e.Button == 2 ? "Middle" : e.Button == 3 ? "Right" : $"Unknown({e.Button})";
|
string buttonName = e.Button == 1 ? "Left" : e.Button == 2 ? "Middle" : e.Button == 3 ? "Right" : $"Unknown({e.Button})";
|
||||||
Console.WriteLine($"[LinuxApplication.GTK] PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})");
|
Console.WriteLine($"[LinuxApplication.GTK] PointerPressed at ({e.X:F1}, {e.Y:F1}), Button={e.Button} ({buttonName})");
|
||||||
|
|
||||||
|
// Route to dialog if one is active
|
||||||
|
if (LinuxDialogService.HasActiveDialog)
|
||||||
|
{
|
||||||
|
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||||
|
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||||
|
LinuxDialogService.TopDialog?.OnPointerPressed(args);
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (LinuxDialogService.HasContextMenu)
|
if (LinuxDialogService.HasContextMenu)
|
||||||
{
|
{
|
||||||
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||||
@@ -842,6 +1099,17 @@ public class LinuxApplication : IDisposable
|
|||||||
private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e)
|
private void OnGtkPointerReleased(object? sender, (double X, double Y, int Button) e)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[DIAG] >>> OnGtkPointerReleased ENTER");
|
Console.WriteLine("[DIAG] >>> OnGtkPointerReleased ENTER");
|
||||||
|
|
||||||
|
// Route to dialog if one is active
|
||||||
|
if (LinuxDialogService.HasActiveDialog)
|
||||||
|
{
|
||||||
|
var button = e.Button == 1 ? PointerButton.Left : e.Button == 2 ? PointerButton.Middle : PointerButton.Right;
|
||||||
|
var args = new PointerEventArgs((float)e.X, (float)e.Y, button);
|
||||||
|
LinuxDialogService.TopDialog?.OnPointerReleased(args);
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_rootView == null) return;
|
if (_rootView == null) return;
|
||||||
|
|
||||||
if (_capturedView != null)
|
if (_capturedView != null)
|
||||||
@@ -870,6 +1138,15 @@ public class LinuxApplication : IDisposable
|
|||||||
|
|
||||||
private void OnGtkPointerMoved(object? sender, (double X, double Y) e)
|
private void OnGtkPointerMoved(object? sender, (double X, double Y) e)
|
||||||
{
|
{
|
||||||
|
// Route to dialog if one is active
|
||||||
|
if (LinuxDialogService.HasActiveDialog)
|
||||||
|
{
|
||||||
|
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||||
|
LinuxDialogService.TopDialog?.OnPointerMoved(args);
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (LinuxDialogService.HasContextMenu)
|
if (LinuxDialogService.HasContextMenu)
|
||||||
{
|
{
|
||||||
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
var args = new PointerEventArgs((float)e.X, (float)e.Y);
|
||||||
@@ -906,24 +1183,42 @@ public class LinuxApplication : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
private void OnGtkKeyPressed(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
||||||
{
|
|
||||||
if (_focusedView != null)
|
|
||||||
{
|
{
|
||||||
var key = ConvertGdkKey(e.KeyVal);
|
var key = ConvertGdkKey(e.KeyVal);
|
||||||
var modifiers = ConvertGdkModifiers(e.State);
|
var modifiers = ConvertGdkModifiers(e.State);
|
||||||
var args = new KeyEventArgs(key, modifiers);
|
var args = new KeyEventArgs(key, modifiers);
|
||||||
|
|
||||||
|
// Route to dialog if one is active
|
||||||
|
if (LinuxDialogService.HasActiveDialog)
|
||||||
|
{
|
||||||
|
LinuxDialogService.TopDialog?.OnKeyDown(args);
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_focusedView != null)
|
||||||
|
{
|
||||||
_focusedView.OnKeyDown(args);
|
_focusedView.OnKeyDown(args);
|
||||||
_gtkWindow?.RequestRedraw();
|
_gtkWindow?.RequestRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
private void OnGtkKeyReleased(object? sender, (uint KeyVal, uint KeyCode, uint State) e)
|
||||||
{
|
|
||||||
if (_focusedView != null)
|
|
||||||
{
|
{
|
||||||
var key = ConvertGdkKey(e.KeyVal);
|
var key = ConvertGdkKey(e.KeyVal);
|
||||||
var modifiers = ConvertGdkModifiers(e.State);
|
var modifiers = ConvertGdkModifiers(e.State);
|
||||||
var args = new KeyEventArgs(key, modifiers);
|
var args = new KeyEventArgs(key, modifiers);
|
||||||
|
|
||||||
|
// Route to dialog if one is active
|
||||||
|
if (LinuxDialogService.HasActiveDialog)
|
||||||
|
{
|
||||||
|
LinuxDialogService.TopDialog?.OnKeyUp(args);
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_focusedView != null)
|
||||||
|
{
|
||||||
_focusedView.OnKeyUp(args);
|
_focusedView.OnKeyUp(args);
|
||||||
_gtkWindow?.RequestRedraw();
|
_gtkWindow?.RequestRedraw();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,4 +189,97 @@ internal static class GtkNative
|
|||||||
|
|
||||||
[DllImport("libgdk-3.so.0")]
|
[DllImport("libgdk-3.so.0")]
|
||||||
public static extern void gdk_event_free(IntPtr eventPtr);
|
public static extern void gdk_event_free(IntPtr eventPtr);
|
||||||
|
|
||||||
|
// Message Dialog support
|
||||||
|
public const int GTK_DIALOG_MODAL = 1;
|
||||||
|
public const int GTK_DIALOG_DESTROY_WITH_PARENT = 2;
|
||||||
|
|
||||||
|
public const int GTK_MESSAGE_INFO = 0;
|
||||||
|
public const int GTK_MESSAGE_WARNING = 1;
|
||||||
|
public const int GTK_MESSAGE_QUESTION = 2;
|
||||||
|
public const int GTK_MESSAGE_ERROR = 3;
|
||||||
|
public const int GTK_MESSAGE_OTHER = 4;
|
||||||
|
|
||||||
|
public const int GTK_BUTTONS_NONE = 0;
|
||||||
|
public const int GTK_BUTTONS_OK = 1;
|
||||||
|
public const int GTK_BUTTONS_CLOSE = 2;
|
||||||
|
public const int GTK_BUTTONS_CANCEL = 3;
|
||||||
|
public const int GTK_BUTTONS_YES_NO = 4;
|
||||||
|
public const int GTK_BUTTONS_OK_CANCEL = 5;
|
||||||
|
|
||||||
|
public const int GTK_RESPONSE_NONE = -1;
|
||||||
|
public const int GTK_RESPONSE_REJECT = -2;
|
||||||
|
public const int GTK_RESPONSE_ACCEPT = -3;
|
||||||
|
public const int GTK_RESPONSE_DELETE_EVENT = -4;
|
||||||
|
public const int GTK_RESPONSE_OK = -5;
|
||||||
|
public const int GTK_RESPONSE_CANCEL = -6;
|
||||||
|
public const int GTK_RESPONSE_CLOSE = -7;
|
||||||
|
public const int GTK_RESPONSE_YES = -8;
|
||||||
|
public const int GTK_RESPONSE_NO = -9;
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_message_dialog_new(
|
||||||
|
IntPtr parent,
|
||||||
|
int flags,
|
||||||
|
int type,
|
||||||
|
int buttons,
|
||||||
|
string message,
|
||||||
|
IntPtr args);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern int gtk_dialog_run(IntPtr dialog);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_window_set_transient_for(IntPtr window, IntPtr parent);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_window_set_modal(IntPtr window, bool modal);
|
||||||
|
|
||||||
|
// Dialog with custom content (for prompt dialogs)
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_dialog_new_with_buttons(
|
||||||
|
string title,
|
||||||
|
IntPtr parent,
|
||||||
|
int flags,
|
||||||
|
string firstButtonText,
|
||||||
|
int firstButtonResponse,
|
||||||
|
string secondButtonText,
|
||||||
|
int secondButtonResponse,
|
||||||
|
IntPtr terminator);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_dialog_get_content_area(IntPtr dialog);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_box_new(int orientation, int spacing);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_box_pack_start(IntPtr box, IntPtr child, bool expand, bool fill, uint padding);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_label_new(string text);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_entry_new();
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_entry_set_text(IntPtr entry, string text);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern IntPtr gtk_entry_get_text(IntPtr entry);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_widget_set_margin_start(IntPtr widget, int margin);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_widget_set_margin_end(IntPtr widget, int margin);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_widget_set_margin_top(IntPtr widget, int margin);
|
||||||
|
|
||||||
|
[DllImport("libgtk-3.so.0")]
|
||||||
|
public static extern void gtk_widget_set_margin_bottom(IntPtr widget, int margin);
|
||||||
|
|
||||||
|
public const int GTK_ORIENTATION_HORIZONTAL = 0;
|
||||||
|
public const int GTK_ORIENTATION_VERTICAL = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,31 @@ internal static class WebKitNative
|
|||||||
private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView);
|
private delegate IntPtr WebKitWebViewGetSettingsDelegate(IntPtr webView);
|
||||||
private delegate void WebKitSettingsSetHardwareAccelerationPolicyDelegate(IntPtr settings, int policy);
|
private delegate void WebKitSettingsSetHardwareAccelerationPolicyDelegate(IntPtr settings, int policy);
|
||||||
private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled);
|
private delegate void WebKitSettingsSetEnableJavascriptDelegate(IntPtr settings, bool enabled);
|
||||||
|
private delegate void WebKitWebViewSetBackgroundColorDelegate(IntPtr webView, ref GdkRGBA color);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct GdkRGBA
|
||||||
|
{
|
||||||
|
public double Red;
|
||||||
|
public double Green;
|
||||||
|
public double Blue;
|
||||||
|
public double Alpha;
|
||||||
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData);
|
public delegate void LoadChangedCallback(IntPtr webView, int loadEvent, IntPtr userData);
|
||||||
|
|
||||||
private delegate ulong GSignalConnectDataDelegate(IntPtr instance, string signalName, LoadChangedCallback callback, IntPtr userData, IntPtr destroyNotify, int connectFlags);
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
|
public delegate bool ScriptDialogCallback(IntPtr webView, IntPtr dialog, IntPtr userData);
|
||||||
|
|
||||||
|
private delegate ulong GSignalConnectDataDelegate(IntPtr instance, string signalName, Delegate callback, IntPtr userData, IntPtr destroyNotify, int connectFlags);
|
||||||
|
|
||||||
|
// WebKitScriptDialog functions
|
||||||
|
private delegate int WebKitScriptDialogGetDialogTypeDelegate(IntPtr dialog);
|
||||||
|
private delegate IntPtr WebKitScriptDialogGetMessageDelegate(IntPtr dialog);
|
||||||
|
private delegate void WebKitScriptDialogConfirmSetConfirmedDelegate(IntPtr dialog, bool confirmed);
|
||||||
|
private delegate IntPtr WebKitScriptDialogPromptGetDefaultTextDelegate(IntPtr dialog);
|
||||||
|
private delegate void WebKitScriptDialogPromptSetTextDelegate(IntPtr dialog, string text);
|
||||||
|
|
||||||
public enum WebKitLoadEvent
|
public enum WebKitLoadEvent
|
||||||
{
|
{
|
||||||
@@ -34,6 +54,14 @@ internal static class WebKitNative
|
|||||||
Finished
|
Finished
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum WebKitScriptDialogType
|
||||||
|
{
|
||||||
|
Alert = 0,
|
||||||
|
Confirm = 1,
|
||||||
|
Prompt = 2,
|
||||||
|
BeforeUnloadConfirm = 3
|
||||||
|
}
|
||||||
|
|
||||||
private static IntPtr _handle;
|
private static IntPtr _handle;
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
|
|
||||||
@@ -59,9 +87,21 @@ internal static class WebKitNative
|
|||||||
private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings;
|
private static WebKitWebViewGetSettingsDelegate? _webkitGetSettings;
|
||||||
private static WebKitSettingsSetHardwareAccelerationPolicyDelegate? _webkitSetHardwareAccel;
|
private static WebKitSettingsSetHardwareAccelerationPolicyDelegate? _webkitSetHardwareAccel;
|
||||||
private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript;
|
private static WebKitSettingsSetEnableJavascriptDelegate? _webkitSetJavascript;
|
||||||
|
private static WebKitWebViewSetBackgroundColorDelegate? _webkitSetBackgroundColor;
|
||||||
private static GSignalConnectDataDelegate? _gSignalConnectData;
|
private static GSignalConnectDataDelegate? _gSignalConnectData;
|
||||||
|
private static WebKitScriptDialogGetDialogTypeDelegate? _webkitScriptDialogGetDialogType;
|
||||||
|
private static WebKitScriptDialogGetMessageDelegate? _webkitScriptDialogGetMessage;
|
||||||
|
private static WebKitScriptDialogConfirmSetConfirmedDelegate? _webkitScriptDialogConfirmSetConfirmed;
|
||||||
|
private static WebKitScriptDialogPromptGetDefaultTextDelegate? _webkitScriptDialogPromptGetDefaultText;
|
||||||
|
private static WebKitScriptDialogPromptSetTextDelegate? _webkitScriptDialogPromptSetText;
|
||||||
|
|
||||||
private static readonly Dictionary<IntPtr, LoadChangedCallback> _loadChangedCallbacks = new Dictionary<IntPtr, LoadChangedCallback>();
|
private static readonly Dictionary<IntPtr, LoadChangedCallback> _loadChangedCallbacks = new Dictionary<IntPtr, LoadChangedCallback>();
|
||||||
|
private static readonly Dictionary<IntPtr, ScriptDialogCallback> _scriptDialogCallbacks = new Dictionary<IntPtr, ScriptDialogCallback>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised when a JavaScript dialog (alert, confirm, prompt) is requested.
|
||||||
|
/// </summary>
|
||||||
|
public static event Action<IntPtr, WebKitScriptDialogType, string>? ScriptDialogRequested;
|
||||||
|
|
||||||
private const int RTLD_NOW = 2;
|
private const int RTLD_NOW = 2;
|
||||||
private const int RTLD_GLOBAL = 256;
|
private const int RTLD_GLOBAL = 256;
|
||||||
@@ -116,6 +156,12 @@ internal static class WebKitNative
|
|||||||
_webkitGetSettings = LoadFunction<WebKitWebViewGetSettingsDelegate>("webkit_web_view_get_settings");
|
_webkitGetSettings = LoadFunction<WebKitWebViewGetSettingsDelegate>("webkit_web_view_get_settings");
|
||||||
_webkitSetHardwareAccel = LoadFunction<WebKitSettingsSetHardwareAccelerationPolicyDelegate>("webkit_settings_set_hardware_acceleration_policy");
|
_webkitSetHardwareAccel = LoadFunction<WebKitSettingsSetHardwareAccelerationPolicyDelegate>("webkit_settings_set_hardware_acceleration_policy");
|
||||||
_webkitSetJavascript = LoadFunction<WebKitSettingsSetEnableJavascriptDelegate>("webkit_settings_set_enable_javascript");
|
_webkitSetJavascript = LoadFunction<WebKitSettingsSetEnableJavascriptDelegate>("webkit_settings_set_enable_javascript");
|
||||||
|
_webkitSetBackgroundColor = LoadFunction<WebKitWebViewSetBackgroundColorDelegate>("webkit_web_view_set_background_color");
|
||||||
|
_webkitScriptDialogGetDialogType = LoadFunction<WebKitScriptDialogGetDialogTypeDelegate>("webkit_script_dialog_get_dialog_type");
|
||||||
|
_webkitScriptDialogGetMessage = LoadFunction<WebKitScriptDialogGetMessageDelegate>("webkit_script_dialog_get_message");
|
||||||
|
_webkitScriptDialogConfirmSetConfirmed = LoadFunction<WebKitScriptDialogConfirmSetConfirmedDelegate>("webkit_script_dialog_confirm_set_confirmed");
|
||||||
|
_webkitScriptDialogPromptGetDefaultText = LoadFunction<WebKitScriptDialogPromptGetDefaultTextDelegate>("webkit_script_dialog_prompt_get_default_text");
|
||||||
|
_webkitScriptDialogPromptSetText = LoadFunction<WebKitScriptDialogPromptSetTextDelegate>("webkit_script_dialog_prompt_set_text");
|
||||||
|
|
||||||
_gobjectHandle = dlopen("libgobject-2.0.so.0", 258);
|
_gobjectHandle = dlopen("libgobject-2.0.so.0", 258);
|
||||||
if (_gobjectHandle != IntPtr.Zero)
|
if (_gobjectHandle != IntPtr.Zero)
|
||||||
@@ -238,6 +284,15 @@ internal static class WebKitNative
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SetBackgroundColor(IntPtr webView, double r, double g, double b, double a = 1.0)
|
||||||
|
{
|
||||||
|
if (_webkitSetBackgroundColor != null && webView != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
var color = new GdkRGBA { Red = r, Green = g, Blue = b, Alpha = a };
|
||||||
|
_webkitSetBackgroundColor(webView, ref color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static ulong ConnectLoadChanged(IntPtr webView, LoadChangedCallback callback)
|
public static ulong ConnectLoadChanged(IntPtr webView, LoadChangedCallback callback)
|
||||||
{
|
{
|
||||||
if (_gSignalConnectData == null || webView == IntPtr.Zero)
|
if (_gSignalConnectData == null || webView == IntPtr.Zero)
|
||||||
@@ -253,4 +308,72 @@ internal static class WebKitNative
|
|||||||
{
|
{
|
||||||
_loadChangedCallbacks.Remove(webView);
|
_loadChangedCallbacks.Remove(webView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects to the script-dialog signal to intercept JavaScript alert/confirm/prompt dialogs.
|
||||||
|
/// Returns true from the callback to prevent the default WebKitGTK dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static ulong ConnectScriptDialog(IntPtr webView, ScriptDialogCallback callback)
|
||||||
|
{
|
||||||
|
if (_gSignalConnectData == null || webView == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[WebKitNative] Cannot connect script-dialog: signal connect not available");
|
||||||
|
return 0uL;
|
||||||
|
}
|
||||||
|
_scriptDialogCallbacks[webView] = callback;
|
||||||
|
return _gSignalConnectData(webView, "script-dialog", callback, IntPtr.Zero, IntPtr.Zero, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DisconnectScriptDialog(IntPtr webView)
|
||||||
|
{
|
||||||
|
_scriptDialogCallbacks.Remove(webView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of a script dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static WebKitScriptDialogType GetScriptDialogType(IntPtr dialog)
|
||||||
|
{
|
||||||
|
if (_webkitScriptDialogGetDialogType == null || dialog == IntPtr.Zero)
|
||||||
|
return WebKitScriptDialogType.Alert;
|
||||||
|
return (WebKitScriptDialogType)_webkitScriptDialogGetDialogType(dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the message from a script dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static string? GetScriptDialogMessage(IntPtr dialog)
|
||||||
|
{
|
||||||
|
if (_webkitScriptDialogGetMessage == null || dialog == IntPtr.Zero)
|
||||||
|
return null;
|
||||||
|
IntPtr msgPtr = _webkitScriptDialogGetMessage(dialog);
|
||||||
|
return msgPtr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(msgPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the confirmed state for a confirm dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetScriptDialogConfirmed(IntPtr dialog, bool confirmed)
|
||||||
|
{
|
||||||
|
_webkitScriptDialogConfirmSetConfirmed?.Invoke(dialog, confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default text for a prompt dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static string? GetScriptDialogPromptDefaultText(IntPtr dialog)
|
||||||
|
{
|
||||||
|
if (_webkitScriptDialogPromptGetDefaultText == null || dialog == IntPtr.Zero)
|
||||||
|
return null;
|
||||||
|
IntPtr textPtr = _webkitScriptDialogPromptGetDefaultText(dialog);
|
||||||
|
return textPtr == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(textPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the text response for a prompt dialog.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetScriptDialogPromptText(IntPtr dialog, string text)
|
||||||
|
{
|
||||||
|
_webkitScriptDialogPromptSetText?.Invoke(dialog, text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ public class SystemThemeService
|
|||||||
};
|
};
|
||||||
|
|
||||||
private FileSystemWatcher? _settingsWatcher;
|
private FileSystemWatcher? _settingsWatcher;
|
||||||
|
private Timer? _pollTimer;
|
||||||
|
private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
private SystemThemeService()
|
private SystemThemeService()
|
||||||
{
|
{
|
||||||
@@ -78,6 +80,7 @@ public class SystemThemeService
|
|||||||
DetectTheme();
|
DetectTheme();
|
||||||
UpdateColors();
|
UpdateColors();
|
||||||
SetupWatcher();
|
SetupWatcher();
|
||||||
|
SetupPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DetectDesktopEnvironment()
|
private void DetectDesktopEnvironment()
|
||||||
@@ -373,6 +376,33 @@ public class SystemThemeService
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetupPolling()
|
||||||
|
{
|
||||||
|
// For GNOME and other desktops that use dconf/gsettings,
|
||||||
|
// file watching doesn't work. Use periodic polling instead.
|
||||||
|
_pollTimer = new Timer(OnPollTimer, null, PollInterval, PollInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPollTimer(object? state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oldTheme = CurrentTheme;
|
||||||
|
DetectTheme();
|
||||||
|
|
||||||
|
if (oldTheme != CurrentTheme)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[SystemThemeService] Theme change detected via polling: {oldTheme} -> {CurrentTheme}");
|
||||||
|
UpdateColors();
|
||||||
|
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs(CurrentTheme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[SystemThemeService] Error in poll timer: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSettingsChanged(object sender, FileSystemEventArgs e)
|
private void OnSettingsChanged(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
// Debounce and check relevant files
|
// Debounce and check relevant files
|
||||||
|
|||||||
@@ -407,7 +407,9 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
canvas.DrawPath(shapePath, borderPaint);
|
canvas.DrawPath(shapePath, borderPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw children
|
// Clip to shape and draw children
|
||||||
|
canvas.Save();
|
||||||
|
canvas.ClipPath(shapePath);
|
||||||
foreach (var child in Children)
|
foreach (var child in Children)
|
||||||
{
|
{
|
||||||
if (child.IsVisible)
|
if (child.IsVisible)
|
||||||
@@ -415,6 +417,7 @@ public class SkiaBorder : SkiaLayoutView
|
|||||||
child.Draw(canvas);
|
child.Draw(canvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
canvas.Restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
nameof(EntryBackgroundColor),
|
nameof(EntryBackgroundColor),
|
||||||
typeof(Color),
|
typeof(Color),
|
||||||
typeof(SkiaEntry),
|
typeof(SkiaEntry),
|
||||||
Colors.White,
|
Colors.Transparent,
|
||||||
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
|
propertyChanged: (b, o, n) => ((SkiaEntry)b).Invalidate());
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -837,12 +837,32 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
Invalidate();
|
Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
|
||||||
|
{
|
||||||
|
// Skip base background drawing if Entry is transparent
|
||||||
|
// (transparent Entry is likely inside a Border that handles appearance)
|
||||||
|
var bgColor = ToSKColor(EntryBackgroundColor);
|
||||||
|
var baseBgColor = GetEffectiveBackgroundColor();
|
||||||
|
if (bgColor.Alpha < 10 && baseBgColor.Alpha < 10)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Otherwise let base class draw
|
||||||
|
base.DrawBackground(canvas, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||||
|
{
|
||||||
|
var bgColor = ToSKColor(EntryBackgroundColor);
|
||||||
|
var isTransparent = bgColor.Alpha < 10; // Consider nearly transparent as transparent
|
||||||
|
|
||||||
|
// Only draw background and border if not transparent
|
||||||
|
// (transparent means the Entry is likely inside a Border that handles appearance)
|
||||||
|
if (!isTransparent)
|
||||||
{
|
{
|
||||||
// Draw background
|
// Draw background
|
||||||
using var bgPaint = new SKPaint
|
using var bgPaint = new SKPaint
|
||||||
{
|
{
|
||||||
Color = ToSKColor(EntryBackgroundColor),
|
Color = bgColor,
|
||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
Style = SKPaintStyle.Fill
|
Style = SKPaintStyle.Fill
|
||||||
};
|
};
|
||||||
@@ -862,6 +882,7 @@ public class SkiaEntry : SkiaView, IInputContext
|
|||||||
StrokeWidth = borderWidth
|
StrokeWidth = borderWidth
|
||||||
};
|
};
|
||||||
canvas.DrawRoundRect(rect, borderPaint);
|
canvas.DrawRoundRect(rect, borderPaint);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate content bounds
|
// Calculate content bounds
|
||||||
var contentBounds = new SKRect(
|
var contentBounds = new SKRect(
|
||||||
|
|||||||
@@ -693,6 +693,8 @@ public class SkiaImage : SkiaView
|
|||||||
using var canvas = new SKCanvas(newBitmap);
|
using var canvas = new SKCanvas(newBitmap);
|
||||||
canvas.Clear(SKColors.Transparent);
|
canvas.Clear(SKColors.Transparent);
|
||||||
canvas.Scale(scale);
|
canvas.Scale(scale);
|
||||||
|
// Translate to handle negative viewBox coordinates (e.g., Material icons use 0 -960 960 960)
|
||||||
|
canvas.Translate(-cullRect.Left, -cullRect.Top);
|
||||||
canvas.DrawPicture(svg.Picture, null);
|
canvas.DrawPicture(svg.Picture, null);
|
||||||
}
|
}
|
||||||
}, cts.Token);
|
}, cts.Token);
|
||||||
|
|||||||
@@ -239,6 +239,20 @@ public class SkiaImageButton : SkiaView
|
|||||||
|
|
||||||
#region Rendering
|
#region Rendering
|
||||||
|
|
||||||
|
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
|
||||||
|
{
|
||||||
|
// Skip base background drawing if button is transparent
|
||||||
|
var baseBgColor = ImageBackgroundColor != null
|
||||||
|
? ToSKColor(ImageBackgroundColor)
|
||||||
|
: GetEffectiveBackgroundColor();
|
||||||
|
|
||||||
|
if (baseBgColor.Alpha < 10)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Otherwise let base class draw
|
||||||
|
base.DrawBackground(canvas, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
protected override void OnDraw(SKCanvas canvas, SKRect bounds)
|
||||||
{
|
{
|
||||||
var padding = Padding;
|
var padding = Padding;
|
||||||
@@ -249,26 +263,29 @@ public class SkiaImageButton : SkiaView
|
|||||||
bounds.Bottom - (float)padding.Bottom);
|
bounds.Bottom - (float)padding.Bottom);
|
||||||
|
|
||||||
// Determine background color
|
// Determine background color
|
||||||
|
var baseBgColor = ImageBackgroundColor != null
|
||||||
|
? ToSKColor(ImageBackgroundColor)
|
||||||
|
: GetEffectiveBackgroundColor();
|
||||||
|
var isTransparentButton = baseBgColor.Alpha < 10;
|
||||||
|
|
||||||
SKColor bgColor;
|
SKColor bgColor;
|
||||||
if (IsPressed)
|
if (IsPressed && !isTransparentButton)
|
||||||
{
|
{
|
||||||
|
// Only show pressed state for non-transparent buttons
|
||||||
bgColor = ToSKColor(PressedBackgroundColor);
|
bgColor = ToSKColor(PressedBackgroundColor);
|
||||||
}
|
}
|
||||||
else if (IsHovered)
|
else if (IsHovered && !isTransparentButton)
|
||||||
{
|
{
|
||||||
|
// Only show hovered state for non-transparent buttons
|
||||||
bgColor = ToSKColor(HoveredBackgroundColor);
|
bgColor = ToSKColor(HoveredBackgroundColor);
|
||||||
}
|
}
|
||||||
else if (ImageBackgroundColor != null)
|
|
||||||
{
|
|
||||||
bgColor = ToSKColor(ImageBackgroundColor);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bgColor = GetEffectiveBackgroundColor();
|
bgColor = baseBgColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw background
|
// Draw background (skip if fully transparent)
|
||||||
if (bgColor != SKColors.Transparent || !IsOpaque)
|
if (bgColor.Alpha > 0)
|
||||||
{
|
{
|
||||||
using var bgPaint = new SKPaint
|
using var bgPaint = new SKPaint
|
||||||
{
|
{
|
||||||
@@ -477,9 +494,11 @@ public class SkiaImageButton : SkiaView
|
|||||||
using var canvas = new SKCanvas(bitmap);
|
using var canvas = new SKCanvas(bitmap);
|
||||||
canvas.Clear(SKColors.Transparent);
|
canvas.Clear(SKColors.Transparent);
|
||||||
canvas.Scale(scale);
|
canvas.Scale(scale);
|
||||||
|
// Translate to handle negative viewBox coordinates (e.g., Material icons use 0 -960 960 960)
|
||||||
|
canvas.Translate(-cullRect.Left, -cullRect.Top);
|
||||||
canvas.DrawPicture(svg.Picture);
|
canvas.DrawPicture(svg.Picture);
|
||||||
Bitmap = bitmap;
|
Bitmap = bitmap;
|
||||||
Console.WriteLine($"[SkiaImageButton] Loaded SVG: {foundPath} ({width}x{height})");
|
Console.WriteLine($"[SkiaImageButton] Loaded SVG: {foundPath} ({width}x{height}), cullRect={cullRect}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -786,9 +805,12 @@ public class SkiaImageButton : SkiaView
|
|||||||
}
|
}
|
||||||
// Fill (3) and Start (0) both use y = bounds.Top
|
// Fill (3) and Start (0) both use y = bounds.Top
|
||||||
|
|
||||||
return new Rect(x, y, finalWidth, finalHeight);
|
var result1 = new Rect(x, y, finalWidth, finalHeight);
|
||||||
|
Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (aligned): Y={result1.Y}, Height={result1.Height}");
|
||||||
|
return result1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[SkiaImageButton] ArrangeOverride output (unchanged): Y={bounds.Y}, Height={bounds.Height}");
|
||||||
return bounds;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -907,12 +907,46 @@ public class SkiaGrid : SkiaLayoutView
|
|||||||
|
|
||||||
// Apply child's margin
|
// Apply child's margin
|
||||||
var margin = child.Margin;
|
var margin = child.Margin;
|
||||||
var marginedBounds = new Rect(
|
var cellX = x + (float)margin.Left;
|
||||||
x + (float)margin.Left,
|
var cellY = y + (float)margin.Top;
|
||||||
y + (float)margin.Top,
|
var cellWidth = width - (float)margin.Left - (float)margin.Right;
|
||||||
width - (float)margin.Left - (float)margin.Right,
|
var cellHeight = height - (float)margin.Top - (float)margin.Bottom;
|
||||||
height - (float)margin.Top - (float)margin.Bottom);
|
|
||||||
child.Arrange(marginedBounds);
|
// Get child's desired size
|
||||||
|
var childDesiredSize = child.Measure(new Size(cellWidth, cellHeight));
|
||||||
|
var childWidth = (float)childDesiredSize.Width;
|
||||||
|
var childHeight = (float)childDesiredSize.Height;
|
||||||
|
|
||||||
|
var vAlign = (int)child.VerticalOptions.Alignment;
|
||||||
|
|
||||||
|
// Apply HorizontalOptions
|
||||||
|
// LayoutAlignment: Start=0, Center=1, End=2, Fill=3
|
||||||
|
float finalX = cellX;
|
||||||
|
float finalWidth = cellWidth;
|
||||||
|
var hAlign = (int)child.HorizontalOptions.Alignment;
|
||||||
|
if (hAlign != 3 && childWidth < cellWidth && childWidth > 0) // 3 = Fill
|
||||||
|
{
|
||||||
|
finalWidth = childWidth;
|
||||||
|
if (hAlign == 1) // Center
|
||||||
|
finalX = cellX + (cellWidth - childWidth) / 2;
|
||||||
|
else if (hAlign == 2) // End
|
||||||
|
finalX = cellX + cellWidth - childWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply VerticalOptions
|
||||||
|
float finalY = cellY;
|
||||||
|
float finalHeight = cellHeight;
|
||||||
|
// vAlign already calculated above for debug logging
|
||||||
|
if (vAlign != 3 && childHeight < cellHeight && childHeight > 0) // 3 = Fill
|
||||||
|
{
|
||||||
|
finalHeight = childHeight;
|
||||||
|
if (vAlign == 1) // Center
|
||||||
|
finalY = cellY + (cellHeight - childHeight) / 2;
|
||||||
|
else if (vAlign == 2) // End
|
||||||
|
finalY = cellY + cellHeight - childHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Arrange(new Rect(finalX, finalY, finalWidth, finalHeight));
|
||||||
}
|
}
|
||||||
return bounds;
|
return bounds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1534,8 +1534,8 @@ public abstract class SkiaView : BindableObject, IDisposable, IAccessible
|
|||||||
canvas.DrawRect(bounds, paint);
|
canvas.DrawRect(bounds, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to BackgroundColor
|
// Fall back to BackgroundColor (skip if transparent)
|
||||||
else if (_backgroundColorSK != SKColors.Transparent)
|
else if (_backgroundColorSK.Alpha > 0)
|
||||||
{
|
{
|
||||||
using var paint = new SKPaint { Color = _backgroundColorSK };
|
using var paint = new SKPaint { Color = _backgroundColorSK };
|
||||||
canvas.DrawRect(bounds, paint);
|
canvas.DrawRect(bounds, paint);
|
||||||
|
|||||||
28
build/OpenMaui.Controls.Linux.targets
Normal file
28
build/OpenMaui.Controls.Linux.targets
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
OpenMaui Linux Build Targets
|
||||||
|
Handles MauiImage items for Linux platform since we don't use the full MAUI SDK targets.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Copy MauiImage items to output directory -->
|
||||||
|
<Target Name="CopyMauiImagesToOutput" AfterTargets="Build" Condition="@(MauiImage) != ''">
|
||||||
|
<Message Importance="normal" Text="OpenMaui: Copying MauiImage items to output..." />
|
||||||
|
|
||||||
|
<Copy SourceFiles="@(MauiImage)"
|
||||||
|
DestinationFolder="$(OutputPath)"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
|
||||||
|
<!-- Also copy to Resources/Images subfolder for alternate lookup -->
|
||||||
|
<Copy SourceFiles="@(MauiImage)"
|
||||||
|
DestinationFolder="$(OutputPath)Resources/Images"
|
||||||
|
SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<!-- Ensure the Resources/Images directory exists -->
|
||||||
|
<Target Name="EnsureResourcesImagesDirectory" BeforeTargets="CopyMauiImagesToOutput">
|
||||||
|
<MakeDir Directories="$(OutputPath)Resources/Images" Condition="!Exists('$(OutputPath)Resources/Images')" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user