Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 206 additions & 0 deletions src/GitHub.UI/Controls/ScrollingVerticalStackPanel.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// A vertical stack panel which implements its own logical scrolling, allowing controls to be
/// fixed horizontally in the scroll area.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class ScrollingVerticalStackPanel : Panel, IScrollInfo
{
const int lineSize = 16;
const int mouseWheelSize = 48;

/// <summary>
/// Attached property which when set to True on a child control, will cause it to be fixed
/// horizontally within the scrollable viewport.
/// </summary>
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();
}
}
}
1 change: 1 addition & 0 deletions src/GitHub.UI/GitHub.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<DesignTime>True</DesignTime>
<DependentUpon>OcticonPaths.resx</DependentUpon>
</Compile>
<Compile Include="Controls\ScrollingVerticalStackPanel.cs" />
<Compile Include="Controls\SectionControl.cs" />
<Compile Include="Controls\Spinner.xaml.cs">
<DependentUpon>Spinner.xaml</DependentUpon>
Expand Down
55 changes: 24 additions & 31 deletions src/GitHub.VisualStudio/Views/GitHubPane/PullRequestDetailView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,26 +162,15 @@

<Rectangle DockPanel.Dock="Top" Style="{StaticResource Separator}" Height="2" Margin="-8,5,-8,0"/>

<ScrollViewer VerticalScrollBarVisibility="Auto" Margin="0,0,-8,0">
<Grid Name="bodyGrid" Margin="0 5 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<Grid.RowDefinitions>
<!-- Author and open time -->
<RowDefinition Height="Auto"/>
<!-- PR body -->
<RowDefinition Height="Auto"/>
<!-- View on github link -->
<RowDefinition Height="Auto"/>
<!-- Reviewers -->
<RowDefinition Height="Auto"/>
<!-- Files changed -->
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0 -4 0 0">
<ScrollViewer CanContentScroll="True"
Margin="0,0,-8,0"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ghfvs:ScrollingVerticalStackPanel>

<StackPanel Orientation="Horizontal"
Margin="0 -4 0 0"
ghfvs:ScrollingVerticalStackPanel.IsFixed="true">
<ghfvs:GitHubActionLink Margin="0 6" Command="{Binding OpenOnGitHub}">
View on GitHub
</ghfvs:GitHubActionLink>
Expand Down Expand Up @@ -255,9 +244,9 @@

<!-- Author and open time -->
<ghfvs:SectionControl Name="descriptionSection"
HeaderText="Description"
IsExpanded="True"
Grid.Row="1">
HeaderText="Description"
IsExpanded="True"
ghfvs:ScrollingVerticalStackPanel.IsFixed="true">
<StackPanel Orientation="Vertical">
<!-- View conversation on GitHub -->
<StackPanel Orientation="Horizontal" Margin="0 4 0 0">
Expand All @@ -282,7 +271,7 @@
HeaderText="Reviewers"
IsExpanded="True"
Margin="0 8 0 0"
Grid.Row="2">
ghfvs:ScrollingVerticalStackPanel.IsFixed="true">
<ItemsControl ItemsSource="{Binding Reviews}" Margin="0 4 12 4">
<ItemsControl.ItemTemplate>
<DataTemplate>
Expand All @@ -294,13 +283,17 @@

<!-- Files changed -->
<ghfvs:SectionControl Name="changesSection"
Grid.Row="4"
IsExpanded="True"
HeaderText="{Binding Files.ChangedFilesCount, StringFormat={x:Static prop:Resources.ChangesCountFormat}}"
Margin="0 8 10 0">
<local:PullRequestFilesView DataContext="{Binding Files}"/>
</ghfvs:SectionControl>
</Grid>
Grid.Row="4"
IsExpanded="True"
HeaderText="{Binding Files.ChangedFilesCount, StringFormat={x:Static prop:Resources.ChangesCountFormat}}"
Margin="0 8 10 0"
ghfvs:ScrollingVerticalStackPanel.IsFixed="true"/>

<!-- Put the changes tree outside its expander, so it can scroll horizontally
while the header remains fixed -->
<local:PullRequestFilesView DataContext="{Binding Files}"
Visibility="{Binding ElementName=changesSection, Path=IsExpanded, Converter={ghfvs:BooleanToVisibilityConverter}}"/>
</ghfvs:ScrollingVerticalStackPanel>
</ScrollViewer>
</DockPanel>
</local:GenericPullRequestDetailView>
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -17,6 +18,7 @@ public partial class PullRequestFilesView : UserControl
public PullRequestFilesView()
{
InitializeComponent();
PreviewMouseWheel += ScrollViewerUtilities.FixMouseWheelScroll;
}

protected override void OnMouseDown(MouseButtonEventArgs e)
Expand Down