Browse Source

Began implementing geometry

pull/59/head
Nelson Carrillo 11 years ago
parent
commit
d66c1545ce
  1. 27
      Cairo/Perspex.Cairo/CairoExtensions.cs
  2. 3
      Cairo/Perspex.Cairo/CairoPlatform.cs
  3. 84
      Cairo/Perspex.Cairo/Media/DrawingContext.cs
  4. 9
      Cairo/Perspex.Cairo/Media/FormattedTextImpl.cs
  5. 1
      Cairo/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs
  6. 6
      Cairo/Perspex.Cairo/Media/StreamGeometryContextImpl.cs
  7. 16
      Cairo/Perspex.Cairo/Perspex.Cairo.csproj
  8. 5
      Cairo/Perspex.Cairo/Renderer.cs
  9. 7
      Cairo/Perspex.Cairo/packages.config
  10. 2
      Gtk/Perspex.Gtk/Input/GtkKeyboardDevice.cs
  11. 7
      Gtk/Perspex.Gtk/Input/GtkMouseDevice.cs
  12. 16
      Gtk/Perspex.Gtk/Perspex.Gtk.csproj
  13. 9
      Gtk/Perspex.Gtk/packages.config
  14. 14
      Perspex-Mono.sln
  15. 161
      Perspex-Windows.sln
  16. 17
      Perspex.Animation/Animatable.cs
  17. 31
      Perspex.Animation/Animate.cs
  18. 58
      Perspex.Animation/Animation.cs
  19. 34
      Perspex.Animation/AnimationExtensions.cs
  20. 59
      Perspex.Animation/Animation`1.cs
  21. 20
      Perspex.Animation/IEasing.cs
  22. 27
      Perspex.Animation/IEasing`1.cs
  23. 29
      Perspex.Animation/LinearDoubleEasing.cs
  24. 18
      Perspex.Animation/LinearEasing.cs
  25. 6
      Perspex.Animation/Perspex.Animation.csproj
  26. 14
      Perspex.Animation/Properties/AssemblyInfo.cs
  27. 40
      Perspex.Animation/PropertyTransition.cs
  28. 5
      Perspex.Animation/PropertyTransitions.cs
  29. 11
      Perspex.Animation/packages.config
  30. 75
      Perspex.Application/Application.cs
  31. 18
      Perspex.Application/Perspex.Application.csproj
  32. 6
      Perspex.Application/Properties/AssemblyInfo.cs
  33. 14
      Perspex.Application/packages.config
  34. 16
      Perspex.Base/Binding.cs
  35. 2
      Perspex.Base/Collections/IPerspexReadOnlyList.cs
  36. 2
      Perspex.Base/Collections/PerspexList.cs
  37. 69
      Perspex.Base/Collections/PerspexReadOnlyListView.cs
  38. 28
      Perspex.Base/Collections/PerspexSingleItemList.cs
  39. 35
      Perspex.Base/Diagnostics/PerspexObjectExtensions.cs
  40. 25
      Perspex.Base/Diagnostics/PerspexPropertyValue.cs
  41. 14
      Perspex.Base/GlobalSuppressions.cs
  42. 9
      Perspex.Base/IDescription.cs
  43. 30
      Perspex.Base/Perspex.Base.csproj
  44. 294
      Perspex.Base/PerspexObject.cs
  45. 53
      Perspex.Base/PerspexObjectExtensions.cs
  46. 288
      Perspex.Base/PerspexProperty.cs
  47. 22
      Perspex.Base/PerspexPropertyChangedEventArgs.cs
  48. 80
      Perspex.Base/PerspexProperty`1.cs
  49. 7
      Perspex.Base/Platform/IPlatformThreadingInterface.cs
  50. 111
      Perspex.Base/PriorityBindingEntry.cs
  51. 210
      Perspex.Base/PriorityLevel.cs
  52. 379
      Perspex.Base/PriorityValue.cs
  53. 3
      Perspex.Base/Properties/AssemblyInfo.cs
  54. 35
      Perspex.Base/Reactive/PerspexObservable.cs
  55. 19
      Perspex.Base/Threading/PerspexScheduler.cs
  56. 84
      Perspex.Base/Utilities/TypeUtilities.cs
  57. 15
      Perspex.Base/packages.config
  58. 10
      Perspex.Controls/Border.cs
  59. 35
      Perspex.Controls/Button.cs
  60. 10
      Perspex.Controls/ColumnDefinitions.cs
  61. 35
      Perspex.Controls/ContentControl.cs
  62. 72
      Perspex.Controls/Control.cs
  63. 4
      Perspex.Controls/ControlExtensions.cs
  64. 6
      Perspex.Controls/DataTemplate.cs
  65. 65
      Perspex.Controls/Deck.cs
  66. 8
      Perspex.Controls/Decorator.cs
  67. 20
      Perspex.Controls/Generators/ItemContainerGenerator.cs
  68. 14
      Perspex.Controls/GlobalSuppressions.cs
  69. 64
      Perspex.Controls/Grid.cs
  70. 8
      Perspex.Controls/INamed.cs
  71. 15
      Perspex.Controls/INavigablePanel.cs
  72. 74
      Perspex.Controls/ItemsControl.cs
  73. 33
      Perspex.Controls/ListBoxItem.cs
  74. 230
      Perspex.Controls/Menu.cs
  75. 547
      Perspex.Controls/MenuItem.cs
  76. 111
      Perspex.Controls/MenuItemAccessKeyHandler.cs
  77. 12
      Perspex.Controls/Panel.cs
  78. 53
      Perspex.Controls/Parsers/GridLengthsParser.cs
  79. 35
      Perspex.Controls/Perspex.Controls.csproj
  80. 14
      Perspex.Controls/PlacementMode.cs
  81. 239
      Perspex.Controls/Popup.cs
  82. 64
      Perspex.Controls/PopupRoot.cs
  83. 4
      Perspex.Controls/Presenters/ContentPresenter.cs
  84. 159
      Perspex.Controls/Presenters/DeckPresenter.cs
  85. 13
      Perspex.Controls/Presenters/IItemsPresenter.cs
  86. 2
      Perspex.Controls/Presenters/IPresenter.cs
  87. 32
      Perspex.Controls/Presenters/ItemsPresenter.cs
  88. 7
      Perspex.Controls/Presenters/ScrollContentPresenter.cs
  89. 14
      Perspex.Controls/Presenters/TextPresenter.cs
  90. 191
      Perspex.Controls/Primitives/AccessText.cs
  91. 73
      Perspex.Controls/Primitives/AdornerLayer.cs
  92. 13
      Perspex.Controls/Primitives/ScrollBar.cs
  93. 350
      Perspex.Controls/Primitives/SelectingItemsControl.cs
  94. 3
      Perspex.Controls/Primitives/TabStrip.cs
  95. 22
      Perspex.Controls/Primitives/TemplatedControl.cs
  96. 4
      Perspex.Controls/Primitives/Track.cs
  97. 10
      Perspex.Controls/RowDefinitions.cs
  98. 3
      Perspex.Controls/Shapes/Ellipse.cs
  99. 3
      Perspex.Controls/Shapes/Rectangle.cs
  100. 55
      Perspex.Controls/StackPanel.cs

27
Cairo/Perspex.Cairo/CairoExtensions.cs

@ -37,24 +37,51 @@ namespace Perspex.Cairo
public static Pango.Weight ToCairo(this Perspex.Media.FontWeight weight)
{
if (weight == Perspex.Media.FontWeight.Light)
{
return Pango.Weight.Light;
}
if (weight == Perspex.Media.FontWeight.Normal || weight == Perspex.Media.FontWeight.Regular)
{
return Pango.Weight.Normal;
}
if (weight == Perspex.Media.FontWeight.DemiBold || weight == Perspex.Media.FontWeight.Medium)
{
return Pango.Weight.Semibold;
}
if (weight == Perspex.Media.FontWeight.Bold)
{
return Pango.Weight.Bold;
}
if (weight == Perspex.Media.FontWeight.UltraBold || weight == Perspex.Media.FontWeight.ExtraBold)
{
return Pango.Weight.Ultrabold;
}
if (weight == Perspex.Media.FontWeight.Black || weight == Perspex.Media.FontWeight.Heavy || weight == Perspex.Media.FontWeight.UltraBlack)
{
return Pango.Weight.Heavy;
}
return Pango.Weight.Ultralight;
}
public static Pango.Alignment ToCairo(this Perspex.Media.TextAlignment alignment)
{
if (alignment == Perspex.Media.TextAlignment.Left)
{
return Pango.Alignment.Left;
}
if (alignment == Perspex.Media.TextAlignment.Center)
{
return Pango.Alignment.Center;
}
return Pango.Alignment.Right;
}
}
}

3
Cairo/Perspex.Cairo/CairoPlatform.cs

@ -35,9 +35,10 @@ namespace Perspex.Cairo
string fontFamily,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment,
Perspex.Media.FontWeight fontWeight)
{
return new FormattedTextImpl(text, fontFamily, fontSize, fontStyle, fontWeight);
return new FormattedTextImpl(text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight);
}
public IRenderer CreateRenderer(IPlatformHandle handle, double width, double height)

84
Cairo/Perspex.Cairo/Media/DrawingContext.cs

@ -118,53 +118,57 @@ namespace Perspex.Cairo.Media
{
var impl = geometry.PlatformImpl as StreamGeometryImpl;
var clone = new Queue<GeometryOp>(impl.Operations);
bool useFill = false;
var pop = this.PushTransform(impl.Transform);
bool useFill = false;
this.SetPen(pen);
this.SetBrush(brush);
while (clone.Count > 0)
using (var pop = this.PushTransform(impl.Transform))
{
while (clone.Count > 0)
{
var current = clone.Dequeue();
var current = clone.Dequeue();
if (current is BeginOp)
{
var bo = current as BeginOp;
this.context.MoveTo(bo.Point.ToCairo());
if (current is BeginOp)
{
var bo = current as BeginOp;
this.context.MoveTo(bo.Point.ToCairo());
useFill = bo.IsFilled;
}
else if (current is LineToOp)
{
var lto = current as LineToOp;
this.context.LineTo(lto.Point.ToCairo());
useFill = bo.IsFilled;
System.Diagnostics.Debug.WriteLine("Start");
}
else if (current is LineToOp)
{
var lto = current as LineToOp;
this.context.LineTo(lto.Point.ToCairo());
}
else if (current is EndOp)
{
if (((EndOp)current).IsClosed)
this.context.ClosePath();
System.Diagnostics.Debug.WriteLine("End");
}
else if (current is CurveToOp)
{
var cto = current as CurveToOp;
this.context.CurveTo(cto.Point.ToCairo(), cto.Point2.ToCairo(), cto.Point3.ToCairo());
}
}
else if (current is EndOp)
System.Diagnostics.Debug.WriteLine("Render");
if (useFill)
{
if (((EndOp)current).IsClosed)
this.context.ClosePath();
this.context.FillPreserve();
}
else if (current is CurveToOp)
else
{
var cto = current as CurveToOp;
this.context.CurveTo(cto.Point.ToCairo(), cto.Point2.ToCairo(), cto.Point3.ToCairo());
this.context.StrokePreserve();
}
}
if (useFill)
{
this.context.StrokePreserve();
this.context.Fill();
}
else
{
this.context.Stroke();
}
pop.Dispose();
}
/// <summary>
@ -220,6 +224,17 @@ namespace Perspex.Cairo.Media
return Disposable.Create(() => this.context.ResetClip());
}
/// <summary>
/// Pushes an opacity value.
/// </summary>
/// <param name="opacity">The opacity.</param>
/// <returns>A disposable used to undo the opacity.</returns>
public IDisposable PushOpacity(double opacity)
{
// TODO: Implement
return Disposable.Empty;
}
/// <summary>
/// Pushes a matrix transformation.
/// </summary>
@ -253,6 +268,7 @@ namespace Perspex.Cairo.Media
{
this.SetBrush(pen.Brush);
this.context.LineWidth = pen.Thickness;
}
}
}

9
Cairo/Perspex.Cairo/Media/FormattedTextImpl.cs

@ -20,6 +20,7 @@ namespace Perspex.Cairo.Media
string fontFamily,
double fontSize,
FontStyle fontStyle,
TextAlignment textAlignment,
FontWeight fontWeight)
{
var context = Locator.Current.GetService<Pango.Context>();
@ -32,6 +33,8 @@ namespace Perspex.Cairo.Media
Style = (Pango.Style)fontStyle,
Weight = fontWeight.ToCairo()
};
this.Layout.Alignment = textAlignment.ToCairo();
}
public Size Constraint
@ -98,6 +101,12 @@ namespace Perspex.Cairo.Media
int width;
int height;
this.Layout.GetPixelSize(out width, out height);
if (this.Layout.Alignment == Pango.Alignment.Right)
{
return new Size(width, height);
}
return new Size(width, height);
}

1
Cairo/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs

@ -12,7 +12,6 @@ namespace Perspex.Cairo.Media.Imaging
public class RenderTargetBitmapImpl : BitmapImpl, IRenderTargetBitmapImpl
{
public RenderTargetBitmapImpl(
Cairo.ImageSurface surface)
: base(surface)

6
Cairo/Perspex.Cairo/Media/StreamGeometryContextImpl.cs

@ -24,21 +24,17 @@ namespace Perspex.Cairo.Media
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
// TODO: Implement
int i = 10;
points.Add(point);
}
public void BeginFigure(Point startPoint, bool isFilled)
{
System.Diagnostics.Debug.WriteLine("IS filled {0}", isFilled);
this.impl.Operations.Enqueue(new BeginOp { Point = startPoint, IsFilled = isFilled });
points.Add(startPoint);
}
public void BezierTo(Point point1, Point point2, Point point3)
{
// TODO: Implement
this.impl.Operations.Enqueue(new CurveToOp { Point = point1, Point2 = point2, Point3 = point3 });
points.Add(point1);
points.Add(point2);
@ -63,6 +59,7 @@ namespace Perspex.Cairo.Media
maxX = System.Math.Max(p.X, maxX);
maxY = System.Math.Max(p.Y, maxY);
}
var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, (int)maxX, (int)maxY));
var clone = new Queue<GeometryOp>(this.impl.Operations);
@ -95,6 +92,7 @@ namespace Perspex.Cairo.Media
}
context.StrokePreserve();
context.FillPreserve();
}
var test = context.StrokeExtents();

16
Cairo/Perspex.Cairo/Perspex.Cairo.csproj

@ -11,7 +11,8 @@
<AssemblyName>Perspex.Cairo</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>b122e795</NuGetPackageImportStamp>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -43,9 +44,9 @@
<Reference Include="pango-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f, processorArchitecture=MSIL">
<Package>gtk-sharp-2.0</Package>
</Reference>
<Reference Include="Splat, Version=1.6.1.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Splat.1.6.1\lib\Net45\Splat.dll</HintPath>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
@ -95,13 +96,6 @@
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets" Condition="Exists('..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

5
Cairo/Perspex.Cairo/Renderer.cs

@ -20,6 +20,8 @@ namespace Perspex.Cairo
/// </summary>
public class Renderer : RendererBase
{
private ImageSurface surface;
/// <summary>
/// Initializes a new instance of the <see cref="Renderer"/> class.
/// </summary>
@ -29,8 +31,7 @@ namespace Perspex.Cairo
public Renderer(IPlatformHandle handle, double width, double height)
{
}
private ImageSurface surface;
public Renderer(ImageSurface surface)
{
this.surface = surface;

7
Cairo/Perspex.Cairo/packages.config

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Splat" version="1.6.1" targetFramework="net45" />
<package id="StyleCop.MSBuild" version="4.7.49.0" targetFramework="net45" developmentDependency="true" />
<package id="Rx-Core" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Splat" version="1.6.2" targetFramework="net45" userInstalled="true" />
</packages>

2
Gtk/Perspex.Gtk/Input/GtkKeyboardDevice.cs

@ -22,7 +22,7 @@ namespace Perspex.Gtk
{
}
public static GtkKeyboardDevice Instance
public static new GtkKeyboardDevice Instance
{
get { return instance; }
}

7
Gtk/Perspex.Gtk/Input/GtkMouseDevice.cs

@ -22,7 +22,7 @@ namespace Perspex.Gtk
{
}
public static GtkMouseDevice Instance
public static new GtkMouseDevice Instance
{
get { return instance; }
}
@ -31,10 +31,5 @@ namespace Perspex.Gtk
{
this.clientPosition = p;
}
protected override Point GetClientPosition()
{
return this.clientPosition;
}
}
}

16
Gtk/Perspex.Gtk/Perspex.Gtk.csproj

@ -8,7 +8,8 @@
<RootNamespace>Perspex.Gtk</RootNamespace>
<AssemblyName>Perspex.Gtk</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>ad245ae8</NuGetPackageImportStamp>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -29,9 +30,9 @@
<ConsolePause>false</ConsolePause>
</PropertyGroup>
<ItemGroup>
<Reference Include="Splat, Version=1.6.1.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\packages\Splat.1.6.1\lib\Net45\Splat.dll</HintPath>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f">
@ -103,11 +104,4 @@
<None Include="packages.config" />
</ItemGroup>
<ItemGroup />
<Import Project="..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets" Condition="Exists('..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets'))" />
</Target>
</Project>

9
Gtk/Perspex.Gtk/packages.config

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="net45" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" />
<package id="Splat" version="1.6.1" targetFramework="net45" />
<package id="StyleCop.MSBuild" version="4.7.49.0" targetFramework="net45" developmentDependency="true" />
<package id="Rx-Core" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Splat" version="1.6.2" targetFramework="net45" userInstalled="true" />
</packages>

14
Perspex-Mono.sln

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22609.0
VisualStudioVersion = 14.0.22823.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Base", "Perspex.Base\Perspex.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject
@ -53,7 +53,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{596AF7
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Cairo.RenderTests", "Tests\Perspex.RenderTests\Perspex.Cairo.RenderTests.csproj", "{DABFD304-D6A4-4752-8123-C2CCF7AC7831}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Cairo.RenderTests", "Tests\Perspex.RenderTests\Perspex.Cairo.RenderTests.csproj", "{E106CF37-4066-4615-B684-172A6D30B058}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -141,10 +141,10 @@ Global
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.Build.0 = Release|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|Any CPU.Build.0 = Release|Any CPU
{E106CF37-4066-4615-B684-172A6D30B058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E106CF37-4066-4615-B684-172A6D30B058}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E106CF37-4066-4615-B684-172A6D30B058}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E106CF37-4066-4615-B684-172A6D30B058}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -158,6 +158,6 @@ Global
{2905FF23-53FB-45E6-AA49-6AF47A172056} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{415E048E-4611-4815-9CF2-D774E29079AC} = {2BAFBE53-7FA4-4BB9-976F-9AFCC4F9847D}
{DB070A10-BF39-4752-8456-86E9D5928478} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{DABFD304-D6A4-4752-8123-C2CCF7AC7831} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{E106CF37-4066-4615-B684-172A6D30B058} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
EndGlobal

161
Perspex-Windows.sln

@ -1,161 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22609.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Win32", "Windows\Perspex.Win32\Perspex.Win32.csproj", "{811A76CF-1CF6-440F-963B-BBE31BD72A82}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication", "TestApplication\TestApplication.csproj", "{E3A1060B-50D0-44E8-88B6-F44EF2E5BD72}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Direct2D1", "Windows\Perspex.Direct2D1\Perspex.Direct2D1.csproj", "{3E908F67-5543-4879-A1DC-08EACE79B3CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Base", "Perspex.Base\Perspex.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.SceneGraph", "Perspex.SceneGraph\Perspex.SceneGraph.csproj", "{EB582467-6ABB-43A1-B052-E981BA910E3A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Layout", "Perspex.Layout\Perspex.Layout.csproj", "{42472427-4774-4C81-8AFF-9F27B8E31721}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windows", "Windows", "{B39A8919-9F95-48FE-AD7B-76E08B509888}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Input", "Perspex.Input\Perspex.Input.csproj", "{62024B2D-53EB-4638-B26B-85EEAA54866E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Interactivity", "Perspex.Interactivity\Perspex.Interactivity.csproj", "{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Controls", "Perspex.Controls\Perspex.Controls.csproj", "{D2221C82-4A25-4583-9B43-D791E3F6820C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Styling", "Perspex.Styling\Perspex.Styling.csproj", "{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Styling.UnitTests", "Tests\Perspex.Styling.UnitTests\Perspex.Styling.UnitTests.csproj", "{47ECDF59-DEF8-4C53-87B1-2098A3429059}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Controls.UnitTests", "Tests\Perspex.Controls.UnitTests\Perspex.Controls.UnitTests.csproj", "{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Themes.Default", "Perspex.Themes.Default\Perspex.Themes.Default.csproj", "{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.SceneGraph.UnitTests", "Tests\Perspex.SceneGraph.UnitTests\Perspex.SceneGraph.UnitTests.csproj", "{76716382-3159-460E-BDA6-C5715CF606D7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Application", "Perspex.Application\Perspex.Application.csproj", "{799A7BB5-3C2C-48B6-85A7-406A12C420DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Diagnostics", "Perspex.Diagnostics\Perspex.Diagnostics.csproj", "{7062AE20-5DCC-4442-9645-8195BDECE63E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Base.UnitTests", "Tests\Perspex.Base.UnitTests\Perspex.Base.UnitTests.csproj", "{2905FF23-53FB-45E6-AA49-6AF47A172056}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{2BAFBE53-7FA4-4BB9-976F-9AFCC4F9847D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NGenerics", "NGenerics\NGenerics.csproj", "{415E048E-4611-4815-9CF2-D774E29079AC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Layout.UnitTests", "Tests\Perspex.Layout.UnitTests\Perspex.Layout.UnitTests.csproj", "{DB070A10-BF39-4752-8456-86E9D5928478}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Animation", "Perspex.Animation\Perspex.Animation.csproj", "{D211E587-D8BC-45B9-95A4-F297C8FA5200}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{596AF748-EAA9-4698-A78C-98724783AFC1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Direct2D1.RenderTests", "Tests\Perspex.RenderTests\Perspex.Direct2D1.RenderTests.csproj", "{DABFD304-D6A4-4752-8123-C2CCF7AC7831}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{811A76CF-1CF6-440F-963B-BBE31BD72A82}.Release|Any CPU.Build.0 = Release|Any CPU
{E3A1060B-50D0-44E8-88B6-F44EF2E5BD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E3A1060B-50D0-44E8-88B6-F44EF2E5BD72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E3A1060B-50D0-44E8-88B6-F44EF2E5BD72}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E3A1060B-50D0-44E8-88B6-F44EF2E5BD72}.Release|Any CPU.Build.0 = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E908F67-5543-4879-A1DC-08EACE79B3CD}.Release|Any CPU.Build.0 = Release|Any CPU
{B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B09B78D8-9B26-48B0-9149-D64A2F120F3F}.Release|Any CPU.Build.0 = Release|Any CPU
{EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB582467-6ABB-43A1-B052-E981BA910E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB582467-6ABB-43A1-B052-E981BA910E3A}.Release|Any CPU.Build.0 = Release|Any CPU
{42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42472427-4774-4C81-8AFF-9F27B8E31721}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42472427-4774-4C81-8AFF-9F27B8E31721}.Release|Any CPU.Build.0 = Release|Any CPU
{62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62024B2D-53EB-4638-B26B-85EEAA54866E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62024B2D-53EB-4638-B26B-85EEAA54866E}.Release|Any CPU.Build.0 = Release|Any CPU
{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B}.Release|Any CPU.Build.0 = Release|Any CPU
{D2221C82-4A25-4583-9B43-D791E3F6820C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2221C82-4A25-4583-9B43-D791E3F6820C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2221C82-4A25-4583-9B43-D791E3F6820C}.Release|Any CPU.Build.0 = Release|Any CPU
{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}.Release|Any CPU.Build.0 = Release|Any CPU
{47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47ECDF59-DEF8-4C53-87B1-2098A3429059}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47ECDF59-DEF8-4C53-87B1-2098A3429059}.Release|Any CPU.Build.0 = Release|Any CPU
{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5CCB5571-7C30-4E7D-967D-0E2158EBD91F}.Release|Any CPU.Build.0 = Release|Any CPU
{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F}.Release|Any CPU.Build.0 = Release|Any CPU
{76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{76716382-3159-460E-BDA6-C5715CF606D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76716382-3159-460E-BDA6-C5715CF606D7}.Release|Any CPU.Build.0 = Release|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{799A7BB5-3C2C-48B6-85A7-406A12C420DA}.Release|Any CPU.Build.0 = Release|Any CPU
{7062AE20-5DCC-4442-9645-8195BDECE63E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7062AE20-5DCC-4442-9645-8195BDECE63E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7062AE20-5DCC-4442-9645-8195BDECE63E}.Release|Any CPU.Build.0 = Release|Any CPU
{2905FF23-53FB-45E6-AA49-6AF47A172056}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2905FF23-53FB-45E6-AA49-6AF47A172056}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2905FF23-53FB-45E6-AA49-6AF47A172056}.Release|Any CPU.Build.0 = Release|Any CPU
{415E048E-4611-4815-9CF2-D774E29079AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{415E048E-4611-4815-9CF2-D774E29079AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{415E048E-4611-4815-9CF2-D774E29079AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{415E048E-4611-4815-9CF2-D774E29079AC}.Release|Any CPU.Build.0 = Release|Any CPU
{DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DB070A10-BF39-4752-8456-86E9D5928478}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB070A10-BF39-4752-8456-86E9D5928478}.Release|Any CPU.Build.0 = Release|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.Build.0 = Release|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{811A76CF-1CF6-440F-963B-BBE31BD72A82} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
{3E908F67-5543-4879-A1DC-08EACE79B3CD} = {B39A8919-9F95-48FE-AD7B-76E08B509888}
{47ECDF59-DEF8-4C53-87B1-2098A3429059} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{5CCB5571-7C30-4E7D-967D-0E2158EBD91F} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{76716382-3159-460E-BDA6-C5715CF606D7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{2905FF23-53FB-45E6-AA49-6AF47A172056} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{415E048E-4611-4815-9CF2-D774E29079AC} = {2BAFBE53-7FA4-4BB9-976F-9AFCC4F9847D}
{DB070A10-BF39-4752-8456-86E9D5928478} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{DABFD304-D6A4-4752-8123-C2CCF7AC7831} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
EndGlobal

17
Perspex.Animation/Animatable.cs

@ -9,10 +9,22 @@ namespace Perspex.Animation
using System.Linq;
using System.Reactive.Linq;
/// <summary>
/// Base class for control which can have property transitions.
/// </summary>
public class Animatable : PerspexObject
{
/// <summary>
/// The property transitions for the control.
/// </summary>
private PropertyTransitions propertyTransitions;
/// <summary>
/// Gets or sets the property transitions for the control.
/// </summary>
/// <value>
/// The property transitions for the control.
/// </value>
public PropertyTransitions PropertyTransitions
{
get
@ -31,6 +43,11 @@ namespace Perspex.Animation
}
}
/// <summary>
/// Reacts to a change in a <see cref="PerspexProperty"/> value in order to animate the
/// change if a <see cref="PropertyTransition"/> is set for the property..
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPropertyChanged(PerspexPropertyChangedEventArgs e)
{
if (e.Priority != BindingPriority.Animation && this.propertyTransitions != null)

31
Perspex.Animation/Animate.cs

@ -28,7 +28,7 @@ namespace Perspex.Animation
private static readonly TimeSpan Tick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
/// <summary>
/// Initializes the static class.
/// Initializes static members of the <see cref="Animate"/> class.
/// </summary>
static Animate()
{
@ -43,6 +43,9 @@ namespace Perspex.Animation
/// <summary>
/// The stopwatch used to track time.
/// </summary>
/// <value>
/// The stopwatch used to track time.
/// </value>
public static Stopwatch Stopwatch
{
get;
@ -53,10 +56,13 @@ namespace Perspex.Animation
/// Gets the animation timer.
/// </summary>
/// <remarks>
/// The animation timer ticks <see cref="FramesPerSecond"/> times per second. The
/// parameter passed to a subsciber is the time span since the animation system was
/// The animation timer ticks <see cref="FramesPerSecond"/> times per second. The
/// parameter passed to a subsciber is the time span since the animation system was
/// initialized.
/// </remarks>
/// <value>
/// The animation timer.
/// </value>
public static IObservable<TimeSpan> Timer
{
get;
@ -78,7 +84,7 @@ namespace Perspex.Animation
public static IObservable<double> GetTimer(TimeSpan duration)
{
var startTime = Stopwatch.Elapsed.Ticks;
var endTime = (startTime + duration.Ticks);
var endTime = startTime + duration.Ticks;
return Timer
.TakeWhile(x => x.Ticks < endTime)
.Select(x => (x.Ticks - startTime) / (double)duration.Ticks)
@ -89,15 +95,14 @@ namespace Perspex.Animation
/// <summary>
/// Animates a <see cref="PerspexProperty"/>.
/// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="target">The target object.</param>
/// <param name="property">The target property.</param>
/// <param name="start">The value of the property at the start of the animation.</param>
/// <param name="finish">The value of the property at the end of the animation.</param>
/// <param name="easing">The easing function to use.</param>
/// <param name="duration">The duration of the animation.</param>
/// <returns>An <see cref="IDisposable"/> that can be used to stop the animation.</returns>
public static IDisposable Property(
/// <returns>An <see cref="Animation"/> that can be used to track or stop the animation.</returns>
public static Animation Property(
PerspexObject target,
PerspexProperty property,
object start,
@ -106,7 +111,7 @@ namespace Perspex.Animation
TimeSpan duration)
{
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
return target.Bind(property, o, BindingPriority.Animation);
return new Animation(o, target.Bind(property, o, BindingPriority.Animation));
}
/// <summary>
@ -119,17 +124,17 @@ namespace Perspex.Animation
/// <param name="finish">The value of the property at the end of the animation.</param>
/// <param name="easing">The easing function to use.</param>
/// <param name="duration">The duration of the animation.</param>
/// <returns>An <see cref="IDisposable"/> that can be used to stop the animation.</returns>
public static IDisposable Property<T>(
/// <returns>An <see cref="Animation"/> that can be used to track or stop the animation.</returns>
public static Animation<T> Property<T>(
PerspexObject target,
PerspexProperty<T> property,
T start,
PerspexProperty<T> property,
T start,
T finish,
IEasing<T> easing,
TimeSpan duration)
{
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish));
return target.Bind(property, o, BindingPriority.Animation);
return new Animation<T>(o, target.Bind(property, o, BindingPriority.Animation));
}
}
}

58
Perspex.Animation/Animation.cs

@ -0,0 +1,58 @@
// -----------------------------------------------------------------------
// <copyright file="Animation.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Animation
{
using System;
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
public class Animation : IObservable<object>, IDisposable
{
/// <summary>
/// The animation being tracked.
/// </summary>
private IObservable<object> inner;
/// <summary>
/// The disposable used to cancel the animation.
/// </summary>
private IDisposable subscription;
/// <summary>
/// Initializes a new instance of the <see cref="Animation"/> class.
/// </summary>
/// <param name="inner">The animation observable being tracked.</param>
/// <param name="subscription">A disposable used to cancel the animation.</param>
public Animation(IObservable<object> inner, IDisposable subscription)
{
this.inner = inner;
this.subscription = subscription;
}
/// <summary>
/// Cancels the animation.
/// </summary>
public void Dispose()
{
this.subscription.Dispose();
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>
/// A reference to an interface that allows observers to stop receiving notifications
/// before the provider has finished sending them.
/// </returns>
public IDisposable Subscribe(IObserver<object> observer)
{
return this.inner.Subscribe(observer);
}
}
}

34
Perspex.Animation/AnimationExtensions.cs

@ -1,5 +1,5 @@
// -----------------------------------------------------------------------
// <copyright file="PropertyTransition.cs" company="Steven Kirk">
// <copyright file="AnimationExtensions.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
@ -8,21 +8,41 @@ namespace Perspex.Animation
{
using System;
/// <summary>
/// Defines animation extension methods.
/// </summary>
public static class AnimationExtensions
{
/// <summary>
/// Returns a new <see cref="PropertyTransition"/> for the specified
/// <see cref="PerspexProperty"/> using linear easing.
/// </summary>
/// <typeparam name="T">The type of the <see cref="PerspexProperty"/>.</typeparam>
/// <param name="property">The property to animate.</param>
/// <param name="milliseconds">The animation duration in milliseconds.</param>
/// <returns>
/// A <see cref="PropertyTransition"/> that can be added to the
/// <see cref="Animatable.PropertyTransitions"/> collection.
/// </returns>
public static PropertyTransition Transition<T>(this PerspexProperty<T> property, int milliseconds)
{
return Transition(property, TimeSpan.FromMilliseconds(milliseconds));
}
/// <summary>
/// Returns a new <see cref="PropertyTransition"/> for the specified
/// <see cref="PerspexProperty"/> using linear easing.
/// </summary>
/// <typeparam name="T">The type of the <see cref="PerspexProperty"/>.</typeparam>
/// <param name="property">The property to animate.</param>
/// <param name="duration">The animation duration.</param>
/// <returns>
/// A <see cref="PropertyTransition"/> that can be added to the
/// <see cref="Animatable.PropertyTransitions"/> collection.
/// </returns>
public static PropertyTransition Transition<T>(this PerspexProperty<T> property, TimeSpan duration)
{
return new PropertyTransition
{
Property = property,
Duration = duration,
Easing = LinearEasing.For<T>(),
};
return new PropertyTransition(property, duration, LinearEasing.For<T>());
}
}
}

59
Perspex.Animation/Animation`1.cs

