New menu item implementation based directly on MenuItem/ToggleMenuFlyoutItem

This commit is contained in:
ClemensFischer 2025-03-21 17:22:07 +01:00
parent 11cd45c099
commit e06dcc5155
12 changed files with 335 additions and 412 deletions

View file

@ -0,0 +1,42 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MapControl.UiTools
{
public class MapMenuItem : MenuItem
{
public MapMenuItem()
{
Icon = new TextBlock
{
FontFamily = new("Segoe MDL2 Assets"),
FontWeight = FontWeight.Black,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
};
}
public string Text
{
get => Header as string;
set => Header = value;
}
protected IEnumerable<MapMenuItem> ParentMenuItems => (Parent as ItemsControl)?.Items.OfType<MapMenuItem>();
protected override Type StyleKeyOverride => typeof(MenuItem);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
base.OnPropertyChanged(args);
if (args.Property == IsCheckedProperty)
{
((TextBlock)Icon).Text = (bool)args.NewValue ? "\uE73E" : ""; // CheckMark
}
}
}
}

View file

@ -1,81 +1,37 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Styling;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MapControl
namespace MapControl.UiTools
{
public class ToggleMenuFlyoutItem : MenuItem
public partial class MenuButton
{
internal static readonly FontFamily SymbolFont = new("Segoe MDL2 Assets");
private readonly TextBlock icon = new()
{
FontFamily = SymbolFont,
FontWeight = FontWeight.Black,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
};
public ToggleMenuFlyoutItem(string text, object item, EventHandler<RoutedEventArgs> click)
{
Icon = icon;
Header = text;
Tag = item;
Click += click;
}
protected override Type StyleKeyOverride => typeof(MenuItem);
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
base.OnPropertyChanged(args);
if (args.Property == IsCheckedProperty)
{
icon.Text = (bool)args.NewValue ? "\uE73E" : ""; // CheckMark
}
}
}
public class MenuButton : Button
{
protected MenuButton(string icon)
public MenuButton()
{
var style = new Style();
style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, ToggleMenuFlyoutItem.SymbolFont));
style.Setters.Add(new Setter(TextBlock.FontFamilyProperty, new FontFamily("Segoe MDL2 Assets")));
style.Setters.Add(new Setter(TextBlock.FontSizeProperty, 20d));
style.Setters.Add(new Setter(PaddingProperty, new Thickness(8)));
Styles.Add(style);
Content = icon;
Flyout = new MenuFlyout();
Loaded += async (s, e) => await Initialize();
}
public string Icon
{
get => Content as string;
set => Content = value;
}
public MenuFlyout Menu => (MenuFlyout)Flyout;
[Content]
public ItemCollection Items => Menu.Items;
protected override Type StyleKeyOverride => typeof(Button);
protected MenuFlyout CreateMenu()
{
var menu = new MenuFlyout();
Flyout = menu;
return menu;
}
protected IEnumerable<ToggleMenuFlyoutItem> GetMenuItems()
{
return ((MenuFlyout)Flyout).Items.OfType<ToggleMenuFlyoutItem>();
}
protected static MenuItem CreateMenuItem(string text, object item, EventHandler<RoutedEventArgs> click)
{
return new ToggleMenuFlyoutItem(text, item, click);
}
protected static Separator CreateSeparator()
{
return new Separator();
}
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Linq;
using System.Threading.Tasks;
#if WPF
using System.Windows;
using System.Windows.Markup;
#elif UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Markup;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
#else
using Avalonia.Metadata;
using FrameworkElement = Avalonia.Controls.Control;
#endif
namespace MapControl.UiTools
{
#if WPF
[ContentProperty(nameof(MapLayer))]
#elif UWP || WINUI
[ContentProperty(Name = nameof(MapLayer))]
#endif
public class MapLayerMenuItem : MapMenuItem
{
#if AVALONIA
[Content]
#endif
public FrameworkElement MapLayer { get; set; }
public Func<Task<FrameworkElement>> MapLayerFactory { get; set; }
public MapLayerMenuItem()
{
Click += async (s, e) =>
{
if (DataContext is MapBase map)
{
await Execute(map);
foreach (var item in ParentMenuItems.OfType<MapLayerMenuItem>())
{
item.IsChecked = map.Children.Contains(item.MapLayer);
}
}
};
}
public virtual async Task Execute(MapBase map)
{
map.MapLayer = MapLayer ?? (MapLayer = await MapLayerFactory.Invoke());
IsChecked = true;
}
}
public class MapOverlayMenuItem : MapLayerMenuItem
{
public override async Task Execute(MapBase map)
{
var layer = MapLayer ?? (MapLayer = await MapLayerFactory.Invoke());
if (map.Children.Contains(layer))
{
map.Children.Remove(layer);
}
else
{
var index = 1;
foreach (var itemLayer in ParentMenuItems?
.OfType<MapOverlayMenuItem>()
.Select(item => item.MapLayer)
.Where(itemLayer => itemLayer != null))
{
if (itemLayer == layer)
{
map.Children.Insert(index, itemLayer);
break;
}
if (map.Children.Contains(itemLayer))
{
index++;
}
}
}
}
}
}

