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) public static Pango.Weight ToCairo(this Perspex.Media.FontWeight weight)
{ {
if (weight == Perspex.Media.FontWeight.Light) if (weight == Perspex.Media.FontWeight.Light)
{
return Pango.Weight.Light; return Pango.Weight.Light;
}
if (weight == Perspex.Media.FontWeight.Normal || weight == Perspex.Media.FontWeight.Regular) if (weight == Perspex.Media.FontWeight.Normal || weight == Perspex.Media.FontWeight.Regular)
{
return Pango.Weight.Normal; return Pango.Weight.Normal;
}
if (weight == Perspex.Media.FontWeight.DemiBold || weight == Perspex.Media.FontWeight.Medium) if (weight == Perspex.Media.FontWeight.DemiBold || weight == Perspex.Media.FontWeight.Medium)
{
return Pango.Weight.Semibold; return Pango.Weight.Semibold;
}
if (weight == Perspex.Media.FontWeight.Bold) if (weight == Perspex.Media.FontWeight.Bold)
{
return Pango.Weight.Bold; return Pango.Weight.Bold;
}
if (weight == Perspex.Media.FontWeight.UltraBold || weight == Perspex.Media.FontWeight.ExtraBold) if (weight == Perspex.Media.FontWeight.UltraBold || weight == Perspex.Media.FontWeight.ExtraBold)
{
return Pango.Weight.Ultrabold; return Pango.Weight.Ultrabold;
}
if (weight == Perspex.Media.FontWeight.Black || weight == Perspex.Media.FontWeight.Heavy || weight == Perspex.Media.FontWeight.UltraBlack) if (weight == Perspex.Media.FontWeight.Black || weight == Perspex.Media.FontWeight.Heavy || weight == Perspex.Media.FontWeight.UltraBlack)
{
return Pango.Weight.Heavy; return Pango.Weight.Heavy;
}
return Pango.Weight.Ultralight; 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, string fontFamily,
double fontSize, double fontSize,
FontStyle fontStyle, FontStyle fontStyle,
TextAlignment textAlignment,
Perspex.Media.FontWeight fontWeight) 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) 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 impl = geometry.PlatformImpl as StreamGeometryImpl;
var clone = new Queue<GeometryOp>(impl.Operations); var clone = new Queue<GeometryOp>(impl.Operations);
bool useFill = false;
var pop = this.PushTransform(impl.Transform);
bool useFill = false;
this.SetPen(pen); 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) if (current is BeginOp)
{ {
var bo = current as BeginOp; var bo = current as BeginOp;
this.context.MoveTo(bo.Point.ToCairo()); this.context.MoveTo(bo.Point.ToCairo());
useFill = bo.IsFilled; useFill = bo.IsFilled;
}
else if (current is LineToOp) System.Diagnostics.Debug.WriteLine("Start");
{ }
var lto = current as LineToOp; else if (current is LineToOp)
this.context.LineTo(lto.Point.ToCairo()); {
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.FillPreserve();
this.context.ClosePath();
} }
else if (current is CurveToOp) else
{ {
var cto = current as CurveToOp; this.context.StrokePreserve();
this.context.CurveTo(cto.Point.ToCairo(), cto.Point2.ToCairo(), cto.Point3.ToCairo());
} }
} }
if (useFill)
{
this.context.StrokePreserve();
this.context.Fill();
}
else
{
this.context.Stroke();
}
pop.Dispose();
} }
/// <summary> /// <summary>
@ -220,6 +224,17 @@ namespace Perspex.Cairo.Media
return Disposable.Create(() => this.context.ResetClip()); 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> /// <summary>
/// Pushes a matrix transformation. /// Pushes a matrix transformation.
/// </summary> /// </summary>
@ -253,6 +268,7 @@ namespace Perspex.Cairo.Media
{ {
this.SetBrush(pen.Brush); this.SetBrush(pen.Brush);
this.context.LineWidth = pen.Thickness; this.context.LineWidth = pen.Thickness;
} }
} }
} }

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

@ -20,6 +20,7 @@ namespace Perspex.Cairo.Media
string fontFamily, string fontFamily,
double fontSize, double fontSize,
FontStyle fontStyle, FontStyle fontStyle,
TextAlignment textAlignment,
FontWeight fontWeight) FontWeight fontWeight)
{ {
var context = Locator.Current.GetService<Pango.Context>(); var context = Locator.Current.GetService<Pango.Context>();
@ -32,6 +33,8 @@ namespace Perspex.Cairo.Media
Style = (Pango.Style)fontStyle, Style = (Pango.Style)fontStyle,
Weight = fontWeight.ToCairo() Weight = fontWeight.ToCairo()
}; };
this.Layout.Alignment = textAlignment.ToCairo();
} }
public Size Constraint public Size Constraint
@ -98,6 +101,12 @@ namespace Perspex.Cairo.Media
int width; int width;
int height; int height;
this.Layout.GetPixelSize(out width, out 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); 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 class RenderTargetBitmapImpl : BitmapImpl, IRenderTargetBitmapImpl
{ {
public RenderTargetBitmapImpl( public RenderTargetBitmapImpl(
Cairo.ImageSurface surface) Cairo.ImageSurface surface)
: base(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) public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{ {
// TODO: Implement
int i = 10;
points.Add(point); points.Add(point);
} }
public void BeginFigure(Point startPoint, bool isFilled) 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 }); this.impl.Operations.Enqueue(new BeginOp { Point = startPoint, IsFilled = isFilled });
points.Add(startPoint); points.Add(startPoint);
} }
public void BezierTo(Point point1, Point point2, Point point3) public void BezierTo(Point point1, Point point2, Point point3)
{ {
// TODO: Implement
this.impl.Operations.Enqueue(new CurveToOp { Point = point1, Point2 = point2, Point3 = point3 }); this.impl.Operations.Enqueue(new CurveToOp { Point = point1, Point2 = point2, Point3 = point3 });
points.Add(point1); points.Add(point1);
points.Add(point2); points.Add(point2);
@ -63,6 +59,7 @@ namespace Perspex.Cairo.Media
maxX = System.Math.Max(p.X, maxX); maxX = System.Math.Max(p.X, maxX);
maxY = System.Math.Max(p.Y, maxY); maxY = System.Math.Max(p.Y, maxY);
} }
var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, (int)maxX, (int)maxY)); var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, (int)maxX, (int)maxY));
var clone = new Queue<GeometryOp>(this.impl.Operations); var clone = new Queue<GeometryOp>(this.impl.Operations);
@ -95,6 +92,7 @@ namespace Perspex.Cairo.Media
} }
context.StrokePreserve(); context.StrokePreserve();
context.FillPreserve();
} }
var test = context.StrokeExtents(); var test = context.StrokeExtents();

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

@ -11,7 +11,8 @@
<AssemblyName>Perspex.Cairo</AssemblyName> <AssemblyName>Perspex.Cairo</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment> <FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>b122e795</NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -43,9 +44,9 @@
<Reference Include="pango-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f, processorArchitecture=MSIL"> <Reference Include="pango-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f, processorArchitecture=MSIL">
<Package>gtk-sharp-2.0</Package> <Package>gtk-sharp-2.0</Package>
</Reference> </Reference>
<Reference Include="Splat, Version=1.6.1.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
<HintPath>..\..\packages\Splat.1.6.1\lib\Net45\Splat.dll</HintPath> <Private>True</Private>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@ -95,13 +96,6 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <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. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">

5
Cairo/Perspex.Cairo/Renderer.cs

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

