Browse Source

Merge remote-tracking branch 'upstream/master' into gridsplitter

pull/362/head
susloparov 11 years ago
parent
commit
382a8d5fa1
  1. 23
      samples/TestApplicationShared/GalleryStyle.cs
  2. 3
      samples/TestApplicationShared/MainWindow.cs
  3. 84
      samples/XamlTestApplicationPcl/TestScrollable.cs
  4. 10
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  5. 1
      samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
  6. 15
      src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs
  7. 1
      src/Markup/Perspex.Markup.Xaml/Converters/ClassesTypeConverter.cs
  8. 30
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  9. 71
      src/Markup/Perspex.Markup/Data/ExpressionObserver.cs
  10. 7
      src/Perspex.Application/Application.cs
  11. 14
      src/Perspex.Base/BindingDescriptor.cs
  12. 31
      src/Perspex.Base/Collections/PerspexList.cs
  13. 19
      src/Perspex.Base/PerspexObject.cs
  14. 4
      src/Perspex.Controls/Button.cs
  15. 253
      src/Perspex.Controls/Classes.cs
  16. 40
      src/Perspex.Controls/ContentControl.cs
  17. 239
      src/Perspex.Controls/Control.cs
  18. 30
      src/Perspex.Controls/ControlExtensions.cs
  19. 4
      src/Perspex.Controls/Decorator.cs
  20. 4
      src/Perspex.Controls/DropDown.cs
  21. 74
      src/Perspex.Controls/Expander.cs
  22. 19
      src/Perspex.Controls/Generators/IItemContainerGenerator.cs
  23. 19
      src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs
  24. 45
      src/Perspex.Controls/Generators/ItemContainer.cs
  25. 22
      src/Perspex.Controls/Generators/ItemContainerEventArgs.cs
  26. 77
      src/Perspex.Controls/Generators/ItemContainerGenerator.cs
  27. 5
      src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs
  28. 87
      src/Perspex.Controls/Generators/TreeContainerIndex.cs
  29. 110
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  30. 5
      src/Perspex.Controls/IControl.cs
  31. 25
      src/Perspex.Controls/IPseudoClasses.cs
  32. 25
      src/Perspex.Controls/IReparentingControl.cs
  33. 28
      src/Perspex.Controls/IReparentingHost.cs
  34. 215
      src/Perspex.Controls/ItemsControl.cs
  35. 31
      src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs
  36. 28
      src/Perspex.Controls/MenuItem.cs
  37. 136
      src/Perspex.Controls/Mixins/ContentControlMixin.cs
  38. 4
      src/Perspex.Controls/Mixins/SelectableMixin.cs
  39. 79
      src/Perspex.Controls/Panel.cs
  40. 17
      src/Perspex.Controls/Perspex.Controls.csproj
  41. 18
      src/Perspex.Controls/Presenters/CarouselPresenter.cs
  42. 30
      src/Perspex.Controls/Presenters/ContentPresenter.cs
  43. 58
      src/Perspex.Controls/Presenters/ItemsPresenter.cs
  44. 60
      src/Perspex.Controls/Presenters/ScrollContentPresenter.cs
  45. 2
      src/Perspex.Controls/Primitives/AdornerDecorator.cs
  46. 41
      src/Perspex.Controls/Primitives/HeaderedItemsControl.cs
  47. 58
      src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs
  48. 97
      src/Perspex.Controls/Primitives/IScrollInfo.cs
  49. 44
      src/Perspex.Controls/Primitives/IScrollable.cs
  50. 5
      src/Perspex.Controls/Primitives/PopupRoot.cs
  51. 126
      src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs
  52. 126
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  53. 40
      src/Perspex.Controls/Primitives/TabStrip.cs
  54. 12
      src/Perspex.Controls/Primitives/TabStripItem.cs
  55. 28
      src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs
  56. 81
      src/Perspex.Controls/Primitives/TemplatedControl.cs
  57. 4
      src/Perspex.Controls/Primitives/Track.cs
  58. 4
      src/Perspex.Controls/ProgressBar.cs
  59. 5
      src/Perspex.Controls/ScrollViewer.cs
  60. 93
      src/Perspex.Controls/TabControl.cs
  61. 29
      src/Perspex.Controls/Templates/TemplateExtensions.cs
  62. 6
      src/Perspex.Controls/TextBlock.cs
  63. 4
      src/Perspex.Controls/TextBox.cs
  64. 7
      src/Perspex.Controls/TopLevel.cs
  65. 20
      src/Perspex.Controls/TreeView.cs
  66. 17
      src/Perspex.Controls/TreeViewItem.cs
  67. 9
      src/Perspex.Diagnostics/ViewModels/TreeNode.cs
  68. 4
      src/Perspex.Layout/LayoutManager.cs
  69. 12
      src/Perspex.Layout/Layoutable.cs
  70. 2
      src/Perspex.SceneGraph/Perspex.SceneGraph.csproj
  71. 18
      src/Perspex.SceneGraph/Point.cs
  72. 77
      src/Perspex.SceneGraph/Rect.cs
  73. 97
      src/Perspex.SceneGraph/Rendering/RendererMixin.cs
  74. 130
      src/Perspex.SceneGraph/Visual.cs
  75. 2
      src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs
  76. 2
      src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs
  77. 5
      src/Perspex.Styling/ILogical.cs
  78. 2
      src/Perspex.Styling/Perspex.Styling.csproj
  79. 4
      src/Perspex.Styling/Properties/AssemblyInfo.cs
  80. 155
      src/Perspex.Styling/Styling/Classes.cs
  81. 6
      src/Perspex.Styling/Styling/IGlobalStyles.cs
  82. 2
      src/Perspex.Styling/Styling/IStyleHost.cs
  83. 12
      src/Perspex.Styling/Styling/IStyleRoot.cs
  84. 9
      src/Perspex.Styling/Styling/IStyleable.cs
  85. 2
      src/Perspex.Styling/Styling/Selector.cs
  86. 23
      src/Perspex.Styling/Styling/Selectors.cs
  87. 8
      src/Perspex.Styling/Styling/Style.cs
  88. 139
      src/Perspex.Styling/Styling/StyleActivator.cs
  89. 13
      src/Perspex.Styling/Styling/StyleBinding.cs
  90. 28
      src/Perspex.Styling/Styling/Styler.cs
  91. 3
      src/Perspex.Themes.Default/Button.paml
  92. 3
      src/Perspex.Themes.Default/DefaultTheme.paml
  93. 123
      src/Perspex.Themes.Default/Expander.paml
  94. 26
      src/Perspex.Themes.Default/MenuItem.paml
  95. 3
      src/Perspex.Themes.Default/Perspex.Themes.Default.csproj
  96. 36
      src/Perspex.Themes.Default/TabControl.paml
  97. 6
      src/Perspex.Themes.Default/TabStrip.paml
  98. 8
      src/Perspex.Themes.Default/TabStripItem.paml
  99. 3
      src/Perspex.Themes.Default/ToggleButton.paml
  100. 2
      src/Perspex.Themes.Default/TreeViewItem.paml

23
samples/TestApplicationShared/GalleryStyle.cs

@ -19,7 +19,7 @@ namespace TestApplication
{
this.AddRange(new[]
{
new Style (s => s.Class(":container").OfType<TabControl> ())
new Style (s => s.Class("container").OfType<TabControl> ())
{
Setters = new[]
{
@ -27,15 +27,15 @@ namespace TestApplication
}
},
new Style(s => s.Class(":container").OfType<TabControl>().Child().Child().Child().Child().Child().OfType<TabItem>())
new Style(s => s.Class("container").OfType<TabControl>().Child().Child().Child().Child().Child().OfType<TabStripItem>())
{
Setters = new[]
{
new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate<TabItem> (TabItemTemplate)),
new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate<TabStripItem>(TabStripItemTemplate)),
}
},
new Style(s => s.Name("internalStrip").OfType<TabStrip>().Child().OfType<TabItem>())
new Style(s => s.Name("PART_TabStrip").OfType<TabStrip>().Child().OfType<TabStripItem>())
{
Setters = new[]
{
@ -44,7 +44,7 @@ namespace TestApplication
}
},
new Style(s => s.Name("internalStrip").OfType<TabStrip>().Child().OfType<TabItem>().Class(":selected"))
new Style(s => s.Name("PART_TabStrip").OfType<TabStrip>().Child().OfType<TabStripItem>().Class(":selected"))
{
Setters = new[]
{
@ -55,7 +55,7 @@ namespace TestApplication
});
}
public static Control TabItemTemplate(TabItem control)
public static Control TabStripItemTemplate(TabStripItem control)
{
return new ContentPresenter
{
@ -72,8 +72,8 @@ namespace TestApplication
}
})
},
Name = "headerPresenter",
[~ContentPresenter.ContentProperty] = control[~HeaderedContentControl.HeaderProperty],
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
};
}
@ -96,9 +96,10 @@ namespace TestApplication
{
Content = new TabStrip
{
Name = "PART_TabStrip",
ItemsPanel = new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical, Gap = 4 }),
Margin = new Thickness(0, 10, 0, 0),
Name = "internalStrip",
MemberSelector = TabControl.HeaderSelector,
[!ItemsControl.ItemsProperty] = control[!ItemsControl.ItemsProperty],
[!!SelectingItemsControl.SelectedItemProperty] = control[!!SelectingItemsControl.SelectedItemProperty],
}
@ -106,8 +107,8 @@ namespace TestApplication
},
new Carousel
{
Name = "carousel",
MemberSelector = control.ContentSelector,
Name = "PART_Content",
MemberSelector = TabControl.ContentSelector,
[~Carousel.TransitionProperty] = control[~TabControl.TransitionProperty],
[!Carousel.ItemsProperty] = control[!ItemsControl.ItemsProperty],
[!Carousel.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty],

3
samples/TestApplicationShared/MainWindow.cs

@ -104,8 +104,7 @@ namespace TestApplication
};
container.Classes.Add(":container");
container.Classes.Add("container");
window.Show();
return window;

84
samples/XamlTestApplicationPcl/TestScrollable.cs

@ -0,0 +1,84 @@
using System;
using Perspex;
using Perspex.Controls;
using Perspex.Controls.Primitives;
using Perspex.Media;
namespace XamlTestApplication
{
public class TestScrollable : Control, IScrollable
{
private int itemCount = 100;
private Size _extent;
private Vector _offset;
private Size _viewport;
private Size _lineSize;
public Action InvalidateScroll { get; set; }
Size IScrollable.Extent
{
get { return _extent; }
}
Vector IScrollable.Offset
{
get { return _offset; }
set
{
_offset = value;
InvalidateVisual();
}
}
Size IScrollable.Viewport
{
get { return _viewport; }
}
protected override Size MeasureOverride(Size availableSize)
{
using (var line = new FormattedText(
"Item 100",
TextBlock.GetFontFamily(this),
TextBlock.GetFontSize(this),
TextBlock.GetFontStyle(this),
TextAlignment.Left,
TextBlock.GetFontWeight(this)))
{
line.Constraint = availableSize;
_lineSize = line.Measure();
return new Size(_lineSize.Width, _lineSize.Height * itemCount);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
_viewport = new Size(finalSize.Width, finalSize.Height / _lineSize.Height);
_extent = new Size(_lineSize.Width, itemCount + 1);
InvalidateScroll?.Invoke();
return finalSize;
}
public override void Render(DrawingContext context)
{
var y = 0.0;
for (var i = (int)_offset.Y; i < itemCount; ++i)
{
using (var line = new FormattedText(
"Item " + (i + 1),
TextBlock.GetFontFamily(this),
TextBlock.GetFontSize(this),
TextBlock.GetFontStyle(this),
TextAlignment.Left,
TextBlock.GetFontWeight(this)))
{
context.DrawText(Brushes.Black, new Point(-_offset.X, y), line);
y += _lineSize.Height;
}
}
}
}
}

10
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -1,15 +1,16 @@
<Window x:Class="XamlTestApplication.MainWindow"
xmlns="https://github.com/perspex"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XamlTestApplication;assembly=XamlTestApplicationPcl"
xmlns:vm="clr-namespace:XamlTestApplication.ViewModels;assembly=XamlTestApplicationPcl"
Title="Perspex Test Application" Width="800" Height="600">
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<Menu Grid.ColumnSpan="2">
<MenuItem Header="_File">
<MenuItem Header="_File" Background="Red">
<MenuItem Header="_Hello">
<MenuItem Header="_Goodbye"/>
<Separator/>
<MenuItem Header="_World"/>
<MenuItem Header="_World" Background="Red"/>
</MenuItem>
<Separator Background="Red"/>
<MenuItem Header="_Test"/>
@ -285,6 +286,11 @@
</Border>
</Grid>
</TabItem>
<TabItem Header="IScrollable">
<ScrollViewer>
<local:TestScrollable/>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</Window>

1
samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj

@ -41,6 +41,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestScrollable.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Compile Include="ViewModels\TestItem.cs" />
<Compile Include="ViewModels\TestNode.cs" />

15
src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs

@ -5,6 +5,7 @@ using System;
using System.Globalization;
using OmniXaml.TypeConversion;
using Perspex.Media.Imaging;
using Perspex.Platform;
namespace Perspex.Markup.Xaml.Converters
{
@ -22,7 +23,19 @@ namespace Perspex.Markup.Xaml.Converters
public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value)
{
return new Bitmap((string)value);
var uri = new Uri((string)value, UriKind.RelativeOrAbsolute);
var scheme = uri.IsAbsoluteUri ? uri.Scheme : "file";
switch (scheme)
{
case "file":
return new Bitmap((string)value);
case "resource":
var assets = PerspexLocator.Current.GetService<IAssetLoader>();
return new Bitmap(assets.Open(uri));
default:
throw new NotSupportedException($"Unsupported bitmap URI scheme: {uri.Scheme}.");
}
}
public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType)

1
src/Markup/Perspex.Markup.Xaml/Converters/ClassesTypeConverter.cs

@ -4,6 +4,7 @@
using System;
using System.Globalization;
using OmniXaml.TypeConversion;
using Perspex.Controls;
using Perspex.Styling;
namespace Perspex.Markup.Xaml.Converters

30
src/Markup/Perspex.Markup.Xaml/Data/Binding.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Perspex.Controls;
@ -216,15 +217,13 @@ namespace Perspex.Markup.Xaml.Data
if (!targetIsDataContext)
{
var update = target.GetObservable(Control.DataContextProperty)
.Skip(1)
.Select(_ => Unit.Default);
var result = new ExpressionObserver(
() => target.GetValue(Control.DataContextProperty),
path);
/// TODO: Instead of doing this, make the ExpressionObserver accept an "update"
/// observable as doing it this way can will cause a leak in Binding as this
/// observable is never unsubscribed.
target.GetObservable(Control.DataContextProperty).Subscribe(x =>
result.UpdateRoot());
path,
update);
return result;
}
@ -245,19 +244,14 @@ namespace Perspex.Markup.Xaml.Data
{
Contract.Requires<ArgumentNullException>(target != null);
var update = target.GetObservable(Control.TemplatedParentProperty)
.Skip(1)
.Select(_ => Unit.Default);
var result = new ExpressionObserver(
() => target.GetValue(Control.TemplatedParentProperty),
path);
if (target.GetValue(Control.TemplatedParentProperty) == null)
{
// TemplatedParent should only be set once, so only listen for the first non-null
// value.
target.GetObservable(Control.TemplatedParentProperty)
.Where(x => x != null)
.Take(1)
.Subscribe(x => result.UpdateRoot());
}
path,
update);
return result;
}

71
src/Markup/Perspex.Markup/Data/ExpressionObserver.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Perspex.Markup.Data.Plugins;
@ -29,10 +30,11 @@ namespace Perspex.Markup.Data
private readonly object _root;
private readonly Func<object> _rootGetter;
private readonly IObservable<object> _rootObservable;
private readonly IObservable<Unit> _update;
private IDisposable _rootObserverSubscription;
private IDisposable _updateSubscription;
private int _count;
private readonly ExpressionNode _node;
private ISubject<object> _empty;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
@ -78,12 +80,18 @@ namespace Perspex.Markup.Data
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
public ExpressionObserver(Func<object> rootGetter, string expression)
/// <param name="update">An observable which triggers a re-read of the getter.</param>
public ExpressionObserver(
Func<object> rootGetter,
string expression,
IObservable<Unit> update)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
Contract.Requires<ArgumentNullException>(update != null);
_rootGetter = rootGetter;
_update = update;
if (!string.IsNullOrWhiteSpace(expression))
{
@ -104,7 +112,11 @@ namespace Perspex.Markup.Data
public bool SetValue(object value)
{
IncrementCount();
UpdateRoot();
if (_rootGetter != null && _node != null)
{
_node.Target = _rootGetter();
}
try
{
@ -156,6 +168,11 @@ namespace Perspex.Markup.Data
/// <inheritdoc/>
string IDescription.Description => Expression;
/// <summary>
/// Gets the root expression node. Used for testing.
/// </summary>
internal ExpressionNode Node => _node;
/// <summary>
/// Gets the leaf node.
/// </summary>
@ -169,26 +186,6 @@ namespace Perspex.Markup.Data
}
}
/// <summary>
/// Causes the root object to be re-read from the root getter.
/// </summary>
/// TODO: Instead of doing this, make the object accept an "update" observable
/// as doing it this way can cause a leak in Binding.
public void UpdateRoot()
{
if (_count > 0 && _rootGetter != null)
{
if (_node != null)
{
_node.Target = _rootGetter();
}
else
{
_empty?.OnNext(_rootGetter());
}
}
}
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
@ -204,14 +201,23 @@ namespace Perspex.Markup.Data
subscription.Dispose();
});
}
else if (_rootObservable != null)
{
return _rootObservable.Subscribe(observer);
}
else
{
if (_empty == null)
if (_update == null)
{
_empty = new BehaviorSubject<object>(_rootGetter());
return Observable.Never<object>().StartWith(_root).Subscribe(observer);
}
else
{
return _update
.Select(_ => _rootGetter())
.StartWith(_rootGetter())
.Subscribe(observer);
}
return _empty.Subscribe(observer);
}
}
@ -222,6 +228,11 @@ namespace Perspex.Markup.Data
if (_rootGetter != null)
{
_node.Target = _rootGetter();
if (_update != null)
{
_updateSubscription = _update.Subscribe(x => _node.Target = _rootGetter());
}
}
else if (_rootObservable != null)
{
@ -244,6 +255,12 @@ namespace Perspex.Markup.Data
_rootObserverSubscription = null;
}
if (_updateSubscription != null)
{
_updateSubscription.Dispose();
_updateSubscription = null;
}
_node.Target = null;
}
}

7
src/Perspex.Application/Application.cs

@ -32,7 +32,7 @@ namespace Perspex
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : IGlobalDataTemplates, IGlobalStyles
public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot
{
static Action _platformInitializationCallback;
@ -135,6 +135,11 @@ namespace Perspex
protected set;
}
/// <summary>
/// Gets the styling parent of the application, which is null.
/// </summary>
IStyleHost IStyleHost.StylingParent => null;
/// <summary>
/// Runs the application's main loop until the <see cref="ICloseable"/> is closed.
/// </summary>

14
src/Perspex.Base/BindingDescriptor.cs

