Browse Source

Merge remote-tracking branch 'Perspex/master' into DocFX

pull/419/head
Wiesław Šoltés 11 years ago
parent
commit
5aa3cb28c4
  1. 6
      samples/ControlCatalog/ControlCatalog.csproj
  2. 1
      samples/ControlCatalog/MainWindow.paml
  3. 15
      samples/ControlCatalog/Pages/CheckBoxPage.paml
  4. 18
      samples/ControlCatalog/Pages/CheckBoxPage.paml.cs
  5. 22
      samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs
  6. 6
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  7. 5
      src/Android/Perspex.Android/SystemDialogImpl.cs
  8. 9
      src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs
  9. 35
      src/Gtk/Perspex.Gtk/SystemDialogImpl.cs
  10. 8
      src/Markup/Perspex.Markup.Xaml/Context/PerspexAttachableXamlMember.cs
  11. 24
      src/Markup/Perspex.Markup.Xaml/Context/PerspexMemberValuePlugin.cs
  12. 4
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs
  13. 29
      src/Markup/Perspex.Markup.Xaml/Context/PropertyAccessor.cs
  14. 2
      src/Markup/Perspex.Markup.Xaml/OmniXAML
  15. 1
      src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
  16. 2
      src/Markup/Perspex.Markup/Data/LogicalNotNode.cs
  17. 2
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  18. 35
      src/Markup/Perspex.Markup/DefaultValueConverter.cs
  19. 6
      src/Perspex.Base/DirectProperty.cs
  20. 7
      src/Perspex.Base/PerspexProperty.cs
  21. 6
      src/Perspex.Base/PerspexPropertyRegistry.cs
  22. 10
      src/Perspex.Base/Utilities/TypeUtilities.cs
  23. 7
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  24. 6
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  25. 1
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  26. 2
      src/Perspex.Controls/Platform/ISystemDialogImpl.cs
  27. 18
      src/Perspex.Controls/Presenters/ItemsPresenter.cs
  28. 2
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  29. 30
      src/Perspex.Controls/Primitives/TemplatedControl.cs
  30. 3
      src/Perspex.Controls/Primitives/ToggleButton.cs
  31. 2
      src/Perspex.Controls/ScrollViewer.cs
  32. 19
      src/Perspex.Controls/SystemDialog.cs
  33. 6
      src/Perspex.Controls/TextBlock.cs
  34. 10
      src/Perspex.Controls/TextBox.cs
  35. 6
      src/Perspex.Controls/TreeViewItem.cs
  36. 15
      src/Perspex.Controls/Utils/UndoRedoHelper.cs
  37. 6
      src/Perspex.HtmlRenderer/HtmlControl.cs
  38. 59
      src/Perspex.Input/Gestures.cs
  39. 28
      src/Perspex.Input/InputElement.cs
  40. 1
      src/Perspex.Input/Perspex.Input.csproj
  41. 31
      src/Perspex.Input/PointerEventArgs.cs
  42. 5
      src/Perspex.Interactivity/Interactive.cs
  43. 77
      src/Perspex.Interactivity/RoutedEvent.cs
  44. 45
      src/Perspex.SceneGraph/Media/Color.cs
  45. 25
      src/Perspex.Themes.Default/CheckBox.paml
  46. 4
      src/Perspex.Themes.Default/ContextMenu.paml
  47. 143
      src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs
  48. 66
      src/Windows/Perspex.Win32/SystemDialogImpl.cs
  49. 6
      tests/Perspex.Base.UnitTests/DirectPropertyTests.cs
  50. 18
      tests/Perspex.Base.UnitTests/PerspexPropertyRegistryTests.cs
  51. 20
      tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs
  52. 33
      tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  53. 82
      tests/Perspex.Interactivity.UnitTests/GestureTests.cs
  54. 13
      tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj
  55. 4
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
  56. 40
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  57. 43
      tests/Perspex.Markup.UnitTests/DefaultValueConverterTests.cs

6
samples/ControlCatalog/ControlCatalog.csproj

