diff --git a/Handlers/MenuBarHandler.cs b/Handlers/MenuBarHandler.cs new file mode 100644 index 0000000..9966ef1 --- /dev/null +++ b/Handlers/MenuBarHandler.cs @@ -0,0 +1,376 @@ +// 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.Controls; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform.Linux.Hosting; +using SkiaSharp; + +namespace Microsoft.Maui.Platform.Linux.Handlers; + +/// +/// Handler for MenuBar on Linux using Skia rendering. +/// Maps MenuBar to SkiaMenuBar platform view. +/// +public partial class MenuBarHandler : ElementHandler +{ + public static IPropertyMapper Mapper = + new PropertyMapper() + { + [nameof(IMenuBar.IsEnabled)] = MapIsEnabled, + }; + + public static CommandMapper CommandMapper = + new() + { + ["Add"] = MapAdd, + ["Remove"] = MapRemove, + ["Clear"] = MapClear, + ["Insert"] = MapInsert, + }; + + public MenuBarHandler() : base(Mapper, CommandMapper) + { + } + + public MenuBarHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaMenuBar CreatePlatformElement() + { + return new SkiaMenuBar(); + } + + protected override void ConnectHandler(SkiaMenuBar platformView) + { + base.ConnectHandler(platformView); + SyncMenuBarItems(); + } + + protected override void DisconnectHandler(SkiaMenuBar platformView) + { + platformView.Items.Clear(); + base.DisconnectHandler(platformView); + } + + private void SyncMenuBarItems() + { + if (PlatformView is null || VirtualView is null) return; + + PlatformView.Items.Clear(); + + foreach (var menuBarItem in VirtualView) + { + if (menuBarItem is MenuBarItem mauiItem) + { + var platformItem = CreatePlatformMenuBarItem(mauiItem); + PlatformView.Items.Add(platformItem); + } + } + + PlatformView.Invalidate(); + } + + private static Platform.MenuBarItem CreatePlatformMenuBarItem(MenuBarItem mauiItem) + { + var platformItem = new Platform.MenuBarItem + { + Text = mauiItem.Text ?? "" + }; + + // MenuBarItem inherits from BaseMenuItem which has a collection + // Use cast to IEnumerable to iterate + if (mauiItem is System.Collections.IEnumerable enumerable) + { + foreach (var child in enumerable) + { + if (child is MenuFlyoutItem flyoutItem) + { + var menuItem = CreatePlatformMenuItem(flyoutItem); + platformItem.Items.Add(menuItem); + } + else if (child is MenuFlyoutSubItem subItem) + { + var menuItem = CreatePlatformMenuItemWithSubs(subItem); + platformItem.Items.Add(menuItem); + } + else if (child is MenuFlyoutSeparator) + { + platformItem.Items.Add(new Platform.MenuItem { IsSeparator = true }); + } + } + } + + return platformItem; + } + + private static Platform.MenuItem CreatePlatformMenuItem(MenuFlyoutItem mauiItem) + { + var menuItem = new Platform.MenuItem + { + Text = mauiItem.Text ?? "", + IsEnabled = mauiItem.IsEnabled, + IconSource = mauiItem.IconImageSource?.ToString() + }; + + // Map keyboard accelerator + if (mauiItem.KeyboardAccelerators.Count > 0) + { + var accel = mauiItem.KeyboardAccelerators[0]; + menuItem.Shortcut = FormatKeyboardAccelerator(accel); + } + + // Connect click event + menuItem.Clicked += (s, e) => + { + if (mauiItem.Command?.CanExecute(mauiItem.CommandParameter) == true) + { + mauiItem.Command.Execute(mauiItem.CommandParameter); + } + (mauiItem as IMenuFlyoutItem)?.Clicked(); + }; + + return menuItem; + } + + private static Platform.MenuItem CreatePlatformMenuItemWithSubs(MenuFlyoutSubItem mauiSubItem) + { + var menuItem = new Platform.MenuItem + { + Text = mauiSubItem.Text ?? "", + IsEnabled = mauiSubItem.IsEnabled, + IconSource = mauiSubItem.IconImageSource?.ToString() + }; + + // MenuFlyoutSubItem is enumerable + if (mauiSubItem is System.Collections.IEnumerable enumerable) + { + foreach (var child in enumerable) + { + if (child is MenuFlyoutItem flyoutItem) + { + menuItem.SubItems.Add(CreatePlatformMenuItem(flyoutItem)); + } + else if (child is MenuFlyoutSubItem nestedSubItem) + { + menuItem.SubItems.Add(CreatePlatformMenuItemWithSubs(nestedSubItem)); + } + else if (child is MenuFlyoutSeparator) + { + menuItem.SubItems.Add(new Platform.MenuItem { IsSeparator = true }); + } + } + } + + return menuItem; + } + + private static string FormatKeyboardAccelerator(KeyboardAccelerator accel) + { + var parts = new List(); + + if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Ctrl)) + parts.Add("Ctrl"); + if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Alt)) + parts.Add("Alt"); + if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Shift)) + parts.Add("Shift"); + + parts.Add(accel.Key ?? ""); + + return string.Join("+", parts); + } + + public static void MapIsEnabled(MenuBarHandler handler, IMenuBar menuBar) + { + if (handler.PlatformView is null) return; + handler.PlatformView.IsEnabled = menuBar.IsEnabled; + } + + public static void MapAdd(MenuBarHandler handler, IMenuBar menuBar, object? args) + { + handler.SyncMenuBarItems(); + } + + public static void MapRemove(MenuBarHandler handler, IMenuBar menuBar, object? args) + { + handler.SyncMenuBarItems(); + } + + public static void MapClear(MenuBarHandler handler, IMenuBar menuBar, object? args) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Items.Clear(); + handler.PlatformView.Invalidate(); + } + + public static void MapInsert(MenuBarHandler handler, IMenuBar menuBar, object? args) + { + handler.SyncMenuBarItems(); + } +} + +/// +/// Handler for MenuFlyout (context menu) on Linux using Skia rendering. +/// Maps IMenuFlyout to SkiaMenuFlyout platform view. +/// +public partial class MenuFlyoutHandler : ElementHandler +{ + public static IPropertyMapper Mapper = + new PropertyMapper() + { + }; + + public static CommandMapper CommandMapper = + new() + { + ["Add"] = MapAdd, + ["Remove"] = MapRemove, + ["Clear"] = MapClear, + }; + + public MenuFlyoutHandler() : base(Mapper, CommandMapper) + { + } + + public MenuFlyoutHandler(IPropertyMapper? mapper, CommandMapper? commandMapper = null) + : base(mapper ?? Mapper, commandMapper ?? CommandMapper) + { + } + + protected override SkiaMenuFlyout CreatePlatformElement() + { + return new SkiaMenuFlyout(); + } + + protected override void ConnectHandler(SkiaMenuFlyout platformView) + { + base.ConnectHandler(platformView); + SyncMenuItems(); + } + + protected override void DisconnectHandler(SkiaMenuFlyout platformView) + { + platformView.Items.Clear(); + base.DisconnectHandler(platformView); + } + + private void SyncMenuItems() + { + if (PlatformView is null || VirtualView is null) return; + + PlatformView.Items.Clear(); + + foreach (var item in VirtualView) + { + if (item is MenuFlyoutItem flyoutItem) + { + PlatformView.Items.Add(CreatePlatformMenuItem(flyoutItem)); + } + else if (item is MenuFlyoutSubItem subItem) + { + PlatformView.Items.Add(CreatePlatformMenuItemWithSubs(subItem)); + } + else if (item is MenuFlyoutSeparator) + { + PlatformView.Items.Add(new Platform.MenuItem { IsSeparator = true }); + } + } + } + + private static Platform.MenuItem CreatePlatformMenuItem(MenuFlyoutItem mauiItem) + { + var menuItem = new Platform.MenuItem + { + Text = mauiItem.Text ?? "", + IsEnabled = mauiItem.IsEnabled, + IconSource = mauiItem.IconImageSource?.ToString() + }; + + // Map keyboard accelerator + if (mauiItem.KeyboardAccelerators.Count > 0) + { + var accel = mauiItem.KeyboardAccelerators[0]; + menuItem.Shortcut = FormatKeyboardAccelerator(accel); + } + + // Connect click event + menuItem.Clicked += (s, e) => + { + if (mauiItem.Command?.CanExecute(mauiItem.CommandParameter) == true) + { + mauiItem.Command.Execute(mauiItem.CommandParameter); + } + (mauiItem as IMenuFlyoutItem)?.Clicked(); + }; + + return menuItem; + } + + private static Platform.MenuItem CreatePlatformMenuItemWithSubs(MenuFlyoutSubItem mauiSubItem) + { + var menuItem = new Platform.MenuItem + { + Text = mauiSubItem.Text ?? "", + IsEnabled = mauiSubItem.IsEnabled, + IconSource = mauiSubItem.IconImageSource?.ToString() + }; + + // MenuFlyoutSubItem is enumerable + if (mauiSubItem is System.Collections.IEnumerable enumerable) + { + foreach (var child in enumerable) + { + if (child is MenuFlyoutItem flyoutItem) + { + menuItem.SubItems.Add(CreatePlatformMenuItem(flyoutItem)); + } + else if (child is MenuFlyoutSubItem nestedSubItem) + { + menuItem.SubItems.Add(CreatePlatformMenuItemWithSubs(nestedSubItem)); + } + else if (child is MenuFlyoutSeparator) + { + menuItem.SubItems.Add(new Platform.MenuItem { IsSeparator = true }); + } + } + } + + return menuItem; + } + + private static string FormatKeyboardAccelerator(KeyboardAccelerator accel) + { + var parts = new List(); + + if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Ctrl)) + parts.Add("Ctrl"); + if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Alt)) + parts.Add("Alt"); + if (accel.Modifiers.HasFlag(KeyboardAcceleratorModifiers.Shift)) + parts.Add("Shift"); + + parts.Add(accel.Key ?? ""); + + return string.Join("+", parts); + } + + public static void MapAdd(MenuFlyoutHandler handler, IMenuFlyout menuFlyout, object? args) + { + handler.SyncMenuItems(); + } + + public static void MapRemove(MenuFlyoutHandler handler, IMenuFlyout menuFlyout, object? args) + { + handler.SyncMenuItems(); + } + + public static void MapClear(MenuFlyoutHandler handler, IMenuFlyout menuFlyout, object? args) + { + if (handler.PlatformView is null) return; + handler.PlatformView.Items.Clear(); + } +}