View file

@ -1,172 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
#if WPF
using System.Windows;
using System.Windows.Markup;
#elif UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Markup;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
#elif AVALONIA
using Avalonia.Interactivity;
using Avalonia.Metadata;
using DependencyProperty = Avalonia.AvaloniaProperty;
using FrameworkElement = Avalonia.Controls.Control;
#endif
namespace MapControl.UiTools
{
#if WPF
[ContentProperty(nameof(Layer))]
#elif UWP || WINUI
[ContentProperty(Name = nameof(Layer))]
#endif
public class MapLayerItem
{
#if AVALONIA
[Content]
#endif
public FrameworkElement Layer { get; set; }
public string Text { get; set; }
public Func<Task<FrameworkElement>> LayerFactory { get; set; }
public async Task<FrameworkElement> GetLayer() => Layer ?? (Layer = await LayerFactory?.Invoke());
}
#if WPF
[ContentProperty(nameof(MapLayers))]
#elif UWP || WINUI
[ContentProperty(Name = nameof(MapLayers))]
#endif
public class MapLayersMenuButton : MenuButton
{
private FrameworkElement selectedLayer;
public MapLayersMenuButton()
: base("\uE81E")
{
((INotifyCollectionChanged)MapLayers).CollectionChanged += async (s, e) => await InitializeMenu();
((INotifyCollectionChanged)MapOverlays).CollectionChanged += async (s, e) => await InitializeMenu();
}
public static readonly DependencyProperty MapProperty =
DependencyPropertyHelper.Register<MapLayersMenuButton, MapBase>(nameof(Map), null,
async (button, oldValue, newValue) => await button.InitializeMenu());
public MapBase Map
{
get => (MapBase)GetValue(MapProperty);
set => SetValue(MapProperty, value);
}
#if AVALONIA
[Content]
#endif
public Collection<MapLayerItem> MapLayers { get; } = new ObservableCollection<MapLayerItem>();
public Collection<MapLayerItem> MapOverlays { get; } = new ObservableCollection<MapLayerItem>();
private async Task InitializeMenu()
{
if (Map != null)
{
var menu = CreateMenu();
foreach (var item in MapLayers)
{
menu.Items.Add(CreateMenuItem(item.Text, item, MapLayerClicked));
}
var initialLayer = MapLayers.Select(l => l.GetLayer()).FirstOrDefault();
if (MapOverlays.Count > 0)
{
if (initialLayer != null)
{
menu.Items.Add(CreateSeparator());
}
foreach (var item in MapOverlays)
{
menu.Items.Add(CreateMenuItem(item.Text, item, MapOverlayClicked));
}
}
if (initialLayer != null)
{
SetMapLayer(await initialLayer);
}
}
}
private async void MapLayerClicked(object sender, RoutedEventArgs e)
{
var item = (FrameworkElement)sender;
var mapLayerItem = (MapLayerItem)item.Tag;
SetMapLayer(await mapLayerItem.GetLayer());
}
private async void MapOverlayClicked(object sender, RoutedEventArgs e)
{
var item = (FrameworkElement)sender;
var mapLayerItem = (MapLayerItem)item.Tag;
ToggleMapOverlay(await mapLayerItem.GetLayer());
}
private void SetMapLayer(FrameworkElement layer)
{
if (selectedLayer != layer)
{
selectedLayer = layer;
Map.MapLayer = selectedLayer;
}
UpdateCheckedStates();
}
private void ToggleMapOverlay(FrameworkElement layer)
{
if (Map.Children.Contains(layer))
{
Map.Children.Remove(layer);
}
else
{
int index = 1;
foreach (var overlay in MapOverlays.Select(o => o.Layer).Where(o => o != null))
{
if (overlay == layer)
{
Map.Children.Insert(index, layer);
break;
}
if (Map.Children.Contains(overlay))
{
index++;
}
}
}
UpdateCheckedStates();
}
private void UpdateCheckedStates()
{
foreach (var item in GetMenuItems())
{
item.IsChecked = Map.Children.Contains(((MapLayerItem)item.Tag).Layer);
}
}
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Diagnostics;
using System.Linq;
#if WPF
using System.Windows.Markup;
#elif UWP
using Windows.UI.Xaml.Markup;
#elif WINUI
using Microsoft.UI.Xaml.Markup;
#else
using Avalonia.Metadata;
#endif
namespace MapControl.UiTools
{
#if WPF
[ContentProperty(nameof(MapProjection))]
#elif UWP || WINUI
[ContentProperty(Name = nameof(MapProjection))]
#endif
public class MapProjectionMenuItem : MapMenuItem
{
#if AVALONIA
[Content]
#endif
public string MapProjection { get; set; }
public MapProjectionMenuItem()
{
Click += (s, e) =>
{
if (DataContext is MapBase map)
{
Execute(map);
foreach (var item in ParentMenuItems.OfType<MapProjectionMenuItem>())
{
item.IsChecked = map.MapProjection.CrsId == item.MapProjection;
}
}
};
}
public void Execute(MapBase map)
{
bool success = true;
if (map.MapProjection.CrsId != MapProjection)
{
try
{
map.MapProjection = MapProjectionFactory.Instance.GetProjection(MapProjection);
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(MapProjectionFactory)}: {ex.Message}");
success = false;
}
}
IsChecked = success;
}
}
}

View file

@ -1,123 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
#if WPF
using System.Windows;
using System.Windows.Markup;
#elif UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Markup;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Markup;
#elif AVALONIA
using Avalonia.Interactivity;
using Avalonia.Metadata;
using DependencyProperty = Avalonia.AvaloniaProperty;
using FrameworkElement = Avalonia.Controls.Control;
#endif
namespace MapControl.UiTools
{
#if WPF
[ContentProperty(nameof(Projection))]
#elif UWP || WINUI
[ContentProperty(Name = nameof(Projection))]
#endif
public class MapProjectionItem
{
#if AVALONIA
[Content]
#endif
public string Projection { get; set; }
public string Text { get; set; }
}
#if WPF
[ContentProperty(nameof(MapProjections))]
#elif UWP || WINUI
[ContentProperty(Name = nameof(MapProjections))]
#endif
public class MapProjectionsMenuButton : MenuButton
{
private string selectedProjection;
public MapProjectionsMenuButton()
: base("\uE809")
{
((INotifyCollectionChanged)MapProjections).CollectionChanged += (s, e) => InitializeMenu();
}
public static readonly DependencyProperty MapProperty =
DependencyPropertyHelper.Register<MapProjectionsMenuButton, MapBase>(nameof(Map), null,
(button, oldValue, newValue) => button.InitializeMenu());
public MapBase Map
{
get => (MapBase)GetValue(MapProperty);
set => SetValue(MapProperty, value);
}
#if AVALONIA
[Content]
#endif
public Collection<MapProjectionItem> MapProjections { get; } = new ObservableCollection<MapProjectionItem>();
private void InitializeMenu()
{
if (Map != null)
{
var menu = CreateMenu();
foreach (var item in MapProjections)
{
menu.Items.Add(CreateMenuItem(item.Text, item.Projection, MapProjectionClicked));
}
var initialProjection = MapProjections.Select(p => p.Projection).FirstOrDefault();
if (initialProjection != null)
{
SetMapProjection(initialProjection);
}
}
}
private void MapProjectionClicked(object sender, RoutedEventArgs e)
{
var item = (FrameworkElement)sender;
var projection = (string)item.Tag;
SetMapProjection(projection);
}
private void SetMapProjection(string projection)
{
if (selectedProjection != projection)
{
try
{
Map.MapProjection = MapProjectionFactory.Instance.GetProjection(projection);
selectedProjection = projection;
}
catch (Exception ex)
{
Debug.WriteLine($"{nameof(MapProjectionFactory)}: {ex.Message}");
}
}
UpdateCheckedStates();
}
private void UpdateCheckedStates()
{
foreach (var item in GetMenuItems())
{
item.IsChecked = selectedProjection == (string)item.Tag;
}
}
}
}

View file

@ -0,0 +1,50 @@
using System.Threading.Tasks;
#if WPF
using System.Windows;
using System.Windows.Controls;
#elif UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
#else
using Avalonia.Controls;
using DependencyProperty = Avalonia.AvaloniaProperty;
#endif
namespace MapControl.UiTools
{
public partial class MenuButton : Button
{
public static readonly DependencyProperty MapProperty =
DependencyPropertyHelper.Register<MenuButton, MapBase>(nameof(Map), null,
async (button, oldValue, newValue) => await button.Initialize());
public MapBase Map
{
get => (MapBase)GetValue(MapProperty);
set => SetValue(MapProperty, value);
}
private async Task Initialize()
{
if (Map != null)
{
DataContext = Map;
if (Items.Count > 0)
{
if (Items[0] is MapLayerMenuItem mapLayerItem)
{
await mapLayerItem.Execute(Map);
}
else if (Items[0] is MapProjectionMenuItem mapProjectionItem)
{
mapProjectionItem.Execute(Map);
}
}
}
}
}
}

View file

@ -40,11 +40,17 @@
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Shared\MapLayersMenuButton.cs">
<Link>MapLayersMenuButton.cs</Link>
<Compile Include="..\Shared\MapLayerMenuItem.cs">
<Link>MapLayerMenuItem.cs</Link>
</Compile>
<Compile Include="..\Shared\MapProjectionsMenuButton.cs">
<Link>MapProjectionsMenuButton.cs</Link>
<Compile Include="..\Shared\MapProjectionMenuItem.cs">
<Link>MapProjectionMenuItem.cs</Link>
</Compile>
<Compile Include="..\Shared\MenuButton.cs">
<Link>MenuButton.cs</Link>
</Compile>
<Compile Include="..\WinUI\MapMenuItem.WinUI.cs">
<Link>MapMenuItem.WinUI.cs</Link>
</Compile>
<Compile Include="..\WinUI\MenuButton.WinUI.cs">
<Link>MenuButton.WinUI.cs</Link>

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
namespace MapControl.UiTools
{
public class MapMenuItem : MenuItem
{
public string Text
{
get => Header as string;
set => Header = value;
}
protected IEnumerable<MapMenuItem> ParentMenuItems
=> (Parent as ItemsControl)?.Items.OfType<MapMenuItem>();
}
}

View file

@ -1,46 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace MapControl.UiTools
{
public class MenuButton : Button
[ContentProperty(nameof(Items))]
public partial class MenuButton
{
static MenuButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MenuButton), new FrameworkPropertyMetadata(typeof(MenuButton)));
}
protected MenuButton(string icon)
public MenuButton()
{
Content = icon;
ContextMenu = new ContextMenu();
DataContextChanged += (s, e) => ContextMenu.DataContext = e.NewValue;
Loaded += async (s, e) => await Initialize();
Click += (s, e) => ContextMenu.IsOpen = true;
}
protected ContextMenu CreateMenu()
public string Icon
{
var menu = new ContextMenu();
ContextMenu = menu;
return menu;
get => Content as string;
set => Content = value;
}
protected IEnumerable<MenuItem> GetMenuItems()
{
return ContextMenu.Items.OfType<MenuItem>();
}
public ContextMenu Menu => ContextMenu;
protected static MenuItem CreateMenuItem(string text, object item, RoutedEventHandler click)
{
var menuItem = new MenuItem { Header = text, Tag = item };
menuItem.Click += click;
return menuItem;
}
protected static Separator CreateSeparator()
{
return new Separator();
}
public ItemCollection Items => ContextMenu.Items;
}
}

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
#if UWP
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
#else
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
#endif
namespace MapControl.UiTools
{
public class MapMenuItem : ToggleMenuFlyoutItem
{
protected IEnumerable<MapMenuItem> ParentMenuItems
=> (VisualTreeHelper.GetParent(this) as Panel)?.Children.OfType<MapMenuItem>();
}
}