@ -78,6 +78,18 @@ namespace Perspex
set;
}
/// <summary>
/// Gets or sets the source observable.
/// </summary>
/// <remarks>
/// If null, then <see cref="Source"/>.<see cref="Property"/> will be used.
/// </remarks>
public IObservable<object> SourceObservable
{
get;
set;
}
/// <summary>
/// Gets a description of the binding.
/// </summary>
@ -128,7 +140,7 @@ namespace Perspex
/// <inheritdoc/>
protected override IDisposable SubscribeCore(IObserver<object> observer)
{
return Source.GetObservable(Property).Subscribe(observer);
return (SourceObservable ?? Source.GetObservable(Property)).Subscribe(observer);
}
}
}

31
src/Perspex.Base/Collections/PerspexList.cs

@ -52,7 +52,7 @@ namespace Perspex.Collections
/// </item>
/// </list>
/// </remarks>
public class PerspexList<T> : IPerspexList<T>, IList, INotifyCollectionChanged, INotifyPropertyChanged
public class PerspexList<T> : IPerspexList<T>, IList
{
private List<T> _inner;
@ -150,7 +150,8 @@ namespace Perspex.Collections
var e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
value,
old);
old,
index);
CollectionChanged(this, e);
}
}
@ -171,7 +172,7 @@ namespace Perspex.Collections
/// Adds an item to the collection.
/// </summary>
/// <param name="item">The item.</param>
public void Add(T item)
public virtual void Add(T item)
{
Validate?.Invoke(item);
int index = _inner.Count;
@ -183,7 +184,7 @@ namespace Perspex.Collections
/// Adds multiple items to the collection.
/// </summary>
/// <param name="items">The items.</param>
public void AddRange(IEnumerable<T> items)
public virtual void AddRange(IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(items != null);
@ -247,6 +248,16 @@ namespace Perspex.Collections
return _inner.GetEnumerator();
}
/// <summary>
/// Gets a range of items from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
public IEnumerable<T> GetRange(int index, int count)
{
return _inner.GetRange(index, count);
}
/// <summary>
/// Gets the index of the specified item in the collection.
/// </summary>
@ -264,7 +275,7 @@ namespace Perspex.Collections
/// </summary>
/// <param name="index">The index.</param>
/// <param name="item">The item.</param>
public void Insert(int index, T item)
public virtual void Insert(int index, T item)
{
Validate?.Invoke(item);
_inner.Insert(index, item);
@ -276,7 +287,7 @@ namespace Perspex.Collections
/// </summary>
/// <param name="index">The index.</param>
/// <param name="items">The items.</param>
public void InsertRange(int index, IEnumerable<T> items)
public virtual void InsertRange(int index, IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(items != null);
@ -302,7 +313,7 @@ namespace Perspex.Collections
/// </summary>
/// <param name="item">The item.</param>
/// <returns>True if the item was found and removed, otherwise false.</returns>
public bool Remove(T item)
public virtual bool Remove(T item)
{
int index = _inner.IndexOf(item);
@ -320,7 +331,7 @@ namespace Perspex.Collections
/// Removes multiple items from the collection.
/// </summary>
/// <param name="items">The items.</param>
public void RemoveAll(IEnumerable<T> items)
public virtual void RemoveAll(IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(items != null);
@ -337,7 +348,7 @@ namespace Perspex.Collections
/// Removes the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
public void RemoveAt(int index)
public virtual void RemoveAt(int index)
{
T item = _inner[index];
_inner.RemoveAt(index);
@ -349,7 +360,7 @@ namespace Perspex.Collections
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
public void RemoveRange(int index, int count)
public virtual void RemoveRange(int index, int count)
{
if (count > 0)
{

19
src/Perspex.Base/PerspexObject.cs

@ -163,13 +163,7 @@ namespace Perspex
{
get
{
return new BindingDescriptor
{
Mode = binding.Mode,
Priority = binding.Priority,
Property = binding.Property,
Source = this,
};
return CreateBindingDescriptor(binding);
}
set
@ -203,6 +197,17 @@ namespace Perspex
}
}
protected virtual BindingDescriptor CreateBindingDescriptor(BindingDescriptor source)
{
return new BindingDescriptor
{
Mode = source.Mode,
Priority = source.Priority,
Property = source.Property,
Source = this,
};
}
public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();

4
src/Perspex.Controls/Button.cs

@ -215,7 +215,7 @@ namespace Perspex.Controls
{
base.OnPointerPressed(e);
Classes.Add(":pressed");
PseudoClasses.Add(":pressed");
e.Device.Capture(this);
e.Handled = true;
@ -231,7 +231,7 @@ namespace Perspex.Controls
base.OnPointerReleased(e);
e.Device.Capture(null);
Classes.Remove(":pressed");
PseudoClasses.Remove(":pressed");
e.Handled = true;
if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover"))

253
src/Perspex.Controls/Classes.cs

@ -0,0 +1,253 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Perspex.Collections;
namespace Perspex.Controls
{
/// <summary>
/// Holds a collection of style classes for an <see cref="IControl"/>.
/// </summary>
/// <remarks>
/// Similar to CSS, each control may have any number of styling classes applied.
/// </remarks>
public class Classes : PerspexList<string>, IPseudoClasses
{
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
public Classes()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
/// <param name="items">The initial items.</param>
public Classes(IEnumerable<string> items)
: base(items)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary>
/// <param name="items">The initial items.</param>
public Classes(params string[] items)
: base(items)
{
}
/// <summary>
/// Adds a style class to the collection.
/// </summary>
/// <param name="name">The class name.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void Add(string name)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
base.Add(name);
}
}
/// <summary>
/// Adds a style classes to the collection.
/// </summary>
/// <param name="names">The class names.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void AddRange(IEnumerable<string> names)
{
var c = new List<string>();
foreach (var name in names)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
c.Add(name);
}
}
base.AddRange(c);
}
/// <summary>
/// Inserts a style class into the collection.
/// </summary>
/// <param name="index">The index to insert the class at.</param>
/// <param name="name">The class name.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void Insert(int index, string name)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
base.Insert(index, name);
}
}
/// <summary>
/// Inserts style classes into the collection.
/// </summary>
/// <param name="index">The index to insert the class at.</param>
/// <param name="names">The class names.</param>
/// <remarks>
/// Only standard classes may be added via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void InsertRange(int index, IEnumerable<string> names)
{
var c = new List<string>();
foreach (var name in names)
{
ThrowIfPseudoclass(name, "added");
if (!Contains(name))
{
c.Add(name);
}
}
base.InsertRange(index, c);
}
/// <summary>
/// Removes a style class from the collection.
/// </summary>
/// <param name="name">The class name.</param>
/// <remarks>
/// Only standard classes may be removed via this method. To remove pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override bool Remove(string name)
{
ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
}
/// <summary>
/// Removes style classes from the collection.
/// </summary>
/// <param name="names">The class name.</param>
/// <remarks>
/// Only standard classes may be removed via this method. To remove pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void RemoveAll(IEnumerable<string> names)
{
var c = new List<string>();
foreach (var name in names)
{
ThrowIfPseudoclass(name, "removed");
if (!Contains(name))
{
c.Add(name);
}
}
base.RemoveAll(c);
}
/// <summary>
/// Removes a style class from the collection.
/// </summary>
/// <param name="index">The index of the class in the collection.</param>
/// <remarks>
/// Only standard classes may be removed via this method. To remove pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="Control.PseudoClasses"/>
/// property.
/// </remarks>
public override void RemoveAt(int index)
{
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
}
/// <summary>
/// Removes style classes from the collection.
/// </summary>
/// <param name="index">The first index to remove.</param>
/// <param name="count">The number of items to remove.</param>
public override void RemoveRange(int index, int count)
{
var names = GetRange(index, count);
base.RemoveRange(index, count);
}
/// <summary>
/// Removes all non-pseudoclasses in the collection and adds a new set.
/// </summary>
/// <param name="source">The new contents of the collection.</param>
public void Replace(IList<string> source)
{
var toRemove = new List<string>();
foreach (var name in source)
{
ThrowIfPseudoclass(name, "added");
}
foreach (var name in this)
{
if (!name.StartsWith(":"))
{
toRemove.Add(name);
}
}
base.RemoveAll(toRemove);
base.AddRange(source);
}
/// <inheritdoc/>
void IPseudoClasses.Add(string name)
{
if (!Contains(name))
{
base.Add(name);
}
}
/// <inheritdoc/>
bool IPseudoClasses.Remove(string name)
{
return base.Remove(name);
}
private void ThrowIfPseudoclass(string name, string operation)
{
if (name.StartsWith(":"))
{
throw new ArgumentException(
$"The pseudoclass '{name}' may only be {operation} by the control itself.");
}
}
}
}

40
src/Perspex.Controls/ContentControl.cs

@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Perspex.Collections;
using System.Linq;
using System.Reactive.Linq;
using Perspex.Controls.Mixins;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
@ -14,7 +16,7 @@ namespace Perspex.Controls
/// <summary>
/// Displays <see cref="Content"/> according to a <see cref="FuncDataTemplate"/>.
/// </summary>
public class ContentControl : TemplatedControl, IContentControl, IReparentingHost
public class ContentControl : TemplatedControl, IContentControl
{
/// <summary>
/// Defines the <see cref="Content"/> property.
@ -35,17 +37,11 @@ namespace Perspex.Controls
PerspexProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
/// <summary>
/// Initializes static members of the <see cref="Button"/> class.
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static ContentControl()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ContentControl"/> class.
/// </summary>
public ContentControl()
{
ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren);
}
/// <summary>
@ -85,31 +81,15 @@ namespace Perspex.Controls
set { SetValue(VerticalContentAlignmentProperty, value); }
}
/// <summary>
/// Gets a writeable logical children collection from the host.
/// </summary>
IPerspexList<ILogical> IReparentingHost.LogicalChildren => LogicalChildren;
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool IReparentingHost.WillReparentChildrenOf(IControl control)
{
return control is IContentPresenter && control.TemplatedParent == this;
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
// We allow ContentControls without ContentPresenters in the template. This can be
// useful for e.g. a simple ToggleButton that displays an image. There's no need to
// have a ContentPresenter in the visual tree for that.
Presenter = nameScope.Find<ContentPresenter>("PART_ContentPresenter");
Presenter = e.NameScope.Find<ContentPresenter>("PART_ContentPresenter");
}
}
}

239
src/Perspex.Controls/Control.cs

@ -2,12 +2,18 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Perspex.Collections;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.Interactivity;
using Perspex.LogicalTree;
using Perspex.Rendering;
using Perspex.Styling;
@ -70,9 +76,11 @@ namespace Perspex.Controls
private readonly Classes _classes = new Classes();
private DataTemplates _dataTemplates;
private IControl _focusAdorner;
private bool _isAttachedToLogicalTree;
private IPerspexList<ILogical> _logicalChildren;
private INameScope _nameScope;
private Styles _styles;
private Subject<Unit> _styleDetach = new Subject<Unit>();
/// <summary>
/// Initializes static members of the <see cref="Control"/> class.
@ -93,6 +101,16 @@ namespace Perspex.Controls
_nameScope = this as INameScope;
}
/// <summary>
/// Raised when the control is attached to a rooted logical tree.
/// </summary>
public event EventHandler<LogicalTreeAttachmentEventArgs> AttachedToLogicalTree;
/// <summary>
/// Raised when the control is detached from a rooted logical tree.
/// </summary>
public event EventHandler<LogicalTreeAttachmentEventArgs> DetachedFromLogicalTree;
/// <summary>
/// Occurs when the <see cref="DataContext"/> property changes.
/// </summary>
@ -128,8 +146,7 @@ namespace Perspex.Controls
{
if (_classes != value)
{
_classes.Clear();
_classes.Add(value);
_classes.Replace(value);
}
}
}
@ -206,6 +223,11 @@ namespace Perspex.Controls
internal set { SetValue(TemplatedParentProperty, value); }
}
/// <summary>
/// Gets a value indicating whether the element is attached to a rooted logical tree.
/// </summary>
bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree;
/// <summary>
/// Gets the control's logical parent.
/// </summary>
@ -216,6 +238,9 @@ namespace Perspex.Controls
/// </summary>
IPerspexReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
IPerspexReadOnlyList<string> IStyleable.Classes => Classes;
/// <summary>
/// Gets the type by which the control is styled.
/// </summary>
@ -227,9 +252,10 @@ namespace Perspex.Controls
/// </remarks>
Type IStyleable.StyleKey => GetType();
/// <summary>
/// Gets the parent style host element.
/// </summary>
/// <inheritdoc/>
IObservable<Unit> IStyleable.StyleDetach => _styleDetach;
/// <inheritdoc/>
IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent;
/// <summary>
@ -252,14 +278,40 @@ namespace Perspex.Controls
if (_logicalChildren == null)
{
var list = new PerspexList<ILogical>();
list.ResetBehavior = ResetBehavior.Remove;
_logicalChildren = list;
LogicalChildren = list;
}
return _logicalChildren;
}
set
{
Contract.Requires<ArgumentNullException>(value != null);
if (_logicalChildren != value)
{
if (_logicalChildren != null)
{
_logicalChildren.CollectionChanged -= LogicalChildrenCollectionChanged;
}
}
if (value is PerspexList<ILogical>)
{
((PerspexList<ILogical>)value).ResetBehavior = ResetBehavior.Remove;
}
_logicalChildren = value;
_logicalChildren.CollectionChanged += LogicalChildrenCollectionChanged;
}
}
/// <summary>
/// Gets the <see cref="Classes"/> collection in a form that allows adding and removing
/// pseudoclasses.
/// </summary>
protected IPseudoClasses PseudoClasses => Classes;
/// <summary>
/// Sets the control's logical parent.
/// </summary>
@ -275,7 +327,26 @@ namespace Perspex.Controls
throw new InvalidOperationException("The Control already has a parent.");
}
SetAndRaise(ParentProperty, ref _parent, (IControl)parent);
InheritanceParent = parent as PerspexObject;
_parent = (IControl)parent;
var root = FindStyleRoot(old);
if (root != null)
{
var e = new LogicalTreeAttachmentEventArgs(root);
OnDetachedFromLogicalTree(e);
}
root = FindStyleRoot(this);
if (root != null)
{
var e = new LogicalTreeAttachmentEventArgs(root);
OnAttachedToLogicalTree(e);
}
RaisePropertyChanged(ParentProperty, old, _parent, BindingPriority.LocalValue);
}
}
@ -316,15 +387,71 @@ namespace Perspex.Controls
{
if (selector((T)e.NewValue))
{
((Control)e.Sender).Classes.Add(className);
((Control)e.Sender).PseudoClasses.Add(className);
}
else
{
((Control)e.Sender).Classes.Remove(className);
((Control)e.Sender).PseudoClasses.Remove(className);
}
});
}
/// <summary>
/// Called when the control is added to a logical tree.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// It is vital that if you override this method you call the base implementation;
/// failing to do so will cause numerous features to not work as expected.
/// </remarks>
protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_nameScope == null)
{
_nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope;
}
if (Name != null)
{
_nameScope?.Register(Name, this);
}
_isAttachedToLogicalTree = true;
PerspexLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
AttachedToLogicalTree?.Invoke(this, e);
foreach (var child in LogicalChildren.OfType<Control>())
{
child.OnAttachedToLogicalTree(e);
}
}
/// <summary>
/// Called when the control is removed from a logical tree.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// It is vital that if you override this method you call the base implementation;
/// failing to do so will cause numerous features to not work as expected.
/// </remarks>
protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (Name != null)
{
_nameScope?.Unregister(Name);
}
_isAttachedToLogicalTree = false;
_styleDetach.OnNext(Unit.Default);
this.TemplatedParent = null;
DetachedFromLogicalTree?.Invoke(this, e);
foreach (var child in LogicalChildren.OfType<Control>())
{
child.OnDetachedFromLogicalTree(e);
}
}
/// <inheritdoc/>
protected override void OnGotFocus(GotFocusEventArgs e)
{
@ -370,35 +497,6 @@ namespace Perspex.Controls
}
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_nameScope == null)
{
_nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope;
}
if (Name != null)
{
_nameScope?.Register(Name, this);
}
PerspexLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
}
/// <inheritdoc/>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (Name != null)
{
_nameScope?.Unregister(Name);
}
}
/// <summary>
/// Called when the <see cref="DataContext"/> is changed and all subscribers to that change
/// have been notified.
@ -414,7 +512,7 @@ namespace Perspex.Controls
/// <param name="collection">The logical children to use.</param>
protected void RedirectLogicalChildren(IPerspexList<ILogical> collection)
{
_logicalChildren = collection;
LogicalChildren = collection;
}
/// <summary>
@ -436,5 +534,66 @@ namespace Perspex.Controls
}
}
}
private static IStyleRoot FindStyleRoot(IStyleHost e)
{
while (e != null)
{
var root = e as IStyleRoot;
if (root != null && root.StylingParent == null)
{
return root;
}
e = e.StylingParent;
}
return null;
}
private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
SetLogicalParent(e.NewItems.Cast<ILogical>());
break;
case NotifyCollectionChangedAction.Remove:
ClearLogicalParent(e.OldItems.Cast<ILogical>());
break;
case NotifyCollectionChangedAction.Replace:
ClearLogicalParent(e.OldItems.Cast<ILogical>());
SetLogicalParent(e.NewItems.Cast<ILogical>());
break;
case NotifyCollectionChangedAction.Reset:
throw new NotSupportedException("Reset should not be signalled on LogicalChildren collection");
}
}
private void SetLogicalParent(IEnumerable<ILogical> children)
{
foreach (var i in children)
{
if (i.LogicalParent == null)
{
((ISetLogicalParent)i).SetParent(this);
}
}
}
private void ClearLogicalParent(IEnumerable<ILogical> children)
{
foreach (var i in children)
{
if (i.LogicalParent == this)
{
((ISetLogicalParent)i).SetParent(null);
}
}
}
}
}

30
src/Perspex.Controls/ControlExtensions.cs

@ -70,5 +70,35 @@ namespace Perspex.Controls
.Select(x => (x as INameScope) ?? NameScope.GetNameScope(x))
.FirstOrDefault(x => x != null);
}
/// <summary>
/// Adds or removes a pseudoclass depending on a boolean value.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="value">True to add the pseudoclass or false to remove.</param>
public static void Set(this IPseudoClasses classes, string name, bool value)
{
if (value)
{
classes.Add(name);
}
else
{
classes.Remove(name);
}
}
/// <summary>
/// Sets a pseudoclass depending on an observable trigger.
/// </summary>
/// <param name="classes">The pseudoclasses collection.</param>
/// <param name="name">The name of the pseudoclass to set.</param>
/// <param name="trigger">The trigger: true adds the pseudoclass, false removes.</param>
/// <returns>A disposable used to cancel the subscription.</returns>
public static IDisposable Set(this IPseudoClasses classes, string name, IObservable<bool> trigger)
{
return trigger.Subscribe(x => classes.Set(name, x));
}
}
}

4
src/Perspex.Controls/Decorator.cs

@ -88,13 +88,13 @@ namespace Perspex.Controls
{
((ISetLogicalParent)oldChild).SetParent(null);
LogicalChildren.Clear();
RemoveVisualChild(oldChild);
VisualChildren.Remove(oldChild);
}
if (newChild != null)
{
((ISetLogicalParent)newChild).SetParent(this);
AddVisualChild(newChild);
VisualChildren.Add(newChild);
LogicalChildren.Add(newChild);
}
}

