More fixes

This commit is contained in:
2026-01-17 15:06:39 +00:00
parent 9451611c3a
commit 10a66cd399
6 changed files with 195 additions and 70 deletions

View File

@@ -30,6 +30,8 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
[nameof(IView.Background)] = MapBackground, [nameof(IView.Background)] = MapBackground,
["BackgroundColor"] = MapBackgroundColor, ["BackgroundColor"] = MapBackgroundColor,
[nameof(IPadding.Padding)] = MapPadding, [nameof(IPadding.Padding)] = MapPadding,
["WidthRequest"] = MapWidthRequest,
["HeightRequest"] = MapHeightRequest,
}; };
public static CommandMapper<IBorderView, BorderHandler> CommandMapper = public static CommandMapper<IBorderView, BorderHandler> CommandMapper =
@@ -59,6 +61,27 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
platformView.MauiView = view; platformView.MauiView = view;
} }
platformView.Tapped += OnPlatformViewTapped; platformView.Tapped += OnPlatformViewTapped;
// Explicitly map properties since they may be set before handler creation
if (VirtualView is VisualElement ve)
{
if (ve.BackgroundColor != null)
{
platformView.BackgroundColor = ve.BackgroundColor;
}
else if (ve.Background is SolidColorBrush brush && brush.Color != null)
{
platformView.BackgroundColor = brush.Color;
}
if (ve.WidthRequest >= 0)
{
platformView.WidthRequest = ve.WidthRequest;
}
if (ve.HeightRequest >= 0)
{
platformView.HeightRequest = ve.HeightRequest;
}
}
} }
protected override void DisconnectHandler(SkiaBorder platformView) protected override void DisconnectHandler(SkiaBorder platformView)
@@ -130,10 +153,15 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
{ {
if (handler.PlatformView is null) return; if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.BackgroundColor != null) if (border is VisualElement ve)
{ {
handler.PlatformView.BackgroundColor = ve.BackgroundColor; var bgColor = ve.BackgroundColor;
handler.PlatformView.Invalidate(); Console.WriteLine($"[BorderHandler] MapBackgroundColor: {bgColor}");
if (bgColor != null)
{
handler.PlatformView.BackgroundColor = bgColor;
handler.PlatformView.Invalidate();
}
} }
} }
@@ -239,4 +267,26 @@ public partial class BorderHandler : ViewHandler<IBorderView, SkiaBorder>
} }
handler.PlatformView.Invalidate(); handler.PlatformView.Invalidate();
} }
public static void MapWidthRequest(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.WidthRequest >= 0)
{
handler.PlatformView.WidthRequest = ve.WidthRequest;
handler.PlatformView.InvalidateMeasure();
}
}
public static void MapHeightRequest(BorderHandler handler, IBorderView border)
{
if (handler.PlatformView is null) return;
if (border is VisualElement ve && ve.HeightRequest >= 0)
{
handler.PlatformView.HeightRequest = ve.HeightRequest;
handler.PlatformView.InvalidateMeasure();
}
}
} }

View File

