// 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.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; using Perspex.Interactivity; using Perspex.LogicalTree; using Perspex.Rendering; using Perspex.Styling; namespace Perspex.Controls { /// /// Base class for Perspex controls. /// /// /// The control class extends and adds the following features: /// /// - An inherited . /// - A property to allow user-defined data to be attached to the control. /// - A collection of class strings for custom styling. /// - Implements to allow styling to work on the control. /// - Implements to form part of a logical tree. /// public class Control : InputElement, IControl, ISetLogicalParent { /// /// Defines the property. /// public static readonly PerspexProperty DataContextProperty = PerspexProperty.Register( nameof(DataContext), inherits: true, notifying: DataContextNotifying); /// /// Defines the property. /// public static readonly PerspexProperty> FocusAdornerProperty = PerspexProperty.Register>(nameof(FocusAdorner)); /// /// Defines the property. /// public static readonly PerspexProperty ParentProperty = PerspexProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. /// public static readonly PerspexProperty TagProperty = PerspexProperty.Register(nameof(Tag)); /// /// Defines the property. /// public static readonly PerspexProperty TemplatedParentProperty = PerspexProperty.Register(nameof(TemplatedParent), inherits: true); /// /// Event raised when an element wishes to be scrolled into view. /// public static readonly RoutedEvent RequestBringIntoViewEvent = RoutedEvent.Register("RequestBringIntoView", RoutingStrategies.Bubble); private IControl _parent; private readonly Classes _classes = new Classes(); private DataTemplates _dataTemplates; private IControl _focusAdorner; private bool _isAttachedToLogicalTree; private IPerspexList _logicalChildren; private INameScope _nameScope; private Styles _styles; private Subject _styleDetach = new Subject(); /// /// Initializes static members of the class. /// static Control() { AffectsMeasure(IsVisibleProperty); PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); } /// /// Initializes a new instance of the class. /// public Control() { _nameScope = this as INameScope; } /// /// Raised when the control is attached to a rooted logical tree. /// public event EventHandler AttachedToLogicalTree; /// /// Raised when the control is detached from a rooted logical tree. /// public event EventHandler DetachedFromLogicalTree; /// /// Occurs when the property changes. /// /// /// This event will be raised when the property has changed and /// all subscribers to that change have been notified. /// public event EventHandler DataContextChanged; /// /// Gets or sets the control's classes. /// /// /// /// Classes can be used to apply user-defined styling to controls, or to allow controls /// that share a common purpose to be easily selected. /// /// /// Even though this property can be set, the setter is only intended for use in object /// initializers. Assigning to this property does not change the underlying collection, /// it simply clears the existing collection and addds the contents of the assigned /// collection. /// /// public Classes Classes { get { return _classes; } set { if (_classes != value) { _classes.Replace(value); } } } /// /// Gets or sets the control's data context. /// /// /// The data context is an inherited property that specifies the default object that will /// be used for data binding. /// public object DataContext { get { return GetValue(DataContextProperty); } set { SetValue(DataContextProperty, value); } } /// /// Gets or sets the control's focus adorner. /// public ITemplate FocusAdorner { get { return GetValue(FocusAdornerProperty); } set { SetValue(FocusAdornerProperty, value); } } /// /// Gets or sets the data templates for the control. /// /// /// Each control may define data templates which are applied to the control itself and its /// children. /// public DataTemplates DataTemplates { get { return _dataTemplates ?? (_dataTemplates = new DataTemplates()); } set { _dataTemplates = value; } } /// /// Gets or sets the styles for the control. /// /// /// Styles for the entire application are added to the Application.Styles collection, but /// each control may in addition define its own styles which are applied to the control /// itself and its children. /// public Styles Styles { get { return _styles ?? (_styles = new Styles()); } set { _styles = value; } } /// /// Gets the control's logical parent. /// public IControl Parent => _parent; /// /// Gets or sets a user-defined object attached to the control. /// public object Tag { get { return GetValue(TagProperty); } set { SetValue(TagProperty, value); } } /// /// Gets the control whose lookless template this control is part of. /// public ITemplatedControl TemplatedParent { get { return GetValue(TemplatedParentProperty); } internal set { SetValue(TemplatedParentProperty, value); } } /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; /// /// Gets the control's logical parent. /// ILogical ILogical.LogicalParent => Parent; /// /// Gets the control's logical children. /// IPerspexReadOnlyList ILogical.LogicalChildren => LogicalChildren; /// IPerspexReadOnlyList IStyleable.Classes => Classes; /// /// Gets the type by which the control is styled. /// /// /// Usually controls are styled by their own type, but there are instances where you want /// a control to be styled by its base type, e.g. creating SpecialButton that /// derives from Button and adds extra functionality but is still styled as a regular /// Button. /// Type IStyleable.StyleKey => GetType(); /// IObservable IStyleable.StyleDetach => _styleDetach; /// IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// /// Gets a value which indicates whether a change to the is in /// the process of being notified. /// protected bool IsDataContextChanging { get; private set; } /// /// Gets the control's logical children. /// protected IPerspexList LogicalChildren { get { if (_logicalChildren == null) { var list = new PerspexList(); LogicalChildren = list; } return _logicalChildren; } set { Contract.Requires(value != null); if (_logicalChildren != value) { if (_logicalChildren != null) { _logicalChildren.CollectionChanged -= LogicalChildrenCollectionChanged; } } if (value is PerspexList) { ((PerspexList)value).ResetBehavior = ResetBehavior.Remove; } _logicalChildren = value; _logicalChildren.CollectionChanged += LogicalChildrenCollectionChanged; } } /// /// Gets the collection in a form that allows adding and removing /// pseudoclasses. /// protected IPseudoClasses PseudoClasses => Classes; /// /// Sets the control's logical parent. /// /// The parent. void ISetLogicalParent.SetParent(ILogical parent) { var old = Parent; if (parent != old) { if (old != null && parent != null) { throw new InvalidOperationException("The Control already has a parent."); } InheritanceParent = parent as PerspexObject; _parent = (IControl)parent; var root = FindStyleRoot(old); if (root != null) { var e = new LogicalTreeAttachmentEventArgs(root); OnDetachedFromLogicalTree(e); } root = FindStyleRoot(this); if (root != null) { var e = new LogicalTreeAttachmentEventArgs(root); OnAttachedToLogicalTree(e); } RaisePropertyChanged(ParentProperty, old, _parent, BindingPriority.LocalValue); } } /// /// Adds a pseudo-class to be set when a property is true. /// /// The property. /// The pseudo-class. protected static void PseudoClass(PerspexProperty property, string className) { PseudoClass(property, x => x, className); } /// /// Adds a pseudo-class to be set when a property equals a certain value. /// /// The type of the property. /// The property. /// Returns a boolean value based on the property value. /// The pseudo-class. protected static void PseudoClass( PerspexProperty property, Func selector, string className) { Contract.Requires(property != null); Contract.Requires(selector != null); Contract.Requires(className != null); Contract.Requires(property != null); if (string.IsNullOrWhiteSpace(className)) { throw new ArgumentException("Cannot supply an empty className."); } property.Changed.Merge(property.Initialized) .Subscribe(e => { if (selector((T)e.NewValue)) { ((Control)e.Sender).PseudoClasses.Add(className); } else { ((Control)e.Sender).PseudoClasses.Remove(className); } }); } /// /// Called when the control is added to a logical tree. /// /// The event args. /// /// It is vital that if you override this method you call the base implementation; /// failing to do so will cause numerous features to not work as expected. /// protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_nameScope == null) { _nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope; } if (Name != null) { _nameScope?.Register(Name, this); } _isAttachedToLogicalTree = true; PerspexLocator.Current.GetService()?.ApplyStyles(this); AttachedToLogicalTree?.Invoke(this, e); foreach (var child in LogicalChildren.OfType()) { child.OnAttachedToLogicalTree(e); } } /// /// Called when the control is removed from a logical tree. /// /// The event args. /// /// It is vital that if you override this method you call the base implementation; /// failing to do so will cause numerous features to not work as expected. /// protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { if (Name != null) { _nameScope?.Unregister(Name); } _isAttachedToLogicalTree = false; _styleDetach.OnNext(Unit.Default); this.TemplatedParent = null; DetachedFromLogicalTree?.Invoke(this, e); foreach (var child in LogicalChildren.OfType()) { child.OnDetachedFromLogicalTree(e); } } /// protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); if (IsFocused && (e.NavigationMethod == NavigationMethod.Tab || e.NavigationMethod == NavigationMethod.Directional)) { var adornerLayer = AdornerLayer.GetAdornerLayer(this); if (adornerLayer != null) { if (_focusAdorner == null) { var template = GetValue(FocusAdornerProperty); if (template != null) { _focusAdorner = template.Build(); } } if (_focusAdorner != null) { AdornerLayer.SetAdornedElement((Visual)_focusAdorner, this); adornerLayer.Children.Add(_focusAdorner); } } } } /// protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); if (_focusAdorner != null) { var adornerLayer = _focusAdorner.Parent as Panel; adornerLayer.Children.Remove(_focusAdorner); _focusAdorner = null; } } /// /// Called when the is changed and all subscribers to that change /// have been notified. /// protected virtual void OnDataContextChanged() { DataContextChanged?.Invoke(this, EventArgs.Empty); } /// /// Makes the control use a different control's logical children as its own. /// /// The logical children to use. protected void RedirectLogicalChildren(IPerspexList collection) { LogicalChildren = collection; } /// /// Called when the property begins and ends being notified. /// /// The object on which the DataContext is changing. /// Whether the notifcation is beginning or ending. private static void DataContextNotifying(PerspexObject o, bool notifying) { var control = o as Control; if (control != null) { control.IsDataContextChanging = notifying; if (!notifying) { control.OnDataContextChanged(); } } } private static IStyleRoot FindStyleRoot(IStyleHost e) { while (e != null) { var root = e as IStyleRoot; if (root != null && root.StylingParent == null) { return root; } e = e.StylingParent; } return null; } private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: SetLogicalParent(e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Remove: ClearLogicalParent(e.OldItems.Cast()); break; case NotifyCollectionChangedAction.Replace: ClearLogicalParent(e.OldItems.Cast()); SetLogicalParent(e.NewItems.Cast()); break; case NotifyCollectionChangedAction.Reset: throw new NotSupportedException("Reset should not be signalled on LogicalChildren collection"); } } private void SetLogicalParent(IEnumerable children) { foreach (var i in children) { if (i.LogicalParent == null) { ((ISetLogicalParent)i).SetParent(this); } } } private void ClearLogicalParent(IEnumerable children) { foreach (var i in children) { if (i.LogicalParent == this) { ((ISetLogicalParent)i).SetParent(null); } } } } }