@ -60,6 +60,9 @@
<Compile Include="MainWindow.paml.cs">
<DependentUpon>MainWindow.paml</DependentUpon>
</Compile>
<Compile Include="Pages\CheckBoxPage.paml.cs">
<DependentUpon>CheckBoxPage.paml</DependentUpon>
</Compile>
<Compile Include="Pages\BorderPage.paml.cs">
<DependentUpon>BorderPage.paml</DependentUpon>
</Compile>
@ -102,6 +105,9 @@
</EmbeddedResource>
<EmbeddedResource Include="Pages\DropDownPage.paml" />
<None Include="packages.config" />
<EmbeddedResource Include="Pages\CheckBoxPage.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Gtk\Perspex.Cairo\Perspex.Cairo.csproj">

1
samples/ControlCatalog/MainWindow.paml

@ -9,6 +9,7 @@
<TabItem Header="Button"><pages:ButtonPage/></TabItem>
<TabItem Header="Canvas"><pages:CanvasPage/></TabItem>
<TabItem Header="Carousel"><pages:CarouselPage/></TabItem>
<TabItem Header="CheckBox"><pages:CheckBoxPage/></TabItem>
<TabItem Header="DropDown"><pages:DropDownPage/></TabItem>
</TabControl>
</Window>

15
samples/ControlCatalog/Pages/CheckBoxPage.paml

@ -0,0 +1,15 @@
<UserControl xmlns="https://github.com/perspex">
<StackPanel Orientation="Vertical" Gap="4">
<TextBlock Classes="h1">CheckBox</TextBlock>
<TextBlock Classes="h2">A check box control</TextBlock>
<StackPanel Orientation="Vertical"
Margin="0,16,0,0"
HorizontalAlignment="Center"
Gap="16">
<CheckBox>Unchecked</CheckBox>
<CheckBox IsChecked="True">Checked</CheckBox>
<CheckBox IsChecked="True" IsEnabled="False">Disabled</CheckBox>
</StackPanel>
</StackPanel>
</UserControl>

18
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);
}
}
}

22
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<TestItem> Items { get; }
@ -68,6 +86,10 @@ namespace XamlTestApplication.ViewModels
public ReactiveCommand<object> ExpandNodesCommand { get; }
public ReactiveCommand<object> OpenFileCommand { get; }
public ReactiveCommand<object> OpenFolderCommand { get; }
public void ExpandNodes(bool expanded)
{
foreach (var node in Nodes)

6
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -6,11 +6,13 @@
Title="Perspex Test Application" Width="800" Height="600">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<Menu Grid.ColumnSpan="2">
<MenuItem Header="_File" Background="Red">
<MenuItem Header="_File">
<MenuItem Header="_Open" Command="{Binding OpenFileCommand}" />
<MenuItem Header="_Open Folder" Command="{Binding OpenFolderCommand}" />
<MenuItem Header="_Hello">
<MenuItem Header="_Goodbye"/>
<Separator/>
<MenuItem Header="_World" Background="Red"/>
<MenuItem Header="_World" />
</MenuItem>
<Separator Background="Red"/>
<MenuItem Header="_Test"/>

5
src/Android/Perspex.Android/SystemDialogImpl.cs

@ -12,5 +12,10 @@ namespace Perspex.Android
{
throw new NotImplementedException();
}
public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
throw new NotImplementedException();
}
}
}

9
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<ArgumentNullException>(context != null);
Contract.Requires<ArgumentNullException> (text != null);
Contract.Requires<ArgumentNullException>(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()
};

35
src/Gtk/Perspex.Gtk/SystemDialogImpl.cs

@ -15,7 +15,7 @@ namespace Perspex.Gtk
public Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{
var tcs = new TaskCompletionSource<string[]>();
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<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
var tcs = new TaskCompletionSource<string>();
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);

8
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);
}
}
}

24
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);
}
}
}

4
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()