@ -0,0 +1,59 @@
// -----------------------------------------------------------------------
// <copyright file="Animation`1.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Animation
{
using System;
/// <summary>
/// Tracks the progress of an animation.
/// </summary>
/// <typeparam name="T">The type of the value being animated./</typeparam>
public class Animation<T> : IObservable<T>, IDisposable
{
/// <summary>
/// The animation being tracked.
/// </summary>
private IObservable<T> inner;
/// <summary>
/// The disposable used to cancel the animation.
/// </summary>
private IDisposable subscription;
/// <summary>
/// Initializes a new instance of the <see cref="Animation{T}"/> class.
/// </summary>
/// <param name="inner">The animation observable being tracked.</param>
/// <param name="subscription">A disposable used to cancel the animation.</param>
public Animation(IObservable<T> inner, IDisposable subscription)
{
this.inner = inner;
this.subscription = subscription;
}
/// <summary>
/// Cancels the animation.
/// </summary>
public void Dispose()
{
this.subscription.Dispose();
}
/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The observer.</param>
/// <returns>
/// A reference to an interface that allows observers to stop receiving notifications
/// before the provider has finished sending them.
/// </returns>
public IDisposable Subscribe(IObserver<T> observer)
{
return this.inner.Subscribe(observer);
}
}
}

20
Perspex.Animation/IEasing.cs

@ -1,18 +1,26 @@
// -----------------------------------------------------------------------
// <copyright file="PropertyTransition.cs" company="Steven Kirk">
// <copyright file="IEasing.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Animation
{
/// <summary>
/// Defines the interface for easing functions.
/// </summary>
public interface IEasing
{
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
object Ease(double progress, object start, object finish);
}
public interface IEasing<T> : IEasing
{
T Ease(double progress, T start, T finish);
}
}

27
Perspex.Animation/IEasing`1.cs

@ -0,0 +1,27 @@
// -----------------------------------------------------------------------
// <copyright file="IEasing`1.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Animation
{
/// <summary>
/// Defines the interface for easing functions.
/// </summary>
/// <typeparam name="T">The type of the property being transitioned.</typeparam>
public interface IEasing<T> : IEasing
{
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
T Ease(double progress, T start, T finish);
}
}

29
Perspex.Animation/LinearDoubleEasing.cs

@ -1,21 +1,42 @@
// -----------------------------------------------------------------------
// <copyright file="PropertyTransition.cs" company="Steven Kirk">
// <copyright file="LinearDoubleEasing.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Animation
{
using System;
/// <summary>
/// Linearly eases a double value.
/// </summary>
public class LinearDoubleEasing : IEasing<double>
{
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
public double Ease(double progress, double start, double finish)
{
return ((finish - start) * progress) + start;
}
public object Ease(double progress, object start, object finish)
/// <summary>
/// Returns the value of the transition for the specified progress.
/// </summary>
/// <param name="progress">The progress of the transition, from 0 to 1.</param>
/// <param name="start">The start value of the transition.</param>
/// <param name="finish">The end value of the transition.</param>
/// <returns>
/// A value between <paramref name="start"/> and <paramref name="finish"/> as determined
/// by <paramref name="progress"/>.
/// </returns>
object IEasing.Ease(double progress, object start, object finish)
{
return this.Ease(progress, (double)start, (double)finish);
}

18
Perspex.Animation/LinearEasing.cs

@ -1,5 +1,5 @@
// -----------------------------------------------------------------------
// <copyright file="PropertyTransition.cs" company="Steven Kirk">
// <copyright file="LinearEasing.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
@ -8,13 +8,25 @@ namespace Perspex.Animation
{
using System;
/// <summary>
/// Returns a linear <see cref="IEasing"/> for the specified type.
/// </summary>
/// <remarks>
/// Unfortunately this class is needed as there's no way to create a true generic easing
/// function at compile time, as mathematical operators don't have an interface.
/// </remarks>
public static class LinearEasing
{
public static LinearDoubleEasing For<T>()
/// <summary>
/// A linear easing function for the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>An easing function.</returns>
public static IEasing<T> For<T>()
{
if (typeof(T) == typeof(double))
{
return new LinearDoubleEasing();
return (IEasing<T>)new LinearDoubleEasing();
}
else
{

6
Perspex.Animation/Perspex.Animation.csproj

@ -41,9 +41,12 @@
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Animation`1.cs" />
<Compile Include="Animation.cs" />
<Compile Include="Animate.cs" />
<Compile Include="Animatable.cs" />
<Compile Include="AnimationExtensions.cs" />
<Compile Include="IEasing`1.cs" />
<Compile Include="LinearEasing.cs" />
<Compile Include="LinearDoubleEasing.cs" />
<Compile Include="IEasing.cs" />
@ -68,6 +71,9 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\packages\StyleCop.Analyzers.1.0.0-alpha008\tools\analyzers\C#\StyleCop.Analyzers.dll" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

14
Perspex.Animation/Properties/AssemblyInfo.cs

@ -1,9 +1,15 @@
using System.Resources;
// -----------------------------------------------------------------------
// <copyright file="AssemblyInfo.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
using System.Resources;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Perspex.Animation")]
@ -19,11 +25,11 @@ using System.Runtime.InteropServices;
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]

40
Perspex.Animation/PropertyTransition.cs

@ -8,12 +8,46 @@ namespace Perspex.Animation
{
using System;
/// <summary>
/// Defines how a property should be animated using a transition.
/// </summary>
public class PropertyTransition
{
public PerspexProperty Property { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PropertyTransition"/> class.
/// </summary>
/// <param name="property">The property to be animated/</param>
/// <param name="duration">The duration of the animation.</param>
/// <param name="easing">The easing function to use.</param>
public PropertyTransition(PerspexProperty property, TimeSpan duration, IEasing easing)
{
this.Property = property;
this.Duration = duration;
this.Easing = easing;
}
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets the property to be animated.
/// </summary>
/// <value>
/// The property to be animated.
/// </value>
public PerspexProperty Property { get; }
public IEasing Easing { get; set; }
/// <summary>
/// Gets the duration of the animation.
/// </summary>
/// <value>
/// The duration of the animation.
/// </value>
public TimeSpan Duration { get; }
/// <summary>
/// Gets the easing function used.
/// </summary>
/// <value>
/// The easing function.
/// </value>
public IEasing Easing { get; }
}
}

5
Perspex.Animation/PropertyTransitions.cs

@ -4,12 +4,13 @@
// </copyright>
// -----------------------------------------------------------------------
using Perspex.Collections;
namespace Perspex.Animation
{
using Perspex.Collections;
/// <summary>
/// A collection of <see cref="PropertyTransition"/> definitions.
/// </summary>
public class PropertyTransitions : PerspexList<PropertyTransition>
{
}

11
Perspex.Animation/packages.config

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Core" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="StyleCop.Analyzers" version="1.0.0-alpha008" targetFramework="portable45-net45+win8" userInstalled="true" />
</packages>

75
Perspex.Application/Application.cs

@ -16,12 +16,36 @@ namespace Perspex
using Perspex.Threading;
using Splat;
/// <summary>
/// Encapsulates a Perspex application.
/// </summary>
/// <remarks>
/// The <see cref="Application"/> class encapsulates Perspex application-specific
/// functionality, including:
/// - A global set of <see cref="DataTemplates"/>.
/// - A global set of <see cref="Styles"/>.
/// - A <see cref="FocusManager"/>.
/// - An <see cref="InputManager"/>.
/// - Registers services needed by the rest of Perspex in the <see cref="RegisterServices"/>
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : IGlobalDataTemplates, IGlobalStyles
{
/// <summary>
/// The application-global data templates.
/// </summary>
private DataTemplates dataTemplates;
/// <summary>
/// The styler that will be used to apply styles to controls.
/// </summary>
private Styler styler = new Styler();
/// <summary>
/// Initializes a new instance of the <see cref="Application"/> class.
/// </summary>
/// <param name="theme">The theme to use.</param>
public Application(Styles theme)
{
if (Current != null)
@ -34,12 +58,24 @@ namespace Perspex
this.RegisterServices();
}
/// <summary>
/// Gets the current instance of the <see cref="Application"/> class.
/// </summary>
/// <value>
/// The current instance of the <see cref="Application"/> class.
/// </value>
public static Application Current
{
get;
private set;
}
/// <summary>
/// Gets or sets the application's global data templates.
/// </summary>
/// <value>
/// The application's global data templates.
/// </value>
public DataTemplates DataTemplates
{
get
@ -58,30 +94,49 @@ namespace Perspex
}
}
/// <summary>
/// Gets the application's focus manager.
/// </summary>
/// <value>
/// The application's focus manager.
/// </value>
public IFocusManager FocusManager
{
get;
private set;
}
/// <summary>
/// Gets the application's input manager.
/// </summary>
/// <value>
/// The application's input manager.
/// </value>
public InputManager InputManager
{
get;
private set;
}
public KeyboardNavigation KeyboardNavigation
{
get;
private set;
}
/// <summary>
/// Gets the application's global styles.
/// </summary>
/// <value>
/// The application's global styles.
/// </value>
/// <remarks>
/// Global styles apply to all windows in the application.
/// </remarks>
public Styles Styles
{
get;
private set;
}
/// <summary>
/// Runs the application's main loop until the <see cref="ICloseable"/> is closed.
/// </summary>
/// <param name="closable">The closable to track</param>
public void Run(ICloseable closable)
{
var source = new CancellationTokenSource();
@ -89,19 +144,21 @@ namespace Perspex
Dispatcher.UIThread.MainLoop(source.Token);
}
/// <summary>
/// Register's the services needed by Perspex.
/// </summary>
protected virtual void RegisterServices()
{
this.FocusManager = new FocusManager();
this.InputManager = new InputManager();
this.KeyboardNavigation = new KeyboardNavigation();
Locator.CurrentMutable.Register(() => new AccessKeyHandler(), typeof(IAccessKeyHandler));
Locator.CurrentMutable.Register(() => this, typeof(IGlobalDataTemplates));
Locator.CurrentMutable.Register(() => this, typeof(IGlobalStyles));
Locator.CurrentMutable.Register(() => this.FocusManager, typeof(IFocusManager));
Locator.CurrentMutable.Register(() => this.InputManager, typeof(IInputManager));
Locator.CurrentMutable.Register(() => this.KeyboardNavigation, typeof(IKeyboardNavigation));
Locator.CurrentMutable.Register(() => new KeyboardNavigationHandler(), typeof(IKeyboardNavigationHandler));
Locator.CurrentMutable.Register(() => this.styler, typeof(IStyler));
Locator.CurrentMutable.Register(() => new LayoutManager(), typeof(ILayoutManager));
Locator.CurrentMutable.Register(() => new RenderManager(), typeof(IRenderManager));
}

18
Perspex.Application/Perspex.Application.csproj

@ -15,7 +15,8 @@
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>088bcbce</NuGetPackageImportStamp>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -70,8 +71,9 @@
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Splat">
<HintPath>..\packages\Splat.1.6.1\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Core">
<HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
@ -90,14 +92,10 @@
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\packages\StyleCop.Analyzers.1.0.0-alpha008\tools\analyzers\C#\StyleCop.Analyzers.dll" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<Import Project="..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets" Condition="Exists('..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

6
Perspex.Application/Properties/AssemblyInfo.cs

@ -7,7 +7,7 @@
using System.Reflection;
using System.Resources;
// General Information about an assembly is controlled through the following
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Perspex.Application")]
@ -23,11 +23,11 @@ using System.Resources;
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]

14
Perspex.Application/packages.config

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Splat" version="1.6.1" targetFramework="portable-net45+win" />
<package id="StyleCop.MSBuild" version="4.7.49.0" targetFramework="portable-net45+win" developmentDependency="true" />
<package id="Rx-Core" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Splat" version="1.6.2" targetFramework="portable45-net45+win8" userInstalled="true" />
<package id="StyleCop.Analyzers" version="1.0.0-alpha008" targetFramework="portable45-net45+win8" userInstalled="true" />
</packages>

16
Perspex.Base/Binding.cs

@ -7,7 +7,7 @@
namespace Perspex
{
using System;
using System.Reactive.Linq;
using System.Reactive;
public enum BindingMode
{
@ -18,7 +18,7 @@ namespace Perspex
OneWayToSource,
}
public class Binding : IObservable<object>
public class Binding : ObservableBase<object>, IDescription
{
public BindingMode Mode
{
@ -44,6 +44,8 @@ namespace Perspex
set;
}
public string Description => string.Format("{0}.{1}", this.Source?.GetType().Name, this.Property.Name);
public static Binding operator !(Binding binding)
{
return binding.WithMode(BindingMode.TwoWay);
@ -54,11 +56,6 @@ namespace Perspex
return binding.WithMode(BindingMode.TwoWay);
}
public IDisposable Subscribe(IObserver<object> observer)
{
return this.Source.GetObservable(this.Property).Subscribe(observer);
}
public Binding WithMode(BindingMode mode)
{
this.Mode = mode;
@ -70,5 +67,10 @@ namespace Perspex
this.Priority = priority;
return this;
}
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
return this.Source.GetObservable(this.Property).Subscribe(observer);
}
}
}

2
Perspex.Base/Collections/IPerspexReadOnlyList.cs

@ -1,5 +1,5 @@
// -----------------------------------------------------------------------
// <copyright file="IReadOnlyPerspexList.cs" company="Steven Kirk">
// <copyright file="IPerspexReadOnlyList.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------

2
Perspex.Base/Collections/PerspexList.cs

@ -20,7 +20,7 @@ namespace Perspex.Collections
/// <remarks>
/// PerspexList is similar to <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/>
/// except that when the <see cref="Clear"/> method is called, it notifies with a
/// <see cref="NotifyCollectionChangedAction.Remove"/> action, passing the items that were
/// <see cref="NotifyCollectionChangedAction.Remove"/> action, passing the items that were
/// removed.
/// </remarks>
public class PerspexList<T> : IPerspexList<T>, INotifyCollectionChanged, INotifyPropertyChanged

69
Perspex.Base/Collections/PerspexReadOnlyListView.cs

@ -29,17 +29,17 @@ namespace Perspex.Collections
if (source != null)
{
this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
}
}
public T this[int index]
{
get { return this.source[index]; }
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public int Count
{
get { return this.source.Count; }
get { return this.source?.Count ?? 0; }
}
public IPerspexReadOnlyList<T> Source
@ -54,6 +54,7 @@ namespace Perspex.Collections
if (this.source != null)
{
this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
if (this.CollectionChanged != null)
{
@ -70,12 +71,13 @@ namespace Perspex.Collections
if (this.source != null)
{
this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
if (this.CollectionChanged != null)
{
var ev = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add,
this.source,
this.source.ToList(),
0);
this.CollectionChanged(this, ev);
}
@ -83,13 +85,15 @@ namespace Perspex.Collections
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public T this[int index]
{
get { return this.source[index]; }
}
public void Dispose()
{
this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
}
public IEnumerator<T> GetEnumerator()
@ -122,7 +126,7 @@ namespace Perspex.Collections
ev = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
e.OldItems,
e.NewStartingIndex);
e.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
ev = new NotifyCollectionChangedEventArgs(
@ -139,6 +143,11 @@ namespace Perspex.Collections
this.CollectionChanged(this, ev);
}
}
private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, e);
}
}
public class PerspexReadOnlyListView<TIn, TOut> : IPerspexReadOnlyList<TOut>, IDisposable
@ -160,22 +169,17 @@ namespace Perspex.Collections
if (source != null)
{
this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
}
}
public TOut this[int index]
{
get
{
return (this.convert != null) ?
this.convert(this.source[index]) :
(TOut)(object)this.source[index];
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public int Count
{
get { return this.source.Count; }
get { return this.source?.Count ?? 0; }
}
public IPerspexReadOnlyList<TIn> Source
@ -190,6 +194,7 @@ namespace Perspex.Collections
if (this.source != null)
{
this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
if (this.CollectionChanged != null)
{
@ -206,6 +211,7 @@ namespace Perspex.Collections
if (this.source != null)
{
this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
if (this.CollectionChanged != null)
{
@ -219,15 +225,22 @@ namespace Perspex.Collections
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
public TOut this[int index]
{
get
{
return (this.convert != null) ?
this.convert(this.source[index]) :
(TOut)(object)this.source[index];
}
}
public void Dispose()
{
if (this.source != null)
{
this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
}
}
@ -248,7 +261,7 @@ namespace Perspex.Collections
return this.GetEnumerator();
}
private IList<TOut> ConvertList(IEnumerable list)
private IList ConvertList(IEnumerable list)
{
return list.Cast<TIn>().Select(this.convert).ToList();
}
@ -271,7 +284,7 @@ namespace Perspex.Collections
ev = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
this.ConvertList(e.OldItems),
e.NewStartingIndex);
e.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
ev = new NotifyCollectionChangedEventArgs(
@ -288,6 +301,10 @@ namespace Perspex.Collections
this.CollectionChanged(this, ev);
}
}
}
private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, e);
}
}
}

