2025-12-19 09:30:16 +00:00
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Maui.Handlers ;
using Microsoft.Maui.Graphics ;
using Microsoft.Maui.Controls ;
using Microsoft.Maui.Platform ;
Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure:
- Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer
- Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit)
- Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget
- Update LinuxApplication with GTK mode, theme handling, and icon support
- Fix duplicate LinuxDispatcher in LinuxMauiContext
Handlers:
- Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView
- Add FlexLayoutHandler and GestureManager
- Update multiple handlers with ToViewHandler fix and missing mappers
- Add MauiHandlerExtensions with ToViewHandler extension method
Views:
- Add SkiaContextMenu with hover, keyboard, and dark theme support
- Add LinuxDialogService with context menu management
- Add SkiaFlexLayout for flex container support
- Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer
- Update SkiaWebView with SetMainWindow, ProcessGtkEvents
- Update SkiaImage with LoadFromBitmap method
Services:
- Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService
- Add GtkHostService, GtkContextMenuService, MauiIconGenerator
Window:
- Add CursorType enum and GtkHostWindow
- Update X11Window with SetIcon, SetCursor methods
Build: SUCCESS (0 errors)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:19:58 -05:00
using Microsoft.Maui.Platform.Linux.Hosting ;
2025-12-19 09:30:16 +00:00
using SkiaSharp ;
namespace Microsoft.Maui.Platform.Linux.Handlers ;
/// <summary>
/// Handler for CollectionView on Linux using Skia rendering.
/// Maps CollectionView to SkiaCollectionView platform view.
/// </summary>
public partial class CollectionViewHandler : ViewHandler < CollectionView , SkiaCollectionView >
{
2025-12-21 13:26:56 -05:00
private bool _isUpdatingSelection ;
2025-12-19 09:30:16 +00:00
public static IPropertyMapper < CollectionView , CollectionViewHandler > Mapper =
new PropertyMapper < CollectionView , CollectionViewHandler > ( ViewHandler . ViewMapper )
{
// ItemsView properties
[nameof(ItemsView.ItemsSource)] = MapItemsSource ,
[nameof(ItemsView.ItemTemplate)] = MapItemTemplate ,
[nameof(ItemsView.EmptyView)] = MapEmptyView ,
[nameof(ItemsView.HorizontalScrollBarVisibility)] = MapHorizontalScrollBarVisibility ,
[nameof(ItemsView.VerticalScrollBarVisibility)] = MapVerticalScrollBarVisibility ,
// SelectableItemsView properties
[nameof(SelectableItemsView.SelectedItem)] = MapSelectedItem ,
[nameof(SelectableItemsView.SelectedItems)] = MapSelectedItems ,
[nameof(SelectableItemsView.SelectionMode)] = MapSelectionMode ,
// StructuredItemsView properties
[nameof(StructuredItemsView.Header)] = MapHeader ,
[nameof(StructuredItemsView.Footer)] = MapFooter ,
[nameof(StructuredItemsView.ItemsLayout)] = MapItemsLayout ,
[nameof(IView.Background)] = MapBackground ,
2025-12-21 13:26:56 -05:00
[nameof(CollectionView.BackgroundColor)] = MapBackgroundColor ,
2025-12-19 09:30:16 +00:00
} ;
public static CommandMapper < CollectionView , CollectionViewHandler > CommandMapper =
new ( ViewHandler . ViewCommandMapper )
{
["ScrollTo"] = MapScrollTo ,
} ;
public CollectionViewHandler ( ) : base ( Mapper , CommandMapper )
{
}
public CollectionViewHandler ( IPropertyMapper ? mapper , CommandMapper ? commandMapper = null )
: base ( mapper ? ? Mapper , commandMapper ? ? CommandMapper )
{
}
protected override SkiaCollectionView CreatePlatformView ( )
{
return new SkiaCollectionView ( ) ;
}
protected override void ConnectHandler ( SkiaCollectionView platformView )
{
base . ConnectHandler ( platformView ) ;
platformView . SelectionChanged + = OnSelectionChanged ;
platformView . Scrolled + = OnScrolled ;
platformView . ItemTapped + = OnItemTapped ;
}
protected override void DisconnectHandler ( SkiaCollectionView platformView )
{
platformView . SelectionChanged - = OnSelectionChanged ;
platformView . Scrolled - = OnScrolled ;
platformView . ItemTapped - = OnItemTapped ;
base . DisconnectHandler ( platformView ) ;
}
private void OnSelectionChanged ( object? sender , CollectionSelectionChangedEventArgs e )
{
2025-12-21 13:26:56 -05:00
if ( VirtualView is null | | _isUpdatingSelection ) return ;
2025-12-19 09:30:16 +00:00
2025-12-21 13:26:56 -05:00
try
2025-12-19 09:30:16 +00:00
{
2025-12-21 13:26:56 -05:00
_isUpdatingSelection = true ;
// Update virtual view selection
if ( VirtualView . SelectionMode = = SelectionMode . Single )
{
var newItem = e . CurrentSelection . FirstOrDefault ( ) ;
if ( ! Equals ( VirtualView . SelectedItem , newItem ) )
{
VirtualView . SelectedItem = newItem ;
}
}
else if ( VirtualView . SelectionMode = = SelectionMode . Multiple )
2025-12-19 09:30:16 +00:00
{
2025-12-21 13:26:56 -05:00
// Clear and update selected items
VirtualView . SelectedItems . Clear ( ) ;
foreach ( var item in e . CurrentSelection )
{
VirtualView . SelectedItems . Add ( item ) ;
}
2025-12-19 09:30:16 +00:00
}
}
2025-12-21 13:26:56 -05:00
finally
{
_isUpdatingSelection = false ;
}
2025-12-19 09:30:16 +00:00
}
private void OnScrolled ( object? sender , ItemsScrolledEventArgs e )
{
VirtualView ? . SendScrolled ( new ItemsViewScrolledEventArgs
{
VerticalOffset = e . ScrollOffset ,
VerticalDelta = 0 ,
HorizontalOffset = 0 ,
HorizontalDelta = 0
} ) ;
}
private void OnItemTapped ( object? sender , ItemsViewItemTappedEventArgs e )
{
2026-01-01 13:05:16 -05:00
if ( VirtualView is null | | _isUpdatingSelection ) return ;
try
{
_isUpdatingSelection = true ;
Console . WriteLine ( $"[CollectionViewHandler] OnItemTapped index={e.Index}, item={e.Item}, SelectionMode={VirtualView.SelectionMode}" ) ;
// Try to get the item view and process gestures
var skiaView = PlatformView ? . GetItemView ( e . Index ) ;
Console . WriteLine ( $"[CollectionViewHandler] GetItemView({e.Index}) returned: {skiaView?.GetType().Name ?? " null "}, MauiView={skiaView?.MauiView?.GetType().Name ?? " null "}" ) ;
if ( skiaView ? . MauiView ! = null )
{
Console . WriteLine ( $"[CollectionViewHandler] Found MauiView: {skiaView.MauiView.GetType().Name}, GestureRecognizers={skiaView.MauiView.GestureRecognizers?.Count ?? 0}" ) ;
if ( GestureManager . ProcessTap ( skiaView . MauiView , 0 , 0 ) )
{
Console . WriteLine ( "[CollectionViewHandler] Gesture processed successfully" ) ;
return ;
}
}
// Handle selection if gesture wasn't processed
if ( VirtualView . SelectionMode = = SelectionMode . Single )
{
VirtualView . SelectedItem = e . Item ;
}
else if ( VirtualView . SelectionMode = = SelectionMode . Multiple )
{
if ( VirtualView . SelectedItems . Contains ( e . Item ) )
{
VirtualView . SelectedItems . Remove ( e . Item ) ;
}
else
{
VirtualView . SelectedItems . Add ( e . Item ) ;
}
}
}
finally
{
_isUpdatingSelection = false ;
}
2025-12-19 09:30:16 +00:00
}
public static void MapItemsSource ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . ItemsSource = collectionView . ItemsSource ;
}
public static void MapItemTemplate ( CollectionViewHandler handler , CollectionView collectionView )
{
2025-12-21 13:26:56 -05:00
if ( handler . PlatformView is null | | handler . MauiContext is null ) return ;
var template = collectionView . ItemTemplate ;
if ( template ! = null )
{
// Set up a renderer that creates views from the DataTemplate
handler . PlatformView . ItemViewCreator = ( item ) = >
{
try
{
// Create view from template
var content = template . CreateContent ( ) ;
if ( content is View view )
{
// Set binding context FIRST so bindings evaluate
view . BindingContext = item ;
// Force binding evaluation by accessing the visual tree
// This ensures child bindings are evaluated before handler creation
PropagateBindingContext ( view , item ) ;
// Create handler for the view
if ( view . Handler = = null & & handler . MauiContext ! = null )
{
Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure:
- Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer
- Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit)
- Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget
- Update LinuxApplication with GTK mode, theme handling, and icon support
- Fix duplicate LinuxDispatcher in LinuxMauiContext
Handlers:
- Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView
- Add FlexLayoutHandler and GestureManager
- Update multiple handlers with ToViewHandler fix and missing mappers
- Add MauiHandlerExtensions with ToViewHandler extension method
Views:
- Add SkiaContextMenu with hover, keyboard, and dark theme support
- Add LinuxDialogService with context menu management
- Add SkiaFlexLayout for flex container support
- Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer
- Update SkiaWebView with SetMainWindow, ProcessGtkEvents
- Update SkiaImage with LoadFromBitmap method
Services:
- Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService
- Add GtkHostService, GtkContextMenuService, MauiIconGenerator
Window:
- Add CursorType enum and GtkHostWindow
- Update X11Window with SetIcon, SetCursor methods
Build: SUCCESS (0 errors)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:19:58 -05:00
view . Handler = view . ToViewHandler ( handler . MauiContext ) ;
2025-12-21 13:26:56 -05:00
}
if ( view . Handler ? . PlatformView is SkiaView skiaView )
{
2026-01-01 13:05:16 -05:00
// Set MauiView so gestures can be processed
skiaView . MauiView = view ;
Console . WriteLine ( $"[CollectionViewHandler.ItemViewCreator] Set MauiView={view.GetType().Name} on {skiaView.GetType().Name}, GestureRecognizers={view.GestureRecognizers?.Count ?? 0}" ) ;
2025-12-21 13:26:56 -05:00
return skiaView ;
}
}
else if ( content is ViewCell cell )
{
cell . BindingContext = item ;
var cellView = cell . View ;
if ( cellView ! = null )
{
if ( cellView . Handler = = null & & handler . MauiContext ! = null )
{
Major production merge: GTK support, context menus, and dispatcher fixes
Core Infrastructure:
- Add Dispatching folder with LinuxDispatcher, LinuxDispatcherProvider, LinuxDispatcherTimer
- Add Native folder with P/Invoke wrappers (GTK, GLib, GDK, Cairo, WebKit)
- Add GTK host window system with GtkHostWindow and GtkSkiaSurfaceWidget
- Update LinuxApplication with GTK mode, theme handling, and icon support
- Fix duplicate LinuxDispatcher in LinuxMauiContext
Handlers:
- Add GtkWebViewManager and GtkWebViewPlatformView for GTK WebView
- Add FlexLayoutHandler and GestureManager
- Update multiple handlers with ToViewHandler fix and missing mappers
- Add MauiHandlerExtensions with ToViewHandler extension method
Views:
- Add SkiaContextMenu with hover, keyboard, and dark theme support
- Add LinuxDialogService with context menu management
- Add SkiaFlexLayout for flex container support
- Update SkiaShell with RefreshTheme, MauiShell, ContentRenderer
- Update SkiaWebView with SetMainWindow, ProcessGtkEvents
- Update SkiaImage with LoadFromBitmap method
Services:
- Add AppInfoService, ConnectivityService, DeviceDisplayService, DeviceInfoService
- Add GtkHostService, GtkContextMenuService, MauiIconGenerator
Window:
- Add CursorType enum and GtkHostWindow
- Update X11Window with SetIcon, SetCursor methods
Build: SUCCESS (0 errors)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:19:58 -05:00
cellView . Handler = cellView . ToViewHandler ( handler . MauiContext ) ;
2025-12-21 13:26:56 -05:00
}
if ( cellView . Handler ? . PlatformView is SkiaView skiaView )
{
return skiaView ;
}
}
}
}
catch
{
// Ignore template creation errors
}
return null ;
} ;
}
handler . PlatformView . Invalidate ( ) ;
2025-12-19 09:30:16 +00:00
}
public static void MapEmptyView ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . EmptyView = collectionView . EmptyView ;
if ( collectionView . EmptyView is string text )
{
handler . PlatformView . EmptyViewText = text ;
}
}
public static void MapHorizontalScrollBarVisibility ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . HorizontalScrollBarVisibility = ( ScrollBarVisibility ) collectionView . HorizontalScrollBarVisibility ;
}
public static void MapVerticalScrollBarVisibility ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . VerticalScrollBarVisibility = ( ScrollBarVisibility ) collectionView . VerticalScrollBarVisibility ;
}
public static void MapSelectedItem ( CollectionViewHandler handler , CollectionView collectionView )
{
2025-12-21 13:26:56 -05:00
if ( handler . PlatformView is null | | handler . _isUpdatingSelection ) return ;
try
{
handler . _isUpdatingSelection = true ;
if ( ! Equals ( handler . PlatformView . SelectedItem , collectionView . SelectedItem ) )
{
handler . PlatformView . SelectedItem = collectionView . SelectedItem ;
}
}
finally
{
handler . _isUpdatingSelection = false ;
}
2025-12-19 09:30:16 +00:00
}
public static void MapSelectedItems ( CollectionViewHandler handler , CollectionView collectionView )
{
2025-12-21 13:26:56 -05:00
if ( handler . PlatformView is null | | handler . _isUpdatingSelection ) return ;
try
{
handler . _isUpdatingSelection = true ;
2025-12-19 09:30:16 +00:00
2025-12-21 13:26:56 -05:00
// Sync selected items
var selectedItems = collectionView . SelectedItems ;
if ( selectedItems ! = null & & selectedItems . Count > 0 )
{
handler . PlatformView . SelectedItem = selectedItems . First ( ) ;
}
}
finally
2025-12-19 09:30:16 +00:00
{
2025-12-21 13:26:56 -05:00
handler . _isUpdatingSelection = false ;
2025-12-19 09:30:16 +00:00
}
}
public static void MapSelectionMode ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . SelectionMode = collectionView . SelectionMode switch
{
SelectionMode . None = > SkiaSelectionMode . None ,
SelectionMode . Single = > SkiaSelectionMode . Single ,
SelectionMode . Multiple = > SkiaSelectionMode . Multiple ,
_ = > SkiaSelectionMode . None
} ;
}
public static void MapHeader ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . Header = collectionView . Header ;
}
public static void MapFooter ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
handler . PlatformView . Footer = collectionView . Footer ;
}
public static void MapItemsLayout ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
var layout = collectionView . ItemsLayout ;
if ( layout is LinearItemsLayout linearLayout )
{
handler . PlatformView . Orientation = linearLayout . Orientation = = Controls . ItemsLayoutOrientation . Vertical
? Platform . ItemsLayoutOrientation . Vertical
: Platform . ItemsLayoutOrientation . Horizontal ;
handler . PlatformView . SpanCount = 1 ;
handler . PlatformView . ItemSpacing = ( float ) linearLayout . ItemSpacing ;
}
else if ( layout is GridItemsLayout gridLayout )
{
handler . PlatformView . Orientation = gridLayout . Orientation = = Controls . ItemsLayoutOrientation . Vertical
? Platform . ItemsLayoutOrientation . Vertical
: Platform . ItemsLayoutOrientation . Horizontal ;
handler . PlatformView . SpanCount = gridLayout . Span ;
handler . PlatformView . ItemSpacing = ( float ) gridLayout . VerticalItemSpacing ;
}
}
public static void MapBackground ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
2025-12-21 13:26:56 -05:00
// Don't override if BackgroundColor is explicitly set
if ( collectionView . BackgroundColor is not null )
return ;
2025-12-19 09:30:16 +00:00
if ( collectionView . Background is SolidColorBrush solidBrush )
{
handler . PlatformView . BackgroundColor = solidBrush . Color . ToSKColor ( ) ;
}
}
2025-12-21 13:26:56 -05:00
public static void MapBackgroundColor ( CollectionViewHandler handler , CollectionView collectionView )
{
if ( handler . PlatformView is null ) return ;
if ( collectionView . BackgroundColor is not null )
{
handler . PlatformView . BackgroundColor = collectionView . BackgroundColor . ToSKColor ( ) ;
}
}
2025-12-19 09:30:16 +00:00
public static void MapScrollTo ( CollectionViewHandler handler , CollectionView collectionView , object? args )
{
if ( handler . PlatformView is null | | args is not ScrollToRequestEventArgs scrollArgs )
return ;
if ( scrollArgs . Mode = = ScrollToMode . Position )
{
handler . PlatformView . ScrollToIndex ( scrollArgs . Index , scrollArgs . IsAnimated ) ;
}
else if ( scrollArgs . Item ! = null )
{
handler . PlatformView . ScrollToItem ( scrollArgs . Item , scrollArgs . IsAnimated ) ;
}
}
2025-12-21 13:26:56 -05:00
/// <summary>
/// Recursively propagates binding context to all child views to force binding evaluation.
/// </summary>
private static void PropagateBindingContext ( View view , object? bindingContext )
{
view . BindingContext = bindingContext ;
// Propagate to children
if ( view is Layout layout )
{
foreach ( var child in layout . Children )
{
if ( child is View childView )
{
PropagateBindingContext ( childView , bindingContext ) ;
}
}
}
else if ( view is ContentView contentView & & contentView . Content ! = null )
{
PropagateBindingContext ( contentView . Content , bindingContext ) ;
}
else if ( view is Border border & & border . Content is View borderContent )
{
PropagateBindingContext ( borderContent , bindingContext ) ;
}
}
2025-12-19 09:30:16 +00:00
}