View file

@ -1,44 +1,31 @@
using System.Collections.Generic;
using System.Linq;
#if UWP
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Markup;
#elif WINUI
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
#endif
namespace MapControl.UiTools
{
public class MenuButton : Button
[ContentProperty(Name = nameof(Items))]
public partial class MenuButton
{
protected MenuButton(string icon)
public MenuButton()
{
Content = new FontIcon { Glyph = icon };
Flyout = new MenuFlyout();
Loaded += async (s, e) => await Initialize();
}
protected MenuFlyout CreateMenu()
public string Icon
{
var menu = new MenuFlyout();
Flyout = menu;
return menu;
get => (Content as FontIcon)?.Glyph;
set => Content = new FontIcon { Glyph = value };
}
protected IEnumerable<ToggleMenuFlyoutItem> GetMenuItems()
{
return ((MenuFlyout)Flyout).Items.OfType<ToggleMenuFlyoutItem>();
}
public MenuFlyout Menu => (MenuFlyout)Flyout;
protected static ToggleMenuFlyoutItem CreateMenuItem(string text, object item, RoutedEventHandler click)
{
var menuItem = new ToggleMenuFlyoutItem { Text = text, Tag = item };
menuItem.Click += click;
return menuItem;
}
protected static MenuFlyoutSeparator CreateSeparator()
{
return new MenuFlyoutSeparator();
}
public IList<MenuFlyoutItemBase> Items => Menu.Items;
}
}