4
src/Perspex.Controls/DropDown.cs

@ -125,14 +125,14 @@ namespace Perspex.Controls
base.OnPointerPressed(e);
}
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
if (_popup != null)
{
_popup.Opened -= PopupOpened;
}
_popup = nameScope.Get<Popup>("PART_Popup");
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
}

74
src/Perspex.Controls/Expander.cs

@ -0,0 +1,74 @@
using Perspex.Animation;
using Perspex.Controls.Primitives;
namespace Perspex.Controls
{
public enum ExpandDirection
{
Down,
Up,
Left,
Right
}
public class Expander : HeaderedContentControl
{
public static readonly PerspexProperty<bool> IsExpandedProperty =
PerspexProperty.Register<Expander, bool>(nameof(IsExpanded), true);
public static readonly PerspexProperty<ExpandDirection> ExpandDirectionProperty =
PerspexProperty.Register<Expander, ExpandDirection>(nameof(ExpandDirection), ExpandDirection.Down);
public static readonly PerspexProperty<IPageTransition> ContentTransitionProperty =
PerspexProperty.Register<Expander, IPageTransition>(nameof(ContentTransition));
static Expander()
{
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
PseudoClass(IsExpandedProperty, ":expanded");
IsExpandedProperty.Changed.AddClassHandler<Expander>(x => x.OnIsExpandedChanged);
}
public bool IsExpanded
{
get { return GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
public ExpandDirection ExpandDirection
{
get { return GetValue(ExpandDirectionProperty); }
set { SetValue(ExpandDirectionProperty, value); }
}
public IPageTransition ContentTransition
{
get { return GetValue(ContentTransitionProperty); }
set { SetValue(ContentTransitionProperty, value); }
}
protected virtual void OnIsExpandedChanged(PerspexPropertyChangedEventArgs e)
{
IVisual visualContent = Presenter;
if (Content != null && ContentTransition != null && visualContent != null)
{
bool forward = ExpandDirection == ExpandDirection.Left ||
ExpandDirection == ExpandDirection.Up;
if (IsExpanded)
{
ContentTransition.Start(null, visualContent, forward);
}
else
{
ContentTransition.Start(visualContent, null, !forward);
}
}
}
}
}

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

@ -16,12 +16,17 @@ namespace Perspex.Controls.Generators
/// <summary>
/// Gets the currently realized containers.
/// </summary>
IEnumerable<IControl> Containers { get; }
IEnumerable<ItemContainer> Containers { get; }
/// <summary>
/// Signalled whenever new containers are initialized.
/// Signalled whenever new containers are materialized.
/// </summary>
IObservable<ItemContainers> ContainersInitialized { get; }
event EventHandler<ItemContainerEventArgs> Materialized;
/// <summary>
/// Event raised whenever containers are dematerialized.
/// </summary>
event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Creates container controls for a collection of items.
@ -32,7 +37,7 @@ namespace Perspex.Controls.Generators
/// <param name="items">The items.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
IEnumerable<IControl> Materialize(
IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
IMemberSelector selector);
@ -45,7 +50,7 @@ namespace Perspex.Controls.Generators
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<IControl> Dematerialize(int startingIndex, int count);
IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count);
/// <summary>
/// Removes a set of created containers and updates the index of later containers to fill
@ -56,13 +61,13 @@ namespace Perspex.Controls.Generators
/// </param>
/// <param name="count">The the number of items to remove.</param>
/// <returns>The removed containers.</returns>
IEnumerable<IControl> RemoveRange(int startingIndex, int count);
IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count);
/// <summary>
/// Clears all created containers and returns the removed controls.
/// </summary>
/// <returns>The removed controls.</returns>
IEnumerable<IControl> Clear();
IEnumerable<ItemContainer> Clear();
/// <summary>
/// Gets the container control representing the item with the specified index.

19
src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs

@ -11,23 +11,8 @@ namespace Perspex.Controls.Generators
public interface ITreeItemContainerGenerator : IItemContainerGenerator
{
/// <summary>
/// Gets the item container for the root of the tree, or null if this generator is itself
/// the root of the tree.
/// Gets the container index for the tree.
/// </summary>
ITreeItemContainerGenerator RootGenerator { get; }
/// <summary>
/// Gets the item container for the specified item, anywhere in the tree.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container, or null if not found.</returns>
IControl TreeContainerFromItem(object item);
/// <summary>
/// Gets the item for the specified item container, anywhere in the tree.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item, or null if not found.</returns>
object TreeItemFromContainer(IControl container);
TreeContainerIndex Index { get; }
}
}

45
src/Perspex.Controls/Generators/ItemContainer.cs

@ -0,0 +1,45 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Perspex.Controls.Generators
{
/// <summary>
/// Holds information about an item container generated by an
/// <see cref="IItemContainerGenerator"/>.
/// </summary>
public class ItemContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainer"/> class.
/// </summary>
/// <param name="container">The container control.</param>
/// <param name="item">The item that the container represents.</param>
/// <param name="index">
/// The index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </param>
public ItemContainer(IControl container, object item, int index)
{
ContainerControl = container;
Item = item;
Index = index;
}
/// <summary>
/// Gets the container control.
/// </summary>
/// <remarks>
/// This will be null if <see cref="Item"/> is null.
/// </remarks>
public IControl ContainerControl { get; }
/// <summary>
/// Gets the item that the container represents.
/// </summary>
public object Item { get; }
/// <summary>
/// Gets the index of the item in the <see cref="ItemsControl.Items"/> collection.
/// </summary>
public int Index { get; }
}
}

22
src/Perspex.Controls/Generators/ItemContainers.cs → src/Perspex.Controls/Generators/ItemContainerEventArgs.cs

@ -1,34 +1,38 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
namespace Perspex.Controls.Generators
{
/// <summary>
/// Holds details about a set of item containers in an <see cref="IItemContainerGenerator"/>.
/// Provides details for the <see cref="IItemContainerGenerator.Materialized"/>
/// and <see cref="IItemContainerGenerator.Dematerialized"/> events.
/// </summary>
public class ItemContainers
public class ItemContainerEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainers"/> class.
/// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
/// </summary>
/// <param name="startingIndex">The index of the first container in the source items.</param>
/// <param name="containers">The containers.</param>
public ItemContainers(int startingIndex, IList<IControl> containers)
public ItemContainerEventArgs(
int startingIndex,
IList<ItemContainer> containers)
{
StartingIndex = startingIndex;
Items = containers;
Containers = containers;
}
/// <summary>
/// Gets the index of the first container in the source items.
/// Gets the containers.
/// </summary>
public int StartingIndex { get; }
public IList<ItemContainer> Containers { get; }
/// <summary>
/// Gets the containers. May contain null entries.
/// Gets the index of the first container in the source items.
/// </summary>
public IList<IControl> Items { get; }
public int StartingIndex { get; }
}
}

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

@ -15,9 +15,7 @@ namespace Perspex.Controls.Generators
/// </summary>
public class ItemContainerGenerator : IItemContainerGenerator
{
private List<IControl> _containers = new List<IControl>();
private readonly Subject<ItemContainers> _containersInitialized = new Subject<ItemContainers>();
private List<ItemContainer> _containers = new List<ItemContainer>();
/// <summary>
/// Initializes a new instance of the <see cref="ItemContainerGenerator"/> class.
@ -31,10 +29,13 @@ namespace Perspex.Controls.Generators
}
/// <inheritdoc/>
public IEnumerable<IControl> Containers => _containers;
public IEnumerable<ItemContainer> Containers => _containers;
/// <inheritdoc/>
public event EventHandler<ItemContainerEventArgs> Materialized;
/// <inheritdoc/>
public IObservable<ItemContainers> ContainersInitialized => _containersInitialized;
public event EventHandler<ItemContainerEventArgs> Dematerialized;
/// <summary>
/// Gets the owner control.
@ -42,7 +43,7 @@ namespace Perspex.Controls.Generators
public IControl Owner { get; }
/// <inheritdoc/>
public IEnumerable<IControl> Materialize(
public IEnumerable<ItemContainer> Materialize(
int startingIndex,
IEnumerable items,
IMemberSelector selector)
@ -50,25 +51,25 @@ namespace Perspex.Controls.Generators
Contract.Requires<ArgumentNullException>(items != null);
int index = startingIndex;
var result = new List<IControl>();
var result = new List<ItemContainer>();
foreach (var item in items)
{
var i = selector != null ? selector.Select(item) : item;
var container = CreateContainer(i);
var container = new ItemContainer(CreateContainer(i), item, index++);
result.Add(container);
}
AddContainers(startingIndex, result);
_containersInitialized.OnNext(new ItemContainers(startingIndex, result));
AddContainers(result);
Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result.Where(x => x != null).ToList();
}
/// <inheritdoc/>
public virtual IEnumerable<IControl> Dematerialize(int startingIndex, int count)
public virtual IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
{
var result = new List<IControl>();
var result = new List<ItemContainer>();
for (int i = startingIndex; i < startingIndex + count; ++i)
{
@ -79,22 +80,31 @@ namespace Perspex.Controls.Generators
}
}
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result;
}
/// <inheritdoc/>
public virtual IEnumerable<IControl> RemoveRange(int startingIndex, int count)
public virtual IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
{
var result = _containers.GetRange(startingIndex, count);
_containers.RemoveRange(startingIndex, count);
Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result));
return result;
}
/// <inheritdoc/>
public virtual IEnumerable<IControl> Clear()
public virtual IEnumerable<ItemContainer> Clear()
{
var result = _containers;
_containers = new List<IControl>();
_containers = new List<ItemContainer>();
if (result.Count > 0)
{
Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result));
}
return result;
}
@ -103,7 +113,7 @@ namespace Perspex.Controls.Generators
{
if (index < _containers.Count)
{
return _containers[index];
return _containers[index]?.ContainerControl;
}
return null;
@ -112,7 +122,19 @@ namespace Perspex.Controls.Generators
/// <inheritdoc/>
public int IndexFromContainer(IControl container)
{
return _containers.IndexOf(container);
var index = 0;
foreach (var i in _containers)
{
if (i?.ContainerControl == container)
{
return index;
}
++index;
}
return -1;
}
/// <summary>
@ -135,33 +157,30 @@ namespace Perspex.Controls.Generators
/// <summary>
/// Adds a collection of containers to the index.
/// </summary>
/// <param name="index">The starting index.</param>
/// <param name="container">The container.</param>
protected void AddContainers(int index, IList<IControl> container)
/// <param name="containers">The containers.</param>
protected void AddContainers(IList<ItemContainer> containers)
{
Contract.Requires<ArgumentNullException>(container != null);
Contract.Requires<ArgumentNullException>(containers != null);
foreach (var c in container)
foreach (var c in containers)
{
while (_containers.Count < index)
while (_containers.Count < c.Index)
{
_containers.Add(null);
}
if (_containers.Count == index)
if (_containers.Count == c.Index)
{
_containers.Add(c);
}
else if (_containers[index] == null)
else if (_containers[c.Index] == null)
{
_containers[index] = c;
_containers[c.Index] = c;
}
else
{
throw new InvalidOperationException("Container already created.");
}
++index;
}
}
@ -171,7 +190,7 @@ namespace Perspex.Controls.Generators
/// <param name="index">The first index.</param>
/// <param name="count">The number of elements in the range.</param>
/// <returns>The containers.</returns>
protected IEnumerable<IControl> GetContainerRange(int index, int count)
protected IEnumerable<ItemContainer> GetContainerRange(int index, int count)
{
return _containers.GetRange(index, count);
}

5
src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs

@ -24,6 +24,9 @@ namespace Perspex.Controls.Generators
PerspexProperty contentProperty)
: base(owner)
{
Contract.Requires<ArgumentNullException>(owner != null);
Contract.Requires<ArgumentNullException>(contentProperty != null);
ContentProperty = contentProperty;
}
@ -48,7 +51,7 @@ namespace Perspex.Controls.Generators
else
{
var result = new T();
result.SetValue(ContentProperty, Owner.MaterializeDataTemplate(item));
result.SetValue(ContentProperty, item);
if (!(item is IControl))
{

87
src/Perspex.Controls/Generators/TreeContainerIndex.cs

@ -0,0 +1,87 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
namespace Perspex.Controls.Generators
{
/// <summary>
/// Maintains an index of all item containers currently materialized by a <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Each <see cref="TreeViewItem"/> has its own <see cref="TreeItemContainerGenerator{T}"/>
/// that maintains the list of its direct children, but they also share an instance of this
/// class in their <see cref="TreeItemContainerGenerator{T}.Index"/> property which tracks
/// the containers materialized for the entire tree.
/// </remarks>
public class TreeContainerIndex
{
private readonly Dictionary<object, IControl> _itemToContainer = new Dictionary<object, IControl>();
private readonly Dictionary<IControl, object> _containerToItem = new Dictionary<IControl, object>();
/// <summary>
/// Gets the currently materialized containers.
/// </summary>
public IEnumerable<IControl> Items => _containerToItem.Keys;
/// <summary>
/// Adds an entry to the index.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="container">The item container.</param>
public void Add(object item, IControl container)
{
_itemToContainer.Add(item, container);
_containerToItem.Add(container, item);
}
/// <summary>
/// Removes a container from the index.
/// </summary>
/// <param name="container">The item container.</param>
public void Remove(IControl container)
{
var item = _containerToItem[container];
_containerToItem.Remove(container);
_itemToContainer.Remove(item);
}
/// <summary>
/// Removes a set of containers from the index.
/// </summary>
/// <param name="containers">The item containers.</param>
public void Remove(IEnumerable<ItemContainer> containers)
{
foreach (var container in containers)
{
var item = _containerToItem[container.ContainerControl];
_containerToItem.Remove(container.ContainerControl);
_itemToContainer.Remove(item);
}
}
/// <summary>
/// Gets the container for an item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container, or null of not found.</returns>
public IControl ContainerFromItem(object item)
{
IControl result;
_itemToContainer.TryGetValue(item, out result);
return result;
}
/// <summary>
/// Gets the item for a container.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item, or null of not found.</returns>
public object ItemFromContainer(IControl container)
{
object result;
_containerToItem.TryGetValue(container, out result);
return result;
}
}
}

110
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -1,6 +1,7 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Perspex.Controls.Templates;
@ -14,9 +15,6 @@ namespace Perspex.Controls.Generators
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : class, IControl, new()
{
private readonly Dictionary<object, T> _itemToContainer;
private readonly Dictionary<IControl, object> _containerToItem;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
@ -24,34 +22,30 @@ namespace Perspex.Controls.Generators
/// <param name="contentProperty">The container's Content property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
/// <param name="rootGenerator">
/// The item container for the root of the tree, or null if this generator is itself the
/// root of the tree.
/// </param>
/// <param name="index">The container index for the tree</param>
public TreeItemContainerGenerator(
IControl owner,
PerspexProperty contentProperty,
PerspexProperty itemsProperty,
PerspexProperty isExpandedProperty,
ITreeItemContainerGenerator rootGenerator)
TreeContainerIndex index)
: base(owner, contentProperty)
{
Contract.Requires<ArgumentNullException>(owner != null);
Contract.Requires<ArgumentNullException>(contentProperty != null);
Contract.Requires<ArgumentNullException>(itemsProperty != null);
Contract.Requires<ArgumentNullException>(isExpandedProperty != null);
Contract.Requires<ArgumentNullException>(index != null);
ItemsProperty = itemsProperty;
IsExpandedProperty = isExpandedProperty;
RootGenerator = rootGenerator;
if (rootGenerator == null)
{
_itemToContainer = new Dictionary<object, T>();
_containerToItem = new Dictionary<IControl, object>();
}
Index = index;
}
/// <summary>
/// Gets the item container for the root of the tree, or null if this generator is itself
/// the root of the tree.
/// Gets the container index for the tree.
/// </summary>
public ITreeItemContainerGenerator RootGenerator { get; }
public TreeContainerIndex Index { get; }
/// <summary>
/// Gets the item container's Items property.
@ -63,30 +57,6 @@ namespace Perspex.Controls.Generators
/// </summary>
protected PerspexProperty IsExpandedProperty { get; }
/// <summary>
/// Gets the item container for the specified item, anywhere in the tree.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The container, or null if not found.</returns>
public IControl TreeContainerFromItem(object item)
{
T result;
_itemToContainer.TryGetValue(item, out result);
return result;
}
/// <summary>
/// Gets the item for the specified item container, anywhere in the tree.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>The item, or null if not found.</returns>
public object TreeItemFromContainer(IControl container)
{
object result;
_containerToItem.TryGetValue(container, out result);
return result;
}
/// <inheritdoc/>
protected override IControl CreateContainer(object item)
{
@ -114,65 +84,29 @@ namespace Perspex.Controls.Generators
result.DataContext = item;
}
AddToIndex(item, result);
Index.Add(item, result);
return result;
}
}
public override IEnumerable<IControl> Clear()
public override IEnumerable<ItemContainer> Clear()
{
ClearIndex();
return base.Clear();
var items = base.Clear();
Index.Remove(items);
return items;
}
public override IEnumerable<IControl> Dematerialize(int startingIndex, int count)
public override IEnumerable<ItemContainer> Dematerialize(int startingIndex, int count)
{
RemoveFromIndex(GetContainerRange(startingIndex, count));
Index.Remove(GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
private void AddToIndex(object item, T container)
public override IEnumerable<ItemContainer> RemoveRange(int startingIndex, int count)
{
if (RootGenerator != null)
{
((TreeItemContainerGenerator<T>)RootGenerator).AddToIndex(item, container);
}
else
{
_itemToContainer.Add(item, container);
_containerToItem.Add(container, item);
}
}
private void RemoveFromIndex(IEnumerable<IControl> containers)
{
if (RootGenerator != null)
{
((TreeItemContainerGenerator<T>)RootGenerator).RemoveFromIndex(containers);
}
else
{
foreach (var container in containers)
{
var item = _containerToItem[container];
_containerToItem.Remove(container);
_itemToContainer.Remove(item);
}
}
}
private void ClearIndex()
{
if (RootGenerator != null)
{
((TreeItemContainerGenerator<T>)RootGenerator).ClearIndex();
}
else
{
_containerToItem.Clear();
_itemToContainer.Clear();
}
Index.Remove(GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
/// <summary>

5
src/Perspex.Controls/IControl.cs

@ -13,6 +13,11 @@ namespace Perspex.Controls
/// </summary>
public interface IControl : IVisual, ILogical, ILayoutable, IInputElement, INamed, IStyleable, IStyleHost
{
/// <summary>
/// Gets or sets the control's styling classes.
/// </summary>
new Classes Classes { get; set; }
/// <summary>
/// Gets or sets the control's data context.
/// </summary>

25
src/Perspex.Controls/IPseudoClasses.cs

@ -0,0 +1,25 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Perspex.Controls
{
/// <summary>
/// Exposes an interface for setting pseudoclasses on a <see cref="Classes"/> collection.
/// </summary>
public interface IPseudoClasses
{
/// <summary>
/// Adds a pseudoclass to the collection.
/// </summary>
/// <param name="name">The pseudoclass name.</param>
void Add(string name);
/// <summary>
/// Removes a pseudoclass from the collection.
/// </summary>
/// <param name="name">The pseudoclass name.</param>
bool Remove(string name);
}
}

25
src/Perspex.Controls/IReparentingControl.cs

@ -1,25 +0,0 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Collections;
namespace Perspex.Controls
{
/// <summary>
/// A control that can make its visual children the logical children of another control.
/// </summary>
public interface IReparentingControl : IControl
{
/// <summary>
/// Requests that the visual children of the control use another control as their logical
/// parent.
/// </summary>
/// <param name="logicalParent">
/// The logical parent for the visual children of the control.
/// </param>
/// <param name="children">
/// The <see cref="ILogical.LogicalChildren"/> collection to modify.
/// </param>
void ReparentLogicalChildren(ILogical logicalParent, IPerspexList<ILogical> children);
}
}

28
src/Perspex.Controls/IReparentingHost.cs

@ -1,28 +0,0 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Collections;
namespace Perspex.Controls
{
/// <summary>
/// A control that can use the visual children of another control as its logical children.
/// </summary>
public interface IReparentingHost : ILogical
{
/// <summary>
/// Gets a writeable logical children collection from the host.
/// </summary>
new IPerspexList<ILogical> LogicalChildren { get; }
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool WillReparentChildrenOf(IControl control);
}
}

215
src/Perspex.Controls/ItemsControl.cs

@ -3,7 +3,7 @@
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@ -21,7 +21,7 @@ namespace Perspex.Controls
/// <summary>
/// Displays a collection of items.
/// </summary>
public class ItemsControl : TemplatedControl, IReparentingHost
public class ItemsControl : TemplatedControl
{
/// <summary>
/// The default value for the <see cref="ItemsPanel"/> property.
@ -64,7 +64,7 @@ namespace Perspex.Controls
/// </summary>
public ItemsControl()
{
Classes.Add(":empty");
PseudoClasses.Add(":empty");
SubscribeToItems(_items);
}
@ -78,6 +78,12 @@ namespace Perspex.Controls
if (_itemContainerGenerator == null)
{
_itemContainerGenerator = CreateItemContainerGenerator();
if (_itemContainerGenerator != null)
{
_itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e);
_itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e);
}
}
return _itemContainerGenerator;
@ -118,38 +124,141 @@ namespace Perspex.Controls
public IItemsPresenter Presenter
{
get;
set;
protected set;
}
/// <inheritdoc/>
IPerspexList<ILogical> IReparentingHost.LogicalChildren => LogicalChildren;
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
protected static object ElementAt(IEnumerable items, int index)
{
var typedItems = items?.Cast<object>();
if (index != -1 && typedItems != null && index < typedItems.Count())
{
return typedItems.ElementAt(index) ?? null;
}
else
{
return null;
}
}
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool IReparentingHost.WillReparentChildrenOf(IControl control)
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
protected static int IndexOf(IEnumerable items, object item)
{
return control is IItemsPresenter && control.TemplatedParent == this;
if (items != null && item != null)
{
var list = items as IList;
if (list != null)
{
return list.IndexOf(item);
}
else
{
int index = 0;
foreach (var i in items)
{
if (Equals(i, item))
{
return index;
}
++index;
}
}
}
return -1;
}
/// <summary>
/// Creates the <see cref="ItemContainerGenerator"/> for the control.
/// </summary>
/// <returns>An <see cref="IItemContainerGenerator"/>.</returns>
/// <returns>
/// An <see cref="IItemContainerGenerator"/> or null.
/// </returns>
/// <remarks>
/// Certain controls such as <see cref="TabControl"/> don't actually create item
/// containers; however they want it to be ItemsControls so that they have an Items
/// property etc. In this case, a derived class can override this method to return null
/// in order to disable the creation of item containers.
/// </remarks>
protected virtual IItemContainerGenerator CreateItemContainerGenerator()
{
return new ItemContainerGenerator(this);
}
/// <summary>
/// Called when new containers are materialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersMaterialized(ItemContainerEventArgs e)
{
var toAdd = new List<ILogical>();
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be added to the logical tree when
// it was added to the Items collection.
if (container.ContainerControl != container.Item)
{
toAdd.Add(container.ContainerControl);
}
}
LogicalChildren.AddRange(toAdd);
}
/// <summary>
/// Called when containers are dematerialized for the <see cref="ItemsControl"/> by its
/// <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="e">The details of the containers.</param>
protected virtual void OnContainersDematerialized(ItemContainerEventArgs e)
{
var toRemove = new List<ILogical>();
foreach (var container in e.Containers)
{
// If the item is its own container, then it will be removed from the logical tree
// when it is removed from the Items collection.
if (container.ContainerControl != container.Item)
{
toRemove.Add(container.ContainerControl);
}
}
LogicalChildren.RemoveAll(toRemove);
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
Presenter = nameScope.Find<IItemsPresenter>("PART_ItemsPresenter");
base.OnTemplateApplied(e);
Presenter = e.NameScope.Find<IItemsPresenter>("PART_ItemsPresenter");
}
/// <inheritdoc/>
protected override void OnTemplateChanged(PerspexPropertyChangedEventArgs e)
{
base.OnTemplateChanged(e);
if (e.NewValue == null)
{
ItemContainerGenerator?.Clear();
}
}
/// <summary>
@ -165,7 +274,11 @@ namespace Perspex.Controls
incc.CollectionChanged -= ItemsCollectionChanged;
}
var oldValue = e.OldValue as IEnumerable;
var newValue = e.NewValue as IEnumerable;
RemoveControlItemsFromLogicalChildren(oldValue);
AddControlItemsToLogicalChildren(newValue);
SubscribeToItems(newValue);
}
@ -177,16 +290,67 @@ namespace Perspex.Controls
/// <param name="e">The event args.</param>
protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddControlItemsToLogicalChildren(e.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection.Count == 0);
}
/// <summary>
/// Given a collection of items, adds those that are controls to the logical children.
/// </summary>
/// <param name="items">The items.</param>
private void AddControlItemsToLogicalChildren(IEnumerable items)
{
var toAdd = new List<ILogical>();
if (collection.Count == 0)
if (items != null)
{
Classes.Add(":empty");
foreach (var i in items)
{
var control = i as IControl;
if (control != null && !LogicalChildren.Contains(control))
{
toAdd.Add(control);
}
}
}
else
LogicalChildren.AddRange(toAdd);
}
/// <summary>
/// Given a collection of items, removes those that are controls to from logical children.
/// </summary>
/// <param name="items">The items.</param>
private void RemoveControlItemsFromLogicalChildren(IEnumerable items)
{
var toRemove = new List<ILogical>();
if (items != null)
{
Classes.Remove(":empty");
foreach (var i in items)
{
var control = i as IControl;
if (control != null)
{
toRemove.Add(control);
}
}
}
LogicalChildren.RemoveAll(toRemove);
}
/// <summary>
@ -195,14 +359,7 @@ namespace Perspex.Controls
/// <param name="items"></param>
private void SubscribeToItems(IEnumerable items)
{
if (items == null || items.Count() == 0)
{
Classes.Add(":empty");
}
else
{
Classes.Remove(":empty");
}
PseudoClasses.Set(":empty", items == null || items.Count() == 0);
var incc = items as INotifyCollectionChanged;

31
src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs

@ -0,0 +1,31 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Perspex.Styling;
namespace Perspex.Controls
{
/// <summary>
/// Holds the event arguments for the <see cref="Control.AttachedToLogicalTree"/> and
/// <see cref="Control.DetachedFromLogicalTree"/> events.
/// </summary>
public class LogicalTreeAttachmentEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="LogicalTreeAttachmentEventArgs"/> class.
/// </summary>
/// <param name="root">The root of the logical tree.</param>
public LogicalTreeAttachmentEventArgs(IStyleHost root)
{
Contract.Requires<ArgumentNullException>(root != null);
Root = root;
}
/// <summary>
/// Gets the root of the logical tree that the control is being attached to or detached from.
/// </summary>
public IStyleHost Root { get; }
}
}

