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() public void Show()
{ {
this.PlatformImpl.Show(); this.PlatformImpl.Show();
this.ExecuteLayoutPass(); this.LayoutManager.ExecuteLayoutPass();
this.IsVisible = true; this.IsVisible = true;
} }
} }

124
Perspex.Controls/TopLevel.cs

@ -6,42 +6,74 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Input; using Perspex.Input;
using Perspex.Input.Raw; using Perspex.Input.Raw;
using Perspex.Layout; using Perspex.Layout;
using Perspex.Media;
using Perspex.Platform; using Perspex.Platform;
using Perspex.Rendering; using Perspex.Rendering;
using Perspex.Styling; using Perspex.Styling;
using Perspex.Threading; using Perspex.Threading;
using Splat; 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 public abstract class TopLevel : ContentControl, ILayoutRoot, IRenderRoot, ICloseable, IFocusScope
{ {
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
/// </summary>
public static readonly PerspexProperty<Size> ClientSizeProperty = public static readonly PerspexProperty<Size> ClientSizeProperty =
PerspexProperty.Register<TopLevel, Size>("ClientSize"); PerspexProperty.Register<TopLevel, Size>("ClientSize");
/// <summary>
/// Defines the <see cref="IsActive"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsActiveProperty = 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; private Dispatcher dispatcher;
/// <summary>
/// The render manager for the window.s
/// </summary>
private IRenderManager renderManager; private IRenderManager renderManager;
/// <summary>
/// The window renderer.
/// </summary>
private IRenderer renderer; private IRenderer renderer;
/// <summary>
/// The input manager for the window.
/// </summary>
private IInputManager inputManager; private IInputManager inputManager;
private bool autoSizing; private bool autoSizing;
/// <summary>
/// Statically initializes the <see cref="TopLevel"/> class.
/// </summary>
static TopLevel() static TopLevel()
{ {
TopLevel.AffectsMeasure(TopLevel.ClientSizeProperty); 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) public TopLevel(ITopLevelImpl impl)
{ {
IPlatformRenderInterface renderInterface = Locator.Current.GetService<IPlatformRenderInterface>(); IPlatformRenderInterface renderInterface = Locator.Current.GetService<IPlatformRenderInterface>();
@ -104,51 +136,86 @@ namespace Perspex.Controls
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x); this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x);
} }
/// <summary>
/// Fired when the window is activated.
/// </summary>
public event EventHandler Activated; public event EventHandler Activated;
/// <summary>
/// Fired when the window is closed.
/// </summary>
public event EventHandler Closed; public event EventHandler Closed;
/// <summary>
/// Fired when the window is deactivated.
/// </summary>
public event EventHandler Deactivated; public event EventHandler Deactivated;
/// <summary>
/// Gets or sets the client size of the window.
/// </summary>
public Size ClientSize public Size ClientSize
{ {
get { return this.GetValue(ClientSizeProperty); } 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 public bool IsActive
{ {
get { return this.GetValue(IsActiveProperty); } get { return this.GetValue(IsActiveProperty); }
private set { this.SetValue(IsActiveProperty, value); } private set { this.SetValue(IsActiveProperty, value); }
} }
/// <summary>
/// Gets the layout manager for the window.
/// </summary>
public ILayoutManager LayoutManager public ILayoutManager LayoutManager
{ {
get; get;
private set; private set;
} }
/// <summary>
/// Gets the platform-specific window implementation.
/// </summary>
public ITopLevelImpl PlatformImpl public ITopLevelImpl PlatformImpl
{ {
get; get;
private set; private set;
} }
/// <summary>
/// Gets the window renderer.
/// </summary>
IRenderer IRenderRoot.Renderer IRenderer IRenderRoot.Renderer
{ {
get { return this.renderer; } get { return this.renderer; }
} }
/// <summary>
/// Gets the window render manager.
/// </summary>
IRenderManager IRenderRoot.RenderManager IRenderManager IRenderRoot.RenderManager
{ {
get { return this.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) Point IRenderRoot.TranslatePointToScreen(Point p)
{ {
return this.PlatformImpl.PointToScreen(p); return this.PlatformImpl.PointToScreen(p);
} }
/// <summary>
/// Activates the window.
/// </summary>
public void Activate() public void Activate()
{ {
this.PlatformImpl.Activate(); this.PlatformImpl.Activate();
@ -160,26 +227,24 @@ namespace Perspex.Controls
return Disposable.Create(() => this.autoSizing = false); 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) protected override Size ArrangeOverride(Size finalSize)
{ {
this.PlatformImpl.ClientSize = finalSize;
return base.ArrangeOverride(finalSize);
}
protected void ExecuteLayoutPass()
{
this.LayoutManager.ExecuteLayoutPass();
using (this.BeginAutoSizing()) using (this.BeginAutoSizing())
{ {
this.ClientSize = new Size( this.PlatformImpl.ClientSize = finalSize;
double.IsNaN(this.Width) ? this.DesiredSize.Width : this.ClientSize.Width,
double.IsNaN(this.Height) ? this.DesiredSize.Height : this.ClientSize.Height);
} }
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() private void HandleActivated()
{ {
if (this.Activated != null) if (this.Activated != null)
@ -191,6 +256,9 @@ namespace Perspex.Controls
this.IsActive = true; this.IsActive = true;
} }
/// <summary>
/// Handles a closed notification from <see cref="ITopLevelImpl.Closed"/>.
/// </summary>
private void HandleClosed() private void HandleClosed()
{ {
if (this.Closed != null) 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() private void HandleDeactivated()
{ {
this.IsActive = false; this.IsActive = false;
@ -209,16 +280,25 @@ namespace Perspex.Controls
} }
} }
/// <summary>
/// Handles input from <see cref="ITopLevelImpl.Input"/>.
/// </summary>
private void HandleInput(RawInputEventArgs e) private void HandleInput(RawInputEventArgs e)
{ {
this.inputManager.Process(e); this.inputManager.Process(e);
} }
/// <summary>
/// Handles a layout request from <see cref="LayoutManager.LayoutNeeded"/>.
/// </summary>
private void HandleLayoutNeeded() 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() private void HandleRenderNeeded()
{ {
this.dispatcher.InvokeAsync( this.dispatcher.InvokeAsync(
@ -226,12 +306,18 @@ namespace Perspex.Controls
DispatcherPriority.Render); DispatcherPriority.Render);
} }
/// <summary>
/// Handles a paint request from <see cref="ITopLevelImpl.Paint"/>.
/// </summary>
private void HandlePaint(Rect rect, IPlatformHandle handle) private void HandlePaint(Rect rect, IPlatformHandle handle)
{ {
this.renderer.Render(this, handle); this.renderer.Render(this, handle);
this.renderManager.RenderFinished(); this.renderManager.RenderFinished();
} }
/// <summary>
/// Handles a resize notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary>
private void HandleResized(Size clientSize) private void HandleResized(Size clientSize)
{ {
if (!this.autoSizing) if (!this.autoSizing)

4
Perspex.Controls/Window.cs

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

2
Perspex.Input/Raw/RawKeyEventArgs.cs

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

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

@ -100,6 +100,7 @@
<Compile Include="ContentControlTests.cs" /> <Compile Include="ContentControlTests.cs" />
<Compile Include="DecoratorTests.cs" /> <Compile Include="DecoratorTests.cs" />
<Compile Include="DeckTests.cs" /> <Compile Include="DeckTests.cs" />
<Compile Include="TopLevelTests.cs" />
<Compile Include="TabStripTests.cs" /> <Compile Include="TabStripTests.cs" />
<Compile Include="TabControlTests.cs" /> <Compile Include="TabControlTests.cs" />
<Compile Include="PanelTests.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 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@ -14,12 +19,6 @@ namespace Perspex.Win32
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Runtime.InteropServices; 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 public class WindowImpl : IWindowImpl
{ {

Loading…
Cancel
Save