From 45c6c9cf02bc3d54bd8ec0504cd066ae9291e6fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 7 May 2015 20:08:59 +0200 Subject: [PATCH] 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. --- Perspex.Controls/PopupRoot.cs | 2 +- Perspex.Controls/TopLevel.cs | 124 +++++-- Perspex.Controls/Window.cs | 4 +- Perspex.Input/Raw/RawKeyEventArgs.cs | 2 +- .../Perspex.Controls.UnitTests.csproj | 1 + .../TopLevelTests.cs | 319 ++++++++++++++++++ Windows/Perspex.Win32/WindowImpl.cs | 11 +- 7 files changed, 434 insertions(+), 29 deletions(-) create mode 100644 Tests/Perspex.Controls.UnitTests/TopLevelTests.cs diff --git a/Perspex.Controls/PopupRoot.cs b/Perspex.Controls/PopupRoot.cs index c7fff84549..ec2e3d0e57 100644 --- a/Perspex.Controls/PopupRoot.cs +++ b/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; } } diff --git a/Perspex.Controls/TopLevel.cs b/Perspex.Controls/TopLevel.cs index 8c24e18361..32d0021ca8 100644 --- a/Perspex.Controls/TopLevel.cs +++ b/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; + /// + /// Base class for top-level windows. + /// + /// + /// This class acts as a base for top level windows such as and + /// . It handles scheduling layout, styling and rendering as well as + /// tracking the window and state. + /// public abstract class TopLevel : ContentControl, ILayoutRoot, IRenderRoot, ICloseable, IFocusScope { + /// + /// Defines the property. + /// public static readonly PerspexProperty ClientSizeProperty = PerspexProperty.Register("ClientSize"); + /// + /// Defines the property. + /// public static readonly PerspexProperty IsActiveProperty = - PerspexProperty.Register("IsActive"); + PerspexProperty.Register("IsActive"); + /// + /// The dispatcher for the window. + /// private Dispatcher dispatcher; + /// + /// The render manager for the window.s + /// private IRenderManager renderManager; + /// + /// The window renderer. + /// private IRenderer renderer; + /// + /// The input manager for the window. + /// private IInputManager inputManager; private bool autoSizing; + /// + /// Statically initializes the class. + /// static TopLevel() { TopLevel.AffectsMeasure(TopLevel.ClientSizeProperty); } + /// + /// Initializes a new instance of the class. + /// + /// The platform-specific window implementation. public TopLevel(ITopLevelImpl impl) { IPlatformRenderInterface renderInterface = Locator.Current.GetService(); @@ -104,51 +136,86 @@ namespace Perspex.Controls this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x); } + /// + /// Fired when the window is activated. + /// public event EventHandler Activated; + /// + /// Fired when the window is closed. + /// public event EventHandler Closed; + /// + /// Fired when the window is deactivated. + /// public event EventHandler Deactivated; + /// + /// Gets or sets the client size of the window. + /// public Size ClientSize { get { return this.GetValue(ClientSizeProperty); } - set { this.SetValue(ClientSizeProperty, value); } + private set { this.SetValue(ClientSizeProperty, value); } } + /// + /// Gets a value that indicates whether the window is active. + /// public bool IsActive { get { return this.GetValue(IsActiveProperty); } private set { this.SetValue(IsActiveProperty, value); } } + /// + /// Gets the layout manager for the window. + /// public ILayoutManager LayoutManager { get; private set; } + /// + /// Gets the platform-specific window implementation. + /// public ITopLevelImpl PlatformImpl { get; private set; } + /// + /// Gets the window renderer. + /// IRenderer IRenderRoot.Renderer { get { return this.renderer; } } + /// + /// Gets the window render manager. + /// IRenderManager IRenderRoot.RenderManager { get { return this.renderManager; } } + /// + /// Translates a point from window coordinates into screen coordinates. + /// + /// The point. + /// The point in screen coordinates. Point IRenderRoot.TranslatePointToScreen(Point p) { return this.PlatformImpl.PointToScreen(p); } + /// + /// Activates the window. + /// public void Activate() { this.PlatformImpl.Activate(); @@ -160,26 +227,24 @@ namespace Perspex.Controls return Disposable.Create(() => this.autoSizing = false); } + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. 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); } + /// + /// Handles an activated notification from . + /// private void HandleActivated() { if (this.Activated != null) @@ -191,6 +256,9 @@ namespace Perspex.Controls this.IsActive = true; } + /// + /// Handles a closed notification from . + /// private void HandleClosed() { if (this.Closed != null) @@ -199,6 +267,9 @@ namespace Perspex.Controls } } + /// + /// Handles a deactivated notification from . + /// private void HandleDeactivated() { this.IsActive = false; @@ -209,16 +280,25 @@ namespace Perspex.Controls } } + /// + /// Handles input from . + /// private void HandleInput(RawInputEventArgs e) { this.inputManager.Process(e); } + /// + /// Handles a layout request from . + /// private void HandleLayoutNeeded() { - this.dispatcher.InvokeAsync(this.ExecuteLayoutPass, DispatcherPriority.Render); + this.dispatcher.InvokeAsync(LayoutManager.ExecuteLayoutPass, DispatcherPriority.Render); } + /// + /// Handles a render request from . + /// private void HandleRenderNeeded() { this.dispatcher.InvokeAsync( @@ -226,12 +306,18 @@ namespace Perspex.Controls DispatcherPriority.Render); } + /// + /// Handles a paint request from . + /// private void HandlePaint(Rect rect, IPlatformHandle handle) { this.renderer.Render(this, handle); this.renderManager.RenderFinished(); } + /// + /// Handles a resize notification from . + /// private void HandleResized(Size clientSize) { if (!this.autoSizing) diff --git a/Perspex.Controls/Window.cs b/Perspex.Controls/Window.cs index 00605c7f36..07e0b2a74f 100644 --- a/Perspex.Controls/Window.cs +++ b/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 ShowDialog() { - this.ExecuteLayoutPass(); + this.LayoutManager.ExecuteLayoutPass(); using (this.BeginAutoSizing()) { diff --git a/Perspex.Input/Raw/RawKeyEventArgs.cs b/Perspex.Input/Raw/RawKeyEventArgs.cs index 9cf852cc92..7050375d6a 100644 --- a/Perspex.Input/Raw/RawKeyEventArgs.cs +++ b/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, diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 9f33679406..27a2f7e59f 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -100,6 +100,7 @@ + diff --git a/Tests/Perspex.Controls.UnitTests/TopLevelTests.cs b/Tests/Perspex.Controls.UnitTests/TopLevelTests.cs new file mode 100644 index 0000000000..a360f415a0 --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/TopLevelTests.cs @@ -0,0 +1,319 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +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(); + 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(); + 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(); + 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(); + 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(); + + var target = new TestTopLevel(impl.Object) + { + Template = ControlTemplate.Create(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(); + + var target = new TestTopLevel(impl.Object) + { + Template = ControlTemplate.Create(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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + impl.SetupAllProperties(); + var target = new TestTopLevel(impl.Object); + + var input = new RawKeyEventArgs( + new Mock().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(); + var globalStyles = new Mock(); + var layoutManager = fixture.Create(); + var renderInterface = fixture.Create(); + var renderManager = fixture.Create(); + var windowImpl = new Mock(); + var theme = new Styles(); + + globalStyles.Setup(x => x.Styles).Returns(theme); + + l.RegisterConstant(new Mock().Object, typeof(IInputManager)); + l.RegisterConstant(new Mock().Object, typeof(IFocusManager)); + l.RegisterConstant(globalStyles.Object, typeof(IGlobalStyles)); + l.RegisterConstant(layoutManager, typeof(ILayoutManager)); + l.RegisterConstant(new Mock().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) + { + } + } + } +} diff --git a/Windows/Perspex.Win32/WindowImpl.cs b/Windows/Perspex.Win32/WindowImpl.cs index 25ffd322b4..f7101a2e81 100644 --- a/Windows/Perspex.Win32/WindowImpl.cs +++ b/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 {