File: PreviewPane\PreviewPane.xaml.cs
Web Access
Project: ..\..\..\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_ckcrqypr_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Navigation;
using Microsoft.CodeAnalysis.Diagnostics.Log;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.Editor.Implementation.Preview;
using IVsUIShell = Microsoft.VisualStudio.Shell.Interop.IVsUIShell;
using OLECMDEXECOPT = Microsoft.VisualStudio.OLE.Interop.OLECMDEXECOPT;
using Microsoft.VisualStudio.Text.Differencing;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.PreviewPane
{
    internal partial class PreviewPane : UserControl, IDisposable
    {
        private const double DefaultWidth = 400;
 
        private static readonly string s_dummyThreeLineTitle = "A" + Environment.NewLine + "A" + Environment.NewLine + "A";
        private static readonly Size s_infiniteSize = new(double.PositiveInfinity, double.PositiveInfinity);
 
        private readonly string _id;
        private readonly bool _logIdVerbatimInTelemetry;
        private readonly IVsUIShell _uiShell;
 
        private bool _isExpanded;
        private readonly double _heightForThreeLineTitle;
        private DifferenceViewerPreview _differenceViewerPreview;
 
        public PreviewPane(
            Image severityIcon,
            string id,
            string title,
            string description,
            Uri helpLink,
            string helpLinkToolTipText,
            IReadOnlyList<object> previewContent,
            bool logIdVerbatimInTelemetry,
            IVsUIShell uiShell,
            Guid optionPageGuid = default)
        {
            InitializeComponent();
 
            Loaded += PreviewPane_Loaded;
 
            _id = id;
            _logIdVerbatimInTelemetry = logIdVerbatimInTelemetry;
            _uiShell = uiShell;
 
            // Initialize header portion.
            if ((severityIcon != null) && !string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(title))
            {
                HeaderStackPanel.Visibility = Visibility.Visible;
 
                SeverityIconBorder.Child = severityIcon;
 
                // Set the initial title text to three lines worth so that we can measure the pixel height
                // that WPF requires to render three lines of text under the current font and DPI settings.
                TitleRun.Text = s_dummyThreeLineTitle;
                TitleTextBlock.Measure(availableSize: s_infiniteSize);
                _heightForThreeLineTitle = TitleTextBlock.DesiredSize.Height;
 
                // Now set the actual title text.
                TitleRun.Text = title;
 
                if (helpLink != null)
                {
                    Contract.ThrowIfNull(helpLinkToolTipText);
                    InitializeHyperlinks(helpLink, helpLinkToolTipText);
                }
 
                if (!string.IsNullOrWhiteSpace(description))
                {
                    DescriptionParagraph.Inlines.Add(description);
                }
            }
 
            _optionPageGuid = optionPageGuid;
 
            if (optionPageGuid == default)
            {
                OptionsButton.Visibility = Visibility.Collapsed;
            }
 
            // Initialize preview (i.e. diff view) portion.
            InitializePreviewElement(previewContent);
        }
 
        public FrameworkElement ParentElement
        {
            get { return (FrameworkElement)GetValue(ParentElementProperty); }
            set { SetValue(ParentElementProperty, value); }
        }
 
        public static readonly DependencyProperty ParentElementProperty =
            DependencyProperty.Register("ParentElement", typeof(FrameworkElement), typeof(PreviewPane), new PropertyMetadata(null));
 
        private void PreviewPane_Loaded(object sender, RoutedEventArgs e)
        {
            Loaded -= PreviewPane_Loaded;
            ParentElement = Parent as FrameworkElement;
        }
 
        public string AutomationName => ServicesVSResources.Preview_pane;
 
        private void InitializePreviewElement(IReadOnlyList<object> previewItems)
        {
            var previewElement = CreatePreviewElement(previewItems);
 
            if (previewElement != null)
            {
                HeaderSeparator.Visibility = Visibility.Visible;
                PreviewDockPanel.Visibility = Visibility.Visible;
                PreviewScrollViewer.Content = previewElement;
                previewElement.VerticalAlignment = VerticalAlignment.Top;
                previewElement.HorizontalAlignment = HorizontalAlignment.Left;
            }
 
            // 1. Width of the header section should not exceed the width of the preview content.
            // In other words, the text we put in the header at the top of the preview pane
            // should not cause the preview pane itself to grow wider than the width required to
            // display the preview content at the bottom of the pane.
            // 2. Adjust the height of the header section so that it displays only three lines worth
            // by default.
            AdjustWidthAndHeight(previewElement);
        }
 
        private FrameworkElement CreatePreviewElement(IReadOnlyList<object> previewItems)
        {
            if (previewItems == null || previewItems.Count == 0)
            {
                return null;
            }
 
            const int MaxItems = 3;
            if (previewItems.Count > MaxItems)
            {
                previewItems = previewItems.Take(MaxItems).Concat("...").ToList();
            }
 
            var grid = new Grid();
 
            foreach (var previewItem in previewItems)
            {
                var previewElement = GetPreviewElement(previewItem);
 
                // no preview element
                if (previewElement == null)
                {
                    continue;
                }
 
                // the very first preview
                if (grid.RowDefinitions.Count == 0)
                {
                    grid.RowDefinitions.Add(new RowDefinition());
                }
                else
                {
                    grid.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto });
                }
 
                // set row position of the element
                Grid.SetRow(previewElement, grid.RowDefinitions.Count - 1);
 
                // add the element to the grid
                grid.Children.Add(previewElement);
 
                // set width of the grid same as the first element
                if (grid.Children.Count == 1)
                {
                    grid.Width = previewElement.Width;
                }
            }
 
            if (grid.Children.Count == 0)
            {
                // no preview
                return null;
            }
 
            // if there is only 1 item, just take preview element as it is without grid
            if (grid.Children.Count == 1)
            {
                var preview = grid.Children[0];
 
                // we need to take it out from visual tree
                grid.Children.Clear();
 
                return (FrameworkElement)preview;
            }
 
            return grid;
        }
 
        private FrameworkElement GetPreviewElement(object previewItem)
        {
            if (previewItem is DifferenceViewerPreview)
            {
                // Contract is there should be only 1 diff viewer, otherwise we leak.
                Contract.ThrowIfFalse(_differenceViewerPreview == null);
 
                // cache the diff viewer so that we can close it when panel goes away.
                // this is a bit weird since we are mutating state here.
                _differenceViewerPreview = (DifferenceViewerPreview)previewItem;
                var viewer = (IWpfDifferenceViewer)_differenceViewerPreview.Viewer;
                PreviewDockPanel.Background = viewer.InlineView.Background;
 
                var previewElement = viewer.VisualElement;
                return previewElement;
            }
 
            if (previewItem is string s)
            {
                return GetPreviewForString(s);
            }
 
            if (previewItem is FrameworkElement frameworkElement)
            {
                return frameworkElement;
            }
 
            // preview item we don't know how to show to users
            return null;
        }
 
        private void InitializeHyperlinks(Uri helpLink, string helpLinkToolTipText)
        {
            IdHyperlink.Inlines.Add(_id);
            IdHyperlink.NavigateUri = helpLink;
            IdHyperlink.IsEnabled = true;
            IdHyperlink.ToolTip = helpLinkToolTipText;
 
            LearnMoreHyperlink.Inlines.Add(string.Format(ServicesVSResources.More_about_0, _id));
            LearnMoreHyperlink.NavigateUri = helpLink;
            LearnMoreHyperlink.ToolTip = helpLinkToolTipText;
        }
 
        private static FrameworkElement GetPreviewForString(string previewContent)
        {
            return new TextBlock()
            {
                Margin = new Thickness(5),
                VerticalAlignment = VerticalAlignment.Center,
                Text = previewContent,
                TextWrapping = TextWrapping.Wrap,
            };
        }
 
        // This method adjusts the width of the header section to match that of the preview content
        // thereby ensuring that the preview pane itself is never wider than the preview content.
        // This method also adjusts the height of the header section so that it displays only three lines
        // worth by default.
        private void AdjustWidthAndHeight(FrameworkElement previewElement)
        {
            double headerStackPanelWidth;
            var titleTextBlockHeight = double.PositiveInfinity;
            if (previewElement == null)
            {
                HeaderStackPanel.Measure(availableSize: s_infiniteSize);
                headerStackPanelWidth = HeaderStackPanel.DesiredSize.Width;
 
                TitleTextBlock.Measure(availableSize: s_infiniteSize);
                titleTextBlockHeight = TitleTextBlock.DesiredSize.Height;
            }
            else
            {
                PreviewDockPanel.Measure(availableSize: new Size(
                    double.IsNaN(previewElement.Width) ? DefaultWidth : previewElement.Width,
                    double.PositiveInfinity));
                headerStackPanelWidth = PreviewDockPanel.DesiredSize.Width;
                if (IsNormal(headerStackPanelWidth))
                {
                    TitleTextBlock.Measure(availableSize: new Size(headerStackPanelWidth, double.PositiveInfinity));
                    titleTextBlockHeight = TitleTextBlock.DesiredSize.Height;
                }
            }
 
            if (IsNormal(headerStackPanelWidth))
            {
                HeaderStackPanel.Width = headerStackPanelWidth;
            }
 
            // If the pixel height required to render the complete title in the
            // TextBlock is larger than that required to render three lines worth,
            // then trim the contents of the TextBlock with an ellipsis at the end and
            // display the expander button that will allow users to view the full text.
            if (HasDescription || (IsNormal(titleTextBlockHeight) && (titleTextBlockHeight > _heightForThreeLineTitle)))
            {
                TitleTextBlock.MaxHeight = _heightForThreeLineTitle;
                TitleTextBlock.TextTrimming = TextTrimming.CharacterEllipsis;
 
                ExpanderToggleButton.Visibility = Visibility.Visible;
 
                if (_isExpanded)
                {
                    ExpanderToggleButton.IsChecked = true;
                }
            }
        }
 
        private static bool IsNormal(double size)
            => size > 0 && !double.IsNaN(size) && !double.IsInfinity(size);
 
        private bool HasDescription
        {
            get
            {
                return DescriptionParagraph.Inlines.Count > 0;
            }
        }
 
        private readonly Guid _optionPageGuid;
        void IDisposable.Dispose()
        {
            // VS editor will call Dispose at which point we should Close() the embedded IWpfDifferenceViewer.
            _differenceViewerPreview?.Dispose();
            _differenceViewerPreview = null;
        }
 
        private void LearnMoreHyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
        {
            if (e.Uri == null)
            {
                return;
            }
 
            VisualStudioNavigateToLinkService.StartBrowser(e.Uri);
            e.Handled = true;
 
            if (sender is not Hyperlink hyperlink)
            {
                return;
            }
 
            DiagnosticLogger.LogHyperlink(hyperlink.Name ?? "Preview", _id, HasDescription, _logIdVerbatimInTelemetry, e.Uri.AbsoluteUri);
        }
 
        private void ExpanderToggleButton_CheckedChanged(object sender, RoutedEventArgs e)
        {
            if (ExpanderToggleButton.IsChecked ?? false)
            {
                TitleTextBlock.MaxHeight = double.PositiveInfinity;
                TitleTextBlock.TextTrimming = TextTrimming.None;
 
                if (HasDescription)
                {
                    DescriptionDockPanel.Visibility = Visibility.Visible;
 
                    if (LearnMoreHyperlink.NavigateUri != null)
                    {
                        LearnMoreTextBlock.Visibility = Visibility.Visible;
                        LearnMoreHyperlink.IsEnabled = true;
                    }
                }
 
                _isExpanded = true;
            }
            else
            {
                TitleTextBlock.MaxHeight = _heightForThreeLineTitle;
                TitleTextBlock.TextTrimming = TextTrimming.CharacterEllipsis;
 
                DescriptionDockPanel.Visibility = Visibility.Collapsed;
 
                _isExpanded = false;
            }
        }
 
        private void OptionsButton_Click(object sender, RoutedEventArgs e)
        {
            if (_optionPageGuid != default)
            {
                ErrorHandler.ThrowOnFailure(_uiShell.PostExecCommand(
                    VSConstants.GUID_VSStandardCommandSet97,
                    (uint)VSConstants.VSStd97CmdID.ToolsOptions,
                    (uint)OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT,
                    _optionPageGuid.ToString()));
            }
        }
    }
}