diff --git a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs index 722af9e3af..8eab91e06d 100644 --- a/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs +++ b/samples/VirtualizationTest/ViewModels/MainWindowViewModel.cs @@ -18,8 +18,8 @@ namespace VirtualizationTest.ViewModels private int _newItemIndex; private IReactiveList _items; private string _prefix = "Item"; - private ScrollBarVisibility _horizontalScrollBarVisibility; - private ScrollBarVisibility _verticalScrollBarVisibility; + private ScrollBarVisibility _horizontalScrollBarVisibility = ScrollBarVisibility.Auto; + private ScrollBarVisibility _verticalScrollBarVisibility = ScrollBarVisibility.Auto; private Orientation _orientation = Orientation.Vertical; private ItemVirtualizationMode _virtualizationMode = ItemVirtualizationMode.Simple; diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 20602d5475..c1489e7138 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Specialized; using System.Linq; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Layout; @@ -97,6 +98,7 @@ namespace Avalonia.Controls.Presenters /// public override Size MeasureOverride(Size availableSize) { + var scrollable = (ILogicalScrollable)Owner; var visualRoot = Owner.GetVisualRoot(); var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxClientSize ?? (visualRoot as TopLevel)?.ClientSize; @@ -115,7 +117,10 @@ namespace Avalonia.Controls.Presenters } } - availableSize = availableSize.WithWidth(double.PositiveInfinity); + if (scrollable.CanHorizontallyScroll) + { + availableSize = availableSize.WithWidth(double.PositiveInfinity); + } } else { @@ -127,7 +132,10 @@ namespace Avalonia.Controls.Presenters } } - availableSize = availableSize.WithHeight(double.PositiveInfinity); + if (scrollable.CanVerticallyScroll) + { + availableSize = availableSize.WithHeight(double.PositiveInfinity); + } } Owner.Panel.Measure(availableSize); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 185193f889..590bfa25ac 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -23,6 +23,8 @@ namespace Avalonia.Controls.Presenters defaultValue: ItemVirtualizationMode.None); private ItemVirtualizer _virtualizer; + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; /// /// Initializes static members of the class. @@ -46,6 +48,31 @@ namespace Avalonia.Controls.Presenters set { SetValue(VirtualizationModeProperty, value); } } + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + bool ILogicalScrollable.CanHorizontallyScroll + { + get { return _canHorizontallyScroll; } + set + { + _canHorizontallyScroll = value; + InvalidateMeasure(); + } + } + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + bool ILogicalScrollable.CanVerticallyScroll + { + get { return _canVerticallyScroll; } + set + { + _canVerticallyScroll = value; + InvalidateMeasure(); + } + } /// bool ILogicalScrollable.IsLogicalScrollEnabled { diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index e41c4e1e28..6c61375054 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -17,6 +17,24 @@ namespace Avalonia.Controls.Presenters /// public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable { + /// + /// Defines the property. + /// + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll, + (o, v) => o.CanHorizontallyScroll = v); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll, + (o, v) => o.CanVerticallyScroll = v); + /// /// Defines the property. /// @@ -41,12 +59,8 @@ namespace Avalonia.Controls.Presenters o => o.Viewport, (o, v) => o.Viewport = v); - /// - /// Defines the property. - /// - public static readonly StyledProperty CanScrollHorizontallyProperty = - ScrollViewer.CanScrollHorizontallyProperty.AddOwner(); - + private bool _canHorizontallyScroll; + private bool _canVerticallyScroll; private Size _extent; private Size _measuredExtent; private Vector _offset; @@ -73,6 +87,24 @@ namespace Avalonia.Controls.Presenters this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanHorizontallyScroll + { + get { return _canHorizontallyScroll; } + set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); } + } + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + public bool CanVerticallyScroll + { + get { return _canVerticallyScroll; } + set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); } + } + /// /// Gets the extent of the scrollable content. /// @@ -100,11 +132,6 @@ namespace Avalonia.Controls.Presenters private set { SetAndRaise(ViewportProperty, ref _viewport, value); } } - /// - /// Gets a value indicating whether the content can be scrolled horizontally. - /// - public bool CanScrollHorizontally => GetValue(CanScrollHorizontallyProperty); - /// /// Attempts to bring a portion of the target visual into view by scrolling the content. /// @@ -182,10 +209,15 @@ namespace Avalonia.Controls.Presenters { measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); - if (!CanScrollHorizontally) + if (!CanHorizontallyScroll) { measureSize = measureSize.WithWidth(availableSize.Width); } + + if (!CanVerticallyScroll) + { + measureSize = measureSize.WithHeight(availableSize.Height); + } } child.Measure(measureSize); @@ -289,7 +321,12 @@ namespace Avalonia.Controls.Presenters if (scrollable.IsLogicalScrollEnabled == true) { _logicalScrollSubscription = new CompositeDisposable( - this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), + this.GetObservable(CanHorizontallyScrollProperty) + .Subscribe(x => scrollable.CanHorizontallyScroll = x), + this.GetObservable(CanVerticallyScrollProperty) + .Subscribe(x => scrollable.CanVerticallyScroll = x), + this.GetObservable(OffsetProperty) + .Skip(1).Subscribe(x => scrollable.Offset = x), Disposable.Create(() => scrollable.InvalidateScroll = null)); UpdateFromScrollable(scrollable); } diff --git a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs index 6c8f463a96..490beb12b3 100644 --- a/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs +++ b/src/Avalonia.Controls/Primitives/ILogicalScrollable.cs @@ -19,6 +19,16 @@ namespace Avalonia.Controls.Primitives /// public interface ILogicalScrollable : IScrollable { + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + bool CanHorizontallyScroll { get; set; } + + /// + /// Gets or sets a value indicating whether the content can be scrolled horizontally. + /// + bool CanVerticallyScroll { get; set; } + /// /// Gets a value indicating whether logical scrolling is enabled on the control. /// diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 0442652540..39854e0071 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; @@ -15,16 +13,34 @@ namespace Avalonia.Controls public class ScrollViewer : ContentControl, IScrollable { /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty CanScrollHorizontallyProperty = - AvaloniaProperty.Register(nameof(CanScrollHorizontally), true); + /// + /// There is no public C# accessor for this property as it is intended to be bound to by a + /// in the control's template. + /// + public static readonly DirectProperty CanHorizontallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanHorizontallyScroll), + o => o.CanHorizontallyScroll); + + /// + /// Defines the property. + /// + /// + /// There is no public C# accessor for this property as it is intended to be bound to by a + /// in the control's template. + /// + public static readonly DirectProperty CanVerticallyScrollProperty = + AvaloniaProperty.RegisterDirect( + nameof(CanVerticallyScroll), + o => o.CanVerticallyScroll); /// /// Defines the property. /// public static readonly DirectProperty ExtentProperty = - AvaloniaProperty.RegisterDirect(nameof(Extent), + AvaloniaProperty.RegisterDirect(nameof(Extent), o => o.Extent, (o, v) => o.Extent = v); @@ -41,7 +57,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly DirectProperty ViewportProperty = - AvaloniaProperty.RegisterDirect(nameof(Viewport), + AvaloniaProperty.RegisterDirect(nameof(Viewport), o => o.Viewport, (o, v) => o.Viewport = v); @@ -85,14 +101,10 @@ namespace Avalonia.Controls /// /// Defines the property. /// - /// - /// There is no public C# accessor for this property as it is intended to be bound to by a - /// in the control's template. - /// public static readonly AttachedProperty HorizontalScrollBarVisibilityProperty = AvaloniaProperty.RegisterAttached( nameof(HorizontalScrollBarVisibility), - ScrollBarVisibility.Auto); + ScrollBarVisibility.Hidden); /// /// Defines the VerticalScrollBarMaximum property. @@ -136,7 +148,7 @@ namespace Avalonia.Controls /// public static readonly AttachedProperty VerticalScrollBarVisibilityProperty = AvaloniaProperty.RegisterAttached( - nameof(VerticalScrollBarVisibility), + nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); private Size _extent; @@ -150,6 +162,8 @@ namespace Avalonia.Controls { AffectsValidation(ExtentProperty, OffsetProperty); AffectsValidation(ViewportProperty, OffsetProperty); + HorizontalScrollBarVisibilityProperty.Changed.AddClassHandler(x => x.ScrollBarVisibilityChanged); + VerticalScrollBarVisibilityProperty.Changed.AddClassHandler(x => x.ScrollBarVisibilityChanged); } /// @@ -218,15 +232,6 @@ namespace Avalonia.Controls } } - /// - /// Gets a value indicating whether the content can be scrolled horizontally. - /// - public bool CanScrollHorizontally - { - get { return GetValue(CanScrollHorizontallyProperty); } - set { SetValue(CanScrollHorizontallyProperty, value); } - } - /// /// Gets or sets the horizontal scrollbar visibility. /// @@ -245,6 +250,22 @@ namespace Avalonia.Controls set { SetValue(VerticalScrollBarVisibilityProperty, value); } } + /// + /// Gets a value indicating whether the viewer can scroll horizontally. + /// + protected bool CanHorizontallyScroll + { + get { return HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled; } + } + + /// + /// Gets a value indicating whether the viewer can scroll vertically. + /// + protected bool CanVerticallyScroll + { + get { return VerticalScrollBarVisibility != ScrollBarVisibility.Disabled; } + } + /// /// Gets the maximum horizontal scrollbar value. /// @@ -316,7 +337,7 @@ namespace Avalonia.Controls /// /// The control to read the value from. /// The value of the property. - public ScrollBarVisibility GetHorizontalScrollBarVisibility(Control control) + public static ScrollBarVisibility GetHorizontalScrollBarVisibility(Control control) { return control.GetValue(HorizontalScrollBarVisibilityProperty); } @@ -326,7 +347,7 @@ namespace Avalonia.Controls /// /// The control to set the value on. /// The value of the property. - public void SetHorizontalScrollBarVisibility(Control control, ScrollBarVisibility value) + public static void SetHorizontalScrollBarVisibility(Control control, ScrollBarVisibility value) { control.SetValue(HorizontalScrollBarVisibilityProperty, value); } @@ -336,7 +357,7 @@ namespace Avalonia.Controls /// /// The control to read the value from. /// The value of the property. - public ScrollBarVisibility GetVerticalScrollBarVisibility(Control control) + public static ScrollBarVisibility GetVerticalScrollBarVisibility(Control control) { return control.GetValue(VerticalScrollBarVisibilityProperty); } @@ -346,7 +367,7 @@ namespace Avalonia.Controls /// /// The control to set the value on. /// The value of the property. - public void SetVerticalScrollBarVisibility(Control control, ScrollBarVisibility value) + public static void SetVerticalScrollBarVisibility(Control control, ScrollBarVisibility value) { control.SetValue(VerticalScrollBarVisibilityProperty, value); } @@ -385,6 +406,30 @@ namespace Avalonia.Controls } } + private void ScrollBarVisibilityChanged(AvaloniaPropertyChangedEventArgs e) + { + var wasEnabled = !ScrollBarVisibility.Disabled.Equals(e.OldValue); + var isEnabled = !ScrollBarVisibility.Disabled.Equals(e.NewValue); + + if (wasEnabled != isEnabled) + { + if (e.Property == HorizontalScrollBarVisibilityProperty) + { + RaisePropertyChanged( + CanHorizontallyScrollProperty, + wasEnabled, + isEnabled); + } + else if (e.Property == VerticalScrollBarVisibilityProperty) + { + RaisePropertyChanged( + CanVerticallyScrollProperty, + wasEnabled, + isEnabled); + } + } + } + private void CalculatedPropertiesChanged() { // Pass old values of 0 here because we don't have the old values at this point, diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 1a663ed3b6..8f4606884e 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Reactive.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Interactivity; @@ -26,9 +25,6 @@ namespace Avalonia.Controls public static readonly StyledProperty AcceptsTabProperty = AvaloniaProperty.Register(nameof(AcceptsTab)); - public static readonly DirectProperty CanScrollHorizontallyProperty = - AvaloniaProperty.RegisterDirect(nameof(CanScrollHorizontally), o => o.CanScrollHorizontally); - public static readonly DirectProperty CaretIndexProperty = AvaloniaProperty.RegisterDirect( nameof(CaretIndex), @@ -92,7 +88,6 @@ namespace Avalonia.Controls private int _caretIndex; private int _selectionStart; private int _selectionEnd; - private bool _canScrollHorizontally; private TextPresenter _presenter; private UndoRedoHelper _undoRedoHelper; private bool _ignoreTextChanges; @@ -106,12 +101,11 @@ namespace Avalonia.Controls public TextBox() { - this.GetObservable(TextWrappingProperty) - .Select(x => x == TextWrapping.NoWrap) - .Subscribe(x => CanScrollHorizontally = x); - - var horizontalScrollBarVisibility = this.GetObservable(AcceptsReturnProperty) - .Select(x => x ? ScrollBarVisibility.Auto : ScrollBarVisibility.Hidden); + var horizontalScrollBarVisibility = Observable.CombineLatest( + this.GetObservable(AcceptsReturnProperty), + this.GetObservable(TextWrappingProperty), + (acceptsReturn, wrapping) => acceptsReturn && wrapping == TextWrapping.NoWrap ? + ScrollBarVisibility.Auto : ScrollBarVisibility.Disabled); Bind( ScrollViewer.HorizontalScrollBarVisibilityProperty, @@ -132,12 +126,6 @@ namespace Avalonia.Controls set { SetValue(AcceptsTabProperty, value); } } - public bool CanScrollHorizontally - { - get { return _canScrollHorizontally; } - private set { SetAndRaise(CanScrollHorizontallyProperty, ref _canScrollHorizontally, value); } - } - public int CaretIndex { get diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 409dd231ad..dee537029c 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -3,11 +3,9 @@ using System; using System.Collections.Specialized; -using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; -using Avalonia.VisualTree; namespace Avalonia.Controls { diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index aa63a1b6c3..57b0c541b8 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -3,11 +3,16 @@ + + - + + Viewport="{TemplateBinding Path=Viewport, Mode=TwoWay}"/> - diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml index 4ce677667b..42c0b2cdd9 100644 --- a/src/Avalonia.Themes.Default/TreeView.xaml +++ b/src/Avalonia.Themes.Default/TreeView.xaml @@ -7,7 +7,7 @@ - + (target.Presenter.Child); } + [Fact] + public void CanHorizontallyScroll_Should_Track_HorizontalScrollBarVisibility() + { + var target = new ScrollViewer(); + var values = new List(); + + target.GetObservable(ScrollViewer.CanHorizontallyScrollProperty).Subscribe(x => values.Add(x)); + target.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + target.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + + Assert.Equal(new[] { true, false, true }, values); + } + + [Fact] + public void CanVerticallyScroll_Should_Track_VerticalScrollBarVisibility() + { + var target = new ScrollViewer(); + var values = new List(); + + target.GetObservable(ScrollViewer.CanVerticallyScrollProperty).Subscribe(x => values.Add(x)); + target.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + target.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + Assert.Equal(new[] { true, false, true }, values); + } + [Fact] public void Offset_Should_Be_Coerced_To_Viewport() { @@ -59,7 +87,7 @@ namespace Avalonia.Controls.UnitTests [~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], - [~ScrollContentPresenter.CanScrollHorizontallyProperty] = control[~ScrollViewer.CanScrollHorizontallyProperty], + [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], }, new ScrollBar { diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 1a07bdc7d1..d4df32a4b3 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -85,7 +85,7 @@ namespace Avalonia.Layout.UnitTests { Width = 200, Height = 200, - CanScrollHorizontally = true, + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Content = textBlock = new TextBlock diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs index 3a9e45a02b..a53809a029 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs @@ -372,6 +372,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering Margin = new Thickness(0, 100, 0, 0), Child = scroll = new ScrollContentPresenter() { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, Content = new StackPanel() { Children = diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs index 9a1d8cb59c..1de6d02a35 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests_HitTesting.cs @@ -357,6 +357,8 @@ namespace Avalonia.Visuals.UnitTests.Rendering Margin = new Thickness(0, 100, 0, 0), Child = scroll = new ScrollContentPresenter() { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, Content = new StackPanel() { Children =