From 95588e43b3b05fcf4c0204cc04a66ef55e0d2fea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:43:03 +0000 Subject: [PATCH 1/4] Initial plan From b4bea4799c270c797498ee6e17cc23a5b832a4f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:58:23 +0000 Subject: [PATCH 2/4] feat: add PreviewView support to ContextMenu for LongPressed mode - Add PreviewView bindable property to ContextMenu - iOS: create ContextMenuPreviewViewController to wrap MAUI view as UIViewController preview - iOS: use previewProvider in UIContextMenuConfiguration when PreviewView is set - Android: show PreviewView in a PopupWindow above anchor on long press, dismiss with menu - Add sample demonstrating PreviewView with a long document title use case - Update CHANGELOG.md (minor bump to 55.3.0) Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- CHANGELOG.md | 3 ++ .../ContextMenus/ContextMenuSamples.xaml | 28 ++++++++++ .../LocalizedStrings.Designer.cs | 6 +++ .../LocalizedStrings/LocalizedStrings.en.resx | 3 ++ .../LocalizedStrings/LocalizedStrings.resx | 3 ++ .../Android/ContextMenuPlatformEffect.cs | 52 ++++++++++++++++++- .../ContextMenus/ContextMenu.Properties.cs | 19 +++++++ .../ContextMenuPlatformEffect.LongPressed.cs | 7 ++- .../iOS/ContextMenuPreviewViewController.cs | 38 ++++++++++++++ 9 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 566fc8355..3b6d125ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [55.3.0] +- [ContextMenu] Added `PreviewView` property to `ContextMenu` for use with `LongPressed` mode. On iOS, the view is shown as the native `UIContextMenuInteraction` preview. On Android, the view is shown in a popup above the context menu. + ## [55.2.2] - [iOS26][Tip] Added more padding. diff --git a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml index a817683ae..72a2b72cd 100644 --- a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml +++ b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml @@ -209,6 +209,34 @@ + + + + + + + + + + + + + + + + diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs index c2b76608b..7eba890a4 100644 --- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs +++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.Designer.cs @@ -615,6 +615,12 @@ internal static string Context_Menu_LongPress_OneItemOneGroup { } } + internal static string Context_Menu_LongPressWithPreview { + get { + return ResourceManager.GetString("Context_Menu_LongPressWithPreview", resourceCulture); + } + } + internal static string DefaultStateViews { get { return ResourceManager.GetString("DefaultStateViews", resourceCulture); diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx index bbcbaa413..189d9a506 100644 --- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx +++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.en.resx @@ -303,6 +303,9 @@ Single item and single group + + Long press with preview + Default state views diff --git a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx index 0f9b07409..503ed5c3f 100644 --- a/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx +++ b/src/app/Components/Resources/LocalizedStrings/LocalizedStrings.resx @@ -308,6 +308,9 @@ Én handling og én gruppe + + Trykk og hold med forhåndsvisning + Standard tilstand views diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs index 79b1bfe02..9aaf837ef 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs @@ -1,6 +1,10 @@ +using Android.Graphics.Drawables; using Android.Views; +using Android.Widget; using AndroidX.AppCompat.Widget; +using DIPS.Mobile.UI.API.Library; using DIPS.Mobile.UI.Components.ContextMenus.Android; +using Microsoft.Maui.Platform; using Object = Java.Lang.Object; using PopupMenu = Android.Widget.PopupMenu; using View = Android.Views.View; @@ -45,13 +49,14 @@ protected override partial void OnAttached() } } - public class ContextMenuHandler : Object, PopupMenu.IOnMenuItemClickListener + public class ContextMenuHandler : Object, PopupMenu.IOnMenuItemClickListener, PopupMenu.IOnDismissListener { private readonly ContextMenu m_contextMenu; private readonly View m_control; private Dictionary m_menuItems; private PopupMenu m_popupMenu; + private PopupWindow? m_previewPopupWindow; public ContextMenuHandler(ContextMenu contextMenu, View view) { @@ -81,14 +86,57 @@ public void OpenContextMenu(object? sender, EventArgs e) })); SetListeners(); - + + if (m_contextMenu.PreviewView != null) + { + ShowPreview(); + } + m_popupMenu.Show(); } + private void ShowPreview() + { + var activity = Platform.CurrentActivity; + var mauiContext = DUI.GetCurrentMauiContext; + if (activity is null || mauiContext is null || m_contextMenu.PreviewView is null) + return; + + var previewNativeView = m_contextMenu.PreviewView.ToPlatform(mauiContext); + + previewNativeView.Measure( + View.MeasureSpec.MakeMeasureSpec(m_control.Width, MeasureSpecMode.AtMost), + View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified)); + + m_previewPopupWindow = new PopupWindow( + previewNativeView, + m_control.Width, + ViewGroup.LayoutParams.WrapContent, + false); + + m_previewPopupWindow.SetBackgroundDrawable(new ColorDrawable(Android.Graphics.Color.Transparent)); + m_previewPopupWindow.Elevation = (float)Sizes.GetSize(SizeName.size_1); + + var previewHeight = previewNativeView.MeasuredHeight; + m_previewPopupWindow.ShowAsDropDown(m_control, 0, -(m_control.Height + previewHeight)); + } + + private void DismissPreview() + { + m_previewPopupWindow?.Dismiss(); + m_previewPopupWindow = null; + } + + public void OnDismiss(PopupMenu? menu) + { + DismissPreview(); + } + private void SetListeners() { m_popupMenu.SetOnMenuItemClickListener(this); + m_popupMenu.SetOnDismissListener(this); } public bool OnMenuItemClick(IMenuItem? theTappedNativeItem) diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs index 83be24bd5..2970b0e03 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/ContextMenu.Properties.cs @@ -93,6 +93,25 @@ public ContextMenuHorizontalOptions ContextMenuHorizontalOptions set => SetValue(ContextMenuHorizontalOptionsProperty, value); } + /// + /// + /// + public static readonly BindableProperty PreviewViewProperty = BindableProperty.Create( + nameof(PreviewView), + typeof(View), + typeof(ContextMenu)); + + /// + /// An optional view to display as a preview when the context menu is opened in mode. + /// On iOS, the view is shown as the native UIContextMenuInteraction preview. + /// On Android, the view is shown in a popup above the context menu. + /// + public View? PreviewView + { + get => (View?)GetValue(PreviewViewProperty); + set => SetValue(PreviewViewProperty, value); + } + /// /// Get the mode of the context menu. /// diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs index 862c5f2d0..eb39939ad 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPlatformEffect.LongPressed.cs @@ -47,7 +47,12 @@ public override void WillEnd(UIContextMenuInteraction interaction, UIContextMenu contextMenu); var menu = UIMenu.Create(contextMenu.Title, dict.Select(k => k.Value).ToArray()); - return UIContextMenuConfiguration.Create(null, null, actions => menu); + var previewView = contextMenu.PreviewView; + Func? previewProvider = previewView != null + ? () => new ContextMenuPreviewViewController(previewView) + : null; + + return UIContextMenuConfiguration.Create(null, previewProvider, actions => menu); } public Element? Element { get; set; } diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs new file mode 100644 index 000000000..07b630b6f --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/iOS/ContextMenuPreviewViewController.cs @@ -0,0 +1,38 @@ +using CoreGraphics; +using DIPS.Mobile.UI.API.Library; +using Microsoft.Maui.Platform; +using UIKit; + +// ReSharper disable once CheckNamespace +namespace DIPS.Mobile.UI.Components.ContextMenus.iOS; + +internal class ContextMenuPreviewViewController : UIViewController +{ + private readonly View m_previewView; + + internal ContextMenuPreviewViewController(View previewView) + { + m_previewView = previewView; + } + + public override void ViewDidLoad() + { + base.ViewDidLoad(); + + var mauiContext = DUI.GetCurrentMauiContext; + if (mauiContext is null || View is null) + return; + + var nativeView = m_previewView.ToPlatform(mauiContext); + View.AddSubview(nativeView); + + var maxWidth = UIScreen.MainScreen.Bounds.Width - Sizes.GetSize(SizeName.content_margin_large) * 2; + var measurement = m_previewView.Measure(maxWidth, double.PositiveInfinity); + + var width = Math.Min(measurement.Width, maxWidth); + var height = measurement.Height; + + PreferredContentSize = new CGSize(width, height); + nativeView.Frame = new CGRect(0, 0, width, height); + } +} From 42c36c6f73378b96168de63a6bcdf12e2a8a3ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Mo=C3=A5s?= Date: Tue, 3 Mar 2026 09:10:57 +0100 Subject: [PATCH 3/4] fixed compilation --- .../ContextMenus/ContextMenuSamples.xaml | 19 ++++++++++--------- .../Android/ContextMenuPlatformEffect.cs | 2 +- .../ContextMenuPlatformEffect.LongPressed.cs | 6 ++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml index 72a2b72cd..e20ec67d6 100644 --- a/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml +++ b/src/app/Components/ComponentsSamples/ContextMenus/ContextMenuSamples.xaml @@ -216,15 +216,16 @@ - - - - + + + + + k.Value).ToArray()); var previewView = contextMenu.PreviewView; - Func? previewProvider = previewView != null - ? () => new ContextMenuPreviewViewController(previewView) - : null; - return UIContextMenuConfiguration.Create(null, previewProvider, actions => menu); + return UIContextMenuConfiguration.Create(null, () => previewView != null ? new ContextMenuPreviewViewController(previewView) + : null, actions => menu); } public Element? Element { get; set; } From 1f1f110c3a36dc4805ece99a62d6caa4c82f3eb3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:19:56 +0000 Subject: [PATCH 4/4] feat(android): add dim background and shadow to ContextMenu preview popup - Wrap preview in FrameLayout with MaterialShapeDrawable (rounded corners, color_surface_default fill) and Elevation for shadow effect - Apply FLAG_DIM_BEHIND with 0.5 dimAmount on the PopupWindow after showing so the background dims behind the preview popup Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com> --- .../Android/ContextMenuPlatformEffect.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs index 38904b8d5..ee6722123 100644 --- a/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs +++ b/src/library/DIPS.Mobile.UI/Components/ContextMenus/Android/ContextMenuPlatformEffect.cs @@ -4,6 +4,9 @@ using AndroidX.AppCompat.Widget; using DIPS.Mobile.UI.API.Library; using DIPS.Mobile.UI.Components.ContextMenus.Android; +using DIPS.Mobile.UI.Effects.Layout; +using DIPS.Mobile.UI.Extensions.Android; +using Google.Android.Material.Shape; using Microsoft.Maui.Platform; using Object = Java.Lang.Object; using PopupMenu = Android.Widget.PopupMenu; @@ -105,21 +108,42 @@ private void ShowPreview() var previewNativeView = m_contextMenu.PreviewView.ToPlatform(mauiContext); - previewNativeView.Measure( + // Wrap in a container with rounded corners and shadow elevation + var container = new FrameLayout(activity); + var background = MaterialShapeDrawableHelper.CreateDrawable(new CornerRadius( + Sizes.GetSize(SizeName.size_2))); + background.FillColor = Resources.Colors.Colors.GetColor(ColorName.color_surface_default) + .ToDefaultColorStateList(); + container.Background = background; + container.ClipToOutline = true; + container.OutlineProvider = ViewOutlineProvider.Background; + container.Elevation = Sizes.GetSize(SizeName.size_3).ToMauiPixel(); + container.AddView(previewNativeView); + + container.Measure( View.MeasureSpec.MakeMeasureSpec(m_control.Width, MeasureSpecMode.AtMost), View.MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified)); m_previewPopupWindow = new PopupWindow( - previewNativeView, + container, m_control.Width, ViewGroup.LayoutParams.WrapContent, false); m_previewPopupWindow.SetBackgroundDrawable(new ColorDrawable(global::Android.Graphics.Color.Transparent)); - m_previewPopupWindow.Elevation = (float)Sizes.GetSize(SizeName.size_1); - var previewHeight = previewNativeView.MeasuredHeight; + var previewHeight = container.MeasuredHeight; m_previewPopupWindow.ShowAsDropDown(m_control, 0, -(m_control.Height + previewHeight)); + + // Apply dim behind the preview popup + var rootView = m_previewPopupWindow.ContentView?.RootView; + if (rootView?.LayoutParameters is WindowManagerLayoutParams layoutParams + && activity.GetSystemService(global::Android.Content.Context.WindowService) is IWindowManager windowManager) + { + layoutParams.Flags |= WindowManagerFlags.DimBehind; + layoutParams.DimAmount = 0.5f; + windowManager.UpdateViewLayout(rootView, layoutParams); + } } private void DismissPreview()