28
src/Perspex.Controls/MenuItem.cs

@ -20,7 +20,7 @@ namespace Perspex.Controls
/// <summary>
/// A menu item control.
/// </summary>
public class MenuItem : SelectingItemsControl, ISelectable
public class MenuItem : HeaderedSelectingItemsControl, ISelectable
{
/// <summary>
/// Defines the <see cref="Command"/> property.
@ -40,12 +40,6 @@ namespace Perspex.Controls
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>
@ -136,7 +130,6 @@ namespace Perspex.Controls
set { SetValue(CommandProperty, value); }
}
/// <summary>
/// Gets or sets an <see cref="KeyGesture"/> associated with this control
/// </summary>
@ -156,15 +149,6 @@ namespace Perspex.Controls
set { SetValue(CommandParameterProperty, value); }
}
/// <summary>
/// Gets or sets the <see cref="MenuItem"/>'s header.
/// </summary>
public object Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
/// </summary>
@ -370,14 +354,12 @@ namespace Perspex.Controls
}
}
/// <summary>
/// Called when the MenuItem's template has been applied.
/// </summary>
protected override void OnTemplateApplied(INameScope nameScope)
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(nameScope);
base.OnTemplateApplied(e);
_popup = nameScope.Get<Popup>("PART_Popup");
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.DependencyResolver = DependencyResolver.Instance;
_popup.PopupRootCreated += PopupRootCreated;
_popup.Opened += PopupOpened;

136
src/Perspex.Controls/Mixins/ContentControlMixin.cs

@ -0,0 +1,136 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Perspex.Collections;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
using Perspex.Interactivity;
namespace Perspex.Controls.Mixins
{
/// <summary>
/// Adds content control functionality to control classes.
/// </summary>
/// <para>
/// The <see cref="ContentControlMixin"/> adds behavior to a control which acts as a content
/// control such as <see cref="ContentControl"/> and <see cref="HeaderedItemsControl"/>. It
/// updates keeps the control's logical children in sync with the content being displayed by
/// the control.
/// </para>
public class ContentControlMixin
{
private static Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>> subscriptions =
new Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>>(() =>
new ConditionalWeakTable<TemplatedControl, IDisposable>());
/// <summary>
/// Initializes a new instance of the <see cref="SelectableMixin"/> class.
/// </summary>
/// <typeparam name="TControl">The control type.</typeparam>
/// <param name="content">The content property.</param>
/// <param name="logicalChildrenSelector">
/// Given an control of <typeparamref name="TControl"/> should return the control's
/// logical children collection.
/// </param>
/// <param name="presenterName">
/// The name of the content presenter in the control's template.
/// </param>
public static void Attach<TControl>(
PerspexProperty content,
Func<TControl, IPerspexList<ILogical>> logicalChildrenSelector,
string presenterName = "PART_ContentPresenter")
where TControl : TemplatedControl
{
Contract.Requires<ArgumentNullException>(content != null);
Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null);
EventHandler<RoutedEventArgs> templateApplied = (s, ev) =>
{
var sender = s as TControl;
if (sender != null)
{
var e = (TemplateAppliedEventArgs)ev;
var presenter = (IControl)e.NameScope.Find(presenterName);
if (presenter != null)
{
var logicalChildren = logicalChildrenSelector(sender);
var subscription = presenter
.GetObservable(ContentPresenter.ChildProperty)
.Subscribe(child => UpdateLogicalChild(
logicalChildren,
logicalChildren.FirstOrDefault(),
child));
subscriptions.Value.Add(sender, subscription);
}
}
};
TemplatedControl.TemplateAppliedEvent.AddClassHandler(
typeof(TControl),
templateApplied,
RoutingStrategies.Direct);
content.Changed.Subscribe(e =>
{
var sender = e.Sender as TControl;
if (sender != null)
{
var logicalChildren = logicalChildrenSelector(sender);
UpdateLogicalChild(logicalChildren, e.OldValue, e.NewValue);
}
});
TemplatedControl.TemplateProperty.Changed.Subscribe(e =>
{
var sender = e.Sender as TControl;
if (sender != null)
{
IDisposable subscription;
if (subscriptions.Value.TryGetValue(sender, out subscription))
{
subscription.Dispose();
subscriptions.Value.Remove(sender);
}
}
});
}
private static event EventHandler<TemplateAppliedEventArgs> TemplateApplied;
private static void OnTemplateApplied(object sender, RoutedEventArgs e)
{
TemplateApplied?.Invoke(sender, (TemplateAppliedEventArgs)e);
}
private static void UpdateLogicalChild(
IPerspexList<ILogical> logicalChildren,
object oldValue,
object newValue)
{
if (oldValue != newValue)
{
var logical = oldValue as ILogical;
if (logical != null)
{
logicalChildren.Remove(logical);
}
logical = newValue as ILogical;
if (logical != null)
{
logicalChildren.Add(logical);
}
}
}
}
}

4
src/Perspex.Controls/Mixins/SelectableMixin.cs

@ -52,7 +52,7 @@ namespace Perspex.Controls.Mixins
{
if ((bool)x.NewValue)
{
sender.Classes.Add(":selected");
((IPseudoClasses)sender.Classes).Add(":selected");
if (((IVisual)sender).IsAttachedToVisualTree)
{
@ -61,7 +61,7 @@ namespace Perspex.Controls.Mixins
}
else
{
sender.Classes.Remove(":selected");
((IPseudoClasses)sender.Classes).Remove(":selected");
}
sender.RaiseEvent(new RoutedEventArgs

79
src/Perspex.Controls/Panel.cs

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Perspex.Collections;
using Perspex.Media;
using Perspex.Metadata;
@ -18,7 +17,7 @@ namespace Perspex.Controls
/// Controls can be added to a <see cref="Panel"/> by adding them to its <see cref="Children"/>
/// collection. All children are layed out to fill the panel.
/// </remarks>
public class Panel : Control, IReparentingControl, IPanel
public class Panel : Control, IPanel
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -68,7 +67,7 @@ namespace Perspex.Controls
{
Contract.Requires<ArgumentNullException>(value != null);
ClearVisualChildren();
VisualChildren.Clear();
_children.Clear();
_children.AddRange(value);
}
@ -83,58 +82,6 @@ namespace Perspex.Controls
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// Requests that the visual children of the panel use another control as their logical
/// parent.
/// </summary>
/// <param name="logicalParent">
/// The logical parent for the visual children of the panel.
/// </param>
/// <param name="children">
/// The <see cref="ILogical.LogicalChildren"/> collection to modify.
/// </param>
void IReparentingControl.ReparentLogicalChildren(ILogical logicalParent, IPerspexList<ILogical> children)
{
Contract.Requires<ArgumentNullException>(logicalParent != null);
Contract.Requires<ArgumentNullException>(children != null);
_childLogicalParent = logicalParent;
RedirectLogicalChildren(children);
foreach (var control in Children)
{
((ISetLogicalParent)control).SetParent(null);
((ISetLogicalParent)control).SetParent((IControl)logicalParent);
children.Add(control);
}
}
/// <summary>
/// Clears <see cref="IControl.Parent"/> for the specified controls.
/// </summary>
/// <param name="controls">The controls.</param>
private void ClearLogicalParent(IEnumerable<IControl> controls)
{
foreach (var control in controls)
{
((ISetLogicalParent)control).SetParent(null);
}
}
/// <summary>
/// Sets <see cref="IControl.Parent"/> for the specified controls.
/// </summary>
/// <param name="controls">The controls.</param>
private void SetLogicalParent(IEnumerable<IControl> controls)
{
var parent = _childLogicalParent as Control;
foreach (var control in controls)
{
((ISetLogicalParent)control).SetParent(parent);
}
}
/// <summary>
/// Called when the <see cref="Children"/> collection changes.
/// </summary>
@ -144,29 +91,35 @@ namespace Perspex.Controls
{
List<Control> controls;
// TODO: Handle Replace.
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
controls = e.NewItems.OfType<Control>().ToList();
SetLogicalParent(controls);
AddVisualChildren(e.NewItems.OfType<Visual>());
LogicalChildren.InsertRange(e.NewStartingIndex, controls);
VisualChildren.AddRange(e.NewItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Remove:
controls = e.OldItems.OfType<Control>().ToList();
ClearLogicalParent(e.OldItems.OfType<Control>());
LogicalChildren.RemoveAll(controls);
RemoveVisualChildren(e.OldItems.OfType<Visual>());
VisualChildren.RemoveAll(e.OldItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Replace:
for (var i = 0; i < e.OldItems.Count; ++i)
{
var index = i + e.OldStartingIndex;
var child = (IControl)e.NewItems[i];
LogicalChildren[index] = child;
VisualChildren[index] = child;
}
break;
case NotifyCollectionChangedAction.Reset:
controls = e.OldItems.OfType<Control>().ToList();
ClearLogicalParent(controls);
LogicalChildren.Clear();
ClearVisualChildren();
AddVisualChildren(_children);
VisualChildren.Clear();
VisualChildren.AddRange(_children);
break;
}

17
src/Perspex.Controls/Perspex.Controls.csproj

@ -42,24 +42,33 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Classes.cs" />
<Compile Include="DockPanel.cs" />
<Compile Include="Expander.cs" />
<Compile Include="Generators\ItemContainer.cs" />
<Compile Include="Generators\TreeContainerIndex.cs" />
<Compile Include="HotkeyManager.cs" />
<Compile Include="INameScope.cs" />
<Compile Include="IPseudoClasses.cs" />
<Compile Include="Mixins\ContentControlMixin.cs" />
<Compile Include="NameScope.cs" />
<Compile Include="NameScopeEventArgs.cs" />
<Compile Include="NameScopeExtensions.cs" />
<Compile Include="Platform\ITopLevelRenderer.cs" />
<Compile Include="Platform\IWindowingPlatform.cs" />
<Compile Include="Platform\PlatformManager.cs" />
<Compile Include="Primitives\HeaderedSelectingControl.cs" />
<Compile Include="Primitives\IScrollable.cs" />
<Compile Include="Primitives\TabStripItem.cs" />
<Compile Include="Primitives\TemplateAppliedEventArgs.cs" />
<Compile Include="SelectionMode.cs" />
<Compile Include="Separator.cs" />
<Compile Include="SystemDialog.cs" />
<Compile Include="Generators\ITreeItemContainerGenerator.cs" />
<Compile Include="Generators\ItemContainers.cs" />
<Compile Include="Generators\ItemContainerEventArgs.cs" />
<Compile Include="IContentControl.cs" />
<Compile Include="IControl.cs" />
<Compile Include="IPanel.cs" />
<Compile Include="IReparentingHost.cs" />
<Compile Include="ISetLogicalParent.cs" />
<Compile Include="MenuItemAccessKeyHandler.cs" />
<Compile Include="Mixins\SelectableMixin.cs" />
@ -71,14 +80,11 @@
<Compile Include="Menu.cs" />
<Compile Include="Button.cs" />
<Compile Include="DropDown.cs" />
<Compile Include="IReparentingControl.cs" />
<Compile Include="Carousel.cs" />
<Compile Include="Platform\IPopupImpl.cs" />
<Compile Include="Platform\ITopLevelImpl.cs" />
<Compile Include="PlacementMode.cs" />
<Compile Include="Primitives\IScrollInfo.cs" />
<Compile Include="Primitives\Popup.cs" />
<Compile Include="Primitives\ScrollInfoAdapter.cs" />
<Compile Include="Canvas.cs" />
<Compile Include="Templates\FuncControlTemplate`2.cs" />
<Compile Include="Templates\FuncDataTemplate`1.cs" />
@ -149,6 +155,7 @@
<Compile Include="TopLevel.cs" />
<Compile Include="Primitives\PopupRoot.cs" />
<Compile Include="Utils\UndoRedoHelper.cs" />
<Compile Include="LogicalTreeAttachmentEventArgs.cs" />
<Compile Include="Window.cs" />
<Compile Include="RowDefinition.cs" />
<Compile Include="RowDefinitions.cs" />

18
src/Perspex.Controls/Presenters/CarouselPresenter.cs

@ -162,20 +162,13 @@ namespace Perspex.Controls.Presenters
/// </summary>
private void CreatePanel()
{
var logicalHost = this.FindReparentingHost();
ClearVisualChildren();
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
AddVisualChild(Panel);
if (logicalHost != null)
{
((IReparentingControl)Panel).ReparentLogicalChildren(
logicalHost,
logicalHost.LogicalChildren);
}
LogicalChildren.Clear();
VisualChildren.Clear();
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
_createdPanel = true;
var task = MoveToPage(-1, SelectedIndex);
@ -204,7 +197,8 @@ namespace Perspex.Controls.Presenters
{
var item = Items.Cast<object>().ElementAt(toIndex);
to = generator.ContainerFromIndex(toIndex) ??
generator.Materialize(toIndex, new[] { item }, MemberSelector).FirstOrDefault();
generator.Materialize(toIndex, new[] { item }, MemberSelector)
.FirstOrDefault()?.ContainerControl;
if (to != null)
{

30
src/Perspex.Controls/Presenters/ContentPresenter.cs

@ -63,6 +63,14 @@ namespace Perspex.Controls.Presenters
}
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_createdChild = false;
InvalidateMeasure();
}
/// <inheritdoc/>
protected override Size MeasureCore(Size availableSize)
{
@ -101,18 +109,12 @@ namespace Perspex.Controls.Presenters
var old = Child;
var content = Content;
var result = this.MaterializeDataTemplate(content);
var logicalHost = this.FindReparentingHost();
var logicalChildren = logicalHost?.LogicalChildren ?? LogicalChildren;
if (old != null)
{
((ISetLogicalParent)old).SetParent(null);
logicalChildren.Remove(old);
ClearVisualChildren();
VisualChildren.Remove(old);
}
Child = result;
if (result != null)
{
if (!(content is IControl))
@ -120,14 +122,12 @@ namespace Perspex.Controls.Presenters
result.DataContext = content;
}
if (result.Parent == null)
{
((ISetLogicalParent)result).SetParent((ILogical)logicalHost ?? this);
}
AddVisualChild(result);
logicalChildren.Remove(old);
logicalChildren.Add(result);
Child = result;
VisualChildren.Add(result);
}
else
{
Child = null;
}
_createdChild = true;

58
src/Perspex.Controls/Presenters/ItemsPresenter.cs

@ -5,6 +5,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Perspex.Controls.Generators;
using Perspex.Controls.Templates;
using Perspex.Input;
@ -150,7 +151,6 @@ namespace Perspex.Controls.Presenters
/// </summary>
private void CreatePanel()
{
ClearVisualChildren();
Panel = ItemsPanel.Build();
Panel.SetValue(TemplatedParentProperty, TemplatedParent);
@ -161,16 +161,10 @@ namespace Perspex.Controls.Presenters
KeyboardNavigationMode.Contained);
}
AddVisualChild(Panel);
var logicalHost = this.FindReparentingHost();
if (logicalHost != null)
{
((IReparentingControl)Panel).ReparentLogicalChildren(
logicalHost,
logicalHost.LogicalChildren);
}
LogicalChildren.Clear();
VisualChildren.Clear();
LogicalChildren.Add(Panel);
VisualChildren.Add(Panel);
KeyboardNavigation.SetTabNavigation(
(InputElement)Panel,
@ -187,7 +181,7 @@ namespace Perspex.Controls.Presenters
{
if (items != null)
{
Panel.Children.AddRange(ItemContainerGenerator.Materialize(0, Items, MemberSelector));
AddContainers(ItemContainerGenerator.Materialize(0, Items, MemberSelector));
INotifyCollectionChanged incc = items as INotifyCollectionChanged;
@ -238,30 +232,28 @@ namespace Perspex.Controls.Presenters
if (_createdPanel)
{
var generator = ItemContainerGenerator;
IEnumerable<IControl> containers;
// TODO: Handle Move and Replace etc.
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
Panel.Children.AddRange(containers);
AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector));
break;
case NotifyCollectionChangedAction.Remove:
containers = generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count);
Panel.Children.RemoveAll(containers);
RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count));
break;
case NotifyCollectionChangedAction.Replace:
generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count);
containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count));
var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector);
AddContainers(containers);
var i = e.NewStartingIndex;
foreach (var container in containers)
{
Panel.Children[i++] = container;
Panel.Children[i++] = container.ContainerControl;
}
break;
@ -269,13 +261,35 @@ namespace Perspex.Controls.Presenters
case NotifyCollectionChangedAction.Move:
// TODO: Implement Move in a more efficient manner.
case NotifyCollectionChangedAction.Reset:
Panel.Children.RemoveAll(generator.Clear());
Panel.Children.AddRange(generator.Materialize(0, Items, MemberSelector));
RemoveContainers(generator.Clear());
AddContainers(generator.Materialize(0, Items, MemberSelector));
break;
}
InvalidateMeasure();
}
}
private void AddContainers(IEnumerable<ItemContainer> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
this.Panel.Children.Add(i.ContainerControl);
}
}
}
private void RemoveContainers(IEnumerable<ItemContainer> items)
{
foreach (var i in items)
{
if (i.ContainerControl != null)
{
this.Panel.Children.Remove(i.ContainerControl);
}
}
}
}
}

