diff --git a/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml b/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml index a9177d725..c0352ef92 100644 --- a/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml +++ b/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml @@ -18,10 +18,12 @@ GeoViewTapped="mapView_GeoViewTapped"/> + VerticalAlignment="Stretch" + PopupAttachmentClicked="popupViewer_PopupAttachmentClicked" + HyperlinkClicked="popupViewer_LinkClicked" /> diff --git a/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml.cs b/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml.cs index 8aab2bfdf..58bfa3d0b 100644 --- a/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml.cs +++ b/src/Samples/Toolkit.SampleApp.UWP/Samples/PopupViewer/PopupViewerSample.xaml.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Windows.UI.Popups; // The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 @@ -20,14 +21,10 @@ public sealed partial class PopupViewerSample : Page public PopupViewerSample() { this.InitializeComponent(); - Map.Loaded += (s, e) => - { - Map.OperationalLayers.RemoveAt(0); // Remove secured layer - }; } // Webmap configured with Popup - public Map Map { get; } = new Map(new Uri("https://www.arcgis.com/home/item.html?id=d4fe39d300c24672b1821fa8450b6ae2")); + public Map Map { get; } = new Map(new Uri("https://www.arcgis.com/home/item.html?id=9f3a674e998f461580006e626611f9ad")); private async void mapView_GeoViewTapped(object sender, GeoViewInputEventArgs e) { @@ -43,11 +40,11 @@ private async void mapView_GeoViewTapped(object sender, GeoViewInputEventArgs e) if (popup != null) { popupViewer.Visibility = Visibility.Visible; - popupViewer.PopupManager = new PopupManager(popup); + popupViewer.Popup = popup; } else { - popupViewer.PopupManager = null; + popupViewer.Popup = null; popupViewer.Visibility = Visibility.Collapsed; } } @@ -116,5 +113,45 @@ private Mapping.Popups.Popup GetPopup(IEnumerable results) return null; } + private async void popupViewer_PopupAttachmentClicked(object sender, UI.Controls.PopupAttachmentClickedEventArgs e) + { + // Override the default attachment click behavior (which will download and save attachment) + if (!e.Attachment.IsLocal) // Attachment hasn't been downloaded + { + try + { + // Make first click just load the attachment (or cancel a loading operation). Otherwise fallback to default behavior + if (e.Attachment.LoadStatus == LoadStatus.NotLoaded) + { + e.Handled = true; + await e.Attachment.LoadAsync(); + } + else if (e.Attachment.LoadStatus == LoadStatus.FailedToLoad) + { + e.Handled = true; + await e.Attachment.RetryLoadAsync(); + } + else if (e.Attachment.LoadStatus == LoadStatus.Loading) + { + e.Handled = true; + e.Attachment.CancelLoad(); + } + } + catch (OperationCanceledException) { } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine("Failed to download attachment", ex.Message); + } + } + } + + private void popupViewer_LinkClicked(object sender, UI.Controls.HyperlinkClickedEventArgs e) + { + // Include below line if you want to prevent the default action + // e.Handled = true; + + // Perform custom action when a link is clicked + System.Diagnostics.Debug.WriteLine(e.Uri); + } } } \ No newline at end of file diff --git a/src/Toolkit/Toolkit.WPF/UI/Controls/PopupViewer/PopupViewer.Theme.xaml b/src/Toolkit/Toolkit.WPF/UI/Controls/PopupViewer/PopupViewer.Theme.xaml index 598ac999e..d8c0566cc 100644 --- a/src/Toolkit/Toolkit.WPF/UI/Controls/PopupViewer/PopupViewer.Theme.xaml +++ b/src/Toolkit/Toolkit.WPF/UI/Controls/PopupViewer/PopupViewer.Theme.xaml @@ -52,62 +52,81 @@ + + + - - - - - + diff --git a/src/Toolkit/Toolkit.WinUI/Esri.ArcGISRuntime.Toolkit.WinUI.csproj b/src/Toolkit/Toolkit.WinUI/Esri.ArcGISRuntime.Toolkit.WinUI.csproj index aed9ead7c..c53aac9cc 100644 --- a/src/Toolkit/Toolkit.WinUI/Esri.ArcGISRuntime.Toolkit.WinUI.csproj +++ b/src/Toolkit/Toolkit.WinUI/Esri.ArcGISRuntime.Toolkit.WinUI.csproj @@ -44,7 +44,7 @@ - + diff --git a/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Theme.xaml b/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Theme.xaml index ff840978d..d8af5c136 100644 --- a/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Theme.xaml +++ b/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Theme.xaml @@ -1,69 +1,213 @@  - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Windows.cs b/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Windows.cs deleted file mode 100644 index 7d6b15248..000000000 --- a/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.Windows.cs +++ /dev/null @@ -1,63 +0,0 @@ -// /******************************************************************************* -// * Copyright 2012-2018 Esri -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// ******************************************************************************/ - -#if WINDOWS_XAML -using Esri.ArcGISRuntime.Mapping.Popups; - -namespace Esri.ArcGISRuntime.Toolkit.UI.Controls -{ - public partial class PopupViewer - { - private void Initialize() => DefaultStyleKey = GetType(); - - /// -#if WINDOWS_XAML - protected override void OnApplyTemplate() -#else - public override void OnApplyTemplate() -#endif - { - base.OnApplyTemplate(); - Refresh(); - } - - private void Refresh() - { - } - - /// - /// Gets or sets the associated PopupManager which contains popup and sketch editor. - /// - private PopupManager? PopupManagerImpl - { - get { return GetValue(PopupManagerProperty) as PopupManager; } - set { SetValue(PopupManagerProperty, value); } - } - - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty PopupManagerProperty = - DependencyProperty.Register(nameof(PopupManager), typeof(PopupManager), typeof(PopupViewer), - new PropertyMetadata(null, OnPopupManagerPropertyChanged)); - - private static void OnPopupManagerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - (d as PopupViewer)?.Refresh(); - } - } -} -#endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.WindowsXaml.cs b/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.WindowsXaml.cs deleted file mode 100644 index 2280a05cf..000000000 --- a/src/Toolkit/Toolkit.WinUI/UI/Controls/PopupViewer/PopupViewer.WindowsXaml.cs +++ /dev/null @@ -1,57 +0,0 @@ -// /******************************************************************************* -// * Copyright 2012-2018 Esri -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// ******************************************************************************/ - -#if WINDOWS_XAML - -using Esri.ArcGISRuntime.Mapping.Popups; - -#if __IOS__ -using Control = UIKit.UIView; -#elif __ANDROID__ -using Control = Android.Views.ViewGroup; -#endif - -namespace Esri.ArcGISRuntime.Toolkit.UI.Controls -{ - /// - /// The PopupViewer control is used to display details and media, edit attributes, geometry and related records, - /// manage attachments of an or a - /// as defined in its . - /// - public partial class PopupViewer : Control - { -#if !__ANDROID__ - /// - /// Initializes a new instance of the class. - /// - public PopupViewer() - : base() - { - Initialize(); - } -#endif - - /// - /// Gets or sets the associated PopupManager which contains popup and sketch editor. - /// - public PopupManager? PopupManager - { - get => PopupManagerImpl; - set => PopupManagerImpl = value; - } - } -} -#endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/Internal/DispatcherExtensions.cs b/src/Toolkit/Toolkit/Internal/DispatcherExtensions.cs index ebc946000..15f6a69ba 100644 --- a/src/Toolkit/Toolkit/Internal/DispatcherExtensions.cs +++ b/src/Toolkit/Toolkit/Internal/DispatcherExtensions.cs @@ -37,6 +37,23 @@ internal static void Dispatch(this System.Windows.Threading.DispatcherObject dOb else dObject.Dispatcher.Invoke(action); } +#elif WINUI + internal static void Dispatch(this DependencyObject dObject, Action action) + { + if (dObject.DispatcherQueue.HasThreadAccess) + action(); + else + dObject.DispatcherQueue.TryEnqueue(() => action()); + } + +#elif WINDOWS_UWP + internal static void Dispatch(this DependencyObject dObject, Action action) + { + if (dObject.Dispatcher.HasThreadAccess) + action(); + else + _ = dObject.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => action()); + } #endif } } diff --git a/src/Toolkit/Toolkit/Internal/HtmlUtility.cs b/src/Toolkit/Toolkit/Internal/HtmlUtility.cs index ed20393ee..a65fecb18 100644 --- a/src/Toolkit/Toolkit/Internal/HtmlUtility.cs +++ b/src/Toolkit/Toolkit/Internal/HtmlUtility.cs @@ -1,4 +1,3 @@ -#if WPF || MAUI using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -301,7 +300,11 @@ internal static MarkupNode BuildDocumentTree(string snippet) else if (name == "font") { if (attr.TryGetValue("color", out var fontColor)) +#if WINDOWS_UWP + newNode.FontColor = ParseCssColor(fontColor); +#else newNode.FontColor = ColorTranslator.FromHtml(fontColor); +#endif if (attr.TryGetValue("size", out var fontSizeStr) && Int32.TryParse(fontSizeStr, out var fontSize)) newNode.FontSize = ParseHtmlFontSize(fontSize); } @@ -326,7 +329,11 @@ internal static MarkupNode BuildDocumentTree(string snippet) case "tr": newNode.Type = MarkupType.TableRow; if (attr.TryGetValue("bgcolor", out var backColor)) +#if WINDOWS_UWP + newNode.BackColor = ParseCssColor(backColor); +#else newNode.BackColor = ColorTranslator.FromHtml(backColor); +#endif // TODO valign break; case "td": @@ -1141,5 +1148,4 @@ internal enum HtmlAlignment Right, Center, Justify, -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/FeatureForm/AttachmentsFormElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/FeatureForm/AttachmentsFormElementView.Windows.cs index 2ed8abf53..6f8d67464 100644 --- a/src/Toolkit/Toolkit/UI/Controls/FeatureForm/AttachmentsFormElementView.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/FeatureForm/AttachmentsFormElementView.Windows.cs @@ -52,7 +52,6 @@ public override void OnApplyTemplate() } if(GetTemplateChild("ItemsScrollView") is ScrollViewer scrollViewer) scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged; - UpdateVisibility(); } private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e) @@ -88,4 +87,4 @@ private void AddAttachmentButton_Click(object sender, RoutedEventArgs e) } } } -#endif +#endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentThumbnailImage.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentThumbnailImage.cs new file mode 100644 index 000000000..121ac7abe --- /dev/null +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentThumbnailImage.cs @@ -0,0 +1,199 @@ +// /******************************************************************************* +// * Copyright 2012-2018 Esri +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ******************************************************************************/ +#if WPF || WINDOWS_XAML +using Esri.ArcGISRuntime.Mapping.Popups; +using Esri.ArcGISRuntime.Toolkit.Internal; +using Esri.ArcGISRuntime.UI; + +#if NET6_0_OR_GREATER +using System.IO; +using System.Runtime.InteropServices.WindowsRuntime; +#endif +#if WINUI +using Microsoft.UI.Xaml.Media.Imaging; +#elif WINDOWS_UWP +using Windows.UI.Xaml.Media.Imaging; +#endif + +namespace Esri.ArcGISRuntime.Toolkit.Primitives +{ + /// + /// Supporting control for the control, + /// used for rendering a . + /// + public class AttachmentThumbnailImage : Control + { + + /// + /// Initializes a new instance of the class. + /// + public AttachmentThumbnailImage() + { + DefaultStyleKey = typeof(AttachmentThumbnailImage); + } + + /// +#if WINDOWS_XAML + protected override void OnApplyTemplate() +#else + public override void OnApplyTemplate() +#endif + { + base.OnApplyTemplate(); + + UpdateVisualState(false); + LoadThumbnail(); + } + + /// + /// Gets or sets the attachment to display. + /// + public PopupAttachment? Attachment + { + get { return (PopupAttachment)GetValue(AttachmentProperty); } + set { SetValue(AttachmentProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AttachmentProperty = + DependencyProperty.Register(nameof(Attachment), typeof(PopupAttachment), typeof(AttachmentThumbnailImage), new PropertyMetadata(null, (d, e) => ((AttachmentThumbnailImage)d).OnAttachmentChanged(e.OldValue as PopupAttachment, e.NewValue as PopupAttachment))); + + private void OnAttachmentChanged(PopupAttachment? oldAttachment, PopupAttachment? newAttachment) + { + var img = GetTemplateChild("PART_Image") as Image; + if(img != null) + { + img.Source = null; + } + if (oldAttachment != null) + { + oldAttachment.LoadStatusChanged -= Attachment_LoadStatusChanged; + } + if (newAttachment != null && newAttachment.LoadStatus != LoadStatus.Loaded) + { + newAttachment.LoadStatusChanged += Attachment_LoadStatusChanged; + } + LoadThumbnail(); + UpdateVisualState(false); + } + + private void Attachment_LoadStatusChanged(object? sender, LoadStatusEventArgs e) + { + if (e.Status == LoadStatus.Loaded) + { + ((ILoadable)sender!).LoadStatusChanged -= Attachment_LoadStatusChanged; + this.Dispatch(LoadThumbnail); + } + this.Dispatch(() => UpdateVisualState(true)); + } + + private void UpdateVisualState(bool useTransistions) + { + var status = Attachment?.LoadStatus ?? LoadStatus.NotLoaded; + bool isLocal = Attachment?.IsLocal ?? false; + if (isLocal) + VisualStateManager.GoToState(this, "AttachmentIsLocal", useTransistions); + else + { + switch (status) + { + case LoadStatus.Loading: + VisualStateManager.GoToState(this, "AttachmentLoading", useTransistions); + break; + case LoadStatus.Loaded: + VisualStateManager.GoToState(this, "AttachmentLoaded", useTransistions); + break; + case LoadStatus.FailedToLoad: + VisualStateManager.GoToState(this, "AttachmentFailedToLoad", useTransistions); + break; + case LoadStatus.NotLoaded: + VisualStateManager.GoToState(this, "AttachmentNotLoaded", useTransistions); + break; + } + } + } + + private async void LoadThumbnail() + { + var img = GetTemplateChild("PART_Image") as Image; + if (img is null || ThumbnailSize <= 0) + return; + try + { +#if WINUI + var size = ThumbnailSize * XamlRoot?.RasterizationScale ?? 1; +#elif WINDOWS_UWP + var size = ThumbnailSize * Windows.Graphics.Display.DisplayInformation.GetForCurrentView()?.RawPixelsPerViewPixel ?? 1; +#elif WPF + var size = ThumbnailSize * VisualTreeHelper.GetDpi(this).PixelsPerDip; +#endif +#if NETFRAMEWORK + if (Attachment != null && Attachment.Type == PopupAttachmentType.Image && Attachment.IsLocal) + { + var thumb = await Attachment.CreateThumbnailAsync((int)size, (int)size); + img.Source = await thumb.ToImageSourceAsync(); + return; + } +#else + if (Attachment != null && Attachment.IsLocal) + { + if (!File.Exists(Attachment.Filename) && Attachment.LoadStatus == LoadStatus.NotLoaded) + { + await Attachment.LoadAsync(); + } + if (File.Exists(Attachment.Filename)) + { + var fs = await Windows.Storage.StorageFile.GetFileFromPathAsync(Attachment.Filename); + + var thumb = await fs.GetThumbnailAsync(Windows.Storage.FileProperties.ThumbnailMode.SingleItem, (uint)size, Windows.Storage.FileProperties.ThumbnailOptions.ResizeThumbnail); + using var ms = new MemoryStream(); + thumb.AsStreamForRead().CopyTo(ms); + ms.Seek(0, SeekOrigin.Begin); +#if WPF + img.Source = System.Windows.Media.Imaging.BitmapFrame.Create(ms, System.Windows.Media.Imaging.BitmapCreateOptions.None, System.Windows.Media.Imaging.BitmapCacheOption.OnLoad); +#else + var source = new BitmapImage(); + await source.SetSourceAsync(ms.AsRandomAccessStream()); + img.Source = source; +#endif + return; + } + } +#endif + } + catch { } + img.Source = null; + } + + /// + /// Gets or sets the size of the thumbnail to display. + /// + public double ThumbnailSize + { + get { return (double)GetValue(ThumbnailSizeProperty); } + set { SetValue(ThumbnailSizeProperty, value); } + } + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ThumbnailSizeProperty = + DependencyProperty.Register(nameof(ThumbnailSize), typeof(double), typeof(AttachmentThumbnailImage), new PropertyMetadata(30d)); + } +} +#endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.Windows.cs index 9a9f5c895..d6f625a69 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.Windows.cs @@ -14,10 +14,15 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF +#if WPF || WINDOWS_XAML using Esri.ArcGISRuntime.Mapping.Popups; -using Microsoft.Win32; -using System.Windows.Controls.Primitives; +using Esri.ArcGISRuntime.UI; +using System.IO; + +#if NET6_0_OR_GREATER +using System.Runtime.InteropServices.WindowsRuntime; + +#endif namespace Esri.ArcGISRuntime.Toolkit.Primitives { diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.cs index 3b9a17405..6edfd7f8d 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/AttachmentsPopupElementView.cs @@ -14,13 +14,12 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF || MAUI using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; using Microsoft.Win32; #if WPF using System.Windows.Controls.Primitives; -#else +#elif MAUI using ListBox = Microsoft.Maui.Controls.CollectionView; using Selector = Microsoft.Maui.Controls.SelectableItemsView; #endif @@ -46,9 +45,9 @@ public AttachmentsPopupElementView() } /// -#if MAUI +#if WINDOWS_XAML || MAUI protected override void OnApplyTemplate() -#else +#elif WPF public override void OnApplyTemplate() #endif { @@ -157,7 +156,7 @@ await Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync( { System.Diagnostics.Trace.WriteLine($"Failed to open attachment: " + ex.Message); } -#else +#elif WPF SaveFileDialog saveFileDialog = new SaveFileDialog(); saveFileDialog.FileName = attachment.Name; if (saveFileDialog.ShowDialog() == true) @@ -173,9 +172,45 @@ await Microsoft.Maui.ApplicationModel.Launcher.Default.OpenAsync( System.Diagnostics.Trace.WriteLine($"Failed to save file to disk: " + ex.Message); } } +#elif WINDOWS_XAML + Windows.Storage.StorageFile? file = null; +#if WINUI + var hwnd = this.XamlRoot?.ContentIslandEnvironment?.AppWindowId.Value ?? 0; + if (hwnd == 0) + return; // Can't show dialog without a root window +#endif + try + { + if (attachment.LoadStatus == LoadStatus.NotLoaded) + await attachment.LoadAsync(); + var fileInfo = new FileInfo(attachment.Filename!); + var savePicker = new Windows.Storage.Pickers.FileSavePicker(); +#if WINUI + WinRT.Interop.InitializeWithWindow.Initialize(savePicker, (nint)hwnd); +#endif + var ext = fileInfo.Extension; + savePicker.FileTypeChoices.Add("*" + ext, new List() { ext }); + savePicker.SuggestedFileName = fileInfo.Name; + file = await savePicker.PickSaveFileAsync(); + if (file != null) + { + Windows.Storage.CachedFileManager.DeferUpdates(file); + using var stream = await attachment.Attachment!.GetDataAsync(); + using var filestream= await file.OpenStreamForWriteAsync(); + await stream.CopyToAsync(filestream); + } + } + catch (System.Exception ex) + { + System.Diagnostics.Trace.WriteLine($"Failed to open attachment: " + ex.Message); + } + finally + { + if (file != null) + _ = Windows.Storage.CachedFileManager.CompleteUpdatesAsync(file); + } #endif } } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.Windows.cs index c8d2de74a..7aa3169d8 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.Windows.cs @@ -14,11 +14,13 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF +#if WPF || WINDOWS_XAML using Esri.ArcGISRuntime.Data; using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; +#if WPF using System.Windows.Documents; +#endif namespace Esri.ArcGISRuntime.Toolkit.Primitives { diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.cs index b9a0af058..1ed22b48c 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/FieldsPopupElementView.cs @@ -14,7 +14,6 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF || MAUI using Esri.ArcGISRuntime.Data; using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; @@ -24,11 +23,17 @@ using Esri.ArcGISRuntime.Toolkit.UI.Controls; #endif + #if MAUI using TextBlock = Microsoft.Maui.Controls.Label; -#else +#elif WPF using System.Windows.Documents; +#elif WINUI +using Microsoft.UI.Xaml.Documents; +#elif WINDOWS_UWP +using Windows.UI.Xaml.Documents; #endif + #if MAUI namespace Esri.ArcGISRuntime.Toolkit.Maui.Primitives #else @@ -56,9 +61,9 @@ public FieldsPopupElementView() /// #if WINDOWS_XAML || MAUI protected override void OnApplyTemplate() -#else +#elif WPF public override void OnApplyTemplate() -# endif +#endif { base.OnApplyTemplate(); RefreshTable(); @@ -77,7 +82,7 @@ public FieldsPopupElement? Element /// Identifies the dependency property. /// public static readonly DependencyProperty ElementProperty = - PropertyHelper.CreateProperty(nameof(Element), null, (s, oldValue, newValue) => s.RefreshTable()); + PropertyHelper.CreateProperty(nameof(Element), null, (s, oldValue, newValue) => s.RefreshTable()); private void RefreshTable() { @@ -152,7 +157,11 @@ private void RefreshTable() if (uri is not null) PopupViewer.GetPopupViewerParent(this)?.OnHyperlinkClicked(uri); }; +#if WINDOWS_XAML + hl.Inlines.Add(new Run() { Text = "View" }); +#else hl.Inlines.Add("View"); +#endif t.Inlines.Add(hl); #endif } @@ -239,5 +248,4 @@ public Style FieldTextStyle public static readonly DependencyProperty FieldTextStyleProperty = PropertyHelper.CreateProperty(nameof(FieldTextStyle)); } -} -#endif +} \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.Windows.cs index eb1e46732..e11a79472 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.Windows.cs @@ -13,11 +13,12 @@ // * See the License for the specific language governing permissions and // * limitations under the License. // ******************************************************************************/ -#if WPF - +#if WPF || WINDOWS_XAML using Esri.ArcGISRuntime.Mapping.Popups; using System.Collections; +#if WPF using System.Windows.Controls.Primitives; +#endif namespace Esri.ArcGISRuntime.Toolkit.Primitives { @@ -28,14 +29,21 @@ namespace Esri.ArcGISRuntime.Toolkit.Primitives [TemplatePart(Name = "PreviousButton", Type = typeof(ButtonBase))] [TemplatePart(Name = "NextButton", Type = typeof(ButtonBase))] public partial class MediaPopupElementView : Control - { + { +#if WPF private ButtonBase? _previousButton; private ButtonBase? _nextButton; private int selectedIndex = 0; +#endif /// +#if WPF public override void OnApplyTemplate() +#elif WINDOWS_XAML + protected override void OnApplyTemplate() +#endif { +#if WPF if (_previousButton != null) { _previousButton.Click -= OnPreviousButtonClicked; @@ -55,9 +63,13 @@ public override void OnApplyTemplate() _nextButton.Click += OnNextButtonClicked; } UpdateContent(); +#elif WINUI + CreatePipsPager(); +#endif base.OnApplyTemplate(); } +#if WPF /// /// Gets or sets the currently display . /// @@ -111,12 +123,38 @@ private void OnNextButtonClicked(object sender, RoutedEventArgs e) UpdateContent(); } +#endif private void OnElementPropertyChanged() { +#if WPF selectedIndex = 0; UpdateContent(); +#elif WinUI + UpdatePipsVisibility(); +#endif + } + +#if WINUI + private void CreatePipsPager() + { + // Instead of creating Pips in XAML which UWP doesn't support, we create it in code here instead of having to maintain two sets of control templates + if (GetTemplateChild("PipsPagerContainer") is ContentControl contentControl && GetTemplateChild("FlipView") is FlipView flipView) + { + PipsPager p = new PipsPager() { HorizontalAlignment = HorizontalAlignment.Center }; + p.SetBinding(PipsPager.NumberOfPagesProperty, new Binding() { Path = new PropertyPath("Element.Media.Count"), Source = this }); + p.SetBinding(PipsPager.SelectedPageIndexProperty, new Binding() { Path = new PropertyPath(nameof(FlipView.SelectedIndex)), Mode = BindingMode.TwoWay, Source = flipView }); + contentControl.Content = p; + } + UpdatePipsVisibility(); + } + private void UpdatePipsVisibility() + { + if (GetTemplateChild("PipsPagerContainer") is ContentControl cc) + { + cc.Visibility = (Element?.Media?.Count ?? 0) > 1 ? Visibility.Visible : Visibility.Collapsed; + } } - +#endif /// /// Gets or sets the template for popup media items. diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.cs index aa3095825..bead5e992 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/MediaPopupElementView.cs @@ -13,8 +13,6 @@ // * See the License for the specific language governing permissions and // * limitations under the License. // ******************************************************************************/ -#if WPF || MAUI - using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; using System.Collections; @@ -54,5 +52,4 @@ public MediaPopupElement? Element public static readonly DependencyProperty ElementProperty = PropertyHelper.CreateProperty(nameof(Element), null, (s, oldValue, newValue) => s.OnElementPropertyChanged()); } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupElementItemsControl.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupElementItemsControl.Windows.cs index c872f2466..39fa1cca1 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupElementItemsControl.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupElementItemsControl.Windows.cs @@ -14,7 +14,7 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF +#if WPF || WINDOWS_XAML using System.Diagnostics; using Esri.ArcGISRuntime.Mapping.Popups; @@ -29,6 +29,7 @@ public class PopupElementItemsControl : ItemsControl /// protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { + base.PrepareContainerForItemOverride(element, item); if (element is ContentPresenter presenter) { if (item is TextPopupElement) @@ -59,7 +60,6 @@ protected override void PrepareContainerForItemOverride(DependencyObject element Debug.Assert(false); } } - base.PrepareContainerForItemOverride(element, item); } /// diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.Windows.cs index 3072201d2..26cb693fb 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.Windows.cs @@ -14,15 +14,19 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF +#if WPF || WINDOWS_XAML using Esri.ArcGISRuntime.Data; using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; using Esri.ArcGISRuntime.UI; using System.IO; +#if WPF using System.Windows.Input; using System.Windows.Media.Imaging; using System.Xaml; +#elif WINDOWS_XAML +using Windows.Foundation; +#endif namespace Esri.ArcGISRuntime.Toolkit.Primitives { @@ -32,14 +36,14 @@ namespace Esri.ArcGISRuntime.Toolkit.Primitives /// public partial class PopupMediaView : ContentControl { - +#if WPF /// protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) { _lastChartSize = 0; base.OnDpiChanged(oldDpi, newDpi); } - +#endif /// protected override Size MeasureOverride(Size constraint) { @@ -49,6 +53,7 @@ protected override Size MeasureOverride(Size constraint) } return base.MeasureOverride(constraint); } + } } #endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.cs index 3add6c07c..cf3641aa2 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupMediaView.cs @@ -14,15 +14,23 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF || MAUI using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; + #if WPF using Esri.ArcGISRuntime.UI; using System.IO; using System.Windows.Input; using System.Windows.Media.Imaging; using System.Xaml; +#elif WINDOWS_XAML +using Esri.ArcGISRuntime.UI; +using Windows.Foundation; +#if WINUI +using Microsoft.UI.Xaml.Media.Imaging; +#elif WINDOWS_UWP +using Windows.UI.Xaml.Media.Imaging; +#endif #endif #if MAUI @@ -102,11 +110,15 @@ private void UpdateImage() img.GestureRecognizers.Add(tapGesture); #else img.Tag = linkUrl; +#if WPF if(img.Cursor != Cursors.Hand) { img.Cursor = Cursors.Hand; img.MouseLeftButtonDown += (s, e) => _ = Launcher.LaunchUriAsync((s as Image)?.Tag as Uri); } +#elif WINDOWS_XAML + img.Tapped += (s, e) => _ = Launcher.LaunchUriAsync((s as Image)?.Tag as Uri); +#endif #endif } Content = img; @@ -139,8 +151,12 @@ private async void UpdateChart(Size desiredSize) { #if MAUI Content = await GenerateChartAsync(desiredWidth, desiredWidth, DeviceDisplay.Current.MainDisplayInfo.Density * 96); -#else +#elif WPF Content = await GenerateChartAsync(desiredWidth, desiredWidth, VisualTreeHelper.GetDpi(this).PixelsPerInchX); +#elif WINUI + Content = await GenerateChartAsync(desiredWidth, desiredWidth, (XamlRoot?.RasterizationScale ?? 1) * 96); +#elif WINDOWS_UWP + Content = await GenerateChartAsync(desiredWidth, desiredWidth, Windows.Graphics.Display.DisplayInformation.GetForCurrentView()?.LogicalDpi ?? 96); #endif } catch @@ -166,6 +182,13 @@ private async void UpdateChart(Size desiredSize) case Microsoft.Maui.ApplicationModel.AppTheme.Light: style = Mapping.ChartImageStyle.Light; break; default: style = Mapping.ChartImageStyle.Neutral; break; } +#elif WINDOWS_XAML + switch (ActualTheme) + { + case ElementTheme.Dark: style = Mapping.ChartImageStyle.Dark; break; + case ElementTheme.Light: style = Mapping.ChartImageStyle.Light; break; + default: style = Mapping.ChartImageStyle.Neutral; break; + } #endif var chart = await PopupMedia.GenerateChartAsync(new Mapping.ChartImageParameters((int)(width * scalefactor), (int)(height * scalefactor)) { Dpi = (float)dpi, Style = style }); var source = await chart.Image.ToImageSourceAsync(); @@ -218,11 +241,14 @@ internal static bool TryCreateImageSource(string? sourceUri, out ImageSource? so var data = Convert.FromBase64String(base64data); #if MAUI var newSource = new StreamImageSource { Stream = (token) => Task.FromResult(new MemoryStream(data)) }; -#else +#elif WPF var newSource = new BitmapImage(); newSource.BeginInit(); newSource.StreamSource = new MemoryStream(data); newSource.EndInit(); +#else + var newSource = new BitmapImage(); + newSource.SetSource(new MemoryStream(data).AsRandomAccessStream()); #endif source = newSource; return true; @@ -243,5 +269,4 @@ internal static bool TryCreateImageSource(string? sourceUri, out ImageSource? so return false; } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.Windows.cs index ee05e97b6..7751fbfdd 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.Windows.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.Windows.cs @@ -14,7 +14,7 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF +#if WPF || WINDOWS_XAML using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.RealTime; using Esri.ArcGISRuntime.Toolkit.Internal; @@ -61,10 +61,12 @@ private static void OnPopupManagerPropertyChanged(DependencyObject d, Dependency private static T? GetParent(DependencyObject? child) where T : DependencyObject { +#if WPF if (child is FrameworkContentElement elm) { child = GetVisualParent(elm); } +#endif if (child is null) return default; var parent = VisualTreeHelper.GetParent(child); @@ -75,6 +77,7 @@ private static void OnPopupManagerPropertyChanged(DependencyObject d, Dependency return parent as T; } +#if WPF private static Visual? GetVisualParent(FrameworkContentElement child) { var parent = child.Parent; @@ -84,6 +87,7 @@ private static void OnPopupManagerPropertyChanged(DependencyObject d, Dependency } return parent as Visual; } +#endif } } #endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs index 3f5e5d2b8..aed84d51d 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/PopupViewer.cs @@ -14,11 +14,12 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF || MAUI using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.RealTime; using Esri.ArcGISRuntime.Toolkit.Internal; using System.ComponentModel; +using Popup = Esri.ArcGISRuntime.Mapping.Popups.Popup; + #if MAUI using Esri.ArcGISRuntime.Toolkit.Maui.Primitives; @@ -83,7 +84,7 @@ public PopupViewer() /// #if WINDOWS_XAML || MAUI protected override void OnApplyTemplate() -#else +#elif WPF public override void OnApplyTemplate() #endif { @@ -110,8 +111,12 @@ private void InvalidatePopup() } #if MAUI Dispatcher.Dispatch(async () => -#else +#elif WPF _ = Dispatcher.InvokeAsync(async () => +#elif WINUI + DispatcherQueue.TryEnqueue(async () => +#elif WINDOWS_UWP + _ = Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () => #endif { try @@ -132,7 +137,11 @@ private void InvalidatePopup() #else var ctrl = GetTemplateChild(ItemsViewName) as ItemsControl; var binding = ctrl?.GetBindingExpression(ItemsControl.ItemsSourceProperty); +#if WPF binding?.UpdateTarget(); +#elif WINDOWS_XAML + ctrl?.SetBinding(ItemsControl.ItemsSourceProperty, new Binding { Path = new PropertyPath("Popup.EvaluatedElements"), Source = this }); +#endif #endif } } @@ -191,8 +200,10 @@ private void OnPopupPropertyChanged(Popup? oldPopup, Popup? newPopup) InvalidatePopup(); #if MAUI (GetTemplateChild(PopupContentScrollViewerName) as ScrollViewer)?.ScrollToAsync(0,0,false); -#else +#elif WPF (GetTemplateChild(PopupContentScrollViewerName) as ScrollViewer)?.ScrollToHome(); +#elif WINDOWS_XAML + (GetTemplateChild(PopupContentScrollViewerName) as ScrollViewer)?.ChangeView(null, 0, null, disableAnimation: true); #endif } @@ -314,5 +325,4 @@ internal HyperlinkClickedEventArgs(Uri uri) /// public Uri Uri { get; } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.WPF.cs similarity index 100% rename from src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.Windows.cs rename to src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.WPF.cs diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.WinUI.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.WinUI.cs new file mode 100644 index 000000000..b40f6eeac --- /dev/null +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.WinUI.cs @@ -0,0 +1,455 @@ +// /******************************************************************************* +// * Copyright 2012-2018 Esri +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// ******************************************************************************/ + +#if WINDOWS_XAML +using Esri.ArcGISRuntime.Mapping.Popups; +using Esri.ArcGISRuntime.Toolkit.Internal; +using Esri.ArcGISRuntime.Toolkit.UI.Controls; + + +#if WINUI +using Microsoft.UI; +using Microsoft.UI.Text; +using Microsoft.UI.Xaml.Documents; +using Windows.UI.Text; +#elif WINDOWS_UWP +using Windows.UI; +using Windows.UI.Text; +using Windows.UI.Xaml.Documents; +#endif + + +namespace Esri.ArcGISRuntime.Toolkit.Primitives +{ + /// + /// Supporting control for the control, + /// used for rendering a . + /// + [TemplatePart(Name = TextAreaName, Type = typeof(RichTextBlock))] + public partial class TextPopupElementView : Control + { + private static Thickness ParagraphMargin = new(0, 0, 0, 16); + private const string TextAreaName = "TextArea"; + + private void OnElementPropertyChanged() + { + // Full list of supported tags and attributes here: https://doc.arcgis.com/en/arcgis-online/reference/supported-html.htm + if (!string.IsNullOrEmpty(Element?.Text) && GetTemplateChild(TextAreaName) is ContentControl rtb) + { + StackPanel container = new StackPanel(); + try + { + var htmlRoot = HtmlUtility.BuildDocumentTree(Element.Text); + var blocks = VisitChildren(htmlRoot); + foreach (var block in blocks) + container.Children.Add(block); + + } + catch + { + // Fallback if something went wrong with the parsing: + // Just display the text without any markup; + var plainText = Element.Text.ToPlainText(); + container.Children.Add(new TextBlock() { Text = plainText }); + } + rtb.Content = container; + } + } + + private static IEnumerable VisitChildren(MarkupNode parent) + { + // Create views for all the children of a given node. + // Nodes with blocks are converted individually, but consecutive inline-only nodes are grouped into textblockss. + List? inlineNodes = null; + foreach (var node in parent.Children) + { + node.InheritAttributes(parent); + if (MapsToBlock(node) || HasAnyBlocks(node)) + { + if (inlineNodes != null) + { + var label = CreateFormattedText(inlineNodes); + ApplyStyle(label, parent); + inlineNodes = null; + yield return label; + } + yield return CreateBlock(node); + } + else + { + inlineNodes ??= new List(); + inlineNodes.Add(node); + } + } + if (inlineNodes != null) + { + var label = CreateFormattedText(inlineNodes); + ApplyStyle(label, parent); + yield return label; + } + } + + private static FrameworkElement CreateBlock(MarkupNode node) + { + // Create a view for a single block node. + switch (node.Type) + { + case MarkupType.List: + // Lists (li and ol) are laid out in a grid, with a narrow column of markers and a wide one for the content. + // +-----+----------------------+ + // | 1. | First item content | + // +-----+----------------------+ + // | 2. | Second item content | + // +-----+----------------------+ + // | ... | + // +-----+----------------------+ + // | 3. | Last item content | + // +-----+----------------------+ + bool isOrdered = node.Token?.Name == "ol"; + var listGrid = new Grid { Margin = new Thickness(0, 0, 0, 16) }; + listGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // bullets + listGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // contents + + var childItems = node.Children.Where(n => n.Type == MarkupType.ListItem).ToList(); // ignore a misplaced non-list-item node + + for (int row = 0; row < childItems.Count; row++) + { + listGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + var markerText = isOrdered ? $"{row + 1}." : "\u2022"; + var tb = new TextBlock { Text = markerText, HorizontalTextAlignment = TextAlignment.End, Margin = new Thickness(5, 0, 5, 0) }; + Grid.SetRow(tb, row); + listGrid.Children.Add(tb); + var item = CreateBlock(childItems[row]); + Grid.SetRow(item, row); + Grid.SetColumn(item, 1); + listGrid.Children.Add(item); + } + return listGrid; + + case MarkupType.Block: + case MarkupType.ListItem: + case MarkupType.TableCell: + bool isPara = node.Token?.Name == "p"; + FrameworkElement view; + if (HasAnyBlocks(node)) + { + var container = new StackPanel(); + if (isPara) + container.Margin = ParagraphMargin; + if (node.BackColor.HasValue) + container.Background = new SolidColorBrush(ConvertColor(node.BackColor.Value)); + + var blocks = VisitChildren(node); + foreach (var block in blocks) + { + container.Children.Add(block); + } + view = container; + } + else + { + foreach (var child in node.Children) // We have to copy the style down to FormattedText's elements + child.InheritAttributes(node); + var label = CreateFormattedText(node.Children); + if (isPara) + label.Margin = ParagraphMargin; + ApplyStyle(label, node); + view = label; + } + if (node.Type == MarkupType.TableCell) + return VerticallyAlignTableCell(node, view); + return view; + + case MarkupType.Divider: + return new Border { Height = 1, Background = new SolidColorBrush(Colors.Gray) }; // TODO: Do we need to set a color? + + case MarkupType.Table: + return ConvertTableToGrid(node); + + case MarkupType.Image: + var imageElement = new Image(); + if (PopupMediaView.TryCreateImageSource(node.Content, out var imageSource)) + imageElement.Source = imageSource; + return imageElement; + + default: + return new Border(); // placeholder for unsupported things + } + + static FrameworkElement VerticallyAlignTableCell(MarkupNode node, FrameworkElement cellContent) + { + // In HTML, table cells are vertically centered by default. + cellContent.VerticalAlignment = VerticalAlignment.Center; + if (node.BackColor.HasValue) + { + // If a table-cell has a background color, we need to wrap the content in a space-filling Grid, + // otherwise the background color will only show directly behind the text and look patchy. + var container = new ContentControl() { HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch }; + container.Content = cellContent; + container.Background = new SolidColorBrush(ConvertColor(node.BackColor.Value)); + return container; + } + else + { + return cellContent; + } + } + } + + private static TextBlock CreateFormattedText(IEnumerable nodes) + { + // Flattens given tree of inline nodes into a single label. + var tb = new TextBlock () { TextWrapping = TextWrapping.WrapWholeWords }; + foreach (var node in nodes) + { + foreach (var span in VisitInline(node)) + { + tb.Inlines.Add(span); + } + } + return tb; + } + + private static IEnumerable VisitInline(MarkupNode node) + { + // Converts a single inline node into a sequence of spans. + // The whole tree is expected to only contain inline nodes. Other nodes are handled by VisitBlock. + switch (node.Type) + { + case MarkupType.Link: + if (Uri.TryCreate(node.Content, UriKind.Absolute, out var linkUri)) + { + // The gesture recognizer will be shared by all the individual spans + var link = new Hyperlink(); + link.Click += (s, e) => + { + PopupViewer.GetPopupViewerParent(s)?.OnHyperlinkClicked(linkUri); + }; + foreach (var subNode in node.Children) + { + subNode.InheritAttributes(node); + foreach (var subSpan in VisitInline(subNode)) + { + ApplyStyle(subSpan, node); + if (node.IsUnderline != false) // Add underline to links by default (unless specifically disabled) + subSpan.TextDecorations = TextDecorations.Underline; + link.Inlines.Add(subSpan); + } + } + yield return link; + } + else + { + // Fallback: treat it as a regular span + goto case MarkupType.Span; + } + break; + + case MarkupType.Span: + case MarkupType.Sub: // Font variants are not supported on MAUI + case MarkupType.Sup: // Font variants are not supported on MAUI + foreach (var subNode in node.Children) + { + subNode.InheritAttributes(node); + foreach (var subSpan in VisitInline(subNode)) + { + yield return subSpan; + } + } + break; + + case MarkupType.Break: + var linebreak = new Span(); + linebreak.Inlines.Add(new LineBreak() ); + yield return linebreak; + break; + + case MarkupType.Text: + var textSpan = new Span(); + textSpan.Inlines.Add(new Run { Text = node.Content }); + ApplyStyle(textSpan, node); + yield return textSpan; + break; + } + } + + private static Grid ConvertTableToGrid(MarkupNode table) + { + // Determines the dimensions of a grid necessary to hold a given table. + // Utilizes a dynamically-sized 2D bitmap (`gridMap`) to mark occupied cells while iterating over the table. + // Expands the grid as necessary based on cell spans and avoids collisions by checking the bitmap. + List> gridMap = new List>(); + + int maxRowUsed = -1; + int maxColUsed = -1; + + var gridView = new Grid(); + + int curRow = 0; + foreach (MarkupNode tr in table.Children) + { + tr.InheritAttributes(table); + int curCol = 0; + foreach (MarkupNode td in tr.Children) + { + // Find the next available cell in this row + EnsureColumnExists(curRow, curCol); + while (gridMap[curRow][curCol]) + { + curCol++; + EnsureColumnExists(curRow, curCol); + } + + int rowSpan = 1; + int colSpan = 1; + + // Create a View for the current table-cell, and add it to the grid. + td.InheritAttributes(tr); + var cellView = CreateBlock(td); + var attr = HtmlUtility.ParseAttributes(td.Token?.Attributes); + if (attr.TryGetValue("colspan", out var colSpanStr) && ushort.TryParse(colSpanStr, out var colSpanFromAttr)) + { + colSpan = colSpanFromAttr; + Grid.SetColumnSpan(cellView, colSpan); + } + if (attr.TryGetValue("rowspan", out var rowSpanStr) && ushort.TryParse(rowSpanStr, out var rowSpanFromAttr)) + { + rowSpan = rowSpanFromAttr; + Grid.SetRowSpan(cellView, colSpan); + } + Grid.SetRow(cellView, curRow); + Grid.SetColumn(cellView, curCol); + gridView.Children.Add(cellView); + + // Mark grid-cells occupied by the current table-cell + for (int i = 0; i < rowSpan; i++) + { + for (int j = 0; j < colSpan; j++) + { + EnsureColumnExists(curRow + i, curCol + j); + + gridMap[curRow + i][curCol + j] = true; + + maxRowUsed = Math.Max(maxRowUsed, curRow + i); + maxColUsed = Math.Max(maxColUsed, curCol + j); + } + } + curCol += colSpan; + } + curRow++; + } + + // Now we know exactly how many rows and columns were necessary to hold the table. Allocate them! + for (int i = 0; i <= maxRowUsed; i++) + gridView.RowDefinitions.Add(new RowDefinition()); + for (int i = 0; i <= maxColUsed; i++) + gridView.ColumnDefinitions.Add(new ColumnDefinition()); + + return gridView; + + // Expand the gridMap as needed to make sure that given row/column exists + void EnsureColumnExists(int row, int col) + { + while (gridMap.Count <= row) + gridMap.Add(new List()); + while (gridMap[row].Count <= col) + gridMap[row].Add(false); + } + } + + private static void ApplyStyle(Span el, MarkupNode node) + { + if (node.IsBold == true) + el.FontWeight = FontWeights.Bold; + else if (node.IsBold == false) + el.FontWeight = FontWeights.Normal; + + if (node.IsItalic == true) + el.FontStyle |= FontStyle.Italic; + else if (node.IsItalic == false) + el.TextDecorations |= ~TextDecorations.Underline; + + if (node.IsUnderline == true) + el.TextDecorations |= TextDecorations.Underline; + else if (node.IsUnderline == false) + el.TextDecorations &= ~TextDecorations.Underline; + + if (node.FontColor.HasValue) + el.Foreground = new SolidColorBrush() { Color = ConvertColor(node.FontColor.Value) }; + // if (node.BackColor.HasValue) + // el.BackgroundColor = new SolidColorBrush() { Color = ConvertColor(node.BackColor.Value) }; + if (node.FontSize.HasValue) + el.FontSize = 16d * node.FontSize.Value; // based on AGOL's default font size + } + + private static void ApplyStyle(TextBlock el, MarkupNode node) + { + if (node.IsBold == true) + el.FontWeight = FontWeights.Bold; + else if (node.IsBold == false) + el.FontWeight = FontWeights.Normal; + + if (node.IsItalic == true) + el.FontStyle |= FontStyle.Italic; + else if (node.IsItalic == false) + el.FontStyle |= ~FontStyle.Italic; + + if (node.IsUnderline == true) + el.TextDecorations |= TextDecorations.Underline; + else if (node.IsUnderline == false) + el.TextDecorations |= ~TextDecorations.Underline; + + if (node.FontColor.HasValue) + el.Foreground = new SolidColorBrush() { Color = ConvertColor(node.FontColor.Value) }; + // if (node.BackColor.HasValue) + // el.Background = new SolidColorBrush() { ConvertColor(node.BackColor.Value) }; + if (node.FontSize.HasValue) + el.FontSize = 16d * node.FontSize.Value; // based on AGOL's default font size + + if (node.Alignment.HasValue) + el.HorizontalTextAlignment = ConvertAlignment(node.Alignment); + } + + private static TextAlignment ConvertAlignment(HtmlAlignment? alignment) => alignment switch + { + HtmlAlignment.Left => TextAlignment.Start, + HtmlAlignment.Center => TextAlignment.Center, + HtmlAlignment.Right => TextAlignment.End, + _ => TextAlignment.Start, + }; + + private static Windows.UI.Color ConvertColor(System.Drawing.Color color) + { + return Windows.UI.Color.FromArgb(color.A, color.R, color.G, color.B); + } + + private static bool HasAnyBlocks(MarkupNode node) + { + return node.Children.Any(c => MapsToBlock(c) || HasAnyBlocks(c)); + } + + private static bool MapsToBlock(MarkupNode node) + { + return node.Type is MarkupType.List or MarkupType.Table or MarkupType.Block or MarkupType.Divider or MarkupType.Image; + } + + private static void NavigateToUri(Hyperlink sender, HyperlinkClickEventArgs args) + { + PopupViewer.GetPopupViewerParent(sender)?.OnHyperlinkClicked(sender.NavigateUri); + } + } +} +#endif \ No newline at end of file diff --git a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs index 6cbb0409e..094965f4a 100644 --- a/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs +++ b/src/Toolkit/Toolkit/UI/Controls/PopupViewer/TextPopupElementView.cs @@ -14,7 +14,6 @@ // * limitations under the License. // ******************************************************************************/ -#if WPF || MAUI using Esri.ArcGISRuntime.Mapping.Popups; using Esri.ArcGISRuntime.Toolkit.Internal; using Esri.ArcGISRuntime.UI; @@ -48,7 +47,7 @@ public TextPopupElementView() /// #if WINDOWS_XAML || MAUI protected override void OnApplyTemplate() -#else +#elif WPF public override void OnApplyTemplate() #endif { @@ -71,5 +70,4 @@ public TextPopupElement? Element public static readonly DependencyProperty ElementProperty = PropertyHelper.CreateProperty(nameof(Element), null, (s, oldValue, newValue) => s.OnElementPropertyChanged()); } -} -#endif \ No newline at end of file +} \ No newline at end of file