Browse Source

Fix tooltip sizing.

By fixing TopLevel sizing... Width and Height now work like all other
controls in that they're only set when the window has an explicitly set
size. Don't think SizeToContent is needed? Closes #24.
pull/58/head
Steven Kirk 12 years ago
parent
commit
45c6c9cf02
  1. 2
      Perspex.Controls/PopupRoot.cs
  2. 124
      Perspex.Controls/TopLevel.cs
  3. 4
      Perspex.Controls/Window.cs
  4. 2
      Perspex.Input/Raw/RawKeyEventArgs.cs
  5. 1
      Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  6. 319
      Tests/Perspex.Controls.UnitTests/TopLevelTests.cs
  7. 11
      Windows/Perspex.Win32/WindowImpl.cs

2
Perspex.Controls/PopupRoot.cs

@ -41,7 +41,7 @@ namespace Perspex.Controls
public void Show()
{
this.PlatformImpl.Show();
this.ExecuteLayoutPass();
this.LayoutManager.ExecuteLayoutPass();
this.IsVisible = true;
}
}

124
Perspex.Controls/TopLevel.cs

@ -6,42 +6,74 @@
namespace Perspex.Controls
{
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Input;
using Perspex.Input.Raw;
using Perspex.Layout;
using Perspex.Media;
using Perspex.Platform;
using Perspex.Rendering;
using Perspex.Styling;
using Perspex.Threading;
using Splat;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
/// <summary>
/// Base class for top-level windows.
/// </summary>
/// <remarks>
/// This class acts as a base for top level windows such as <see cref="Window"/> and
/// <see cref="PopupRoot"/>. It handles scheduling layout, styling and rendering as well as
/// tracking the window <see cref="ClientSize"/> and <see cref="IsActive"/> state.
/// </remarks>
public abstract class TopLevel : ContentControl, ILayoutRoot, IRenderRoot, ICloseable, IFocusScope
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
/// </summary>
public static readonly PerspexProperty<Size> ClientSizeProperty =
PerspexProperty.Register<TopLevel, Size>("ClientSize");
/// <summary>
/// Defines the <see cref="IsActive"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsActiveProperty =
PerspexProperty.Register<Window, bool>("IsActive");
PerspexProperty.Register<TopLevel, bool>("IsActive");
/// <summary>
/// The dispatcher for the window.
/// </summary>
private Dispatcher dispatcher;
/// <summary>
/// The render manager for the window.s
/// </summary>
private IRenderManager renderManager;
/// <summary>
/// The window renderer.
/// </summary>
private IRenderer renderer;
/// <summary>
/// The input manager for the window.
/// </summary>
private IInputManager inputManager;
private bool autoSizing;
/// <summary>
/// Statically initializes the <see cref="TopLevel"/> class.
/// </summary>
static TopLevel()
{
TopLevel.AffectsMeasure(TopLevel.ClientSizeProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="TopLevel"/> class.
/// </summary>
/// <param name="impl">The platform-specific window implementation.</param>
public TopLevel(ITopLevelImpl impl)
{
IPlatformRenderInterface renderInterface = Locator.Current.GetService<IPlatformRenderInterface>();
@ -104,51 +136,86 @@ namespace Perspex.Controls
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x);
}
/// <summary>
/// Fired when the window is activated.
/// </summary>
public event EventHandler Activated;
/// <summary>
/// Fired when the window is closed.
/// </summary>
public event EventHandler Closed;
/// <summary>
/// Fired when the window is deactivated.
/// </summary>
public event EventHandler Deactivated;
/// <summary>
/// Gets or sets the client size of the window.
/// </summary>
public Size ClientSize
{
get { return this.GetValue(ClientSizeProperty); }
set { this.SetValue(ClientSizeProperty, value); }
private set { this.SetValue(ClientSizeProperty, value); }
}
/// <summary>
/// Gets a value that indicates whether the window is active.
/// </summary>
public bool IsActive
{
get { return this.GetValue(IsActiveProperty); }
private set { this.SetValue(IsActiveProperty, value); }
}
/// <summary>
/// Gets the layout manager for the window.
/// </summary>
public ILayoutManager LayoutManager
{
get;
private set;
}
/// <summary>
/// Gets the platform-specific window implementation.
/// </summary>
public ITopLevelImpl PlatformImpl
{
get;
private set;
}
/// <summary>
/// Gets the window renderer.
/// </summary>
IRenderer IRenderRoot.Renderer
{
get { return this.renderer; }
}
/// <summary>
/// Gets the window render manager.
/// </summary>
IRenderManager IRenderRoot.RenderManager
{
get { return this.renderManager; }
}
/// <summary>
/// Translates a point from window coordinates into screen coordinates.
/// </summary>
/// <param name="p">The point.</param>
/// <returns>The point in screen coordinates.</returns>
Point IRenderRoot.TranslatePointToScreen(Point p)
{
return this.PlatformImpl.PointToScreen(p);
}
/// <summary>
/// Activates the window.
/// </summary>
public void Activate()
{
this.PlatformImpl.Activate();
@ -160,26 +227,24 @@ namespace Perspex.Controls
return Disposable.Create(() => this.autoSizing = false);
}
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
this.PlatformImpl.ClientSize = finalSize;
return base.ArrangeOverride(finalSize);
}
protected void ExecuteLayoutPass()
{
this.LayoutManager.ExecuteLayoutPass();
using (this.BeginAutoSizing())
{
this.ClientSize = new Size(
double.IsNaN(this.Width) ? this.DesiredSize.Width : this.ClientSize.Width,
double.IsNaN(this.Height) ? this.DesiredSize.Height : this.ClientSize.Height);
this.PlatformImpl.ClientSize = finalSize;
}
this.PlatformImpl.Invalidate(new Rect(this.ClientSize));
return base.ArrangeOverride(finalSize);
}
/// <summary>
/// Handles an activated notification from <see cref="ITopLevelImpl.Activated"/>.
/// </summary>
private void HandleActivated()
{
if (this.Activated != null)
@ -191,6 +256,9 @@ namespace Perspex.Controls
this.IsActive = true;
}
/// <summary>
/// Handles a closed notification from <see cref="ITopLevelImpl.Closed"/>.
/// </summary>
private void HandleClosed()
{
if (this.Closed != null)
@ -199,6 +267,9 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Handles a deactivated notification from <see cref="ITopLevelImpl.Deactivated"/>.
/// </summary>
private void HandleDeactivated()
{
this.IsActive = false;
@ -209,16 +280,25 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Handles input from <see cref="ITopLevelImpl.Input"/>.
/// </summary>
private void HandleInput(RawInputEventArgs e)
{
this.inputManager.Process(e);
}
/// <summary>
/// Handles a layout request from <see cref="LayoutManager.LayoutNeeded"/>.
/// </summary>
private void HandleLayoutNeeded()
{
this.dispatcher.InvokeAsync(this.ExecuteLayoutPass, DispatcherPriority.Render);
this.dispatcher.InvokeAsync(LayoutManager.ExecuteLayoutPass, DispatcherPriority.Render);
}
/// <summary>
/// Handles a render request from <see cref="RenderManager.RenderNeeded"/>.
/// </summary>
private void HandleRenderNeeded()
{
this.dispatcher.InvokeAsync(
@ -226,12 +306,18 @@ namespace Perspex.Controls
DispatcherPriority.Render);
}
/// <summary>
/// Handles a paint request from <see cref="ITopLevelImpl.Paint"/>.
/// </summary>
private void HandlePaint(Rect rect, IPlatformHandle handle)
{
this.renderer.Render(this, handle);
this.renderManager.RenderFinished();
}
/// <summary>
/// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary>
private void HandleResized(Size clientSize)
{
if (!this.autoSizing)

4
Perspex.Controls/Window.cs

@ -68,7 +68,7 @@ namespace Perspex.Controls
public void Show()
{
this.ExecuteLayoutPass();
this.LayoutManager.ExecuteLayoutPass();
using (this.BeginAutoSizing())
{
@ -83,7 +83,7 @@ namespace Perspex.Controls
public Task<TResult> ShowDialog<TResult>()
{
this.ExecuteLayoutPass();
this.LayoutManager.ExecuteLayoutPass();
using (this.BeginAutoSizing())
{

2
Perspex.Input/Raw/RawKeyEventArgs.cs

@ -15,7 +15,7 @@ namespace Perspex.Input.Raw
public class RawKeyEventArgs : RawInputEventArgs
{
public RawKeyEventArgs(
KeyboardDevice device,
IKeyboardDevice device,
uint timestamp,
RawKeyEventType type,
Key key,

1
Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -100,6 +100,7 @@
<Compile Include="ContentControlTests.cs" />
<Compile Include="DecoratorTests.cs" />
<Compile Include="DeckTests.cs" />
<Compile Include="TopLevelTests.cs" />
<Compile Include="TabStripTests.cs" />
<Compile Include="TabControlTests.cs" />
<Compile Include="PanelTests.cs" />

319
Tests/Perspex.Controls.UnitTests/TopLevelTests.cs

@ -0,0 +1,319 @@
// -----------------------------------------------------------------------
// <copyright file="TopLevelTests.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls.UnitTests
{
using Moq;
using Perspex.Controls.Presenters;
using Perspex.Input;
using Perspex.Input.Raw;
using Perspex.Layout;
using Perspex.Platform;
using Perspex.Rendering;
using Perspex.Styling;
using Ploeh.AutoFixture;
using Ploeh.AutoFixture.AutoMoq;
using Splat;
using Xunit;
public class TopLevelTests
{
[Fact]
public void ClientSize_Should_Be_Set_On_Construction()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
Assert.Equal(new Size(123, 456), target.ClientSize);
}
}
[Fact]
public void Width_Should_Not_Be_Set_On_Construction()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
Assert.Equal(double.NaN, target.Width);
}
}
[Fact]
public void Height_Should_Not_Be_Set_On_Construction()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
Assert.Equal(double.NaN, target.Height);
}
}
[Fact]
public void Layout_Pass_Should_Not_Be_Automatically_Scheduled()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
var target = new TestTopLevel(impl.Object);
// The layout pass should be scheduled by the derived class.
var layoutManagerMock = Mock.Get(target.LayoutManager);
layoutManagerMock.Verify(x => x.ExecuteLayoutPass(), Times.Never);
}
}
[Fact]
public void Bounds_Should_Be_Set_After_Layout_Pass()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
Locator.CurrentMutable.RegisterConstant(new LayoutManager(), typeof(ILayoutManager));
var impl = new Mock<ITopLevelImpl>();
var target = new TestTopLevel(impl.Object)
{
Template = ControlTemplate.Create<TestTopLevel>(x =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = x[~TestTopLevel.ContentProperty],
}),
Content = new TextBlock
{
Width = 321,
Height = 432,
}
};
target.LayoutManager.ExecuteLayoutPass();
Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds);
}
}
[Fact]
public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
Locator.CurrentMutable.RegisterConstant(new LayoutManager(), typeof(ILayoutManager));
var impl = new Mock<ITopLevelImpl>();
var target = new TestTopLevel(impl.Object)
{
Template = ControlTemplate.Create<TestTopLevel>(x =>
new ContentPresenter
{
[~ContentPresenter.ContentProperty] = x[~TestTopLevel.ContentProperty],
}),
Content = new TextBlock
{
Width = 321,
Height = 432,
}
};
target.LayoutManager.ExecuteLayoutPass();
impl.VerifySet(x => x.ClientSize = new Size(321, 432));
}
}
[Fact]
public void Width_And_Height_Should_Not_Be_Set_After_Layout_Pass()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
target.LayoutManager.ExecuteLayoutPass();
Assert.Equal(double.NaN, target.Width);
Assert.Equal(double.NaN, target.Height);
}
}
[Fact]
public void Width_And_Height_Should_Be_Set_After_Window_Resize_Notification()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.SetupAllProperties();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
// The user has resized the window, so we can no longer auto-size.
var target = new TestTopLevel(impl.Object);
impl.Object.Resized(new Size(100, 200));
Assert.Equal(100, target.Width);
Assert.Equal(200, target.Height);
}
}
[Fact]
public void Activate_Should_Call_Impl_Activate()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
var target = new TestTopLevel(impl.Object);
target.Activate();
impl.Verify(x => x.Activate());
}
}
[Fact]
public void Impl_Activate_Should_Call_Raise_Activated_Event()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.SetupAllProperties();
bool raised = false;
var target = new TestTopLevel(impl.Object);
target.Activated += (s, e) => raised = true;
impl.Object.Activated();
Assert.True(raised);
}
}
[Fact]
public void Impl_Close_Should_Call_Raise_Closed_Event()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.SetupAllProperties();
bool raised = false;
var target = new TestTopLevel(impl.Object);
target.Closed += (s, e) => raised = true;
impl.Object.Closed();
Assert.True(raised);
}
}
[Fact]
public void Impl_Deactivate_Should_Call_Raise_Activated_Event()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.SetupAllProperties();
bool raised = false;
var target = new TestTopLevel(impl.Object);
target.Deactivated += (s, e) => raised = true;
impl.Object.Deactivated();
Assert.True(raised);
}
}
[Fact]
public void Impl_Input_Should_Pass_Input_To_InputManager()
{
using (Locator.CurrentMutable.WithResolver())
{
this.RegisterServices();
var impl = new Mock<ITopLevelImpl>();
impl.SetupAllProperties();
var target = new TestTopLevel(impl.Object);
var input = new RawKeyEventArgs(
new Mock<IKeyboardDevice>().Object,
0,
RawKeyEventType.KeyDown,
Key.A,
"A");
impl.Object.Input(input);
var inputManagerMock = Mock.Get(InputManager.Instance);
inputManagerMock.Verify(x => x.Process(input));
}
}
private void RegisterServices()
{
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var l = Locator.CurrentMutable;
var formattedText = fixture.Create<IFormattedTextImpl>();
var globalStyles = new Mock<IGlobalStyles>();
var layoutManager = fixture.Create<ILayoutManager>();
var renderInterface = fixture.Create<IPlatformRenderInterface>();
var renderManager = fixture.Create<IRenderManager>();
var windowImpl = new Mock<IWindowImpl>();
var theme = new Styles();
globalStyles.Setup(x => x.Styles).Returns(theme);
l.RegisterConstant(new Mock<IInputManager>().Object, typeof(IInputManager));
l.RegisterConstant(new Mock<IFocusManager>().Object, typeof(IFocusManager));
l.RegisterConstant(globalStyles.Object, typeof(IGlobalStyles));
l.RegisterConstant(layoutManager, typeof(ILayoutManager));
l.RegisterConstant(new Mock<IPlatformThreadingInterface>().Object, typeof(IPlatformThreadingInterface));
l.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface));
l.RegisterConstant(renderManager, typeof(IRenderManager));
l.RegisterConstant(new Styler(), typeof(IStyler));
}
private class TestTopLevel : TopLevel
{
public TestTopLevel(ITopLevelImpl impl)
: base(impl)
{
}
}
}
}

11
Windows/Perspex.Win32/WindowImpl.cs

@ -6,6 +6,11 @@
namespace Perspex.Win32
{
using Perspex.Controls;
using Perspex.Input.Raw;
using Perspex.Platform;
using Perspex.Win32.Input;
using Perspex.Win32.Interop;
using System;
using System.Collections.Generic;
using System.ComponentModel;
@ -14,12 +19,6 @@ namespace Perspex.Win32
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Perspex.Controls;
using Perspex.Input.Raw;
using Perspex.Platform;
using Perspex.Win32.Input;
using Perspex.Win32.Interop;
public class WindowImpl : IWindowImpl
{

Loading…
Cancel
Save