60
src/Perspex.Controls/Presenters/ScrollContentPresenter.cs

@ -3,6 +3,9 @@
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Controls.Primitives;
using Perspex.Input;
using Perspex.Layout;
using Perspex.VisualTree;
@ -39,6 +42,7 @@ namespace Perspex.Controls.Presenters
PerspexProperty.Register<ScrollContentPresenter, bool>("CanScrollHorizontally", true);
private Size _measuredExtent;
private IDisposable _scrollableSubscription;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -56,6 +60,8 @@ namespace Perspex.Controls.Presenters
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
GetObservable(ChildProperty).Subscribe(ChildChanged);
}
/// <summary>
@ -149,19 +155,24 @@ namespace Perspex.Controls.Presenters
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
var content = Content as ILayoutable;
var child = Child;
if (content != null)
if (child != null)
{
var measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
var measureSize = availableSize;
if (!CanScrollHorizontally)
if (_scrollableSubscription == null)
{
measureSize = measureSize.WithWidth(availableSize.Width);
measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
if (!CanScrollHorizontally)
{
measureSize = measureSize.WithWidth(availableSize.Width);
}
}
content.Measure(measureSize);
var size = content.DesiredSize;
child.Measure(measureSize);
var size = child.DesiredSize;
_measuredExtent = size;
return size.Constrain(availableSize);
}
@ -175,16 +186,21 @@ namespace Perspex.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
var offset = default(Vector);
Viewport = finalSize;
Extent = _measuredExtent;
if (_scrollableSubscription == null)
{
Viewport = finalSize;
Extent = _measuredExtent;
offset = Offset;
}
if (child != null)
{
var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
child.Arrange(new Rect((Point)(-Offset), size));
child.Arrange(new Rect((Point)(-offset), size));
return finalSize;
}
@ -209,6 +225,30 @@ namespace Perspex.Controls.Presenters
e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect);
}
private void ChildChanged(IControl child)
{
var scrollable = child as IScrollable;
_scrollableSubscription?.Dispose();
_scrollableSubscription = null;
if (scrollable != null)
{
scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
_scrollableSubscription = new CompositeDisposable(
GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
Disposable.Create(() => scrollable.InvalidateScroll = null));
UpdateFromScrollable(scrollable);
}
}
private void UpdateFromScrollable(IScrollable scrollable)
{
Viewport = scrollable.Viewport;
Extent = scrollable.Extent;
Offset = scrollable.Offset;
}
private static Vector ValidateOffset(ScrollContentPresenter o, Vector value)
{
return ScrollViewer.CoerceOffset(

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

@ -10,7 +10,7 @@ namespace Perspex.Controls.Primitives
AdornerLayer = new AdornerLayer();
((ISetLogicalParent)AdornerLayer).SetParent(this);
AdornerLayer.ZIndex = int.MaxValue;
AddVisualChild(AdornerLayer);
VisualChildren.Add(AdornerLayer);
}
public AdornerLayer AdornerLayer

41
src/Perspex.Controls/Primitives/HeaderedItemsControl.cs

@ -1,17 +1,58 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Perspex.Controls.Mixins;
using Perspex.Controls.Presenters;
namespace Perspex.Controls.Primitives
{
/// <summary>
/// Represents an <see cref="ItemsControl"/> with a related header.
/// </summary>
public class HeaderedItemsControl : ItemsControl
{
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedItemsControl>();
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static HeaderedItemsControl()
{
ContentControlMixin.Attach<HeaderedItemsControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
}
/// <summary>
/// Gets or sets the content of the control's header.
/// </summary>
public object Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public ContentPresenter HeaderPresenter
{
get;
private set;
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
}
}
}

58
src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs

@ -0,0 +1,58 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Perspex.Controls.Mixins;
using Perspex.Controls.Presenters;
namespace Perspex.Controls.Primitives
{
/// <summary>
/// Represents a <see cref="SelectingItemsControl"/> with a related header.
/// </summary>
public class HeaderedSelectingItemsControl : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="Header"/> property.
/// </summary>
public static readonly PerspexProperty<object> HeaderProperty =
HeaderedContentControl.HeaderProperty.AddOwner<HeaderedSelectingItemsControl>();
/// <summary>
/// Initializes static members of the <see cref="ContentControl"/> class.
/// </summary>
static HeaderedSelectingItemsControl()
{
ContentControlMixin.Attach<HeaderedSelectingItemsControl>(
HeaderProperty,
x => x.LogicalChildren,
"PART_HeaderPresenter");
}
/// <summary>
/// Gets or sets the content of the control's header.
/// </summary>
public object Header
{
get { return GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
/// <summary>
/// Gets the header presenter from the control's template.
/// </summary>
public ContentPresenter HeaderPresenter
{
get;
private set;
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
}
}
}

97
src/Perspex.Controls/Primitives/IScrollInfo.cs

@ -1,97 +0,0 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Perspex.Controls.Primitives
{
public interface IScrollInfoBase
{
/// <summary>
/// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant
/// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo()
/// on this object when properties change.
/// </summary>
ScrollViewer ScrollOwner { get; set; }
Rect MakeVisible(Visual visual, Rect rectangle);
}
public interface IVerticalScrollInfo : IScrollInfoBase
{
/// <summary>
/// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible.
/// </summary>
double VerticalOffset { get; set; }
/// <summary>
/// ExtentHeight contains the full vertical range of the scrolled content.
/// </summary>
double ExtentHeight { get; }
/// <summary>
/// ViewportHeight contains the currently visible vertical range of the scrolled content.
/// </summary>
double ViewportHeight { get; }
/// <summary>
/// This property indicates to the IScrollInfo whether or not it can scroll in the vertical given dimension.
/// </summary>
bool CanVerticallyScroll { get; set; }
void LineDown();
void LineUp();
void MouseWheelDown();
void MouseWheelUp();
void PageDown();
void PageUp();
}
public interface IHorizontalScrollInfo : IScrollInfoBase
{
/// <summary>
/// ExtentWidth contains the full horizontal range of the scrolled content.
/// </summary>
double ExtentWidth { get; }
/// <summary>
/// ViewportWidth contains the currently visible horizontal range of the scrolled content.
/// </summary>
double ViewportWidth { get; }
/// <summary>
/// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible.
/// </summary>
double HorizontalOffset { get; set; }
/// <summary>
/// This property indicates to the IScrollInfo whether or not it can scroll in the horizontal given dimension.
/// </summary>
bool CanHorizontallyScroll { get; set; }
void LineLeft();
void LineRight();
void MouseWheelLeft();
void MouseWheelRight();
void PageLeft();
void PageRight();
}
public interface IScrollInfo : IHorizontalScrollInfo, IVerticalScrollInfo
{
}
}

44
src/Perspex.Controls/Primitives/IScrollable.cs

@ -0,0 +1,44 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Perspex.Controls.Primitives
{
/// <summary>
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// <see cref="ScrollViewer"/>.
/// </summary>
public interface IScrollable
{
/// <summary>
/// Gets or sets the scroll invalidation method.
/// </summary>
/// <remarks>
/// <para>
/// This method notifies the attached <see cref="ScrollViewer"/> of a change in
/// the <see cref="Extent"/>, <see cref="Offset"/> or <see cref="Viewport"/> properties.
/// </para>
/// <para>
/// This property is set by the parent <see cref="ScrollViewer"/> when the
/// <see cref="IScrollable"/> is placed inside it.
/// </para>
/// </remarks>
Action InvalidateScroll { get; set; }
/// <summary>
/// Gets the extent of the scrollable content, in logical units
/// </summary>
Size Extent { get; }
/// <summary>
/// Gets or sets the current scroll offset, in logical units.
/// </summary>
Vector Offset { get; set; }
/// <summary>
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
}
}

5
src/Perspex.Controls/Primitives/PopupRoot.cs

@ -44,7 +44,6 @@ namespace Perspex.Controls.Primitives
public PopupRoot(IPerspexDependencyResolver dependencyResolver)
: base(PlatformManager.CreatePopup(), dependencyResolver)
{
GetObservable(ParentProperty).Subscribe(x => InheritanceParent = (PerspexObject)x);
}
/// <summary>
@ -85,9 +84,9 @@ namespace Perspex.Controls.Primitives
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(nameScope);
base.OnTemplateApplied(e);
if (Parent.TemplatedParent != null)
{

126
src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs

@ -1,126 +0,0 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Perspex.Controls.Primitives
{
public class ScrollInfoAdapter : IScrollInfo
{
private readonly IScrollInfoBase _nfo;
public ScrollInfoAdapter(IScrollInfoBase nfo)
{
_nfo = nfo;
}
public ScrollViewer ScrollOwner
{
get { return _nfo.ScrollOwner; }
set { _nfo.ScrollOwner = value; }
}
public double ExtentWidth => (_nfo as IHorizontalScrollInfo)?.ExtentWidth ?? 0;
public double ViewportWidth => (_nfo as IHorizontalScrollInfo)?.ViewportWidth ?? 0;
public double ExtentHeight => (_nfo as IVerticalScrollInfo)?.ExtentHeight ?? 0;
public double ViewportHeight => (_nfo as IVerticalScrollInfo)?.ViewportHeight ?? 0;
private double _horizontalOffset;
public double HorizontalOffset
{
get
{
return (_nfo as IHorizontalScrollInfo)?.HorizontalOffset ?? _horizontalOffset;
}
set
{
var info = (_nfo as IHorizontalScrollInfo);
if (info == null)
_horizontalOffset = value;
else
info.HorizontalOffset = value;
}
}
private double _verticalOffset;
public double VerticalOffset
{
get
{
return (_nfo as IVerticalScrollInfo)?.VerticalOffset ?? _verticalOffset;
}
set
{
var info = (_nfo as IVerticalScrollInfo);
if (info == null)
_verticalOffset = value;
else
info.VerticalOffset = value;
}
}
public void LineLeft() => (_nfo as IHorizontalScrollInfo)?.LineLeft();
public void LineRight() => (_nfo as IHorizontalScrollInfo)?.LineRight();
public void MouseWheelLeft() => (_nfo as IHorizontalScrollInfo)?.MouseWheelLeft();
public void MouseWheelRight() => (_nfo as IHorizontalScrollInfo)?.MouseWheelRight();
public void PageLeft() => (_nfo as IHorizontalScrollInfo)?.PageLeft();
public Rect MakeVisible(Visual visual, Rect rectangle) => _nfo.MakeVisible(visual, rectangle);
public void PageRight() => (_nfo as IHorizontalScrollInfo)?.PageRight();
public void LineDown() => (_nfo as IVerticalScrollInfo)?.LineDown();
public void LineUp() => (_nfo as IVerticalScrollInfo)?.LineUp();
public void MouseWheelDown() => (_nfo as IVerticalScrollInfo)?.MouseWheelDown();
public void MouseWheelUp() => (_nfo as IVerticalScrollInfo)?.MouseWheelUp();
public void PageDown() => (_nfo as IVerticalScrollInfo)?.PageDown();
public void PageUp() => (_nfo as IVerticalScrollInfo)?.PageUp();
private bool _canVerticallyScroll;
public bool CanVerticallyScroll
{
get
{
return (_nfo as IVerticalScrollInfo)?.CanVerticallyScroll ?? _canVerticallyScroll;
}
set
{
var info = (_nfo as IVerticalScrollInfo);
if (info == null)
_canVerticallyScroll = value;
else
info.CanVerticallyScroll = value;
}
}
private bool _canHorizontallyScroll;
public bool CanHorizontallyScroll
{
get
{
return (_nfo as IHorizontalScrollInfo)?.CanHorizontallyScroll ?? _canHorizontallyScroll;
}
set
{
var info = (_nfo as IHorizontalScrollInfo);
if (info == null)
_canHorizontallyScroll = value;
else
info.CanHorizontallyScroll = value;
}
}
}
}

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

@ -98,7 +98,6 @@ namespace Perspex.Controls.Primitives
/// </summary>
public SelectingItemsControl()
{
ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized);
}
/// <summary>
@ -286,6 +285,27 @@ namespace Perspex.Controls.Primitives
}
}
/// <inheritdoc/>
protected override void OnContainersMaterialized(ItemContainerEventArgs e)
{
base.OnContainersMaterialized(e);
var selectedIndex = SelectedIndex;
var selectedContainer = e.Containers
.FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true);
if (selectedContainer != null)
{
SelectedIndex = selectedContainer.Index;
}
else if (selectedIndex >= e.StartingIndex &&
selectedIndex < e.StartingIndex + e.Containers.Count)
{
var container = e.Containers[selectedIndex - e.StartingIndex];
MarkContainerSelected(container.ContainerControl, true);
}
}
/// <inheritdoc/>
protected override void OnDataContextChanged()
{
@ -379,7 +399,7 @@ namespace Perspex.Controls.Primitives
bool rangeModifier = false,
bool toggleModifier = false)
{
var index = ItemContainerGenerator.IndexFromContainer(container);
var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1;
if (index != -1)
{
@ -416,61 +436,6 @@ namespace Perspex.Controls.Primitives
return false;
}
/// <summary>
/// Gets the item at the specified index in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="index">The index.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
private static object ElementAt(IEnumerable items, int index)
{
var typedItems = items?.Cast<object>();
if (index != -1 && typedItems != null && index < typedItems.Count())
{
return typedItems.ElementAt(index) ?? null;
}
else
{
return null;
}
}
/// <summary>
/// Gets the index of an item in a collection.
/// </summary>
/// <param name="items">The collection.</param>
/// <param name="item">The item.</param>
/// <returns>The index of the item or -1 if the item was not found.</returns>
private static int IndexOf(IEnumerable items, object item)
{
if (items != null && item != null)
{
var list = items as IList;
if (list != null)
{
return list.IndexOf(item);
}
else
{
int index = 0;
foreach (var i in items)
{
if (Equals(i, item))
{
return index;
}
++index;
}
}
}
return -1;
}
/// <summary>
/// Gets a range of items from an IEnumerable.
/// </summary>
@ -523,27 +488,6 @@ namespace Perspex.Controls.Primitives
}
}
/// <summary>
/// Called when new containers are initialized by the <see cref="ItemContainerGenerator"/>.
/// </summary>
/// <param name="containers">The containers.</param>
private void ContainersInitialized(ItemContainers containers)
{
var selectedIndex = SelectedIndex;
var selectedContainer = containers.Items.OfType<ISelectable>().FirstOrDefault(x => x.IsSelected);
if (selectedContainer != null)
{
SelectedIndex = containers.Items.IndexOf((IControl)selectedContainer) + containers.StartingIndex;
}
else if (selectedIndex >= containers.StartingIndex &&
selectedIndex < containers.StartingIndex + containers.Items.Count)
{
var container = containers.Items[selectedIndex - containers.StartingIndex];
MarkContainerSelected(container, true);
}
}
/// <summary>
/// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
/// </summary>
@ -593,7 +537,6 @@ namespace Perspex.Controls.Primitives
try
{
var selectable = container as ISelectable;
var styleable = container as IStyleable;
_ignoreContainerSelectionChanged = true;
@ -601,16 +544,9 @@ namespace Perspex.Controls.Primitives
{
selectable.IsSelected = selected;
}
else if (styleable != null)
else
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
((IPseudoClasses)container.Classes).Set(":selected", selected);
}
}
finally
@ -626,7 +562,7 @@ namespace Perspex.Controls.Primitives
/// <param name="selected">Whether the item should be selected or deselected.</param>
private void MarkItemSelected(int index, bool selected)
{
var container = ItemContainerGenerator.ContainerFromIndex(index);
var container = ItemContainerGenerator?.ContainerFromIndex(index);
if (container != null)
{
@ -656,6 +592,8 @@ namespace Perspex.Controls.Primitives
/// <param name="e">The event args.</param>
private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var generator = ItemContainerGenerator;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
@ -681,9 +619,15 @@ namespace Perspex.Controls.Primitives
break;
case NotifyCollectionChangedAction.Reset:
foreach (var item in ItemContainerGenerator.Containers)
if (generator != null)
{
MarkContainerSelected(item, false);
foreach (var item in generator.Containers)
{
if (item != null)
{
MarkContainerSelected(item.ContainerControl, false);
}
}
}
if (!_syncingSelectedItems)

40
src/Perspex.Controls/Primitives/TabStrip.cs

@ -1,52 +1,26 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Perspex.Controls.Generators;
using Perspex.Controls.Templates;
using Perspex.Input;
namespace Perspex.Controls.Primitives
{
public class TabStrip : SelectingItemsControl
{
public static readonly PerspexProperty<TabItem> SelectedTabProperty =
TabControl.SelectedTabProperty.AddOwner<TabStrip>();
private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
static TabStrip()
{
MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
}
public TabStrip()
{
GetObservable(SelectedItemProperty).Subscribe(x => SelectedTab = x as TabItem);
GetObservable(SelectedTabProperty).Subscribe(x => SelectedItem = x as TabItem);
}
public TabItem SelectedTab
{
get { return GetValue(SelectedTabProperty); }
set { SetValue(SelectedTabProperty, value); }
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
TabControl tabControl = TemplatedParent as TabControl;
IItemContainerGenerator result;
if (tabControl != null)
{
result = tabControl.ItemContainerGenerator;
}
else
{
result = new ItemContainerGenerator<TabItem>(this, TabItem.ContentProperty);
}
return result;
return new ItemContainerGenerator<TabStripItem>(this, ContentControl.ContentProperty);
}
/// <inheritdoc/>
@ -70,5 +44,11 @@ namespace Perspex.Controls.Primitives
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
private static object SelectHeader(object o)
{
var headered = o as IHeadered;
return (headered != null) ? (headered.Header ?? string.Empty) : o;
}
}
}

12
src/Perspex.Controls/Primitives/TabStripItem.cs

@ -0,0 +1,12 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Perspex.Controls.Primitives
{
/// <summary>
/// Represents a tab in a <see cref="TabStrip"/>.
/// </summary>
public class TabStripItem : ListBoxItem
{
}
}

28
src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs

@ -0,0 +1,28 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Interactivity;
namespace Perspex.Controls.Primitives
{
/// <summary>
/// Holds the details of the <see cref="TemplatedControl.TemplateApplied"/> event.
/// </summary>
public class TemplateAppliedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="TemplateAppliedEventArgs"/> class.
/// </summary>
/// <param name="nameScope">The applied template's name scope.</param>
public TemplateAppliedEventArgs(INameScope nameScope)
: base(TemplatedControl.TemplateAppliedEvent)
{
NameScope = nameScope;
}
/// <summary>
/// Gets the name scope of the applied template.
/// </summary>
public INameScope NameScope { get; }
}
}

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

@ -3,8 +3,10 @@
using System;
using System.Linq;
using System.Reactive.Linq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Templates;
using Perspex.Interactivity;
using Perspex.Media;
using Perspex.Styling;
using Perspex.VisualTree;
@ -72,6 +74,14 @@ namespace Perspex.Controls.Primitives
public static readonly PerspexProperty<IControlTemplate> TemplateProperty =
PerspexProperty.Register<TemplatedControl, IControlTemplate>("Template");
/// <summary>
/// Defines the <see cref="TemplateApplied"/> routed event.
/// </summary>
public static readonly RoutedEvent<TemplateAppliedEventArgs> TemplateAppliedEvent =
RoutedEvent.Register<TemplatedControl, TemplateAppliedEventArgs>(
"TemplateApplied",
RoutingStrategies.Direct);
private bool _templateApplied;
private readonly ILogger _templateLog;
@ -81,12 +91,8 @@ namespace Perspex.Controls.Primitives
/// </summary>
static TemplatedControl()
{
TemplateProperty.Changed.Subscribe(e =>
{
var templatedControl = (TemplatedControl)e.Sender;
templatedControl._templateApplied = false;
templatedControl.InvalidateMeasure();
});
ClipToBoundsProperty.OverrideDefaultValue<TemplatedControl>(true);
TemplateProperty.Changed.AddClassHandler<TemplatedControl>(x => x.OnTemplateChanged);
}
/// <summary>
@ -102,6 +108,15 @@ namespace Perspex.Controls.Primitives
});
}
/// <summary>
/// Raised when the control's template is applied.
/// </summary>
public event EventHandler<TemplateAppliedEventArgs> TemplateApplied
{
add { AddHandler(TemplateAppliedEvent, value); }
remove { RemoveHandler(TemplateAppliedEvent, value); }
}
/// <summary>
/// Gets or sets the brush used to draw the control's background.
/// </summary>
@ -188,7 +203,7 @@ namespace Perspex.Controls.Primitives
{
if (!_templateApplied)
{
ClearVisualChildren();
VisualChildren.Clear();
if (Template != null)
{
@ -198,30 +213,60 @@ namespace Perspex.Controls.Primitives
var nameScope = new NameScope();
NameScope.SetNameScope((Control)child, nameScope);
// We need to call SetTemplatedParentAndApplyChildTemplates twice - once
// before the controls are added to the visual tree so that the logical
// tree can be set up before styling is applied.
((ISetLogicalParent)child).SetParent(this);
// We need to call SetupTemplateControls twice:
// - Once before the controls are added to the visual/logical trees so that the
// TemplatedParent property is set and names are registered; if
// TemplatedParent is not set when the control is added to the logical tree,
// then styles with the /template/ selector won't match.
// - Once after the controls are added to the logical tree (and thus styled) to
// call ApplyTemplate on nested templated controls and register any of our
// templated children that appear as children of presenters in these nested
// templated child controls.
SetupTemplateControls(child, nameScope);
// And again after the controls are added to the visual tree, and have their
// styling and thus Template property set.
AddVisualChild((Visual)child);
VisualChildren.Add(child);
((ISetLogicalParent)child).SetParent(this);
SetupTemplateControls(child, nameScope);
OnTemplateApplied(nameScope);
OnTemplateApplied(new TemplateAppliedEventArgs(nameScope));
}
_templateApplied = true;
}
}
protected sealed override BindingDescriptor CreateBindingDescriptor(BindingDescriptor source)
{
var result = base.CreateBindingDescriptor(source);
// If the binding is a template binding, then complete when the Template changes.
if (source.Priority == BindingPriority.TemplatedParent)
{
var templateChanged = GetObservable(TemplateProperty).Skip(1);
result.SourceObservable = result.Source.GetObservable(result.Property)
.TakeUntil(templateChanged);
}
return result;
}
/// <summary>
/// Called when the control's template is applied.
/// </summary>
/// <param name="nameScope">The template name scope.</param>
protected virtual void OnTemplateApplied(INameScope nameScope)
/// <param name="e">The event args.</param>
protected virtual void OnTemplateApplied(TemplateAppliedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>
/// Called when the <see cref="Template"/> property changes.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e)
{
_templateApplied = false;
InvalidateMeasure();
}
/// <summary>

4
src/Perspex.Controls/Primitives/Track.cs

@ -45,12 +45,12 @@ namespace Perspex.Controls.Primitives
val.Item1.DragDelta -= ThumbDragged;
}
ClearVisualChildren();
VisualChildren.Clear();
if (val.Item2 != null)
{
val.Item2.DragDelta += ThumbDragged;
AddVisualChild(val.Item2);
VisualChildren.Add(val.Item2);
}
});
}