29
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(

2
src/Markup/Perspex.Markup.Xaml/OmniXAML

@ -1 +1 @@
Subproject commit a0b7a7f7a415409497539f69e39deb457f7ca629
Subproject commit 27005c8b4d41f2a6d3b85da7eb6ed994b57e0138

1
src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj

@ -42,6 +42,7 @@
</Compile>
<Compile Include="Context\NameScopeWrapper.cs" />
<Compile Include="Context\PerspexAttachableXamlMember.cs" />
<Compile Include="Context\PerspexMemberValuePlugin.cs" />
<Compile Include="Context\PerspexNamespaceRegistry.cs" />
<Compile Include="Context\PerspexObjectAssembler.cs" />
<Compile Include="Context\PerspexTypeFeatureProvider.cs" />

2
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<object> observer)

2
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);
}

35
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;
}
}
}

6
src/Perspex.Base/DirectProperty.cs

@ -24,11 +24,13 @@ namespace Perspex
/// <param name="name">The name of the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property. May be null.</param>
/// <param name="metadata">The property metadata.</param>
public DirectProperty(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null)
: base(name, typeof(TOwner), new PropertyMetadata())
Action<TOwner, TValue> setter,
PropertyMetadata metadata)
: base(name, typeof(TOwner), metadata)
{
Contract.Requires<ArgumentNullException>(getter != null);

7
src/Perspex.Base/PerspexProperty.cs

@ -342,16 +342,19 @@ namespace Perspex
/// <param name="name">The name of the property.</param>
/// <param name="getter">Gets the current value of the property.</param>
/// <param name="setter">Sets the value of the property.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static DirectProperty<TOwner, TValue> RegisterDirect<TOwner, TValue>(
string name,
Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null)
Action<TOwner, TValue> setter = null,
BindingMode defaultBindingMode = BindingMode.OneWay)
where TOwner : IPerspexObject
{
Contract.Requires<ArgumentNullException>(name != null);
var result = new DirectProperty<TOwner, TValue>(name, getter, setter);
var metadata = new PropertyMetadata(defaultBindingMode);
var result = new DirectProperty<TOwner, TValue>(name, getter, setter, metadata);
PerspexPropertyRegistry.Instance.Register(typeof(TOwner), result);
return result;
}

6
src/Perspex.Base/PerspexPropertyRegistry.cs

@ -92,7 +92,7 @@ namespace Perspex
}
/// <summary>
/// Finds <see cref="PerspexProperty"/> registered on a type.
/// Finds a <see cref="PerspexProperty"/> registered on a type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
@ -155,6 +155,7 @@ namespace Perspex
Contract.Requires<ArgumentNullException>(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]);

10
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
{

7
src/Perspex.Controls/Generators/IItemContainerGenerator.cs

@ -52,6 +52,13 @@ namespace Perspex.Controls.Generators
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count);
/// <summary>
/// Inserts space for newly inserted containers in the index.
/// </summary>
/// <param name="index">The index at which space should be inserted.</param>
/// <param name="count">The number of blank spaces to create.</param>
void InsertSpace(int index, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
/// the gap.

6
src/Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -85,6 +85,12 @@ namespace Perspex.Controls.Generators
return result;
}
/// <inheritdoc/>
public virtual void InsertSpace(int index, int count)
{
_containers.InsertRange(index, Enumerable.Repeat<ItemContainer>(null, count));
}
/// <inheritdoc/>
public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
{

1
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

2
src/Perspex.Controls/Platform/ISystemDialogImpl.cs

@ -18,5 +18,7 @@ namespace Perspex.Controls.Platform
/// <param name="parent">The parent window.</param>
/// <returns>A task returning the selected filenames.</returns>
Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent);
Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent);
}
}

18
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);
}
}
}
}

2
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -62,7 +62,7 @@ namespace Perspex.Controls.Primitives
PerspexProperty.RegisterDirect<SelectingItemsControl, object>(
nameof(SelectedItem),
o => o.SelectedItem,
(o, v) => o.SelectedItem = v);
(o, v) => o.SelectedItem = v, BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="SelectedItems"/> property.

30
src/Perspex.Controls/Primitives/TemplatedControl.cs