28
Perspex.Base/Collections/PerspexSingleItemList.cs

@ -1,5 +1,5 @@
// -----------------------------------------------------------------------
// <copyright file="PerspexList.cs" company="Steven Kirk">
// <copyright file="PerspexSingleItemList.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
@ -36,18 +36,9 @@ namespace Perspex.Collections
this.item = item;
}
public T this[int index]
{
get
{
if (index < 0 || index >= this.Count)
{
throw new ArgumentOutOfRangeException();
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
return item;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public int Count
{
@ -96,9 +87,18 @@ namespace Perspex.Collections
}
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public T this[int index]
{
get
{
if (index < 0 || index >= this.Count)
{
throw new ArgumentOutOfRangeException();
}
public event PropertyChangedEventHandler PropertyChanged;
return this.item;
}
}
public IEnumerator<T> GetEnumerator()
{

35
Perspex.Base/Diagnostics/PerspexObjectExtensions.cs

@ -0,0 +1,35 @@
// --------------------------------------------------------------------
// <copyright file="PerspexObjectExtensions.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Diagnostics
{
public static class PerspexObjectExtensions
{
public static PerspexPropertyValue GetDiagnostic(this PerspexObject o, PerspexProperty property)
{
var set = o.GetSetValues();
PriorityValue value;
if (set.TryGetValue(property, out value))
{
return new PerspexPropertyValue(
property,
value.Value,
(BindingPriority)value.ValuePriority,
value.GetDiagnostic());
}
else
{
return new PerspexPropertyValue(
property,
o.GetValue(property),
BindingPriority.Unset,
"Unset");
}
}
}
}

25
Perspex.Base/Diagnostics/PerspexPropertyValue.cs

@ -1,4 +1,4 @@
// -----------------------------------------------------------------------
// --------------------------------------------------------------------
// <copyright file="PerspexPropertyValue.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
@ -8,23 +8,24 @@ namespace Perspex.Diagnostics
{
public class PerspexPropertyValue
{
public PerspexPropertyValue(PerspexProperty property, object value)
public PerspexPropertyValue(
PerspexProperty property,
object value,
BindingPriority priority,
string diagnostic)
{
this.Property = property;
this.CurrentValue = value;
this.Value = value;
this.Priority = priority;
this.Diagnostic = diagnostic;
}
public PerspexPropertyValue(PerspexProperty property, PriorityValue priorityValue)
{
this.Property = property;
this.CurrentValue = priorityValue.Value;
this.PriorityValue = priorityValue;
}
public PerspexProperty Property { get; }
public PerspexProperty Property { get; private set; }
public object Value { get; }
public object CurrentValue { get; private set; }
public BindingPriority Priority { get; }
public PriorityValue PriorityValue { get; private set; }
public string Diagnostic { get; }
}
}

14
Perspex.Base/GlobalSuppressions.cs

@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// <copyright file="GlobalSuppressions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields must be private",
Justification = "PerspexProperty fields should not be private.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1609:Property documentation must have value",
Justification = "This rule is fscking pointless")]

9
Perspex.Base/IDescription.cs

@ -6,8 +6,17 @@
namespace Perspex
{
/// <summary>
/// Interface for objects with a <see cref="Description"/>.
/// </summary>
public interface IDescription
{
/// <summary>
/// Gets the description of the object.
/// </summary>
/// <value>
/// The description of the object.
/// </value>
string Description { get; }
}
}

30
Perspex.Base/Perspex.Base.csproj

@ -15,6 +15,8 @@
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -35,6 +37,11 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="Binding.cs" />
<Compile Include="Diagnostics\PerspexObjectExtensions.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="PerspexObjectExtensions.cs" />
<Compile Include="PerspexProperty`1.cs" />
<Compile Include="PriorityBindingEntry.cs" />
<Compile Include="Collections\IPerspexList.cs" />
<Compile Include="Collections\PerspexReadOnlyListView.cs" />
<Compile Include="Collections\PerspexList.cs" />
@ -43,7 +50,6 @@
<Compile Include="Diagnostics\PerspexPropertyValue.cs" />
<Compile Include="IDescription.cs" />
<Compile Include="Collections\PerspexListExtensions.cs" />
<Compile Include="IObservableDescription.cs" />
<Compile Include="Collections\IPerspexReadOnlyList.cs" />
<Compile Include="PerspexObject.cs" />
<Compile Include="PerspexProperty.cs" />
@ -51,17 +57,25 @@
<Compile Include="Platform\IPlatformHandle.cs" />
<Compile Include="Platform\IPlatformThreadingInterface.cs" />
<Compile Include="Platform\PlatformHandle.cs" />
<Compile Include="PriorityLevel.cs" />
<Compile Include="PriorityValue.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reactive\PerspexObservable.cs" />
<Compile Include="Threading\Dispatcher.cs" />
<Compile Include="Threading\DispatcherPriority.cs" />
<Compile Include="Threading\DispatcherTimer.cs" />
<Compile Include="Threading\MainLoop.cs" />
<Compile Include="Threading\PerspexScheduler.cs" />
<Compile Include="Utilities\TypeUtilities.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Splat">
<HintPath>..\packages\Splat.1.6.1\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\packages\Serilog.1.5.5\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Serilog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Core">
<HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
@ -86,14 +100,10 @@
<Name>NGenerics</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\packages\StyleCop.Analyzers.1.0.0-alpha008\tools\analyzers\C#\StyleCop.Analyzers.dll" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<Import Project="..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets" Condition="Exists('..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

294
Perspex.Base/PerspexObject.cs

@ -10,10 +10,13 @@ namespace Perspex
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using Perspex.Diagnostics;
using Splat;
using Perspex.Reactive;
using Serilog;
using Serilog.Core.Enrichers;
using Perspex.Utilities;
/// <summary>
/// The priority of a binding.
@ -23,20 +26,20 @@ namespace Perspex
/// <summary>
/// A value that comes from an animation.
/// </summary>
Animation = -1,
Animation = -2,
/// <summary>
/// A local value.
/// </summary>
LocalValue,
LocalValue = 0,
/// <summary>
/// A triggered style binding.
/// </summary>
/// <remarks>
/// A style trigger is a selector such as .class which overrides a
/// A style trigger is a selector such as .class which overrides a
/// <see cref="TemplatedParent"/> binding. In this way, a basic control can have
/// for example a Background from the templated parent which changes when the
/// for example a Background from the templated parent which changes when the
/// control has the :pointerover class.
/// </remarks>
StyleTrigger,
@ -63,7 +66,7 @@ namespace Perspex
/// <remarks>
/// This class is analogous to DependencyObject in WPF.
/// </remarks>
public class PerspexObject : INotifyPropertyChanged, IEnableLogger
public class PerspexObject : INotifyPropertyChanged
{
/// <summary>
/// The registered properties by type.
@ -87,25 +90,33 @@ namespace Perspex
/// </summary>
private PropertyChangedEventHandler inpcChanged;
/// <summary>
/// A serilog logger for logging property events.
/// </summary>
private ILogger propertyLog;
/// <summary>
/// Initializes a new instance of the <see cref="PerspexObject"/> class.
/// </summary>
public PerspexObject()
{
foreach (var p in this.GetAllValues())
this.propertyLog = Log.ForContext(new[]
{
var priority = p.PriorityValue != null ?
(BindingPriority)p.PriorityValue.ValuePriority :
BindingPriority.LocalValue;
new PropertyEnricher("Area", "Property"),
new PropertyEnricher("SourceContext", this.GetType()),
new PropertyEnricher("Id", this.GetHashCode()),
});
foreach (var property in this.GetRegisteredProperties())
{
var e = new PerspexPropertyChangedEventArgs(
this,
p.Property,
PerspexProperty.UnsetValue,
p.CurrentValue,
priority);
this,
property,
PerspexProperty.UnsetValue,
property.GetDefaultValue(this.GetType()),
BindingPriority.Unset);
p.Property.NotifyInitialized(e);
property.NotifyInitialized(e);
}
}
@ -124,9 +135,12 @@ namespace Perspex
}
/// <summary>
/// Gets or sets the parent object that inherited <see cref="PerspexProperty"/> values
/// Gets or sets the parent object that inherited <see cref="PerspexProperty"/> values
/// are inherited from.
/// </summary>
/// <value>
/// The inheritance parent.
/// </value>
protected PerspexObject InheritanceParent
{
get
@ -200,8 +214,8 @@ namespace Perspex
set
{
BindingMode mode = (binding.Mode == BindingMode.Default) ?
binding.Property.DefaultBindingMode :
BindingMode mode = (binding.Mode == BindingMode.Default) ?
binding.Property.DefaultBindingMode :
binding.Mode;
Binding sourceBinding = value as Binding;
@ -217,7 +231,7 @@ namespace Perspex
this.Bind(binding.Property, value, binding.Priority);
break;
case BindingMode.OneTime:
this.SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property));
this.SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property), binding.Priority);
break;
case BindingMode.OneWayToSource:
sourceBinding.Source.Bind(sourceBinding.Property, this.GetObservable(binding.Property), binding.Priority);
@ -262,7 +276,7 @@ namespace Perspex
/// <param name="type">The type.</param>
/// <param name="property">The property.</param>
/// <remarks>
/// You won't usually want to call this method directly, instead use the
/// You won't usually want to call this method directly, instead use the
/// <see cref="PerspexProperty.Register"/> method.
/// </remarks>
public static void Register(Type type, PerspexProperty property)
@ -291,7 +305,7 @@ namespace Perspex
public void ClearValue(PerspexProperty property)
{
Contract.Requires<NullReferenceException>(property != null);
this.SetValue(property, PerspexProperty.UnsetValue);
}
@ -304,31 +318,35 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(property != null);
return Observable.Create<object>(observer =>
{
EventHandler<PerspexPropertyChangedEventArgs> handler = (s, e) =>
return new PerspexObservable<object>(
observer =>
{
if (e.Property == property)
EventHandler<PerspexPropertyChangedEventArgs> handler = (s, e) =>
{
observer.OnNext(e.NewValue);
}
};
if (e.Property == property)
{
observer.OnNext(e.NewValue);
}
};
this.PropertyChanged += handler;
observer.OnNext(this.GetValue(property));
return () =>
{
this.PropertyChanged -= handler;
};
}).StartWith(this.GetValue(property));
this.PropertyChanged += handler;
return Disposable.Create(() =>
{
this.PropertyChanged -= handler;
});
},
this.GetObservableDescription(property));
}
/// <summary>
/// Gets an observable for a <see cref="PerspexProperty"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="property"></param>
/// <returns></returns>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="property">The property.</param>
/// <returns>An observable.</returns>
public IObservable<T> GetObservable<T>(PerspexProperty<T> property)
{
Contract.Requires<NullReferenceException>(property != null);
@ -339,28 +357,31 @@ namespace Perspex
/// <summary>
/// Gets an observable for a <see cref="PerspexProperty"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="property"></param>
/// <returns></returns>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>An observable which when subscribed pushes the old and new values of the
/// property each time it is changed.</returns>
public IObservable<Tuple<T, T>> GetObservableWithHistory<T>(PerspexProperty<T> property)
{
return Observable.Create<Tuple<T, T>>(observer =>
{
EventHandler<PerspexPropertyChangedEventArgs> handler = (s, e) =>
return new PerspexObservable<Tuple<T, T>>(
observer =>
{
if (e.Property == property)
EventHandler<PerspexPropertyChangedEventArgs> handler = (s, e) =>
{
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
}
};
if (e.Property == property)
{
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
}
};
this.PropertyChanged += handler;
this.PropertyChanged += handler;
return () =>
{
this.PropertyChanged -= handler;
};
});
return Disposable.Create(() =>
{
this.PropertyChanged -= handler;
});
},
this.GetObservableDescription(property));
}
/// <summary>
@ -396,6 +417,7 @@ namespace Perspex
/// <summary>
/// Gets a <see cref="PerspexProperty"/> value.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
public T GetValue<T>(PerspexProperty<T> property)
@ -405,23 +427,12 @@ namespace Perspex
return (T)this.GetValue((PerspexProperty)property);
}
public IEnumerable<PerspexPropertyValue> GetAllValues()
{
foreach (PerspexProperty property in this.GetRegisteredProperties())
{
PriorityValue value;
if (this.values.TryGetValue(property, out value))
{
yield return new PerspexPropertyValue(property, value);
}
else
{
yield return new PerspexPropertyValue(property, this.GetValue(property));
}
}
}
/// <summary>
/// Gets all properties that are registered on this object.
/// </summary>
/// <returns>
/// A collection of <see cref="PerspexProperty"/> objects.
/// </returns>
public IEnumerable<PerspexProperty> GetRegisteredProperties()
{
Type type = this.GetType();
@ -442,17 +453,6 @@ namespace Perspex
}
}
/// <summary>
/// Gets all of the <see cref="PerspexProperty"/> values explicitly set on this object.
/// </summary>
public IEnumerable<PerspexPropertyValue> GetSetValues()
{
foreach (var value in this.values)
{
yield return new PerspexPropertyValue(value.Key, value.Value);
}
}
/// <summary>
/// Checks whether a <see cref="PerspexProperty"/> is set on this object.
/// </summary>
@ -497,11 +497,14 @@ namespace Perspex
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
public void SetValue(PerspexProperty property, object value)
/// <param name="priority">The priority of the value.</param>
public void SetValue(
PerspexProperty property,
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
const int Priority = (int)BindingPriority.LocalValue;
PriorityValue v;
if (!this.IsRegistered(property))
@ -512,13 +515,13 @@ namespace Perspex
this.GetType()));
}
if (!PriorityValue.IsValidValue(value, property.PropertyType))
if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
property.Name,
value,
value.GetType().FullName));
value?.GetType().FullName ?? "(null)"));
}
if (!this.values.TryGetValue(property, out v))
@ -531,15 +534,13 @@ namespace Perspex
v = this.CreatePriorityValue(property);
this.values.Add(property, v);
}
this.Log().Debug(
"Set local value of {0}.{1} (#{2:x8}) to {3}",
this.GetType().Name,
property.Name,
this.GetHashCode(),
value);
v.Replace(Observable.Never<object>().StartWith(value), Priority);
this.propertyLog.Verbose(
"Set {Property} to {$Value} with priority {Priority}",
property,
value,
priority);
v.SetDirectValue(value, (int)priority);
}
/// <summary>
@ -548,17 +549,20 @@ namespace Perspex
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
public void SetValue<T>(PerspexProperty<T> property, T value)
/// <param name="priority">The priority of the value.</param>
public void SetValue<T>(
PerspexProperty<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
this.SetValue((PerspexProperty)property, value);
this.SetValue((PerspexProperty)property, value, priority);
}
/// <summary>
/// Binds a <see cref="PerspexProperty"/> to an observable.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param>
@ -575,27 +579,27 @@ namespace Perspex
PriorityValue v;
IDescription description = source as IDescription;
if (!this.IsRegistered(property))
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
this.GetType()));
}
if (!this.values.TryGetValue(property, out v))
{
v = this.CreatePriorityValue(property);
this.values.Add(property, v);
}
this.Log().Debug(
"Bound value of {0}.{1} (#{2:x8}) to {3}",
this.GetType().Name,
property.Name,
this.GetHashCode(),
description != null ? description.Description : "[Anonymous]");
this.propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
if (priority == BindingPriority.LocalValue)
{
return v.Replace(source, (int)priority);
}
else
{
return v.Add(source, (int)priority);
}
return v.Add(source, (int)priority);
}
/// <summary>
@ -609,8 +613,8 @@ namespace Perspex
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
PerspexProperty<T> property,
IObservable<T> source,
PerspexProperty<T> property,
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires<NullReferenceException>(property != null);
@ -624,20 +628,22 @@ namespace Perspex
/// <param name="property">The property on this object.</param>
/// <param name="source">The source object.</param>
/// <param name="sourceProperty">The property on the source object.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
/// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this. Two-way
/// bindings are always at the LocalValue priority.
/// The binding is first carried out from <paramref name="source"/> to this.
/// </remarks>
public void BindTwoWay(
PerspexProperty property,
PerspexObject source,
PerspexProperty sourceProperty)
public IDisposable BindTwoWay(
PerspexProperty property,
PerspexObject source,
PerspexProperty sourceProperty,
BindingPriority priority = BindingPriority.LocalValue)
{
source.GetObservable(sourceProperty).Subscribe(x => this.SetValue(property, x));
this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x));
return new CompositeDisposable(
this.Bind(property, source.GetObservable(sourceProperty)),
source.Bind(sourceProperty, this.GetObservable(property)));
}
/// <summary>
@ -654,6 +660,15 @@ namespace Perspex
}
}
/// <summary>
/// Gets all priority values set on the object.
/// </summary>
/// <returns>A collection of property/value tuples.</returns>
internal IDictionary<PerspexProperty, PriorityValue> GetSetValues()
{
return this.values;
}
/// <summary>
/// Forces re-coercion of properties when a property value changes.
/// </summary>
@ -707,13 +722,12 @@ namespace Perspex
{
this.RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority);
this.Log().Debug(
"Value of {0}.{1} (#{2:x8}) changed from {3} to {4}",
this.GetType().Name,
property.Name,
this.GetHashCode(),
this.propertyLog.Verbose(
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
oldValue,
newValue);
newValue,
(BindingPriority)result.ValuePriority);
}
});
@ -755,6 +769,16 @@ namespace Perspex
}
}
/// <summary>
/// Gets a description of a property that van be used in observables.
/// </summary>
/// <param name="property">The property</param>
/// <returns>The description.</returns>
private string GetObservableDescription(PerspexProperty property)
{
return string.Format("{0}.{1}", this.GetType().Name, property.Name);
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event.
/// </summary>
@ -763,17 +787,17 @@ namespace Perspex
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
private void RaisePropertyChanged(
PerspexProperty property,
object oldValue,
object newValue,
PerspexProperty property,
object oldValue,
object newValue,
BindingPriority priority)
{
Contract.Requires<NullReferenceException>(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this,
property,
oldValue,
this,
property,
oldValue,
newValue,
priority);

53
Perspex.Base/PerspexObjectExtensions.cs

@ -0,0 +1,53 @@
// -----------------------------------------------------------------------
// <copyright file="PerspexObjectExtensions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
{
using System;
using System.Reactive.Linq;
/// <summary>
/// Provides extension methods for <see cref="PerspexObject"/> and related classes.
/// </summary>
public static class PerspexObjectExtensions
{
/// <summary>
/// Subscribes to a property changed notifications for changes that originate from a
/// <typeparamref name="TTarget"/>.
/// </summary>
/// <typeparam name="TTarget">The type of the property change sender.</typeparam>
/// <param name="observable">The property changed observable.</param>
/// <param name="handler">Given a TTarget, returns the handler.</param>
/// <returns>A disposable that can be used to terminate the subscription.</returns>
public static IDisposable AddClassHandler<TTarget>(
this IObservable<PerspexPropertyChangedEventArgs> observable,
Func<TTarget, Action<PerspexPropertyChangedEventArgs>> handler)
where TTarget : class
{
return observable.Subscribe(e => SubscribeAdapter(e, handler));
}
/// <summary>
/// Observer method for <see cref="AddClassHandler{TTarget}(IObservable{PerspexPropertyChangedEventArgs},
/// Func{TTarget, Action{PerspexPropertyChangedEventArgs}})"/>.
/// </summary>
/// <typeparam name="TTarget">The sender type to accept.</typeparam>
/// <param name="e">The event args.</param>
/// <param name="handler">Given a TTarget, returns the handler.</param>
private static void SubscribeAdapter<TTarget>(
PerspexPropertyChangedEventArgs e,
Func<TTarget, Action<PerspexPropertyChangedEventArgs>> handler)
where TTarget : class
{
var target = e.Sender as TTarget;
if (target != null)
{
handler(target)(e);
}
}
}
}

288
Perspex.Base/PerspexProperty.cs

@ -10,6 +10,7 @@ namespace Perspex
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Reflection;
using Perspex.Utilities;
/// <summary>
/// A perspex property.
@ -22,6 +23,7 @@ namespace Perspex
/// <summary>
/// Represents an unset property value.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields must be private", Justification = "It's readonly")]
public static readonly object UnsetValue = new Unset();
/// <summary>
@ -39,11 +41,6 @@ namespace Perspex
/// </summary>
private Subject<PerspexPropertyChangedEventArgs> changed = new Subject<PerspexPropertyChangedEventArgs>();
/// <summary>
/// The coerce function.
/// </summary>
private Func<PerspexObject, object, object> coerce;
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
@ -54,14 +51,16 @@ namespace Perspex
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
/// <param name="isAttached">Whether the property is an attached property.</param>
public PerspexProperty(
string name,
Type valueType,
Type ownerType,
object defaultValue,
bool inherits,
BindingMode defaultBindingMode,
Func<PerspexObject, object, object> coerce)
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.Default,
Func<PerspexObject, object, object> coerce = null,
bool isAttached = false)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(valueType != null);
@ -74,38 +73,64 @@ namespace Perspex
this.Inherits = inherits;
this.DefaultBindingMode = defaultBindingMode;
this.Coerce = coerce;
this.IsAttached = isAttached;
}
/// <summary>
/// Gets the name of the property.
/// </summary>
public string Name { get; private set; }
/// <value>
/// The name of the property.
/// </value>
public string Name { get; }
/// <summary>
/// Gets the type of the property's value.
/// </summary>
public Type PropertyType { get; private set; }
/// <value>
/// The type of the property's value.
/// </value>
public Type PropertyType { get; }
/// <summary>
/// Gets the type of the class that registers the property.
/// </summary>
public Type OwnerType { get; private set; }
/// <value>
/// The type of the class that registers the property.
/// </value>
public Type OwnerType { get; }
/// <summary>
/// Gets a value indicating whether the property inherits its value.
/// </summary>
public bool Inherits { get; private set; }
/// <value>
/// A value indicating whether the property inherits its value.
/// </value>
public bool Inherits { get; }
/// <summary>
/// Gets the default binding mode for the property.
/// </summary>
/// <returns></returns>
public BindingMode DefaultBindingMode { get; private set; }
/// <value>
/// The default binding mode for the property.
/// </value>
public BindingMode DefaultBindingMode { get; }
/// <summary>
/// Gets the property's coerce function.
/// </summary>
public Func<PerspexObject, object, object> Coerce { get; private set; }
/// <value>
/// The property's coerce function.
/// </value>
public Func<PerspexObject, object, object> Coerce { get; }
/// <summary>
/// Gets a value indicating whether this is an attached property.
/// </summary>
/// <value>
/// A value indicating whether this is an attached property.
/// </value>
public bool IsAttached { get; }
/// <summary>
/// Gets an observable that is fired when this property is initialized on a
@ -113,24 +138,62 @@ namespace Perspex
/// </summary>
/// <remarks>
/// This observable is fired each time a new <see cref="PerspexObject"/> is constructed
/// for all properties registered on the object's type. The default value of the property
/// for the object is passed in the args' NewValue (OldValue will always be
/// for all properties registered on the object's type. The default value of the property
/// for the object is passed in the args' NewValue (OldValue will always be
/// <see cref="UnsetValue"/>.
/// </remarks>
/// <value>
/// An observable that is fired when this property is initialized on a new
/// <see cref="PerspexObject"/> instance.
/// </value>
public IObservable<PerspexPropertyChangedEventArgs> Initialized
{
get { return this.initialized; }
}
/// <summary>
/// Gets an observable that is fired when this property changes on any
/// Gets an observable that is fired when this property changes on any
/// <see cref="PerspexObject"/> instance.
/// </summary>
/// <value>
/// An observable that is fired when this property changes on any
/// <see cref="PerspexObject"/> instance.
/// </value>
public IObservable<PerspexPropertyChangedEventArgs> Changed
{
get { return this.changed; }
}
/// <summary>
/// Provides access to a property's binding via the <see cref="PerspexObject"/>
/// indexer.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>A <see cref="Binding"/> describing the binding.</returns>
public static Binding operator !(PerspexProperty property)
{
return new Binding
{
Priority = BindingPriority.LocalValue,
Property = property,
};
}
/// <summary>
/// Provides access to a property's template binding via the <see cref="PerspexObject"/>
/// indexer.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>A <see cref="Binding"/> describing the binding.</returns>
public static Binding operator ~(PerspexProperty property)
{
return new Binding
{
Priority = BindingPriority.TemplatedParent,
Property = property,
};
}
/// <summary>
/// Registers a <see cref="PerspexProperty"/>.
/// </summary>
@ -158,7 +221,8 @@ namespace Perspex
defaultValue,
inherits,
defaultBindingMode,
coerce);
coerce,
false);
PerspexObject.Register(typeof(TOwner), result);
@ -183,17 +247,17 @@ namespace Perspex
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> coerce = null)
where TOwner : PerspexObject
{
Contract.Requires<NullReferenceException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
typeof(TOwner) + "." + name,
name,
typeof(TOwner),
defaultValue,
inherits,
defaultBindingMode,
coerce);
coerce,
true);
PerspexObject.Register(typeof(THost), result);
@ -201,37 +265,43 @@ namespace Perspex
}
/// <summary>
/// Provides access to a property's binding via the <see cref="PerspexObject"/>
/// indexer.
/// Registers an attached <see cref="PerspexProperty"/>.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>A <see cref="Binding"/> describing the binding.</returns>
public static Binding operator !(PerspexProperty property)
/// <typeparam name="THost">The type of the class that the property is to be registered on.</typeparam>
/// <typeparam name="TValue">The type of the property's value.</typeparam>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that is registering the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
/// <returns>A <see cref="PerspexProperty{TValue}"/></returns>
public static PerspexProperty<TValue> RegisterAttached<THost, TValue>(
string name,
Type ownerType,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> coerce = null)
{
return new Binding
{
Priority = BindingPriority.LocalValue,
Property = property,
};
}
Contract.Requires<NullReferenceException>(name != null);
/// <summary>
/// Provides access to a property's template binding via the <see cref="PerspexObject"/>
/// indexer.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>A <see cref="Binding"/> describing the binding.</returns>
public static Binding operator ~(PerspexProperty property)
{
return new Binding
{
Priority = BindingPriority.TemplatedParent,
Property = property,
};
PerspexProperty<TValue> result = new PerspexProperty<TValue>(
name,
ownerType,
defaultValue,
inherits,
defaultBindingMode,
coerce,
true);
PerspexObject.Register(typeof(THost), result);
return result;
}
/// <summary>
/// Returns a binding accessor that can be passed to <see cref="PerspexObject"/>'s []
/// Returns a binding accessor that can be passed to <see cref="PerspexObject"/>'s []
/// operator to initiate a binding.
/// </summary>
/// <returns>A <see cref="Binding"/>.</returns>
@ -270,23 +340,28 @@ namespace Perspex
return this.defaultValues[this.OwnerType];
}
/// <summary>
/// Checks whether the <paramref name="value"/> is valid for the property.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>True if the value is valid, otherwise false.</returns>
public bool IsValidValue(object value)
{
if (value == UnsetValue)
{
return true;
}
else if (value == null)
{
return !this.PropertyType.GetTypeInfo().IsValueType ||
Nullable.GetUnderlyingType(this.PropertyType) != null;
}
return TypeUtilities.TryCast(this.PropertyType, value, out value);
}
return this.PropertyType.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());
/// <summary>
/// Overrides the default value for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultValue">The default value.</param>
public void OverrideDefaultValue<T>(object defaultValue)
{
this.OverrideDefaultValue(typeof(T), defaultValue);
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// Overrides the default value for the property on the specified type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="defaultValue">The default value.</param>
@ -294,7 +369,15 @@ namespace Perspex
{
Contract.Requires<NullReferenceException>(type != null);
// TODO: Ensure correct type.
if (!TypeUtilities.TryCast(this.PropertyType, defaultValue, out defaultValue))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.Name,
defaultValue,
defaultValue.GetType().FullName));
}
if (this.defaultValues.ContainsKey(type))
{
throw new InvalidOperationException("Default value is already set for this property.");
@ -303,93 +386,46 @@ namespace Perspex
this.defaultValues.Add(type, defaultValue);
}
/// <summary>
/// Gets the string representation of the property.
/// </summary>
/// <returns>The property's string representation.</returns>
public override string ToString()
{
return this.Name;
}
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>
/// <param name="e">The observable arguments.</param>
internal void NotifyInitialized(PerspexPropertyChangedEventArgs e)
{
this.initialized.OnNext(e);
}
/// <summary>
/// Notifies the <see cref="Changed"/> observable.
/// </summary>
/// <param name="e">The observable arguments.</param>
internal void NotifyChanged(PerspexPropertyChangedEventArgs e)
{
this.changed.OnNext(e);
}
/// <summary>
/// Class representing the <see cref="UnsetValue"/>.
/// </summary>
private class Unset
{
/// <summary>
/// Returns the string representation of the <see cref="UnsetValue"/>.
/// </summary>
/// <returns>The string "(unset)".</returns>
public override string ToString()
{
return "{Unset}";
return "(unset)";
}
}
}
/// <summary>
/// A typed perspex property.
/// </summary>
public class PerspexProperty<TValue> : PerspexProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
public PerspexProperty(
string name,
Type ownerType,
TValue defaultValue,
bool inherits,
BindingMode defaultBindingMode,
Func<PerspexObject, TValue, TValue> coerce)
: base(
name,
typeof(TValue),
ownerType,
defaultValue,
inherits,
defaultBindingMode,
Convert(coerce))
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);
}
/// <summary>
/// Registers the property on another type.
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public PerspexProperty<TValue> AddOwner<TOwner>()
{
PerspexObject.Register(typeof(TOwner), this);
return this;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>The default value.</returns>
public TValue GetDefaultValue<T>()
{
return (TValue)this.GetDefaultValue(typeof(T));
}
/// <summary>
/// Converts from a typed coercion function to an untyped.
/// </summary>
/// <param name="f">The typed coercion function.</param>
/// <returns>Te untyped coercion function.</returns>
private static Func<PerspexObject, object, object> Convert(Func<PerspexObject, TValue, TValue> f)
{
return f != null ? (o, v) => f(o, (TValue)v) : (Func<PerspexObject, object, object>)null;
}
}
}