4
src/Perspex.Controls/ProgressBar.cs

@ -26,9 +26,9 @@ namespace Perspex.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
_indicator = nameScope.Get<Border>("PART_Indicator");
_indicator = e.NameScope.Get<Border>("PART_Indicator");
UpdateIndicator(Bounds.Size);
}

5
src/Perspex.Controls/ScrollViewer.cs

@ -2,6 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Perspex.Controls.Presenters;
using Perspex.Controls.Primitives;
@ -117,6 +120,8 @@ namespace Perspex.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
private IDisposable _scrollableSubscription;
/// <summary>
/// Initializes static members of the <see cref="ScrollViewer"/> class.
/// </summary>

93
src/Perspex.Controls/TabControl.cs

@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Animation;
using Perspex.Controls.Presenters;
using Perspex.Controls.Generators;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
@ -11,23 +11,26 @@ namespace Perspex.Controls
/// <summary>
/// A tab control that displays a tab strip along with the content of the selected tab.
/// </summary>
public class TabControl : SelectingItemsControl, IReparentingHost
public class TabControl : SelectingItemsControl
{
/// <summary>
/// Defines the <see cref="SelectedTab"/> property.
/// </summary>
public static readonly PerspexProperty<TabItem> SelectedTabProperty =
PerspexProperty.Register<TabControl, TabItem>("SelectedTab");
/// <summary>
/// Defines the <see cref="Transition"/> property.
/// </summary>
public static readonly PerspexProperty<IPageTransition> TransitionProperty =
Carousel.TransitionProperty.AddOwner<TabControl>();
Perspex.Controls.Carousel.TransitionProperty.AddOwner<TabControl>();
private static readonly IMemberSelector s_contentSelector =
/// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the content of a <see cref="TabItem"/>.
/// </summary>
public static readonly IMemberSelector ContentSelector =
new FuncMemberSelector<object, object>(SelectContent);
/// <summary>
/// Defines an <see cref="IMemberSelector"/> that selects the header of a <see cref="TabItem"/>.
/// </summary>
public static readonly IMemberSelector HeaderSelector =
new FuncMemberSelector<object, object>(SelectHeader);
/// <summary>
/// Defines the <see cref="TabStripPlacement"/> property.
/// </summary>
@ -41,22 +44,25 @@ namespace Perspex.Controls
{
SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue<TabControl>(false);
SelectedItemProperty.Changed.AddClassHandler<TabControl>(x => x.SelectedItemChanged);
AffectsMeasure(TabStripPlacementProperty);
}
/// <summary>
/// Gets an <see cref="IMemberSelector"/> that selects the content of a <see cref="TabItem"/>.
/// Gets the pages portion of the <see cref="TabControl"/>'s template.
/// </summary>
public IMemberSelector ContentSelector => s_contentSelector;
public IControl Pages
{
get;
private set;
}
/// <summary>
/// Gets the <see cref="SelectingItemsControl.SelectedItem"/> as a <see cref="TabItem"/>.
/// Gets the tab strip portion of the <see cref="TabControl"/>'s template.
/// </summary>
public TabItem SelectedTab
public IControl TabStrip
{
get { return GetValue(SelectedTabProperty); }
private set { SetValue(SelectedTabProperty, value); }
get;
private set;
}
/// <summary>
@ -77,17 +83,21 @@ namespace Perspex.Controls
set { SetValue(TabStripPlacementProperty, value); }
}
/// <summary>
/// Asks the control whether it wants to reparent the logical children of the specified
/// control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>
/// True if the control wants to reparent its logical children otherwise false.
/// </returns>
bool IReparentingHost.WillReparentChildrenOf(IControl control)
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return control is CarouselPresenter;
// TabControl doesn't actually create items - instead its TabStrip and Carousel
// children create the items. However we want it to be a SelectingItemsControl
// so that it has the Items/SelectedItem etc properties. In this case, we can
// return a null ItemContainerGenerator to disable the creation of item containers.
return null;
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
TabStrip = e.NameScope.Find<IControl>("PART_TabStrip");
Pages = e.NameScope.Find<IControl>("PART_Content");
}
/// <summary>
@ -110,14 +120,31 @@ namespace Perspex.Controls
}
/// <summary>
/// Called when the <see cref="SelectingItemsControl.SelectedIndex"/> property changes.
/// Selects the header of a tab item.
/// </summary>
/// <param name="e">The event args.</param>
private void SelectedItemChanged(PerspexPropertyChangedEventArgs e)
/// <param name="o">The tab item.</param>
/// <returns>The content.</returns>
private static object SelectHeader(object o)
{
var item = e.NewValue as IContentControl;
var content = item?.Content ?? item;
SelectedTab = item as TabItem;
var headered = o as IHeadered;
var control = o as IControl;
if (headered != null)
{
return headered.Header ?? string.Empty;
}
else if (control != null)
{
// Non-headered control items should result in TabStripItems with empty content.
// If a TabStrip is created with non IHeadered controls as its items, don't try to
// display the control in the TabStripItem: the content portion will also try to
// display this control, resulting in dual-parentage breakage.
return string.Empty;
}
else
{
return o;
}
}
}
}

29
src/Perspex.Controls/Templates/TemplateExtensions.cs

@ -12,35 +12,6 @@ namespace Perspex.Controls.Templates
{
public static class TemplateExtensions
{
public static IReparentingHost FindReparentingHost(this IControl control)
{
var tp = control.TemplatedParent;
var chain = new List<IReparentingHost>();
while (tp != null)
{
var reparentingHost = tp as IReparentingHost;
var styleable = tp as IStyleable;
if (reparentingHost != null)
{
chain.Add(reparentingHost);
}
tp = styleable?.TemplatedParent ?? null;
}
foreach (var reparenting in chain.AsEnumerable().Reverse())
{
if (reparenting.WillReparentChildrenOf(control))
{
return reparenting;
}
}
return null;
}
public static IEnumerable<Control> GetTemplateChildren(this ITemplatedControl control)
{
var visual = control as IVisual;

6
src/Perspex.Controls/TextBlock.cs

@ -26,6 +26,7 @@ namespace Perspex.Controls
public static readonly PerspexProperty<string> FontFamilyProperty =
PerspexProperty.RegisterAttached<TextBlock, Control, string>(
nameof(FontFamily),
defaultValue: "Courier New",
inherits: true);
/// <summary>
@ -34,6 +35,7 @@ namespace Perspex.Controls
public static readonly PerspexProperty<double> FontSizeProperty =
PerspexProperty.RegisterAttached<TextBlock, Control, double>(
nameof(FontSize),
defaultValue: 12,
inherits: true);
/// <summary>
@ -343,8 +345,8 @@ namespace Perspex.Controls
{
var result = new FormattedText(
Text ?? string.Empty,
FontFamily ?? "Arial",
FontSize > 0 ? FontSize : 12,
FontFamily,
FontSize,
FontStyle,
TextAlignment,
FontWeight);

4
src/Perspex.Controls/TextBox.cs

@ -157,9 +157,9 @@ namespace Perspex.Controls
set { SetValue(TextWrappingProperty, value); }
}
protected override void OnTemplateApplied(INameScope nameScope)
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
{
_presenter = nameScope.Get<TextPresenter>("PART_TextPresenter");
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
_presenter.Cursor = new Cursor(StandardCursorType.Ibeam);
}

7
src/Perspex.Controls/TopLevel.cs

@ -24,7 +24,7 @@ namespace Perspex.Controls
/// <see cref="PopupRoot"/>. It handles scheduling layout, styling and rendering as well as
/// tracking the window <see cref="ClientSize"/> and <see cref="IsActive"/> state.
/// </remarks>
public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable
public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot
{
/// <summary>
/// Defines the <see cref="ClientSize"/> property.
@ -204,6 +204,11 @@ namespace Perspex.Controls
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
IStyleHost IStyleHost.StylingParent
{
get { return PerspexLocator.Current.GetService<IGlobalStyles>(); }
}
/// <summary>
/// Whether an auto-size operation is in progress.
/// </summary>

20
src/Perspex.Controls/TreeView.cs

@ -58,7 +58,7 @@ namespace Perspex.Controls
TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
null);
new TreeContainerIndex());
}
/// <inheritdoc/>
@ -101,13 +101,13 @@ namespace Perspex.Controls
bool rangeModifier = false,
bool toggleModifier = false)
{
var item = ItemContainerGenerator.TreeItemFromContainer(container);
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
if (item != null)
{
if (SelectedItem != null)
{
var old = ItemContainerGenerator.TreeContainerFromItem(SelectedItem);
var old = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem);
MarkContainerSelected(old, false);
}
@ -162,7 +162,7 @@ namespace Perspex.Controls
if (item != null)
{
if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator)
if (item.ItemContainerGenerator.Index == this.ItemContainerGenerator.Index)
{
return item;
}
@ -179,22 +179,14 @@ namespace Perspex.Controls
private void MarkContainerSelected(IControl container, bool selected)
{
var selectable = container as ISelectable;
var styleable = container as IStyleable;
if (selectable != null)
{
selectable.IsSelected = selected;
}
else if (styleable != null)
else
{
if (selected)
{
styleable.Classes.Add(":selected");
}
else
{
styleable.Classes.Remove(":selected");
}
((IPseudoClasses)container.Classes).Set(":selected", selected);
}
}
}

17
src/Perspex.Controls/TreeViewItem.cs

@ -7,8 +7,7 @@ using Perspex.Controls.Mixins;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.Input;
using Perspex.Rendering;
using Perspex.VisualTree;
using Perspex.LogicalTree;
namespace Perspex.Controls
{
@ -79,14 +78,20 @@ namespace Perspex.Controls
TreeViewItem.HeaderProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
_treeView?.ItemContainerGenerator);
_treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_treeView = this.GetVisualAncestors().OfType<TreeView>().FirstOrDefault();
base.OnAttachedToLogicalTree(e);
_treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
ItemContainerGenerator.Clear();
}
/// <inheritdoc/>