@ -56,6 +56,12 @@ namespace Perspex.Controls.Primitives
public static readonly StyledProperty<FontStyle> FontStyleProperty =
TextBlock.FontStyleProperty.AddOwner<TemplatedControl>();
/// <summary>
/// Defines the <see cref="FontWeight"/> property.
/// </summary>
public static readonly StyledProperty<FontWeight> FontWeightProperty =
TextBlock.FontWeightProperty.AddOwner<TemplatedControl>();
/// <summary>
/// Defines the <see cref="Foreground"/> property.
/// </summary>
@ -177,6 +183,15 @@ namespace Perspex.Controls.Primitives
set { SetValue(FontStyleProperty, value); }
}
/// <summary>
/// Gets or sets the font weight used to draw the control's text.
/// </summary>
public FontWeight FontWeight
{
get { return GetValue(FontWeightProperty); }
set { SetValue(FontWeightProperty, value); }
}
/// <summary>
/// Gets or sets the brush used to draw the control's text and other foreground elements.
/// </summary>
@ -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
/// <param name="e">The event args.</param>
protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e)
{
if (_templateApplied && VisualChildren.Count > 0)
{
_templateApplied = false;
}
_templateApplied = false;
InvalidateMeasure();
}

3
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<ToggleButton, bool>(
"IsChecked",
o => o.IsChecked,
(o,v) => o.IsChecked = v);
(o,v) => o.IsChecked = v, BindingMode.TwoWay);
private bool _isChecked;

2
src/Perspex.Controls/ScrollViewer.cs

@ -85,7 +85,7 @@ namespace Perspex.Controls
/// <see cref="ScrollContentPresenter"/> in the control's template.
/// </remarks>
public static readonly AttachedProperty<ScrollBarVisibility> HorizontalScrollBarVisibilityProperty =
PerspexProperty.RegisterAttached<ScrollBar, Control, ScrollBarVisibility>(
PerspexProperty.RegisterAttached<ScrollViewer, Control, ScrollBarVisibility>(
nameof(HorizontalScrollBarVisibility),
ScrollBarVisibility.Auto);

19
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<FileDialogFilter> Filters { get; set; } = new List<FileDialogFilter>();
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<string> ShowAsync(Window window = null)
=>
@ -31,6 +34,14 @@ namespace Perspex.Controls
=> PerspexLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, window?.PlatformImpl);
}
public class OpenFolderDialog : FileSystemDialog
{
public string DefaultDirectory { get; set; }
public Task<string> ShowAsync(Window window = null)
=> PerspexLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, window?.PlatformImpl);
}
public abstract class SystemDialog
{
public string Title { get; set; }

6
src/Perspex.Controls/TextBlock.cs

@ -102,6 +102,9 @@ namespace Perspex.Controls
static TextBlock()
{
AffectsRender(ForegroundProperty);
AffectsRender(FontWeightProperty);
AffectsRender(FontSizeProperty);
AffectsRender(FontStyleProperty);
}
/// <summary>
@ -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();

10
src/Perspex.Controls/TextBox.cs

@ -26,6 +26,16 @@ namespace Perspex.Controls
public static readonly StyledProperty<bool> AcceptsTabProperty =
PerspexProperty.Register<TextBox, bool>("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<bool> CanScrollHorizontally =
ScrollViewer.CanScrollHorizontallyProperty.AddOwner<TextBox>();
public static readonly StyledProperty<ScrollBarVisibility> HorizontalScrollBarVisibilityProperty =
ScrollViewer.HorizontalScrollBarVisibilityProperty.AddOwner<TextBox>();
public static readonly StyledProperty<ScrollBarVisibility> VerticalScrollBarVisibilityProperty =
ScrollViewer.VerticalScrollBarVisibilityProperty.AddOwner<TextBox>();
// TODO: Should CaretIndex, SelectionStart/End and Text be direct properties?
public static readonly StyledProperty<int> CaretIndexProperty =
PerspexProperty.Register<TextBox, int>("CaretIndex", validate: ValidateCaretIndex);

6
src/Perspex.Controls/TreeViewItem.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls
/// Defines the <see cref="IsExpanded"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsExpandedProperty =
PerspexProperty.Register<TreeViewItem, bool>("IsExpanded");
PerspexProperty.Register<TreeViewItem, bool>("IsExpanded", default(bool), false, Data.BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
@ -30,7 +30,7 @@ namespace Perspex.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel
{
{
[KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue,
});
@ -40,7 +40,7 @@ namespace Perspex.Controls
/// Initializes static members of the <see cref="TreeViewItem"/> class.
/// </summary>
static TreeViewItem()
{
{
SelectableMixin.Attach<TreeViewItem>(IsSelectedProperty);
FocusableProperty.OverrideDefaultValue<TreeViewItem>(true);
ItemsPanelProperty.OverrideDefaultValue<TreeViewItem>(DefaultPanel);

15
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()

6
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();
}
}
}
}

59
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<RoutedEventArgs> TappedEvent = RoutedEvent.Register<RoutedEventArgs>(
"Tapped",
RoutingStrategies.Bubble,
typeof(Gestures));
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = RoutedEvent.Register<RoutedEventArgs>(
"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));
}
}
}
}
}