22
Perspex.Base/PerspexPropertyChangedEventArgs.cs

@ -11,6 +11,14 @@ namespace Perspex
/// </summary>
public class PerspexPropertyChangedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PerspexPropertyChangedEventArgs"/> class.
/// </summary>
/// <param name="sender">The object that the property changed on.</param>
/// <param name="property">The property that changed.</param>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
public PerspexPropertyChangedEventArgs(
PerspexObject sender,
PerspexProperty property,
@ -28,27 +36,39 @@ namespace Perspex
/// <summary>
/// Gets the <see cref="PerspexObject"/> that the property changed on.
/// </summary>
/// <returns></returns>
/// <value>The sender object.</value>
public PerspexObject Sender { get; private set; }
/// <summary>
/// Gets the property that changed.
/// </summary>
/// <value>
/// The property that changed.
/// </value>
public PerspexProperty Property { get; private set; }
/// <summary>
/// Gets the old value of the property.
/// </summary>
/// <value>
/// The old value of the property.
/// </value>
public object OldValue { get; private set; }
/// <summary>
/// Gets the new value of the property.
/// </summary>
/// <value>
/// The new value of the property.
/// </value>
public object NewValue { get; private set; }
/// <summary>
/// Gets the priority of the binding that produced the value.
/// </summary>
/// <value>
/// The priority of the binding that produced the value.
/// </value>
public BindingPriority Priority { get; private set; }
}
}

80
Perspex.Base/PerspexProperty`1.cs

@ -0,0 +1,80 @@
// -----------------------------------------------------------------------
// <copyright file="PerspexProperty`1.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
{
using System;
/// <summary>
/// A typed perspex property.
/// </summary>
/// <typeparam name="TValue">The value type of the property.</typeparam>
public class PerspexProperty<TValue> : PerspexProperty
{
/// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty{TValue}"/> class.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="ownerType">The type of the class that registers the property.</param>
/// <param name="defaultValue">The default value of the property.</param>
/// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param>
/// <param name="isAttached">Whether the property is an attached property.</param>
public PerspexProperty(
string name,
Type ownerType,
TValue defaultValue = default(TValue),
bool inherits = false,
BindingMode defaultBindingMode = BindingMode.Default,
Func<PerspexObject, TValue, TValue> coerce = null,
bool isAttached = false)
: base(
name,
typeof(TValue),
ownerType,
defaultValue,
inherits,
defaultBindingMode,
Convert(coerce),
isAttached)
{
Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(ownerType != null);
}
/// <summary>
/// Registers the property on another type.
/// </summary>
/// <typeparam name="TOwner">The type of the additional owner.</typeparam>
/// <returns>The property.</returns>
public PerspexProperty<TValue> AddOwner<TOwner>()
{
PerspexObject.Register(typeof(TOwner), this);
return this;
}
/// <summary>
/// Gets the default value for the property on the specified type.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>The default value.</returns>
public TValue GetDefaultValue<T>()
{
return (TValue)this.GetDefaultValue(typeof(T));
}
/// <summary>
/// Converts from a typed coercion function to an untyped.
/// </summary>
/// <param name="f">The typed coercion function.</param>
/// <returns>Te untyped coercion function.</returns>
private static Func<PerspexObject, object, object> Convert(Func<PerspexObject, TValue, TValue> f)
{
return f != null ? (o, v) => f(o, (TValue)v) : (Func<PerspexObject, object, object>)null;
}
}
}

7
Perspex.Base/Platform/IPlatformThreadingInterface.cs

@ -7,15 +7,16 @@
namespace Perspex.Platform
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Perspex.Threading;
/// <summary>
/// Provides platform-specific services relating to threading.
/// </summary>
public interface IPlatformThreadingInterface
{
/// <summary>
/// Checks whether there are messages waiting to be processed.
/// </summary>
/// <returns>True if there are messages waiting, otherwise false.</returns>
bool HasMessages();
/// <summary>

111
Perspex.Base/PriorityBindingEntry.cs

@ -0,0 +1,111 @@
// -----------------------------------------------------------------------
// <copyright file="PriorityBindingEntry.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
{
using System;
/// <summary>
/// A registered binding in a <see cref="PriorityValue"/>.
/// </summary>
internal class PriorityBindingEntry : IDisposable
{
/// <summary>
/// The binding subscription.
/// </summary>
private IDisposable subscription;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityBindingEntry"/> class.
/// </summary>
/// <param name="index">
/// The binding index. Later bindings should have higher indexes.
/// </param>
public PriorityBindingEntry(int index)
{
this.Index = index;
}
/// <summary>
/// Gets the observable associated with the entry.
/// </summary>
public IObservable<object> Observable { get; private set; }
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
private set;
}
/// <summary>
/// Gets the binding entry index. Later bindings will have higher indexes.
/// </summary>
public int Index
{
get;
}
/// <summary>
/// The current value of the binding.
/// </summary>
public object Value
{
get;
private set;
}
/// <summary>
/// Starts listening to the binding.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param>
public void Start(
IObservable<object> binding,
Action<PriorityBindingEntry> changed,
Action<PriorityBindingEntry> completed)
{
Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(completed != null);
if (this.subscription != null)
{
throw new Exception("PriorityValue.Entry.Start() called more than once.");
}
this.Observable = binding;
this.Value = PerspexProperty.UnsetValue;
if (binding is IDescription)
{
this.Description = ((IDescription)binding).Description;
}
this.subscription = binding.Subscribe(
value =>
{
this.Value = value;
changed(this);
},
() => completed(this));
}
/// <summary>
/// Ends the binding subscription.
/// </summary>
public void Dispose()
{
if (this.subscription != null)
{
this.subscription.Dispose();
}
}
}
}

210
Perspex.Base/PriorityLevel.cs

@ -0,0 +1,210 @@
// -----------------------------------------------------------------------
// <copyright file="PriorityLevel.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
{
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
/// <summary>
/// Determines how the current binding is selected for a <see cref="PriorityLevel"/>.
/// </summary>
internal enum LevelPrecedenceMode
{
/// <summary>
/// The latest fired binding is used as the current value.
/// </summary>
Latest,
/// <summary>
/// The latest added binding is used as the current value.
/// </summary>
Newest,
}
/// <summary>
/// Stores bindings for a priority level in a <see cref="PriorityValue"/>.
/// </summary>
/// <remarks>
/// <para>
/// Each priority level in a <see cref="PriorityValue"/> has a current <see cref="Value"/>,
/// a list of <see cref="Bindings"/> and a <see cref="DirectValue"/>. When there are no
/// bindings present, or all bindings return <see cref="PerspexProperty.UnsetValue"/> then
/// <code>Value</code> will equal <code>DirectValue</code>.
/// </para>
/// <para>
/// When there are bindings present, then the latest added binding that doesn't return
/// <code>UnsetValue</code> will take precedence. The active binding is returned by the
/// <see cref="ActiveBindingIndex"/> property (which refers to the active binding's
/// <see cref="PriorityBindingEntry.Index"/> property rather than the index in
/// <code>Bindings</code>).
/// </para>
/// <para>
/// If <code>DirectValue</code> is set while a binding is active, then it will replace the
/// current value until the active binding fires again.
/// </para>
/// </remarks>
internal class PriorityLevel
{
/// <summary>
/// Method called when current value changes.
/// </summary>
private Action<PriorityLevel> changed;
/// <summary>
/// The current direct value.
/// </summary>
private object directValue;
/// <summary>
/// The index of the next <see cref="PriorityBindingEntry"/>.
/// </summary>
private int nextIndex;
private LevelPrecedenceMode mode;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityLevel"/> class.
/// </summary>
/// <param name="priority">The priority.</param>
/// <param name="mode">The precedence mode.</param>
/// <param name="changed">A method to be called when the current value changes.</param>
public PriorityLevel(
int priority,
LevelPrecedenceMode mode,
Action<PriorityLevel> changed)
{
Contract.Requires<ArgumentNullException>(changed != null);
this.mode = mode;
this.changed = changed;
this.Priority = priority;
this.Value = this.directValue = PerspexProperty.UnsetValue;
this.ActiveBindingIndex = -1;
this.Bindings = new LinkedList<PriorityBindingEntry>();
}
/// <summary>
/// Gets the priority of this level.
/// </summary>
public int Priority { get; }
/// <summary>
/// Gets or sets the direct value for this priority level.
/// </summary>
public object DirectValue
{
get
{
return this.directValue;
}
set
{
this.Value = this.directValue = value;
this.changed(this);
}
}
/// <summary>
/// Gets the current binding for the priority level.
/// </summary>
public object Value { get; private set; }
/// <summary>
/// Gets the <see cref="PriorityBindingEntry.Index"/> value of the active binding, or -1
/// if no binding is active.
/// </summary>
public int ActiveBindingIndex { get; private set; }
/// <summary>
/// Gets the bindings for the priority level.
/// </summary>
public LinkedList<PriorityBindingEntry> Bindings { get; }
/// <summary>
/// Adds a binding.
/// </summary>
/// <param name="binding">The binding to add.</param>
/// <returns>A disposable used to remove the binding.</returns>
public IDisposable Add(IObservable<object> binding)
{
Contract.Requires<ArgumentNullException>(binding != null);
var entry = new PriorityBindingEntry(this.nextIndex++);
var node = this.Bindings.AddFirst(entry);
entry.Start(binding, this.Changed, this.Completed);
return Disposable.Create(() =>
{
this.Bindings.Remove(node);
if (entry.Index >= this.ActiveBindingIndex)
{
this.ActivateFirstBinding();
}
});
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> changes value.
/// </summary>
/// <param name="entry">The entry that changed.</param>
private void Changed(PriorityBindingEntry entry)
{
if (this.mode == LevelPrecedenceMode.Latest || entry.Index >= this.ActiveBindingIndex)
{
if (entry.Value != PerspexProperty.UnsetValue)
{
this.Value = entry.Value;
this.ActiveBindingIndex = entry.Index;
this.changed(this);
}
else
{
this.ActivateFirstBinding();
}
}
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> completes.
/// </summary>
/// <param name="entry">The entry that completed.</param>
private void Completed(PriorityBindingEntry entry)
{
this.Bindings.Remove(entry);
if (entry.Index >= this.ActiveBindingIndex)
{
this.ActivateFirstBinding();
}
}
/// <summary>
/// Activates the first binding that has a value.
/// </summary>
private void ActivateFirstBinding()
{
foreach (var binding in this.Bindings)
{
if (binding.Value != PerspexProperty.UnsetValue)
{
this.Value = binding.Value;
this.ActiveBindingIndex = binding.Index;
this.changed(this);
return;
}
}
this.Value = this.DirectValue;
this.ActiveBindingIndex = -1;
this.changed(this);
}
}
}

379
Perspex.Base/PriorityValue.cs

@ -8,23 +8,25 @@ namespace Perspex
{
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using System.Text;
using Perspex.Utilities;
/// <summary>
/// Maintains a list of prioritised bindings together with a current value.
/// </summary>
/// <remarks>
/// Bindings, in the form of <see cref="IObservable<object>"/>s are added to the object using
/// Bindings, in the form of <see cref="IObservable{object}"/>s are added to the object using
/// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
/// represent higher priorites. The current <see cref="Value"/> is selected from the highest
/// priority binding that doesn't return <see cref="PerspexProperty.UnsetValue"/>. Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes to a distinct new value, the
/// <see cref="Changed"/> observable is fired with the old and new values.
/// has a higher priority. Each time the value changes, the <see cref="Changed"/> observable is
/// fired with the old and new values.
/// </remarks>
public class PriorityValue
internal class PriorityValue
{
/// <summary>
/// The name of the property.
@ -37,9 +39,9 @@ namespace Perspex
private Type valueType;
/// <summary>
/// The currently registered binding entries.
/// The currently registered bindings organised by priority.
/// </summary>
private LinkedList<BindingEntry> bindings = new LinkedList<BindingEntry>();
private Dictionary<int, PriorityLevel> levels = new Dictionary<int, PriorityLevel>();
/// <summary>
/// The changed observable.
@ -51,6 +53,9 @@ namespace Perspex
/// </summary>
private object value;
/// <summary>
/// The function used to coerce the value, if any.
/// </summary>
private Func<object, object> coerce;
/// <summary>
@ -69,8 +74,11 @@ namespace Perspex
}
/// <summary>
/// Fired whenever the current <see cref="Value"/> changes to a new distinct value.
/// Fired whenever the current <see cref="Value"/> changes.
/// </summary>
/// <remarks>
/// The old and new values may be the same, this class does not check for distinct values.
/// </remarks>
public IObservable<Tuple<object, object>> Changed
{
get { return this.changed; }
@ -93,39 +101,6 @@ namespace Perspex
private set;
}
/// <summary>
/// Checks whether a value is valid for a type.
/// </summary>
/// <param name="value"></param>
/// <param name="propertyType"></param>
/// <returns></returns>
public static bool IsValidValue(object value, Type propertyType)
{
TypeInfo type = propertyType.GetTypeInfo();
if (value == PerspexProperty.UnsetValue)
{
return true;
}
else if (value == null)
{
if (type.IsValueType &&
(!type.IsGenericType || !(type.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
return false;
}
}
else
{
if (!type.IsAssignableFrom(value.GetType().GetTypeInfo()))
{
return false;
}
}
return true;
}
/// <summary>
/// Adds a new binding.
/// </summary>
@ -136,110 +111,72 @@ namespace Perspex
/// </returns>
public IDisposable Add(IObservable<object> binding, int priority)
{
BindingEntry entry = new BindingEntry();
LinkedListNode<BindingEntry> insert = this.bindings.First;
while (insert != null && insert.Value.Priority < priority)
{
insert = insert.Next;
}
if (insert == null)
{
this.bindings.AddLast(entry);
}
else
{
this.bindings.AddBefore(insert, entry);
}
entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted);
return Disposable.Create(() =>
{
this.Remove(entry);
});
return this.GetLevel(priority).Add(binding);
}
/// <summary>
/// Adds a new binding, replacing all those of the same priority.
/// Sets the direct value for a specified priority.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <returns>
/// A disposable that will remove the binding.
/// </returns>
public IDisposable Replace(IObservable<object> binding, int priority)
/// <param name="value">The value.</param>
/// <param name="priority">The priority</param>
public void SetDirectValue(object value, int priority)
{
BindingEntry entry = new BindingEntry();
LinkedListNode<BindingEntry> insert = this.bindings.First;
while (insert != null && insert.Value.Priority < priority)
{
insert = insert.Next;
}
while (insert != null && insert.Value.Priority == priority)
{
LinkedListNode<BindingEntry> next = insert.Next;
insert.Value.Dispose();
this.bindings.Remove(insert);
insert = next;
}
this.GetLevel(priority).DirectValue = value;
}
if (insert == null)
{
this.bindings.AddLast(entry);
}
else
/// <summary>
/// Gets the currently active bindings on this object.
/// </summary>
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<PriorityBindingEntry> GetBindings()
{
foreach (var level in this.levels)
{
this.bindings.AddBefore(insert, entry);
foreach (var binding in level.Value.Bindings)
{
yield return binding;
}
}
entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted);
return Disposable.Create(() =>
{
this.Remove(entry);
});
}
/// <summary>
/// Removes all bindings with the specified priority.
/// Returns diagnostic string that can help the user debug the bindings in effect on
/// this object.
/// </summary>
/// <param name="priority">The priority.</param>
public void Clear(int priority)
/// <returns>A diagnostic string.</returns>
public string GetDiagnostic()
{
LinkedListNode<BindingEntry> item = this.bindings.First;
bool removed = false;
var b = new StringBuilder();
var first = true;
while (item != null && item.Value.Priority <= priority)
foreach (var level in this.levels)
{
LinkedListNode<BindingEntry> next = item.Next;
if (item.Value.Priority == priority)
if (!first)
{
item.Value.Dispose();
this.bindings.Remove(item);
removed = true;
b.AppendLine();
}
item = next;
}
b.Append(this.ValuePriority == level.Key ? "*" : string.Empty);
b.Append("Priority ");
b.Append(level.Key);
b.Append(": ");
b.AppendLine(level.Value.Value?.ToString() ?? "(null)");
b.AppendLine("--------");
b.Append("Direct: ");
b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)");
if (removed && priority <= this.ValuePriority)
{
this.UpdateValue();
foreach (var binding in level.Value.Bindings)
{
b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty);
b.Append(binding.Description ?? binding.Observable.GetType().Name);
b.Append(": ");
b.AppendLine(binding.Value?.ToString() ?? "(null)");
}
first = false;
}
}
/// <summary>
/// Gets the currently active bindings on this object.
/// </summary>
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<BindingEntry> GetBindings()
{
return this.bindings;
return b.ToString();
}
/// <summary>
@ -249,185 +186,87 @@ namespace Perspex
{
if (this.coerce != null)
{
this.SetValue(this.Value, this.ValuePriority);
PriorityLevel level;
if (this.levels.TryGetValue(this.ValuePriority, out level))
{
this.UpdateValue(level.Value, level.Priority);
}
}
}
/// <summary>
/// Throws an exception if <paramref name="value"/> is invalid.
/// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
/// doesn't already exist.
/// </summary>
/// <param name="value">The value.</param>
private void VerifyValidValue(object value)
/// <param name="priority">The priority.</param>
/// <returns>The priority level.</returns>
private PriorityLevel GetLevel(int priority)
{
if (!IsValidValue(value, this.valueType))
PriorityLevel result;
if (!this.levels.TryGetValue(priority, out result))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.name,
value,
value.GetType().FullName));
var mode = (LevelPrecedenceMode)(priority % 2);
result = new PriorityLevel(priority, mode, this.ValueChanged);
this.levels.Add(priority, result);
}
return result;
}
/// <summary>
/// Called when a binding's value changes.
/// Updates the current <see cref="Value"/> and notifies all subscibers.
/// </summary>
/// <param name="changed">The changed entry.</param>
private void EntryChanged(BindingEntry changed)
/// <param name="value">The value to set.</param>
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
if (changed.Priority <= this.ValuePriority)
if (!TypeUtilities.TryCast(this.valueType, value, out value))
{
this.UpdateValue();
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.name,
value,
value?.GetType().FullName ?? "(null)"));
}
}
/// <summary>
/// Called when a binding completes.
/// </summary>
/// <param name="changed">The completed entry.</param>
private void EntryCompleted(BindingEntry entry)
{
this.Remove(entry);
}
/// <summary>
/// Sets the current value and notifies all observers.
/// </summary>
/// <param name="value">The new value.</param>
/// <param name="priority">The priority of the binding which produced the value.</param>
private void SetValue(object value, int priority)
{
VerifyValidValue(value);
var old = this.value;
if (this.coerce != null)
{
value = this.coerce(value);
VerifyValidValue(value);
}
object old = this.value;
this.ValuePriority = priority;
if (!EqualityComparer<object>.Default.Equals(old, value))
{
this.value = value;
this.changed.OnNext(Tuple.Create(old, value));
}
this.value = value;
this.changed.OnNext(Tuple.Create(old, this.value));
}
/// <summary>
/// Removes the specified binding entry and updates the current value.
/// Called when the value for a priority level changes.
/// </summary>
/// <param name="entry">The binding entry to remove.</param>
private void Remove(BindingEntry entry)
/// <param name="level">The priority level of the changed entry.</param>
private void ValueChanged(PriorityLevel level)
{
entry.Dispose();
this.bindings.Remove(entry);
this.UpdateValue();
}
/// <summary>
/// Updates the current value.
/// </summary>
private void UpdateValue()
{
foreach (BindingEntry entry in this.bindings)
{
if (entry.Value != PerspexProperty.UnsetValue)
{
this.SetValue(entry.Value, entry.Priority);
return;
}
}
this.SetValue(PerspexProperty.UnsetValue, int.MaxValue);
}
/// <summary>
/// A registered binding.
/// </summary>
public class BindingEntry : IDisposable
{
/// <summary>
/// The binding subscription.
/// </summary>
private IDisposable subscription;
/// <summary>
/// Gets a description of the binding.
/// </summary>
public string Description
{
get;
private set;
}
/// <summary>
/// The priority of the binding.
/// </summary>
public int Priority
{
get;
private set;
}
/// <summary>
/// The current value of the binding.
/// </summary>
public object Value
if (level.Priority <= this.ValuePriority)
{
get;
private set;
}
/// <summary>
/// Starts listening to the specified binding.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param>
public void Start(
IObservable<object> binding,
int priority,
Action<BindingEntry> changed,
Action<BindingEntry> completed)
{
Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
Contract.Requires<ArgumentNullException>(completed != null);
if (this.subscription != null)
if (level.Value != PerspexProperty.UnsetValue)
{
throw new Exception("PriorityValue.Entry.Start() called more than once.");
this.UpdateValue(level.Value, level.Priority);
}
this.Priority = priority;
this.Value = PerspexProperty.UnsetValue;
if (binding is IDescription)
else
{
this.Description = ((IDescription)binding).Description;
}
this.subscription = binding.Subscribe(
value =>
foreach (var i in this.levels.Values.OrderBy(x => x.Priority))
{
this.Value = value;
changed(this);
},
() => completed(this));
}
/// <summary>
/// Ends the binding subscription.
/// </summary>
public void Dispose()
{
if (this.subscription != null)
{
this.subscription.Dispose();
if (i.Value != PerspexProperty.UnsetValue)
{
this.UpdateValue(i.Value, i.Priority);
return;
}
}
this.UpdateValue(PerspexProperty.UnsetValue, int.MaxValue);
}
}
}

