diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj index 6547b5bf99..2cb1dd0369 100644 --- a/Perspex.Base/Perspex.Base.csproj +++ b/Perspex.Base/Perspex.Base.csproj @@ -37,6 +37,7 @@ + diff --git a/Perspex.Base/PerspexListExtensions.cs b/Perspex.Base/PerspexListExtensions.cs new file mode 100644 index 0000000000..c62a8bfad0 --- /dev/null +++ b/Perspex.Base/PerspexListExtensions.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex +{ + public static class PerspexListExtensions + { + public static IDisposable ForEachItem( + this IReadOnlyPerspexList collection, + Action added, + Action removed) + { + NotifyCollectionChangedEventHandler handler = (_, e) => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (T i in e.NewItems) + { + added(i); + } + break; + + case NotifyCollectionChangedAction.Replace: + foreach (T i in e.OldItems) + { + removed(i); + } + + foreach (T i in e.NewItems) + { + added(i); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (T i in e.OldItems) + { + removed(i); + } + break; + } + }; + + foreach (T i in collection) + { + added(i); + } + + collection.CollectionChanged += handler; + + System.Diagnostics.Debug.WriteLine("Tracked " + collection.GetHashCode()); + + return Disposable.Create(() => collection.CollectionChanged -= handler); + } + + public static IDisposable TrackItemPropertyChanged( + this IReadOnlyPerspexList collection, + Action> callback) + { + List tracked = new List(); + + PropertyChangedEventHandler handler = (s, e) => + { + callback(Tuple.Create(s, e)); + }; + + collection.ForEachItem( + x => + { + var inpc = x as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged += handler; + tracked.Add(inpc); + } + }, + x => + { + var inpc = x as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged -= handler; + tracked.Remove(inpc); + } + }); + + return Disposable.Create(() => + { + foreach (var i in tracked) + { + i.PropertyChanged -= handler; + } + }); + } + } +} diff --git a/Perspex.Base/PerspexObject.cs b/Perspex.Base/PerspexObject.cs index c2d0387763..b5febbdbdd 100644 --- a/Perspex.Base/PerspexObject.cs +++ b/Perspex.Base/PerspexObject.cs @@ -8,6 +8,7 @@ namespace Perspex { using System; using System.Collections.Generic; + using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Reflection; @@ -56,7 +57,7 @@ namespace Perspex /// /// This class is analogous to DependencyObject in WPF. /// - public class PerspexObject : IEnableLogger + public class PerspexObject : INotifyPropertyChanged, IEnableLogger { /// /// The registered properties by type. @@ -76,10 +77,24 @@ namespace Perspex new Dictionary(); /// - /// Raised when a value changes on this object/ + /// Event handler for implementation. + /// + private PropertyChangedEventHandler inpcChanged; + + /// + /// Raised when a value changes on this object. /// public event EventHandler PropertyChanged; + /// + /// Raised when a value changes on this object. + /// + event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged + { + add { this.inpcChanged += value; } + remove { this.inpcChanged -= value; } + } + /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -646,6 +661,12 @@ namespace Perspex property.NotifyChanged(e); this.PropertyChanged(this, e); } + + if (this.inpcChanged != null) + { + PropertyChangedEventArgs e = new PropertyChangedEventArgs(property.Name); + this.inpcChanged(this, e); + } } } } diff --git a/Perspex.Controls/Button.cs b/Perspex.Controls/Button.cs index 95462d27fc..c6ef99954f 100644 --- a/Perspex.Controls/Button.cs +++ b/Perspex.Controls/Button.cs @@ -34,7 +34,10 @@ namespace Perspex.Controls if (this.Classes.Contains(":pointerover")) { - RoutedEventArgs click = new RoutedEventArgs(ClickEvent, this); + RoutedEventArgs click = new RoutedEventArgs + { + RoutedEvent = ClickEvent, + }; this.RaiseEvent(click); } }; diff --git a/Perspex.Controls/Grid.cs b/Perspex.Controls/Grid.cs index 4cc626b139..617cc50982 100644 --- a/Perspex.Controls/Grid.cs +++ b/Perspex.Controls/Grid.cs @@ -27,25 +27,64 @@ namespace Perspex.Controls public static readonly PerspexProperty RowSpanProperty = PerspexProperty.RegisterAttached("RowSpan", 1); + private ColumnDefinitions columnDefinitions; + + private RowDefinitions rowDefinitions; + private Segment[,] rowMatrix; + private Segment[,] colMatrix; public Grid() { - this.ColumnDefinitions = new ColumnDefinitions(); - this.RowDefinitions = new RowDefinitions(); } public ColumnDefinitions ColumnDefinitions { - get; - set; + get + { + if (this.columnDefinitions == null) + { + this.ColumnDefinitions = new ColumnDefinitions(); + } + + return this.columnDefinitions; + } + + set + { + if (this.columnDefinitions != null) + { + throw new NotSupportedException("Reassigning ColumnDefinitions not yet implemented."); + } + + this.columnDefinitions = value; + this.columnDefinitions.TrackItemPropertyChanged(_ => this.InvalidateMeasure()); + } } public RowDefinitions RowDefinitions { - get; - set; + get + { + if (this.rowDefinitions == null) + { + this.RowDefinitions = new RowDefinitions(); + } + + return this.rowDefinitions; + } + + set + { + if (this.rowDefinitions != null) + { + throw new NotSupportedException("Reassigning RowDefinitions not yet implemented."); + } + + this.rowDefinitions = value; + this.rowDefinitions.TrackItemPropertyChanged(_ => this.InvalidateMeasure()); + } } public static int GetColumn(PerspexObject element) diff --git a/Perspex.Controls/GridSplitter.cs b/Perspex.Controls/GridSplitter.cs new file mode 100644 index 0000000000..93d62c346f --- /dev/null +++ b/Perspex.Controls/GridSplitter.cs @@ -0,0 +1,34 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls +{ + using System; + using Perspex.Controls.Primitives; + using Perspex.Input; + + public class GridSplitter : Thumb + { + private Grid grid; + + protected override void OnDragDelta(VectorEventArgs e) + { + int col = this.GetValue(Grid.ColumnProperty); + + if (grid != null && col > 0) + { + grid.ColumnDefinitions[col - 1].Width = new GridLength( + grid.ColumnDefinitions[col - 1].ActualWidth + e.Vector.X, + GridUnitType.Pixel); + } + } + + protected override void OnVisualParentChanged(Visual oldParent) + { + this.grid = this.GetVisualParent(); + } + } +} diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj index ce2217bd1e..55b51c2ec4 100644 --- a/Perspex.Controls/Perspex.Controls.csproj +++ b/Perspex.Controls/Perspex.Controls.csproj @@ -40,6 +40,7 @@ + @@ -65,6 +66,7 @@ + diff --git a/Perspex.Controls/Primitives/Thumb.cs b/Perspex.Controls/Primitives/Thumb.cs new file mode 100644 index 0000000000..5c9adfcb26 --- /dev/null +++ b/Perspex.Controls/Primitives/Thumb.cs @@ -0,0 +1,108 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.Primitives +{ + using System; + using Perspex.Input; + using Perspex.Interactivity; + + public class Thumb : TemplatedControl + { + public static readonly RoutedEvent DragStartedEvent = + RoutedEvent.Register("DragStarted", RoutingStrategy.Bubble); + + public static readonly RoutedEvent DragDeltaEvent = + RoutedEvent.Register("DragDelta", RoutingStrategy.Bubble); + + public static readonly RoutedEvent DragCompletedEvent = + RoutedEvent.Register("DragCompleted", RoutingStrategy.Bubble); + + Point? lastPoint; + + public Thumb() + { + this.DragStarted += (_, e) => this.OnDragStarted(e); + this.DragDelta += (_, e) => this.OnDragDelta(e); + this.DragCompleted += (_, e) => this.OnDragCompleted(e); + } + + public event EventHandler DragStarted + { + add { this.AddHandler(DragStartedEvent, value); } + remove { this.RemoveHandler(DragStartedEvent, value); } + } + + public event EventHandler DragDelta + { + add { this.AddHandler(DragDeltaEvent, value); } + remove { this.RemoveHandler(DragDeltaEvent, value); } + } + + public event EventHandler DragCompleted + { + add { this.AddHandler(DragCompletedEvent, value); } + remove { this.RemoveHandler(DragCompletedEvent, value); } + } + + protected virtual void OnDragStarted(VectorEventArgs e) + { + } + + protected virtual void OnDragDelta(VectorEventArgs e) + { + } + + protected virtual void OnDragCompleted(VectorEventArgs e) + { + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + if (this.lastPoint.HasValue) + { + var ev = new VectorEventArgs + { + RoutedEvent = DragDeltaEvent, + Vector = e.GetPosition(this) - this.lastPoint.Value, + }; + + this.RaiseEvent(ev); + } + } + + protected override void OnPointerPressed(PointerEventArgs e) + { + e.Device.Capture(this); + this.lastPoint = e.GetPosition(this); + + var ev = new VectorEventArgs + { + RoutedEvent = DragStartedEvent, + Vector = (Vector)this.lastPoint, + }; + + this.RaiseEvent(ev); + } + + protected override void OnPointerReleased(PointerEventArgs e) + { + if (this.lastPoint.HasValue) + { + e.Device.Capture(null); + this.lastPoint = null; + + var ev = new VectorEventArgs + { + RoutedEvent = DragCompletedEvent, + Vector = (Vector)e.GetPosition(this), + }; + + this.RaiseEvent(ev); + } + } + } +} diff --git a/Perspex.Diagnostics/DevTools.cs b/Perspex.Diagnostics/DevTools.cs index 682b71753f..17f35f08a2 100644 --- a/Perspex.Diagnostics/DevTools.cs +++ b/Perspex.Diagnostics/DevTools.cs @@ -46,7 +46,13 @@ namespace Perspex.Diagnostics .Where(x => x != null) .Cast() .Select(x => new ControlDetails(x.Visual)), + [Grid.ColumnProperty] = 2, + }; + + var splitter = new GridSplitter + { [Grid.ColumnProperty] = 1, + Width = 4, }; this.Content = new Grid @@ -54,11 +60,13 @@ namespace Perspex.Diagnostics ColumnDefinitions = new ColumnDefinitions { new ColumnDefinition(1, GridUnitType.Star), + new ColumnDefinition(4, GridUnitType.Pixel), new ColumnDefinition(3, GridUnitType.Star), }, Children = new Controls { treeView, + splitter, detailsView, } }; diff --git a/Perspex.Input/InputElement.cs b/Perspex.Input/InputElement.cs index 0fda582268..4c0038aae4 100644 --- a/Perspex.Input/InputElement.cs +++ b/Perspex.Input/InputElement.cs @@ -39,6 +39,9 @@ namespace Perspex.Input public static readonly RoutedEvent PointerLeaveEvent = RoutedEvent.Register("PointerLeave", RoutingStrategy.Direct); + public static readonly RoutedEvent PointerMovedEvent = + RoutedEvent.Register("PointerMove", RoutingStrategy.Bubble); + public static readonly RoutedEvent PointerPressedEvent = RoutedEvent.Register("PointerPressed", RoutingStrategy.Bubble); @@ -53,6 +56,7 @@ namespace Perspex.Input this.PreviewKeyDown += (_, e) => this.OnPreviewKeyDown(e); this.PointerEnter += (_, e) => this.OnPointerEnter(e); this.PointerLeave += (_, e) => this.OnPointerLeave(e); + this.PointerMoved += (_, e) => this.OnPointerMoved(e); this.PointerPressed += (_, e) => this.OnPointerPressed(e); this.PointerReleased += (_, e) => this.OnPointerReleased(e); } @@ -93,6 +97,12 @@ namespace Perspex.Input remove { this.RemoveHandler(PointerLeaveEvent, value); } } + public event EventHandler PointerMoved + { + add { this.AddHandler(PointerMovedEvent, value); } + remove { this.RemoveHandler(PointerMovedEvent, value); } + } + public event EventHandler PointerPressed { add { this.AddHandler(PointerPressedEvent, value); } @@ -156,6 +166,10 @@ namespace Perspex.Input this.IsPointerOver = false; } + protected virtual void OnPointerMoved(PointerEventArgs e) + { + } + protected virtual void OnPointerPressed(PointerEventArgs e) { } diff --git a/Perspex.Input/MouseDevice.cs b/Perspex.Input/MouseDevice.cs index 89fa0cb8cd..4ef82f543b 100644 --- a/Perspex.Input/MouseDevice.cs +++ b/Perspex.Input/MouseDevice.cs @@ -81,9 +81,12 @@ namespace Perspex.Input private void MouseMove(IMouseDevice device, IVisual visual, Point p) { + IInteractive source; + if (this.Captured == null) { this.InputManager.SetPointerOver(this, visual, p); + source = visual as IInteractive; } else { @@ -95,6 +98,18 @@ namespace Perspex.Input } this.InputManager.SetPointerOver(this, this.Captured, p - offset); + source = this.Captured as IInteractive; + } + + if (source != null) + { + source.RaiseEvent(new PointerEventArgs + { + Device = this, + RoutedEvent = InputElement.PointerMovedEvent, + OriginalSource = source, + Source = source, + }); } } diff --git a/Perspex.Input/Perspex.Input.csproj b/Perspex.Input/Perspex.Input.csproj index d9282a1148..35d8451ee4 100644 --- a/Perspex.Input/Perspex.Input.csproj +++ b/Perspex.Input/Perspex.Input.csproj @@ -66,6 +66,7 @@ + diff --git a/Perspex.Input/VectorEventArgs.cs b/Perspex.Input/VectorEventArgs.cs new file mode 100644 index 0000000000..be6cf7c1e9 --- /dev/null +++ b/Perspex.Input/VectorEventArgs.cs @@ -0,0 +1,16 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2013 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Input +{ + using System; + using Perspex.Interactivity; + + public class VectorEventArgs : RoutedEventArgs + { + public Vector Vector { get; set; } + } +} diff --git a/Perspex.Interactivity/Interactive.cs b/Perspex.Interactivity/Interactive.cs index f9c16d84f5..509ffcedd5 100644 --- a/Perspex.Interactivity/Interactive.cs +++ b/Perspex.Interactivity/Interactive.cs @@ -59,6 +59,9 @@ namespace Perspex.Interactivity { Contract.Requires(e != null); + e.Source = e.Source ?? this; + e.OriginalSource = e.OriginalSource ?? this; + if (!e.Handled) { switch (e.RoutedEvent.RoutingStrategy) diff --git a/Perspex.Interactivity/RoutedEventArgs.cs b/Perspex.Interactivity/RoutedEventArgs.cs index 23d97b9818..9e54d1d585 100644 --- a/Perspex.Interactivity/RoutedEventArgs.cs +++ b/Perspex.Interactivity/RoutedEventArgs.cs @@ -10,21 +10,6 @@ namespace Perspex.Interactivity public class RoutedEventArgs : EventArgs { - public RoutedEventArgs() - { - } - - public RoutedEventArgs(RoutedEvent routedEvent) - { - this.RoutedEvent = routedEvent; - } - - public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source) - { - this.RoutedEvent = routedEvent; - this.Source = source; - } - public bool Handled { get; set; } public IInteractive OriginalSource { get; set; } diff --git a/Perspex.Themes.Default/DefaultTheme.cs b/Perspex.Themes.Default/DefaultTheme.cs index e4f7f1ee5f..bf33d21b2e 100644 --- a/Perspex.Themes.Default/DefaultTheme.cs +++ b/Perspex.Themes.Default/DefaultTheme.cs @@ -15,6 +15,7 @@ namespace Perspex.Themes.Default this.Add(new ButtonStyle()); this.Add(new CheckBoxStyle()); this.Add(new ContentControlStyle()); + this.Add(new GridSplitterStyle()); this.Add(new ItemsControlStyle()); this.Add(new TabControlStyle()); this.Add(new TabItemStyle()); diff --git a/Perspex.Themes.Default/GridSplitterStyle.cs b/Perspex.Themes.Default/GridSplitterStyle.cs new file mode 100644 index 0000000000..99cb0d04aa --- /dev/null +++ b/Perspex.Themes.Default/GridSplitterStyle.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Themes.Default +{ + using System.Linq; + using Perspex.Controls; + using Perspex.Controls.Presenters; + using Perspex.Media; + using Perspex.Styling; + + public class GridSplitterStyle : Styles + { + public GridSplitterStyle() + { + this.AddRange(new[] + { + new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(GridSplitter.TemplateProperty, ControlTemplate.Create(this.Template)), + new Setter(GridSplitter.WidthProperty, 4), + }, + }, + }); + } + + private Control Template(GridSplitter control) + { + Border border = new Border + { + [~Border.BackgroundProperty] = control[~GridSplitter.BackgroundProperty], + }; + + return border; + } + } +} diff --git a/Perspex.Themes.Default/Perspex.Themes.Default.csproj b/Perspex.Themes.Default/Perspex.Themes.Default.csproj index 30dd73a5f5..e436a1f65c 100644 --- a/Perspex.Themes.Default/Perspex.Themes.Default.csproj +++ b/Perspex.Themes.Default/Perspex.Themes.Default.csproj @@ -66,6 +66,7 @@ +