7
Cairo/Perspex.Cairo/packages.config

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Rx-Core" version="2.2.5" targetFramework="net45" /> <package id="Rx-Core" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" /> <package id="Rx-Interfaces" version="2.2.5" targetFramework="net45" userInstalled="true" />
<package id="Splat" version="1.6.1" targetFramework="net45" /> <package id="Splat" version="1.6.2" targetFramework="net45" userInstalled="true" />
<package id="StyleCop.MSBuild" version="4.7.49.0" targetFramework="net45" developmentDependency="true" />
</packages> </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; } 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; } get { return instance; }
} }
@ -31,10 +31,5 @@ namespace Perspex.Gtk
{ {
this.clientPosition = p; 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> <RootNamespace>Perspex.Gtk</RootNamespace>
<AssemblyName>Perspex.Gtk</AssemblyName> <AssemblyName>Perspex.Gtk</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>ad245ae8</NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -29,9 +30,9 @@
<ConsolePause>false</ConsolePause> <ConsolePause>false</ConsolePause>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Splat, Version=1.6.1.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath>
<HintPath>..\..\packages\Splat.1.6.1\lib\Net45\Splat.dll</HintPath> <Private>True</Private>
</Reference> </Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f"> <Reference Include="gtk-sharp, Version=2.12.0.0, Culture=neutral, PublicKeyToken=35e10195dab3c99f">
@ -103,11 +104,4 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<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> </Project>

9
Gtk/Perspex.Gtk/packages.config

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

14
Perspex-Mono.sln

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14 # Visual Studio 14
VisualStudioVersion = 14.0.22609.0 VisualStudioVersion = 14.0.22823.1
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Base", "Perspex.Base\Perspex.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Base", "Perspex.Base\Perspex.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}"
EndProject EndProject
@ -53,7 +53,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{596AF7
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{D211E587-D8BC-45B9-95A4-F297C8FA5200}.Release|Any CPU.Build.0 = 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 {E106CF37-4066-4615-B684-172A6D30B058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Debug|Any CPU.Build.0 = Debug|Any CPU {E106CF37-4066-4615-B684-172A6D30B058}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DABFD304-D6A4-4752-8123-C2CCF7AC7831}.Release|Any CPU.ActiveCfg = Release|Any CPU {E106CF37-4066-4615-B684-172A6D30B058}.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}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -158,6 +158,6 @@ Global
{2905FF23-53FB-45E6-AA49-6AF47A172056} = {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} {415E048E-4611-4815-9CF2-D774E29079AC} = {2BAFBE53-7FA4-4BB9-976F-9AFCC4F9847D}
{DB070A10-BF39-4752-8456-86E9D5928478} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {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 EndGlobalSection
EndGlobal 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.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
/// <summary>
/// Base class for control which can have property transitions.
/// </summary>
public class Animatable : PerspexObject public class Animatable : PerspexObject
{ {
/// <summary>
/// The property transitions for the control.
/// </summary>
private PropertyTransitions propertyTransitions; 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 public PropertyTransitions PropertyTransitions
{ {
get 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) protected override void OnPropertyChanged(PerspexPropertyChangedEventArgs e)
{ {
if (e.Priority != BindingPriority.Animation && this.propertyTransitions != null) 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); private static readonly TimeSpan Tick = TimeSpan.FromSeconds(1.0 / FramesPerSecond);
/// <summary> /// <summary>
/// Initializes the static class. /// Initializes static members of the <see cref="Animate"/> class.
/// </summary> /// </summary>
static Animate() static Animate()
{ {
@ -43,6 +43,9 @@ namespace Perspex.Animation
/// <summary> /// <summary>
/// The stopwatch used to track time. /// The stopwatch used to track time.
/// </summary> /// </summary>
/// <value>
/// The stopwatch used to track time.
/// </value>
public static Stopwatch Stopwatch public static Stopwatch Stopwatch
{ {
get; get;
@ -53,10 +56,13 @@ namespace Perspex.Animation
/// Gets the animation timer. /// Gets the animation timer.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The animation timer ticks <see cref="FramesPerSecond"/> times per second. The /// 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 /// parameter passed to a subsciber is the time span since the animation system was
/// initialized. /// initialized.
/// </remarks> /// </remarks>
/// <value>
/// The animation timer.
/// </value>
public static IObservable<TimeSpan> Timer public static IObservable<TimeSpan> Timer
{ {
get; get;
@ -78,7 +84,7 @@ namespace Perspex.Animation
public static IObservable<double> GetTimer(TimeSpan duration) public static IObservable<double> GetTimer(TimeSpan duration)
{ {
var startTime = Stopwatch.Elapsed.Ticks; var startTime = Stopwatch.Elapsed.Ticks;
var endTime = (startTime + duration.Ticks); var endTime = startTime + duration.Ticks;
return Timer return Timer
.TakeWhile(x => x.Ticks < endTime) .TakeWhile(x => x.Ticks < endTime)
.Select(x => (x.Ticks - startTime) / (double)duration.Ticks) .Select(x => (x.Ticks - startTime) / (double)duration.Ticks)
@ -89,15 +95,14 @@ namespace Perspex.Animation
/// <summary> /// <summary>
/// Animates a <see cref="PerspexProperty"/>. /// Animates a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>
/// <typeparam name="T">The property type.</typeparam>
/// <param name="target">The target object.</param> /// <param name="target">The target object.</param>
/// <param name="property">The target property.</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="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="finish">The value of the property at the end of the animation.</param>
/// <param name="easing">The easing function to use.</param> /// <param name="easing">The easing function to use.</param>
/// <param name="duration">The duration of the animation.</param> /// <param name="duration">The duration of the animation.</param>
/// <returns>An <see cref="IDisposable"/> that can be used to stop the animation.</returns> /// <returns>An <see cref="Animation"/> that can be used to track or stop the animation.</returns>
public static IDisposable Property( public static Animation Property(
PerspexObject target, PerspexObject target,
PerspexProperty property, PerspexProperty property,
object start, object start,
@ -106,7 +111,7 @@ namespace Perspex.Animation
TimeSpan duration) TimeSpan duration)
{ {
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish)); 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> /// <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="finish">The value of the property at the end of the animation.</param>
/// <param name="easing">The easing function to use.</param> /// <param name="easing">The easing function to use.</param>
/// <param name="duration">The duration of the animation.</param> /// <param name="duration">The duration of the animation.</param>
/// <returns>An <see cref="IDisposable"/> that can be used to stop the animation.</returns> /// <returns>An <see cref="Animation"/> that can be used to track or stop the animation.</returns>
public static IDisposable Property<T>( public static Animation<T> Property<T>(
PerspexObject target, PerspexObject target,
PerspexProperty<T> property, PerspexProperty<T> property,
T start, T start,
T finish, T finish,
IEasing<T> easing, IEasing<T> easing,
TimeSpan duration) TimeSpan duration)
{ {
var o = GetTimer(duration).Select(progress => easing.Ease(progress, start, finish)); 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -8,21 +8,41 @@ namespace Perspex.Animation
{ {
using System; using System;
/// <summary>
/// Defines animation extension methods.
/// </summary>
public static class AnimationExtensions 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) public static PropertyTransition Transition<T>(this PerspexProperty<T> property, int milliseconds)
{ {
return Transition(property, TimeSpan.FromMilliseconds(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) public static PropertyTransition Transition<T>(this PerspexProperty<T> property, TimeSpan duration)
{ {
return new PropertyTransition return new PropertyTransition(property, duration, LinearEasing.For<T>());
{
Property = property,
Duration = duration,
Easing = 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
namespace Perspex.Animation namespace Perspex.Animation
{ {
/// <summary>
/// Defines the interface for easing functions.
/// </summary>
public interface IEasing 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); 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
namespace Perspex.Animation namespace Perspex.Animation
{ {
using System; /// <summary>
/// Linearly eases a double value.
/// </summary>
public class LinearDoubleEasing : IEasing<double> 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) public double Ease(double progress, double start, double finish)
{ {
return ((finish - start) * progress) + start; 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); 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -8,13 +8,25 @@ namespace Perspex.Animation
{ {
using System; 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 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)) if (typeof(T) == typeof(double))
{ {
return new LinearDoubleEasing(); return (IEasing<T>)new LinearDoubleEasing();
} }
else else
{ {

6
Perspex.Animation/Perspex.Animation.csproj

@ -41,9 +41,12 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Animation`1.cs" />
<Compile Include="Animation.cs" />
<Compile Include="Animate.cs" /> <Compile Include="Animate.cs" />
<Compile Include="Animatable.cs" /> <Compile Include="Animatable.cs" />
<Compile Include="AnimationExtensions.cs" /> <Compile Include="AnimationExtensions.cs" />
<Compile Include="IEasing`1.cs" />
<Compile Include="LinearEasing.cs" /> <Compile Include="LinearEasing.cs" />
<Compile Include="LinearDoubleEasing.cs" /> <Compile Include="LinearDoubleEasing.cs" />
<Compile Include="IEasing.cs" /> <Compile Include="IEasing.cs" />
@ -68,6 +71,9 @@
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </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="$(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. <!-- 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. 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.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; 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 // set of attributes. Change these attribute values to modify the information
// associated with an assembly. // associated with an assembly.
[assembly: AssemblyTitle("Perspex.Animation")] [assembly: AssemblyTitle("Perspex.Animation")]
@ -19,11 +25,11 @@ using System.Runtime.InteropServices;
// Version information for an assembly consists of the following four values: // Version information for an assembly consists of the following four values:
// //
// Major Version // Major Version
// Minor Version // Minor Version
// Build Number // Build Number
// Revision // 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: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyVersion("1.0.0.0")]

40
Perspex.Animation/PropertyTransition.cs

@ -8,12 +8,46 @@ namespace Perspex.Animation
{ {
using System; using System;
/// <summary>
/// Defines how a property should be animated using a transition.
/// </summary>
public class PropertyTransition 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> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
using Perspex.Collections;
namespace Perspex.Animation namespace Perspex.Animation
{ {
using Perspex.Collections; using Perspex.Collections;
/// <summary>
/// A collection of <see cref="PropertyTransition"/> definitions.
/// </summary>
public class PropertyTransitions : PerspexList<PropertyTransition> public class PropertyTransitions : PerspexList<PropertyTransition>
{ {
} }

11
Perspex.Animation/packages.config

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Rx-Core" 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+win" /> <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+win" /> <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+win" /> <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+win" /> <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> </packages>

75
Perspex.Application/Application.cs

@ -16,12 +16,36 @@ namespace Perspex
using Perspex.Threading; using Perspex.Threading;
using Splat; 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 public class Application : IGlobalDataTemplates, IGlobalStyles
{ {
/// <summary>
/// The application-global data templates.
/// </summary>
private DataTemplates dataTemplates; private DataTemplates dataTemplates;
/// <summary>
/// The styler that will be used to apply styles to controls.
/// </summary>
private Styler styler = new Styler(); 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) public Application(Styles theme)
{ {
if (Current != null) if (Current != null)
@ -34,12 +58,24 @@ namespace Perspex
this.RegisterServices(); 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 public static Application Current
{ {
get; get;
private set; private set;
} }
/// <summary>
/// Gets or sets the application's global data templates.
/// </summary>
/// <value>
/// The application's global data templates.
/// </value>
public DataTemplates DataTemplates public DataTemplates DataTemplates
{ {
get 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 public IFocusManager FocusManager
{ {
get; get;
private set; private set;
} }
/// <summary>
/// Gets the application's input manager.
/// </summary>
/// <value>
/// The application's input manager.
/// </value>
public InputManager InputManager public InputManager InputManager
{ {
get; get;
private set; private set;
} }
public KeyboardNavigation KeyboardNavigation /// <summary>
{ /// Gets the application's global styles.
get; /// </summary>
private set; /// <value>
} /// The application's global styles.
/// </value>
/// <remarks>
/// Global styles apply to all windows in the application.
/// </remarks>
public Styles Styles public Styles Styles
{ {
get; get;
private set; 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) public void Run(ICloseable closable)
{ {
var source = new CancellationTokenSource(); var source = new CancellationTokenSource();
@ -89,19 +144,21 @@ namespace Perspex
Dispatcher.UIThread.MainLoop(source.Token); Dispatcher.UIThread.MainLoop(source.Token);
} }
/// <summary>
/// Register's the services needed by Perspex.
/// </summary>
protected virtual void RegisterServices() protected virtual void RegisterServices()
{ {
this.FocusManager = new FocusManager(); this.FocusManager = new FocusManager();
this.InputManager = new InputManager(); 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(IGlobalDataTemplates));
Locator.CurrentMutable.Register(() => this, typeof(IGlobalStyles)); Locator.CurrentMutable.Register(() => this, typeof(IGlobalStyles));
Locator.CurrentMutable.Register(() => this.FocusManager, typeof(IFocusManager)); Locator.CurrentMutable.Register(() => this.FocusManager, typeof(IFocusManager));
Locator.CurrentMutable.Register(() => this.InputManager, typeof(IInputManager)); 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(() => this.styler, typeof(IStyler));
Locator.CurrentMutable.Register(() => new LayoutManager(), typeof(ILayoutManager)); Locator.CurrentMutable.Register(() => new LayoutManager(), typeof(ILayoutManager));
Locator.CurrentMutable.Register(() => new RenderManager(), typeof(IRenderManager)); 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> <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile> <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>088bcbce</NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -70,8 +71,9 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Splat"> <Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Splat.1.6.1\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath> <HintPath>..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Private>True</Private>
</Reference> </Reference>
<Reference Include="System.Reactive.Core"> <Reference Include="System.Reactive.Core">
<HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath> <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="app.config" />
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </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="$(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. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">

6
Perspex.Application/Properties/AssemblyInfo.cs

@ -7,7 +7,7 @@
using System.Reflection; using System.Reflection;
using System.Resources; 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 // set of attributes. Change these attribute values to modify the information
// associated with an assembly. // associated with an assembly.
[assembly: AssemblyTitle("Perspex.Application")] [assembly: AssemblyTitle("Perspex.Application")]
@ -23,11 +23,11 @@ using System.Resources;
// Version information for an assembly consists of the following four values: // Version information for an assembly consists of the following four values:
// //
// Major Version // Major Version
// Minor Version // Minor Version
// Build Number // Build Number
// Revision // 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: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyVersion("1.0.0.0")]

14
Perspex.Application/packages.config

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

16
Perspex.Base/Binding.cs

@ -7,7 +7,7 @@
namespace Perspex namespace Perspex
{ {
using System; using System;
using System.Reactive.Linq; using System.Reactive;
public enum BindingMode public enum BindingMode
{ {
@ -18,7 +18,7 @@ namespace Perspex
OneWayToSource, OneWayToSource,
} }
public class Binding : IObservable<object> public class Binding : ObservableBase<object>, IDescription
{ {
public BindingMode Mode public BindingMode Mode
{ {
@ -44,6 +44,8 @@ namespace Perspex
set; set;
} }
public string Description => string.Format("{0}.{1}", this.Source?.GetType().Name, this.Property.Name);
public static Binding operator !(Binding binding) public static Binding operator !(Binding binding)
{ {
return binding.WithMode(BindingMode.TwoWay); return binding.WithMode(BindingMode.TwoWay);
@ -54,11 +56,6 @@ namespace Perspex
return binding.WithMode(BindingMode.TwoWay); return binding.WithMode(BindingMode.TwoWay);
} }
public IDisposable Subscribe(IObserver<object> observer)
{
return this.Source.GetObservable(this.Property).Subscribe(observer);
}
public Binding WithMode(BindingMode mode) public Binding WithMode(BindingMode mode)
{ {
this.Mode = mode; this.Mode = mode;
@ -70,5 +67,10 @@ namespace Perspex
this.Priority = priority; this.Priority = priority;
return this; 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

2
Perspex.Base/Collections/PerspexList.cs

@ -20,7 +20,7 @@ namespace Perspex.Collections
/// <remarks> /// <remarks>
/// PerspexList is similar to <see cref="System.Collections.ObjectModel.ObservableCollection{T}"/> /// 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 /// 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. /// removed.
/// </remarks> /// </remarks>
public class PerspexList<T> : IPerspexList<T>, INotifyCollectionChanged, INotifyPropertyChanged public class PerspexList<T> : IPerspexList<T>, INotifyCollectionChanged, INotifyPropertyChanged

69
Perspex.Base/Collections/PerspexReadOnlyListView.cs

@ -29,17 +29,17 @@ namespace Perspex.Collections
if (source != null) if (source != null)
{ {
this.source.CollectionChanged += this.SourceCollectionChanged; this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
} }
} }
public T this[int index] public event NotifyCollectionChangedEventHandler CollectionChanged;
{
get { return this.source[index]; } public event PropertyChangedEventHandler PropertyChanged;
}
public int Count public int Count
{ {
get { return this.source.Count; } get { return this.source?.Count ?? 0; }
} }
public IPerspexReadOnlyList<T> Source public IPerspexReadOnlyList<T> Source
@ -54,6 +54,7 @@ namespace Perspex.Collections
if (this.source != null) if (this.source != null)
{ {
this.source.CollectionChanged -= this.SourceCollectionChanged; this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
if (this.CollectionChanged != null) if (this.CollectionChanged != null)
{ {
@ -70,12 +71,13 @@ namespace Perspex.Collections
if (this.source != null) if (this.source != null)
{ {
this.source.CollectionChanged += this.SourceCollectionChanged; this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
if (this.CollectionChanged != null) if (this.CollectionChanged != null)
{ {
var ev = new NotifyCollectionChangedEventArgs( var ev = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, NotifyCollectionChangedAction.Add,
this.source, this.source.ToList(),
0); 0);
this.CollectionChanged(this, ev); this.CollectionChanged(this, ev);
} }
@ -83,13 +85,15 @@ namespace Perspex.Collections
} }
} }
public event NotifyCollectionChangedEventHandler CollectionChanged; public T this[int index]
{
public event PropertyChangedEventHandler PropertyChanged; get { return this.source[index]; }
}
public void Dispose() public void Dispose()
{ {
this.source.CollectionChanged -= this.SourceCollectionChanged; this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
} }
public IEnumerator<T> GetEnumerator() public IEnumerator<T> GetEnumerator()
@ -122,7 +126,7 @@ namespace Perspex.Collections
ev = new NotifyCollectionChangedEventArgs( ev = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Remove,
e.OldItems, e.OldItems,
e.NewStartingIndex); e.OldStartingIndex);
break; break;
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
ev = new NotifyCollectionChangedEventArgs( ev = new NotifyCollectionChangedEventArgs(
@ -139,6 +143,11 @@ namespace Perspex.Collections
this.CollectionChanged(this, ev); this.CollectionChanged(this, ev);
} }
} }
private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e)
{
this.PropertyChanged?.Invoke(this, e);
}
} }
public class PerspexReadOnlyListView<TIn, TOut> : IPerspexReadOnlyList<TOut>, IDisposable public class PerspexReadOnlyListView<TIn, TOut> : IPerspexReadOnlyList<TOut>, IDisposable
@ -160,22 +169,17 @@ namespace Perspex.Collections
if (source != null) if (source != null)
{ {
this.source.CollectionChanged += this.SourceCollectionChanged; this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
} }
} }
public TOut this[int index] public event NotifyCollectionChangedEventHandler CollectionChanged;
{
get public event PropertyChangedEventHandler PropertyChanged;
{
return (this.convert != null) ?
this.convert(this.source[index]) :
(TOut)(object)this.source[index];
}
}
public int Count public int Count
{ {
get { return this.source.Count; } get { return this.source?.Count ?? 0; }
} }
public IPerspexReadOnlyList<TIn> Source public IPerspexReadOnlyList<TIn> Source
@ -190,6 +194,7 @@ namespace Perspex.Collections
if (this.source != null) if (this.source != null)
{ {
this.source.CollectionChanged -= this.SourceCollectionChanged; this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
if (this.CollectionChanged != null) if (this.CollectionChanged != null)
{ {
@ -206,6 +211,7 @@ namespace Perspex.Collections
if (this.source != null) if (this.source != null)
{ {
this.source.CollectionChanged += this.SourceCollectionChanged; this.source.CollectionChanged += this.SourceCollectionChanged;
this.source.PropertyChanged += this.SourcePropertyChanged;
if (this.CollectionChanged != null) if (this.CollectionChanged != null)
{ {
@ -219,15 +225,22 @@ namespace Perspex.Collections
} }
} }
public event NotifyCollectionChangedEventHandler CollectionChanged; public TOut this[int index]
{
public event PropertyChangedEventHandler PropertyChanged; get
{
return (this.convert != null) ?
this.convert(this.source[index]) :
(TOut)(object)this.source[index];
}
}
public void Dispose() public void Dispose()
{ {
if (this.source != null) if (this.source != null)
{ {
this.source.CollectionChanged -= this.SourceCollectionChanged; this.source.CollectionChanged -= this.SourceCollectionChanged;
this.source.PropertyChanged -= this.SourcePropertyChanged;
} }
} }
@ -248,7 +261,7 @@ namespace Perspex.Collections
return this.GetEnumerator(); return this.GetEnumerator();
} }
private IList<TOut> ConvertList(IEnumerable list) private IList ConvertList(IEnumerable list)
{ {
return list.Cast<TIn>().Select(this.convert).ToList(); return list.Cast<TIn>().Select(this.convert).ToList();
} }
@ -271,7 +284,7 @@ namespace Perspex.Collections
ev = new NotifyCollectionChangedEventArgs( ev = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, NotifyCollectionChangedAction.Remove,
this.ConvertList(e.OldItems), this.ConvertList(e.OldItems),
e.NewStartingIndex); e.OldStartingIndex);
break; break;
case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Replace:
ev = new NotifyCollectionChangedEventArgs( ev = new NotifyCollectionChangedEventArgs(
@ -288,6 +301,10 @@ namespace Perspex.Collections
this.CollectionChanged(this, ev); 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -36,18 +36,9 @@ namespace Perspex.Collections
this.item = item; this.item = item;
} }
public T this[int index] public event NotifyCollectionChangedEventHandler CollectionChanged;
{
get
{
if (index < 0 || index >= this.Count)
{
throw new ArgumentOutOfRangeException();
}
return item; public event PropertyChangedEventHandler PropertyChanged;
}
}
public int Count 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() 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 file="PerspexPropertyValue.cs" company="Steven Kirk">
// Copyright 2014 MIT Licence. See licence.md for more information. // Copyright 2014 MIT Licence. See licence.md for more information.
// </copyright> // </copyright>
@ -8,23 +8,24 @@ namespace Perspex.Diagnostics
{ {
public class PerspexPropertyValue public class PerspexPropertyValue
{ {
public PerspexPropertyValue(PerspexProperty property, object value) public PerspexPropertyValue(
PerspexProperty property,
object value,
BindingPriority priority,
string diagnostic)
{ {
this.Property = property; this.Property = property;
this.CurrentValue = value; this.Value = value;
this.Priority = priority;
this.Diagnostic = diagnostic;
} }
public PerspexPropertyValue(PerspexProperty property, PriorityValue priorityValue) public PerspexProperty Property { get; }
{
this.Property = property;
this.CurrentValue = priorityValue.Value;
this.PriorityValue = priorityValue;
}
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 namespace Perspex
{ {
/// <summary>
/// Interface for objects with a <see cref="Description"/>.
/// </summary>
public interface IDescription public interface IDescription
{ {
/// <summary>
/// Gets the description of the object.
/// </summary>
/// <value>
/// The description of the object.
/// </value>
string Description { get; } 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> <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile> <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -35,6 +37,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Binding.cs" /> <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\IPerspexList.cs" />
<Compile Include="Collections\PerspexReadOnlyListView.cs" /> <Compile Include="Collections\PerspexReadOnlyListView.cs" />
<Compile Include="Collections\PerspexList.cs" /> <Compile Include="Collections\PerspexList.cs" />
@ -43,7 +50,6 @@
<Compile Include="Diagnostics\PerspexPropertyValue.cs" /> <Compile Include="Diagnostics\PerspexPropertyValue.cs" />
<Compile Include="IDescription.cs" /> <Compile Include="IDescription.cs" />
<Compile Include="Collections\PerspexListExtensions.cs" /> <Compile Include="Collections\PerspexListExtensions.cs" />
<Compile Include="IObservableDescription.cs" />
<Compile Include="Collections\IPerspexReadOnlyList.cs" /> <Compile Include="Collections\IPerspexReadOnlyList.cs" />
<Compile Include="PerspexObject.cs" /> <Compile Include="PerspexObject.cs" />
<Compile Include="PerspexProperty.cs" /> <Compile Include="PerspexProperty.cs" />
@ -51,17 +57,25 @@
<Compile Include="Platform\IPlatformHandle.cs" /> <Compile Include="Platform\IPlatformHandle.cs" />
<Compile Include="Platform\IPlatformThreadingInterface.cs" /> <Compile Include="Platform\IPlatformThreadingInterface.cs" />
<Compile Include="Platform\PlatformHandle.cs" /> <Compile Include="Platform\PlatformHandle.cs" />
<Compile Include="PriorityLevel.cs" />
<Compile Include="PriorityValue.cs" /> <Compile Include="PriorityValue.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reactive\PerspexObservable.cs" />
<Compile Include="Threading\Dispatcher.cs" /> <Compile Include="Threading\Dispatcher.cs" />
<Compile Include="Threading\DispatcherPriority.cs" /> <Compile Include="Threading\DispatcherPriority.cs" />
<Compile Include="Threading\DispatcherTimer.cs" /> <Compile Include="Threading\DispatcherTimer.cs" />
<Compile Include="Threading\MainLoop.cs" /> <Compile Include="Threading\MainLoop.cs" />
<Compile Include="Threading\PerspexScheduler.cs" /> <Compile Include="Threading\PerspexScheduler.cs" />
<Compile Include="Utilities\TypeUtilities.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Splat"> <Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\packages\Splat.1.6.1\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath> <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>
<Reference Include="System.Reactive.Core"> <Reference Include="System.Reactive.Core">
<HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath> <HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
@ -86,14 +100,10 @@
<Name>NGenerics</Name> <Name>NGenerics</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </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="$(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. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <Target Name="BeforeBuild">

294
Perspex.Base/PerspexObject.cs

@ -10,10 +10,13 @@ namespace Perspex
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using Perspex.Diagnostics; using Perspex.Reactive;
using Splat; using Serilog;
using Serilog.Core.Enrichers;
using Perspex.Utilities;
/// <summary> /// <summary>
/// The priority of a binding. /// The priority of a binding.
@ -23,20 +26,20 @@ namespace Perspex
/// <summary> /// <summary>
/// A value that comes from an animation. /// A value that comes from an animation.
/// </summary> /// </summary>
Animation = -1, Animation = -2,
/// <summary> /// <summary>
/// A local value. /// A local value.
/// </summary> /// </summary>
LocalValue, LocalValue = 0,
/// <summary> /// <summary>
/// A triggered style binding. /// A triggered style binding.
/// </summary> /// </summary>
/// <remarks> /// <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 /// <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. /// control has the :pointerover class.
/// </remarks> /// </remarks>
StyleTrigger, StyleTrigger,
@ -63,7 +66,7 @@ namespace Perspex
/// <remarks> /// <remarks>
/// This class is analogous to DependencyObject in WPF. /// This class is analogous to DependencyObject in WPF.
/// </remarks> /// </remarks>
public class PerspexObject : INotifyPropertyChanged, IEnableLogger public class PerspexObject : INotifyPropertyChanged
{ {
/// <summary> /// <summary>
/// The registered properties by type. /// The registered properties by type.
@ -87,25 +90,33 @@ namespace Perspex
/// </summary> /// </summary>
private PropertyChangedEventHandler inpcChanged; private PropertyChangedEventHandler inpcChanged;
/// <summary>
/// A serilog logger for logging property events.
/// </summary>
private ILogger propertyLog;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PerspexObject"/> class. /// Initializes a new instance of the <see cref="PerspexObject"/> class.
/// </summary> /// </summary>
public PerspexObject() public PerspexObject()
{ {
foreach (var p in this.GetAllValues()) this.propertyLog = Log.ForContext(new[]
{ {
var priority = p.PriorityValue != null ? new PropertyEnricher("Area", "Property"),
(BindingPriority)p.PriorityValue.ValuePriority : new PropertyEnricher("SourceContext", this.GetType()),
BindingPriority.LocalValue; new PropertyEnricher("Id", this.GetHashCode()),
});
foreach (var property in this.GetRegisteredProperties())
{
var e = new PerspexPropertyChangedEventArgs( var e = new PerspexPropertyChangedEventArgs(
this, this,
p.Property, property,
PerspexProperty.UnsetValue, PerspexProperty.UnsetValue,
p.CurrentValue, property.GetDefaultValue(this.GetType()),
priority); BindingPriority.Unset);
p.Property.NotifyInitialized(e); property.NotifyInitialized(e);
} }
} }
@ -124,9 +135,12 @@ namespace Perspex
} }
/// <summary> /// <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. /// are inherited from.
/// </summary> /// </summary>
/// <value>
/// The inheritance parent.
/// </value>
protected PerspexObject InheritanceParent protected PerspexObject InheritanceParent
{ {
get get
@ -200,8 +214,8 @@ namespace Perspex
set set
{ {
BindingMode mode = (binding.Mode == BindingMode.Default) ? BindingMode mode = (binding.Mode == BindingMode.Default) ?
binding.Property.DefaultBindingMode : binding.Property.DefaultBindingMode :
binding.Mode; binding.Mode;
Binding sourceBinding = value as Binding; Binding sourceBinding = value as Binding;
@ -217,7 +231,7 @@ namespace Perspex
this.Bind(binding.Property, value, binding.Priority); this.Bind(binding.Property, value, binding.Priority);
break; break;
case BindingMode.OneTime: case BindingMode.OneTime:
this.SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property)); this.SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property), binding.Priority);
break; break;
case BindingMode.OneWayToSource: case BindingMode.OneWayToSource:
sourceBinding.Source.Bind(sourceBinding.Property, this.GetObservable(binding.Property), binding.Priority); 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="type">The type.</param>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <remarks> /// <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. /// <see cref="PerspexProperty.Register"/> method.
/// </remarks> /// </remarks>
public static void Register(Type type, PerspexProperty property) public static void Register(Type type, PerspexProperty property)
@ -291,7 +305,7 @@ namespace Perspex
public void ClearValue(PerspexProperty property) public void ClearValue(PerspexProperty property)
{ {
Contract.Requires<NullReferenceException>(property != null); Contract.Requires<NullReferenceException>(property != null);
this.SetValue(property, PerspexProperty.UnsetValue); this.SetValue(property, PerspexProperty.UnsetValue);
} }
@ -304,31 +318,35 @@ namespace Perspex
{ {
Contract.Requires<NullReferenceException>(property != null); Contract.Requires<NullReferenceException>(property != null);
return Observable.Create<object>(observer => return new PerspexObservable<object>(
{ observer =>
EventHandler<PerspexPropertyChangedEventArgs> handler = (s, e) =>
{ {
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;
{
this.PropertyChanged -= handler; return Disposable.Create(() =>
}; {
}).StartWith(this.GetValue(property)); this.PropertyChanged -= handler;
});
},
this.GetObservableDescription(property));
} }
/// <summary> /// <summary>
/// Gets an observable for a <see cref="PerspexProperty"/>. /// Gets an observable for a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T">The property type.</typeparam>
/// <param name="property"></param> /// <param name="property">The property.</param>
/// <returns></returns> /// <returns>An observable.</returns>
public IObservable<T> GetObservable<T>(PerspexProperty<T> property) public IObservable<T> GetObservable<T>(PerspexProperty<T> property)
{ {
Contract.Requires<NullReferenceException>(property != null); Contract.Requires<NullReferenceException>(property != null);
@ -339,28 +357,31 @@ namespace Perspex
/// <summary> /// <summary>
/// Gets an observable for a <see cref="PerspexProperty"/>. /// Gets an observable for a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property"></param> /// <param name="property">The property.</param>
/// <returns></returns> /// <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) public IObservable<Tuple<T, T>> GetObservableWithHistory<T>(PerspexProperty<T> property)
{ {
return Observable.Create<Tuple<T, T>>(observer => return new PerspexObservable<Tuple<T, T>>(
{ observer =>
EventHandler<PerspexPropertyChangedEventArgs> handler = (s, e) =>
{ {
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 () => return Disposable.Create(() =>
{ {
this.PropertyChanged -= handler; this.PropertyChanged -= handler;
}; });
}); },
this.GetObservableDescription(property));
} }
/// <summary> /// <summary>
@ -396,6 +417,7 @@ namespace Perspex
/// <summary> /// <summary>
/// Gets a <see cref="PerspexProperty"/> value. /// Gets a <see cref="PerspexProperty"/> value.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <returns>The value.</returns> /// <returns>The value.</returns>
public T GetValue<T>(PerspexProperty<T> property) public T GetValue<T>(PerspexProperty<T> property)
@ -405,23 +427,12 @@ namespace Perspex
return (T)this.GetValue((PerspexProperty)property); return (T)this.GetValue((PerspexProperty)property);
} }
public IEnumerable<PerspexPropertyValue> GetAllValues() /// <summary>
{ /// Gets all properties that are registered on this object.
foreach (PerspexProperty property in this.GetRegisteredProperties()) /// </summary>
{ /// <returns>
PriorityValue value; /// A collection of <see cref="PerspexProperty"/> objects.
/// </returns>
if (this.values.TryGetValue(property, out value))
{
yield return new PerspexPropertyValue(property, value);
}
else
{
yield return new PerspexPropertyValue(property, this.GetValue(property));
}
}
}
public IEnumerable<PerspexProperty> GetRegisteredProperties() public IEnumerable<PerspexProperty> GetRegisteredProperties()
{ {
Type type = this.GetType(); 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> /// <summary>
/// Checks whether a <see cref="PerspexProperty"/> is set on this object. /// Checks whether a <see cref="PerspexProperty"/> is set on this object.
/// </summary> /// </summary>
@ -497,11 +497,14 @@ namespace Perspex
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</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); Contract.Requires<NullReferenceException>(property != null);
const int Priority = (int)BindingPriority.LocalValue;
PriorityValue v; PriorityValue v;
if (!this.IsRegistered(property)) if (!this.IsRegistered(property))
@ -512,13 +515,13 @@ namespace Perspex
this.GetType())); this.GetType()));
} }
if (!PriorityValue.IsValidValue(value, property.PropertyType)) if (!TypeUtilities.TryCast(property.PropertyType, value, out value))
{ {
throw new InvalidOperationException(string.Format( throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})", "Invalid value for Property '{0}': {1} ({2})",
property.Name, property.Name,
value, value,
value.GetType().FullName)); value?.GetType().FullName ?? "(null)"));
} }
if (!this.values.TryGetValue(property, out v)) if (!this.values.TryGetValue(property, out v))
@ -531,15 +534,13 @@ namespace Perspex
v = this.CreatePriorityValue(property); v = this.CreatePriorityValue(property);
this.values.Add(property, v); this.values.Add(property, v);
} }
this.Log().Debug( this.propertyLog.Verbose(
"Set local value of {0}.{1} (#{2:x8}) to {3}", "Set {Property} to {$Value} with priority {Priority}",
this.GetType().Name, property,
property.Name, value,
this.GetHashCode(), priority);
value); v.SetDirectValue(value, (int)priority);
v.Replace(Observable.Never<object>().StartWith(value), Priority);
} }
/// <summary> /// <summary>
@ -548,17 +549,20 @@ namespace Perspex
/// <typeparam name="T">The type of the property.</typeparam> /// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="value">The value.</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); Contract.Requires<NullReferenceException>(property != null);
this.SetValue((PerspexProperty)property, value); this.SetValue((PerspexProperty)property, value, priority);
} }
/// <summary> /// <summary>
/// Binds a <see cref="PerspexProperty"/> to an observable. /// Binds a <see cref="PerspexProperty"/> to an observable.
/// </summary> /// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param> /// <param name="property">The property.</param>
/// <param name="source">The observable.</param> /// <param name="source">The observable.</param>
/// <param name="priority">The priority of the binding.</param> /// <param name="priority">The priority of the binding.</param>
@ -575,27 +579,27 @@ namespace Perspex
PriorityValue v; PriorityValue v;
IDescription description = source as IDescription; 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)) if (!this.values.TryGetValue(property, out v))
{ {
v = this.CreatePriorityValue(property); v = this.CreatePriorityValue(property);
this.values.Add(property, v); this.values.Add(property, v);
} }
this.Log().Debug( this.propertyLog.Verbose(
"Bound value of {0}.{1} (#{2:x8}) to {3}", "Bound {Property} to {Binding} with priority {Priority}",
this.GetType().Name, property,
property.Name, source,
this.GetHashCode(), priority);
description != null ? description.Description : "[Anonymous]");
if (priority == BindingPriority.LocalValue) return v.Add(source, (int)priority);
{
return v.Replace(source, (int)priority);
}
else
{
return v.Add(source, (int)priority);
}
} }
/// <summary> /// <summary>
@ -609,8 +613,8 @@ namespace Perspex
/// A disposable which can be used to terminate the binding. /// A disposable which can be used to terminate the binding.
/// </returns> /// </returns>
public IDisposable Bind<T>( public IDisposable Bind<T>(
PerspexProperty<T> property, PerspexProperty<T> property,
IObservable<T> source, IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue) BindingPriority priority = BindingPriority.LocalValue)
{ {
Contract.Requires<NullReferenceException>(property != null); Contract.Requires<NullReferenceException>(property != null);
@ -624,20 +628,22 @@ namespace Perspex
/// <param name="property">The property on this object.</param> /// <param name="property">The property on this object.</param>
/// <param name="source">The source object.</param> /// <param name="source">The source object.</param>
/// <param name="sourceProperty">The property on the source object.</param> /// <param name="sourceProperty">The property on the source object.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns> /// <returns>
/// A disposable which can be used to terminate the binding. /// A disposable which can be used to terminate the binding.
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this. Two-way /// The binding is first carried out from <paramref name="source"/> to this.
/// bindings are always at the LocalValue priority.
/// </remarks> /// </remarks>
public void BindTwoWay( public IDisposable BindTwoWay(
PerspexProperty property, PerspexProperty property,
PerspexObject source, PerspexObject source,
PerspexProperty sourceProperty) PerspexProperty sourceProperty,
BindingPriority priority = BindingPriority.LocalValue)
{ {
source.GetObservable(sourceProperty).Subscribe(x => this.SetValue(property, x)); return new CompositeDisposable(
this.GetObservable(property).Subscribe(x => source.SetValue(sourceProperty, x)); this.Bind(property, source.GetObservable(sourceProperty)),
source.Bind(sourceProperty, this.GetObservable(property)));
} }
/// <summary> /// <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> /// <summary>
/// Forces re-coercion of properties when a property value changes. /// Forces re-coercion of properties when a property value changes.
/// </summary> /// </summary>
@ -707,13 +722,12 @@ namespace Perspex
{ {
this.RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority); this.RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority);
this.Log().Debug( this.propertyLog.Verbose(
"Value of {0}.{1} (#{2:x8}) changed from {3} to {4}", "{Property} changed from {$Old} to {$Value} with priority {Priority}",
this.GetType().Name, property,
property.Name,
this.GetHashCode(),
oldValue, 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> /// <summary>
/// Raises the <see cref="PropertyChanged"/> event. /// Raises the <see cref="PropertyChanged"/> event.
/// </summary> /// </summary>
@ -763,17 +787,17 @@ namespace Perspex
/// <param name="newValue">The new property value.</param> /// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param> /// <param name="priority">The priority of the binding that produced the value.</param>
private void RaisePropertyChanged( private void RaisePropertyChanged(
PerspexProperty property, PerspexProperty property,
object oldValue, object oldValue,
object newValue, object newValue,
BindingPriority priority) BindingPriority priority)
{ {
Contract.Requires<NullReferenceException>(property != null); Contract.Requires<NullReferenceException>(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this, this,
property, property,
oldValue, oldValue,
newValue, newValue,
priority); 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.Collections.Generic;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Reflection; using System.Reflection;
using Perspex.Utilities;
/// <summary> /// <summary>
/// A perspex property. /// A perspex property.
@ -22,6 +23,7 @@ namespace Perspex
/// <summary> /// <summary>
/// Represents an unset property value. /// Represents an unset property value.
/// </summary> /// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields must be private", Justification = "It's readonly")]
public static readonly object UnsetValue = new Unset(); public static readonly object UnsetValue = new Unset();
/// <summary> /// <summary>
@ -39,11 +41,6 @@ namespace Perspex
/// </summary> /// </summary>
private Subject<PerspexPropertyChangedEventArgs> changed = new Subject<PerspexPropertyChangedEventArgs>(); private Subject<PerspexPropertyChangedEventArgs> changed = new Subject<PerspexPropertyChangedEventArgs>();
/// <summary>
/// The coerce function.
/// </summary>
private Func<PerspexObject, object, object> coerce;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PerspexProperty"/> class. /// Initializes a new instance of the <see cref="PerspexProperty"/> class.
/// </summary> /// </summary>
@ -54,14 +51,16 @@ namespace Perspex
/// <param name="inherits">Whether the property inherits its value.</param> /// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param> /// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="coerce">A coercion function.</param> /// <param name="coerce">A coercion function.</param>
/// <param name="isAttached">Whether the property is an attached property.</param>
public PerspexProperty( public PerspexProperty(
string name, string name,
Type valueType, Type valueType,
Type ownerType, Type ownerType,
object defaultValue, object defaultValue,
bool inherits, bool inherits = false,
BindingMode defaultBindingMode, BindingMode defaultBindingMode = BindingMode.Default,
Func<PerspexObject, object, object> coerce) Func<PerspexObject, object, object> coerce = null,
bool isAttached = false)
{ {
Contract.Requires<NullReferenceException>(name != null); Contract.Requires<NullReferenceException>(name != null);
Contract.Requires<NullReferenceException>(valueType != null); Contract.Requires<NullReferenceException>(valueType != null);
@ -74,38 +73,64 @@ namespace Perspex
this.Inherits = inherits; this.Inherits = inherits;
this.DefaultBindingMode = defaultBindingMode; this.DefaultBindingMode = defaultBindingMode;
this.Coerce = coerce; this.Coerce = coerce;
this.IsAttached = isAttached;
} }
/// <summary> /// <summary>
/// Gets the name of the property. /// Gets the name of the property.
/// </summary> /// </summary>
public string Name { get; private set; } /// <value>
/// The name of the property.
/// </value>
public string Name { get; }
/// <summary> /// <summary>
/// Gets the type of the property's value. /// Gets the type of the property's value.
/// </summary> /// </summary>
public Type PropertyType { get; private set; } /// <value>
/// The type of the property's value.
/// </value>
public Type PropertyType { get; }
/// <summary> /// <summary>
/// Gets the type of the class that registers the property. /// Gets the type of the class that registers the property.
/// </summary> /// </summary>
public Type OwnerType { get; private set; } /// <value>
/// The type of the class that registers the property.
/// </value>
public Type OwnerType { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the property inherits its value. /// Gets a value indicating whether the property inherits its value.
/// </summary> /// </summary>
public bool Inherits { get; private set; } /// <value>
/// A value indicating whether the property inherits its value.
/// </value>
public bool Inherits { get; }
/// <summary> /// <summary>
/// Gets the default binding mode for the property. /// Gets the default binding mode for the property.
/// </summary> /// </summary>
/// <returns></returns> /// <value>
public BindingMode DefaultBindingMode { get; private set; } /// The default binding mode for the property.
/// </value>
public BindingMode DefaultBindingMode { get; }
/// <summary> /// <summary>
/// Gets the property's coerce function. /// Gets the property's coerce function.
/// </summary> /// </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> /// <summary>
/// Gets an observable that is fired when this property is initialized on a /// Gets an observable that is fired when this property is initialized on a
@ -113,24 +138,62 @@ namespace Perspex
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This observable is fired each time a new <see cref="PerspexObject"/> is constructed /// 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 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 the object is passed in the args' NewValue (OldValue will always be
/// <see cref="UnsetValue"/>. /// <see cref="UnsetValue"/>.
/// </remarks> /// </remarks>
/// <value>
/// An observable that is fired when this property is initialized on a new
/// <see cref="PerspexObject"/> instance.
/// </value>
public IObservable<PerspexPropertyChangedEventArgs> Initialized public IObservable<PerspexPropertyChangedEventArgs> Initialized
{ {
get { return this.initialized; } get { return this.initialized; }
} }
/// <summary> /// <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. /// <see cref="PerspexObject"/> instance.
/// </summary> /// </summary>
/// <value>
/// An observable that is fired when this property changes on any
/// <see cref="PerspexObject"/> instance.
/// </value>
public IObservable<PerspexPropertyChangedEventArgs> Changed public IObservable<PerspexPropertyChangedEventArgs> Changed
{ {
get { return this.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> /// <summary>
/// Registers a <see cref="PerspexProperty"/>. /// Registers a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>
@ -158,7 +221,8 @@ namespace Perspex
defaultValue, defaultValue,
inherits, inherits,
defaultBindingMode, defaultBindingMode,
coerce); coerce,
false);
PerspexObject.Register(typeof(TOwner), result); PerspexObject.Register(typeof(TOwner), result);
@ -183,17 +247,17 @@ namespace Perspex
bool inherits = false, bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay, BindingMode defaultBindingMode = BindingMode.OneWay,
Func<PerspexObject, TValue, TValue> coerce = null) Func<PerspexObject, TValue, TValue> coerce = null)
where TOwner : PerspexObject
{ {
Contract.Requires<NullReferenceException>(name != null); Contract.Requires<NullReferenceException>(name != null);
PerspexProperty<TValue> result = new PerspexProperty<TValue>( PerspexProperty<TValue> result = new PerspexProperty<TValue>(
typeof(TOwner) + "." + name, name,
typeof(TOwner), typeof(TOwner),
defaultValue, defaultValue,
inherits, inherits,
defaultBindingMode, defaultBindingMode,
coerce); coerce,
true);
PerspexObject.Register(typeof(THost), result); PerspexObject.Register(typeof(THost), result);
@ -201,37 +265,43 @@ namespace Perspex
} }
/// <summary> /// <summary>
/// Provides access to a property's binding via the <see cref="PerspexObject"/> /// Registers an attached <see cref="PerspexProperty"/>.
/// indexer.
/// </summary> /// </summary>
/// <param name="property">The property.</param> /// <typeparam name="THost">The type of the class that the property is to be registered on.</typeparam>
/// <returns>A <see cref="Binding"/> describing the binding.</returns> /// <typeparam name="TValue">The type of the property's value.</typeparam>
public static Binding operator !(PerspexProperty property) /// <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 Contract.Requires<NullReferenceException>(name != null);
{
Priority = BindingPriority.LocalValue,
Property = property,
};
}
/// <summary> PerspexProperty<TValue> result = new PerspexProperty<TValue>(
/// Provides access to a property's template binding via the <see cref="PerspexObject"/> name,
/// indexer. ownerType,
/// </summary> defaultValue,
/// <param name="property">The property.</param> inherits,
/// <returns>A <see cref="Binding"/> describing the binding.</returns> defaultBindingMode,
public static Binding operator ~(PerspexProperty property) coerce,
{ true);
return new Binding
{ PerspexObject.Register(typeof(THost), result);
Priority = BindingPriority.TemplatedParent,
Property = property, return result;
};
} }
/// <summary> /// <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. /// operator to initiate a binding.
/// </summary> /// </summary>
/// <returns>A <see cref="Binding"/>.</returns> /// <returns>A <see cref="Binding"/>.</returns>
@ -270,23 +340,28 @@ namespace Perspex
return this.defaultValues[this.OwnerType]; 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) public bool IsValidValue(object value)
{ {
if (value == UnsetValue) return TypeUtilities.TryCast(this.PropertyType, value, out value);
{ }
return true;
}
else if (value == null)
{
return !this.PropertyType.GetTypeInfo().IsValueType ||
Nullable.GetUnderlyingType(this.PropertyType) != null;
}
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> /// <summary>
/// Gets the default value for the property on the specified type. /// Overrides the default value for the property on the specified type.
/// </summary> /// </summary>
/// <param name="type">The type.</param> /// <param name="type">The type.</param>
/// <param name="defaultValue">The default value.</param> /// <param name="defaultValue">The default value.</param>
@ -294,7 +369,15 @@ namespace Perspex
{ {
Contract.Requires<NullReferenceException>(type != null); 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)) if (this.defaultValues.ContainsKey(type))
{ {
throw new InvalidOperationException("Default value is already set for this property."); throw new InvalidOperationException("Default value is already set for this property.");
@ -303,93 +386,46 @@ namespace Perspex
this.defaultValues.Add(type, defaultValue); 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() public override string ToString()
{ {
return this.Name; return this.Name;
} }
/// <summary>
/// Notifies the <see cref="Initialized"/> observable.
/// </summary>
/// <param name="e">The observable arguments.</param>
internal void NotifyInitialized(PerspexPropertyChangedEventArgs e) internal void NotifyInitialized(PerspexPropertyChangedEventArgs e)
{ {
this.initialized.OnNext(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) internal void NotifyChanged(PerspexPropertyChangedEventArgs e)
{ {
this.changed.OnNext(e); this.changed.OnNext(e);
} }
/// <summary>
/// Class representing the <see cref="UnsetValue"/>.
/// </summary>
private class Unset private class Unset
{ {
/// <summary>
/// Returns the string representation of the <see cref="UnsetValue"/>.
/// </summary>
/// <returns>The string "(unset)".</returns>
public override string ToString() 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> /// </summary>
public class PerspexPropertyChangedEventArgs 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( public PerspexPropertyChangedEventArgs(
PerspexObject sender, PerspexObject sender,
PerspexProperty property, PerspexProperty property,
@ -28,27 +36,39 @@ namespace Perspex
/// <summary> /// <summary>
/// Gets the <see cref="PerspexObject"/> that the property changed on. /// Gets the <see cref="PerspexObject"/> that the property changed on.
/// </summary> /// </summary>
/// <returns></returns> /// <value>The sender object.</value>
public PerspexObject Sender { get; private set; } public PerspexObject Sender { get; private set; }
/// <summary> /// <summary>
/// Gets the property that changed. /// Gets the property that changed.
/// </summary> /// </summary>
/// <value>
/// The property that changed.
/// </value>
public PerspexProperty Property { get; private set; } public PerspexProperty Property { get; private set; }
/// <summary> /// <summary>
/// Gets the old value of the property. /// Gets the old value of the property.
/// </summary> /// </summary>
/// <value>
/// The old value of the property.
/// </value>
public object OldValue { get; private set; } public object OldValue { get; private set; }
/// <summary> /// <summary>
/// Gets the new value of the property. /// Gets the new value of the property.
/// </summary> /// </summary>
/// <value>
/// The new value of the property.
/// </value>
public object NewValue { get; private set; } public object NewValue { get; private set; }
/// <summary> /// <summary>
/// Gets the priority of the binding that produced the value. /// Gets the priority of the binding that produced the value.
/// </summary> /// </summary>
/// <value>
/// The priority of the binding that produced the value.
/// </value>
public BindingPriority Priority { get; private set; } 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 namespace Perspex.Platform
{ {
using System; using System;
using System.Threading;
using System.Threading.Tasks;
using Perspex.Threading;
/// <summary> /// <summary>
/// Provides platform-specific services relating to threading. /// Provides platform-specific services relating to threading.
/// </summary> /// </summary>
public interface IPlatformThreadingInterface 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(); bool HasMessages();
/// <summary> /// <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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Reflection; using System.Reflection;
using System.Text;
using Perspex.Utilities;
/// <summary> /// <summary>
/// Maintains a list of prioritised bindings together with a current value. /// Maintains a list of prioritised bindings together with a current value.
/// </summary> /// </summary>
/// <remarks> /// <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 /// 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 /// 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 /// 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 /// 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 /// has a higher priority. Each time the value changes, the <see cref="Changed"/> observable is
/// <see cref="Changed"/> observable is fired with the old and new values. /// fired with the old and new values.
/// </remarks> /// </remarks>
public class PriorityValue internal class PriorityValue
{ {
/// <summary> /// <summary>
/// The name of the property. /// The name of the property.
@ -37,9 +39,9 @@ namespace Perspex
private Type valueType; private Type valueType;
/// <summary> /// <summary>
/// The currently registered binding entries. /// The currently registered bindings organised by priority.
/// </summary> /// </summary>
private LinkedList<BindingEntry> bindings = new LinkedList<BindingEntry>(); private Dictionary<int, PriorityLevel> levels = new Dictionary<int, PriorityLevel>();
/// <summary> /// <summary>
/// The changed observable. /// The changed observable.
@ -51,6 +53,9 @@ namespace Perspex
/// </summary> /// </summary>
private object value; private object value;
/// <summary>
/// The function used to coerce the value, if any.
/// </summary>
private Func<object, object> coerce; private Func<object, object> coerce;
/// <summary> /// <summary>
@ -69,8 +74,11 @@ namespace Perspex
} }
/// <summary> /// <summary>
/// Fired whenever the current <see cref="Value"/> changes to a new distinct value. /// Fired whenever the current <see cref="Value"/> changes.
/// </summary> /// </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 public IObservable<Tuple<object, object>> Changed
{ {
get { return this.changed; } get { return this.changed; }
@ -93,39 +101,6 @@ namespace Perspex
private set; 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> /// <summary>
/// Adds a new binding. /// Adds a new binding.
/// </summary> /// </summary>
@ -136,110 +111,72 @@ namespace Perspex
/// </returns> /// </returns>
public IDisposable Add(IObservable<object> binding, int priority) public IDisposable Add(IObservable<object> binding, int priority)
{ {
BindingEntry entry = new BindingEntry(); return this.GetLevel(priority).Add(binding);
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);
});
} }
/// <summary> /// <summary>
/// Adds a new binding, replacing all those of the same priority. /// Sets the direct value for a specified priority.
/// </summary> /// </summary>
/// <param name="binding">The binding.</param> /// <param name="value">The value.</param>
/// <param name="priority">The binding priority.</param> /// <param name="priority">The priority</param>
/// <returns> public void SetDirectValue(object value, int priority)
/// A disposable that will remove the binding.
/// </returns>
public IDisposable Replace(IObservable<object> binding, int priority)
{ {
BindingEntry entry = new BindingEntry(); this.GetLevel(priority).DirectValue = value;
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;
}
if (insert == null) /// <summary>
{ /// Gets the currently active bindings on this object.
this.bindings.AddLast(entry); /// </summary>
} /// <returns>An enumerable collection of bindings.</returns>
else 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> /// <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> /// </summary>
/// <param name="priority">The priority.</param> /// <returns>A diagnostic string.</returns>
public void Clear(int priority) public string GetDiagnostic()
{ {
LinkedListNode<BindingEntry> item = this.bindings.First; var b = new StringBuilder();
bool removed = false; var first = true;
while (item != null && item.Value.Priority <= priority) foreach (var level in this.levels)
{ {
LinkedListNode<BindingEntry> next = item.Next; if (!first)
if (item.Value.Priority == priority)
{ {
item.Value.Dispose(); b.AppendLine();
this.bindings.Remove(item);
removed = true;
} }
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) foreach (var binding in level.Value.Bindings)
{ {
this.UpdateValue(); 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> return b.ToString();
/// Gets the currently active bindings on this object.
/// </summary>
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<BindingEntry> GetBindings()
{
return this.bindings;
} }
/// <summary> /// <summary>
@ -249,185 +186,87 @@ namespace Perspex
{ {
if (this.coerce != null) 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> /// <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> /// </summary>
/// <param name="value">The value.</param> /// <param name="priority">The priority.</param>
private void VerifyValidValue(object value) /// <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( var mode = (LevelPrecedenceMode)(priority % 2);
"Invalid value for Property '{0}': {1} ({2})", result = new PriorityLevel(priority, mode, this.ValueChanged);
this.name, this.levels.Add(priority, result);
value,
value.GetType().FullName));
} }
return result;
} }
/// <summary> /// <summary>
/// Called when a binding's value changes. /// Updates the current <see cref="Value"/> and notifies all subscibers.
/// </summary> /// </summary>
/// <param name="changed">The changed entry.</param> /// <param name="value">The value to set.</param>
private void EntryChanged(BindingEntry changed) /// <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> var old = this.value;
/// 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);
if (this.coerce != null) if (this.coerce != null)
{ {
value = this.coerce(value); value = this.coerce(value);
VerifyValidValue(value);
} }
object old = this.value;
this.ValuePriority = priority; this.ValuePriority = priority;
this.value = value;
if (!EqualityComparer<object>.Default.Equals(old, value)) this.changed.OnNext(Tuple.Create(old, this.value));
{
this.value = value;
this.changed.OnNext(Tuple.Create(old, value));
}
} }
/// <summary> /// <summary>
/// Removes the specified binding entry and updates the current value. /// Called when the value for a priority level changes.
/// </summary> /// </summary>
/// <param name="entry">The binding entry to remove.</param> /// <param name="level">The priority level of the changed entry.</param>
private void Remove(BindingEntry entry) private void ValueChanged(PriorityLevel level)
{ {
entry.Dispose(); if (level.Priority <= this.ValuePriority)
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
{ {
get; if (level.Value != PerspexProperty.UnsetValue)
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)
{ {
throw new Exception("PriorityValue.Entry.Start() called more than once."); this.UpdateValue(level.Value, level.Priority);
} }
else
this.Priority = priority;
this.Value = PerspexProperty.UnsetValue;
if (binding is IDescription)
{ {
this.Description = ((IDescription)binding).Description; foreach (var i in this.levels.Values.OrderBy(x => x.Priority))
}
this.subscription = binding.Subscribe(
value =>
{ {
this.Value = value; if (i.Value != PerspexProperty.UnsetValue)
changed(this); {
}, this.UpdateValue(i.Value, i.Priority);
() => completed(this)); return;
} }
}
/// <summary>
/// Ends the binding subscription. this.UpdateValue(PerspexProperty.UnsetValue, int.MaxValue);
/// </summary>
public void Dispose()
{
if (this.subscription != null)
{
this.subscription.Dispose();
} }
} }
} }

3
Perspex.Base/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@
using System.Reflection; using System.Reflection;
using System.Resources; using System.Resources;
using System.Runtime.CompilerServices;
// 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 // 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.*")]
[assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("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 public class PerspexScheduler : LocalScheduler
{ {
private static readonly PerspexScheduler instance = new PerspexScheduler(); public static readonly PerspexScheduler Instance = new PerspexScheduler();
public static PerspexScheduler Instance
{
get { return instance; }
}
public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action) public override IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
{ {
return DispatcherTimer.Run(() => return DispatcherTimer.Run(
{ () =>
action(this, state); {
return false; action(this, state);
}, dueTime); 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"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Rx-Core" 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+win" /> <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+win" /> <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+win" /> <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+win" /> <package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable-net45+win8" userInstalled="true" />
<package id="Splat" version="1.6.1" targetFramework="portable-net45+win" /> <package id="Serilog" version="1.5.5" targetFramework="portable45-net45+win8" userInstalled="true" />
<package id="StyleCop.MSBuild" version="4.7.49.0" targetFramework="portable-net45+win" developmentDependency="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> </packages>

10
Perspex.Controls/Border.cs

@ -51,7 +51,7 @@ namespace Perspex.Controls
Brush background = this.Background; Brush background = this.Background;
Brush borderBrush = this.BorderBrush; Brush borderBrush = this.BorderBrush;
double borderThickness = this.BorderThickness; 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) if (background != null)
{ {
@ -85,10 +85,12 @@ namespace Perspex.Controls
if (content != null) if (content != null)
{ {
content.Measure(availableSize.Deflate(padding)); 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) protected override void OnDetachedFromVisualTree(IRenderRoot oldRoot)
{ {
base.OnDetachedFromVisualTree(oldRoot); base.OnDetachedFromVisualTree(oldRoot);
@ -203,7 +236,7 @@ namespace Perspex.Controls
private void ListenForDefault(IInputElement root) private void ListenForDefault(IInputElement root)
{ {
root.AddHandler(InputElement.KeyDownEvent, this.RootKeyDown, RoutingStrategies.Tunnel); root.AddHandler(InputElement.KeyDownEvent, this.RootKeyDown);
} }
private void StopListeningForDefault(IInputElement root) private void StopListeningForDefault(IInputElement root)

10
Perspex.Controls/ColumnDefinitions.cs

@ -6,9 +6,19 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using System.Linq;
using Perspex.Collections; using Perspex.Collections;
using Perspex.Controls.Parsers;
public class ColumnDefinitions : PerspexList<ColumnDefinition> 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() public ContentControl()
{ {
this.GetObservableWithHistory(ContentProperty).Subscribe(this.SetContentParent);
} }
public object Content public object Content
@ -37,6 +36,12 @@ namespace Perspex.Controls
set { this.SetValue(ContentProperty, value); } set { this.SetValue(ContentProperty, value); }
} }
public ContentPresenter Presenter
{
get;
private set;
}
public HorizontalAlignment HorizontalContentAlignment public HorizontalAlignment HorizontalContentAlignment
{ {
get { return this.GetValue(HorizontalContentAlignmentProperty); } get { return this.GetValue(HorizontalContentAlignmentProperty); }
@ -59,32 +64,8 @@ namespace Perspex.Controls
// We allow ContentControls without ContentPresenters in the template. This can be // 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 // 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. // have a ContentPresenter in the visual tree for that.
var presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter"); this.Presenter = this.FindTemplateChild<ContentPresenter>("contentPresenter");
this.logicalChildren.Source = ((ILogical)this.Presenter)?.LogicalChildren;
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;
}
} }
} }
} }

72
Perspex.Controls/Control.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls
using Perspex.Styling; using Perspex.Styling;
using Splat; using Splat;
public class Control : InputElement, ILogical, IStyleable, IStyleHost public class Control : InputElement, INamed, ILogical, IStyleable, IStyleHost
{ {
public static readonly PerspexProperty<object> DataContextProperty = public static readonly PerspexProperty<object> DataContextProperty =
PerspexProperty.Register<Control, object>("DataContext", inherits: true); PerspexProperty.Register<Control, object>("DataContext", inherits: true);
@ -101,7 +101,7 @@ namespace Perspex.Controls
} }
} }
public string Id public string Name
{ {
get get
{ {
@ -192,6 +192,40 @@ namespace Perspex.Controls
this.RaiseEvent(ev); 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) protected override void OnGotFocus(GotFocusEventArgs e)
{ {
base.OnGotFocus(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) protected override void OnAttachedToVisualTree(IRenderRoot root)
{ {
base.OnAttachedToVisualTree(root); base.OnAttachedToVisualTree(root);

4
Perspex.Controls/ControlExtensions.cs

@ -17,11 +17,11 @@ namespace Perspex.Controls
public static class ControlExtensions 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() return control.GetLogicalDescendents()
.OfType<T>() .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 class DataTemplate : IDataTemplate
{ {
public static readonly DataTemplate Default = 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) public DataTemplate(Func<object, Control> build)
: this(o => true, build) : this(o => true, build)
@ -39,7 +39,9 @@ namespace Perspex.Controls
public static bool IsInstance(object o, Type t) 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) 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) if (content != null)
{ {
content.Measure(availableSize.Deflate(padding)); 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; private ItemContainerGeneratorState state;
public ItemContainerGenerator(ItemsControl owner) public ItemContainerGenerator(Control owner)
{ {
this.Owner = owner; this.Owner = owner;
} }
@ -48,7 +48,7 @@ namespace Perspex.Controls.Generators
} }
} }
protected ItemsControl Owner protected Control Owner
{ {
get; get;
private set; private set;
@ -84,10 +84,18 @@ namespace Perspex.Controls.Generators
foreach (object item in items) foreach (object item in items)
{ {
Control container = this.CreateContainerOverride(item); Control container = this.CreateContainerOverride(item);
container.DataContext = item;
container.TemplatedParent = null; if (container != null)
this.AddInternal(item, container); {
result.Add(container); if (container.DataContext == null)
{
container.DataContext = item;
}
container.TemplatedParent = null;
this.AddInternal(item, container);
result.Add(container);
}
} }
} }
finally 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)); child.Measure(new Size(childSizeX, childSizeY));
Size desired = child.DesiredSize.Value; Size desired = child.DesiredSize;
// Elements distribute their height based on two rules: // Elements distribute their height based on two rules:
// 1) Elements with rowspan/colspan == 1 distribute their height first // 1) Elements with rowspan/colspan == 1 distribute their height first
@ -420,8 +420,8 @@ namespace Perspex.Controls
{ {
int colCount = this.ColumnDefinitions.Count; int colCount = this.ColumnDefinitions.Count;
int rowCount = this.RowDefinitions.Count; int rowCount = this.RowDefinitions.Count;
int colMatrixDim = this.colMatrix.GetUpperBound(0) + 1; int colMatrixDim = this.colMatrix.GetLength(0);
int rowMatrixDim = this.rowMatrix.GetUpperBound(0) + 1; int rowMatrixDim = this.rowMatrix.GetLength(0);
this.RestoreMeasureResults(); this.RestoreMeasureResults();
@ -517,8 +517,8 @@ namespace Perspex.Controls
private void CreateMatrices(int rowCount, int colCount) private void CreateMatrices(int rowCount, int colCount)
{ {
if (this.rowMatrix == null || this.colMatrix == null || if (this.rowMatrix == null || this.colMatrix == null ||
this.rowMatrix.GetUpperBound(0) != rowCount - 1 || this.rowMatrix.GetLength(0) != rowCount ||
this.colMatrix.GetUpperBound(0) != colCount - 1) this.colMatrix.GetLength(0) != colCount)
{ {
this.rowMatrix = new Segment[rowCount, rowCount]; this.rowMatrix = new Segment[rowCount, rowCount];
this.colMatrix = new Segment[colCount, colCount]; this.colMatrix = new Segment[colCount, colCount];
@ -527,10 +527,11 @@ namespace Perspex.Controls
private void ExpandStarCols(Size availableSize) private void ExpandStarCols(Size availableSize)
{ {
int matrixCount = this.colMatrix.GetLength(0);
int columnsCount = this.ColumnDefinitions.Count; int columnsCount = this.ColumnDefinitions.Count;
double width = availableSize.Width; 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) 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); width = Math.Max(0, width);
if (columnsCount > 0) 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) if (this.colMatrix[i, i].Type == GridUnitType.Star)
{ {
@ -559,13 +560,14 @@ namespace Perspex.Controls
private void ExpandStarRows(Size availableSize) private void ExpandStarRows(Size availableSize)
{ {
int matrixCount = this.rowMatrix.GetLength(0);
int rowCount = this.RowDefinitions.Count; int rowCount = this.RowDefinitions.Count;
double height = availableSize.Height; double height = availableSize.Height;
// When expanding star rows, we need to zero out their height before // When expanding star rows, we need to zero out their height before
// calling AssignSize. AssignSize takes care of distributing the // calling AssignSize. AssignSize takes care of distributing the
// available size when there are Mins and Maxs applied. // 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) 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) 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) 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; 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; this.colMatrix[c, c].OfferedSize = this.colMatrix[c, c].DesiredSize;
} }
@ -716,17 +721,20 @@ namespace Perspex.Controls
private void SaveMeasureResults() 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; 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; this.colMatrix[i, j].OriginalSize = this.colMatrix[i, j].OfferedSize;
} }
@ -735,17 +743,20 @@ namespace Perspex.Controls
private void RestoreMeasureResults() 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; 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; 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) public GridWalker(Grid grid, Segment[,] rowMatrix, Segment[,] colMatrix)
{ {
int rowMatrixDim = rowMatrix.GetLength(0);
int colMatrixDim = colMatrix.GetLength(0);
foreach (Control child in grid.Children) foreach (Control child in grid.Children)
{ {
bool starCol = false; bool starCol = false;
@ -809,10 +823,10 @@ namespace Perspex.Controls
bool autoCol = false; bool autoCol = false;
bool autoRow = false; bool autoRow = false;
int col = Math.Min(Grid.GetColumn(child), colMatrix.GetUpperBound(0)); int col = Math.Min(Grid.GetColumn(child), colMatrixDim - 1);
int row = Math.Min(Grid.GetRow(child), rowMatrix.GetUpperBound(0)); int row = Math.Min(Grid.GetRow(child), rowMatrixDim - 1);
int colspan = Math.Min(Grid.GetColumnSpan(child), colMatrix.GetUpperBound(0)); int colspan = Math.Min(Grid.GetColumnSpan(child), colMatrixDim - 1);
int rowspan = Math.Min(Grid.GetRowSpan(child), rowMatrix.GetUpperBound(0)); int rowspan = Math.Min(Grid.GetRowSpan(child), rowMatrixDim - 1);
for (int r = row; r < row + rowspan; r++) 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 2014 MIT Licence. See licence.md for more information.
// </copyright> // </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;
using System.Collections; using System.Collections;
using System.ComponentModel; using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Perspex.Collections; using Perspex.Collections;
@ -16,7 +16,8 @@ namespace Perspex.Controls
using Perspex.Controls.Presenters; using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives; using Perspex.Controls.Primitives;
using Perspex.Controls.Templates; using Perspex.Controls.Templates;
using Perspex.VisualTree; using Perspex.Controls.Utils;
using Perspex.Styling;
public class ItemsControl : TemplatedControl, ILogical public class ItemsControl : TemplatedControl, ILogical
{ {
@ -35,11 +36,20 @@ namespace Perspex.Controls
private PerspexReadOnlyListView<IVisual, ILogical> logicalChildren = private PerspexReadOnlyListView<IVisual, ILogical> logicalChildren =
new PerspexReadOnlyListView<IVisual, ILogical>(x => (ILogical)x); 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() public ItemsControl()
{ {
this.GetObservableWithHistory(ItemsProperty).Subscribe(this.ItemsChanged); this.ItemsChanged(null, null);
} }
public ItemContainerGenerator ItemContainerGenerator 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() protected virtual ItemContainerGenerator CreateItemContainerGenerator()
{ {
return new ItemContainerGenerator(this); return new ItemContainerGenerator(this);
@ -83,24 +107,19 @@ namespace Perspex.Controls
protected override void OnTemplateApplied() protected override void OnTemplateApplied()
{ {
this.presenter = this.FindTemplateChild<ItemsPresenter>("itemsPresenter"); this.Presenter = this.FindTemplateChild<IItemsPresenter>("itemsPresenter");
if (this.presenter != null)
{
this.logicalChildren.Source = ((IVisual)this.presenter.Panel).VisualChildren;
}
} }
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"); this.Classes.Add(":empty");
} }
@ -109,26 +128,25 @@ namespace Perspex.Controls
this.Classes.Remove(":empty"); 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");
{ }
this.Classes.Add(":empty"); else
} {
else this.Classes.Remove(":empty");
{
this.Classes.Remove(":empty");
}
} }
} }
} }

33
Perspex.Controls/ListBoxItem.cs

@ -6,20 +6,51 @@
namespace Perspex.Controls 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 public class ListBoxItem : ContentControl, ISelectable
{ {
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
/// </summary>
public static readonly PerspexProperty<bool> IsSelectedProperty = 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() static ListBoxItem()
{ {
Control.PseudoClass(IsSelectedProperty, ":selected"); Control.PseudoClass(IsSelectedProperty, ":selected");
IsSelectedProperty.Changed.Subscribe(IsSelectedChanged);
} }
/// <summary>
/// Gets or sets the selection state of the item.
/// </summary>
public bool IsSelected public bool IsSelected
{ {
get { return this.GetValue(IsSelectedProperty); } get { return this.GetValue(IsSelectedProperty); }
set { this.SetValue(IsSelectedProperty, value); } 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 ILogical IItemsPanel.ChildLogicalParent
{ {
get { return this.childLogicalParent; } get
set { this.childLogicalParent = value; } {
return this.childLogicalParent;
}
set
{
this.childLogicalParent = value;
this.SetLogicalParent(this.Children);
}
} }
protected virtual void OnChildrenAdded(IEnumerable<Control> child) 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> <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile> <TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@ -34,17 +36,29 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="MenuItemAccessKeyHandler.cs" />
<Compile Include="Parsers\GridLengthsParser.cs" />
<Compile Include="Primitives\AccessText.cs" />
<Compile Include="Border.cs" /> <Compile Include="Border.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="MenuItem.cs" />
<Compile Include="Menu.cs" />
<Compile Include="Button.cs" /> <Compile Include="Button.cs" />
<Compile Include="AdornerTemplate.cs" /> <Compile Include="AdornerTemplate.cs" />
<Compile Include="DropDown.cs" /> <Compile Include="DropDown.cs" />
<Compile Include="IContentControl.cs" /> <Compile Include="IContentControl.cs" />
<Compile Include="IItemsPanel.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\IPopupImpl.cs" />
<Compile Include="Platform\ITopLevelImpl.cs" /> <Compile Include="Platform\ITopLevelImpl.cs" />
<Compile Include="PlacementMode.cs" />
<Compile Include="Popup.cs" /> <Compile Include="Popup.cs" />
<Compile Include="IDataTemplate.cs" /> <Compile Include="IDataTemplate.cs" />
<Compile Include="Presenters\IItemsPresenter.cs" />
<Compile Include="Presenters\IPresenter.cs" /> <Compile Include="Presenters\IPresenter.cs" />
<Compile Include="Presenters\DeckPresenter.cs" />
<Compile Include="Primitives\AdornerDecorator.cs" /> <Compile Include="Primitives\AdornerDecorator.cs" />
<Compile Include="Primitives\AdornerLayer.cs" /> <Compile Include="Primitives\AdornerLayer.cs" />
<Compile Include="RadioButton.cs" /> <Compile Include="RadioButton.cs" />
@ -88,9 +102,11 @@
<Compile Include="Primitives\Thumb.cs" /> <Compile Include="Primitives\Thumb.cs" />
<Compile Include="RequestBringIntoViewEventArgs.cs" /> <Compile Include="RequestBringIntoViewEventArgs.cs" />
<Compile Include="Shapes\Ellipse.cs" /> <Compile Include="Shapes\Ellipse.cs" />
<Compile Include="ToolTip.cs" />
<Compile Include="UserControl.cs" /> <Compile Include="UserControl.cs" />
<Compile Include="Templates\TemplateExtensions.cs" /> <Compile Include="Templates\TemplateExtensions.cs" />
<Compile Include="TextWrapping.cs" /> <Compile Include="TextWrapping.cs" />
<Compile Include="Utils\IEnumerableUtils.cs" />
<Compile Include="Utils\StringUtils.cs" /> <Compile Include="Utils\StringUtils.cs" />
<Compile Include="TopLevel.cs" /> <Compile Include="TopLevel.cs" />
<Compile Include="PopupRoot.cs" /> <Compile Include="PopupRoot.cs" />
@ -118,8 +134,13 @@
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Splat"> <Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
<HintPath>..\packages\Splat.1.6.1\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath> <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>
<Reference Include="System.Reactive.Core"> <Reference Include="System.Reactive.Core">
<HintPath>..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath> <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> <Name>Perspex.Styling</Name>
</ProjectReference> </ProjectReference>
</ItemGroup> </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="$(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. <!-- 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. Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild"> <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 System;
using Perspex.Interactivity; using Perspex.Interactivity;
using Perspex.Platform; using Perspex.Collections;
using Perspex.Rendering; 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 = 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 = 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 = 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 = 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; private PopupRoot popupRoot;
/// <summary>
/// The top level control of the Popup's visual tree.
/// </summary>
private TopLevel topLevel; 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() static Popup()
{ {
IsOpenProperty.Changed.Subscribe(x => IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
{ }
Popup popup = x.Sender as Popup;
if (popup != null) /// <summary>
{ /// Initializes a new instance of the <see cref="Popup"/> class.
if ((bool)x.NewValue) /// </summary>
{ public Popup()
popup.Open(); {
} this.GetObservableWithHistory(ChildProperty).Subscribe(this.ChildChanged);
else
{
popup.Close();
}
}
});
} }
/// <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 public Control Child
{ {
get { return this.GetValue(ChildProperty); } get { return this.GetValue(ChildProperty); }
set { this.SetValue(ChildProperty, value); } 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 public bool IsOpen
{ {
get { return this.GetValue(IsOpenProperty); } get { return this.GetValue(IsOpenProperty); }
set { this.SetValue(IsOpenProperty, value); } 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 public Control PlacementTarget
{ {
get { return this.GetValue(PlacementTargetProperty); } get { return this.GetValue(PlacementTargetProperty); }
set { this.SetValue(PlacementTargetProperty, value); } 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 public bool StaysOpen
{ {
get { return this.GetValue(StaysOpenProperty); } get { return this.GetValue(StaysOpenProperty); }
set { this.SetValue(StaysOpenProperty, value); } 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() public void Open()
{ {
if (this.popupRoot == null) if (this.popupRoot == null)
{ {
this.popupRoot = new PopupRoot() this.popupRoot = new PopupRoot(this.DependencyResolver)
{ {
Parent = this, Parent = this,
[~PopupRoot.ContentProperty] = this[~ChildProperty], [~PopupRoot.ContentProperty] = this[~ChildProperty],
@ -95,9 +201,16 @@ namespace Perspex.Controls
this.popupRoot.AddHandler(PopupRoot.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Bubble, true); this.popupRoot.AddHandler(PopupRoot.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Bubble, true);
this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel); this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel);
this.PopupRootCreated?.Invoke(this, EventArgs.Empty);
this.popupRoot.Show(); this.popupRoot.Show();
this.IsOpen = true;
this.Opened?.Invoke(this, EventArgs.Empty);
} }
/// <summary>
/// Closes the popup.
/// </summary>
public void Close() public void Close()
{ {
if (this.popupRoot != null) if (this.popupRoot != null)
@ -109,30 +222,98 @@ namespace Perspex.Controls
} }
this.IsOpen = false; 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) protected override Size MeasureCore(Size availableSize)
{ {
return new Size(); 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) protected override void OnAttachedToVisualTree(IRenderRoot root)
{ {
base.OnAttachedToVisualTree(root); base.OnAttachedToVisualTree(root);
this.topLevel = root as TopLevel; 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; 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() 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 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) private void MaybeClose(object sender, EventArgs e)
{ {
if (!this.StaysOpen) if (!this.StaysOpen)

64
Perspex.Controls/PopupRoot.cs

@ -6,41 +6,99 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using System;
using Perspex.Interactivity;
using Perspex.Media; using Perspex.Media;
using Perspex.Platform; using Perspex.Platform;
using Splat; 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() static PopupRoot()
{ {
BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White); BackgroundProperty.OverrideDefaultValue(typeof(PopupRoot), Brushes.White);
} }
/// <summary>
/// Initializes a new instance of the <see cref="PopupRoot"/> class.
/// </summary>
public PopupRoot() 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 public new IPopupImpl PlatformImpl
{ {
get { return (IPopupImpl)base.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) public void SetPosition(Point p)
{ {
this.PlatformImpl.SetPosition(p); this.PlatformImpl.SetPosition(p);
} }
/// <summary>
/// Hides the popup.
/// </summary>
public void Hide() public void Hide()
{ {
this.PlatformImpl.Hide(); this.PlatformImpl.Hide();
this.IsVisible = false;
} }
/// <summary>
/// Shows the popup.
/// </summary>
public void Show() public void Show()
{ {
this.PlatformImpl.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) if (child != null)
{ {
child.Measure(availableSize); child.Measure(availableSize);
return child.DesiredSize.Value; return child.DesiredSize;
} }
return new Size(); return new Size();
@ -87,6 +87,8 @@ namespace Perspex.Controls.Presenters
if (content != null) if (content != null)
{ {
result = this.MaterializeDataTemplate(content); result = this.MaterializeDataTemplate(content);
result.Parent = this.TemplatedParent as Control;
var templatedParent = this.TemplatedParent as TemplatedControl; var templatedParent = this.TemplatedParent as TemplatedControl;
if (templatedParent != null) 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 a <see cref="TemplatedControl"/> then that signals that the visual child
/// of the presenter is not a part of the template. /// of the presenter is not a part of the template.
/// </remarks> /// </remarks>
public interface IPresenter : IVisual public interface IPresenter : IVisual, INamed
{ {
} }
} }

32
Perspex.Controls/Presenters/ItemsPresenter.cs

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

7
Perspex.Controls/Presenters/ScrollContentPresenter.cs

@ -76,7 +76,7 @@ namespace Perspex.Controls.Presenters
} }
content.Measure(measureSize); content.Measure(measureSize);
var size = content.DesiredSize.Value; var size = content.DesiredSize;
this.measuredExtent = size; this.measuredExtent = size;
return size.Constrain(availableSize); return size.Constrain(availableSize);
} }
@ -95,7 +95,10 @@ namespace Perspex.Controls.Presenters
if (child != null) 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; return finalSize;
} }

14
Perspex.Controls/Presenters/TextPresenter.cs

@ -100,7 +100,14 @@ namespace Perspex.Controls.Presenters
if (this.caretBlink) 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 selectionStart = this.SelectionStart;
var selectionEnd = this.SelectionEnd; var selectionEnd = this.SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd); var start = Math.Min(selectionStart, selectionEnd);
@ -165,6 +172,7 @@ namespace Perspex.Controls.Presenters
this.FontFamily, this.FontFamily,
this.FontSize, this.FontSize,
this.FontStyle, this.FontStyle,
TextAlignment.Left,
this.FontWeight)) this.FontWeight))
{ {
return formattedText.Measure(); 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 namespace Perspex.Controls.Primitives
{ {
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Perspex.VisualTree; using Perspex.VisualTree;
@ -16,8 +17,14 @@ namespace Perspex.Controls.Primitives
public static PerspexProperty<Visual> AdornedElementProperty = public static PerspexProperty<Visual> AdornedElementProperty =
PerspexProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement"); PerspexProperty.RegisterAttached<AdornerLayer, Visual, Visual>("AdornedElement");
private static PerspexProperty<AdornedElementInfo> AdornedElementInfoProperty =
PerspexProperty.RegisterAttached<AdornerLayer, Visual, AdornedElementInfo>("AdornedElementInfo");
private BoundsTracker tracker = new BoundsTracker();
static AdornerLayer() static AdornerLayer()
{ {
AdornedElementProperty.Changed.Subscribe(AdornedElementChanged);
IsHitTestVisibleProperty.OverrideDefaultValue(typeof(AdornerLayer), false); IsHitTestVisibleProperty.OverrideDefaultValue(typeof(AdornerLayer), false);
} }
@ -45,25 +52,28 @@ namespace Perspex.Controls.Primitives
foreach (var child in this.Children) 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); child.Arrange(info.Bounds.Bounds);
var position = new Point(0, 0) * transform;
child.Arrange(new Rect(position, adorned.Bounds.Size));
} }
else else
{ {
child.Arrange(new Rect(child.DesiredSize.Value)); child.Arrange(new Rect(child.DesiredSize));
} }
} }
return finalSize; 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(); this.InvalidateArrange();
} }
@ -71,5 +81,54 @@ namespace Perspex.Controls.Primitives
{ {
this.InvalidateArrange(); 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;
using System.Reactive; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Perspex.Controls.Templates;
public class ScrollBar : TemplatedControl public class ScrollBar : TemplatedControl
{ {
@ -88,6 +89,18 @@ namespace Perspex.Controls.Primitives
return base.MeasureOverride(availableSize); 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() private bool CalculateIsVisible()
{ {
switch (this.Visibility) switch (this.Visibility)

350
Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -7,112 +7,219 @@
namespace Perspex.Controls.Primitives namespace Perspex.Controls.Primitives
{ {
using System; using System;
using System.Collections;
using System.Linq; using System.Linq;
using System.Collections;
using System.Collections.Specialized;
using Perspex.Controls.Utils;
using Perspex.Input; using Perspex.Input;
using Perspex.VisualTree; 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 = 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() static SelectingItemsControl()
{ {
FocusableProperty.OverrideDefaultValue(typeof(SelectingItemsControl), true); IsSelectedChangedEvent.AddClassHandler<SelectingItemsControl>(x => x.ItemIsSelectedChanged);
SelectedIndexProperty.Changed.Subscribe(SelectedIndexChanged);
SelectedItemProperty.Changed.Subscribe(x => SelectedItemProperty.Changed.Subscribe(SelectedItemChanged);
{ }
var control = x.Sender as SelectingItemsControl;
if (control != null) /// <summary>
{ /// Gets or sets the index of the selected item.
control.SelectedItemChanged(x.NewValue); /// </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 public object SelectedItem
{ {
get { return this.GetValue(SelectedItemProperty); } get { return this.GetValue(SelectedItemProperty); }
set { this.SetValue(SelectedItemProperty, value); } 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) base.ItemsChanged(oldValue, newValue);
{
int index = 0; 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) 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: this.SelectedItem = this.ItemContainerGenerator.GetItemForContainer(next);
offset = -1; }
}
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; break;
case FocusNavigationDirection.Down: case Key.Left:
offset = 1; this.MoveSelection(FocusNavigationDirection.Left);
break; 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 container = this.ItemContainerGenerator.GetContainerForItem(selected);
var index = currentIndex + offset;
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) e.Handled = true;
{
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;
} }
/// <summary>
/// Called when the pointer is pressed within the control.
/// </summary>
/// <param name="e">The event args.</param>
protected override void OnPointerPressed(PointerPressEventArgs e) protected override void OnPointerPressed(PointerPressEventArgs e)
{ {
IVisual source = (IVisual)e.Source; IVisual source = (IVisual)e.Source;
@ -128,12 +235,120 @@ namespace Perspex.Controls.Primitives
if (item != null) if (item != null)
{ {
this.SelectedItem = item; this.SelectedItem = item;
selectable.BringIntoView();
FocusManager.Instance.Focus(selectable);
} }
} }
e.Handled = true; 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) private void SelectedItemChanged(object selected)
{ {
var containers = this.ItemContainerGenerator.GetAll() var containers = this.ItemContainerGenerator.GetAll()
@ -143,10 +358,29 @@ namespace Perspex.Controls.Primitives
this.ItemContainerGenerator.GetContainerForItem(selected) : this.ItemContainerGenerator.GetContainerForItem(selected) :
null; null;
if (this.Presenter != null && this.Presenter.Panel != null)
{
KeyboardNavigation.SetTabOnceActiveElement(this.Presenter.Panel, selectedContainer);
}
foreach (var item in containers) foreach (var item in containers)
{ {
item.IsSelected = item == selectedContainer; 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() 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 public TabItem SelectedTab

22
Perspex.Controls/Primitives/TemplatedControl.cs

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

4
Perspex.Controls/Primitives/Track.cs

@ -103,11 +103,11 @@ namespace Perspex.Controls.Primitives
if (this.Orientation == Orientation.Horizontal) if (this.Orientation == Orientation.Horizontal)
{ {
return new Size(0, thumb.DesiredSize.Value.Height); return new Size(0, thumb.DesiredSize.Height);
} }
else 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 namespace Perspex.Controls
{ {
using System.Linq;
using Perspex.Collections; using Perspex.Collections;
using Perspex.Controls.Parsers;
public class RowDefinitions : PerspexList<RowDefinition> 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) if (this.geometry == null || this.geometrySize != this.Bounds.Size)
{ {
var rect = new Rect(0, 0, this.Bounds.Size.Width, this.Bounds.Size.Height); var rect = new Rect(this.Bounds.Size).Deflate(this.StrokeThickness);
rect = rect.Deflate(this.StrokeThickness / 2);
this.geometry = new EllipseGeometry(rect); this.geometry = new EllipseGeometry(rect);
this.geometrySize = this.Bounds.Size; 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) 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; this.geometrySize = this.Bounds.Size;
} }

55
Perspex.Controls/StackPanel.cs

@ -7,10 +7,8 @@
namespace Perspex.Controls namespace Perspex.Controls
{ {
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using Perspex.Input;
using System.Threading.Tasks;
public enum Orientation public enum Orientation
{ {
@ -18,7 +16,7 @@ namespace Perspex.Controls
Horizontal, Horizontal,
} }
public class StackPanel : Panel public class StackPanel : Panel, INavigablePanel
{ {
public static readonly PerspexProperty<double> GapProperty = public static readonly PerspexProperty<double> GapProperty =
PerspexProperty.Register<StackPanel, double>("Gap"); PerspexProperty.Register<StackPanel, double>("Gap");
@ -38,6 +36,49 @@ namespace Perspex.Controls
set { this.SetValue(OrientationProperty, value); } 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) protected override Size MeasureOverride(Size availableSize)
{ {
double childAvailableWidth = double.PositiveInfinity; double childAvailableWidth = double.PositiveInfinity;
@ -75,7 +116,7 @@ namespace Perspex.Controls
foreach (Control child in this.Children) foreach (Control child in this.Children)
{ {
child.Measure(new Size(childAvailableWidth, childAvailableHeight)); child.Measure(new Size(childAvailableWidth, childAvailableHeight));
Size size = child.DesiredSize.Value; Size size = child.DesiredSize;
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {
@ -109,8 +150,8 @@ namespace Perspex.Controls
foreach (Control child in this.Children) foreach (Control child in this.Children)
{ {
double childWidth = child.DesiredSize.Value.Width; double childWidth = child.DesiredSize.Width;
double childHeight = child.DesiredSize.Value.Height; double childHeight = child.DesiredSize.Height;
if (Orientation == Orientation.Vertical) if (Orientation == Orientation.Vertical)
{ {

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

Loading…
Cancel
Save