3
Perspex.Base/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
@ -32,3 +33,5 @@ using System.Resources;
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly:InternalsVisibleTo("Perspex.Base.UnitTests")]

35
Perspex.Base/Reactive/PerspexObservable.cs

@ -0,0 +1,35 @@
// -----------------------------------------------------------------------
// <copyright file="PerspexObservable.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Reactive
{
using System;
using System.Reactive;
using System.Reactive.Disposables;
public sealed class PerspexObservable<T> : ObservableBase<T>, IDescription
{
private readonly Func<IObserver<T>, IDisposable> subscribe;
public PerspexObservable(Func<IObserver<T>, IDisposable> subscribe, string description)
{
if (subscribe == null)
{
throw new ArgumentNullException("subscribe");
}
this.subscribe = subscribe;
this.Description = description;
}
public string Description { get; }
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
return this.subscribe(observer) ?? Disposable.Empty;
}
}
}

19
Perspex.Base/Threading/PerspexScheduler.cs

@ -12,20 +12,17 @@ namespace Perspex.Threading
public class PerspexScheduler : LocalScheduler
{
private static readonly PerspexScheduler instance = new PerspexScheduler();
public static PerspexScheduler Instance
{
get { return instance; }
}
public static readonly PerspexScheduler Instance = new PerspexScheduler();
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
{
return DispatcherTimer.Run(() =>
{
action(this, state);
return false;
}, dueTime);
return DispatcherTimer.Run(
() =>
{
action(this, state);
return false;
},
dueTime);
}
}
}

84
Perspex.Base/Utilities/TypeUtilities.cs

@ -0,0 +1,84 @@
// -----------------------------------------------------------------------
// <copyright file="TypeUtilities.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Utilities
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
/// <summary>
/// Provides utilities for working with types at runtime.
/// </summary>
internal static class TypeUtilities
{
private static readonly Dictionary<Type, List<Type>> Conversions = new Dictionary<Type, List<Type>>()
{
{ typeof(decimal), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
{ typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
{ typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } },
{ typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
{ typeof(ushort), new List<Type> { typeof(byte), typeof(char) } },
{ typeof(short), new List<Type> { typeof(byte) } }
};
/// <summary>
/// Try to cast a value to a type, using implicit conversions if possible.
/// </summary>
/// <param name="to">The type to cast to.</param>
/// <param name="value">The value to cast.</param>
/// <param name="result">If sucessful, contains the cast value.</param>
/// <returns>True if the cast was sucessful, otherwise false.</returns>
public static bool TryCast(Type to, object value, out object result)
{
Contract.Requires<NullReferenceException>(to != null);
if (value == null)
{
var t = to.GetTypeInfo();
result = null;
return !t.IsValueType || (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(Nullable<>)));
}
var from = value.GetType();
if (value == PerspexProperty.UnsetValue)
{
result = value;
return true;
}
else if (to.GetTypeInfo().IsAssignableFrom(from.GetTypeInfo()))
{
result = value;
return true;
}
else if (Conversions.ContainsKey(to) && Conversions[to].Contains(from))
{
result = Convert.ChangeType(value, to);
return true;
}
else
{
var cast = from.GetTypeInfo()
.GetDeclaredMethods("op_Implicit")
.FirstOrDefault(m => m.ReturnType == to);
if (cast != null)
{
result = cast.Invoke(null, new[] { value });
return true;
}
}
result = null;
return false;
}
}
}

15
Perspex.Base/packages.config

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win" />
<package id="Splat" version="1.6.1" targetFramework="portable-net45+win" />
<package id="StyleCop.MSBuild" version="4.7.49.0" targetFramework="portable-net45+win" developmentDependency="true" />
<package id="Rx-Core" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Serilog" version="1.5.5" targetFramework="portable45-net45+win8" userInstalled="true" />
<package id="Splat" version="1.6.2" targetFramework="portable45-net45+win8" userInstalled="true" />
<package id="StyleCop.Analyzers" version="1.0.0-alpha008" targetFramework="portable45-net45+win8" userInstalled="true" />
</packages>

10
Perspex.Controls/Border.cs

@ -51,7 +51,7 @@ namespace Perspex.Controls
Brush background = this.Background;
Brush borderBrush = this.BorderBrush;
double borderThickness = this.BorderThickness;
Rect rect = new Rect(this.Bounds.Size).Deflate(this.BorderThickness / 2);
Rect rect = new Rect(this.Bounds.Size).Deflate(this.BorderThickness);
if (background != null)
{
@ -85,10 +85,12 @@ namespace Perspex.Controls
if (content != null)
{
content.Measure(availableSize.Deflate(padding));
return content.DesiredSize.Value.Inflate(padding);
return content.DesiredSize.Inflate(padding);
}
else
{
return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
}
return new Size();
}
}
}

35
Perspex.Controls/Button.cs

@ -100,6 +100,39 @@ namespace Perspex.Controls
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
this.RaiseClickEvent();
e.Handled = true;
}
else if (e.Key == Key.Space)
{
if (this.ClickMode == ClickMode.Press)
{
this.RaiseClickEvent();
}
e.Handled = true;
}
base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (e.Key == Key.Space)
{
if (this.ClickMode == ClickMode.Release)
{
this.RaiseClickEvent();
}
e.Handled = true;
}
}
protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot)
{
base.OnDetachedFromVisualTree(oldRoot);
@ -203,7 +236,7 @@ namespace Perspex.Controls
private void ListenForDefault(IInputElement root)
{
root.AddHandler(InputElement.KeyDownEvent, this.RootKeyDown, RoutingStrategies.Tunnel);
root.AddHandler(InputElement.KeyDownEvent, this.RootKeyDown);
}
private void StopListeningForDefault(IInputElement root)

10
Perspex.Controls/ColumnDefinitions.cs

@ -6,9 +6,19 @@
namespace Perspex.Controls
{
using System.Linq;
using Perspex.Collections;
using Perspex.Controls.Parsers;
public class ColumnDefinitions : PerspexList<ColumnDefinition>
{
public ColumnDefinitions()
{
}
public ColumnDefinitions(string s)
{
this.AddRange(GridLengthsParser.Parse(s).Select(x => new ColumnDefinition(x)));
}
}
}

35
Perspex.Controls/ContentControl.cs

@ -28,7 +28,6 @@ namespace Perspex.Controls
public ContentControl()
{
this.GetObservableWithHistory(ContentProperty).Subscribe(this.SetContentParent);
}
public object Content
@ -37,6 +36,12 @@ namespace Perspex.Controls
set { this.SetValue(ContentProperty, value); }
}
public ContentPresenter Presenter
{
get;
private set;
}
public HorizontalAlignment HorizontalContentAlignment
{
get { return this.GetValue(HorizontalContentAlignmentProperty); }
@ -59,32 +64,8 @@ namespace Perspex.Controls
// We allow ContentControls without ContentPresenters in the template. This can be
// useful for e.g. a simple ToggleButton that displays an image. There's no need to
// have a ContentPresenter in the visual tree for that.
var presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter");
if (presenter != null)
{
this.logicalChildren.Source = ((ILogical)presenter).LogicalChildren;
}
else
{
this.logicalChildren.Source = null;
}
}
private void SetContentParent(Tuple<object, object> change)
{
var control1 = change.Item1 as Control;
var control2 = change.Item2 as Control;
if (control1 != null)
{
control1.Parent = null;
}
if (control2 != null)
{
control2.Parent = this;
}
this.Presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter");
this.logicalChildren.Source = ((ILogical)this.Presenter)?.LogicalChildren;
}
}
}

72
Perspex.Controls/Control.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls
using Perspex.Styling;
using Splat;
public class Control : InputElement, ILogical, IStyleable, IStyleHost
public class Control : InputElement, INamed, ILogical, IStyleable, IStyleHost
{
public static readonly PerspexProperty<object> DataContextProperty =
PerspexProperty.Register<Control, object>("DataContext", inherits: true);
@ -101,7 +101,7 @@ namespace Perspex.Controls
}
}
public string Id
public string Name
{
get
{
@ -192,6 +192,40 @@ namespace Perspex.Controls
this.RaiseEvent(ev);
}
protected static void PseudoClass(PerspexProperty<bool> property, string className)
{
PseudoClass(property, x => x, className);
}
protected static void PseudoClass<T>(
PerspexProperty<T> property,
Func<T, bool> selector,
string className)
{
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(selector != null);
Contract.Requires<ArgumentNullException>(className != null);
Contract.Requires<ArgumentNullException>(property != null);
if (string.IsNullOrWhiteSpace(className))
{
throw new ArgumentException("Cannot supply an empty className.");
}
Observable.Merge(property.Changed, property.Initialized)
.Subscribe(e =>
{
if (selector((T)e.NewValue))
{
((Control)e.Sender).Classes.Add(className);
}
else
{
((Control)e.Sender).Classes.Remove(className);
}
});
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
@ -233,40 +267,6 @@ namespace Perspex.Controls
}
}
protected static void PseudoClass(PerspexProperty<bool> property, string className)
{
PseudoClass(property, x => x, className);
}
protected static void PseudoClass<T>(
PerspexProperty<T> property,
Func<T, bool> selector,
string className)
{
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(selector != null);
Contract.Requires<ArgumentNullException>(className != null);
Contract.Requires<ArgumentNullException>(property != null);
if (string.IsNullOrWhiteSpace(className))
{
throw new ArgumentException("Cannot supply an empty className.");
}
Observable.Merge(property.Changed, property.Initialized)
.Subscribe(e =>
{
if (selector((T)e.NewValue))
{
((Control)e.Sender).Classes.Add(className);
}
else
{
((Control)e.Sender).Classes.Remove(className);
}
});
}
protected override void OnAttachedToVisualTree(IRenderRoot root)
{
base.OnAttachedToVisualTree(root);

4
Perspex.Controls/ControlExtensions.cs

@ -17,11 +17,11 @@ namespace Perspex.Controls
public static class ControlExtensions
{
public static T FindControl<T>(this Control control, string id) where T : Control
public static T FindControl<T>(this Control control, string name) where T : Control
{
return control.GetLogicalDescendents()
.OfType<T>()
.FirstOrDefault(x => x.Id == id);
.FirstOrDefault(x => x.Name == name);
}
}
}

6
Perspex.Controls/DataTemplate.cs

@ -12,7 +12,7 @@ namespace Perspex.Controls
public class DataTemplate : IDataTemplate
{
public static readonly DataTemplate Default =
new DataTemplate(typeof(object), o => new TextBlock { Text = o.ToString() });
new DataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
public DataTemplate(Func<object, Control> build)
: this(o => true, build)
@ -39,7 +39,9 @@ namespace Perspex.Controls
public static bool IsInstance(object o, Type t)
{
return t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo());
return (o != null) ?
t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()) :
false;
}
bool IDataTemplate.Match(object data)

65
Perspex.Controls/Deck.cs

@ -0,0 +1,65 @@
// -----------------------------------------------------------------------
// <copyright file="Deck.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
using System.Collections;
using Perspex.Animation;
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Controls.Utils;
using Perspex.Input;
/// <summary>
/// A selecting items control that displays a single item that fills the control.
/// </summary>
public class Deck : SelectingItemsControl
{
public static readonly PerspexProperty<IPageTransition> TransitionProperty =
PerspexProperty.Register<Deck, IPageTransition>("Transition");
private static readonly ItemsPanelTemplate PanelTemplate =
new ItemsPanelTemplate(() => new Panel());
static Deck()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Deck), PanelTemplate);
}
public IPageTransition Transition
{
get { return this.GetValue(TransitionProperty); }
set { this.SetValue(TransitionProperty, value); }
}
protected override void ItemsChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.ItemsChanged(oldValue, newValue);
var items = this.Items;
if (items != null && items.Count() > 0)
{
this.SelectedIndex = 0;
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
// Ignore key presses.
}
protected override void OnPointerPressed(PointerPressEventArgs e)
{
// Ignore pointer presses.
}
protected override void OnTemplateApplied()
{
base.OnTemplateApplied();
}
}
}

8
Perspex.Controls/Decorator.cs

@ -80,10 +80,12 @@ namespace Perspex.Controls
if (content != null)
{
content.Measure(availableSize.Deflate(padding));
return content.DesiredSize.Value.Inflate(padding);
return content.DesiredSize.Inflate(padding);
}
else
{
return new Size(padding.Left + padding.Right, padding.Bottom + padding.Top);
}
return new Size();
}
}
}

20
Perspex.Controls/Generators/ItemContainerGenerator.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls.Generators
private ItemContainerGeneratorState state;
public ItemContainerGenerator(ItemsControl owner)
public ItemContainerGenerator(Control owner)
{
this.Owner = owner;
}
@ -48,7 +48,7 @@ namespace Perspex.Controls.Generators
}
}
protected ItemsControl Owner
protected Control Owner
{
get;
private set;
@ -84,10 +84,18 @@ namespace Perspex.Controls.Generators
foreach (object item in items)
{
Control container = this.CreateContainerOverride(item);
container.DataContext = item;
container.TemplatedParent = null;
this.AddInternal(item, container);
result.Add(container);
if (container != null)
{
if (container.DataContext == null)
{
container.DataContext = item;
}
container.TemplatedParent = null;
this.AddInternal(item, container);
result.Add(container);
}
}
}
finally

14
Perspex.Controls/GlobalSuppressions.cs

@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// <copyright file="GlobalSuppressions.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.MaintainabilityRules",
"SA1401:Fields must be private",
Justification = "PerspexProperty fields should not be private.")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1609:Property documentation must have value",
Justification = "This rule is fscking pointless")]

64
Perspex.Controls/Grid.cs

