Files
logikonline e55230c441
All checks were successful
CI / Build (Linux) (push) Successful in 21s
refactor: replace Console.WriteLine with DiagnosticLog service
Replace 495+ Console.WriteLine debug statements across handlers, dispatching, services, views, and window components with centralized DiagnosticLog service for proper logging infrastructure. Add new DiagnosticLog.cs service with Debug/Error methods to eliminate debug logging pollution in production code.
2026-03-06 22:06:08 -05:00

565 lines
20 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;
namespace Microsoft.Maui.Platform.Linux.Hosting;
/// <summary>
/// Renders MAUI views to Skia platform views.
/// Handles the conversion of the view hierarchy.
/// </summary>
public class LinuxViewRenderer
{
private readonly IMauiContext _mauiContext;
/// <summary>
/// Static reference to the current MAUI Shell for navigation support.
/// Used when Shell.Current is not available through normal lifecycle.
/// </summary>
public static Shell? CurrentMauiShell { get; private set; }
/// <summary>
/// Static reference to the current SkiaShell for navigation updates.
/// </summary>
public static SkiaShell? CurrentSkiaShell { get; private set; }
/// <summary>
/// Navigate to a route using the SkiaShell directly.
/// Use this instead of Shell.Current.GoToAsync on Linux.
/// </summary>
/// <param name="route">The route to navigate to (e.g., "Buttons" or "//Buttons")</param>
/// <returns>True if navigation succeeded</returns>
public static bool NavigateToRoute(string route)
{
if (CurrentSkiaShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell is null");
return false;
}
// Clean up the route - remove leading // or /
var cleanRoute = route.TrimStart('/');
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Navigating to: {cleanRoute}");
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
{
var section = CurrentSkiaShell.Sections[i];
if (section.Route.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase) ||
section.Title.Equals(cleanRoute, StringComparison.OrdinalIgnoreCase))
{
DiagnosticLog.Debug("LinuxViewRenderer", $"NavigateToRoute: Found section {i}: {section.Title}");
CurrentSkiaShell.NavigateToSection(i);
return true;
}
}
DiagnosticLog.Warn("LinuxViewRenderer", $"NavigateToRoute: Route not found: {cleanRoute}");
return false;
}
/// <summary>
/// Current renderer instance for page rendering.
/// </summary>
public static LinuxViewRenderer? CurrentRenderer { get; set; }
/// <summary>
/// Pushes a page onto the navigation stack.
/// </summary>
/// <param name="page">The page to push</param>
/// <returns>True if successful</returns>
public static bool PushPage(Page page)
{
DiagnosticLog.Debug("LinuxViewRenderer", $"PushPage: Pushing page: {page.GetType().Name}");
if (CurrentSkiaShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentSkiaShell is null");
return false;
}
if (CurrentRenderer == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: CurrentRenderer is null");
return false;
}
try
{
// Render the page through the proper handler system
// This ensures all properties (including BackgroundColor via AppThemeBinding) are mapped
var skiaPage = CurrentRenderer.RenderPage(page);
if (skiaPage == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PushPage: Failed to render page through handler");
return false;
}
// Push onto SkiaShell's navigation stack
CurrentSkiaShell.PushAsync(skiaPage, page.Title ?? "Detail");
DiagnosticLog.Debug("LinuxViewRenderer", "PushPage: Successfully pushed page via handler system");
return true;
}
catch (Exception ex)
{
DiagnosticLog.Error("LinuxViewRenderer", "PushPage failed", ex);
return false;
}
}
/// <summary>
/// Pops the current page from the navigation stack.
/// </summary>
/// <returns>True if successful</returns>
public static bool PopPage()
{
DiagnosticLog.Debug("LinuxViewRenderer", "PopPage: Popping page");
if (CurrentSkiaShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "PopPage: CurrentSkiaShell is null");
return false;
}
return CurrentSkiaShell.PopAsync();
}
public LinuxViewRenderer(IMauiContext mauiContext)
{
_mauiContext = mauiContext ?? throw new ArgumentNullException(nameof(mauiContext));
// Store reference for push/pop navigation
CurrentRenderer = this;
}
/// <summary>
/// Renders a MAUI page and returns the corresponding SkiaView.
/// </summary>
public SkiaView? RenderPage(Page page)
{
if (page == null)
return null;
// Special handling for Shell - Shell is our navigation container
if (page is Shell shell)
{
return RenderShell(shell);
}
// Set handler context
page.Handler?.DisconnectHandler();
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)
{
return skiaPage;
}
return null;
}
/// <summary>
/// Renders a MAUI Shell with all its navigation structure.
/// </summary>
private SkiaShell RenderShell(Shell shell)
{
// Store reference for navigation - Shell.Current is computed from Application.Current.Windows
// Our platform handles navigation through SkiaShell directly
CurrentMauiShell = shell;
var skiaShell = new SkiaShell
{
Title = shell.Title ?? "App",
FlyoutBehavior = shell.FlyoutBehavior switch
{
FlyoutBehavior.Flyout => ShellFlyoutBehavior.Flyout,
FlyoutBehavior.Locked => ShellFlyoutBehavior.Locked,
FlyoutBehavior.Disabled => ShellFlyoutBehavior.Disabled,
_ => ShellFlyoutBehavior.Flyout
},
MauiShell = shell
};
// Apply shell colors based on theme
ApplyShellColors(skiaShell, shell);
// Render flyout header if present
if (shell.FlyoutHeader is View headerView)
{
var skiaHeader = RenderView(headerView);
if (skiaHeader != null)
{
skiaShell.FlyoutHeaderView = skiaHeader;
skiaShell.FlyoutHeaderHeight = (float)(headerView.HeightRequest > 0 ? headerView.HeightRequest : 140.0);
}
}
// Render flyout footer if present, otherwise use version text
if (shell.FlyoutFooter is View footerView)
{
var skiaFooter = RenderView(footerView);
if (skiaFooter != null)
{
skiaShell.FlyoutFooterView = skiaFooter;
skiaShell.FlyoutFooterHeight = (float)(footerView.HeightRequest > 0 ? footerView.HeightRequest : 40.0);
}
}
else
{
// Fallback: use assembly version as footer text
var version = Assembly.GetEntryAssembly()?.GetName().Version;
skiaShell.FlyoutFooterText = $"Version {version?.Major ?? 1}.{version?.Minor ?? 0}.{version?.Build ?? 0}";
}
// Process shell items into sections
foreach (var item in shell.Items)
{
ProcessShellItem(skiaShell, item);
}
// Store reference to SkiaShell for navigation
CurrentSkiaShell = skiaShell;
// Set up content renderer and color refresher delegates
skiaShell.ContentRenderer = CreateShellContentPage;
skiaShell.ColorRefresher = ApplyShellColors;
// Subscribe to MAUI Shell navigation events to update SkiaShell
shell.Navigated += OnShellNavigated;
shell.Navigating += (s, e) => DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Navigating: {e.Target}");
DiagnosticLog.Debug("LinuxViewRenderer", $"Shell navigation events subscribed. Sections: {skiaShell.Sections.Count}");
for (int i = 0; i < skiaShell.Sections.Count; i++)
{
DiagnosticLog.Debug("LinuxViewRenderer", $"Section {i}: Route='{skiaShell.Sections[i].Route}', Title='{skiaShell.Sections[i].Title}'");
}
return skiaShell;
}
/// <summary>
/// Applies shell colors based on the current theme (dark/light mode).
/// </summary>
private static void ApplyShellColors(SkiaShell skiaShell, Shell shell)
{
bool isDark = Application.Current?.UserAppTheme == AppTheme.Dark;
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Theme is: {(isDark ? "Dark" : "Light")}");
// Flyout background color
if (shell.FlyoutBackgroundColor != null && shell.FlyoutBackgroundColor != Colors.Transparent)
{
skiaShell.FlyoutBackgroundColor = shell.FlyoutBackgroundColor;
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: FlyoutBackgroundColor from MAUI: {skiaShell.FlyoutBackgroundColor}");
}
else
{
skiaShell.FlyoutBackgroundColor = isDark
? Color.FromRgb(30, 30, 30)
: Color.FromRgb(255, 255, 255);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: Using default FlyoutBackgroundColor: {skiaShell.FlyoutBackgroundColor}");
}
// Flyout text color
skiaShell.FlyoutTextColor = isDark
? Color.FromRgb(224, 224, 224)
: Color.FromRgb(33, 33, 33);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: FlyoutTextColor: {skiaShell.FlyoutTextColor}");
// Content background color
skiaShell.ContentBackgroundColor = isDark
? Color.FromRgb(18, 18, 18)
: Color.FromRgb(250, 250, 250);
DiagnosticLog.Debug("LinuxViewRenderer", $"ApplyShellColors: ContentBackgroundColor: {skiaShell.ContentBackgroundColor}");
// NavBar background color
if (shell.BackgroundColor != null && shell.BackgroundColor != Colors.Transparent)
{
skiaShell.NavBarBackgroundColor = shell.BackgroundColor;
}
else
{
skiaShell.NavBarBackgroundColor = Color.FromRgb(33, 150, 243); // Material blue
}
}
/// <summary>
/// Handles MAUI Shell navigation events and updates SkiaShell accordingly.
/// </summary>
private static void OnShellNavigated(object? sender, ShellNavigatedEventArgs e)
{
DiagnosticLog.Debug("LinuxViewRenderer", $"OnShellNavigated called - Source: {e.Source}, Current: {e.Current?.Location}, Previous: {e.Previous?.Location}");
if (CurrentSkiaShell == null || CurrentMauiShell == null)
{
DiagnosticLog.Warn("LinuxViewRenderer", "CurrentSkiaShell or CurrentMauiShell is null");
return;
}
// Get the current route from the Shell
var currentState = CurrentMauiShell.CurrentState;
var location = currentState?.Location?.OriginalString ?? "";
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Location: {location}, Sections: {CurrentSkiaShell.Sections.Count}");
// Find the matching section in SkiaShell by route
for (int i = 0; i < CurrentSkiaShell.Sections.Count; i++)
{
var section = CurrentSkiaShell.Sections[i];
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Checking section {i}: Route='{section.Route}', Title='{section.Title}'");
if (!string.IsNullOrEmpty(section.Route) && location.Contains(section.Route, StringComparison.OrdinalIgnoreCase))
{
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by route! Navigating to section {i}");
if (i != CurrentSkiaShell.CurrentSectionIndex)
{
CurrentSkiaShell.NavigateToSection(i);
}
return;
}
if (!string.IsNullOrEmpty(section.Title) && location.Contains(section.Title, StringComparison.OrdinalIgnoreCase))
{
DiagnosticLog.Debug("LinuxViewRenderer", $"Navigation: Match found by title! Navigating to section {i}");
if (i != CurrentSkiaShell.CurrentSectionIndex)
{
CurrentSkiaShell.NavigateToSection(i);
}
return;
}
}
DiagnosticLog.Warn("LinuxViewRenderer", $"Navigation: No matching section found for location: {location}");
}
/// <summary>
/// Process a ShellItem (FlyoutItem, TabBar, etc.) into SkiaShell sections.
/// </summary>
private void ProcessShellItem(SkiaShell skiaShell, ShellItem item)
{
if (item is FlyoutItem flyoutItem)
{
// Each FlyoutItem becomes a section
var section = new ShellSection
{
Title = flyoutItem.Title ?? "",
Route = flyoutItem.Route ?? flyoutItem.Title ?? ""
};
// Process the items within the FlyoutItem
foreach (var shellSection in flyoutItem.Items)
{
foreach (var content in shellSection.Items)
{
var shellContent = new ShellContent
{
Title = content.Title ?? shellSection.Title ?? flyoutItem.Title ?? "",
Route = content.Route ?? "",
MauiShellContent = content
};
// Create the page content
var pageContent = CreateShellContentPage(content);
if (pageContent != null)
{
shellContent.Content = pageContent;
}
section.Items.Add(shellContent);
}
}
// If there's only one item, use it as the main section content
if (section.Items.Count == 1)
{
section.Title = section.Items[0].Title;
}
skiaShell.AddSection(section);
}
else if (item is TabBar tabBar)
{
// TabBar items get their own sections
foreach (var tab in tabBar.Items)
{
var section = new ShellSection
{
Title = tab.Title ?? "",
Route = tab.Route ?? ""
};
foreach (var content in tab.Items)
{
var shellContent = new ShellContent
{
Title = content.Title ?? tab.Title ?? "",
Route = content.Route ?? "",
MauiShellContent = content
};
var pageContent = CreateShellContentPage(content);
if (pageContent != null)
{
shellContent.Content = pageContent;
}
section.Items.Add(shellContent);
}
skiaShell.AddSection(section);
}
}
else
{
// Generic ShellItem
var section = new ShellSection
{
Title = item.Title ?? "",
Route = item.Route ?? ""
};
foreach (var shellSection in item.Items)
{
foreach (var content in shellSection.Items)
{
var shellContent = new ShellContent
{
Title = content.Title ?? "",
Route = content.Route ?? "",
MauiShellContent = content
};
var pageContent = CreateShellContentPage(content);
if (pageContent != null)
{
shellContent.Content = pageContent;
}
section.Items.Add(shellContent);
}
}
skiaShell.AddSection(section);
}
}
/// <summary>
/// Creates the page content for a ShellContent.
/// </summary>
private SkiaView? CreateShellContentPage(Controls.ShellContent content)
{
try
{
// Try to create the page from the content template
Page? page = null;
if (content.ContentTemplate != null)
{
page = content.ContentTemplate.CreateContent() as Page;
}
if (page == null && content.Content is Page contentPage)
{
page = contentPage;
}
if (page is ContentPage cp && cp.Content != null)
{
// Wrap in a scroll view if not already scrollable
var contentView = RenderView(cp.Content);
if (contentView != null)
{
// Get page background color if set
Color? bgColor = null;
if (cp.BackgroundColor != null && cp.BackgroundColor != Colors.Transparent)
{
bgColor = cp.BackgroundColor;
DiagnosticLog.Debug("LinuxViewRenderer", $"CreateShellContentPage: Page BackgroundColor: {bgColor}");
}
if (contentView is SkiaScrollView scrollView)
{
if (bgColor != null)
{
scrollView.BackgroundColor = bgColor;
}
return scrollView;
}
else
{
var newScrollView = new SkiaScrollView
{
Content = contentView
};
if (bgColor != null)
{
newScrollView.BackgroundColor = bgColor;
}
return newScrollView;
}
}
}
}
catch (Exception)
{
// Silently handle template creation errors
}
return null;
}
/// <summary>
/// Renders a MAUI view and returns the corresponding SkiaView.
/// </summary>
public SkiaView? RenderView(IView view)
{
if (view == null)
return null;
try
{
// Disconnect any existing handler
if (view is Element element && element.Handler != null)
{
element.Handler.DisconnectHandler();
}
// Create handler for the view
// The handler's ConnectHandler and property mappers handle child views automatically
var handler = view.ToHandler(_mauiContext);
if (handler?.PlatformView is not SkiaView skiaView)
{
// If no Skia handler, create a fallback
return CreateFallbackView(view);
}
// Handlers manage their own children via ConnectHandler and property mappers
// No manual child rendering needed here - that caused "View already has a parent" errors
return skiaView;
}
catch (Exception)
{
return CreateFallbackView(view);
}
}
/// <summary>
/// Creates a fallback view for unsupported view types.
/// </summary>
private SkiaView CreateFallbackView(IView view)
{
// For views without handlers, create a placeholder
return new SkiaLabel
{
Text = $"[{view.GetType().Name}]",
TextColor = Colors.Gray,
FontSize = 12
};
}
}