9
src/Perspex.Diagnostics/ViewModels/TreeNode.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Specialized;
using System.Reactive;
using System.Reactive.Linq;
using Perspex.Controls;
@ -18,7 +19,13 @@ namespace Perspex.Diagnostics.ViewModels
Control = control;
Type = control.GetType().Name;
control.Classes.Changed.Select(_ => Unit.Default)
var classesChanged = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => control.Classes.CollectionChanged += x,
x => control.Classes.CollectionChanged -= x);
classesChanged.Select(_ => Unit.Default)
.StartWith(Unit.Default)
.Subscribe(_ =>
{

4
src/Perspex.Layout/LayoutManager.cs

@ -226,12 +226,12 @@ namespace Perspex.Layout
{
var parent = item.Control.GetVisualParent<ILayoutable>();
while (parent.PreviousMeasure == null)
while (parent != null && parent.PreviousMeasure == null)
{
parent = parent.GetVisualParent<ILayoutable>();
}
if (parent.GetVisualRoot() == Root)
if (parent != null && parent.GetVisualRoot() == Root)
{
parent.Measure(parent.PreviousMeasure.Value, true);
}

12
src/Perspex.Layout/Layoutable.cs

@ -472,7 +472,7 @@ namespace Perspex.Layout
height = Math.Min(height, MaxHeight);
height = Math.Max(height, MinHeight);
return new Size(width, height).Inflate(Margin);
return NonNegative(new Size(width, height).Inflate(Margin));
}
else
{
@ -649,6 +649,16 @@ namespace Perspex.Layout
double.IsNaN(size.Width) || double.IsNaN(size.Height);
}
/// <summary>
/// Ensures neither component of a <see cref="Size"/> is negative.
/// </summary>
/// <param name="size">The size.</param>
/// <returns>The non-negative size.</returns>
private static Size NonNegative(Size size)
{
return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0));
}
/// <summary>
/// Gets the layout root, together with its distance.
/// </summary>

2
src/Perspex.SceneGraph/Perspex.SceneGraph.csproj

@ -118,7 +118,7 @@
<Compile Include="Rect.cs" />
<Compile Include="Rendering\IRenderRoot.cs" />
<Compile Include="Rendering\IRenderQueueManager.cs" />
<Compile Include="Rendering\RendererBase.cs" />
<Compile Include="Rendering\RendererMixin.cs" />
<Compile Include="Rendering\RenderQueueManager.cs" />
<Compile Include="Size.cs" />
<Compile Include="Thickness.cs" />

18
src/Perspex.SceneGraph/Point.cs

@ -229,6 +229,24 @@ namespace Perspex
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y);
}
/// <summary>
/// Transforms the point by a matrix.
/// </summary>
/// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns>
public Point Transform(Matrix transform)
{
var x = X;
var y = Y;
var xadd = y * transform.M21 + transform.M31;
var yadd = x * transform.M12 + transform.M32;
x *= transform.M11;
x += xadd;
y *= transform.M22;
y += yadd;
return new Point(x, y);
}
/// <summary>
/// Returns a new point with the specified X coordinate.
/// </summary>

77
src/Perspex.SceneGraph/Rect.cs

@ -3,6 +3,7 @@
using System;
using System.Globalization;
using System.Linq;
namespace Perspex
{
@ -11,6 +12,11 @@ namespace Perspex
/// </summary>
public struct Rect
{
/// <summary>
/// An empty rectangle.
/// </summary>
public static readonly Rect Empty = default(Rect);
/// <summary>
/// The X position.
/// </summary>
@ -328,25 +334,74 @@ namespace Perspex
/// <returns>The intersection.</returns>
public Rect Intersect(Rect rect)
{
double x = Math.Max(_x, rect._x);
double y = Math.Max(_y, rect._y);
double width = Math.Min(Right, rect.Right) - x;
double height = Math.Min(Bottom, rect.Bottom) - y;
var newLeft = (rect.X > X) ? rect.X : X;
var newTop = (rect.Y > Y) ? rect.Y : Y;
var newRight = (rect.Right < Right) ? rect.Right : Right;
var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom;
if (width < 0 || height < 0)
if ((newRight > newLeft) && (newBottom > newTop))
{
return new Rect(
double.PositiveInfinity,
double.PositiveInfinity,
double.NegativeInfinity,
double.NegativeInfinity);
return new Rect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
}
else
{
return new Rect(x, y, width, height);
return Empty;
}
}
/// <summary>
/// Determines whether a rectangle intersects with this rectangle.
/// </summary>
/// <param name="rect">The other rectangle.</param>
/// <returns>
/// True if the specified rectangle intersects with this one; otherwise false.
/// </returns>
public bool Intersects(Rect rect)
{
return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom);
}
/// <summary>
/// Returns the axis-aligned bounding box of a transformed rectangle.
/// </summary>
/// <param name="matrix">The transform.</param>
/// <returns>The bounding box</returns>
public Rect TransformToAABB(Matrix matrix)
{
var points = new[]
{
TopLeft.Transform(matrix),
TopRight.Transform(matrix),
BottomRight.Transform(matrix),
BottomLeft.Transform(matrix),
};
var left = double.MaxValue;
var right = double.MinValue;
var top = double.MaxValue;
var bottom = double.MinValue;
foreach (var p in points)
{
if (p.X < left) left = p.X;
if (p.X > right) right = p.X;
if (p.Y < top) top = p.Y;
if (p.Y > bottom) bottom = p.Y;
}
return new Rect(new Point(left, top), new Point(right, bottom));
}
/// <summary>
/// Translates the rectangle by an offset.
/// </summary>
/// <param name="offset">The offset.</param>
/// <returns>The translated rectangle.</returns>
public Rect Translate(Vector offset)
{
return new Rect(Position + offset, Size);
}
/// <summary>
/// Returns the string representation of the rectangle.
/// </summary>

97
src/Perspex.SceneGraph/Rendering/RendererBase.cs → src/Perspex.SceneGraph/Rendering/RendererMixin.cs

@ -5,14 +5,13 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Perspex.Media;
using Perspex.Platform;
namespace Perspex.Rendering
{
/// <summary>
/// Base class for standard renderers.
/// Extension methods for rendering.
/// </summary>
/// <remarks>
/// This class provides implements the platform-independent parts of <see cref="IRenderTarget"/>.
@ -21,11 +20,20 @@ namespace Perspex.Rendering
[SuppressMessage("ReSharper", "ForCanBeConvertedToForeach")]
public static class RendererMixin
{
static int s_frameNum;
static int s_fps;
static int s_currentFrames;
static TimeSpan s_lastMeasure;
static readonly Stopwatch s_stopwatch = Stopwatch.StartNew();
private static int s_frameNum;
private static int s_fps;
private static int s_currentFrames;
private static TimeSpan s_lastMeasure;
private static readonly Stopwatch s_stopwatch = Stopwatch.StartNew();
private static readonly Stack<List<IVisual>> s_listPool = new Stack<List<IVisual>>();
private static readonly ZIndexComparer s_visualComparer = new ZIndexComparer();
/// <summary>
/// Gets or sets a value which determines whether an FPS counted will be drawn on each
/// rendered frame.
/// </summary>
public static bool DrawFpsCounter { get; set; }
/// <summary>
/// Renders the specified visual.
/// </summary>
@ -62,17 +70,30 @@ namespace Perspex.Rendering
}
}
public static bool DrawFpsCounter { get; set; }
/// <summary>
/// Renders the specified visual.
/// </summary>
/// <param name="visual">The visual to render.</param>
///
/// <param name="context">The drawing context.</param>
public static void Render(this DrawingContext context, IVisual visual)
{
context.Render(visual, visual.Bounds);
}
/// <summary>
/// Renders the specified visual.
/// </summary>
/// <param name="visual">The visual to render.</param>
/// <param name="context">The drawing context.</param>
/// <param name="clipRect">
/// The current clip rect, in coordinates relative to <paramref name="visual"/>.
/// </param>
private static void Render(this DrawingContext context, IVisual visual, Rect clipRect)
{
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size);
if (visual.IsVisible && opacity > 0)
{
var m = Matrix.CreateTranslation(visual.Bounds.Position);
@ -83,49 +104,71 @@ namespace Perspex.Rendering
{
var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
renderTransform = (-offset)*visual.RenderTransform.Value*(offset);
renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
}
m = renderTransform * m;
if (clipToBounds)
{
clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size));
}
m = renderTransform*m;
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState))
using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
using (context.PushTransformContainer())
{
visual.Render(context);
var lst = GetSortedVisualList(visual.VisualChildren);
foreach (var child in lst)
{
context.Render(child);
var childBounds = GetTransformedBounds(child);
if (!child.ClipToBounds || clipRect.Intersects(childBounds))
{
var childClipRect = clipRect.Translate(-childBounds.Position);
context.Render(child, childClipRect);
}
}
ReturnListToPool(lst);
}
}
}
static readonly Stack<List<IVisual>> ListPool = new Stack<List<IVisual>>();
static readonly ZIndexComparer VisualComparer = new ZIndexComparer();
class ZIndexComparer : IComparer<IVisual>
{
public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex);
}
static void ReturnListToPool(List<IVisual> lst)
private static void ReturnListToPool(List<IVisual> lst)
{
lst.Clear();
ListPool.Push(lst);
s_listPool.Push(lst);
}
static List<IVisual> GetSortedVisualList(IReadOnlyList<IVisual> source)
private static List<IVisual> GetSortedVisualList(IReadOnlyList<IVisual> source)
{
var lst = ListPool.Count == 0 ? new List<IVisual>() : ListPool.Pop();
var lst = s_listPool.Count == 0 ? new List<IVisual>() : s_listPool.Pop();
for (var c = 0; c < source.Count; c++)
lst.Add(source[c]);
lst.Sort(VisualComparer);
lst.Sort(s_visualComparer);
return lst;
}
private static Rect GetTransformedBounds(IVisual visual)
{
if (visual.RenderTransform == null)
{
return visual.Bounds;
}
else
{
return visual.Bounds.TransformToAABB(visual.RenderTransform.Value);
}
}
class ZIndexComparer : IComparer<IVisual>
{
public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex);
}
}
}

130
src/Perspex.SceneGraph/Visual.cs

@ -81,11 +81,6 @@ namespace Perspex
/// </summary>
private string _name;
/// <summary>
/// Holds the children of the visual.
/// </summary>
private readonly PerspexList<IVisual> _visualChildren;
/// <summary>
/// The visual's bounds relative to its parent.
/// </summary>
@ -128,9 +123,10 @@ namespace Perspex
new PropertyEnricher("Id", GetHashCode()),
});
_visualChildren = new PerspexList<IVisual>();
_visualChildren.ResetBehavior = ResetBehavior.Remove;
_visualChildren.CollectionChanged += VisualChildrenChanged;
var visualChildren = new PerspexList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.CollectionChanged += VisualChildrenChanged;
VisualChildren = visualChildren;
}
/// <summary>
@ -249,6 +245,15 @@ namespace Perspex
set { SetValue(ZIndexProperty, value); }
}
/// <summary>
/// Gets the control's visual children.
/// </summary>
protected IPerspexList<IVisual> VisualChildren
{
get;
private set;
}
/// <summary>
/// Gets a value indicating whether this scene graph node is attached to a visual root.
/// </summary>
@ -257,7 +262,7 @@ namespace Perspex
/// <summary>
/// Gets the scene graph node's child nodes.
/// </summary>
IPerspexReadOnlyList<IVisual> IVisual.VisualChildren => _visualChildren;
IPerspexReadOnlyList<IVisual> IVisual.VisualChildren => VisualChildren;
/// <summary>
/// Gets the scene graph node's parent node.
@ -333,61 +338,6 @@ namespace Perspex
property.Changed.Subscribe(AffectsRenderInvalidate);
}
/// <summary>
/// Adds a visual child to the control.
/// </summary>
/// <param name="visual">The child to add.</param>
protected void AddVisualChild(IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
_visualChildren.Add(visual);
}
/// <summary>
/// Adds visual children to the control.
/// </summary>
/// <param name="visuals">The children to add.</param>
protected void AddVisualChildren(IEnumerable<IVisual> visuals)
{
Contract.Requires<ArgumentNullException>(visuals != null);
_visualChildren.AddRange(visuals);
}
/// <summary>
/// Removes all visual children from the control.
/// </summary>
protected void ClearVisualChildren()
{
_visualChildren.Clear();
}
/// <summary>
/// Removes a visual child from the control;
/// </summary>
/// <param name="visual">The child to remove.</param>
protected void RemoveVisualChild(IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
_visualChildren.Remove(visual);
}
/// <summary>
/// Removes a visual children from the control;
/// </summary>
/// <param name="visuals">The children to remove.</param>
protected void RemoveVisualChildren(IEnumerable<IVisual> visuals)
{
Contract.Requires<ArgumentNullException>(visuals != null);
foreach (var v in visuals)
{
_visualChildren.Remove(v);
}
}
/// <summary>
/// Called when the control is added to a visual tree.
/// </summary>
@ -433,20 +383,6 @@ namespace Perspex
(e.Sender as Visual)?.InvalidateVisual();
}
/// <summary>
/// Gets the event args for an <see cref="AttachedToVisualTree"/> or
/// <see cref="DetachedFromVisualTree"/> event.
/// </summary>
/// <returns>
/// A <see cref="VisualTreeAttachmentEventArgs"/> if the visual currently has a root;
/// otherwise null.
/// </returns>
private VisualTreeAttachmentEventArgs GetAttachmentEventArgs()
{
var root = this.GetSelfAndVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
return root != null ? new VisualTreeAttachmentEventArgs(root) : null;
}
/// <summary>
/// Gets the visual offset from the specified ancestor.
/// </summary>
@ -544,38 +480,28 @@ namespace Perspex
}
var old = _visualParent;
_visualParent = value;
if (_isAttachedToVisualTree)
{
var oldArgs = GetAttachmentEventArgs();
_visualParent = value;
if (oldArgs != null)
{
NotifyDetachedFromVisualTree(oldArgs);
}
}
else
{
_visualParent = value;
var root = (this as IRenderRoot) ??
old.GetSelfAndVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var e = new VisualTreeAttachmentEventArgs(root);
NotifyDetachedFromVisualTree(e);
}
if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
{
var newArgs = GetAttachmentEventArgs();
if (newArgs != null)
{
NotifyAttachedToVisualTree(newArgs);
}
var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var e = new VisualTreeAttachmentEventArgs(root);
NotifyAttachedToVisualTree(e);
}
RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue);
}
/// <summary>
/// Called when the <see cref="_visualChildren"/> collection changes.
/// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
@ -586,7 +512,6 @@ namespace Perspex
case NotifyCollectionChangedAction.Add:
foreach (Visual v in e.NewItems)
{
v.InheritanceParent = this;
v.SetVisualParent(this);
}
@ -595,7 +520,6 @@ namespace Perspex
case NotifyCollectionChangedAction.Remove:
foreach (Visual v in e.OldItems)
{
v.InheritanceParent = null;
v.SetVisualParent(null);
}
@ -616,9 +540,9 @@ namespace Perspex
OnAttachedToVisualTree(e);
if (_visualChildren != null)
if (VisualChildren != null)
{
foreach (Visual child in _visualChildren.OfType<Visual>())
foreach (Visual child in VisualChildren.OfType<Visual>())
{
child.NotifyAttachedToVisualTree(e);
}
@ -637,9 +561,9 @@ namespace Perspex
_isAttachedToVisualTree = false;
OnDetachedFromVisualTree(e);
if (_visualChildren != null)
if (VisualChildren != null)
{
foreach (Visual child in _visualChildren.OfType<Visual>())
foreach (Visual child in VisualChildren.OfType<Visual>())
{
child.NotifyDetachedFromVisualTree(e);
}

2
src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs

@ -47,7 +47,7 @@ namespace Perspex.VisualTree
var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds);
// TODO: Track transform and clip rectangle.
return bounds.Select(x => new TransformedBounds((Rect)x, (Rect)new Rect(), (Matrix)Matrix.Identity));
return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity));
}
/// <summary>

2
src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs

@ -18,6 +18,8 @@ namespace Perspex
/// <param name="root">The root visual.</param>
public VisualTreeAttachmentEventArgs(IRenderRoot root)
{
Contract.Requires<ArgumentNullException>(root != null);
Root = root;
}

5
src/Perspex.Styling/ILogical.cs

@ -10,6 +10,11 @@ namespace Perspex
/// </summary>
public interface ILogical
{
/// <summary>
/// Gets a value indicating whether the element is attached to a rooted logical tree.
/// </summary>
bool IsAttachedToLogicalTree { get; }
/// <summary>
/// Gets the logical parent.
/// </summary>

2
src/Perspex.Styling/Perspex.Styling.csproj

@ -45,11 +45,11 @@
<Compile Include="ILogical.cs" />
<Compile Include="LogicalTree\LogicalExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Styling\Classes.cs" />
<Compile Include="Styling\IGlobalStyles.cs" />
<Compile Include="Styling\ISetter.cs" />
<Compile Include="Styling\IStyle.cs" />
<Compile Include="Styling\IStyleable.cs" />
<Compile Include="Styling\IStyleRoot.cs" />
<Compile Include="Styling\IStyleHost.cs" />
<Compile Include="Styling\IStyler.cs" />
<Compile Include="Styling\ITemplatedControl.cs" />

4
src/Perspex.Styling/Properties/AssemblyInfo.cs

@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reflection;
using System.Runtime.CompilerServices;
using Perspex.Metadata;
[assembly: AssemblyTitle("Perspex.Styling")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")]
[assembly: InternalsVisibleTo("Perspex.Styling.UnitTests")]

155
src/Perspex.Styling/Styling/Classes.cs

@ -1,155 +0,0 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive;
using System.Reactive.Subjects;
namespace Perspex.Styling
{
public class Classes : ICollection<string>, INotifyCollectionChanged
{
private readonly List<string> _inner;
private readonly Subject<NotifyCollectionChangedEventArgs> _beforeChanged
= new Subject<NotifyCollectionChangedEventArgs>();
private readonly Subject<NotifyCollectionChangedEventArgs> _changed
= new Subject<NotifyCollectionChangedEventArgs>();
private readonly Subject<NotifyCollectionChangedEventArgs> _afterChanged
= new Subject<NotifyCollectionChangedEventArgs>();
public Classes()
{
_inner = new List<string>();
}
public Classes(params string[] classes)
{
_inner = new List<string>(classes);
}
public Classes(IEnumerable<string> classes)
{
_inner = new List<string>(classes);
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
public int Count => _inner.Count;
public bool IsReadOnly => false;
public IObservable<NotifyCollectionChangedEventArgs> BeforeChanged => _beforeChanged;
public IObservable<NotifyCollectionChangedEventArgs> Changed => _changed;
public IObservable<NotifyCollectionChangedEventArgs> AfterChanged => _afterChanged;
public void Add(string item)
{
Add(Enumerable.Repeat(item, 1));
}
public void Add(params string[] items)
{
Add((IEnumerable<string>)items);
}
public void Add(IEnumerable<string> items)
{
items = items.Except(_inner);
NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add,
items);
_beforeChanged.OnNext(e);
_inner.AddRange(items);
RaiseChanged(e);
}
public void Clear()
{
NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset);
_beforeChanged.OnNext(e);
_inner.Clear();
RaiseChanged(e);
}
public bool Contains(string item)
{
return _inner.Contains(item);
}
public void CopyTo(string[] array, int arrayIndex)
{
_inner.CopyTo(array, arrayIndex);
}
public IEnumerator<string> GetEnumerator()
{
return _inner.GetEnumerator();
}
public override string ToString()
{
return string.Join(" ", this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return _inner.GetEnumerator();
}
public bool Remove(string item)
{
return Remove(Enumerable.Repeat(item, 1));
}
public bool Remove(params string[] items)
{
return Remove((IEnumerable<string>)items);
}
public bool Remove(IEnumerable<string> items)
{
items = items.Intersect(_inner);
if (items.Any())
{
NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove,
items);
_beforeChanged.OnNext(e);
foreach (string item in items)
{
_inner.Remove(item);
}
RaiseChanged(e);
return true;
}
else
{
return false;
}
}
private void RaiseChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
_changed.OnNext(e);
_afterChanged.OnNext(e);
}
}
}