@@ -688,53 +688,17 @@ public class LinuxApplication : IDisposable
} }
/// <summary> /// <summary>
/// Triggers MAUI's internal RequestedThemeChanged event to force AppThemeBinding updates. /// Called after theme change to refresh views.
/// Note: MAUI's Application.UserAppTheme setter automatically triggers RequestedThemeChanged
/// via WeakEventManager, which AppThemeBinding subscribes to. This method handles
/// any additional platform-specific refresh needed.
/// </summary> /// </summary>
private void TriggerMauiThemeChanged() private void TriggerMauiThemeChanged()
{ {
try var app = Application.Current;
{ if (app == null) return;
var app = Application.Current;
if (app == null) return;
var currentTheme = app.UserAppTheme; Console.WriteLine($"[LinuxApplication] Theme is now: {app.UserAppTheme}, RequestedTheme: {app.RequestedTheme}");
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) private void RefreshViewTheme(SkiaView view)
@@ -749,8 +713,9 @@ public class LinuxApplication : IDisposable
// This ensures theme-dependent bindings are re-evaluated // This ensures theme-dependent bindings are re-evaluated
try try
{ {
// Background/BackgroundColor // Background/BackgroundColor - both need updating for AppThemeBinding
handler.UpdateValue(nameof(IView.Background)); handler.UpdateValue(nameof(IView.Background));
handler.UpdateValue("BackgroundColor");
// For ImageButton, force Source to be re-mapped // For ImageButton, force Source to be re-mapped
if (mauiView is Microsoft.Maui.Controls.ImageButton) if (mauiView is Microsoft.Maui.Controls.ImageButton)
@@ -788,8 +753,29 @@ public class LinuxApplication : IDisposable
} }
} }
// Special handling for ItemsViews (CollectionView, ListView)
// Their item views are cached separately and need to be refreshed
if (view is SkiaItemsView itemsView)
{
itemsView.RefreshTheme();
}
// Special handling for NavigationPage - it stores content in _currentPage
if (view is SkiaNavigationPage navPage && navPage.CurrentPage != null)
{
RefreshViewTheme(navPage.CurrentPage);
}
// Special handling for ContentPage - it stores content in Content property
if (view is SkiaPage page && page.Content != null)
{
RefreshViewTheme(page.Content);
}
// Recursively process children // Recursively process children
foreach (var child in view.Children) // Note: SkiaLayoutView hides SkiaView.Children with 'new', so we need to cast
IReadOnlyList<SkiaView> children = view is SkiaLayoutView layout ? layout.Children : view.Children;
foreach (var child in children)
{ {
RefreshViewTheme(child); RefreshViewTheme(child);
} }

View File

@@ -341,6 +341,14 @@ public class SkiaBorder : SkiaLayoutView
return path; return path;
} }
/// <summary>
/// Override to skip rectangular background - OnDraw handles it with the correct shape.
/// </summary>
protected override void DrawBackground(SKCanvas canvas, SKRect bounds)
{
// Don't draw rectangular background - OnDraw draws background with shape path
}
protected override void OnDraw(SKCanvas canvas, SKRect bounds) protected override void OnDraw(SKCanvas canvas, SKRect bounds)
{ {
float strokeThickness = (float)StrokeThickness; float strokeThickness = (float)StrokeThickness;

View File

@@ -77,7 +77,7 @@ public class SkiaEditor : SkiaView, IInputContext
nameof(BorderColor), nameof(BorderColor),
typeof(Color), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
Color.FromRgb(0xBD, 0xBD, 0xBD), Colors.Transparent,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -301,7 +301,7 @@ public class SkiaEditor : SkiaView, IInputContext
nameof(EditorBackgroundColor), nameof(EditorBackgroundColor),
typeof(Color), typeof(Color),
typeof(SkiaEditor), typeof(SkiaEditor),
Colors.White, Colors.Transparent,
BindingMode.TwoWay, BindingMode.TwoWay,
propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate()); propertyChanged: (b, o, n) => ((SkiaEditor)b).Invalidate());
@@ -891,15 +891,18 @@ public class SkiaEditor : SkiaView, IInputContext
}; };
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint); canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), bgPaint);
// Draw border // Draw border only if BorderColor is not transparent
using var borderPaint = new SKPaint if (BorderColor != null && BorderColor != Colors.Transparent && BorderColor.Alpha > 0)
{ {
Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor), using var borderPaint = new SKPaint
Style = SKPaintStyle.Stroke, {
StrokeWidth = IsFocused ? 2 : 1, Color = IsFocused ? ToSKColor(CursorColor) : ToSKColor(BorderColor),
IsAntialias = true Style = SKPaintStyle.Stroke,
}; StrokeWidth = IsFocused ? 2 : 1,
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint); IsAntialias = true
};
canvas.DrawRoundRect(new SKRoundRect(bounds, cornerRadius), borderPaint);
}
// Setup text rendering // Setup text rendering
using var font = new SKFont(SKTypeface.Default, fontSize); using var font = new SKFont(SKTypeface.Default, fontSize);
@@ -1085,6 +1088,13 @@ public class SkiaEditor : SkiaView, IInputContext
{ {
if (!IsEnabled) return; if (!IsEnabled) return;
// Handle right-click context menu
if (e.Button == PointerButton.Right)
{
ShowContextMenu(e.X, e.Y);
return;
}
IsFocused = true; IsFocused = true;
// Use screen coordinates for proper hit detection // Use screen coordinates for proper hit detection
@@ -1491,6 +1501,39 @@ public class SkiaEditor : SkiaView, IInputContext
_selectionLength = 0; _selectionLength = 0;
} }
private void ShowContextMenu(float x, float y)
{
Console.WriteLine($"[SkiaEditor] ShowContextMenu at ({x}, {y})");
bool hasSelection = _selectionLength != 0;
bool hasText = !string.IsNullOrEmpty(Text);
bool hasClipboard = !string.IsNullOrEmpty(SystemClipboard.GetText());
bool isEditable = !IsReadOnly;
GtkContextMenuService.ShowContextMenu(new List<GtkMenuItem>
{
new GtkMenuItem("Cut", () =>
{
CutToClipboard();
Invalidate();
}, hasSelection && isEditable),
new GtkMenuItem("Copy", () =>
{
CopyToClipboard();
}, hasSelection),
new GtkMenuItem("Paste", () =>
{
PasteFromClipboard();
Invalidate();
}, hasClipboard && isEditable),
GtkMenuItem.Separator,
new GtkMenuItem("Select All", () =>
{
SelectAll();
Invalidate();
}, hasText)
});
}
#endregion #endregion
/// <summary> /// <summary>

