diff --git a/Perspex.sln b/Perspex.sln index 22b196d2c8..3b97ab21cf 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -96,6 +96,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.HtmlRenderer", "src EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup", "src\Markup\Perspex.Markup\Perspex.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup.UnitTests", "tests\Perspex.Markup.UnitTests\Perspex.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 @@ -235,6 +239,14 @@ Global {5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.Build.0 = Release|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.Build.0 = Release|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -260,5 +272,7 @@ Global {54F237D5-A70A-4752-9656-0C70B1A7B047} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {FB05AC90-89BA-4F2F-A924-F37875FB547C} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066} + {6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C} + {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection EndGlobal diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index ce9e4a540d..83e6fad5e1 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -138,6 +138,7 @@ namespace TestApplication container.Classes.Add(":container"); + DevTools.Attach(window); window.Show(); Application.Current.Run(window); } @@ -340,8 +341,11 @@ namespace TestApplication Foreground = SolidColorBrush.Parse("#727272"), Margin = new Thickness(0, 0, 0, 10) }, - new TextBox { Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", Width = 200 }, - new TextBox { AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, Width = 200, Height = 150, Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." }, + + new TextBox { Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", Width = 200}, + new TextBox { Width = 200, Watermark="Watermark"}, + new TextBox { Width = 200, Watermark="Floating Watermark", UseFloatingWatermark = true }, + new TextBox { AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, Width = 200, Height = 150, Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." }, new TextBlock { Margin = new Thickness(0, 40, 0, 0), diff --git a/samples/XamlTestApplication/Views/MainWindow.paml b/samples/XamlTestApplication/Views/MainWindow.paml index 7fcf6bf749..6fc0b89309 100644 --- a/samples/XamlTestApplication/Views/MainWindow.paml +++ b/samples/XamlTestApplication/Views/MainWindow.paml @@ -3,7 +3,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:XamlTestApplication.ViewModels;assembly=XamlTestApplication" Title="Perspex Test Application" Height="350" Width="525" SizeToContent="WidthAndHeight" > - diff --git a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs index 03e5ab6879..f7524fad43 100644 --- a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs @@ -160,7 +160,7 @@ namespace Perspex.Cairo.Media /// /// The pen. /// The rectangle bounds. - public void DrawRectange(Pen pen, Rect rect, float cornerRadius) + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius) { using (var p = SetPen(pen, rect.Size)) { diff --git a/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs b/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs index 4a4ec5002a..3b08b06812 100644 --- a/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs +++ b/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs @@ -24,8 +24,8 @@ namespace Perspex.Cairo.Media TextAlignment textAlignment, FontWeight fontWeight) { - Contract.Requires(context != null); - Contract.Requires (text != null); + Contract.Requires(context != null); + Contract.Requires (text != null); Layout = new Pango.Layout(context); _text = text; Layout.SetText(text); diff --git a/src/Gtk/Perspex.Gtk/WindowImpl.cs b/src/Gtk/Perspex.Gtk/WindowImpl.cs index 1faab58bc2..1914207d08 100644 --- a/src/Gtk/Perspex.Gtk/WindowImpl.cs +++ b/src/Gtk/Perspex.Gtk/WindowImpl.cs @@ -16,7 +16,7 @@ namespace Perspex.Gtk public class WindowImpl : Gtk.Window, IWindowImpl { - private TopLevel _owner; + private IInputRoot _inputRoot; private IPlatformHandle _windowHandle; @@ -105,9 +105,9 @@ namespace Perspex.Gtk return new Point(point.X + x, point.Y + y); } - public void SetOwner(TopLevel owner) + public void SetInputRoot(IInputRoot inputRoot) { - _owner = owner; + _inputRoot = inputRoot; } public void SetTitle(string title) @@ -158,7 +158,7 @@ namespace Perspex.Gtk var e = new RawMouseEventArgs( GtkMouseDevice.Instance, evnt.Time, - _owner, + _inputRoot, evnt.Button == 0 ? RawMouseEventType.LeftButtonDown : evnt.Button == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, @@ -172,7 +172,7 @@ namespace Perspex.Gtk var e = new RawMouseEventArgs( GtkMouseDevice.Instance, evnt.Time, - _owner, + _inputRoot, evnt.Button == 0 ? RawMouseEventType.LeftButtonUp : evnt.Button == 1 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp, @@ -241,7 +241,7 @@ namespace Perspex.Gtk var e = new RawMouseEventArgs( GtkMouseDevice.Instance, evnt.Time, - _owner, + _inputRoot, RawMouseEventType.Move, position, GetModifierKeys(evnt.State)); Input(e); diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index e5af8d5d4c..933a39dbe4 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -12,6 +12,7 @@ using OmniXaml.Builder; using OmniXaml.TypeConversion; using OmniXaml.Typing; using Perspex.Controls; +using Perspex.Input; using Perspex.Markup.Xaml.Templates; using Perspex.Markup.Xaml.Converters; using Perspex.Markup.Xaml.DataBinding; @@ -21,23 +22,37 @@ using Perspex.Media.Imaging; using Perspex.Metadata; using Perspex.Platform; using Perspex.Styling; +using Perspex.Controls.Primitives; namespace Perspex.Markup.Xaml.Context { - public class PerspexWiringContext : IWiringContext + public class PerspexWiringContext : WiringContext { - private readonly WiringContext _context; private const string PerspexNs = "https://github.com/perspex"; public PerspexWiringContext(ITypeFactory typeFactory) + : this(typeFactory, new TypeFeatureProvider(GetContentPropertyProvider(), GetConverterProvider())) { - var featureProvider = new TypeFeatureProvider(GetContentPropertyProvider(), GetConverterProvider()); + } + public PerspexWiringContext(ITypeFactory typeFactory, TypeFeatureProvider featureProvider) + : base(CreateTypeContext(typeFactory, featureProvider), featureProvider) + { + } + + private static ITypeContext CreateTypeContext(ITypeFactory typeFactory, TypeFeatureProvider featureProvider) + { var xamlNamespaceRegistry = CreateXamlNamespaceRegistry(); var perspexPropertyBinder = new PerspexPropertyBinder(featureProvider.ConverterProvider); - var xamlTypeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider, perspexPropertyBinder); - var typeContext = new TypeContext(xamlTypeRepository, xamlNamespaceRegistry, typeFactory); - _context = new WiringContext(typeContext, featureProvider); + var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider, perspexPropertyBinder); + + typeRepository.RegisterMetadata(new Metadata().WithMemberDependency(x => x.Value, x => x.Property)); + typeRepository.RegisterMetadata( + new Metadata() + .WithMemberDependency(x => x.SelectedIndex, x => x.Items) + .WithMemberDependency(x => x.SelectedItem, x => x.Items)); + + return new TypeContext(typeRepository, xamlNamespaceRegistry, typeFactory); } private static XamlNamespaceRegistry CreateXamlNamespaceRegistry() @@ -88,6 +103,7 @@ namespace Perspex.Markup.Xaml.Context new TypeConverterRegistration(typeof(Thickness), new ThicknessTypeConverter()), new TypeConverterRegistration(typeof(Selector), new SelectorTypeConverter()), new TypeConverterRegistration(typeof(TimeSpan), new TimeSpanTypeConverter()), + new TypeConverterRegistration(typeof(KeyGesture), new KeyGestureConverter()) }; typeConverterProvider.AddAll(converters); @@ -114,9 +130,5 @@ namespace Perspex.Markup.Xaml.Context return contentPropertyProvider; } - - public ITypeContext TypeContext => _context.TypeContext; - - public ITypeFeatureProvider FeatureProvider => _context.FeatureProvider; } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs new file mode 100644 index 0000000000..ebf1159d76 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Converters/KeyGestureConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using OmniXaml.TypeConversion; +using Perspex.Input; + +namespace Perspex.Markup.Xaml.Converters +{ + class KeyGestureConverter : ITypeConverter + { + public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType) + { + return false; + } + + public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value) + { + return KeyGesture.Parse((string)value); + } + + public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 06eb0f4792..f0533e2688 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -39,6 +39,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs new file mode 100644 index 0000000000..e5dc9f2810 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -0,0 +1,102 @@ +// 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.Subjects; + +namespace Perspex.Markup.Binding +{ + internal abstract class ExpressionNode : IObservable + { + private object _target; + + private Subject _subject; + + private ExpressionValue _value = ExpressionValue.None; + + public ExpressionNode Next { get; set; } + + public object Target + { + get { return _target; } + set + { + if (_target != null) + { + Unsubscribe(_target); + } + + _target = value; + + if (_target != null) + { + SubscribeAndUpdate(_target); + } + else + { + CurrentValue = ExpressionValue.None; + } + + if (Next != null) + { + Next.Target = CurrentValue.Value; + } + } + } + + public ExpressionValue CurrentValue + { + get + { + return _value; + } + + set + { + _value = value; + + if (Next != null) + { + Next.Target = value.Value; + } + + if (_subject != null) + { + _subject.OnNext(value); + } + } + } + + public virtual bool SetValue(object value) + { + return Next?.SetValue(value) ?? false; + } + + public virtual IDisposable Subscribe(IObserver observer) + { + if (Next != null) + { + return Next.Subscribe(observer); + } + else + { + if (_subject == null) + { + _subject = new Subject(); + } + + observer.OnNext(CurrentValue); + return _subject.Subscribe(observer); + } + } + + protected virtual void SubscribeAndUpdate(object target) + { + CurrentValue = new ExpressionValue(target); + } + + protected virtual void Unsubscribe(object target) + { + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs new file mode 100644 index 0000000000..953b1728b0 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -0,0 +1,29 @@ +// 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 Perspex.Markup.Binding.Parsers; + +namespace Perspex.Markup.Binding +{ + internal static class ExpressionNodeBuilder + { + public static ExpressionNode Build(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + { + throw new ArgumentException("'expression' may not be empty."); + } + + var reader = new Reader(expression); + var node = ExpressionParser.Parse(reader); + + if (!reader.End) + { + throw new ExpressionParseException(reader, "Expected end of expression."); + } + + return node; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs new file mode 100644 index 0000000000..bd0b08b7c3 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -0,0 +1,88 @@ +// 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.Linq; +using System.Reactive; +using System.Reactive.Disposables; + +namespace Perspex.Markup.Binding +{ + /// + /// Observes the value of an expression on a root object. + /// + public class ExpressionObserver : ObservableBase + { + private int _count; + private ExpressionNode _node; + + /// + /// Initializes a new instance of the class. + /// + /// The root object. + /// The expression. + public ExpressionObserver(object root, string expression) + { + Root = root; + _node = ExpressionNodeBuilder.Build(expression); + } + + /// + /// Attempts to set the value of a property expression. + /// + /// The value to set. + /// + /// True if the value could be set; false if the expression does not evaluate to a + /// property. + /// + public bool SetValue(object value) + { + IncrementCount(); + + try + { + return _node.SetValue(value); + } + finally + { + DecrementCount(); + } + } + + /// + /// Gets the root object that the expression is being observed on. + /// + public object Root { get; } + + /// + protected override IDisposable SubscribeCore(IObserver observer) + { + IncrementCount(); + + var subscription = _node.Subscribe(observer); + + return Disposable.Create(() => + { + DecrementCount(); + subscription.Dispose(); + }); + } + + private void IncrementCount() + { + if (_count++ == 0) + { + _node.Target = Root; + } + } + + private void DecrementCount() + { + if (--_count == 0) + { + _node.Target = null; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs b/src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs new file mode 100644 index 0000000000..06505e1de8 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs @@ -0,0 +1,24 @@ +// 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 Perspex.Markup.Binding.Parsers; + +namespace Perspex.Markup.Binding +{ + public class ExpressionParseException : Exception + { + internal ExpressionParseException(int column, string message) + : base(message) + { + Column = column; + } + + internal ExpressionParseException(Reader r, string message) + : this(r.Position, message) + { + } + + public int Column { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs new file mode 100644 index 0000000000..2441acda73 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs @@ -0,0 +1,38 @@ +// 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; + +namespace Perspex.Markup.Binding +{ + /// + /// Holds the value for an . + /// + public struct ExpressionValue + { + /// + /// An that has no value. + /// + public static readonly ExpressionValue None = new ExpressionValue(); + + /// + /// Initializes a new instance of the struct. + /// + /// + public ExpressionValue(object value) + { + HasValue = true; + Value = value; + } + + /// + /// Gets a value indicating whether the evaluated expression resulted in a value. + /// + public bool HasValue { get; } + + /// + /// Gets a the result of the expression. + /// + public object Value { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/IndexerNode.cs b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs new file mode 100644 index 0000000000..953852a6db --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs @@ -0,0 +1,106 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Reflection; + +namespace Perspex.Markup.Binding +{ + internal class IndexerNode : ExpressionNode + { + private int[] _intArgs; + + public IndexerNode(IList arguments) + { + Arguments = arguments; + + var intArgs = Arguments.OfType().ToArray(); + + if (intArgs.Length == arguments.Count) + { + _intArgs = intArgs; + } + } + + public IList Arguments { get; } + + protected override void SubscribeAndUpdate(object target) + { + CurrentValue = GetValue(target); + + var incc = target as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += CollectionChanged; + } + } + + protected override void Unsubscribe(object target) + { + var incc = target as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged -= CollectionChanged; + } + } + + private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + bool update = false; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + update = _intArgs[0] >= e.NewStartingIndex; + break; + case NotifyCollectionChangedAction.Remove: + update = _intArgs[0] >= e.OldStartingIndex; + break; + case NotifyCollectionChangedAction.Replace: + update = _intArgs[0] >= e.NewStartingIndex && + _intArgs[0] < e.NewStartingIndex + e.NewItems.Count; + break; + case NotifyCollectionChangedAction.Move: + update = (_intArgs[0] >= e.NewStartingIndex && + _intArgs[0] < e.NewStartingIndex + e.NewItems.Count) || + (_intArgs[0] >= e.OldStartingIndex && + _intArgs[0] < e.OldStartingIndex + e.OldItems.Count); + break; + case NotifyCollectionChangedAction.Reset: + update = true; + break; + } + + if (update) + { + CurrentValue = GetValue(sender); + } + } + + private ExpressionValue GetValue(object target) + { + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + + if (typeInfo.IsArray && _intArgs != null) + { + return new ExpressionValue(((Array)target).GetValue(_intArgs)); + } + else if (target is IList && _intArgs?.Length == 1) + { + if (_intArgs[0] < list.Count) + { + return new ExpressionValue(list[_intArgs[0]]); + } + } + + return ExpressionValue.None; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs new file mode 100644 index 0000000000..d20972c639 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -0,0 +1,40 @@ +// 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.Globalization; +using System.Reactive.Linq; + +namespace Perspex.Markup.Binding +{ + internal class LogicalNotNode : ExpressionNode + { + public override bool SetValue(object value) + { + throw new NotSupportedException("Cannot set a negated binding."); + } + + public override IDisposable Subscribe(IObserver observer) + { + return Next.Select(x => Negate(x)).Subscribe(observer); + } + + private ExpressionValue Negate(ExpressionValue v) + { + if (v.HasValue) + { + try + { + var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture); + return new ExpressionValue(!boolean); + } + catch + { + // TODO: Maybe should log something here. + } + } + + return ExpressionValue.None; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs new file mode 100644 index 0000000000..bba9226a10 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs @@ -0,0 +1,67 @@ +// 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; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class ArgumentListParser + { + public static IList Parse(Reader r, char open, char close) + { + if (r.Peek == open) + { + var result = new List(); + + r.Take(); + + while (!r.End) + { + var literal = LiteralParser.Parse(r); + + if (literal != null) + { + result.Add(literal); + } + else + { + throw new ExpressionParseException(r, "Expected integer."); + } + + r.SkipWhitespace(); + + if (r.End) + { + throw new ExpressionParseException(r, "Expected ','."); + } + else if (r.TakeIf(close)) + { + return result; + } + else + { + if (r.Take() != ',') + { + throw new ExpressionParseException(r, "Expected ','."); + } + + r.SkipWhitespace(); + } + } + + if (!r.End) + { + r.Take(); + return result; + } + else + { + throw new ExpressionParseException(r, "Expected ']'."); + } + } + + return null; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs new file mode 100644 index 0000000000..300585f730 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs @@ -0,0 +1,125 @@ +// 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.Linq; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class ExpressionParser + { + public static ExpressionNode Parse(Reader r) + { + var nodes = new List(); + var state = State.Start; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(r, nodes); + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r, "Unexpected end of expression."); + } + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault(); + } + + private static State ParseStart(Reader r, IList nodes) + { + if (ParseNot(r)) + { + nodes.Add(new LogicalNotNode()); + return State.Start; + } + else + { + var identifier = IdentifierParser.Parse(r); + + if (identifier != null) + { + nodes.Add(new PropertyAccessorNode(identifier)); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(Reader r, IList nodes) + { + if (ParseMemberAccessor(r)) + { + return State.BeforeMember; + } + else + { + var args = ArgumentListParser.Parse(r, '[', ']'); + + if (args != null) + { + if (args.Count == 0) + { + throw new ExpressionParseException(r, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode(args)); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseBeforeMember(Reader r, IList nodes) + { + var identifier = IdentifierParser.Parse(r); + + if (identifier != null) + { + nodes.Add(new PropertyAccessorNode(identifier)); + return State.AfterMember; + } + + return State.End; + } + + private static bool ParseNot(Reader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(Reader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + AfterMember, + BeforeMember, + End, + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs new file mode 100644 index 0000000000..6f3c760d74 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs @@ -0,0 +1,51 @@ +// 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.Globalization; +using System.Text; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class IdentifierParser + { + public static string Parse(Reader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + var result = new StringBuilder(); + + while (!r.End && IsValidIdentifierChar(r.Peek)) + { + result.Append(r.Take()); + } + + return result.ToString(); + } + else + { + return null; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c)) + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs new file mode 100644 index 0000000000..08056f4aab --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs @@ -0,0 +1,34 @@ +// 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.Text; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class LiteralParser + { + public static object Parse(Reader r) + { + if (char.IsDigit(r.Peek)) + { + StringBuilder result = new StringBuilder(); + + while (!r.End) + { + if (char.IsDigit(r.Peek)) + { + result.Append(r.Take()); + } + else + { + break; + } + } + + return int.Parse(result.ToString()); + } + + return null; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs b/src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs new file mode 100644 index 0000000000..34c2746487 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs @@ -0,0 +1,44 @@ +// 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; + +namespace Perspex.Markup.Binding.Parsers +{ + internal class Reader + { + private string _s; + private int _i; + + public Reader(string s) + { + _s = s; + } + + public bool End => _i == _s.Length; + public char Peek => _s[_i]; + public int Position => _i; + public char Take() => _s[_i++]; + + public void SkipWhitespace() + { + while (!End && char.IsWhiteSpace(Peek)) + { + Take(); + } + } + + public bool TakeIf(char c) + { + if (Peek == c) + { + Take(); + return true; + } + else + { + return false; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs new file mode 100644 index 0000000000..08ecafac6a --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -0,0 +1,140 @@ +// 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.ComponentModel; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Perspex.Markup.Binding +{ + internal class PropertyAccessorNode : ExpressionNode + { + private PropertyInfo _propertyInfo; + private IDisposable _subscription; + + public PropertyAccessorNode(string propertyName) + { + PropertyName = propertyName; + } + + public string PropertyName { get; } + + public override bool SetValue(object value) + { + if (Next != null) + { + return Next.SetValue(value); + } + else + { + if (_propertyInfo != null) + { + _propertyInfo.SetValue(Target, value); + return true; + } + + return false; + } + } + + protected override void SubscribeAndUpdate(object target) + { + bool set = false; + + if (target != null) + { + _propertyInfo = target.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName); + + if (_propertyInfo != null) + { + ReadValue(target); + set = true; + + var inpc = target as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged += PropertyChanged; + } + } + } + else + { + _propertyInfo = null; + } + + if (!set) + { + CurrentValue = ExpressionValue.None; + } + } + + protected override void Unsubscribe(object target) + { + var inpc = target as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged -= PropertyChanged; + } + } + + private void ReadValue(object target) + { + var value = _propertyInfo.GetValue(target); + var observable = value as IObservable; + var task = value as Task; + bool set = false; + + if (observable != null) + { + CurrentValue = ExpressionValue.None; + set = true; + _subscription = observable + .ObserveOn(SynchronizationContext.Current) + .Subscribe(x => CurrentValue = new ExpressionValue(x)); + } + else if (task != null) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + if (task.Status == TaskStatus.RanToCompletion) + { + CurrentValue = new ExpressionValue(resultProperty.GetValue(task)); + set = true; + } + else + { + task.ContinueWith( + x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + } + } + } + else + { + CurrentValue = new ExpressionValue(value); + set = true; + } + + if (!set) + { + CurrentValue = ExpressionValue.None; + } + } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName) + { + ReadValue(sender); + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj new file mode 100644 index 0000000000..aee9048774 --- /dev/null +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -0,0 +1,81 @@ + + + + + 11.0 + Debug + AnyCPU + {6417E941-21BC-467B-A771-0DE389353CE6} + Library + Properties + Perspex.Markup + Perspex.Markup + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + ..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll + True + + + ..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll + True + + + ..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll + True + + + ..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll + True + + + + + + + + \ No newline at end of file diff --git a/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9076f81754 --- /dev/null +++ b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Perspex.Markup")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.Markup")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: InternalsVisibleTo("Perspex.Markup.UnitTests")] \ No newline at end of file diff --git a/src/Markup/Perspex.Markup/packages.config b/src/Markup/Perspex.Markup/packages.config new file mode 100644 index 0000000000..5bc4ae393b --- /dev/null +++ b/src/Markup/Perspex.Markup/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index ac93f82fec..f0a3c1a8ee 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -119,6 +119,9 @@ namespace Perspex private set; } + /// + /// Gets the application clipboard. + /// public IClipboard Clipboard => _clipboard.Value; /// diff --git a/src/Perspex.Base/BindingPriority.cs b/src/Perspex.Base/BindingPriority.cs new file mode 100644 index 0000000000..e26898d202 --- /dev/null +++ b/src/Perspex.Base/BindingPriority.cs @@ -0,0 +1,47 @@ +// 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. + +namespace Perspex +{ + /// + /// The priority of a binding. + /// + public enum BindingPriority + { + /// + /// A value that comes from an animation. + /// + Animation = -1, + + /// + /// A local value. + /// + LocalValue = 0, + + /// + /// A triggered style binding. + /// + /// + /// A style trigger is a selector such as .class which overrides a + /// binding. In this way, a basic control can have + /// for example a Background from the templated parent which changes when the + /// control has the :pointerover class. + /// + StyleTrigger, + + /// + /// A binding to a property on the templated parent. + /// + TemplatedParent, + + /// + /// A style binding. + /// + Style, + + /// + /// The binding is uninitialized. + /// + Unset = int.MaxValue, + } +} \ No newline at end of file diff --git a/src/Perspex.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Perspex.Base/Metadata/XmlnsDefinitionAttribute.cs index 63371bf80f..8e9fddc50b 100644 --- a/src/Perspex.Base/Metadata/XmlnsDefinitionAttribute.cs +++ b/src/Perspex.Base/Metadata/XmlnsDefinitionAttribute.cs @@ -1,21 +1,35 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// 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; namespace Perspex.Metadata { + /// + /// Maps an XML namespace to a CLR namespace for use in XAML. + /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public class XmlnsDefinitionAttribute : Attribute { - public string XmlNamespace { get; set; } - public string ClrNamespace { get; set; } - + /// + /// Initializes a new instance of the class. + /// + /// The URL of the XML namespace. + /// The CLR namespace. public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace) { XmlNamespace = xmlNamespace; ClrNamespace = clrNamespace; } + + /// + /// Gets or sets the URL of the XML namespace. + /// + public string XmlNamespace { get; set; } + + /// + /// Gets or sets the CLR namespace. + /// + public string ClrNamespace { get; set; } } } diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index 258afdc1b4..abe7582049 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 8a1d05fcb7..7a0e211f2c 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -15,48 +15,6 @@ using Serilog.Core.Enrichers; namespace Perspex { - /// - /// The priority of a binding. - /// - public enum BindingPriority - { - /// - /// A value that comes from an animation. - /// - Animation = -1, - - /// - /// A local value. - /// - LocalValue = 0, - - /// - /// A triggered style binding. - /// - /// - /// A style trigger is a selector such as .class which overrides a - /// binding. In this way, a basic control can have - /// for example a Background from the templated parent which changes when the - /// control has the :pointerover class. - /// - StyleTrigger, - - /// - /// A binding to a property on the templated parent. - /// - TemplatedParent, - - /// - /// A style binding. - /// - Style, - - /// - /// The binding is uninitialized. - /// - Unset = int.MaxValue, - } - /// /// An object with support. /// @@ -112,11 +70,15 @@ namespace Perspex foreach (var property in GetRegisteredProperties()) { + object value = property.IsDirect ? + property.Getter(this) : + property.GetDefaultValue(GetType()); + var e = new PerspexPropertyChangedEventArgs( this, property, PerspexProperty.UnsetValue, - property.GetDefaultValue(GetType()), + value, BindingPriority.Unset); property.NotifyInitialized(e); @@ -222,7 +184,7 @@ namespace Perspex binding.Mode; var sourceBinding = value as BindingDescriptor; - if (sourceBinding == null && mode != BindingMode.OneWay) + if (sourceBinding == null && mode > BindingMode.OneWay) { throw new InvalidOperationException("Can only bind OneWay to plain IObservable."); } @@ -253,7 +215,7 @@ namespace Perspex /// A collection of definitions. public static IEnumerable GetRegisteredProperties(Type type) { - Contract.Requires(type != null); + Contract.Requires(type != null); TypeInfo i = type.GetTypeInfo(); @@ -301,8 +263,8 @@ namespace Perspex /// public static void Register(Type type, PerspexProperty property) { - Contract.Requires(type != null); - Contract.Requires(property != null); + Contract.Requires(type != null); + Contract.Requires(property != null); List list; @@ -338,7 +300,7 @@ namespace Perspex /// The property. public void ClearValue(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); SetValue(property, PerspexProperty.UnsetValue); } @@ -350,7 +312,7 @@ namespace Perspex /// An observable. public IObservable GetObservable(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); return new PerspexObservable( observer => @@ -383,7 +345,7 @@ namespace Perspex /// An observable. public IObservable GetObservable(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); return GetObservable((PerspexProperty)property).Cast(); } @@ -425,27 +387,34 @@ namespace Perspex /// The value. public object GetValue(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); - object result; - - PriorityValue value; - - if (_values.TryGetValue(property, out value)) + if (property.IsDirect) { - result = value.Value; + return GetRegistered(property).Getter(this); } else { - result = PerspexProperty.UnsetValue; - } + object result = PerspexProperty.UnsetValue; + PriorityValue value; - if (result == PerspexProperty.UnsetValue) - { - result = GetDefaultValue(property); - } + if (!IsRegistered(property)) + { + ThrowNotRegistered(property); + } - return result; + if (_values.TryGetValue(property, out value)) + { + result = value.Value; + } + + if (result == PerspexProperty.UnsetValue) + { + result = GetDefaultValue(property); + } + + return result; + } } /// @@ -456,9 +425,16 @@ namespace Perspex /// The value. public T GetValue(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); - return (T)GetValue((PerspexProperty)property); + if (property.IsDirect) + { + return ((PerspexProperty)GetRegistered(property)).Getter(this); + } + else + { + return (T)GetValue((PerspexProperty)property); + } } /// @@ -479,7 +455,7 @@ namespace Perspex /// True if the property is set, otherwise false. public bool IsSet(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); return _values.ContainsKey(property); } @@ -491,24 +467,7 @@ namespace Perspex /// True if the property is registered, otherwise false. public bool IsRegistered(PerspexProperty property) { - Type type = GetType(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - if (list.Contains(property)) - { - return true; - } - } - - type = type.GetTypeInfo().BaseType; - } - - return false; + return FindRegistered(property) != null; } /// @@ -522,37 +481,50 @@ namespace Perspex object value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - - PriorityValue v; - var originalValue = value; + Contract.Requires(property != null); - if (!IsRegistered(property)) + if (property.IsDirect) { - throw new InvalidOperationException(string.Format( - "Property '{0}' not registered on '{1}'", - property.Name, - GetType())); - } + property = GetRegistered(property); - if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) - { - throw new InvalidOperationException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); - } + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } - if (!_values.TryGetValue(property, out v)) + property.Setter(this, value); + } + else { - if (value == PerspexProperty.UnsetValue) + PriorityValue v; + var originalValue = value; + + if (!IsRegistered(property)) + { + ThrowNotRegistered(property); + } + + if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) { - return; + throw new ArgumentException(string.Format( + "Invalid value for Property '{0}': '{1}' ({2})", + property.Name, + originalValue, + originalValue?.GetType().FullName ?? "(null)")); } - v = CreatePriorityValue(property); - _values.Add(property, v); + if (!_values.TryGetValue(property, out v)) + { + if (value == PerspexProperty.UnsetValue) + { + return; + } + + v = CreatePriorityValue(property); + _values.Add(property, v); + } + + v.SetValue(value, (int)priority); } _propertyLog.Verbose( @@ -560,7 +532,6 @@ namespace Perspex property, value, priority); - v.SetDirectValue(value, (int)priority); } /// @@ -575,9 +546,23 @@ namespace Perspex T value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + Contract.Requires(property != null); + + if (property.IsDirect) + { + property = (PerspexProperty)GetRegistered(property); + + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } - SetValue((PerspexProperty)property, value, priority); + property.Setter(this, value); + } + else + { + SetValue((PerspexProperty)property, value, priority); + } } /// @@ -594,32 +579,48 @@ namespace Perspex IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - - PriorityValue v; - IDescription description = source as IDescription; + Contract.Requires(property != null); - if (!IsRegistered(property)) + if (property.IsDirect) { - throw new InvalidOperationException(string.Format( - "Property '{0}' not registered on '{1}'", - property.Name, - GetType())); - } + property = GetRegistered(property); - if (!_values.TryGetValue(property, out v)) - { - v = CreatePriorityValue(property); - _values.Add(property, v); + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + _propertyLog.Verbose( + "Bound {Property} to {Binding} with priority LocalValue", + property, + source); + + return source.Subscribe(x => SetValue(property, x)); } + else + { + PriorityValue v; + IDescription description = source as IDescription; - _propertyLog.Verbose( - "Bound {Property} to {Binding} with priority {Priority}", - property, - source, - priority); + if (!IsRegistered(property)) + { + ThrowNotRegistered(property); + } + + if (!_values.TryGetValue(property, out v)) + { + v = CreatePriorityValue(property); + _values.Add(property, v); + } - return v.Add(source, (int)priority); + _propertyLog.Verbose( + "Bound {Property} to {Binding} with priority {Priority}", + property, + source, + priority); + + return v.Add(source, (int)priority); + } } /// @@ -637,9 +638,23 @@ namespace Perspex IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + Contract.Requires(property != null); - return Bind((PerspexProperty)property, source.Select(x => (object)x), priority); + if (property.IsDirect) + { + property = (PerspexProperty)GetRegistered(property); + + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + return source.Subscribe(x => SetValue(property, x)); + } + else + { + return Bind((PerspexProperty)property, source.Select(x => (object)x), priority); + } } /// @@ -713,6 +728,69 @@ namespace Perspex { } + /// + /// Raises the event. + /// + /// The property that has changed. + /// The old property value. + /// The new property value. + /// The priority of the binding that produced the value. + protected void RaisePropertyChanged( + PerspexProperty property, + object oldValue, + object newValue, + BindingPriority priority) + { + Contract.Requires(property != null); + + PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority); + + OnPropertyChanged(e); + property.NotifyChanged(e); + + if (PropertyChanged != null) + { + PropertyChanged(this, e); + } + + if (_inpcChanged != null) + { + PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); + _inpcChanged(this, e2); + } + } + + /// + /// Sets the backing field for a direct perspex property, raising the + /// event if the value has changed. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// + /// True if the value changed, otherwise false. + /// + protected bool SetAndRaise(PerspexProperty property, ref T field, T value) + { + if (!object.Equals(field, value)) + { + var old = field; + field = value; + RaisePropertyChanged(property, old, value, BindingPriority.LocalValue); + return true; + } + else + { + return false; + } + } + /// /// Creates a for a . /// @@ -772,6 +850,59 @@ namespace Perspex } } + /// + /// Given a returns a registered perspex property that is + /// equal. + /// + /// The property. + /// The registered property or null if not found. + /// + /// Calling AddOwner on a direct PerspexProperty creates new new PerspexProperty with + /// an overridden getter and setter. This property is a different object but is equal + /// according to . + /// + public PerspexProperty FindRegistered(PerspexProperty property) + { + Type type = GetType(); + + while (type != null) + { + List list; + + if (s_registered.TryGetValue(type, out list)) + { + var index = list.IndexOf(property); + + if (index != -1) + { + return list[index]; + } + } + + type = type.GetTypeInfo().BaseType; + } + + return null; + } + + /// + /// Given a returns a registered perspex property that is + /// equal or throws if not found. + /// + /// The property. + /// The registered property. + public PerspexProperty GetRegistered(PerspexProperty property) + { + var result = FindRegistered(property); + + if (result == null) + { + ThrowNotRegistered(property); + } + + return result; + } + /// /// Called when a property is changed on the current . /// @@ -801,40 +932,13 @@ namespace Perspex } /// - /// Raises the event. + /// Throws an exception indicating that the specified property is not registered on this + /// object. /// - /// The property that has changed. - /// The old property value. - /// The new property value. - /// The priority of the binding that produced the value. - private void RaisePropertyChanged( - PerspexProperty property, - object oldValue, - object newValue, - BindingPriority priority) + /// The property + private void ThrowNotRegistered(PerspexProperty p) { - Contract.Requires(property != null); - - PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); - - OnPropertyChanged(e); - property.NotifyChanged(e); - - if (PropertyChanged != null) - { - PropertyChanged(this, e); - } - - if (_inpcChanged != null) - { - PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, e2); - } + throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}"); } } } diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 9950ec9028..3bda662038 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -15,13 +15,18 @@ namespace Perspex /// /// This class is analogous to DependencyProperty in WPF. /// - public class PerspexProperty + public class PerspexProperty : IEquatable { /// /// Represents an unset property value. /// public static readonly object UnsetValue = new Unset(); + /// + /// Gets the next ID that will be allocated to a property. + /// + private static int s_nextId = 1; + /// /// The default values for the property, by type. /// @@ -43,6 +48,11 @@ namespace Perspex private readonly Dictionary> _validation = new Dictionary>(); + /// + /// Gets the ID of the property. + /// + private int _id; + /// /// Initializes a new instance of the class. /// @@ -64,9 +74,9 @@ namespace Perspex Func validate = null, bool isAttached = false) { - Contract.Requires(name != null); - Contract.Requires(valueType != null); - Contract.Requires(ownerType != null); + Contract.Requires(name != null); + Contract.Requires(valueType != null); + Contract.Requires(ownerType != null); if (name.Contains(".")) { @@ -80,6 +90,7 @@ namespace Perspex Inherits = inherits; DefaultBindingMode = defaultBindingMode; IsAttached = isAttached; + _id = s_nextId++; if (validate != null) { @@ -87,6 +98,69 @@ namespace Perspex } } + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The type of the property's value. + /// The type of the class that registers the property. + /// Gets the current value of the property. + /// Sets the value of the property. + public PerspexProperty( + string name, + Type valueType, + Type ownerType, + Func getter, + Action setter) + { + Contract.Requires(name != null); + Contract.Requires(valueType != null); + Contract.Requires(ownerType != null); + Contract.Requires(getter != null); + + if (name.Contains(".")) + { + throw new ArgumentException("'name' may not contain periods."); + } + + Name = name; + PropertyType = valueType; + OwnerType = ownerType; + Getter = getter; + Setter = setter; + IsDirect = true; + _id = s_nextId++; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// A new getter. + /// A new setter. + protected PerspexProperty( + PerspexProperty source, + Func getter, + Action setter) + { + Contract.Requires(source != null); + Contract.Requires(getter != null); + + if (!source.IsDirect) + { + throw new InvalidOperationException( + "This method can only be called on direct PerspexProperties."); + } + + Name = source.Name; + PropertyType = source.PropertyType; + OwnerType = source.OwnerType; + Getter = getter; + Setter = setter; + IsDirect = true; + _id = source._id; + } + /// /// Gets the name of the property. /// @@ -135,6 +209,11 @@ namespace Perspex /// public bool IsAttached { get; } + /// + /// Gets a value indicating whether this is a direct property. + /// + public bool IsDirect { get; } + /// /// Gets an observable that is fired when this property is initialized on a /// new instance. @@ -191,6 +270,49 @@ namespace Perspex }; } + /// + /// Gets the getter function for direct properties. + /// + internal Func Getter { get; } + + /// + /// Gets the etter function for direct properties. + /// + internal Action Setter { get; } + + /// + /// Tests two s for equality. + /// + /// The first property. + /// The second property. + /// True if the properties are equal, otherwise false. + public static bool operator ==(PerspexProperty a, PerspexProperty b) + { + if (object.ReferenceEquals(a, b)) + { + return true; + } + else if (((object)a == null) || ((object)b == null)) + { + return false; + } + else + { + return a.Equals(b); + } + } + + /// + /// Tests two s for unequality. + /// + /// The first property. + /// The second property. + /// True if the properties are equal, otherwise false. + public static bool operator !=(PerspexProperty a, PerspexProperty b) + { + return !(a == b); + } + /// /// Registers a . /// @@ -210,7 +332,7 @@ namespace Perspex Func validate = null) where TOwner : PerspexObject { - Contract.Requires(name != null); + Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, @@ -226,6 +348,34 @@ namespace Perspex return result; } + /// + /// Registers a direct . + /// + /// The type of the class that is registering the property. + /// The type of the property's value. + /// The name of the property. + /// Gets the current value of the property. + /// Sets the value of the property. + /// A + public static PerspexProperty RegisterDirect( + string name, + Func getter, + Action setter = null) + where TOwner : PerspexObject + { + Contract.Requires(name != null); + + PerspexProperty result = new PerspexProperty( + name, + typeof(TOwner), + Cast(getter), + Cast(setter)); + + PerspexObject.Register(typeof(TOwner), result); + + return result; + } + /// /// Registers an attached . /// @@ -245,7 +395,7 @@ namespace Perspex BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) { - Contract.Requires(name != null); + Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, @@ -281,7 +431,7 @@ namespace Perspex BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) { - Contract.Requires(name != null); + Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, @@ -297,6 +447,25 @@ namespace Perspex return result; } + /// + public override bool Equals(object obj) + { + var p = obj as PerspexProperty; + return p != null ? Equals(p) : false; + } + + /// + public bool Equals(PerspexProperty other) + { + return other != null && _id == other._id; + } + + /// + public override int GetHashCode() + { + return _id; + } + /// /// Returns a binding accessor that can be passed to 's [] /// operator to initiate a binding. @@ -320,7 +489,7 @@ namespace Perspex /// The default value. public object GetDefaultValue(Type type) { - Contract.Requires(type != null); + Contract.Requires(type != null); while (type != null) { @@ -346,7 +515,7 @@ namespace Perspex /// public Func GetValidationFunc(Type type) { - Contract.Requires(type != null); + Contract.Requires(type != null); while (type != null) { @@ -390,7 +559,7 @@ namespace Perspex /// The default value. public void OverrideDefaultValue(Type type, object defaultValue) { - Contract.Requires(type != null); + Contract.Requires(type != null); if (!TypeUtilities.TryCast(PropertyType, defaultValue, out defaultValue)) { @@ -416,7 +585,7 @@ namespace Perspex /// The validation function. public void OverrideValidation(Type type, Func validation) { - Contract.Requires(type != null); + Contract.Requires(type != null); if (_validation.ContainsKey(type)) { @@ -453,9 +622,37 @@ namespace Perspex _changed.OnNext(e); } + /// + /// Casts a getter function accepting a typed owner to one accepting a + /// . + /// + /// The owner type. + /// The property value type. + /// The typed function. + /// The untyped function. + private static Func Cast(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + + /// + /// Casts a setter action accepting a typed owner to one accepting a + /// . + /// + /// The owner type. + /// The property value type. + /// The typed action. + /// The untyped action. + private static Action Cast(Action f) + where TOwner : PerspexObject + { + return f != null ? (o, v) => f((TOwner)o, v) : (Action)null; + } + /// /// Casts a validation function accepting a typed owner to one accepting a - /// . + /// . /// /// The owner type. /// The property value type. diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 14fd84f528..3c81b5e91b 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -36,24 +36,87 @@ namespace Perspex defaultValue, inherits, defaultBindingMode, - Convert(validate), + Cast(validate), isAttached) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The type of the class that registers the property. + /// Gets the current value of the property. + /// Sets the value of the property. + public PerspexProperty( + string name, + Type ownerType, + Func getter, + Action setter) + : base(name, typeof(TValue), ownerType, CastParamReturn(getter), CastParams(setter)) + { + Getter = getter; + Setter = setter; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// A new getter. + /// A new setter. + private PerspexProperty( + PerspexProperty source, + Func getter, + Action setter) + : base(source, CastParamReturn(getter), CastParams(setter)) + { + Getter = getter; + Setter = setter; + } + + /// + /// Gets the getter function for direct properties. + /// + internal new Func Getter { get; } + + /// + /// Gets the etter function for direct properties. + /// + internal new Action Setter { get; } + /// /// Registers the property on another type. /// /// The type of the additional owner. /// The property. - public PerspexProperty AddOwner() + public PerspexProperty AddOwner() where TOwner : PerspexObject { + if (IsDirect) + { + throw new InvalidOperationException( + "You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty."); + } + PerspexObject.Register(typeof(TOwner), this); return this; } + /// + /// Registers the direct property on another type. + /// + /// The type of the additional owner. + /// The property. + public PerspexProperty AddOwner( + Func getter, + Action setter = null) + where TOwner : PerspexObject + { + var result = new PerspexProperty(this, CastReturn(getter), CastParam1(setter)); + PerspexObject.Register(typeof(TOwner), result); + return result; + } + /// /// Gets the default value for the property on the specified type. /// @@ -78,11 +141,59 @@ namespace Perspex } /// - /// Converts from a typed validation function to an untyped. + /// Casts a typed getter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Func CastParamReturn(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + + /// + /// Casts a typed getter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Func CastReturn(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + + /// + /// Casts a typed setter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Action CastParams(Action f) + where TOwner : PerspexObject + { + return (f != null) ? (o, v) => f((TOwner)o, (TValue)v) : (Action)null; + } + + /// + /// Casts a typed setter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Action CastParam1(Action f) + where TOwner : PerspexObject + { + return (f != null) ? (o, v) => f((TOwner)o, v) : (Action)null; + } + + /// + /// Casts a typed validation function to an untyped. /// /// The typed validation function. /// The untyped validation function. - private static Func Convert(Func f) + private static Func Cast(Func f) { return f != null ? (o, v) => f(o, (TValue)v) : (Func)null; } diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index 4be6b88861..53ecd8da99 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -105,11 +105,11 @@ namespace Perspex } /// - /// Sets the direct value for a specified priority. + /// Sets the value for a specified priority. /// /// The value. /// The priority - public void SetDirectValue(object value, int priority) + public void SetValue(object value, int priority) { GetLevel(priority).DirectValue = value; } diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index 81bd5ad250..98909d9801 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -35,7 +35,7 @@ namespace Perspex.Utilities /// True if the cast was sucessful, otherwise false. public static bool TryCast(Type to, object value, out object result) { - Contract.Requires(to != null); + Contract.Requires(to != null); if (value == null) { diff --git a/src/Perspex.Controls/Border.cs b/src/Perspex.Controls/Border.cs index 7b1e1916e2..1d8a3524d6 100644 --- a/src/Perspex.Controls/Border.cs +++ b/src/Perspex.Controls/Border.cs @@ -98,7 +98,7 @@ namespace Perspex.Controls if (borderBrush != null && borderThickness > 0) { - context.DrawRectange(new Pen(borderBrush, borderThickness), rect, cornerRadius); + context.DrawRectangle(new Pen(borderBrush, borderThickness), rect, cornerRadius); } } diff --git a/src/Perspex.Controls/Button.cs b/src/Perspex.Controls/Button.cs index 50a4871837..41736206c9 100644 --- a/src/Perspex.Controls/Button.cs +++ b/src/Perspex.Controls/Button.cs @@ -44,6 +44,9 @@ namespace Perspex.Controls public static readonly PerspexProperty CommandProperty = PerspexProperty.Register(nameof(Command)); + public static readonly PerspexProperty HotKeyProperty = + HotKeyManager.HotKeyProperty.AddOwner public static readonly PerspexProperty ParentProperty = - PerspexProperty.Register(nameof(Parent)); + PerspexProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. @@ -66,16 +64,12 @@ namespace Perspex.Controls 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 string _id; - + private string _name; private IPerspexList _logicalChildren; - private Styles _styles; /// @@ -179,22 +173,22 @@ namespace Perspex.Controls { get { - return _id; + return _name; } set { - if (_id != null) + if (_name != null) { - throw new InvalidOperationException("ID already set."); + throw new InvalidOperationException("Name already set."); } if (((IVisual)this).VisualParent != null) { - throw new InvalidOperationException("Cannot set ID : control already added to tree."); + throw new InvalidOperationException("Cannot set Name : control already added to tree."); } - _id = value; + _name = value; } } @@ -227,7 +221,7 @@ namespace Perspex.Controls /// /// Gets the control's logical parent. /// - public IControl Parent => GetValue(ParentProperty); + public IControl Parent => _parent; /// /// Gets or sets a user-defined object attached to the control. @@ -297,7 +291,7 @@ namespace Perspex.Controls throw new InvalidOperationException("The Control already has a parent."); } - SetValue(ParentProperty, parent); + SetAndRaise(ParentProperty, ref _parent, (IControl)parent); } /// diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index a465ce68a9..c2ac601bc3 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -26,11 +26,15 @@ namespace Perspex.Controls ContentControl.VerticalContentAlignmentProperty.AddOwner(); public static readonly PerspexProperty IsDropDownOpenProperty = - PerspexProperty.Register("IsDropDownOpen"); + PerspexProperty.RegisterDirect( + nameof(IsDropDownOpen), + o => o.IsDropDownOpen, + (o, v) => o.IsDropDownOpen = v); public static readonly PerspexProperty SelectionBoxItemProperty = PerspexProperty.Register("SelectionBoxItem"); + private bool _isDropDownOpen; private Popup _popup; static DropDown() @@ -64,8 +68,8 @@ namespace Perspex.Controls public bool IsDropDownOpen { - get { return GetValue(IsDropDownOpenProperty); } - set { SetValue(IsDropDownOpenProperty, value); } + get { return _isDropDownOpen; } + set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } } public object SelectionBoxItem @@ -140,6 +144,8 @@ namespace Perspex.Controls if (control != null) { + control.Measure(Size.Infinity); + SelectionBoxItem = new Rectangle { Width = control.DesiredSize.Width, diff --git a/src/Perspex.Controls/HotkeyManager.cs b/src/Perspex.Controls/HotkeyManager.cs new file mode 100644 index 0000000000..6b7fb117c1 --- /dev/null +++ b/src/Perspex.Controls/HotkeyManager.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using Perspex.Controls; +using Perspex.Controls.Utils; +using Perspex.Input; + +namespace Perspex.Controls +{ + public class HotKeyManager + { + public static PerspexProperty HotKeyProperty + = PerspexProperty.RegisterAttached("HotKey", typeof (HotKeyManager)); + + class HotkeyCommandWrapper : ICommand + { + public HotkeyCommandWrapper(IControl control) + { + Control = control; + } + + public IControl Control; + + private ICommand GetCommand() => Control.GetValue(Button.CommandProperty); + + public bool CanExecute(object parameter) => GetCommand()?.CanExecute(parameter) ?? false; + + public void Execute(object parameter) => GetCommand()?.Execute(parameter); + + //Implementation isn't needed in this case + public event EventHandler CanExecuteChanged; + } + + + class Manager + { + private readonly IControl _control; + private TopLevel _root; + private IDisposable _parentSub; + private IDisposable _hotkeySub; + private KeyGesture _hotkey; + private HotkeyCommandWrapper _wrapper; + private KeyBinding _binding; + + public Manager(IControl control) + { + _control = control; + _wrapper = new HotkeyCommandWrapper(_control); + } + + public void Init() + { + _hotkeySub = _control.GetObservable(HotKeyProperty).Subscribe(OnHotkeyChanged); + _parentSub = AncestorFinder.Create(_control, typeof (TopLevel)).Subscribe(OnParentChanged); + } + + private void OnParentChanged(IControl control) + { + Unregister(); + _root = (TopLevel) control; + Register(); + } + + private void OnHotkeyChanged(KeyGesture hotkey) + { + if (hotkey == null) + //Subscription will be recreated by static property watcher + Stop(); + else + { + Unregister(); + _hotkey = hotkey; + Register(); + } + } + + void Unregister() + { + if (_root != null && _binding != null) + _root.KeyBindings.Remove(_binding); + _binding = null; + } + + void Register() + { + if (_root != null && _hotkey != null) + { + _binding = new KeyBinding() {Gesture = _hotkey, Command = _wrapper}; + _root.KeyBindings.Add(_binding); + } + } + + void Stop() + { + Unregister(); + _parentSub.Dispose(); + _hotkeySub.Dispose(); + } + } + + static HotKeyManager() + { + HotKeyProperty.Changed.Subscribe(args => + { + var control = args.Sender as IControl; + if (args.OldValue != null|| control == null) + return; + new Manager(control).Init(); + }); + } + public static void SetHotKey(PerspexObject target, KeyGesture value) => target.SetValue(HotKeyProperty, value); + public static KeyGesture GetHotKey(PerspexObject target) => target.GetValue(HotKeyProperty); + } +} diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 2939347e67..6649b43cb0 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -33,7 +33,7 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty ItemsProperty = - PerspexProperty.Register(nameof(Items)); + PerspexProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); /// /// Defines the property. @@ -47,6 +47,7 @@ namespace Perspex.Controls public static readonly PerspexProperty MemberSelectorProperty = PerspexProperty.Register(nameof(MemberSelector)); + private IEnumerable _items = new PerspexList(); private IItemContainerGenerator _itemContainerGenerator; /// @@ -63,7 +64,7 @@ namespace Perspex.Controls public ItemsControl() { Classes.Add(":empty"); - Items = new PerspexList(); + SubscribeToItems(_items); } /// @@ -87,8 +88,8 @@ namespace Perspex.Controls /// public IEnumerable Items { - get { return GetValue(ItemsProperty); } - set { SetValue(ItemsProperty, value); } + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, value); } } /// @@ -155,30 +156,17 @@ namespace Perspex.Controls /// The event args. protected virtual void ItemsChanged(PerspexPropertyChangedEventArgs e) { + System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set items"); + var incc = e.OldValue as INotifyCollectionChanged; if (incc != null) { - incc.CollectionChanged += ItemsCollectionChanged; + incc.CollectionChanged -= ItemsCollectionChanged; } var newValue = e.NewValue as IEnumerable; - - if (newValue == null || newValue.Count() == 0) - { - Classes.Add(":empty"); - } - else - { - Classes.Remove(":empty"); - } - - incc = newValue as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged += ItemsCollectionChanged; - } + SubscribeToItems(newValue); } /// @@ -200,5 +188,28 @@ namespace Perspex.Controls Classes.Remove(":empty"); } } + + /// + /// Subscribes to an collection. + /// + /// + private void SubscribeToItems(IEnumerable items) + { + if (items == null || items.Count() == 0) + { + Classes.Add(":empty"); + } + else + { + Classes.Remove(":empty"); + } + + var incc = items as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += ItemsCollectionChanged; + } + } } } diff --git a/src/Perspex.Controls/Menu.cs b/src/Perspex.Controls/Menu.cs index db424ddcf8..3513843c37 100644 --- a/src/Perspex.Controls/Menu.cs +++ b/src/Perspex.Controls/Menu.cs @@ -28,7 +28,11 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty IsOpenProperty = - PerspexProperty.Register(nameof(IsOpen)); + PerspexProperty.RegisterDirect( + nameof(IsOpen), + o => o.IsOpen); + + private bool _isOpen; /// /// Tracks event handlers added to the root of the visual tree. @@ -50,8 +54,8 @@ namespace Perspex.Controls /// public bool IsOpen { - get { return GetValue(IsOpenProperty); } - private set { SetValue(IsOpenProperty, value); } + get { return _isOpen; } + private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } } /// diff --git a/src/Perspex.Controls/MenuItem.cs b/src/Perspex.Controls/MenuItem.cs index 99bb5cad01..0acc7ae20f 100644 --- a/src/Perspex.Controls/MenuItem.cs +++ b/src/Perspex.Controls/MenuItem.cs @@ -28,6 +28,9 @@ namespace Perspex.Controls public static readonly PerspexProperty CommandProperty = Button.CommandProperty.AddOwner(); + public static readonly PerspexProperty HotKeyProperty = + HotKeyManager.HotKeyProperty.AddOwner