6
src/Perspex.Styling/Styling/IGlobalStyles.cs

@ -3,8 +3,10 @@
namespace Perspex.Styling
{
public interface IGlobalStyles
/// <summary>
/// Defines the style host that provides styles global to the application.
/// </summary>
public interface IGlobalStyles : IStyleRoot
{
Styles Styles { get; }
}
}

2
src/Perspex.Styling/Styling/IStyleHost.cs

@ -6,7 +6,7 @@ namespace Perspex.Styling
/// <summary>
/// Defines an element that has a <see cref="Styles"/> collection.
/// </summary>
public interface IStyleHost : IVisual
public interface IStyleHost
{
/// <summary>
/// Gets the styles for the element.

12
src/Perspex.Styling/Styling/IStyleRoot.cs

@ -0,0 +1,12 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Perspex.Styling
{
/// <summary>
/// Denotes the root <see cref="IStyleHost"/> in a tree.
/// </summary>
public interface IStyleRoot : IStyleHost
{
}
}

9
src/Perspex.Styling/Styling/IStyleable.cs

@ -2,6 +2,8 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Perspex.Collections;
using System.Reactive;
namespace Perspex.Styling
{
@ -10,10 +12,15 @@ namespace Perspex.Styling
/// </summary>
public interface IStyleable : IObservablePropertyBag, INamed
{
/// <summary>
/// Raised when the control's style should be removed.
/// </summary>
IObservable<Unit> StyleDetach { get; }
/// <summary>
/// Gets the list of classes for the control.
/// </summary>
Classes Classes { get; }
IPerspexReadOnlyList<string> Classes { get; }
/// <summary>
/// Gets the type by which the control is styled.

2
src/Perspex.Styling/Styling/Selector.cs

@ -141,7 +141,7 @@ namespace Perspex.Styling
if (inputs.Count > 0)
{
return new SelectorMatch(new StyleActivator(inputs));
return new SelectorMatch(StyleActivator.And(inputs));
}
else
{

23
src/Perspex.Styling/Styling/Selectors.cs

@ -3,6 +3,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reflection;
@ -61,7 +64,7 @@ namespace Perspex.Styling
{
Contract.Requires<ArgumentNullException>(previous != null);
return new Selector(previous, x => MatchIs(x, type), type.Name, type);
return new Selector(previous, x => MatchIs(x, type), $":is({type.Name})", type);
}
/// <summary>
@ -176,10 +179,16 @@ namespace Perspex.Styling
private static SelectorMatch MatchClass(IStyleable control, string name)
{
return new SelectorMatch(
Observable
.Return(control.Classes.Contains(name))
.Concat(control.Classes.Changed.Select(e => control.Classes.Contains(name))));
var observable = Observable.FromEventPattern<
NotifyCollectionChangedEventHandler,
NotifyCollectionChangedEventArgs>(
x => control.Classes.CollectionChanged += x,
x => control.Classes.CollectionChanged -= x)
.Select(_ => Unit.Default)
.StartWith(Unit.Default)
.Select(_ => control.Classes.Contains(name));
return new SelectorMatch(observable);
}
private static SelectorMatch MatchDescendent(IStyleable control, Selector previous)
@ -209,9 +218,7 @@ namespace Perspex.Styling
}
}
return new SelectorMatch(new StyleActivator(
descendentMatches,
ActivatorMode.Or));
return new SelectorMatch(StyleActivator.Or(descendentMatches));
}
private static SelectorMatch MatchIs(IStyleable control, Type type)

8
src/Perspex.Styling/Styling/Style.cs

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Perspex.Metadata;
@ -13,6 +14,8 @@ namespace Perspex.Styling
/// </summary>
public class Style : IStyle
{
private static readonly IObservable<bool> True = Observable.Never<bool>().StartWith(true);
/// <summary>
/// Initializes a new instance of the <see cref="Style"/> class.
/// </summary>
@ -56,9 +59,12 @@ namespace Perspex.Styling
if (match.ImmediateResult != false)
{
var activator = (match.ObservableResult ?? True)
.TakeUntil(control.StyleDetach);
foreach (var setter in Setters)
{
setter.Apply(this, control, match.ObservableResult);
setter.Apply(this, control, activator);
}
}
}

139
src/Perspex.Styling/Styling/StyleActivator.cs

@ -4,7 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive;
using System.Reactive.Linq;
namespace Perspex.Styling
{
@ -14,134 +15,28 @@ namespace Perspex.Styling
Or,
}
public class StyleActivator : IObservable<bool>, IDisposable
public static class StyleActivator
{
private readonly ActivatorMode _mode;
private readonly bool[] _values;
private readonly List<IDisposable> _subscriptions = new List<IDisposable>();
private readonly List<IObserver<bool>> _observers = new List<IObserver<bool>>();
public StyleActivator(
IList<IObservable<bool>> inputs,
ActivatorMode mode = ActivatorMode.And)
{
int i = 0;
_mode = mode;
_values = new bool[inputs.Count];
foreach (IObservable<bool> input in inputs)
{
int capturedIndex = i;
IDisposable subscription = input.Subscribe(
x => Update(capturedIndex, x),
x => Finish(capturedIndex),
() => Finish(capturedIndex));
_subscriptions.Add(subscription);
++i;
}
}
public bool CurrentValue
{
get;
private set;
}
public bool HasCompleted
{
get;
private set;
}
public void Dispose()
{
foreach (IObserver<bool> observer in _observers)
{
observer.OnCompleted();
}
foreach (IDisposable subscription in _subscriptions)
{
subscription.Dispose();
}
}
public IDisposable Subscribe(IObserver<bool> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
observer.OnNext(CurrentValue);
if (HasCompleted)
{
observer.OnCompleted();
return Disposable.Empty;
}
else
{
_observers.Add(observer);
return Disposable.Create(() => _observers.Remove(observer));
}
}
private void Update(int index, bool value)
{
_values[index] = value;
bool current;
switch (_mode)
{
case ActivatorMode.And:
current = _values.All(x => x);
break;
case ActivatorMode.Or:
current = _values.Any(x => x);
break;
default:
throw new InvalidOperationException("Invalid Activator mode.");
}
if (current != CurrentValue)
{
Push(current);
CurrentValue = current;
}
}
private void Finish(int i)
public static IObservable<bool> And(IEnumerable<IObservable<bool>> inputs)
{
// We can unsubscribe from everything if the completed observable:
// - Is the only subscription.
// - Has finished on 'false' and we're in And mode
// - Has finished on 'true' and we're in Or mode
var value = _values[i];
var unsubscribe =
(_values.Length == 1) ||
(_mode == ActivatorMode.And ? !value : value);
var sourceArray = inputs.Select(s => s.Publish().RefCount()).ToArray();
if (unsubscribe)
{
foreach (IDisposable subscription in _subscriptions)
{
subscription.Dispose();
}
var terminate = sourceArray
.ToObservable()
.SelectMany(x => x.LastAsync()
.Where(y => y == false));
HasCompleted = true;
}
return sourceArray
.CombineLatest(values => values.All(x => x))
.DistinctUntilChanged()
.TakeUntil(terminate);
}
private void Push(bool value)
public static IObservable<bool> Or(IEnumerable<IObservable<bool>> inputs)
{
foreach (IObserver<bool> observer in _observers)
{
observer.OnNext(value);
}
return inputs.CombineLatest()
.Select(values => values.Any(x => x))
.DistinctUntilChanged();
}
}
}

13
src/Perspex.Styling/Styling/StyleBinding.cs

@ -61,7 +61,8 @@ namespace Perspex.Styling
/// </summary>
public object ActivatedValue
{
get; }
get;
}
/// <summary>
/// Gets a description of the binding.
@ -90,16 +91,16 @@ namespace Perspex.Styling
if (Source == null)
{
return _activator.Subscribe(
active => observer.OnNext(active ? ActivatedValue : PerspexProperty.UnsetValue),
observer.OnError,
observer.OnCompleted);
return _activator
.Select(active => active ? ActivatedValue : PerspexProperty.UnsetValue)
.Subscribe(observer);
}
else
{
return _activator
.CombineLatest(Source, (x, y) => new { Active = x, Value = y })
.Subscribe(x => observer.OnNext(x.Active ? x.Value : PerspexProperty.UnsetValue));
.Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue)
.Subscribe(observer);
}
}
}

28
src/Perspex.Styling/Styling/Styler.cs

@ -2,8 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Perspex.VisualTree;
namespace Perspex.Styling
{
@ -11,39 +9,27 @@ namespace Perspex.Styling
{
public void ApplyStyles(IStyleable control)
{
IVisual visual = control as IVisual;
IStyleHost styleContainer = visual
.GetSelfAndVisualAncestors()
.OfType<IStyleHost>()
.FirstOrDefault();
IGlobalStyles global = PerspexLocator.Current.GetService<IGlobalStyles>();
var styleHost = control as IStyleHost;
global?.Styles.Attach(control, null);
if (styleContainer != null)
if (styleHost != null)
{
ApplyStyles(control, styleContainer);
ApplyStyles(control, styleHost);
}
}
private void ApplyStyles(IStyleable control, IStyleHost container)
private void ApplyStyles(IStyleable control, IStyleHost styleHost)
{
Contract.Requires<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(container != null);
Contract.Requires<ArgumentNullException>(styleHost != null);
var parentContainer = container.StylingParent;
var parentContainer = styleHost.StylingParent;
if (parentContainer != null)
{
ApplyStyles(control, parentContainer);
}
container.Styles.Attach(control, container);
}
private IStyleHost GetParentContainer(IStyleHost container)
{
return container.GetVisualAncestors().OfType<IStyleHost>().FirstOrDefault();
styleHost.Styles.Attach(control, styleHost);
}
}
}

3
src/Perspex.Themes.Default/Button.paml

@ -13,7 +13,8 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

3
src/Perspex.Themes.Default/DefaultTheme.paml

@ -18,10 +18,11 @@
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ScrollBar.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ScrollViewer.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabControl.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabItem.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabStrip.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TabStripItem.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TextBox.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ToggleButton.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.Expander.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.ToolTip.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TreeView.paml"/>
<StyleInclude Source="resource://application/Perspex.Themes.Default/Perspex.Themes.Default.TreeViewItem.paml"/>

123
src/Perspex.Themes.Default/Expander.paml

@ -0,0 +1,123 @@
<Styles xmlns="https://github.com/perspex">
<Style Selector="Expander">
<Setter Property="ContentTransition">
<Setter.Value>
<CrossFade Duration="00:00:00.25" />
</Setter.Value>
</Setter>
</Style>
<Style Selector="Expander[ExpandDirection=Down]">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<Grid RowDefinitions="Auto,*">
<ToggleButton Name="PART_toggle" Grid.Row="0" Content="{TemplateBinding Header}" IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
<ContentPresenter Name="PART_ContentPresenter"
Grid.Row="1"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Expander[ExpandDirection=Up]">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<Grid RowDefinitions="*,Auto">
<ToggleButton Name="PART_toggle" Grid.Row="1" Content="{TemplateBinding Header}" IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
<ContentPresenter Name="PART_ContentPresenter"
Grid.Row="0"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Expander[ExpandDirection=Right]">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<Grid ColumnDefinitions="Auto,*">
<ToggleButton Name="PART_toggle" Grid.Column="0" Content="{TemplateBinding Header}" IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
<ContentPresenter Name="PART_ContentPresenter"
Grid.Column="1"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Expander[ExpandDirection=Left]">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}">
<Grid ColumnDefinitions="*,Auto">
<ToggleButton Name="PART_toggle" Grid.Column="1" Content="{TemplateBinding Header}" IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}" />
<ContentPresenter Name="PART_ContentPresenter"
Grid.Column="0"
IsVisible="{TemplateBinding IsExpanded}"
Content="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Expander /template/ ToggleButton#PART_toggle">
<Setter Property="Template">
<ControlTemplate>
<Border BorderThickness="1">
<Grid ColumnDefinitions="Auto,Auto">
<Border Grid.Column="0" Width="20" Height="20" HorizontalAlignment="Center" VerticalAlignment="Center">
<Path Fill="Black"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 2 L 4 6 L 0 10 Z" />
</Border>
<ContentPresenter Grid.Column="1" Content="{TemplateBinding Content}" VerticalAlignment="Center" />
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Expander /template/ ToggleButton#PART_toggle:pointerover /template/ Border">
<Setter Property="BorderBrush" Value="#ffaaaaaa" />
</Style>
<Style Selector="Expander:down:expanded /template/ ToggleButton#PART_toggle /template/ Path">
<Setter Property="RenderTransform">
<RotateTransform Angle="90" />
</Setter>
</Style>
<Style Selector="Expander:up:expanded /template/ ToggleButton#PART_toggle /template/ Path">
<Setter Property="RenderTransform">
<RotateTransform Angle="-90" />
</Setter>
</Style>
<Style Selector="Expander:left:expanded /template/ ToggleButton#PART_toggle /template/ Path">
<Setter Property="RenderTransform">
<RotateTransform Angle="180" />
</Setter>
</Style>
<Style Selector="Expander:right /template/ ToggleButton#PART_toggle /template/ Path">
<Setter Property="RenderTransform">
<RotateTransform Angle="180" />
</Setter>
</Style>
<Style Selector="Expander:right:expanded /template/ ToggleButton#PART_toggle /template/ Path">
<Setter Property="RenderTransform">
<RotateTransform Angle="0" />
</Setter>
</Style>
</Styles>

26
src/Perspex.Themes.Default/MenuItem.paml

@ -24,7 +24,8 @@
IsVisible="False"
Margin="3"
VerticalAlignment="Center"/>
<ContentPresenter Content="{TemplateBinding Header}"
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Grid.Column="2">
@ -44,22 +45,22 @@
PlacementMode="Right"
StaysOpen="True"
IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}">
<Border Background="#fff0f0f0"
<Border Background="{TemplateBinding Background}"
BorderBrush="#ff999999"
BorderThickness="1"
Padding="2">
<ScrollViewer>
<Panel>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
MemberSelector="{TemplateBinding MemberSelector}"/>
<Rectangle Name="iconSeparator"
Fill="#ffd7d7d7"
HorizontalAlignment="Left"
IsHitTestVisible="False"
Margin="29,2,0,2"
Width="1"/>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
MemberSelector="{TemplateBinding MemberSelector}"/>
</Panel>
</ScrollViewer>
</Border>
@ -78,7 +79,8 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Panel>
<ContentPresenter Content="{TemplateBinding Header}"
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
@ -89,22 +91,22 @@
<Popup Name="PART_Popup"
IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"
StaysOpen="True">
<Border Background="#fff0f0f0"
<Border Background="{TemplateBinding Background}"
BorderBrush="#ff999999"
BorderThickness="1"
Padding="2">
<ScrollViewer>
<Panel>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
MemberSelector="{TemplateBinding MemberSelector}"/>
<Rectangle Name="iconSeparator"
Fill="#ffd7d7d7"
HorizontalAlignment="Left"
IsHitTestVisible="False"
Margin="29,2,0,2"
Width="1"/>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
MemberSelector="{TemplateBinding MemberSelector}"/>
</Panel>
</ScrollViewer>
</Border>

3
src/Perspex.Themes.Default/Perspex.Themes.Default.csproj

@ -143,6 +143,7 @@
<EmbeddedResource Include="ToolTip.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="Expander.paml" />
<None Include="packages.config" />
<EmbeddedResource Include="PopupRoot.paml">
<SubType>Designer</SubType>
@ -159,7 +160,7 @@
<EmbeddedResource Include="TabControl.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="TabItem.paml">
<EmbeddedResource Include="TabStripItem.paml">
<SubType>Designer</SubType>
</EmbeddedResource>
<EmbeddedResource Include="TabStrip.paml">

36
src/Perspex.Themes.Default/TabControl.paml

@ -3,10 +3,13 @@
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="Auto,*">
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}"/>
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
Grid.Row="1"/>
@ -18,11 +21,14 @@
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="*,Auto">
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"/>
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}"
Grid.Row="1"/>
</Grid>
@ -35,7 +41,9 @@
<Grid ColumnDefinitions="Auto,*">
<Border>
<ScrollViewer>
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}">
<TabStrip.ItemsPanel>
<ItemsPanelTemplate>
@ -45,8 +53,9 @@
</TabStrip>
</ScrollViewer>
</Border>
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"
Grid.Column="1"/>
@ -58,13 +67,16 @@
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto">
<Carousel Items="{TemplateBinding Items}"
MemberSelector="Content"
<Carousel Name="PART_Content"
MemberSelector="{Static TabControl.ContentSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex}"
Transition="{TemplateBinding Transition}"/>
<Border Grid.Column="1">
<ScrollViewer>
<TabStrip Items="{TemplateBinding Items}"
<TabStrip Name="PART_TabStrip"
MemberSelector="{Static TabControl.HeaderSelector}"
Items="{TemplateBinding Items}"
SelectedIndex="{TemplateBinding Path=SelectedIndex, Mode=TwoWay}">
<TabStrip.ItemsPanel>
<ItemsPanelTemplate>

6
src/Perspex.Themes.Default/TabStrip.paml

@ -2,7 +2,9 @@
<Style Selector="TabStrip">
<Setter Property="Template">
<ControlTemplate>
<ItemsPresenter Items="{TemplateBinding Items}"
<ItemsPresenter Name="PART_ItemsPresenter"
MemberSelector="{TemplateBinding MemberSelector}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"/>
</ControlTemplate>
</Setter>
@ -12,7 +14,7 @@
</ItemsPanelTemplate>
</Setter>
</Style>
<Style Selector="TabStrip > TabItem">
<Style Selector="TabStrip > TabStripItem">
<Setter Property="Margin" Value="16"/>
</Style>
</Styles>

8
src/Perspex.Themes.Default/TabItem.paml → src/Perspex.Themes.Default/TabStripItem.paml

@ -1,14 +1,16 @@
<Styles xmlns="https://github.com/perspex">
<Style Selector="TabItem">
<Style Selector="TabStripItem">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_HeaderPresenter" Content="{TemplateBinding Header}"/>
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"/>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabItem:selected">
<Style Selector="TabStripItem:selected">
<Setter Property="Foreground" Value="Black"/>
</Style>
</Styles>

3
src/Perspex.Themes.Default/ToggleButton.paml

@ -13,7 +13,8 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Content="{TemplateBinding Content}"
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
TextBlock.Foreground="{TemplateBinding Foreground}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

2
src/Perspex.Themes.Default/TreeViewItem.paml

@ -12,7 +12,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Grid.Column="1">
<ContentPresenter Name="PART_ContentPresenter"
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}"/>
</Border>

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

Loading…
Cancel
Save