diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index ff3e6df64f..afc456584a 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -60,6 +60,9 @@ MainWindow.paml + + CheckBoxPage.paml + BorderPage.paml @@ -102,6 +105,9 @@ + + Designer + diff --git a/samples/ControlCatalog/MainWindow.paml b/samples/ControlCatalog/MainWindow.paml index c692b34fc6..ac900e7516 100644 --- a/samples/ControlCatalog/MainWindow.paml +++ b/samples/ControlCatalog/MainWindow.paml @@ -9,6 +9,7 @@ + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.paml b/samples/ControlCatalog/Pages/CheckBoxPage.paml new file mode 100644 index 0000000000..bb1d991980 --- /dev/null +++ b/samples/ControlCatalog/Pages/CheckBoxPage.paml @@ -0,0 +1,15 @@ + + + CheckBox + A check box control + + + Unchecked + Checked + Disabled + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.paml.cs b/samples/ControlCatalog/Pages/CheckBoxPage.paml.cs new file mode 100644 index 0000000000..38ca6d3c3d --- /dev/null +++ b/samples/ControlCatalog/Pages/CheckBoxPage.paml.cs @@ -0,0 +1,18 @@ +using Perspex.Controls; +using Perspex.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class CheckBoxPage : UserControl + { + public CheckBoxPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + PerspexXamlLoader.Load(this); + } + } +} diff --git a/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs b/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs index 9ac24a622e..3ae72e138c 100644 --- a/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs +++ b/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using ReactiveUI; +using Perspex.Controls; namespace XamlTestApplication.ViewModels { @@ -59,6 +60,23 @@ namespace XamlTestApplication.ViewModels CollapseNodesCommand.Subscribe(_ => ExpandNodes(false)); ExpandNodesCommand = ReactiveCommand.Create(); ExpandNodesCommand.Subscribe(_ => ExpandNodes(true)); + + OpenFileCommand = ReactiveCommand.Create(); + OpenFileCommand.Subscribe(_ => + { + var ofd = new OpenFileDialog(); + + ofd.ShowAsync(); + }); + + OpenFolderCommand = ReactiveCommand.Create(); + OpenFolderCommand.Subscribe(_ => + { + var ofd = new OpenFolderDialog(); + + ofd.ShowAsync(); + }); + } public List Items { get; } @@ -68,6 +86,10 @@ namespace XamlTestApplication.ViewModels public ReactiveCommand ExpandNodesCommand { get; } + public ReactiveCommand OpenFileCommand { get; } + + public ReactiveCommand OpenFolderCommand { get; } + public void ExpandNodes(bool expanded) { foreach (var node in Nodes) diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index cadb70edac..e5155b0b95 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -6,11 +6,13 @@ Title="Perspex Test Application" Width="800" Height="600"> - + + + - + diff --git a/src/Android/Perspex.Android/SystemDialogImpl.cs b/src/Android/Perspex.Android/SystemDialogImpl.cs index 60f9de914c..d60fad8230 100644 --- a/src/Android/Perspex.Android/SystemDialogImpl.cs +++ b/src/Android/Perspex.Android/SystemDialogImpl.cs @@ -12,5 +12,10 @@ namespace Perspex.Android { throw new NotImplementedException(); } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs b/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs index 956a522002..e8fc9bc924 100644 --- a/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs +++ b/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs @@ -15,6 +15,11 @@ namespace Perspex.Cairo.Media private Size _size; private readonly string _text; + static double CorrectScale(double input) + { + return input * 0.75; + } + public FormattedTextImpl( Pango.Context context, string text, @@ -25,14 +30,14 @@ namespace Perspex.Cairo.Media FontWeight fontWeight) { Contract.Requires(context != null); - Contract.Requires (text != null); + Contract.Requires(text != null); Layout = new Pango.Layout(context); _text = text; Layout.SetText(text); Layout.FontDescription = new Pango.FontDescription { Family = fontFamily, - Size = Pango.Units.FromDouble(fontSize), + Size = Pango.Units.FromDouble(CorrectScale(fontSize)), Style = (Pango.Style)fontStyle, Weight = fontWeight.ToCairo() }; diff --git a/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs b/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs index 869bd2eb70..9789beb7c7 100644 --- a/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs +++ b/src/Gtk/Perspex.Gtk/SystemDialogImpl.cs @@ -15,7 +15,7 @@ namespace Perspex.Gtk public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) { var tcs = new TaskCompletionSource(); - var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl) parent), + var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl)parent), dialog is OpenFileDialog ? FileChooserAction.Open : FileChooserAction.Save, @@ -44,7 +44,38 @@ namespace Perspex.Gtk dlg.Hide(); dlg.Dispose(); }; - + + dlg.Close += delegate + { + tcs.TrySetResult(null); + dlg.Dispose(); + }; + dlg.Show(); + return tcs.Task; + } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + var tcs = new TaskCompletionSource(); + var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl)parent), + FileChooserAction.SelectFolder, + "Cancel", ResponseType.Cancel, + "Select Folder", ResponseType.Accept) + { + + }; + + dlg.Modal = true; + + dlg.Response += (_, args) => + { + if (args.ResponseId == ResponseType.Accept) + tcs.TrySetResult(dlg.Filename); + + dlg.Hide(); + dlg.Dispose(); + }; + dlg.Close += delegate { tcs.TrySetResult(null); diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs index a43d9a8be9..8b48e8e871 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs @@ -20,14 +20,14 @@ namespace Perspex.Markup.Xaml.Context { } - public override void SetValue(object instance, object value) + public override string ToString() { - PropertyAccessor.SetValue(instance, this, value); + return "Perspex Attachable XAML Member " + base.ToString(); } - public override string ToString() + protected override IMemberValuePlugin LookupXamlMemberValueConnector() { - return "Perspex Attachable XAML Member " + base.ToString(); + return new PerspexMemberValuePlugin(this); } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexMemberValuePlugin.cs new file mode 100644 index 0000000000..277326fcb9 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexMemberValuePlugin.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 OmniXaml.Typing; + +namespace Perspex.Markup.Xaml.Context +{ + public class PerspexMemberValuePlugin : MemberValuePlugin + { + private readonly MutableMember _xamlMember; + + public PerspexMemberValuePlugin(MutableMember xamlMember) + : base(xamlMember) + { + _xamlMember = xamlMember; + } + + public override void SetValue(object instance, object value) + { + PropertyAccessor.SetValue(instance, _xamlMember, value); + } + } +} diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs index ba88ca5c35..b7e85d73ab 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs @@ -18,9 +18,9 @@ namespace Perspex.Markup.Xaml.Context { } - public override void SetValue(object instance, object value) + protected override IMemberValuePlugin LookupXamlMemberValueConnector() { - PropertyAccessor.SetValue(instance, this, value); + return new PerspexMemberValuePlugin(this); } public override string ToString() diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs b/src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs index 0634e848ac..944b3301ec 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs @@ -43,36 +43,19 @@ namespace Perspex.Markup.Xaml.Context private static PerspexProperty FindPerspexProperty(object instance, MutableMember member) { - var target = instance as IPerspexObject; - var attached = member as PerspexAttachableXamlMember; + var target = instance as PerspexObject; if (target == null) { return null; } - PerspexProperty property; - string propertyName; - - if (attached == null) - { - propertyName = member.Name; - property = PerspexPropertyRegistry.Instance.GetRegistered((PerspexObject)target) - .FirstOrDefault(x => x.Name == propertyName); - } - else - { - // Ensure the OwnerType's static ctor has been run. - RuntimeHelpers.RunClassConstructor(attached.DeclaringType.UnderlyingType.TypeHandle); - - propertyName = attached.DeclaringType.UnderlyingType.Name + '.' + member.Name; - - property = PerspexPropertyRegistry.Instance.GetRegistered((PerspexObject)target) - .Where(x => x.IsAttached && x.OwnerType == attached.DeclaringType.UnderlyingType) - .FirstOrDefault(x => x.Name == member.Name); - } + var attached = member as PerspexAttachableXamlMember; + var propertyName = attached == null ? + member.Name : + member.DeclaringType.Name + "." + member.Name; - return property; + return PerspexPropertyRegistry.Instance.FindRegistered(target, propertyName); } private static void SetBinding( diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index a0b7a7f7a4..27005c8b4d 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit a0b7a7f7a415409497539f69e39deb457f7ca629 +Subproject commit 27005c8b4d41f2a6d3b85da7eb6ed994b57e0138 diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index ea64071aa3..984762d50f 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -42,6 +42,7 @@ + diff --git a/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs index 2447aee4f2..a2941684b4 100644 --- a/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Data/LogicalNotNode.cs @@ -11,7 +11,7 @@ namespace Perspex.Markup.Data { public override bool SetValue(object value) { - throw new NotSupportedException("Cannot set a negated binding."); + return false; } public override IDisposable Subscribe(IObserver observer) diff --git a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index 13970a9054..c52d1a3a26 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -105,7 +105,7 @@ namespace Perspex.Markup.Data.Plugins private void PropertyChanged(object sender, PropertyChangedEventArgs e) { - if (e.PropertyName == _property.Name) + if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { _changed(Value); } diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs index e49c1c3ae6..23bb0f774c 100644 --- a/src/Markup/Perspex.Markup/DefaultValueConverter.cs +++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs @@ -3,6 +3,8 @@ using System; using System.Globalization; +using System.Linq; +using System.Reflection; using Perspex.Utilities; namespace Perspex.Markup @@ -30,7 +32,9 @@ namespace Perspex.Markup { object result; - if (value != null && TypeUtilities.TryConvert(targetType, value, culture, out result)) + if (value != null && + (TypeUtilities.TryConvert(targetType, value, culture, out result) || + TryConvertEnum(value, targetType, culture, out result))) { return result; } @@ -52,5 +56,34 @@ namespace Perspex.Markup { return Convert(value, targetType, parameter, culture); } + + private bool TryConvertEnum(object value, Type targetType, CultureInfo cultur, out object result) + { + var valueTypeInfo = value.GetType().GetTypeInfo(); + var targetTypeInfo = targetType.GetTypeInfo(); + + if (valueTypeInfo.IsEnum && !targetTypeInfo.IsEnum) + { + var enumValue = (int)value; + + if (TypeUtilities.TryCast(targetType, enumValue, out result)) + { + return true; + } + } + else if (!valueTypeInfo.IsEnum && targetTypeInfo.IsEnum) + { + object intValue; + + if (TypeUtilities.TryCast(typeof(int), value, out intValue)) + { + result = Enum.ToObject(targetType, intValue); + return true; + } + } + + result = null; + return false; + } } } diff --git a/src/Perspex.Base/DirectProperty.cs b/src/Perspex.Base/DirectProperty.cs index c926f28240..7ae19ec520 100644 --- a/src/Perspex.Base/DirectProperty.cs +++ b/src/Perspex.Base/DirectProperty.cs @@ -24,11 +24,13 @@ namespace Perspex /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. May be null. + /// The property metadata. public DirectProperty( string name, Func getter, - Action setter = null) - : base(name, typeof(TOwner), new PropertyMetadata()) + Action setter, + PropertyMetadata metadata) + : base(name, typeof(TOwner), metadata) { Contract.Requires(getter != null); diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 4c0b6fd724..8998627849 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -342,16 +342,19 @@ namespace Perspex /// The name of the property. /// Gets the current value of the property. /// Sets the value of the property. + /// The default binding mode for the property. /// A public static DirectProperty RegisterDirect( string name, Func getter, - Action setter = null) + Action setter = null, + BindingMode defaultBindingMode = BindingMode.OneWay) where TOwner : IPerspexObject { Contract.Requires(name != null); - var result = new DirectProperty(name, getter, setter); + var metadata = new PropertyMetadata(defaultBindingMode); + var result = new DirectProperty(name, getter, setter, metadata); PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result); return result; } diff --git a/src/Perspex.Base/PerspexPropertyRegistry.cs b/src/Perspex.Base/PerspexPropertyRegistry.cs index 9e24fd7a2f..871837331a 100644 --- a/src/Perspex.Base/PerspexPropertyRegistry.cs +++ b/src/Perspex.Base/PerspexPropertyRegistry.cs @@ -92,7 +92,7 @@ namespace Perspex } /// - /// Finds registered on a type. + /// Finds a registered on a type. /// /// The type. /// The property. @@ -155,6 +155,7 @@ namespace Perspex Contract.Requires(name != null); var parts = name.Split('.'); + var types = GetImplementedTypes(type).ToList(); if (parts.Length < 1 || parts.Length > 2) { @@ -167,11 +168,10 @@ namespace Perspex if (parts.Length == 1) { propertyName = parts[0]; + results = results.Where(x => !x.IsAttached || types.Contains(x.OwnerType.Name)); } else { - var types = GetImplementedTypes(type); - if (!types.Contains(parts[0])) { results = results.Where(x => x.OwnerType.Name == parts[0]); diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index 3d19de5f97..c6d16d56b2 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -119,6 +119,12 @@ namespace Perspex.Utilities return true; } + if (to == typeof(string)) + { + result = Convert.ToString(value); + return true; + } + if (to.GetTypeInfo().IsEnum && from == typeof(string)) { if (Enum.IsDefined(to, (string)value)) @@ -131,9 +137,7 @@ namespace Perspex.Utilities bool containsFrom = Conversions.ContainsKey(from); bool containsTo = Conversions.ContainsKey(to); - if ((containsFrom && containsTo) || - (from == typeof(string) && containsTo) || - (to == typeof(string) && containsFrom)) + if ((containsFrom && containsTo) || (from == typeof(string) && containsTo)) { try { diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 85628e3374..490ee8899b 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -52,6 +52,13 @@ namespace Perspex.Controls.Generators /// The removed containers. IEnumerable Dematerialize(int startingIndex, int count); + /// + /// Inserts space for newly inserted containers in the index. + /// + /// The index at which space should be inserted. + /// The number of blank spaces to create. + void InsertSpace(int index, int count); + /// /// Removes a set of created containers and updates the index of later containers to fill /// the gap. diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index 2733de5770..f1f49affbf 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -85,6 +85,12 @@ namespace Perspex.Controls.Generators return result; } + /// + public virtual void InsertSpace(int index, int count) + { + _containers.InsertRange(index, Enumerable.Repeat(null, count)); + } + /// public virtual IEnumerable RemoveRange(int startingIndex, int count) { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 9feaab3d25..11a56aa556 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -68,6 +68,7 @@ namespace Perspex.Controls.Generators } else if (container != null) { + Index.Add(item, container); return container; } else diff --git a/src/Perspex.Controls/Platform/ISystemDialogImpl.cs b/src/Perspex.Controls/Platform/ISystemDialogImpl.cs index d3af982f95..f45eec8f95 100644 --- a/src/Perspex.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Perspex.Controls/Platform/ISystemDialogImpl.cs @@ -18,5 +18,7 @@ namespace Perspex.Controls.Platform /// The parent window. /// A task returning the selected filenames. Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent); + + Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent); } } diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 29e796fd75..909a1e011f 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using Perspex.Controls.Generators; using Perspex.Controls.Templates; +using Perspex.Controls.Utils; using Perspex.Input; using Perspex.Styling; @@ -237,6 +238,11 @@ namespace Perspex.Controls.Presenters switch (e.Action) { case NotifyCollectionChangedAction.Add: + if (e.NewStartingIndex + e.NewItems.Count < this.Items.Count()) + { + generator.InsertSpace(e.NewStartingIndex, e.NewItems.Count); + } + AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); break; @@ -281,7 +287,17 @@ namespace Perspex.Controls.Presenters { if (i.ContainerControl != null) { - this.Panel.Children.Add(i.ContainerControl); + if (i.Index < this.Panel.Children.Count) + { + // HACK: This will insert at the wrong place when there are null items, + // but all of this will need to be rewritten when we implement + // virtualization so hope no-one notices until then :) + this.Panel.Children.Insert(i.Index, i.ContainerControl); + } + else + { + this.Panel.Children.Add(i.ContainerControl); + } } } } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 9ec708dd6b..f6e405a24a 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -62,7 +62,7 @@ namespace Perspex.Controls.Primitives PerspexProperty.RegisterDirect( nameof(SelectedItem), o => o.SelectedItem, - (o, v) => o.SelectedItem = v); + (o, v) => o.SelectedItem = v, BindingMode.TwoWay); /// /// Defines the property. diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 7c3d7dd7cf..10e312bb2b 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -56,6 +56,12 @@ namespace Perspex.Controls.Primitives public static readonly StyledProperty FontStyleProperty = TextBlock.FontStyleProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FontWeightProperty = + TextBlock.FontWeightProperty.AddOwner(); + /// /// Defines the property. /// @@ -177,6 +183,15 @@ namespace Perspex.Controls.Primitives set { SetValue(FontStyleProperty, value); } } + /// + /// Gets or sets the font weight used to draw the control's text. + /// + public FontWeight FontWeight + { + get { return GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + /// /// Gets or sets the brush used to draw the control's text and other foreground elements. /// @@ -236,7 +251,15 @@ namespace Perspex.Controls.Primitives { if (!_templateApplied) { - VisualChildren.Clear(); + if (VisualChildren.Count > 0) + { + foreach (var child in this.GetTemplateChildren()) + { + child.SetValue(TemplatedParentProperty, null); + } + + VisualChildren.Clear(); + } if (Template != null) { @@ -303,6 +326,11 @@ namespace Perspex.Controls.Primitives /// The event args. protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e) { + if (_templateApplied && VisualChildren.Count > 0) + { + _templateApplied = false; + } + _templateApplied = false; InvalidateMeasure(); } diff --git a/src/Perspex.Controls/Primitives/ToggleButton.cs b/src/Perspex.Controls/Primitives/ToggleButton.cs index 8d464464ec..c0aedf00ba 100644 --- a/src/Perspex.Controls/Primitives/ToggleButton.cs +++ b/src/Perspex.Controls/Primitives/ToggleButton.cs @@ -3,6 +3,7 @@ using System; using Perspex.Interactivity; +using Perspex.Data; namespace Perspex.Controls.Primitives { @@ -12,7 +13,7 @@ namespace Perspex.Controls.Primitives PerspexProperty.RegisterDirect( "IsChecked", o => o.IsChecked, - (o,v) => o.IsChecked = v); + (o,v) => o.IsChecked = v, BindingMode.TwoWay); private bool _isChecked; diff --git a/src/Perspex.Controls/ScrollViewer.cs b/src/Perspex.Controls/ScrollViewer.cs index 99bf9e8933..8b5920b8e2 100644 --- a/src/Perspex.Controls/ScrollViewer.cs +++ b/src/Perspex.Controls/ScrollViewer.cs @@ -85,7 +85,7 @@ namespace Perspex.Controls /// in the control's template. /// public static readonly AttachedProperty HorizontalScrollBarVisibilityProperty = - PerspexProperty.RegisterAttached( + PerspexProperty.RegisterAttached( nameof(HorizontalScrollBarVisibility), ScrollBarVisibility.Auto); diff --git a/src/Perspex.Controls/SystemDialog.cs b/src/Perspex.Controls/SystemDialog.cs index 6eae1e9e9d..bac791dd36 100644 --- a/src/Perspex.Controls/SystemDialog.cs +++ b/src/Perspex.Controls/SystemDialog.cs @@ -5,17 +5,20 @@ using Perspex.Controls.Platform; namespace Perspex.Controls { - public abstract class FileDialog : SystemDialog + public abstract class FileDialog : FileSystemDialog { public List Filters { get; set; } = new List(); - public string InitialFileName { get; set; } - public string InitialDirectory { get; set; } + public string InitialFileName { get; set; } } + public abstract class FileSystemDialog : SystemDialog + { + public string InitialDirectory { get; set; } + } public class SaveFileDialog : FileDialog { - public string DefaultExtension { get; set; } + public string DefaultExtension { get; set; } public async Task ShowAsync(Window window = null) => @@ -31,6 +34,14 @@ namespace Perspex.Controls => PerspexLocator.Current.GetService().ShowFileDialogAsync(this, window?.PlatformImpl); } + public class OpenFolderDialog : FileSystemDialog + { + public string DefaultDirectory { get; set; } + + public Task ShowAsync(Window window = null) + => PerspexLocator.Current.GetService().ShowFolderDialogAsync(this, window?.PlatformImpl); + } + public abstract class SystemDialog { public string Title { get; set; } diff --git a/src/Perspex.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index d9e1c5520d..29a760b11f 100644 --- a/src/Perspex.Controls/TextBlock.cs +++ b/src/Perspex.Controls/TextBlock.cs @@ -102,6 +102,9 @@ namespace Perspex.Controls static TextBlock() { AffectsRender(ForegroundProperty); + AffectsRender(FontWeightProperty); + AffectsRender(FontSizeProperty); + AffectsRender(FontStyleProperty); } /// @@ -113,7 +116,8 @@ namespace Perspex.Controls this.GetObservable(TextProperty).Select(_ => Unit.Default), this.GetObservable(TextAlignmentProperty).Select(_ => Unit.Default), this.GetObservable(FontSizeProperty).Select(_ => Unit.Default), - this.GetObservable(FontStyleProperty).Select(_ => Unit.Default)) + this.GetObservable(FontStyleProperty).Select(_ => Unit.Default), + this.GetObservable(FontWeightProperty).Select(_=>Unit.Default)) .Subscribe(_ => { InvalidateFormattedText(); diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index 8a0aece63d..72f32aff1c 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -26,6 +26,16 @@ namespace Perspex.Controls public static readonly StyledProperty AcceptsTabProperty = PerspexProperty.Register("AcceptsTab"); + // TODO: Remove these when "(attached.property)" syntax is supported in bindings. They are + // bound to in the TextBox template, but ideally they should just be bound to the attached + // property. + public static readonly StyledProperty CanScrollHorizontally = + ScrollViewer.CanScrollHorizontallyProperty.AddOwner(); + public static readonly StyledProperty HorizontalScrollBarVisibilityProperty = + ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner(); + public static readonly StyledProperty VerticalScrollBarVisibilityProperty = + ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner(); + // TODO: Should CaretIndex, SelectionStart/End and Text be direct properties? public static readonly StyledProperty CaretIndexProperty = PerspexProperty.Register("CaretIndex", validate: ValidateCaretIndex); diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index fe74ebfd93..d5369840ad 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/src/Perspex.Controls/TreeViewItem.cs @@ -20,7 +20,7 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly StyledProperty IsExpandedProperty = - PerspexProperty.Register("IsExpanded"); + PerspexProperty.Register("IsExpanded", default(bool), false, Data.BindingMode.TwoWay); /// /// Defines the property. @@ -30,7 +30,7 @@ namespace Perspex.Controls private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel - { + { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, }); @@ -40,7 +40,7 @@ namespace Perspex.Controls /// Initializes static members of the class. /// static TreeViewItem() - { + { SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); diff --git a/src/Perspex.Controls/Utils/UndoRedoHelper.cs b/src/Perspex.Controls/Utils/UndoRedoHelper.cs index 9de2ca9b35..60b6839dad 100644 --- a/src/Perspex.Controls/Utils/UndoRedoHelper.cs +++ b/src/Perspex.Controls/Utils/UndoRedoHelper.cs @@ -37,7 +37,12 @@ namespace Perspex.Controls.Utils public void Undo() { - _host.UndoRedoState= (_currentNode = _currentNode?.Previous ?? _currentNode).Value; + if (_currentNode != null) + { + _currentNode = _currentNode.Previous; + } + + _host.UndoRedoState = _currentNode.Value; } public bool IsLastState => _currentNode.Next == null; @@ -62,8 +67,12 @@ namespace Perspex.Controls.Utils } public void Redo() - { - _host.UndoRedoState = (_currentNode = _currentNode?.Next ?? _currentNode).Value; + { + if (_currentNode != null) { + _currentNode = _currentNode.Next; + } + + _host.UndoRedoState = _currentNode.Value; } public void Snapshot() diff --git a/src/Perspex.HtmlRenderer/HtmlControl.cs b/src/Perspex.HtmlRenderer/HtmlControl.cs index 97d7c517e5..e5e8864933 100644 --- a/src/Perspex.HtmlRenderer/HtmlControl.cs +++ b/src/Perspex.HtmlRenderer/HtmlControl.cs @@ -549,7 +549,11 @@ namespace Perspex.Controls.Html htmlContainer.SetHtml((string) e.NewValue, control._baseCssData); control.InvalidateMeasure(); control.InvalidateVisual(); - control.InvokeMouseMove(); + + if (control.VisualRoot != null) + { + control.InvokeMouseMove(); + } } } } diff --git a/src/Perspex.Input/Gestures.cs b/src/Perspex.Input/Gestures.cs new file mode 100644 index 0000000000..48e9d87f86 --- /dev/null +++ b/src/Perspex.Input/Gestures.cs @@ -0,0 +1,59 @@ +// 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.Interactivity; + +namespace Perspex.Input +{ + public static class Gestures + { + public static readonly RoutedEvent TappedEvent = RoutedEvent.Register( + "Tapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + public static readonly RoutedEvent DoubleTappedEvent = RoutedEvent.Register( + "DoubleTapped", + RoutingStrategies.Bubble, + typeof(Gestures)); + + private static IInteractive s_lastPress; + + static Gestures() + { + InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed); + InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); + } + + private static void PointerPressed(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerPressedEventArgs)ev; + + if (e.ClickCount <= 1) + { + s_lastPress = e.Source; + } + else if (e.ClickCount == 2 && s_lastPress == e.Source) + { + e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + } + } + } + + private static void PointerReleased(RoutedEventArgs ev) + { + if (ev.Route == RoutingStrategies.Bubble) + { + var e = (PointerReleasedEventArgs)ev; + + if (s_lastPress == e.Source) + { + s_lastPress.RaiseEvent(new RoutedEventArgs(TappedEvent)); + } + } + } + } +} diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index dab429cbdf..00e53df646 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -137,6 +137,16 @@ namespace Perspex.Input "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + /// + /// Defines the event. + /// + public static readonly RoutedEvent TappedEvent = Gestures.TappedEvent; + + /// + /// Defines the event. + /// + public static readonly RoutedEvent DoubleTappedEvent = Gestures.DoubleTappedEvent; + private bool _isFocused; private bool _isPointerOver; @@ -259,6 +269,24 @@ namespace Perspex.Input remove { RemoveHandler(PointerWheelChangedEvent, value); } } + /// + /// Occurs when a tap gesture occurs on the control. + /// + public event EventHandler Tapped + { + add { AddHandler(TappedEvent, value); } + remove { RemoveHandler(TappedEvent, value); } + } + + /// + /// Occurs when a double-tap gesture occurs on the control. + /// + public event EventHandler DoubleTapped + { + add { AddHandler(DoubleTappedEvent, value); } + remove { RemoveHandler(DoubleTappedEvent, value); } + } + /// /// Gets or sets a value indicating whether the control can receive focus. /// diff --git a/src/Perspex.Input/Perspex.Input.csproj b/src/Perspex.Input/Perspex.Input.csproj index 264eda7473..80e1566f1c 100644 --- a/src/Perspex.Input/Perspex.Input.csproj +++ b/src/Perspex.Input/Perspex.Input.csproj @@ -68,6 +68,7 @@ + diff --git a/src/Perspex.Input/PointerEventArgs.cs b/src/Perspex.Input/PointerEventArgs.cs index fab4c2a9c6..25fbf7f26d 100644 --- a/src/Perspex.Input/PointerEventArgs.cs +++ b/src/Perspex.Input/PointerEventArgs.cs @@ -8,6 +8,17 @@ namespace Perspex.Input { public class PointerEventArgs : RoutedEventArgs { + public PointerEventArgs() + { + + } + + public PointerEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + + } + public IPointerDevice Device { get; set; } public InputModifiers InputModifiers { get; set; } @@ -28,12 +39,32 @@ namespace Perspex.Input public class PointerPressedEventArgs : PointerEventArgs { + public PointerPressedEventArgs() + : base(InputElement.PointerPressedEvent) + { + } + + public PointerPressedEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } + public int ClickCount { get; set; } public MouseButton MouseButton { get; set; } } public class PointerReleasedEventArgs : PointerEventArgs { + public PointerReleasedEventArgs() + : base(InputElement.PointerReleasedEvent) + { + } + + public PointerReleasedEventArgs(RoutedEvent routedEvent) + : base(routedEvent) + { + } + public MouseButton MouseButton { get; set; } } } diff --git a/src/Perspex.Interactivity/Interactive.cs b/src/Perspex.Interactivity/Interactive.cs index a6bfd0d94c..c348a3ed83 100644 --- a/src/Perspex.Interactivity/Interactive.cs +++ b/src/Perspex.Interactivity/Interactive.cs @@ -122,16 +122,19 @@ namespace Perspex.Interactivity { e.Route = RoutingStrategies.Direct; RaiseEventImpl(e); + e.RoutedEvent.InvokeRouteFinished(e); } if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0) { TunnelEvent(e); + e.RoutedEvent.InvokeRouteFinished(e); } if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0) { BubbleEvent(e); + e.RoutedEvent.InvokeRouteFinished(e); } } @@ -175,7 +178,7 @@ namespace Perspex.Interactivity { Contract.Requires(e != null); - e.RoutedEvent.InvokeClassHandlers(this, e); + e.RoutedEvent.InvokeRaised(this, e); List subscriptions; diff --git a/src/Perspex.Interactivity/RoutedEvent.cs b/src/Perspex.Interactivity/RoutedEvent.cs index 3fa8f9aaf5..9b5dee61f5 100644 --- a/src/Perspex.Interactivity/RoutedEvent.cs +++ b/src/Perspex.Interactivity/RoutedEvent.cs @@ -2,8 +2,7 @@ // 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.Expressions; +using System.Reactive.Subjects; using System.Reflection; using System.Runtime.ExceptionServices; @@ -19,7 +18,8 @@ namespace Perspex.Interactivity public class RoutedEvent { - private readonly List _subscriptions = new List(); + private Subject> _raised = new Subject>(); + private Subject _routeFinished = new Subject(); public RoutedEvent( string name, @@ -62,6 +62,9 @@ namespace Perspex.Interactivity private set; } + public IObservable> Raised => _raised; + public IObservable RouteFinished => _routeFinished; + public static RoutedEvent Register( string name, RoutingStrategies routingStrategy) @@ -83,27 +86,24 @@ namespace Perspex.Interactivity return new RoutedEvent(name, routingStrategy, ownerType); } - public void AddClassHandler(Type type, EventHandler handler, RoutingStrategies routes) + public IDisposable AddClassHandler( + Type targetType, + EventHandler handler, + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) { - _subscriptions.Add(new ClassEventSubscription + return Raised.Subscribe(args => { - TargetType = type, - Handler = handler, - Routes = routes, - }); - } + var sender = args.Item1; + var e = args.Item2; - internal void InvokeClassHandlers(object sender, RoutedEventArgs e) - { - foreach (var sub in _subscriptions) - { - if (sub.TargetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && - ((e.Route == RoutingStrategies.Direct) || (e.Route & sub.Routes) != 0) && - (!e.Handled || sub.AlsoIfHandled)) + if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && + ((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) && + (!e.Handled || handledEventsToo)) { try { - sub.Handler.DynamicInvoke(sender, e); + handler.DynamicInvoke(sender, e); } catch (TargetInvocationException ex) { @@ -111,12 +111,17 @@ namespace Perspex.Interactivity ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } } - } + }); } - private class ClassEventSubscription : EventSubscription + internal void InvokeRaised(object sender, RoutedEventArgs e) { - public Type TargetType { get; set; } + _raised.OnNext(Tuple.Create(sender, e)); + } + + internal void InvokeRouteFinished(RoutedEventArgs e) + { + _routeFinished.OnNext(e); } } @@ -130,26 +135,24 @@ namespace Perspex.Interactivity Contract.Requires(ownerType != null); } - public void AddClassHandler( + public IDisposable AddClassHandler( Func> handler, - RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble) - where TTarget : class + RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, + bool handledEventsToo = false) + where TTarget : class, IInteractive { - AddClassHandler(typeof(TTarget), (s, e) => ClassHandlerAdapter(s, e, handler), routes); - } + EventHandler adapter = (sender, e) => + { + var target = sender as TTarget; + var args = e as TEventArgs; - private static void ClassHandlerAdapter( - object sender, - RoutedEventArgs e, - Func> handler) where TTarget : class - { - var target = sender as TTarget; - var args = e as TEventArgs; + if (target != null && args != null) + { + handler(target)(args); + } + }; - if (target != null && args != null) - { - handler(target)(args); - } + return AddClassHandler(typeof(TTarget), adapter, routes, handledEventsToo); } } } diff --git a/src/Perspex.SceneGraph/Media/Color.cs b/src/Perspex.SceneGraph/Media/Color.cs index dbc5a108ac..a7b9ec9016 100644 --- a/src/Perspex.SceneGraph/Media/Color.cs +++ b/src/Perspex.SceneGraph/Media/Color.cs @@ -16,22 +16,30 @@ namespace Perspex.Media /// /// Gets or sets the Alpha component of the color. /// - public byte A { get; set; } + public byte A { get; } /// /// Gets or sets the Red component of the color. /// - public byte R { get; set; } + public byte R { get; } /// /// Gets or sets the Green component of the color. /// - public byte G { get; set; } + public byte G { get; } /// /// Gets or sets the Blue component of the color. /// - public byte B { get; set; } + public byte B { get; } + + public Color(byte a, byte r, byte g, byte b) + { + A = a; + R = r; + G = g; + B = b; + } /// /// Creates a from alpha, red, green and blue components. @@ -43,13 +51,7 @@ namespace Perspex.Media /// The color. public static Color FromArgb(byte a, byte r, byte g, byte b) { - return new Color - { - A = a, - R = r, - G = g, - B = b, - }; + return new Color(a, r, g, b); } /// @@ -61,13 +63,7 @@ namespace Perspex.Media /// The color. public static Color FromRgb(byte r, byte g, byte b) { - return new Color - { - A = 0xff, - R = r, - G = g, - B = b, - }; + return new Color(0xff, r, g, b); } /// @@ -77,13 +73,12 @@ namespace Perspex.Media /// The color. public static Color FromUInt32(uint value) { - return new Color - { - A = (byte)((value >> 24) & 0xff), - R = (byte)((value >> 16) & 0xff), - G = (byte)((value >> 8) & 0xff), - B = (byte)(value & 0xff), - }; + return new Color( + (byte)((value >> 24) & 0xff), + (byte)((value >> 16) & 0xff), + (byte)((value >> 8) & 0xff), + (byte)(value & 0xff) + ); } /// diff --git a/src/Perspex.Themes.Default/CheckBox.paml b/src/Perspex.Themes.Default/CheckBox.paml index b45210cfdb..cea4b2d114 100644 --- a/src/Perspex.Themes.Default/CheckBox.paml +++ b/src/Perspex.Themes.Default/CheckBox.paml @@ -5,19 +5,21 @@ - - + VerticalAlignment="Center"> + + + \ No newline at end of file diff --git a/src/Perspex.Themes.Default/ContextMenu.paml b/src/Perspex.Themes.Default/ContextMenu.paml index ed491ebe24..07cb3bfe91 100644 --- a/src/Perspex.Themes.Default/ContextMenu.paml +++ b/src/Perspex.Themes.Default/ContextMenu.paml @@ -2,8 +2,8 @@ - - + + ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) @@ -44,16 +45,15 @@ namespace Perspex.Win32 dialog.InitialFileName?.CopyTo(0, fileBuffer, 0, dialog.InitialFileName.Length); string userSelectedExt = null; - + fixed (char* pFileBuffer = fileBuffer) fixed (char* pFilterBuffer = filterBuffer) fixed (char* pDefExt = defExt) fixed (char* pInitDir = dialog.InitialDirectory) fixed (char* pTitle = dialog.Title) { - var ofn = new UnmanagedMethods.OpenFileName() - { + { hwndOwner = hWnd, hInstance = IntPtr.Zero, lCustData = IntPtr.Zero, @@ -128,5 +128,65 @@ namespace Perspex.Win32 return files.Select(f => Path.Combine(dir, f)).ToArray(); }); } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return Task.Factory.StartNew(() => + { + string result = string.Empty; + + var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var frm = (IFileDialog)(new UnmanagedMethods.FileOpenDialogRCW()); + uint options; + frm.GetOptions(out options); + options |= (uint)(UnmanagedMethods.FOS.FOS_PICKFOLDERS | UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT); + frm.SetOptions(options); + + if (dialog.InitialDirectory != null) + { + IShellItem directoryShellItem; + var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.InitialDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + frm.SetFolder(directoryShellItem); + } + } + + if (dialog.DefaultDirectory != null) + { + IShellItem directoryShellItem; + var riid = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"); //IShellItem + if (UnmanagedMethods.SHCreateItemFromParsingName(dialog.DefaultDirectory, IntPtr.Zero, ref riid, out directoryShellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + frm.SetDefaultFolder(directoryShellItem); + } + } + + if (frm.Show(hWnd) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + IShellItem shellItem; + if (frm.GetResult(out shellItem) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + IntPtr pszString; + if (shellItem.GetDisplayName(UnmanagedMethods.SIGDN_FILESYSPATH, out pszString) == (uint)UnmanagedMethods.HRESULT.S_OK) + { + if (pszString != IntPtr.Zero) + { + try + { + result = Marshal.PtrToStringAuto(pszString); + } + finally + { + Marshal.FreeCoTaskMem(pszString); + } + } + } + } + } + + return result; + }); + } } } diff --git a/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs b/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs index 68e1c62cf3..9e23e59f80 100644 --- a/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Perspex.Base.UnitTests/DirectPropertyTests.cs @@ -30,7 +30,11 @@ namespace Perspex.Base.UnitTests [Fact] public void IsDirect_Property_Returns_True() { - var target = new DirectProperty("test", o => null); + var target = new DirectProperty( + "test", + o => null, + null, + new PropertyMetadata()); Assert.True(target.IsDirect); } diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs index d4ebec056f..371857bd19 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs @@ -86,6 +86,22 @@ namespace Perspex.Base.UnitTests Assert.Equal(AttachedOwner.AttachedProperty, result); } + [Fact] + public void FindRegistered_Doesnt_Finds_Unqualified_Attached_Property() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(Class2), "Attached"); + + Assert.Null(result); + } + + [Fact] + public void FindRegistered_Finds_Unqualified_Attached_Property_On_Registering_Type() + { + var result = PerspexPropertyRegistry.Instance.FindRegistered(typeof(AttachedOwner), "Attached"); + + Assert.True(AttachedOwner.AttachedProperty == result); + } + [Fact] public void FindRegistered_Finds_AddOwnered_Untyped_Attached_Property() { @@ -168,7 +184,7 @@ namespace Perspex.Base.UnitTests { } - private class AttachedOwner + private class AttachedOwner : Class1 { public static readonly AttachedProperty AttachedProperty = PerspexProperty.RegisterAttached("Attached"); diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index dd7b41caee..10737ad4d1 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -147,6 +147,26 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(new[] { "foo", "baz", "bar" }, text); } + [Fact] + public void Inserting_Items_Should_Update_Containers() + { + var items = new ObservableCollection { "foo", "bar", "baz" }; + var target = new ItemsPresenter + { + Items = items, + }; + + target.ApplyTemplate(); + items.Insert(2, "insert"); + + var text = target.Panel.Children + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text); + } + [Fact] public void Setting_Items_To_Null_Should_Remove_Containers() { diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 4de5d8fd3b..a93c6b3026 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -311,6 +311,39 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void Applying_New_Template_Clears_TemplatedParent_Of_Old_Template_Children() + { + var target = new TestTemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator + { + Child = new Border(), + }) + }; + + target.ApplyTemplate(); + + var decorator = (Decorator)target.GetVisualChildren().Single(); + var border = (Border)decorator.Child; + + Assert.Equal(target, decorator.TemplatedParent); + Assert.Equal(target, border.TemplatedParent); + + target.Template = new FuncControlTemplate(_ => new Canvas()); + + // Templated children should not be removed here: the control may be re-added + // somewhere with the same template, so they could still be of use. + Assert.Same(decorator, target.GetVisualChildren().Single()); + Assert.Equal(target, decorator.TemplatedParent); + Assert.Equal(target, border.TemplatedParent); + + target.ApplyTemplate(); + + Assert.Null(decorator.TemplatedParent); + Assert.Null(border.TemplatedParent); + } + private static IControl ScrollingContentControlTemplate(ContentControl control) { return new Border diff --git a/tests/Perspex.Interactivity.UnitTests/GestureTests.cs b/tests/Perspex.Interactivity.UnitTests/GestureTests.cs new file mode 100644 index 0000000000..9712ea480e --- /dev/null +++ b/tests/Perspex.Interactivity.UnitTests/GestureTests.cs @@ -0,0 +1,82 @@ +// 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.Collections.Generic; +using Perspex.Controls; +using Perspex.Input; +using Xunit; + +namespace Perspex.Interactivity.UnitTests +{ + public class GestureTests + { + [Fact] + public void Tapped_Should_Follow_Pointer_Pressed_Released() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt" }, result); + } + + [Fact] + public void Tapped_Should_Be_Raised_Even_When_PointerPressed_Handled() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + border.AddHandler(Border.PointerPressedEvent, (s, e) => e.Handled = true); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + + Assert.Equal(new[] { "bt", "dt" }, result); + } + + [Fact] + public void DoubleTapped_Should_Follow_Pointer_Pressed_Released_Pressed() + { + Border border; + var decorator = new Decorator + { + Child = border = new Border() + }; + var result = new List(); + + decorator.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("dp")); + decorator.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("dr")); + decorator.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("dt")); + decorator.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("ddt")); + border.AddHandler(Border.PointerPressedEvent, (s, e) => result.Add("bp")); + border.AddHandler(Border.PointerReleasedEvent, (s, e) => result.Add("br")); + border.AddHandler(Gestures.TappedEvent, (s, e) => result.Add("bt")); + border.AddHandler(Gestures.DoubleTappedEvent, (s, e) => result.Add("bdt")); + + border.RaiseEvent(new PointerPressedEventArgs()); + border.RaiseEvent(new PointerReleasedEventArgs()); + border.RaiseEvent(new PointerPressedEventArgs { ClickCount = 2 }); + + Assert.Equal(new[] { "bp", "dp", "br", "dr", "bt", "dt", "bp", "dp", "bdt", "ddt" }, result); + } + } +} diff --git a/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj b/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj index 69137a9d35..9532487e13 100644 --- a/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj +++ b/tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj @@ -59,6 +59,7 @@ + @@ -74,6 +75,14 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Perspex.Base + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} Perspex.Interactivity @@ -86,6 +95,10 @@ {EB582467-6ABB-43A1-B052-E981BA910E3A} Perspex.SceneGraph + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling + diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs index 3581a71563..9dc40ece00 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs @@ -81,12 +81,12 @@ namespace Perspex.Markup.UnitTests.Data } [Fact] - public void SetValue_Should_Throw() + public void SetValue_Should_Return_False() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "!Foo"); - Assert.Throws(() => target.SetValue("bar")); + Assert.False(target.SetValue("bar")); } } } diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index fe9336ebe6..ebf98a363e 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -108,6 +108,34 @@ namespace Perspex.Markup.UnitTests.Data Assert.Equal(0, data.SubscriptionCount); } + [Fact] + public void Should_Trigger_PropertyChanged_On_Null_Or_Empty_String() + { + var data = new Class1 { Bar = "foo" }; + var target = new ExpressionObserver(data, "Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + + Assert.Equal(new[] { "foo" }, result); + + data.Bar = "bar"; + + Assert.Equal(new[] { "foo" }, result); + + data.RaisePropertyChanged(string.Empty); + + Assert.Equal(new[] { "foo", "bar" }, result); + + data.RaisePropertyChanged(null); + + Assert.Equal(new[] { "foo", "bar", "bar" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + } + [Fact] public void Should_Track_End_Of_Property_Chain_Changing() { @@ -324,6 +352,13 @@ namespace Perspex.Markup.UnitTests.Data } } + private string _bar; + public string Bar + { + get { return _bar; } + set { _bar = value; } + } + public INext Next { get { return _next; } @@ -333,6 +368,11 @@ namespace Perspex.Markup.UnitTests.Data RaisePropertyChanged(nameof(Next)); } } + + public void RaisePropertyChanged(string propertyName) + { + base.RaisePropertyChanged(propertyName); + } } private class Class2 : NotifyingBase, INext diff --git a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs index b438d6af1d..55034a3c53 100644 --- a/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs +++ b/tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Globalization; +using Perspex.Controls; using Xunit; namespace Perspex.Markup.UnitTests @@ -44,6 +45,18 @@ namespace Perspex.Markup.UnitTests Assert.Equal(TestEnum.Bar, result); } + [Fact] + public void Can_Convert_Int_To_Enum() + { + var result = DefaultValueConverter.Instance.Convert( + 1, + typeof(TestEnum), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(TestEnum.Bar, result); + } + [Fact] public void Can_Convert_Double_To_String() { @@ -57,15 +70,27 @@ namespace Perspex.Markup.UnitTests } [Fact] - public void Can_Convert_Double_To_Int() + public void Can_Convert_Enum_To_Int() { var result = DefaultValueConverter.Instance.Convert( - 5.0, + TestEnum.Bar, typeof(int), null, CultureInfo.InvariantCulture); - Assert.Equal(5, result); + Assert.Equal(1, result); + } + + [Fact] + public void Can_Convert_Enum_To_String() + { + var result = DefaultValueConverter.Instance.Convert( + TestEnum.Bar, + typeof(string), + null, + CultureInfo.InvariantCulture); + + Assert.Equal("Bar", result); } [Fact] @@ -80,6 +105,18 @@ namespace Perspex.Markup.UnitTests Assert.Equal(5.0, result); } + [Fact] + public void Cannot_Convert_Between_Different_Enum_Types() + { + var result = DefaultValueConverter.Instance.Convert( + TestEnum.Foo, + typeof(Orientation), + null, + CultureInfo.InvariantCulture); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + private enum TestEnum { Foo,