View File

@@ -129,6 +129,18 @@ public class SkiaItemsView : SkiaView
_scrollOffset = 0; _scrollOffset = 0;
} }
/// <summary>
/// Called when theme changes to refresh all cached item views.
/// Clears the item view cache so items are recreated with new theme colors.
/// </summary>
public virtual void RefreshTheme()
{
// Clear cached views to force recreation with new AppThemeBinding values
_itemViewCache.Clear();
_itemHeights.Clear();
Invalidate();
}
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
RefreshItems(); RefreshItems();

View File

@@ -433,24 +433,50 @@ public class SkiaContentPage : SkiaPage
}; };
float rightEdge = navBarBounds.Right - 16; float rightEdge = navBarBounds.Right - 16;
const float iconSize = 24f;
const float itemPadding = 12f;
foreach (var item in primaryItems.AsEnumerable().Reverse()) foreach (var item in primaryItems.AsEnumerable().Reverse())
{ {
var textBounds = new SKRect(); float itemWidth;
textPaint.MeasureText(item.Text, ref textBounds); float itemLeft;
var itemWidth = textBounds.Width + 24; // Padding if (item.Icon != null)
var itemLeft = rightEdge - itemWidth; {
// Icon-based toolbar item
itemWidth = iconSize + itemPadding * 2;
itemLeft = rightEdge - itemWidth;
// Store hit area for click handling
item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom);
// Draw icon centered in the hit area
var iconX = itemLeft + itemPadding;
var iconY = navBarBounds.MidY - iconSize / 2;
var destRect = new SKRect(iconX, iconY, iconX + iconSize, iconY + iconSize);
canvas.DrawBitmap(item.Icon, destRect);
Console.WriteLine($"[SkiaContentPage] Drew toolbar icon '{item.Text}' at ({iconX}, {iconY})");
}
else
{
// Text-based toolbar item (fallback)
var textBounds = new SKRect();
textPaint.MeasureText(item.Text, ref textBounds);
itemWidth = textBounds.Width + 24;
itemLeft = rightEdge - itemWidth;
// Store hit area for click handling
item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom);
// Draw text
var x = itemLeft + 12;
var y = navBarBounds.MidY - textBounds.MidY;
canvas.DrawText(item.Text, x, y, textPaint);
}
// Store hit area for click handling
item.HitBounds = new SKRect(itemLeft, navBarBounds.Top, rightEdge, navBarBounds.Bottom);
Console.WriteLine($"[SkiaContentPage] Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}"); Console.WriteLine($"[SkiaContentPage] Toolbar item '{item.Text}' HitBounds set to {item.HitBounds}");
// Draw text
var x = itemLeft + 12;
var y = navBarBounds.MidY - textBounds.MidY;
canvas.DrawText(item.Text, x, y, textPaint);
rightEdge = itemLeft - 8; // Gap between items rightEdge = itemLeft - 8; // Gap between items
} }
} }