Browse Source

Scale ComboBox popup according to RenderTransform.

pull/8042/head
Steven Kirk 4 years ago
parent
commit
d1956d18b4
  1. 56
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  2. 42
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  3. 150
      src/Avalonia.Controls/Primitives/Popup.cs
  4. 39
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  5. 18
      src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml
  6. 22
      src/Avalonia.Themes.Default/Controls/PopupRoot.xaml
  7. 3
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  8. 16
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  9. 14
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  10. 4
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

56
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -2,6 +2,7 @@ using System;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@ -17,16 +18,50 @@ namespace Avalonia.Controls.Primitives
public interface IPopupHost : IDisposable, IFocusScope
{
/// <summary>
/// Sets the control to display in the popup.
/// Gets or sets the fixed width of the popup.
/// </summary>
/// <param name="control"></param>
void SetChild(IControl? control);
double Width { get; set; }
/// <summary>
/// Gets or sets the minimum width of the popup.
/// </summary>
double MinWidth { get; set; }
/// <summary>
/// Gets or sets the maximum width of the popup.
/// </summary>
double MaxWidth { get; set; }
/// <summary>
/// Gets or sets the fixed height of the popup.
/// </summary>
double Height { get; set; }
/// <summary>
/// Gets or sets the minimum height of the popup.
/// </summary>
double MinHeight { get; set; }
/// <summary>
/// Gets or sets the maximum height of the popup.
/// </summary>
double MaxHeight { get; set; }
/// <summary>
/// Gets the presenter from the control's template.
/// </summary>
IContentPresenter? Presenter { get; }
/// <summary>
/// Gets or sets whether the popup appears on top of all other windows.
/// </summary>
bool Topmost { get; set; }
/// <summary>
/// Gets or sets a transform that will be applied to the popup.
/// </summary>
Transform? Transform { get; set; }
/// <summary>
/// Gets the root of the visual tree in the case where the popup is presented using a
/// separate visual tree.
@ -57,6 +92,12 @@ namespace Avalonia.Controls.Primitives
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null);
/// <summary>
/// Sets the control to display in the popup.
/// </summary>
/// <param name="control"></param>
void SetChild(IControl? control);
/// <summary>
/// Shows the popup.
/// </summary>
@ -66,14 +107,5 @@ namespace Avalonia.Controls.Primitives
/// Hides the popup.
/// </summary>
void Hide();
/// <summary>
/// Binds the constraints of the popup host to a set of properties, usally those present on
/// <see cref="Popup"/>.
/// </summary>
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty);
}
}

42
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -11,6 +11,12 @@ namespace Avalonia.Controls.Primitives
{
public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup
{
/// <summary>
/// Defines the <see cref="Transform"/> property.
/// </summary>
public static readonly StyledProperty<Transform?> TransformProperty =
PopupRoot.TransformProperty.AddOwner<OverlayPopupHost>();
private readonly OverlayLayer _overlayLayer;
private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters();
private ManagedPopupPositioner _positioner;
@ -29,10 +35,22 @@ namespace Avalonia.Controls.Primitives
}
public IVisual? HostedVisualTreeRoot => null;
public Transform? Transform
{
get => GetValue(TransformProperty);
set => SetValue(TransformProperty, value);
}
/// <inheritdoc/>
IInteractive? IInteractive.InteractiveParent => Parent;
bool IPopupHost.Topmost
{
get => false;
set { /* Not currently supported in overlay popups */ }
}
public void Dispose() => Hide();
@ -48,28 +66,6 @@ namespace Avalonia.Controls.Primitives
_shown = false;
}
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
// Topmost property is not supported
var bindings = new List<IDisposable>();
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
Bind(WidthProperty, widthProperty);
Bind(MinWidthProperty, minWidthProperty);
Bind(MaxWidthProperty, maxWidthProperty);
Bind(HeightProperty, heightProperty);
Bind(MinHeightProperty, minHeightProperty);
Bind(MaxHeightProperty, maxHeightProperty);
return Disposable.Create(() =>
{
foreach (var x in bindings)
x.Dispose();
});
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,

150
src/Avalonia.Controls/Primitives/Popup.cs