28
src/Perspex.Input/InputElement.cs

@ -137,6 +137,16 @@ namespace Perspex.Input
"PointerWheelChanged",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Tapped"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> TappedEvent = Gestures.TappedEvent;
/// <summary>
/// Defines the <see cref="DoubleTapped"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isFocused;
private bool _isPointerOver;
@ -259,6 +269,24 @@ namespace Perspex.Input
remove { RemoveHandler(PointerWheelChangedEvent, value); }
}
/// <summary>
/// Occurs when a tap gesture occurs on the control.
/// </summary>
public event EventHandler<RoutedEventArgs> Tapped
{
add { AddHandler(TappedEvent, value); }
remove { RemoveHandler(TappedEvent, value); }
}
/// <summary>
/// Occurs when a double-tap gesture occurs on the control.
/// </summary>
public event EventHandler<RoutedEventArgs> DoubleTapped
{
add { AddHandler(DoubleTappedEvent, value); }
remove { RemoveHandler(DoubleTappedEvent, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the control can receive focus.
/// </summary>

1
src/Perspex.Input/Perspex.Input.csproj

@ -68,6 +68,7 @@
</Compile>
<Compile Include="Cursors.cs" />
<Compile Include="AccessKeyHandler.cs" />
<Compile Include="Gestures.cs" />
<Compile Include="KeyBinding.cs" />
<Compile Include="KeyGesture.cs" />
<Compile Include="Platform\IClipboard.cs" />

31
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; }
}
}

5
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<ArgumentNullException>(e != null);
e.RoutedEvent.InvokeClassHandlers(this, e);
e.RoutedEvent.InvokeRaised(this, e);
List<EventSubscription> subscriptions;

77
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<ClassEventSubscription> _subscriptions = new List<ClassEventSubscription>();
private Subject<Tuple<object, RoutedEventArgs>> _raised = new Subject<Tuple<object, RoutedEventArgs>>();
private Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
public RoutedEvent(
string name,
@ -62,6 +62,9 @@ namespace Perspex.Interactivity
private set;
}
public IObservable<Tuple<object, RoutedEventArgs>> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
string name,
RoutingStrategies routingStrategy)
@ -83,27 +86,24 @@ namespace Perspex.Interactivity
return new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
}
public void AddClassHandler(Type type, EventHandler<RoutedEventArgs> handler, RoutingStrategies routes)
public IDisposable AddClassHandler(
Type targetType,
EventHandler<RoutedEventArgs> 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<ArgumentNullException>(ownerType != null);
}
public void AddClassHandler<TTarget>(
public IDisposable AddClassHandler<TTarget>(
Func<TTarget, Action<TEventArgs>> 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<TTarget>(s, e, handler), routes);
}
EventHandler<RoutedEventArgs> adapter = (sender, e) =>
{
var target = sender as TTarget;
var args = e as TEventArgs;
private static void ClassHandlerAdapter<TTarget>(
object sender,
RoutedEventArgs e,
Func<TTarget, Action<TEventArgs>> 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);
}
}
}

