diff --git a/src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs b/src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs new file mode 100644 index 0000000000..5e423ba8c1 --- /dev/null +++ b/src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs @@ -0,0 +1,206 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Media; + +namespace GitHub.UI.Controls +{ + /// + /// A vertical stack panel which implements its own logical scrolling, allowing controls to be + /// fixed horizontally in the scroll area. + /// + /// + /// This panel is needed by the PullRequestDetailsView because of #1698: there is no default + /// panel in WPF which allows the horizontal scrollbar to always be present at the bottom while + /// also making the PR description etc be fixed horizontally (non-scrollable) in the viewport. + /// + public class ScrollingVerticalStackPanel : Panel, IScrollInfo + { + const int lineSize = 16; + const int mouseWheelSize = 48; + + /// + /// Attached property which when set to True on a child control, will cause it to be fixed + /// horizontally within the scrollable viewport. + /// + public static readonly DependencyProperty IsFixedProperty = + DependencyProperty.RegisterAttached( + "IsFixed", + typeof(bool), + typeof(ScrollingVerticalStackPanel), + new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure)); + + public bool CanHorizontallyScroll + { + get { return true; } + set { } + } + + public bool CanVerticallyScroll + { + get { return true; } + set { } + } + + public double ExtentHeight { get; private set; } + public double ExtentWidth { get; private set; } + public double HorizontalOffset { get; private set; } + public double VerticalOffset { get; private set; } + public double ViewportHeight { get; private set; } + public double ViewportWidth { get; private set; } + public ScrollViewer ScrollOwner { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Can only be applied to controls")] + public static bool GetIsFixed(FrameworkElement control) + { + return (bool)control.GetValue(IsFixedProperty); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Can only be applied to controls")] + public static void SetIsFixed(FrameworkElement control, bool value) + { + control.SetValue(IsFixedProperty, value); + } + + public void LineDown() => SetVerticalOffset(VerticalOffset + lineSize); + public void LineLeft() => SetHorizontalOffset(HorizontalOffset - lineSize); + public void LineRight() => SetHorizontalOffset(HorizontalOffset + lineSize); + public void LineUp() => SetVerticalOffset(VerticalOffset - lineSize); + public void MouseWheelDown() => SetVerticalOffset(VerticalOffset + mouseWheelSize); + public void MouseWheelLeft() => SetHorizontalOffset(HorizontalOffset - mouseWheelSize); + public void MouseWheelRight() => SetHorizontalOffset(HorizontalOffset + mouseWheelSize); + public void MouseWheelUp() => SetVerticalOffset(VerticalOffset - mouseWheelSize); + public void PageDown() => SetVerticalOffset(VerticalOffset + ViewportHeight); + public void PageLeft() => SetHorizontalOffset(HorizontalOffset - ViewportWidth); + public void PageRight() => SetHorizontalOffset(HorizontalOffset + ViewportWidth); + public void PageUp() => SetVerticalOffset(VerticalOffset - ViewportHeight); + + public Rect MakeVisible(Visual visual, Rect rectangle) + { + var transform = visual.TransformToVisual(this); + var rect = transform.TransformBounds(rectangle); + var offsetX = HorizontalOffset; + var offsetY = VerticalOffset; + + if (rect.Bottom > ViewportHeight) + { + var delta = rect.Bottom - ViewportHeight; + offsetY += delta; + rect.Y -= delta; + } + + if (rect.Y < 0) + { + offsetY += rect.Y; + } + + // We technially should be trying to also show the right-hand side of the rect here + // using the same technique that we just used to show the bottom of the rect above, + // but in the case of the PR details view, the left hand side of the item is much + // more important than the right hand side and it actually feels better to not do + // this. If this control is used elsewhere and this behavior is required, we could + // put in a switch to enable it. + + if (rect.X < 0) + { + offsetX += rect.X; + } + + SetHorizontalOffset(offsetX); + SetVerticalOffset(offsetY); + + return rect; + } + + public void SetHorizontalOffset(double offset) + { + var value = Math.Max(0, Math.Min(offset, ExtentWidth - ViewportWidth)); + + if (value != HorizontalOffset) + { + HorizontalOffset = value; + InvalidateArrange(); + } + } + + public void SetVerticalOffset(double offset) + { + var value = Math.Max(0, Math.Min(offset, ExtentHeight - ViewportHeight)); + + if (value != VerticalOffset) + { + VerticalOffset = value; + InvalidateArrange(); + } + } + + protected override void ParentLayoutInvalidated(UIElement child) + { + base.ParentLayoutInvalidated(child); + } + + protected override Size MeasureOverride(Size availableSize) + { + var maxWidth = 0.0; + var height = 0.0; + + foreach (FrameworkElement child in Children) + { + var isFixed = GetIsFixed(child); + var childConstraint = new Size( + isFixed ? availableSize.Width : double.PositiveInfinity, + double.PositiveInfinity); + child.Measure(childConstraint); + + if (height - VerticalOffset < availableSize.Height) + { + maxWidth = Math.Max(maxWidth, child.DesiredSize.Width); + } + + height += child.DesiredSize.Height; + } + + UpdateScrollInfo(new Size(maxWidth, height), availableSize); + + return new Size( + Math.Min(maxWidth, availableSize.Width), + Math.Min(height, availableSize.Height)); + } + + protected override Size ArrangeOverride(Size finalSize) + { + var y = -VerticalOffset; + var thisRect = new Rect(finalSize); + var visibleMaxWidth = 0.0; + + foreach (FrameworkElement child in Children) + { + var isFixed = GetIsFixed(child); + var x = isFixed ? 0 : -HorizontalOffset; + var childRect = new Rect(x, y, child.DesiredSize.Width, child.DesiredSize.Height); + child.Arrange(childRect); + y += child.DesiredSize.Height; + + if (childRect.IntersectsWith(thisRect) && childRect.Right > visibleMaxWidth) + { + visibleMaxWidth = childRect.Right; + } + } + + UpdateScrollInfo(new Size(visibleMaxWidth, ExtentHeight), new Size(finalSize.Width, finalSize.Height)); + return finalSize; + } + + void UpdateScrollInfo(Size extent, Size viewport) + { + ExtentWidth = extent.Width; + ExtentHeight = extent.Height; + ScrollOwner?.InvalidateScrollInfo(); + ViewportWidth = viewport.Width; + ViewportHeight = viewport.Height; + ScrollOwner?.InvalidateScrollInfo(); + } + } +} diff --git a/src/GitHub.UI/GitHub.UI.csproj b/src/GitHub.UI/GitHub.UI.csproj index b5e6c4b839..ae7d07f227 100644 --- a/src/GitHub.UI/GitHub.UI.csproj +++ b/src/GitHub.UI/GitHub.UI.csproj @@ -85,6 +85,7 @@ True OcticonPaths.resx + Spinner.xaml diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml index 85419b6c70..67f493c3a5 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml @@ -162,26 +162,15 @@ - - - - - - - - - - - - - - - - - - - - + + + + View on GitHub @@ -255,9 +244,9 @@ + HeaderText="Description" + IsExpanded="True" + ghfvs:ScrollingVerticalStackPanel.IsFixed="true"> @@ -282,7 +271,7 @@ HeaderText="Reviewers" IsExpanded="True" Margin="0 8 0 0" - Grid.Row="2"> + ghfvs:ScrollingVerticalStackPanel.IsFixed="true"> @@ -294,13 +283,17 @@ - - - + Grid.Row="4" + IsExpanded="True" + HeaderText="{Binding Files.ChangedFilesCount, StringFormat={x:Static prop:Resources.ChangesCountFormat}}" + Margin="0 8 10 0" + ghfvs:ScrollingVerticalStackPanel.IsFixed="true"/> + + + + \ No newline at end of file diff --git a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs index 2f2956ef45..9b87f84b8c 100644 --- a/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs +++ b/src/GitHub.VisualStudio/Views/GitHubPane/PullRequestFilesView.xaml.cs @@ -7,6 +7,7 @@ using GitHub.Exports; using GitHub.UI.Helpers; using GitHub.ViewModels.GitHubPane; +using GitHub.VisualStudio.UI.Helpers; namespace GitHub.VisualStudio.Views.GitHubPane { @@ -17,6 +18,7 @@ public partial class PullRequestFilesView : UserControl public PullRequestFilesView() { InitializeComponent(); + PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll; } protected override void OnMouseDown(MouseButtonEventArgs e)