// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Controls.Platform;
using Perspex.Controls.Primitives;
using Perspex.Input;
using Perspex.Input.Raw;
using Perspex.Layout;
using Perspex.Platform;
using Perspex.Rendering;
using Perspex.Styling;
using Perspex.Threading;
namespace Perspex.Controls
{
///
/// 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, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable
{
///
/// Defines the property.
///
public static readonly PerspexProperty ClientSizeProperty =
PerspexProperty.RegisterDirect(nameof(ClientSize), o => o.ClientSize);
///
/// Defines the property.
///
public static readonly PerspexProperty IsActiveProperty =
PerspexProperty.RegisterDirect(nameof(IsActive), o => o.IsActive);
///
/// Defines the property.
///
public static readonly PerspexProperty PointerOverElementProperty =
PerspexProperty.Register(nameof(IInputRoot.PointerOverElement));
private readonly IRenderQueueManager _renderQueueManager;
private readonly IInputManager _inputManager;
private readonly IAccessKeyHandler _accessKeyHandler;
private readonly IKeyboardNavigationHandler _keyboardNavigationHandler;
private Size _clientSize;
private bool _isActive;
///
/// Initializes static members of the class.
///
static TopLevel()
{
AffectsMeasure(ClientSizeProperty);
}
///
/// Initializes a new instance of the class.
///
/// The platform-specific window implementation.
public TopLevel(ITopLevelImpl impl)
: this(impl, PerspexLocator.Current)
{
}
///
/// Initializes a new instance of the class.
///
/// The platform-specific window implementation.
///
/// The dependency resolver to use. If null the default dependency resolver will be used.
///
public TopLevel(ITopLevelImpl impl, IPerspexDependencyResolver dependencyResolver)
{
if (impl == null)
{
throw new InvalidOperationException(
"Could not create window implementation: maybe no windowing subsystem was initialized?");
}
PlatformImpl = impl;
dependencyResolver = dependencyResolver ?? PerspexLocator.Current;
var renderInterface = TryGetService(dependencyResolver);
var styler = TryGetService(dependencyResolver);
_accessKeyHandler = TryGetService(dependencyResolver);
_inputManager = TryGetService(dependencyResolver);
_keyboardNavigationHandler = TryGetService(dependencyResolver);
LayoutManager = TryGetService(dependencyResolver);
_renderQueueManager = TryGetService(dependencyResolver);
(TryGetService(dependencyResolver) ?? new DefaultTopLevelRenderer()).Attach(this);
PlatformImpl.SetInputRoot(this);
PlatformImpl.Activated = HandleActivated;
PlatformImpl.Deactivated = HandleDeactivated;
PlatformImpl.Closed = HandleClosed;
PlatformImpl.Input = HandleInput;
PlatformImpl.Resized = HandleResized;
Size clientSize = ClientSize = PlatformImpl.ClientSize;
if (LayoutManager != null)
{
LayoutManager.Root = this;
LayoutManager.LayoutNeeded.Subscribe(_ => HandleLayoutNeeded());
LayoutManager.LayoutCompleted.Subscribe(_ => HandleLayoutCompleted());
}
if (_keyboardNavigationHandler != null)
{
_keyboardNavigationHandler.SetOwner(this);
}
if (_accessKeyHandler != null)
{
_accessKeyHandler.SetOwner(this);
}
styler?.ApplyStyles(this);
GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl.ClientSize = x);
GetObservable(PointerOverElementProperty)
.Select(
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty())
.Switch().Subscribe(cursor => PlatformImpl.SetCursor(cursor?.PlatformCursor));
}
///
/// 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 _clientSize; }
private set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); }
}
///
/// Gets a value that indicates whether the window is active.
///
public bool IsActive
{
get { return _isActive; }
private set { SetAndRaise(IsActiveProperty, ref _isActive, value); }
}
///
/// Gets the layout manager for the window.
///
public ILayoutManager LayoutManager
{
get;
}
///
/// Gets the platform-specific window implementation.
///
public ITopLevelImpl PlatformImpl
{
get;
}
///
/// Gets the window render manager.
///
IRenderQueueManager IRenderRoot.RenderQueueManager => _renderQueueManager;
///
/// Gets the access key handler for the window.
///
IAccessKeyHandler IInputRoot.AccessKeyHandler => _accessKeyHandler;
///
/// Gets or sets the keyboard navigation handler for the window.
///
IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler;
///
/// Gets or sets the input element that the pointer is currently over.
///
IInputElement IInputRoot.PointerOverElement
{
get { return GetValue(PointerOverElementProperty); }
set { SetValue(PointerOverElementProperty, value); }
}
///
/// Gets or sets a value indicating whether access keys are shown in the window.
///
bool IInputRoot.ShowAccessKeys
{
get { return GetValue(AccessText.ShowAccessKeyProperty); }
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
///
/// Whether an auto-size operation is in progress.
///
protected bool AutoSizing
{
get;
private set;
}
///
/// Translates a point from window coordinates into screen coordinates.
///
/// The point.
/// The point in screen coordinates.
Point IRenderRoot.TranslatePointToScreen(Point p)
{
return PlatformImpl.PointToScreen(p);
}
///
/// Activates the window.
///
public void Activate()
{
PlatformImpl.Activate();
}
///
/// Begins an auto-resize operation.
///
/// A disposable used to finish the operation.
///
/// When an auto-resize operation is in progress any resize events received will not be
/// cause the new size to be written to the and
/// properties.
///
protected IDisposable BeginAutoSizing()
{
AutoSizing = true;
return Disposable.Create(() => AutoSizing = false);
}
///
/// Carries out the arrange pass of the window.
///
/// The final window size.
/// The parameter unchanged.
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl.ClientSize = finalSize;
}
return base.ArrangeOverride(PlatformImpl.ClientSize);
}
///
/// Handles a resize notification from .
///
/// The new client size.
protected virtual void HandleResized(Size clientSize)
{
if (!AutoSizing)
{
Width = clientSize.Width;
Height = clientSize.Height;
}
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
PlatformImpl.Invalidate(new Rect(clientSize));
}
///
protected override void OnAttachedToVisualTree(IRenderRoot root)
{
base.OnAttachedToVisualTree(root);
throw new InvalidOperationException(
$"Control '{GetType().Name}' is a top level control and cannot be added as a child.");
}
///
/// Tries to get a service from an , throwing an
/// exception if not found.
///
/// The service type.
/// The resolver.
/// The service.
private static T TryGetService(IPerspexDependencyResolver resolver) where T : class
{
var result = resolver.GetService();
if (result == null)
{
System.Diagnostics.Debug.WriteLineIf(
result == null,
$"Could not create {typeof(T).Name} : maybe Application.RegisterServices() wasn't called?");
}
return result;
}
///
/// Handles an activated notification from .
///
private void HandleActivated()
{
if (Activated != null)
{
Activated(this, EventArgs.Empty);
}
var scope = this as IFocusScope;
if (scope != null)
{
FocusManager.Instance.SetFocusScope(scope);
}
IsActive = true;
}
///
/// Handles a closed notification from .
///
private void HandleClosed()
{
if (Closed != null)
{
Closed(this, EventArgs.Empty);
}
}
///
/// Handles a deactivated notification from .
///
private void HandleDeactivated()
{
IsActive = false;
if (Deactivated != null)
{
Deactivated(this, EventArgs.Empty);
}
}
///
/// Handles input from .
///
/// The event args.
private void HandleInput(RawInputEventArgs e)
{
_inputManager.Process(e);
}
///
/// Handles a layout request from .
///
private void HandleLayoutNeeded()
{
Dispatcher.UIThread.InvokeAsync(LayoutManager.ExecuteLayoutPass, DispatcherPriority.Render);
}
///
/// Handles a layout completion request from .
///
private void HandleLayoutCompleted()
{
_renderQueueManager?.InvalidateRender(this);
}
}
}