45
src/Perspex.SceneGraph/Media/Color.cs

@ -16,22 +16,30 @@ namespace Perspex.Media
/// <summary>
/// Gets or sets the Alpha component of the color.
/// </summary>
public byte A { get; set; }
public byte A { get; }
/// <summary>
/// Gets or sets the Red component of the color.
/// </summary>
public byte R { get; set; }
public byte R { get; }
/// <summary>
/// Gets or sets the Green component of the color.
/// </summary>
public byte G { get; set; }
public byte G { get; }
/// <summary>
/// Gets or sets the Blue component of the color.
/// </summary>
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;
}
/// <summary>
/// Creates a <see cref="Color"/> from alpha, red, green and blue components.
@ -43,13 +51,7 @@ namespace Perspex.Media
/// <returns>The color.</returns>
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);
}
/// <summary>
@ -61,13 +63,7 @@ namespace Perspex.Media
/// <returns>The color.</returns>
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);
}
/// <summary>
@ -77,13 +73,12 @@ namespace Perspex.Media
/// <returns>The color.</returns>
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)
);
}
/// <summary>

25
src/Perspex.Themes.Default/CheckBox.paml

@ -5,19 +5,21 @@
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*">
<Border BorderBrush="{TemplateBinding BorderBrush}"
<Border Name="border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Width="18"
Height="18"
VerticalAlignment="Center"/>
<Path Name="checkMark"
Fill="#ff333333"
Width="11"
Height="10"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/>
VerticalAlignment="Center">
<Path Name="checkMark"
Fill="#ff333333"
Width="11"
Height="10"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 1145.607177734375,430 C1145.607177734375,430 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1141.449951171875,435.0772705078125 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1139.232177734375,433.0999755859375 1138,434.5538330078125 1138,434.5538330078125 1138,434.5538330078125 1141.482177734375,438 1141.482177734375,438 1141.482177734375,438 1141.96875,437.9375 1141.96875,437.9375 1141.96875,437.9375 1147,431.34619140625 1147,431.34619140625 1147,431.34619140625 1145.607177734375,430 1145.607177734375,430 z"/>
</Border>
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="4,0,0,0"
@ -33,4 +35,7 @@
<Style Selector="CheckBox:checked /template/ Path#checkMark">
<Setter Property="IsVisible" Value="True"/>
</Style>
<Style Selector="CheckBox:disabled /template/ Border#border">
<Setter Property="Opacity" Value="0.5"/>
</Style>
</Styles>

4
src/Perspex.Themes.Default/ContextMenu.paml

@ -2,8 +2,8 @@
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="TextBlock.FontSize" Value="12" />
<Setter Property="TextBlock.FontWeight" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"

143
src/Windows/Perspex.Win32/Interop/UnmanagedMethods.cs