@ -14,6 +14,8 @@ using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.VisualTree;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Controls.Primitives
{
@ -33,6 +35,12 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<Control?> ChildProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(Child));
/// <summary>
/// Defines the <see cref="InheritsTransform"/> property.
/// </summary>
public static readonly StyledProperty<bool> InheritsTransformProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(InheritsTransform));
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
@ -196,6 +204,16 @@ namespace Avalonia.Controls.Primitives
set;
}
/// <summary>
/// Gets or sets a value that determines whether the popup inherits the render transform
/// from its <see cref="PlacementTarget"/>. Defaults to false.
/// </summary>
public bool InheritsTransform
{
get => GetValue(InheritsTransformProperty);
set => SetValue(InheritsTransformProperty, value);
}
/// <summary>
/// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed.
/// </summary>
@ -395,24 +413,29 @@ namespace Avalonia.Controls.Primitives
}
_isOpenRequested = false;
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
var handlerCleanup = new CompositeDisposable(7);
popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty).DisposeWith(handlerCleanup);
UpdateHostSizing(popupHost, topLevel, placementTarget);
popupHost.Topmost = Topmost;
popupHost.SetChild(Child);
((ISetLogicalParent)popupHost).SetParent(this);
popupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
if (InheritsTransform && placementTarget is Control c)
{
SubscribeToEventHandler<Control, EventHandler<AvaloniaPropertyChangedEventArgs>>(
c,
PlacementTargetPropertyChanged,
(x, handler) => x.PropertyChanged += handler,
(x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup);
}
else
{
popupHost.Transform = null;
}
UpdateHostPosition(popupHost, placementTarget);
SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
@ -494,7 +517,7 @@ namespace Avalonia.Controls.Primitives
}
}
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
_openState = new PopupOpenState(placementTarget, topLevel, popupHost, cleanupPopup);
WindowManagerAddShadowHintChanged(popupHost, WindowManagerAddShadowHint);
@ -542,7 +565,93 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e);
Close();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (_openState is not null)
{
if (change.Property == WidthProperty ||
change.Property == MinWidthProperty ||
change.Property == MaxWidthProperty ||
change.Property == HeightProperty ||
change.Property == MinHeightProperty ||
change.Property == MaxHeightProperty)
{
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
else if (change.Property == PlacementTargetProperty ||
change.Property == PlacementModeProperty ||
change.Property == HorizontalOffsetProperty ||
change.Property == VerticalOffsetProperty ||
change.Property == PlacementAnchorProperty ||
change.Property == PlacementConstraintAdjustmentProperty ||
change.Property == PlacementRectProperty)
{
if (change.Property == PlacementTargetProperty)
{
var newTarget = change.GetNewValue<Control?>() ?? this.FindLogicalAncestorOfType<IControl>();
if (newTarget is null || newTarget.GetVisualRoot() != _openState.TopLevel)
{
Close();
return;
}
_openState.PlacementTarget = newTarget;
}
UpdateHostPosition(_openState.PopupHost, _openState.PlacementTarget);
}
else if (change.Property == TopmostProperty)
{
_openState.PopupHost.Topmost = change.GetNewValue<bool>();
}
}
}
private void UpdateHostPosition(IPopupHost popupHost, IControl placementTarget)
{
popupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect ?? new Rect(default, placementTarget.Bounds.Size));
}
private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, IControl placementTarget)
{
var scaleX = 1.0;
var scaleY = 1.0;
if (InheritsTransform && placementTarget.TransformToVisual(topLevel) is Matrix m)
{
scaleX = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
scaleY = Math.Sqrt(m.M11 * m.M11 + m.M12 * m.M12);
// Ideally we'd only assign a ScaleTransform here when the scale != 1, but there's
// an issue with LayoutTransformControl in that it sets its LayoutTransform property
// with LocalValue priority in ArrangeOverride in certain cases when LayoutTransform
// is null, which breaks TemplateBindings to this property. Offending commit/line:
//
// https://github.com/AvaloniaUI/Avalonia/commit/6fbe1c2180ef45a940e193f1b4637e64eaab80ed#diff-5344e793df13f462126a8153ef46c44194f244b6890f25501709bae51df97f82R54
popupHost.Transform = new ScaleTransform(scaleX, scaleY);
}
else
{
popupHost.Transform = null;
}
popupHost.Width = Width * scaleX;
popupHost.MinWidth = MinWidth * scaleX;
popupHost.MaxWidth = MaxWidth * scaleX;
popupHost.Height = Height * scaleY;
popupHost.MinHeight = MinHeight * scaleY;
popupHost.MaxHeight = MaxHeight * scaleY;
}
private void HandlePositionChange()
{
if (_openState != null)
@ -824,6 +933,14 @@ namespace Avalonia.Controls.Primitives
}
}
private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (_openState is not null && e.Property == Visual.TransformedBoundsProperty)
{
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
}
private void WindowLostFocus()
{
if (IsLightDismissEnabled)
@ -862,15 +979,16 @@ namespace Avalonia.Controls.Primitives
private readonly IDisposable _cleanup;
private IDisposable? _presenterCleanup;
public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
public PopupOpenState(IControl placementTarget, TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
{
PlacementTarget = placementTarget;
TopLevel = topLevel;
PopupHost = popupHost;
_cleanup = cleanup;
}
public TopLevel TopLevel { get; }
public IControl PlacementTarget { get; set; }
public IPopupHost PopupHost { get; }
public void SetPresenterSubscription(IDisposable? presenterCleanup)

39
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
@ -8,7 +6,6 @@ using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls.Primitives
{
@ -17,6 +14,12 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public sealed class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost, IPopupHost
{
/// <summary>
/// Defines the <see cref="Transform"/> property.
/// </summary>
public static readonly StyledProperty<Transform?> TransformProperty =
AvaloniaProperty.Register<PopupRoot, Transform?>(nameof(Transform));
private PopupPositionerParameters _positionerParameters;
/// <summary>
@ -54,6 +57,15 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public new IPopupImpl? PlatformImpl => (IPopupImpl?)base.PlatformImpl;
/// <summary>
/// Gets or sets a transform that will be applied to the popup.
/// </summary>
public Transform? Transform
{
get => GetValue(TransformProperty);
set => SetValue(TransformProperty, value);
}
/// <summary>
/// Gets the parent control in the event route.
/// </summary>
@ -103,27 +115,6 @@ namespace Avalonia.Controls.Primitives
IVisual IPopupHost.HostedVisualTreeRoot => this;
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty,
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty)
{
var bindings = new List<IDisposable>();
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to]));
Bind(WidthProperty, widthProperty);
Bind(MinWidthProperty, minWidthProperty);
Bind(MaxWidthProperty, maxWidthProperty);
Bind(HeightProperty, heightProperty);
Bind(MinHeightProperty, minHeightProperty);
Bind(MaxHeightProperty, maxHeightProperty);
Bind(TopmostProperty, topmostProperty);
return Disposable.Create(() =>
{
foreach (var x in bindings)
x.Dispose();
});
}
protected override Size MeasureOverride(Size availableSize)
{
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;

18
src/Avalonia.Themes.Default/Controls/OverlayPopupHost.xaml

@ -1,4 +1,4 @@
<Style xmlns="https://github.com/avaloniaui"
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="OverlayPopupHost">
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
@ -9,13 +9,15 @@
<Setter Property="Template">
<ControlTemplate>
<!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test -->
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</LayoutTransformControl>
</ControlTemplate>
</Setter>
</Style>

22
src/Avalonia.Themes.Default/Controls/PopupRoot.xaml

@ -10,16 +10,18 @@
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
</LayoutTransformControl>
</ControlTemplate>
</Setter>
</Style>

3
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -119,7 +119,8 @@
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}"
PlacementTarget="Background"
IsLightDismissEnabled="True">
IsLightDismissEnabled="True"
InheritsTransform="True">
<Border x:Name="PopupBorder"
Background="{DynamicResource ComboBoxDropDownBackground}"
BorderBrush="{DynamicResource ComboBoxDropDownBorderBrush}"

16
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@ -6,13 +6,15 @@
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</LayoutTransformControl>
</ControlTemplate>
</Setter>
</Style>

14
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@ -10,16 +10,18 @@
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<VisualLayerManager IsPopup="True">
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"/>
</VisualLayerManager>
</Panel>
</VisualLayerManager>
</Panel>
</LayoutTransformControl>
</ControlTemplate>
</Setter>
</Style>

4
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -325,6 +325,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(
new[]
{
"LayoutTransformControl",
"VisualLayerManager",
"ContentPresenter",
"ContentPresenter",
@ -337,6 +338,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(
new[]
{
"LayoutTransformControl",
"Panel",
"Border",
"VisualLayerManager",
@ -356,6 +358,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(
new object[]
{
popupRoot,
popupRoot,
popupRoot,
target,
@ -372,6 +375,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
popupRoot,
popupRoot,
popupRoot,
popupRoot,
target,
null,
},

Loading…
Cancel
Save