@ -359,7 +359,7 @@ namespace Perspex.Controls
}
child.Measure(new Size(childSizeX, childSizeY));
Size desired = child.DesiredSize.Value;
Size desired = child.DesiredSize;
// Elements distribute their height based on two rules:
// 1) Elements with rowspan/colspan == 1 distribute their height first
@ -420,8 +420,8 @@ namespace Perspex.Controls
{
int colCount = this.ColumnDefinitions.Count;
int rowCount = this.RowDefinitions.Count;
int colMatrixDim = this.colMatrix.GetUpperBound(0) + 1;
int rowMatrixDim = this.rowMatrix.GetUpperBound(0) + 1;
int colMatrixDim = this.colMatrix.GetLength(0);
int rowMatrixDim = this.rowMatrix.GetLength(0);
this.RestoreMeasureResults();
@ -517,8 +517,8 @@ namespace Perspex.Controls
private void CreateMatrices(int rowCount, int colCount)
{
if (this.rowMatrix == null || this.colMatrix == null ||
this.rowMatrix.GetUpperBound(0) != rowCount - 1 ||
this.colMatrix.GetUpperBound(0) != colCount - 1)
this.rowMatrix.GetLength(0) != rowCount ||
this.colMatrix.GetLength(0) != colCount)
{
this.rowMatrix = new Segment[rowCount, rowCount];
this.colMatrix = new Segment[colCount, colCount];
@ -527,10 +527,11 @@ namespace Perspex.Controls
private void ExpandStarCols(Size availableSize)
{
int matrixCount = this.colMatrix.GetLength(0);
int columnsCount = this.ColumnDefinitions.Count;
double width = availableSize.Width;
for (int i = 0; i < this.colMatrix.GetUpperBound(0) + 1; i++)
for (int i = 0; i < matrixCount; i++)
{
if (this.colMatrix[i, i].Type == GridUnitType.Star)
{
@ -542,12 +543,12 @@ namespace Perspex.Controls
}
}
this.AssignSize(this.colMatrix, 0, this.colMatrix.GetUpperBound(0), ref width, GridUnitType.Star, false);
this.AssignSize(this.colMatrix, 0, matrixCount - 1, ref width, GridUnitType.Star, false);
width = Math.Max(0, width);
if (columnsCount > 0)
{
for (int i = 0; i < this.colMatrix.GetUpperBound(0) + 1; i++)
for (int i = 0; i < matrixCount; i++)
{
if (this.colMatrix[i, i].Type == GridUnitType.Star)
{
@ -559,13 +560,14 @@ namespace Perspex.Controls
private void ExpandStarRows(Size availableSize)
{
int matrixCount = this.rowMatrix.GetLength(0);
int rowCount = this.RowDefinitions.Count;
double height = availableSize.Height;
// When expanding star rows, we need to zero out their height before
// calling AssignSize. AssignSize takes care of distributing the
// available size when there are Mins and Maxs applied.
for (int i = 0; i < this.rowMatrix.GetUpperBound(0) + 1; i++)
for (int i = 0; i < matrixCount; i++)
{
if (this.rowMatrix[i, i].Type == GridUnitType.Star)
{
@ -577,11 +579,11 @@ namespace Perspex.Controls
}
}
this.AssignSize(this.rowMatrix, 0, this.rowMatrix.GetUpperBound(0), ref height, GridUnitType.Star, false);
this.AssignSize(this.rowMatrix, 0, matrixCount - 1, ref height, GridUnitType.Star, false);
if (rowCount > 0)
{
for (int i = 0; i < this.rowMatrix.GetUpperBound(0) + 1; i++)
for (int i = 0; i < matrixCount; i++)
{
if (this.rowMatrix[i, i].Type == GridUnitType.Star)
{
@ -703,12 +705,15 @@ namespace Perspex.Controls
}
}
for (int r = 0; r < this.rowMatrix.GetUpperBound(0) + 1; r++)
int rowMatrixDim = this.rowMatrix.GetLength(0);
int colMatrixDim = this.colMatrix.GetLength(0);
for (int r = 0; r < rowMatrixDim; r++)
{
this.rowMatrix[r, r].OfferedSize = this.rowMatrix[r, r].DesiredSize;
}
for (int c = 0; c < this.colMatrix.GetUpperBound(0) + 1; c++)
for (int c = 0; c < colMatrixDim; c++)
{
this.colMatrix[c, c].OfferedSize = this.colMatrix[c, c].DesiredSize;
}
@ -716,17 +721,20 @@ namespace Perspex.Controls
private void SaveMeasureResults()
{
for (int i = 0; i < this.rowMatrix.GetUpperBound(0) + 1; i++)
int rowMatrixDim = this.rowMatrix.GetLength(0);
int colMatrixDim = this.colMatrix.GetLength(0);
for (int i = 0; i < rowMatrixDim; i++)
{
for (int j = 0; j < this.rowMatrix.GetUpperBound(0) + 1; j++)
for (int j = 0; j < rowMatrixDim; j++)
{
this.rowMatrix[i, j].OriginalSize = this.rowMatrix[i, j].OfferedSize;
}
}
for (int i = 0; i < this.colMatrix.GetUpperBound(0); i++)
for (int i = 0; i < colMatrixDim; i++)
{
for (int j = 0; j < this.colMatrix.GetUpperBound(0); j++)
for (int j = 0; j < colMatrixDim; j++)
{
this.colMatrix[i, j].OriginalSize = this.colMatrix[i, j].OfferedSize;
}
@ -735,17 +743,20 @@ namespace Perspex.Controls
private void RestoreMeasureResults()
{
for (int i = 0; i < this.rowMatrix.GetUpperBound(0) + 1; i++)
int rowMatrixDim = this.rowMatrix.GetLength(0);
int colMatrixDim = this.colMatrix.GetLength(0);
for (int i = 0; i < rowMatrixDim; i++)
{
for (int j = 0; j < this.rowMatrix.GetUpperBound(0) + 1; j++)
for (int j = 0; j < rowMatrixDim; j++)
{
this.rowMatrix[i, j].OfferedSize = this.rowMatrix[i, j].OriginalSize;
}
}
for (int i = 0; i < this.colMatrix.GetUpperBound(0) + 1; i++)
for (int i = 0; i < colMatrixDim; i++)
{
for (int j = 0; j < this.colMatrix.GetUpperBound(0) + 1; j++)
for (int j = 0; j < colMatrixDim; j++)
{
this.colMatrix[i, j].OfferedSize = this.colMatrix[i, j].OriginalSize;
}
@ -802,6 +813,9 @@ namespace Perspex.Controls
{
public GridWalker(Grid grid, Segment[,] rowMatrix, Segment[,] colMatrix)
{
int rowMatrixDim = rowMatrix.GetLength(0);
int colMatrixDim = colMatrix.GetLength(0);
foreach (Control child in grid.Children)
{
bool starCol = false;
@ -809,10 +823,10 @@ namespace Perspex.Controls
bool autoCol = false;
bool autoRow = false;
int col = Math.Min(Grid.GetColumn(child), colMatrix.GetUpperBound(0));
int row = Math.Min(Grid.GetRow(child), rowMatrix.GetUpperBound(0));
int colspan = Math.Min(Grid.GetColumnSpan(child), colMatrix.GetUpperBound(0));
int rowspan = Math.Min(Grid.GetRowSpan(child), rowMatrix.GetUpperBound(0));
int col = Math.Min(Grid.GetColumn(child), colMatrixDim - 1);
int row = Math.Min(Grid.GetRow(child), rowMatrixDim - 1);
int colspan = Math.Min(Grid.GetColumnSpan(child), colMatrixDim - 1);
int rowspan = Math.Min(Grid.GetRowSpan(child), rowMatrixDim - 1);
for (int r = row; r < row + rowspan; r++)
{

8
Perspex.Base/IObservableDescription.cs → Perspex.Controls/INamed.cs

@ -1,13 +1,13 @@
// -----------------------------------------------------------------------
// <copyright file="IObservableDescription.cs" company="Steven Kirk">
// <copyright file="TemplateExtensions.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex
namespace Perspex.Controls
{
public interface IObservableDescription
public interface INamed
{
string Description { get; }
string Name { get; }
}
}

15
Perspex.Controls/INavigablePanel.cs

@ -0,0 +1,15 @@
// -----------------------------------------------------------------------
// <copyright file="INavigablePanel.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
using Perspex.Input;
public interface INavigablePanel
{
Control GetControl(FocusNavigationDirection direction, Control from);
}
}

74
Perspex.Controls/ItemsControl.cs

@ -8,7 +8,7 @@ namespace Perspex.Controls
{
using System;
using System.Collections;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Perspex.Collections;
@ -16,7 +16,8 @@ namespace Perspex.Controls
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.VisualTree;
using Perspex.Controls.Utils;
using Perspex.Styling;
public class ItemsControl : TemplatedControl, ILogical
{
@ -35,11 +36,20 @@ namespace Perspex.Controls
private PerspexReadOnlyListView<IVisual, ILogical> logicalChildren =
new PerspexReadOnlyListView<IVisual, ILogical>(x => (ILogical)x);
private ItemsPresenter presenter;
private IItemsPresenter presenter;
static ItemsControl()
{
ItemsProperty.Changed.Subscribe(e =>
{
var control = e.Sender as ItemsControl;
control?.ItemsChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);
});
}
public ItemsControl()
{
this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged);
this.ItemsChanged(null, null);
}
public ItemContainerGenerator ItemContainerGenerator
@ -76,6 +86,20 @@ namespace Perspex.Controls
}
}
protected IItemsPresenter Presenter
{
get
{
return this.presenter;
}
set
{
this.presenter = value;
this.logicalChildren.Source = ((IVisual)value?.Panel)?.VisualChildren;
}
}
protected virtual ItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
@ -83,24 +107,19 @@ namespace Perspex.Controls
protected override void OnTemplateApplied()
{
this.presenter = this.FindTemplateChild<ItemsPresenter>("itemsPresenter");
if (this.presenter != null)
{
this.logicalChildren.Source = ((IVisual)this.presenter.Panel).VisualChildren;
}
this.Presenter = this.FindTemplateChild<IItemsPresenter>("itemsPresenter");
}
private void ItemsChanged(Tuple<IEnumerable, IEnumerable> value)
protected virtual void ItemsChanged(IEnumerable oldValue, IEnumerable newValue)
{
INotifyPropertyChanged inpc = value.Item1 as INotifyPropertyChanged;
var incc = oldValue as INotifyCollectionChanged;
if (inpc != null)
if (incc != null)
{
inpc.PropertyChanged -= this.ItemsPropertyChanged;
incc.CollectionChanged += this.ItemsCollectionChanged;
}
if (value.Item2 == null || !value.Item2.OfType<object>().Any())
if (newValue == null || newValue.Count() == 0)
{
this.Classes.Add(":empty");
}
@ -109,26 +128,25 @@ namespace Perspex.Controls
this.Classes.Remove(":empty");
}
inpc = value.Item2 as INotifyPropertyChanged;
incc = newValue as INotifyCollectionChanged;
if (inpc != null)
if (incc != null)
{
inpc.PropertyChanged += this.ItemsPropertyChanged;
incc.CollectionChanged += this.ItemsCollectionChanged;
}
}
private void ItemsPropertyChanged(object sender, PropertyChangedEventArgs e)
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.PropertyName == "Count")
var collection = sender as ICollection;
if (collection.Count == 0)
{
if (((IList)sender).Count == 0)
{
this.Classes.Add(":empty");
}
else
{
this.Classes.Remove(":empty");
}
this.Classes.Add(":empty");
}
else
{
this.Classes.Remove(":empty");
}
}
}

33
Perspex.Controls/ListBoxItem.cs

@ -6,20 +6,51 @@
namespace Perspex.Controls
{
using System;
using Perspex.Controls.Primitives;
using Perspex.Interactivity;
/// <summary>
/// An selectable item in a <see cref="ListBox"/>.
/// </summary>
public class ListBoxItem : ContentControl, ISelectable
{
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsSelectedProperty =
PerspexProperty.Register<ListBoxItem, bool>("IsSelected");
PerspexProperty.Register<ListBoxItem, bool>(nameof(IsSelected));
/// <summary>
/// Initializes static members of the <see cref="ListBoxItem"/> class.
/// </summary>
static ListBoxItem()
{
Control.PseudoClass(IsSelectedProperty, ":selected");
IsSelectedProperty.Changed.Subscribe(IsSelectedChanged);
}
/// <summary>
/// Gets or sets the selection state of the item.
/// </summary>
public bool IsSelected
{
get { return this.GetValue(IsSelectedProperty); }
set { this.SetValue(IsSelectedProperty, value); }
}
/// <summary>
/// Called when the <see cref="IsSelected"/> property changes on an object.
/// </summary>
/// <param name="e">The sender.</param>
private static void IsSelectedChanged(PerspexPropertyChangedEventArgs e)
{
var interactive = e.Sender as IInteractive;
if (interactive != null)
{
interactive.RaiseEvent(new RoutedEventArgs(SelectingItemsControl.IsSelectedChangedEvent));
}
}
}
}

230
Perspex.Controls/Menu.cs

@ -0,0 +1,230 @@
// -----------------------------------------------------------------------
// <copyright file="Menu.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
using System;
using System.Linq;
using System.Reactive.Disposables;
using Perspex.Input;
using Perspex.LogicalTree;
using Perspex.Rendering;
using Perspex.Interactivity;
using Perspex.Controls.Primitives;
/// <summary>
/// A top-level menu control.
/// </summary>
public class Menu : SelectingItemsControl, IFocusScope, IMainMenu
{
/// <summary>
/// Defines the default items panel used by a <see cref="Menu"/>.
/// </summary>
private static readonly ItemsPanelTemplate DefaultPanel =
new ItemsPanelTemplate(() => new StackPanel { Orientation = Orientation.Horizontal });
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsOpenProperty =
PerspexProperty.Register<Menu, bool>(nameof(IsOpen));
/// <summary>
/// Tracks event handlers added to the root of the visual tree.
/// </summary>
private IDisposable subscription;
/// <summary>
/// Initializes static members of the <see cref="Menu"/> class.
/// </summary>
static Menu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
MenuItem.ClickEvent.AddClassHandler<Menu>(x => x.OnMenuClick);
MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
}
/// <summary>
/// Gets a value indicating whether the menu is open.
/// </summary>
public bool IsOpen
{
get { return this.GetValue(IsOpenProperty); }
private set { this.SetValue(IsOpenProperty, value); }
}
/// <summary>
/// Gets the selected <see cref="MenuItem"/> container.
/// </summary>
private MenuItem SelectedMenuItem
{
get
{
return (this.SelectedItem != null) ?
(MenuItem)this.ItemContainerGenerator.GetContainerForItem(this.SelectedItem) :
null;
}
}
/// <summary>
/// Closes the menu.
/// </summary>
public void Close()
{
foreach (MenuItem i in this.GetLogicalChildren())
{
i.IsSubMenuOpen = false;
}
this.IsOpen = false;
this.SelectedIndex = -1;
}
/// <summary>
/// Opens the menu in response to the Alt/F10 key.
/// </summary>
public void Open()
{
this.SelectedIndex = 0;
this.SelectedMenuItem.Focus();
this.IsOpen = true;
}
/// <summary>
/// Called when the <see cref="MenuItem"/> is attached to the visual tree.
/// </summary>
/// <param name="root">The root of the visual tree.</param>
protected override void OnAttachedToVisualTree(IRenderRoot root)
{
base.OnAttachedToVisualTree(root);
var topLevel = root as TopLevel;
topLevel.Deactivated += this.Deactivated;
var pointerPress = topLevel.AddHandler(
InputElement.PointerPressedEvent,
this.TopLevelPreviewPointerPress,
RoutingStrategies.Tunnel);
this.subscription = new CompositeDisposable(
pointerPress,
Disposable.Create(() => topLevel.Deactivated -= this.Deactivated));
var inputRoot = root as IInputRoot;
if (inputRoot != null && inputRoot.AccessKeyHandler != null)
{
inputRoot.AccessKeyHandler.MainMenu = this;
}
}
/// <summary>
/// Called when the <see cref="Menu"/> is detached from the visual tree.
/// </summary>
/// <param name="oldRoot">The root of the visual tree being detached from.</param>
protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot)
{
base.OnDetachedFromVisualTree(oldRoot);
this.subscription.Dispose();
}
/// <summary>
/// Called when a key is pressed within the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
bool menuWasOpen = this.SelectedMenuItem?.IsSubMenuOpen ?? false;
base.OnKeyDown(e);
if (menuWasOpen)
{
// If a menu item was open and we navigate to a new one with the arrow keys, open
// that menu and select the first item.
var selection = this.SelectedMenuItem;
if (selection != null && !selection.IsSubMenuOpen)
{
selection.IsSubMenuOpen = true;
selection.SelectedIndex = 0;
}
}
}
/// <summary>
/// Called when the menu loses focus.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
this.SelectedItem = null;
}
/// <summary>
/// Called when a submenu opens somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
var menuItem = e.Source as MenuItem;
if (menuItem != null && menuItem.Parent == this)
{
foreach (var child in this.GetLogicalChildren().OfType<MenuItem>())
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
this.IsOpen = true;
}
/// <summary>
/// Called when the top-level window is deactivated.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void Deactivated(object sender, EventArgs e)
{
this.Close();
}
/// <summary>
/// Called when a submenu is clicked somewhere in the menu.
/// </summary>
/// <param name="e">The event args.</param>
private void OnMenuClick(RoutedEventArgs e)
{
this.Close();
FocusManager.Instance.Focus(null);
e.Handled = true;
}
/// <summary>
/// Called when the pointer is pressed anywhere on the window.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void TopLevelPreviewPointerPress(object sender, PointerPressEventArgs e)
{
if (this.IsOpen)
{
var control = e.Source as ILogical;
if (!this.IsLogicalParentOf(control))
{
this.Close();
}
}
}
}
}

547
Perspex.Controls/MenuItem.cs

@ -0,0 +1,547 @@
// -----------------------------------------------------------------------
// <copyright file="MenuItem.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Perspex.Controls.Primitives;
using Perspex.Input;
using Perspex.Interactivity;
using Perspex.Controls.Templates;
using Perspex.Controls.Presenters;
using Perspex.VisualTree;
using Perspex.Threading;
using Splat;
/// <summary>
/// A menu item control.
/// </summary>
public class MenuItem : SelectingItemsControl, ISelectable
{
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
public static readonly PerspexProperty<ICommand> CommandProperty =
Button.CommandProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="CommandParameter"/> property.
/// </summary>
public static readonly PerspexProperty<object> CommandParameterProperty =
Button.CommandParameterProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedItemsControl.HeaderProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="Icon"/> property.
/// </summary>
public static readonly PerspexProperty<object> IconProperty =
PerspexProperty.Register<MenuItem, object>(nameof(Icon));
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner<MenuItem>();
/// <summary>
/// Defines the <see cref="IsSubMenuOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsSubMenuOpenProperty =
PerspexProperty.Register<MenuItem, bool>(nameof(IsSubMenuOpen));
/// <summary>
/// Defines the <see cref="Click"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="SubmenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> SubmenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(SubmenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// The timer used to display submenus.
/// </summary>
private IDisposable submenuTimer;
/// <summary>
/// The submenu popup.
/// </summary>
private Popup popup;
/// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class.
/// </summary>
static MenuItem()
{
FocusableProperty.OverrideDefaultValue<MenuItem>(true);
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick);
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened);
IsSubMenuOpenProperty.Changed.AddClassHandler<MenuItem>(x => x.SubMenuOpenChanged);
AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<MenuItem>(x => x.AccessKeyPressed);
}
/// <summary>
/// Occurs when a <see cref="MenuItem"/> without a submenu is clicked.
/// </summary>
public event EventHandler<RoutedEventArgs> Click
{
add { this.AddHandler(ClickEvent, value); }
remove { this.RemoveHandler(ClickEvent, value); }
}
/// <summary>
/// Occurs when a <see cref="MenuItem"/>'s submenu is opened.
/// </summary>
public event EventHandler<RoutedEventArgs> SubmenuOpened
{
add { this.AddHandler(SubmenuOpenedEvent, value); }
remove { this.RemoveHandler(SubmenuOpenedEvent, value); }
}
/// <summary>
/// Gets or sets the command associated with the menu item.
/// </summary>
public ICommand Command
{
get { return this.GetValue(CommandProperty); }
set { this.SetValue(CommandProperty, value); }
}
/// <summary>
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
/// <see cref="MenuItem"/>.
/// </summary>
public object CommandParameter
{
get { return this.GetValue(CommandParameterProperty); }
set { this.SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="MenuItem"/>'s header.
/// </summary>
public object Header
{
get { return this.GetValue(HeaderProperty); }
set { this.SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
/// </summary>
public object Icon
{
get { return this.GetValue(IconProperty); }
set { this.SetValue(IconProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
/// </summary>
public bool IsSelected
{
get { return this.GetValue(IsSelectedProperty); }
set { this.SetValue(IsSelectedProperty, value); }
}
/// <summary>
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
/// open.
/// </summary>
public bool IsSubMenuOpen
{
get { return this.GetValue(IsSubMenuOpenProperty); }
set { this.SetValue(IsSubMenuOpenProperty, value); }
}
/// <summary>
/// Gets or sets a value that indicates whether the <see cref="MenuItem"/> has a submenu.
/// </summary>
public bool HasSubMenu
{
get { return !this.Classes.Contains(":empty"); }
}
/// <summary>
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level menu item.
/// </summary>
public bool IsTopLevel
{
get { return this.Parent is Menu; }
}
/// <summary>
/// Called when the <see cref="MenuItem"/> is clicked.
/// </summary>
/// <param name="e">The click event args.</param>
protected virtual void OnClick(RoutedEventArgs e)
{
if (this.Command != null)
{
this.Command.Execute(this.CommandParameter);
}
}
/// <summary>
/// Called when the <see cref="MenuItem"/> recieves focus.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
this.IsSelected = true;
}
/// <summary>
/// Called when a key is pressed in the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
// Some keypresses we want to pass straight to the parent MenuItem/Menu without giving
// this MenuItem the chance to handle them. This is usually e.g. when the submenu is
// closed so passing them to the base would try to move the selection in a hidden
// submenu.
var passStraightToParent = true;
switch (e.Key)
{
case Key.Up:
passStraightToParent = !this.IsSubMenuOpen;
break;
case Key.Down:
if (this.IsTopLevel && this.HasSubMenu && !this.IsSubMenuOpen)
{
this.SelectedIndex = 0;
this.IsSubMenuOpen = true;
e.Handled = true;
}
passStraightToParent = !this.IsSubMenuOpen;
break;
case Key.Left:
if (!this.IsTopLevel && this.IsSubMenuOpen)
{
this.IsSubMenuOpen = false;
e.Handled = true;
}
passStraightToParent = this.IsTopLevel || !this.IsSubMenuOpen;
break;
case Key.Right:
if (!this.IsTopLevel && this.HasSubMenu && !this.IsSubMenuOpen)
{
this.SelectedIndex = 0;
this.IsSubMenuOpen = true;
e.Handled = true;
}
passStraightToParent = this.IsTopLevel || !this.IsSubMenuOpen;
break;
case Key.Enter:
if (this.HasSubMenu)
{
goto case Key.Right;
}
else
{
this.RaiseEvent(new RoutedEventArgs(ClickEvent));
e.Handled = true;
}
break;
case Key.Escape:
if (this.IsSubMenuOpen)
{
this.IsSubMenuOpen = false;
e.Handled = true;
}
break;
}
if (!passStraightToParent)
{
base.OnKeyDown(e);
}
}
/// <summary>
/// Called when the pointer enters the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerEnter(PointerEventArgs e)
{
base.OnPointerEnter(e);
var menu = this.Parent as Menu;
if (menu != null)
{
if (menu.IsOpen)
{
this.IsSubMenuOpen = true;
}
}
else if (this.HasSubMenu && !this.IsSubMenuOpen)
{
this.submenuTimer = DispatcherTimer.Run(
() => this.IsSubMenuOpen = true,
TimeSpan.FromMilliseconds(400));
}
}
/// <summary>
/// Called when the pointer leaves the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerLeave(PointerEventArgs e)
{
base.OnPointerLeave(e);
if (this.submenuTimer != null)
{
this.submenuTimer.Dispose();
this.submenuTimer = null;
}
}
/// <summary>
/// Called when the pointer is pressed over the <see cref="MenuItem"/>.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerPressed(PointerPressEventArgs e)
{
base.OnPointerPressed(e);
if (!this.HasSubMenu)
{
this.RaiseEvent(new RoutedEventArgs(ClickEvent));
}
else if (this.IsTopLevel)
{
this.IsSubMenuOpen = !this.IsSubMenuOpen;
}
else
{
this.IsSubMenuOpen = true;
}
e.Handled = true;
}
/// <summary>
/// Called when a submenu is opened on this MenuItem or a child MenuItem.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnSubmenuOpened(RoutedEventArgs e)
{
var menuItem = e.Source as MenuItem;
if (menuItem != null && menuItem.Parent == this)
{
foreach (var child in this.Items.OfType<MenuItem>())
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
}
/// <summary>
/// Called when the MenuItem's template has been applied.
/// </summary>
protected override void OnTemplateApplied()
{
base.OnTemplateApplied();
this.popup = this.GetTemplateChild<Popup>("popup");
this.popup.DependencyResolver = DependencyResolver.Instance;
this.popup.PopupRootCreated += this.PopupRootCreated;
this.popup.Opened += this.PopupOpened;
this.popup.Closed += this.PopupClosed;
}
/// <summary>
/// Called when the menu item's access key is pressed.
/// </summary>
/// <param name="e">The event args.</param>
private void AccessKeyPressed(RoutedEventArgs e)
{
if (this.HasSubMenu)
{
this.SelectedIndex = 0;
this.IsSubMenuOpen = true;
}
else
{
this.RaiseEvent(new RoutedEventArgs(ClickEvent));
}
e.Handled = true;
}
/// <summary>
/// Closes all submenus of the menu item.
/// </summary>
private void CloseSubmenus()
{
foreach (var child in this.Items.OfType<MenuItem>())
{
child.IsSubMenuOpen = false;
}
}
/// <summary>
/// Called when the <see cref="IsSubMenuOpen"/> property changes.
/// </summary>
/// <param name="e">The property change event.</param>
private void SubMenuOpenChanged(PerspexPropertyChangedEventArgs e)
{
var value = (bool)e.NewValue;
if (value)
{
this.RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
this.IsSelected = true;
}
else
{
this.CloseSubmenus();
this.SelectedIndex = -1;
}
}
/// <summary>
/// Called when the submenu's <see cref="PopupRoot"/> is created.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void PopupRootCreated(object sender, EventArgs e)
{
var popup = (Popup)sender;
ItemsPresenter presenter = null;
// Our ItemsPresenter is in a Popup which means that it's only created when the
// Popup is opened, therefore it wasn't found by ItemsControl.OnTemplateApplied.
// Now the Popup has been opened for the first time it should exist, so make sure
// the PopupRoot's template is applied and look for the ItemsPresenter.
foreach (var c in popup.PopupRoot.GetSelfAndVisualDescendents().OfType<Control>())
{
if (c.Name == "itemsPresenter" && c is ItemsPresenter)
{
presenter = c as ItemsPresenter;
break;
}
c.ApplyTemplate();
}
if (presenter != null)
{
// The presenter was found. Set its TemplatedParent so it thinks that it had a
// normal birth; may it never know its own perveristy.
presenter.TemplatedParent = this;
this.Presenter = presenter;
}
}
/// <summary>
/// Called when the submenu's <see cref="Popup"/> is opened.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void PopupOpened(object sender, EventArgs e)
{
var selected = this.SelectedItem;
if (selected != null)
{
var container = this.ItemContainerGenerator.GetContainerForItem(selected);
if (container != null)
{
container.Focus();
}
}
}
/// <summary>
/// Called when the submenu's <see cref="Popup"/> is closed.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void PopupClosed(object sender, EventArgs e)
{
this.SelectedItem = null;
}
/// <summary>
/// A dependency resolver which returns a <see cref="MenuItemAccessKeyHandler"/>.
/// </summary>
private class DependencyResolver : IDependencyResolver
{
/// <summary>
/// Gets the default instance of <see cref="DependencyResolver"/>.
/// </summary>
public static readonly DependencyResolver Instance = new DependencyResolver();
/// <summary>
/// Disposes of all managed resources.
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Gets a service of the specified type.
/// </summary>
/// <param name="serviceType">The service type.</param>
/// <param name="contract">An optional contract.</param>
/// <returns>A service of the requested type.</returns>
public object GetService(Type serviceType, string contract = null)
{
if (serviceType == typeof(IAccessKeyHandler))
{
return new MenuItemAccessKeyHandler();
}
else
{
return Locator.Current.GetService(serviceType, contract);
}
}
/// <summary>
/// Gets collection of services of the specified type.
/// </summary>
/// <param name="serviceType">The service type.</param>
/// <param name="contract">An optional contract.</param>
/// <returns>A collection of services of the requested type.</returns>
public IEnumerable<object> GetServices(Type serviceType, string contract = null)
{
return Locator.Current.GetServices(serviceType, contract);
}
}
}
}

111
Perspex.Controls/MenuItemAccessKeyHandler.cs

@ -0,0 +1,111 @@
// -----------------------------------------------------------------------
// <copyright file="MenuItemAccessKeyHandler.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.Input;
using Perspex.Interactivity;
/// <summary>
/// Handles access keys within a <see cref="MenuItem"/>
/// </summary>
public class MenuItemAccessKeyHandler : IAccessKeyHandler
{
/// <summary>
/// The registered access keys.
/// </summary>
private List<Tuple<string, IInputElement>> registered = new List<Tuple<string, IInputElement>>();
/// <summary>
/// The window to which the handler belongs.
/// </summary>
private IInputRoot owner;
/// <summary>
/// Gets or sets the window's main menu.
/// </summary>
/// <remarks>
/// This property is ignored as a menu item cannot have a main menu.
/// </remarks>
public IMainMenu MainMenu { get; set; }
/// <summary>
/// Sets the owner of the access key handler.
/// </summary>
/// <param name="owner">The owner.</param>
/// <remarks>
/// This method can only be called once, typically by the owner itself on creation.
/// </remarks>
public void SetOwner(IInputRoot owner)
{
Contract.Requires<ArgumentNullException>(owner != null);
if (this.owner != null)
{
throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
}
this.owner = owner;
this.owner.AddHandler(InputElement.KeyDownEvent, this.OnKeyDown);
}
/// <summary>
/// Registers an input element to be associated with an access key.
/// </summary>
/// <param name="accessKey">The access key.</param>
/// <param name="element">The input element.</param>
public void Register(char accessKey, IInputElement element)
{
var existing = this.registered.FirstOrDefault(x => x.Item2 == element);
if (existing != null)
{
this.registered.Remove(existing);
}
this.registered.Add(Tuple.Create(accessKey.ToString().ToUpper(), element));
}
/// <summary>
/// Unregisters the access keys associated with the input element.
/// </summary>
/// <param name="element">The input element.</param>
public void Unregister(IInputElement element)
{
foreach (var i in this.registered.Where(x => x.Item2 == element).ToList())
{
this.registered.Remove(i);
}
}
/// <summary>
/// Handles a key being pressed in the menu.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected virtual void OnKeyDown(object sender, KeyEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Text))
{
var text = e.Text.ToUpper();
var focus = this.registered
.Where(x => x.Item1 == text && x.Item2.IsEffectivelyVisible)
.FirstOrDefault()?.Item2;
if (focus != null)
{
focus.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
}
e.Handled = true;
}
}
}
}

12
Perspex.Controls/Panel.cs

@ -72,8 +72,16 @@ namespace Perspex.Controls
ILogical IItemsPanel.ChildLogicalParent
{
get { return this.childLogicalParent; }
set { this.childLogicalParent = value; }
get
{
return this.childLogicalParent;
}
set
{
this.childLogicalParent = value;
this.SetLogicalParent(this.Children);
}
}
protected virtual void OnChildrenAdded(IEnumerable<Control> child)

53
Perspex.Controls/Parsers/GridLengthsParser.cs

@ -0,0 +1,53 @@
// -----------------------------------------------------------------------
// <copyright file="GridLengthsParser.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls.Parsers
{
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Parses a string of <see cref="GridLength"/>s for <see cref="ColumnDefinitions"/> and
/// <see cref="RowDefinitions"/>.
/// </summary>
public static class GridLengthsParser
{
/// <summary>
/// Parses a string of <see cref="GridLength"/>s.
/// </summary>
/// <param name="s">The string.</param>
/// <returns>A collection of <see cref="GridLength"/>s.</returns>
public static IEnumerable<GridLength> Parse(string s)
{
var parts = s.Split(',').Select(x => x.ToUpperInvariant().Trim());
foreach (var part in parts)
{
if (part == "AUTO")
{
yield return GridLength.Auto;
}
else if (part.EndsWith("*"))
{
var valueString = part.Substring(0, part.Length - 1).Trim();
var value = valueString.Length > 0 ? double.Parse(valueString) : 1;
yield return new GridLength(value, GridUnitType.Star);
}
else if (part.EndsWith("PX"))
{
var value = double.Parse(part.Substring(0, part.Length - 2));
yield return new GridLength(value, GridUnitType.Pixel);
}
else
{
throw new FormatException("Invalid grid length: " + part);
}
}
}
}
}

35
Perspex.Controls/Perspex.Controls.csproj

@ -15,6 +15,8 @@
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
@ -34,17 +36,29 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="MenuItemAccessKeyHandler.cs" />
<Compile Include="Parsers\GridLengthsParser.cs" />
<Compile Include="Primitives\AccessText.cs" />
<Compile Include="Border.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="MenuItem.cs" />
<Compile Include="Menu.cs" />
<Compile Include="Button.cs" />
<Compile Include="AdornerTemplate.cs" />
<Compile Include="DropDown.cs" />
<Compile Include="IContentControl.cs" />
<Compile Include="IItemsPanel.cs" />
<Compile Include="INamed.cs" />
<Compile Include="INavigablePanel.cs" />
<Compile Include="Deck.cs" />
<Compile Include="Platform\IPopupImpl.cs" />
<Compile Include="Platform\ITopLevelImpl.cs" />
<Compile Include="PlacementMode.cs" />
<Compile Include="Popup.cs" />
<Compile Include="IDataTemplate.cs" />
<Compile Include="Presenters\IItemsPresenter.cs" />
<Compile Include="Presenters\IPresenter.cs" />
<Compile Include="Presenters\DeckPresenter.cs" />
<Compile Include="Primitives\AdornerDecorator.cs" />
<Compile Include="Primitives\AdornerLayer.cs" />
<Compile Include="RadioButton.cs" />
@ -88,9 +102,11 @@
<Compile Include="Primitives\Thumb.cs" />
<Compile Include="RequestBringIntoViewEventArgs.cs" />
<Compile Include="Shapes\Ellipse.cs" />
<Compile Include="ToolTip.cs" />
<Compile Include="UserControl.cs" />
<Compile Include="Templates\TemplateExtensions.cs" />
<Compile Include="TextWrapping.cs" />
<Compile Include="Utils\IEnumerableUtils.cs" />
<Compile Include="Utils\StringUtils.cs" />
<Compile Include="TopLevel.cs" />
<Compile Include="PopupRoot.cs" />
@ -118,8 +134,13 @@
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Splat">
<HintPath>..\packages\Splat.1.6.1\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\packages\Serilog.1.5.5\lib\portable-net45+win+wpa81+wp80+MonoAndroid10+MonoTouch10\Serilog.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Core">
<HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
@ -168,14 +189,10 @@
<Name>Perspex.Styling</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\packages\StyleCop.Analyzers.1.0.0-alpha008\tools\analyzers\C#\StyleCop.Analyzers.dll" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<Import Project="..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets" Condition="Exists('..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\StyleCop.MSBuild.4.7.49.0\build\StyleCop.MSBuild.Targets'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

14
Perspex.Controls/PlacementMode.cs

@ -0,0 +1,14 @@
// -----------------------------------------------------------------------
// <copyright file="PlacementMode.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls
{
public enum PlacementMode
{
Bottom,
Right,
}
}

239
Perspex.Controls/Popup.cs

@ -8,76 +8,182 @@ namespace Perspex.Controls
{
using System;
using Perspex.Interactivity;
using Perspex.Platform;
using Perspex.Collections;
using Perspex.Rendering;
using Perspex.VisualTree;
using Splat;
public class Popup : Control
/// <summary>
/// Displays a popup window.
/// </summary>
public class Popup : Control, ILogical, IVisualTreeHost
{
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly PerspexProperty<Control> ChildProperty =
PerspexProperty.Register<Popup, Control>("Child");
PerspexProperty.Register<Popup, Control>(nameof(Child));
/// <summary>
/// Defines the <see cref="IsOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsOpenProperty =
PerspexProperty.Register<Popup, bool>("IsOpen");
PerspexProperty.Register<Popup, bool>(nameof(IsOpen));
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly PerspexProperty<PlacementMode> PlacementModeProperty =
PerspexProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly PerspexProperty<Control> PlacementTargetProperty =
PerspexProperty.Register<Popup, Control>("PlacementTarget");
PerspexProperty.Register<Popup, Control>(nameof(PlacementTarget));
/// <summary>
/// Defines the <see cref="StaysOpen"/> property.
/// </summary>
public static readonly PerspexProperty<bool> StaysOpenProperty =
PerspexProperty.Register<Popup, bool>("StaysOpen", true);
PerspexProperty.Register<Popup, bool>(nameof(StaysOpen), true);
/// <summary>
/// The root of the popup.
/// </summary>
private PopupRoot popupRoot;
/// <summary>
/// The top level control of the Popup's visual tree.
/// </summary>
private TopLevel topLevel;
/// <summary>
/// The popup's logical child.
/// </summary>
private PerspexSingleItemList<ILogical> logicalChild = new PerspexSingleItemList<ILogical>();
/// <summary>
/// Initializes static members of the <see cref="Popup"/> class.
/// </summary>
static Popup()
{
IsOpenProperty.Changed.Subscribe(x =>
{
Popup popup = x.Sender as Popup;
IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
}
if (popup != null)
{
if ((bool)x.NewValue)
{
popup.Open();
}
else
{
popup.Close();
}
}
});
/// <summary>
/// Initializes a new instance of the <see cref="Popup"/> class.
/// </summary>
public Popup()
{
this.GetObservableWithHistory(ChildProperty).Subscribe(this.ChildChanged);
}
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler Closed;
/// <summary>
/// Raised when the popup opens.
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Raised when the popup root has been created, but before it has been shown.
/// </summary>
public event EventHandler PopupRootCreated;
/// <summary>
/// Gets or sets the control to display in the popup.
/// </summary>
public Control Child
{
get { return this.GetValue(ChildProperty); }
set { this.SetValue(ChildProperty, value); }
}
/// <summary>
/// Gets or sets a dependency resolver for the <see cref="PopupRoot"/>.
/// </summary>
/// <remarks>
/// This property allows a client to customize the behaviour of the popup by injecting
/// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
/// </remarks>
public IDependencyResolver DependencyResolver
{
get;
set;
}
/// <summary>
/// Gets or sets a value indicating whether the popup is currently open.
/// </summary>
public bool IsOpen
{
get { return this.GetValue(IsOpenProperty); }
set { this.SetValue(IsOpenProperty, value); }
}
/// <summary>
/// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public PlacementMode PlacementMode
{
get { return this.GetValue(PlacementModeProperty); }
set { this.SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control PlacementTarget
{
get { return this.GetValue(PlacementTargetProperty); }
set { this.SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Gets the root of the popup window.
/// </summary>
public PopupRoot PopupRoot
{
get { return this.popupRoot; }
}
/// <summary>
/// Gets or sets a value indicating whether the popup should stay open when the popup loses focus.
/// </summary>
public bool StaysOpen
{
get { return this.GetValue(StaysOpenProperty); }
set { this.SetValue(StaysOpenProperty, value); }
}
/// <summary>
/// Gets the logical children of the popup.
/// </summary>
IPerspexReadOnlyList<ILogical> ILogical.LogicalChildren
{
get { return this.logicalChild; }
}
/// <summary>
/// Gets the root of the popup window.
/// </summary>
IVisual IVisualTreeHost.Root
{
get { return this.popupRoot; }
}
/// <summary>
/// Opens the popup.
/// </summary>
public void Open()
{
if (this.popupRoot == null)
{
this.popupRoot = new PopupRoot()
this.popupRoot = new PopupRoot(this.DependencyResolver)
{
Parent = this,
[~PopupRoot.ContentProperty] = this[~ChildProperty],
@ -95,9 +201,16 @@ namespace Perspex.Controls
this.popupRoot.AddHandler(PopupRoot.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Bubble, true);
this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel);
this.PopupRootCreated?.Invoke(this, EventArgs.Empty);
this.popupRoot.Show();
this.IsOpen = true;
this.Opened?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Closes the popup.
/// </summary>
public void Close()
{
if (this.popupRoot != null)
@ -109,30 +222,98 @@ namespace Perspex.Controls
}
this.IsOpen = false;
this.Closed?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>A size of 0,0 as Popup itself takes up no space.</returns>
protected override Size MeasureCore(Size availableSize)
{
return new Size();
}
/// <summary>
/// Called when the control is added to the visual tree.
/// </summary>
/// <param name="root">THe root of the visual tree.</param>
protected override void OnAttachedToVisualTree(IRenderRoot root)
{
base.OnAttachedToVisualTree(root);
this.topLevel = root as TopLevel;
}
protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot)
/// <summary>
/// Called when the control is removed to the visual tree.
/// </summary>
/// <param name="root">THe root of the visual tree.</param>
protected override void OnDetachedFromVisualTree(IRenderRoot root)
{
base.OnDetachedFromVisualTree(oldRoot);
base.OnDetachedFromVisualTree(root);
this.topLevel = null;
}
/// <summary>
/// Called when the <see cref="IsOpen"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private void IsOpenChanged(PerspexPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
this.Open();
}
else
{
this.Close();
}
}
/// <summary>
/// Called when the <see cref="Child"/> property changes.
/// </summary>
/// <param name="values">The old and new values.</param>
private void ChildChanged(Tuple<Control, Control> values)
{
if (values.Item1 != null)
{
values.Item1.Parent = null;
}
if (values.Item2 != null)
{
values.Item2.Parent = this;
}
this.logicalChild.SingleItem = values.Item2;
}
/// <summary>
/// Gets the position for the popup based on the placement properties.
/// </summary>
/// <returns>The popup's position in screen coordinates.</returns>
private Point GetPosition()
{
if (this.PlacementTarget != null)
var target = this.PlacementTarget ?? this.GetVisualParent<Control>();
Point point;
if (target != null)
{
return this.PlacementTarget.PointToScreen(new Point(0, this.PlacementTarget.Bounds.Size.Height));
switch (this.PlacementMode)
{
case PlacementMode.Bottom:
point = new Point(0, target.Bounds.Height);
break;
case PlacementMode.Right:
point = new Point(target.Bounds.Width, 0);
break;
default:
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
}
return target.PointToScreen(point);
}
else
{
@ -140,6 +321,12 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Conditionally closes the popup in response to an event, based on the value of the
/// <see cref="StaysOpen"/> property.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void MaybeClose(object sender, EventArgs e)
{
if (!this.StaysOpen)

64
Perspex.Controls/PopupRoot.cs

@ -6,41 +6,99 @@
namespace Perspex.Controls
{
using System;
using Perspex.Interactivity;
using Perspex.Media;
using Perspex.Platform;
using Splat;
using Perspex.VisualTree;
public class PopupRoot : TopLevel
/// <summary>
/// The root window of a <see cref="Popup"/>.
/// </summary>
public class PopupRoot : TopLevel, IInteractive, IHostedVisualTreeRoot
{
/// <summary>
/// Initializes static members of the <see cref="PopupRoot"/> class.
/// </summary>
static PopupRoot()
{
BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White);
}
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
public PopupRoot()
: base(Locator.Current.GetService<IPopupImpl>())
: this(null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
/// <param name="dependencyResolver">
/// The dependency resolver to use. If null the default dependency resolver will be used.
/// </param>
public PopupRoot(IDependencyResolver dependencyResolver)
: base(Locator.Current.GetService<IPopupImpl>(), dependencyResolver)
{
this.GetObservable(ParentProperty).Subscribe(x => this.InheritanceParent = x);
}
/// <summary>
/// Gets the platform-specific window implementation.
/// </summary>
public new IPopupImpl PlatformImpl
{
get { return (IPopupImpl)base.PlatformImpl; }
}
/// <summary>
/// Gets the parent control in the event route.
/// </summary>
/// <remarks>
/// Popup events are passed to their parent window. This facilitates this.
/// </remarks>
IInteractive IInteractive.InteractiveParent
{
get { return this.Parent; }
}
/// <summary>
/// Gets the control that is hosting the popup root.
/// </summary>
IVisual IHostedVisualTreeRoot.Host
{
get { return this.Parent; }
}
/// <summary>
/// Sets the position of the popup in screen coordinates.
/// </summary>
/// <param name="p">The position.</param>
public void SetPosition(Point p)
{
this.PlatformImpl.SetPosition(p);
}
/// <summary>
/// Hides the popup.
/// </summary>
public void Hide()
{
this.PlatformImpl.Hide();
this.IsVisible = false;
}
/// <summary>
/// Shows the popup.
/// </summary>
public void Show()
{
this.PlatformImpl.Show();
this.ExecuteLayoutPass();
this.LayoutManager.ExecuteLayoutPass();
this.IsVisible = true;
}
}
}

4
Perspex.Controls/Presenters/ContentPresenter.cs

@ -65,7 +65,7 @@ namespace Perspex.Controls.Presenters
if (child != null)
{
child.Measure(availableSize);
return child.DesiredSize.Value;
return child.DesiredSize;
}
return new Size();
@ -87,6 +87,8 @@ namespace Perspex.Controls.Presenters
if (content != null)
{
result = this.MaterializeDataTemplate(content);
result.Parent = this.TemplatedParent as Control;
var templatedParent = this.TemplatedParent as TemplatedControl;
if (templatedParent != null)

159
Perspex.Controls/Presenters/DeckPresenter.cs

@ -0,0 +1,159 @@
// -----------------------------------------------------------------------
// <copyright file="DeckPresenter.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls.Presenters
{
using Perspex.Animation;
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Controls.Utils;
using Perspex.Input;
using Perspex.Styling;
using System;
using System.Collections;
using System.Linq;
using System.Reactive.Linq;
public class DeckPresenter : Control, IItemsPresenter, ITemplatedControl
{
public static readonly PerspexProperty<IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<DeckPresenter>();
public static readonly PerspexProperty<ItemsPanelTemplate> ItemsPanelProperty =
ItemsControl.ItemsPanelProperty.AddOwner<DeckPresenter>();
public static readonly PerspexProperty<object> SelectedItemProperty =
SelectingItemsControl.SelectedItemProperty.AddOwner<DeckPresenter>();
public static readonly PerspexProperty<IPageTransition> TransitionProperty =
Deck.TransitionProperty.AddOwner<DeckPresenter>();
private bool createdPanel;
public DeckPresenter()
{
this.GetObservableWithHistory(SelectedItemProperty).Subscribe(this.SelectedItemChanged);
}
public ItemContainerGenerator ItemContainerGenerator
{
get;
private set;
}
public IEnumerable Items
{
get { return this.GetValue(ItemsProperty); }
set { this.SetValue(ItemsProperty, value); }
}
public ItemsPanelTemplate ItemsPanel
{
get { return this.GetValue(ItemsPanelProperty); }
set { this.SetValue(ItemsPanelProperty, value); }
}
public object SelectedItem
{
get { return this.GetValue(SelectedItemProperty); }
set { this.SetValue(SelectedItemProperty, value); }
}
public Panel Panel
{
get;
private set;
}
public IPageTransition Transition
{
get { return this.GetValue(TransitionProperty); }
set { this.SetValue(TransitionProperty, value); }
}
public override sealed void ApplyTemplate()
{
if (!this.createdPanel)
{
this.CreatePanel();
}
}
protected override Size MeasureOverride(Size availableSize)
{
this.Panel.Measure(availableSize);
return this.Panel.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
this.Panel.Arrange(new Rect(finalSize));
return finalSize;
}
private void CreatePanel()
{
this.ClearVisualChildren();
this.Panel = this.ItemsPanel.Build();
this.Panel.TemplatedParent = this;
((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical;
this.AddVisualChild(this.Panel);
this.createdPanel = true;
this.SelectedItemChanged(Tuple.Create<object, object>(null, this.SelectedItem));
}
private IItemContainerGenerator GetGenerator()
{
if (this.ItemContainerGenerator == null)
{
ItemsControl i = this.TemplatedParent as ItemsControl;
this.ItemContainerGenerator = i?.ItemContainerGenerator ?? new ItemContainerGenerator(this);
}
return this.ItemContainerGenerator;
}
private async void SelectedItemChanged(Tuple<object, object> value)
{
if (this.createdPanel)
{
var generator = this.GetGenerator();
Control from = null;
Control to = null;
int fromIndex = -1;
int toIndex = -1;
if (value.Item1 != null)
{
from = generator.GetContainerForItem(value.Item1);
fromIndex = this.Items.IndexOf(value.Item1);
}
if (value.Item2 != null)
{
to = generator.Generate(new[] { value.Item2 }).FirstOrDefault();
if (to != null)
{
this.Panel.Children.Add(to);
toIndex = this.Items.IndexOf(value.Item2);
}
}
if (this.Transition != null)
{
await this.Transition.Start(from, to, fromIndex < toIndex);
}
if (from != null)
{
this.Panel.Children.Remove(from);
generator.Remove(new[] { value.Item1 });
}
}
}
}
}

13
Perspex.Controls/Presenters/IItemsPresenter.cs

@ -0,0 +1,13 @@
// -----------------------------------------------------------------------
// <copyright file="IPresenter.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls.Presenters
{
public interface IItemsPresenter : IPresenter
{
Panel Panel { get; }
}
}

2
Perspex.Controls/Presenters/IPresenter.cs

@ -18,7 +18,7 @@ namespace Perspex.Controls.Presenters
/// of a <see cref="TemplatedControl"/> then that signals that the visual child
/// of the presenter is not a part of the template.
/// </remarks>
public interface IPresenter : IVisual
public interface IPresenter : IVisual, INamed
{
}
}

32
Perspex.Controls/Presenters/ItemsPresenter.cs

@ -6,14 +6,15 @@
namespace Perspex.Controls.Presenters
{
using Perspex.Controls.Generators;
using Perspex.Input;
using Perspex.Styling;
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Reactive.Linq;
using Perspex.Controls.Generators;
using Perspex.Styling;
public class ItemsPresenter : Control, IVisual, IPresenter, ITemplatedControl
public class ItemsPresenter : Control, IItemsPresenter, ITemplatedControl
{
public static readonly PerspexProperty<IEnumerable> ItemsProperty =
ItemsControl.ItemsProperty.AddOwner<ItemsPresenter>();
@ -23,11 +24,24 @@ namespace Perspex.Controls.Presenters
private bool createdPanel;
static ItemsPresenter()
{
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue(
typeof(ItemsPresenter),
KeyboardNavigationMode.Once);
}
public ItemsPresenter()
{
this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged);
}
public ItemContainerGenerator ItemContainerGenerator
{
get;
private set;
}
public IEnumerable Items
{
get { return this.GetValue(ItemsProperty); }
@ -57,7 +71,7 @@ namespace Perspex.Controls.Presenters
protected override Size MeasureOverride(Size availableSize)
{
this.Panel.Measure(availableSize);
return this.Panel.DesiredSize.Value;
return this.Panel.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
@ -71,6 +85,7 @@ namespace Perspex.Controls.Presenters
this.ClearVisualChildren();
this.Panel = this.ItemsPanel.Build();
this.Panel.TemplatedParent = this;
KeyboardNavigation.SetTabNavigation(this.Panel, KeyboardNavigation.GetTabNavigation(this));
((IItemsPanel)this.Panel).ChildLogicalParent = this.TemplatedParent as ILogical;
this.AddVisualChild(this.Panel);
this.createdPanel = true;
@ -79,14 +94,13 @@ namespace Perspex.Controls.Presenters
private IItemContainerGenerator GetGenerator()
{
ItemsControl i = this.TemplatedParent as ItemsControl;
if (i == null)
if (this.ItemContainerGenerator == null)
{
throw new InvalidOperationException("ItemsPresenter must be part of an ItemsControl template.");
ItemsControl i = this.TemplatedParent as ItemsControl;
this.ItemContainerGenerator = i?.ItemContainerGenerator ?? new ItemContainerGenerator(this);
}
return i.ItemContainerGenerator;
return this.ItemContainerGenerator;
}
private void ItemsChanged(Tuple<IEnumerable, IEnumerable> value)

7
Perspex.Controls/Presenters/ScrollContentPresenter.cs

@ -76,7 +76,7 @@ namespace Perspex.Controls.Presenters
}
content.Measure(measureSize);
var size = content.DesiredSize.Value;
var size = content.DesiredSize;
this.measuredExtent = size;
return size.Constrain(availableSize);
}
@ -95,7 +95,10 @@ namespace Perspex.Controls.Presenters
if (child != null)
{
child.Arrange(new Rect((Point)(-this.Offset), child.DesiredSize.Value));
var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
child.Arrange(new Rect((Point)(-this.Offset), size));
return finalSize;
}

14
Perspex.Controls/Presenters/TextPresenter.cs

@ -100,7 +100,14 @@ namespace Perspex.Controls.Presenters
if (this.caretBlink)
{
context.DrawLine(new Pen(caretBrush, 1), charPos.TopLeft, charPos.BottomLeft);
var x = Math.Floor(charPos.X) + 0.5;
var y = Math.Floor(charPos.Y) + 0.5;
var b = Math.Ceiling(charPos.Bottom) - 0.5;
context.DrawLine(
new Pen(caretBrush, 1),
new Point(x, y),
new Point(x, b));
}
}
}
@ -133,9 +140,9 @@ namespace Perspex.Controls.Presenters
}
}
protected override FormattedText CreateFormattedText()
protected override FormattedText CreateFormattedText(Size constraint)
{
var result = base.CreateFormattedText();
var result = base.CreateFormattedText(constraint);
var selectionStart = this.SelectionStart;
var selectionEnd = this.SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
@ -165,6 +172,7 @@ namespace Perspex.Controls.Presenters
this.FontFamily,
this.FontSize,
this.FontStyle,
TextAlignment.Left,
this.FontWeight))
{
return formattedText.Measure();

191
Perspex.Controls/Primitives/AccessText.cs

@ -0,0 +1,191 @@
// -----------------------------------------------------------------------
// <copyright file="AccessText.cs" company="Steven Kirk">
// Copyright 2015 MIT Licence. See licence.md for more information.
// </copyright>
// -----------------------------------------------------------------------
namespace Perspex.Controls.Primitives
{
using System;
using Perspex.Media;
using Perspex.Input;
using Perspex.Rendering;
/// <summary>
/// A text block that displays a character prefixed with an underscore as an access key.
/// </summary>
public class AccessText : TextBlock
{
/// <summary>
/// Defines the <see cref="ShowAccessKey"/> attached property.
/// </summary>
public static readonly PerspexProperty<bool> ShowAccessKeyProperty =
PerspexProperty.RegisterAttached<AccessText, Control, bool>("ShowAccessKey", inherits: true);
/// <summary>
/// The access key handler for the current window.
/// </summary>
private IAccessKeyHandler accessKeys;
/// <summary>
/// Initializes static members of the <see cref="AccessText"/> class.
/// </summary>
static AccessText()
{
AffectsRender(ShowAccessKeyProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="AccessText"/> class.
/// </summary>
public AccessText()
{
this.GetObservable(TextProperty).Subscribe(this.TextChanged);
}
/// <summary>
/// Gets the access key.
/// </summary>
public char AccessKey
{
get;
private set;
}
/// <summary>
/// Gets or sets a value indicating whether the access key should be underlined.
/// </summary>
public bool ShowAccessKey
{
get { return this.GetValue(ShowAccessKeyProperty); }
set { this.SetValue(ShowAccessKeyProperty, value); }
}
/// <summary>
/// Renders the <see cref="AccessText"/> to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(IDrawingContext context)
{
base.Render(context);
int underscore = this.Text?.IndexOf('_') ?? -1;
if (underscore != -1 && this.ShowAccessKey)
{
var rect = this.FormattedText.HitTestTextPosition(underscore);
var offset = new Vector(0, -0.5);
context.DrawLine(
new Pen(this.Foreground, 1),
rect.BottomLeft + offset,
rect.BottomRight + offset);
}
}
/// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text.
/// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns>
protected override FormattedText CreateFormattedText(Size constraint)
{
var result = new FormattedText(
this.StripAccessKey(this.Text),
this.FontFamily,
this.FontSize,
this.FontStyle,
this.TextAlignment,
this.FontWeight);
result.Constraint = constraint;
return result;
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size availableSize)
{
var result = base.MeasureOverride(availableSize);
return result.WithHeight(result.Height + 1);
}
/// <summary>
/// Called when the control is attached to a visual tree.
/// </summary>
/// <param name="root">The root of the visual tree.</param>
protected override void OnAttachedToVisualTree(IRenderRoot root)
{
base.OnAttachedToVisualTree(root);
this.accessKeys = (root as IInputRoot)?.AccessKeyHandler;
if (this.accessKeys != null && this.AccessKey != 0)
{
this.accessKeys.Register(this.AccessKey, this);
}
}
/// <summary>
/// Called when the control is detached from a visual tree.
/// </summary>
/// <param name="root">The root of the visual tree.</param>
protected override void OnDetachedFromVisualTree(IRenderRoot root)
{
base.OnDetachedFromVisualTree(root);
if (this.accessKeys != null && this.AccessKey != 0)
{
this.accessKeys.Unregister(this);
this.accessKeys = null;
}
}
/// <summary>
/// Returns a string with the first underscore stripped.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>The text with the first underscore stripped.</returns>
private string StripAccessKey(string text)
{
var position = text.IndexOf('_');
if (position == -1)
{
return text;
}
else
{
return text.Substring(0, position) + text.Substring(position + 1);
}
}
/// <summary>
/// Called when the <see cref="Text"/> property changes.
/// </summary>
/// <param name="text">The new text.</param>
private void TextChanged(string text)
{
var key = (char)0;
if (text != null)
{
int underscore = text.IndexOf('_');
if (underscore != -1 && underscore < text.Length - 1)
{
key = text[underscore + 1];
}
}
this.AccessKey = key;
if (this.accessKeys != null && this.AccessKey != 0)
{
this.accessKeys.Register(this.AccessKey, this);
}
}
}
}

73
Perspex.Controls/Primitives/AdornerLayer.cs

@ -6,6 +6,7 @@
namespace Perspex.Controls.Primitives
{
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.VisualTree;
@ -16,8 +17,14 @@ namespace Perspex.Controls.Primitives
public static PerspexProperty<Visual> AdornedElementProperty =
PerspexProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement");
private static PerspexProperty<AdornedElementInfo> AdornedElementInfoProperty =
PerspexProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo>("AdornedElementInfo");
private BoundsTracker tracker = new BoundsTracker();
static AdornerLayer()
{
AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
IsHitTestVisibleProperty.OverrideDefaultValue(typeof(AdornerLayer), false);
}
@ -45,25 +52,28 @@ namespace Perspex.Controls.Primitives
foreach (var child in this.Children)
{
var adorned = GetAdornedElement(child);
var info = child.GetValue(AdornedElementInfoProperty);
if (adorned != null)
if (info != null)
{
var transform = adorned.TransformToVisual(parent);
var position = new Point(0, 0) * transform;
child.Arrange(new Rect(position, adorned.Bounds.Size));
child.Arrange(info.Bounds.Bounds);
}
else
{
child.Arrange(new Rect(child.DesiredSize.Value));
child.Arrange(new Rect(child.DesiredSize));
}
}
return finalSize;
}
protected override void OnChildrenAdded(IEnumerable<Control> child)
protected override void OnChildrenAdded(IEnumerable<Control> children)
{
foreach (var i in children)
{
this.UpdateAdornedElement(i, i.GetValue(AdornedElementProperty));
}
this.InvalidateArrange();
}
@ -71,5 +81,54 @@ namespace Perspex.Controls.Primitives
{
this.InvalidateArrange();
}
private void UpdateAdornedElement(Visual adorner, Visual adorned)
{
var info = adorner.GetValue(AdornedElementInfoProperty);
if (info != null)
{
info.Subscription.Dispose();
if (adorned == null)
{
adorner.ClearValue(AdornedElementInfoProperty);
}
}
if (adorned != null)
{
if (info == null)
{
info = new AdornedElementInfo();
adorner.SetValue(AdornedElementInfoProperty, info);
}
info.Subscription = this.tracker.Track(adorned).Subscribe(x =>
{
info.Bounds = x;
this.InvalidateArrange();
});
}
}
private static void AdornedElementChanged(PerspexPropertyChangedEventArgs e)
{
var adorner = (Visual)e.Sender;
var adorned = (Visual)e.NewValue;
var layer = adorner.GetVisualParent<AdornerLayer>();
if (layer != null)
{
layer.UpdateAdornedElement(adorner, adorned);
}
}
private class AdornedElementInfo
{
public IDisposable Subscription { get; set; }
public TransformedBounds Bounds { get; set; }
}
}
}

13
Perspex.Controls/Primitives/ScrollBar.cs

@ -9,6 +9,7 @@ namespace Perspex.Controls.Primitives
using System;
using System.Reactive;
using System.Reactive.Linq;
using Perspex.Controls.Templates;
public class ScrollBar : TemplatedControl
{
@ -88,6 +89,18 @@ namespace Perspex.Controls.Primitives
return base.MeasureOverride(availableSize);
}
protected override void OnTemplateApplied()
{
base.OnTemplateApplied();
// Binding between this.Value and track.Value must be done explicitly like this rather
// than using standard bindings as it shouldn't be able to to be overridden by binding
// e.g. ScrollBar.Value.
var track = this.GetTemplateChild<Track>("track");
track.GetObservable(ValueProperty).Subscribe(x => this.Value = x);
this.GetObservable(ValueProperty).Subscribe(x => track.Value = x);
}
private bool CalculateIsVisible()
{
switch (this.Visibility)

350
Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -7,112 +7,219 @@
namespace Perspex.Controls.Primitives
{
using System;
using System.Collections;
using System.Linq;
using System.Collections;
using System.Collections.Specialized;
using Perspex.Controls.Utils;
using Perspex.Input;
using Perspex.VisualTree;
using Perspex.Interactivity;
public class SelectingItemsControl : ItemsControl
/// <summary>
/// An <see cref="ItemsControl"/> that maintains a selection.
/// </summary>
/// <remarks>
/// TODO: Support multiple selection.
/// </remarks>
public abstract class SelectingItemsControl : ItemsControl
{
/// <summary>
/// Defines the <see cref="SelectedIndex"/> property.
/// </summary>
public static readonly PerspexProperty<int> SelectedIndexProperty =
PerspexProperty.Register<SelectingItemsControl, int>(
nameof(SelectedIndex),
defaultValue: -1,
coerce: CoerceSelectedIndex);
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
/// </summary>
public static readonly PerspexProperty<object> SelectedItemProperty =
PerspexProperty.Register<SelectingItemsControl, object>("SelectedItem");
PerspexProperty.Register<SelectingItemsControl, object>(
nameof(SelectedItem),
coerce: CoerceSelectedItem);
/// <summary>
/// Event that should be raised by items that implement <see cref="ISelectable"/> to
/// notify the parent <see cref="SelectingItemsControl"/> that their selection state
/// has changed.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> IsSelectedChangedEvent =
RoutedEvent.Register<SelectingItemsControl, RoutedEventArgs>("IsSelectedChanged", RoutingStrategies.Bubble);
/// <summary>
/// Initializes static members of the <see cref="SelectingItemsControl"/> class.
/// </summary>
static SelectingItemsControl()
{
FocusableProperty.OverrideDefaultValue(typeof(SelectingItemsControl), true);
SelectedItemProperty.Changed.Subscribe(x =>
{
var control = x.Sender as SelectingItemsControl;
IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ItemIsSelectedChanged);
SelectedIndexProperty.Changed.Subscribe(SelectedIndexChanged);
SelectedItemProperty.Changed.Subscribe(SelectedItemChanged);
}
if (control != null)
{
control.SelectedItemChanged(x.NewValue);
}
});
/// <summary>
/// Gets or sets the index of the selected item.
/// </summary>
public int SelectedIndex
{
get { return this.GetValue(SelectedIndexProperty); }
set { this.SetValue(SelectedIndexProperty, value); }
}
/// <summary>
/// Gets or sets the selected item.
/// </summary>
public object SelectedItem
{
get { return this.GetValue(SelectedItemProperty); }
set { this.SetValue(SelectedItemProperty, value); }
}
protected static int GetIndexOfItem(IEnumerable items, object item)
/// <summary>
/// Called when the <see cref="Items"/> property changes.
/// </summary>
/// <param name="oldValue">The old value of the property.</param>
/// <param name="newValue">The new value of the property.</param>
protected override void ItemsChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (items != null)
{
int index = 0;
base.ItemsChanged(oldValue, newValue);
var selected = this.SelectedItem;
foreach (var i in items)
if (selected != null)
{
if (newValue == null || !newValue.Contains(selected))
{
if (object.ReferenceEquals(i, item))
this.SelectedItem = null;
}
}
}
/// <summary>
/// Called when a <see cref="INotifyCollectionChanged.CollectionChanged"/> event is raised
/// on <see cref="Items"/>.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
protected override void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.ItemsCollectionChanged(sender, e);
var selected = this.SelectedItem;
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset:
if (e.OldItems.Contains(selected))
{
return index;
this.SelectedItem = null;
}
++index;
}
break;
case NotifyCollectionChangedAction.Move:
this.SelectedItem = this.Items.IndexOf(selected);
break;
}
return -1;
}
protected int GetIndexOfItem(object item)
/// <summary>
/// Called when the selection on a child item changes.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void ItemIsSelectedChanged(RoutedEventArgs e)
{
return GetIndexOfItem(this.Items, item);
var selectable = e.Source as ISelectable;
if (selectable != null && selectable != this && selectable.IsSelected)
{
var container = this.ItemContainerGenerator.GetItemForContainer((Control)selectable);
if (container != null)
{
this.SelectedItem = container;
e.Handled = true;
}
}
}
/// <summary>
/// Moves the selection in the specified direction.
/// </summary>
/// <param name="direction">The direction.</param>
protected virtual void MoveSelection(FocusNavigationDirection direction)
{
if (this.SelectedItem != null)
var panel = this.Presenter?.Panel as INavigablePanel;
var selected = this.SelectedItem;
var container = selected != null ?
this.ItemContainerGenerator.GetContainerForItem(selected) :
null;
if (panel != null)
{
int offset = 0;
var next = panel.GetControl(direction, container);
switch (direction)
if (next != null)
{
case FocusNavigationDirection.Up:
offset = -1;
this.SelectedItem = this.ItemContainerGenerator.GetItemForContainer(next);
}
}
else
{
// TODO: Try doing a visual search?
}
}
/// <summary>
/// Called when a key is pressed within the control.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!e.Handled)
{
switch (e.Key)
{
case Key.Up:
this.MoveSelection(FocusNavigationDirection.Up);
break;
case Key.Down:
this.MoveSelection(FocusNavigationDirection.Down);
break;
case FocusNavigationDirection.Down:
offset = 1;
case Key.Left:
this.MoveSelection(FocusNavigationDirection.Left);
break;
case Key.Right:
this.MoveSelection(FocusNavigationDirection.Right);
break;
default:
return;
}
if (offset != 0)
var selected = this.SelectedItem;
if (selected != null)
{
var currentIndex = GetIndexOfItem(this.SelectedItem);
var index = currentIndex + offset;
var container = this.ItemContainerGenerator.GetContainerForItem(selected);
if (index >= 0 && index < this.Items.Cast<object>().Count())
if (container != null)
{
this.SelectedItem = this.Items.Cast<object>().ElementAt(index);
container.BringIntoView();
FocusManager.Instance.Focus(container, true);
}
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
this.MoveSelection(FocusNavigationDirection.Up);
break;
case Key.Down:
this.MoveSelection(FocusNavigationDirection.Down);
break;
case Key.Left:
this.MoveSelection(FocusNavigationDirection.Left);
break;
case Key.Right:
this.MoveSelection(FocusNavigationDirection.Right);
break;
e.Handled = true;
}
e.Handled = true;
}
/// <summary>
/// Called when the pointer is pressed within the control.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerPressed(PointerPressEventArgs e)
{
IVisual source = (IVisual)e.Source;
@ -128,12 +235,120 @@ namespace Perspex.Controls.Primitives
if (item != null)
{
this.SelectedItem = item;
selectable.BringIntoView();
FocusManager.Instance.Focus(selectable);
}
}
e.Handled = true;
}
/// <summary>
/// Called when the control's template has been applied.
/// </summary>
protected override void OnTemplateApplied()
{
base.OnTemplateApplied();
this.SelectedItemChanged(this.SelectedItem);
}
/// <summary>
/// Provides coercion for the <see cref="SelectedIndex"/> property.
/// </summary>
/// <param name="o">The object on which the property has changed.</param>
/// <param name="value">The proposed value.</param>
/// <returns>The coerced value.</returns>
private static int CoerceSelectedIndex(PerspexObject o, int value)
{
var control = o as SelectingItemsControl;
if (control != null)
{
if (value < -1)
{
return -1;
}
else if (value > -1)
{
var items = control.Items;
if (items != null)
{
var count = items.Count();
return Math.Min(value, count - 1);
}
else
{
return -1;
}
}
}
return value;
}
/// <summary>
/// Provides coercion for the <see cref="SelectedItem"/> property.
/// </summary>
/// <param name="o">The object on which the property has changed.</param>
/// <param name="value">The proposed value.</param>
/// <returns>The coerced value.</returns>
private static object CoerceSelectedItem(PerspexObject o, object value)
{
var control = o as SelectingItemsControl;
if (control != null)
{
if (value != null && (control.Items == null || control.Items.IndexOf(value) == -1))
{
return null;
}
}
return value;
}
/// <summary>
/// Called when the <see cref="SelectedIndex"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
{
var control = e.Sender as SelectingItemsControl;
if (control != null)
{
var index = (int)e.NewValue;
if (index == -1)
{
control.SelectedItem = null;
}
else
{
control.SelectedItem = control.Items.ElementAt((int)e.NewValue);
}
}
}
/// <summary>
/// Called when the <see cref="SelectedItem"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
private static void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
{
var control = e.Sender as SelectingItemsControl;
if (control != null)
{
control.SelectedItemChanged(e.NewValue);
}
}
/// <summary>
/// Called when the <see cref="SelectedItem"/> property changes.
/// </summary>
/// <param name="selected">The new selected item.</param>
private void SelectedItemChanged(object selected)
{
var containers = this.ItemContainerGenerator.GetAll()
@ -143,10 +358,29 @@ namespace Perspex.Controls.Primitives
this.ItemContainerGenerator.GetContainerForItem(selected) :
null;
if (this.Presenter != null && this.Presenter.Panel != null)
{
KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer);
}
foreach (var item in containers)
{
item.IsSelected = item == selectedContainer;
}
if (selected == null)
{
this.SelectedIndex = -1;
}
else
{
var items = this.Items;
if (items != null)
{
this.SelectedIndex = items.IndexOf(selected);
}
}
}
}
}

3
Perspex.Controls/Primitives/TabStrip.cs

@ -27,7 +27,8 @@ namespace Perspex.Controls.Primitives
public TabStrip()
{
this.BindTwoWay(SelectedTabProperty, this, SelectingItemsControl.SelectedItemProperty);
this.GetObservable(SelectedItemProperty).Subscribe(x => this.SelectedTab = x as TabItem);
this.GetObservable(SelectedTabProperty).Subscribe(x => this.SelectedItem = x as TabItem);
}
public TabItem SelectedTab

22
Perspex.Controls/Primitives/TemplatedControl.cs

@ -13,7 +13,8 @@ namespace Perspex.Controls.Primitives
using Perspex.Media;
using Perspex.Styling;
using Perspex.VisualTree;
using Splat;
using Serilog;
using Serilog.Core.Enrichers;
public class TemplatedControl : Control, ITemplatedControl
{
@ -46,6 +47,8 @@ namespace Perspex.Controls.Primitives
private bool templateApplied;
private ILogger templateLog;
static TemplatedControl()
{
TemplateProperty.Changed.Subscribe(e =>
@ -56,6 +59,16 @@ namespace Perspex.Controls.Primitives
});
}
public TemplatedControl()
{
this.templateLog = Log.ForContext(new[]
{
new PropertyEnricher("Area", "Template"),
new PropertyEnricher("SourceContext", this.GetType()),
new PropertyEnricher("Id", this.GetHashCode()),
});
}
public Brush Background
{
get { return this.GetValue(BackgroundProperty); }
@ -122,10 +135,7 @@ namespace Perspex.Controls.Primitives
if (this.Template != null)
{
this.Log().Debug(
"Creating template for {0} (#{1:x8})",
this.GetType().Name,
this.GetHashCode());
this.templateLog.Verbose("Creating control template");
var child = this.Template.Build(this);
this.SetTemplatedParent(child);
@ -166,7 +176,7 @@ namespace Perspex.Controls.Primitives
if (child != null)
{
child.Measure(availableSize);
return child.DesiredSize.Value;
return child.DesiredSize;
}
return new Size();

4
Perspex.Controls/Primitives/Track.cs

@ -103,11 +103,11 @@ namespace Perspex.Controls.Primitives
if (this.Orientation == Orientation.Horizontal)
{
return new Size(0, thumb.DesiredSize.Value.Height);
return new Size(0, thumb.DesiredSize.Height);
}
else
{
return new Size(thumb.DesiredSize.Value.Width, 0);
return new Size(thumb.DesiredSize.Width, 0);
}
}

10
Perspex.Controls/RowDefinitions.cs

@ -6,9 +6,19 @@
namespace Perspex.Controls
{
using System.Linq;
using Perspex.Collections;
using Perspex.Controls.Parsers;
public class RowDefinitions : PerspexList<RowDefinition>
{
public RowDefinitions()
{
}
public RowDefinitions(string s)
{
this.AddRange(GridLengthsParser.Parse(s).Select(x => new RowDefinition(x)));
}
}
}

3
Perspex.Controls/Shapes/Ellipse.cs

@ -20,8 +20,7 @@ namespace Perspex.Controls.Shapes
{
if (this.geometry == null || this.geometrySize != this.Bounds.Size)
{
var rect = new Rect(0, 0, this.Bounds.Size.Width, this.Bounds.Size.Height);
rect = rect.Deflate(this.StrokeThickness / 2);
var rect = new Rect(this.Bounds.Size).Deflate(this.StrokeThickness);
this.geometry = new EllipseGeometry(rect);
this.geometrySize = this.Bounds.Size;
}

3
Perspex.Controls/Shapes/Rectangle.cs

@ -20,7 +20,8 @@ namespace Perspex.Controls.Shapes
{
if (this.geometry == null || this.geometrySize != this.Bounds.Size)
{
this.geometry = new RectangleGeometry(new Rect(0, 0, this.Bounds.Size.Width, this.Bounds.Size.Height));
var rect = new Rect(this.Bounds.Size).Deflate(this.StrokeThickness);
this.geometry = new RectangleGeometry(rect);
this.geometrySize = this.Bounds.Size;
}

55
Perspex.Controls/StackPanel.cs

@ -7,10 +7,8 @@
namespace Perspex.Controls
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Perspex.Input;
public enum Orientation
{
@ -18,7 +16,7 @@ namespace Perspex.Controls
Horizontal,
}
public class StackPanel : Panel
public class StackPanel : Panel, INavigablePanel
{
public static readonly PerspexProperty<double> GapProperty =
PerspexProperty.Register<StackPanel, double>("Gap");
@ -38,6 +36,49 @@ namespace Perspex.Controls
set { this.SetValue(OrientationProperty, value); }
}
Control INavigablePanel.GetControl(FocusNavigationDirection direction, Control from)
{
var horiz = this.Orientation == Orientation.Horizontal;
int index = this.Children.IndexOf(from);
switch (direction)
{
case FocusNavigationDirection.First:
index = 0;
break;
case FocusNavigationDirection.Last:
index = this.Children.Count - 1;
break;
case FocusNavigationDirection.Next:
++index;
break;
case FocusNavigationDirection.Previous:
++index;
break;
case FocusNavigationDirection.Left:
index = horiz ? index - 1 : -1;
break;
case FocusNavigationDirection.Right:
index = horiz ? index + 1 : -1;
break;
case FocusNavigationDirection.Up:
index = horiz ? -1 : index - 1;
break;
case FocusNavigationDirection.Down:
index = horiz ? -1 : index + 1;
break;
}
if (index >= 0 && index < this.Children.Count)
{
return this.Children[index];
}
else
{
return null;
}
}
protected override Size MeasureOverride(Size availableSize)
{
double childAvailableWidth = double.PositiveInfinity;
@ -75,7 +116,7 @@ namespace Perspex.Controls
foreach (Control child in this.Children)
{
child.Measure(new Size(childAvailableWidth, childAvailableHeight));
Size size = child.DesiredSize.Value;
Size size = child.DesiredSize;
if (Orientation == Orientation.Vertical)
{
@ -109,8 +150,8 @@ namespace Perspex.Controls
foreach (Control child in this.Children)
{
double childWidth = child.DesiredSize.Value.Width;
double childHeight = child.DesiredSize.Value.Height;
double childWidth = child.DesiredSize.Width;
double childHeight = child.DesiredSize.Height;
if (Orientation == Orientation.Vertical)
{

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save