@ -3,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@ -702,6 +703,13 @@ namespace Perspex.Win32.Interop
return SetClassLong64(hWnd, nIndex, dwNewLong);
}
[ComImport, ClassInterface(ClassInterfaceType.None), TypeLibType(TypeLibTypeFlags.FCanCreate), Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
internal class FileOpenDialogRCW { }
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool OpenClipboard(IntPtr hWndOwner);
@ -873,6 +881,41 @@ namespace Perspex.Win32.Interop
}
public enum HRESULT : long
{
S_FALSE = 0x0001,
S_OK = 0x0000,
E_INVALIDARG = 0x80070057,
E_OUTOFMEMORY = 0x8007000E
}
public const uint SIGDN_FILESYSPATH = 0x80058000;
[Flags]
internal enum FOS : uint
{
FOS_OVERWRITEPROMPT = 0x00000002,
FOS_STRICTFILETYPES = 0x00000004,
FOS_NOCHANGEDIR = 0x00000008,
FOS_PICKFOLDERS = 0x00000020,
FOS_FORCEFILESYSTEM = 0x00000040, // Ensure that items returned are filesystem items.
FOS_ALLNONSTORAGEITEMS = 0x00000080, // Allow choosing items that have no storage.
FOS_NOVALIDATE = 0x00000100,
FOS_ALLOWMULTISELECT = 0x00000200,
FOS_PATHMUSTEXIST = 0x00000800,
FOS_FILEMUSTEXIST = 0x00001000,
FOS_CREATEPROMPT = 0x00002000,
FOS_SHAREAWARE = 0x00004000,
FOS_NOREADONLYRETURN = 0x00008000,
FOS_NOTESTFILECREATE = 0x00010000,
FOS_HIDEMRUPLACES = 0x00020000,
FOS_HIDEPINNEDPLACES = 0x00040000,
FOS_NODEREFERENCELINKS = 0x00100000,
FOS_DONTADDTORECENT = 0x02000000,
FOS_FORCESHOWHIDDEN = 0x10000000,
FOS_DEFAULTNOMINIMODE = 0x20000000
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct OpenFileName
{
@ -899,6 +942,104 @@ namespace Perspex.Win32.Interop
public IntPtr reservedPtr;
public int reservedInt;
public int flagsEx;
}
}
}
[ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IFileDialog
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
[PreserveSig()]
uint Show([In, Optional] IntPtr hwndOwner); //IModalWindow
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr rgFilterSpec);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileTypeIndex([In] uint iFileType);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetFileTypeIndex(out uint piFileType);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Unadvise([In] uint dwCookie);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetOptions([In] uint fos);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetOptions(out uint fos);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetResult([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, uint fdap);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Close([MarshalAs(UnmanagedType.Error)] uint hr);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetClientGuid([In] ref Guid guid);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint ClearClientData();
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
}
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IShellItem
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint BindToHandler([In] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppvOut);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetDisplayName([In] uint sigdnName, out IntPtr ppszName);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
uint Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
}
}

66
src/Windows/Perspex.Win32/SystemDialogImpl.cs

@ -13,6 +13,7 @@ using Perspex.Win32.Interop;
namespace Perspex.Win32
{
class SystemDialogImpl : ISystemDialogImpl
{
public unsafe Task<string[]> 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<string> 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;
});
}
}
}

6
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<Class1, string>("test", o => null);
var target = new DirectProperty<Class1, string>(
"test",
o => null,
null,
new PropertyMetadata());
Assert.True(target.IsDirect);
}

18
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<string> AttachedProperty =
PerspexProperty.RegisterAttached<AttachedOwner, Class1, string>("Attached");

20
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<string> { "foo", "bar", "baz" };
var target = new ItemsPresenter
{
Items = items,
};
target.ApplyTemplate();
items.Insert(2, "insert");
var text = target.Panel.Children
.OfType<TextBlock>()
.Select(x => x.Text)
.ToList();
Assert.Equal(new[] { "foo", "bar", "insert", "baz" }, text);
}
[Fact]
public void Setting_Items_To_Null_Should_Remove_Containers()
{

33
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

82
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<string>();
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<string>();
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<string>();
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);
}
}
}

13
tests/Perspex.Interactivity.UnitTests/Perspex.Interactivity.UnitTests.csproj

@ -59,6 +59,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="GestureTests.cs" />
<Compile Include="InteractiveTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
@ -74,6 +75,14 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Controls\Perspex.Controls.csproj">
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project>
<Name>Perspex.Controls</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Input\Perspex.Input.csproj">
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project>
<Name>Perspex.Input</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Interactivity\Perspex.Interactivity.csproj">
<Project>{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}</Project>
<Name>Perspex.Interactivity</Name>
@ -86,6 +95,10 @@
<Project>{EB582467-6ABB-43A1-B052-E981BA910E3A}</Project>
<Name>Perspex.SceneGraph</Name>
</ProjectReference>
<ProjectReference Include="..\..\src\Perspex.Styling\Perspex.Styling.csproj">
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project>
<Name>Perspex.Styling</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

4
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<NotSupportedException>(() => target.SetValue("bar"));
Assert.False(target.SetValue("bar"));
}
}
}

40
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<object>();
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

43
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,

Loading…
Cancel
Save