From 20a86e1564c53450a468231fa2f2fdc60f9bf808 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 May 2015 19:33:34 +0200 Subject: [PATCH 01/12] Added scroll viewer to devtools property pane. --- Perspex.Diagnostics/DevTools.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Perspex.Diagnostics/DevTools.cs b/Perspex.Diagnostics/DevTools.cs index f1cba06010..df76a45e54 100644 --- a/Perspex.Diagnostics/DevTools.cs +++ b/Perspex.Diagnostics/DevTools.cs @@ -70,16 +70,19 @@ namespace Perspex.Diagnostics } }; - var detailsView = new ContentControl + var detailsView = new ScrollViewer { - DataTemplates = new DataTemplates + Content = new ContentControl { - new DataTemplate(CreateDetailsView), - }, - [!ContentControl.ContentProperty] = treeView[!TreeView.SelectedItemProperty] + DataTemplates = new DataTemplates + { + new DataTemplate(CreateDetailsView), + }, + [!ContentControl.ContentProperty] = treeView[!TreeView.SelectedItemProperty] .Where(x => x != null) .Cast() .Select(x => new ControlDetails(x.Control)), + }, [Grid.ColumnProperty] = 2, }; From f1150c95a2eabf43a5dcd3eec9cfae15d6433a10 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 May 2015 21:26:12 +0200 Subject: [PATCH 02/12] Use ObservableBase for StyleBinding. Also use IDescription instead of IObservableDescription as IObservableDescription is about to be deleted. --- Perspex.Styling/StyleBinding.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Perspex.Styling/StyleBinding.cs b/Perspex.Styling/StyleBinding.cs index 622f667017..b9e7b8a35f 100644 --- a/Perspex.Styling/StyleBinding.cs +++ b/Perspex.Styling/StyleBinding.cs @@ -7,7 +7,7 @@ namespace Perspex.Styling { using System; - using System.Reactive.Subjects; + using System.Reactive; /// /// Provides an observable for a style. @@ -17,7 +17,7 @@ namespace Perspex.Styling /// a bool. When the activator produces true, this observable will produce . /// When the activator produces false it will produce . /// - internal class StyleBinding : IObservable, IObservableDescription + internal class StyleBinding : ObservableBase, IDescription { /// /// The activator. @@ -63,7 +63,7 @@ namespace Perspex.Styling /// /// The observer. /// IDisposable object used to unsubscribe from the observable sequence. - public IDisposable Subscribe(IObserver observer) + protected override IDisposable SubscribeCore(IObserver observer) { Contract.Requires(observer != null); return this.activator.Subscribe( From 8b18504e5e8f7697f86c713a956bae243bf0b48e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 09:18:42 +0200 Subject: [PATCH 03/12] Use ObservableBase, IDescription for Binding. --- Perspex.Base/Binding.cs | 16 +++++++++------- Perspex.Base/PriorityBindingEntry.cs | 3 +++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Perspex.Base/Binding.cs b/Perspex.Base/Binding.cs index 88e2d96b65..93dd105187 100644 --- a/Perspex.Base/Binding.cs +++ b/Perspex.Base/Binding.cs @@ -7,7 +7,7 @@ namespace Perspex { using System; - using System.Reactive.Linq; + using System.Reactive; public enum BindingMode { @@ -18,7 +18,7 @@ namespace Perspex OneWayToSource, } - public class Binding : IObservable + public class Binding : ObservableBase, IDescription { public BindingMode Mode { @@ -44,6 +44,8 @@ namespace Perspex set; } + public string Description => string.Format("{0}.{1}", this.Source.GetType().Name, this.Property.Name); + public static Binding operator !(Binding binding) { return binding.WithMode(BindingMode.TwoWay); @@ -54,11 +56,6 @@ namespace Perspex return binding.WithMode(BindingMode.TwoWay); } - public IDisposable Subscribe(IObserver observer) - { - return this.Source.GetObservable(this.Property).Subscribe(observer); - } - public Binding WithMode(BindingMode mode) { this.Mode = mode; @@ -70,5 +67,10 @@ namespace Perspex this.Priority = priority; return this; } + + protected override IDisposable SubscribeCore(IObserver observer) + { + return this.Source.GetObservable(this.Property).Subscribe(observer); + } } } diff --git a/Perspex.Base/PriorityBindingEntry.cs b/Perspex.Base/PriorityBindingEntry.cs index 5e2ff6a018..1463f21a63 100644 --- a/Perspex.Base/PriorityBindingEntry.cs +++ b/Perspex.Base/PriorityBindingEntry.cs @@ -24,6 +24,8 @@ namespace Perspex this.Index = index; } + public IObservable Observable { get; private set; } + /// /// Gets a description of the binding. /// @@ -67,6 +69,7 @@ namespace Perspex throw new Exception("PriorityValue.Entry.Start() called more than once."); } + this.Observable = binding; this.Value = PerspexProperty.UnsetValue; if (binding is IDescription) From 040f099385eb7af099079d7df80324f740fe9702 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 09:19:19 +0200 Subject: [PATCH 04/12] Remove IObservableDescription. It is superseded by IDescription. --- Perspex.Base/IObservableDescription.cs | 13 ------------- Perspex.Base/Perspex.Base.csproj | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 Perspex.Base/IObservableDescription.cs diff --git a/Perspex.Base/IObservableDescription.cs b/Perspex.Base/IObservableDescription.cs deleted file mode 100644 index fb7fb3e415..0000000000 --- a/Perspex.Base/IObservableDescription.cs +++ /dev/null @@ -1,13 +0,0 @@ -// ----------------------------------------------------------------------- -// -// Copyright 2014 MIT Licence. See licence.md for more information. -// -// ----------------------------------------------------------------------- - -namespace Perspex -{ - public interface IObservableDescription - { - string Description { get; } - } -} diff --git a/Perspex.Base/Perspex.Base.csproj b/Perspex.Base/Perspex.Base.csproj index fdba33905a..d6c261cee3 100644 --- a/Perspex.Base/Perspex.Base.csproj +++ b/Perspex.Base/Perspex.Base.csproj @@ -46,7 +46,6 @@ - @@ -57,6 +56,7 @@ + From cffe103acc1c148b59f7c12f6a4bfff12004d467 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 09:21:25 +0200 Subject: [PATCH 05/12] Added PerspexObservable. Similar to Rx's AnonymousObservable that is created using Observable.Create, but it also implements IDescription. This description is populated when created by PerspexObject.GetObservable. --- Perspex.Base/PerspexObject.cs | 34 +++++++++++++-------- Perspex.Base/Reactive/PerspexObservable.cs | 35 ++++++++++++++++++++++ 2 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 Perspex.Base/Reactive/PerspexObservable.cs diff --git a/Perspex.Base/PerspexObject.cs b/Perspex.Base/PerspexObject.cs index 564698a54b..25994d2093 100644 --- a/Perspex.Base/PerspexObject.cs +++ b/Perspex.Base/PerspexObject.cs @@ -15,7 +15,7 @@ namespace Perspex using Perspex.Diagnostics; using Splat; using System.Reactive.Disposables; - + using Perspex.Reactive; /// /// The priority of a binding. @@ -306,7 +306,7 @@ namespace Perspex { Contract.Requires(property != null); - return Observable.Create(observer => + return new PerspexObservable(observer => { EventHandler handler = (s, e) => { @@ -318,19 +318,19 @@ namespace Perspex this.PropertyChanged += handler; - return () => + return Disposable.Create(() => { this.PropertyChanged -= handler; - }; - }).StartWith(this.GetValue(property)); + }); + }, this.GetObservableDescription(property)).StartWith(this.GetValue(property)); } /// /// Gets an observable for a . /// - /// - /// - /// + /// The property type. + /// The property. + /// An observable. public IObservable GetObservable(PerspexProperty property) { Contract.Requires(property != null); @@ -346,7 +346,7 @@ namespace Perspex /// public IObservable> GetObservableWithHistory(PerspexProperty property) { - return Observable.Create>(observer => + return new PerspexObservable>(observer => { EventHandler handler = (s, e) => { @@ -358,11 +358,11 @@ namespace Perspex this.PropertyChanged += handler; - return () => + return Disposable.Create(() => { this.PropertyChanged -= handler; - }; - }); + }); + }, this.GetObservableDescription(property)); } /// @@ -759,6 +759,16 @@ namespace Perspex } } + /// + /// Gets a description of a property that van be used in observables. + /// + /// The property + /// The description. + private string GetObservableDescription(PerspexProperty property) + { + return string.Format("{0}.{1}", this.GetType().Name, property.Name); + } + /// /// Raises the event. /// diff --git a/Perspex.Base/Reactive/PerspexObservable.cs b/Perspex.Base/Reactive/PerspexObservable.cs new file mode 100644 index 0000000000..ad5b1568bb --- /dev/null +++ b/Perspex.Base/Reactive/PerspexObservable.cs @@ -0,0 +1,35 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2014 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Reactive +{ + using System; + using System.Reactive; + using System.Reactive.Disposables; + + public sealed class PerspexObservable : ObservableBase, IDescription + { + private readonly Func, IDisposable> subscribe; + + public string Description { get; } + + public PerspexObservable(Func, IDisposable> subscribe, string description) + { + if (subscribe == null) + { + throw new ArgumentNullException("subscribe"); + } + + this.subscribe = subscribe; + this.Description = description; + } + + protected override IDisposable SubscribeCore(IObserver observer) + { + return this.subscribe(observer) ?? Disposable.Empty; + } + } +} \ No newline at end of file From 78595886217e4b3c5aea0bf41a23aceba6e75817 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 09:59:18 +0200 Subject: [PATCH 06/12] Don't use StartWith in GetObservable As it erases the Description. --- Perspex.Base/PerspexObject.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Perspex.Base/PerspexObject.cs b/Perspex.Base/PerspexObject.cs index 25994d2093..a91d06b3f0 100644 --- a/Perspex.Base/PerspexObject.cs +++ b/Perspex.Base/PerspexObject.cs @@ -316,13 +316,15 @@ namespace Perspex } }; + observer.OnNext(this.GetValue(property)); + this.PropertyChanged += handler; return Disposable.Create(() => { this.PropertyChanged -= handler; }); - }, this.GetObservableDescription(property)).StartWith(this.GetValue(property)); + }, this.GetObservableDescription(property)); } /// From 921b0ca725c32c2fe33479d0cba80e2bbd51d1e0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 11:49:03 +0200 Subject: [PATCH 07/12] Binding.Source may be null. --- Perspex.Base/Binding.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Perspex.Base/Binding.cs b/Perspex.Base/Binding.cs index 93dd105187..dc050b7e92 100644 --- a/Perspex.Base/Binding.cs +++ b/Perspex.Base/Binding.cs @@ -44,7 +44,7 @@ namespace Perspex set; } - public string Description => string.Format("{0}.{1}", this.Source.GetType().Name, this.Property.Name); + public string Description => string.Format("{0}.{1}", this.Source?.GetType().Name, this.Property.Name); public static Binding operator !(Binding binding) { From bf9071dcd057a377cd094b2699ba332d25154322 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 11:54:05 +0200 Subject: [PATCH 08/12] Fix ScrollBar.Value. Wasn't being updated inside ListBox and this is why... --- Perspex.Controls/Primitives/ScrollBar.cs | 13 +++++++++++++ Perspex.Themes.Default/ScrollBarStyle.cs | 2 +- Perspex.Themes.Default/ScrollViewerStyle.cs | 16 +++++++++++----- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Perspex.Controls/Primitives/ScrollBar.cs b/Perspex.Controls/Primitives/ScrollBar.cs index 523edba956..4b36ff8a51 100644 --- a/Perspex.Controls/Primitives/ScrollBar.cs +++ b/Perspex.Controls/Primitives/ScrollBar.cs @@ -9,6 +9,7 @@ namespace Perspex.Controls.Primitives using System; using System.Reactive; using System.Reactive.Linq; + using Perspex.Controls.Templates; public class ScrollBar : TemplatedControl { @@ -88,6 +89,18 @@ namespace Perspex.Controls.Primitives return base.MeasureOverride(availableSize); } + protected override void OnTemplateApplied() + { + base.OnTemplateApplied(); + + // Binding between this.Value and track.Value must be done explicitly like this rather + // than using standard bindings as it shouldn't be able to to be overridden by binding + // e.g. ScrollBar.Value. + var track = this.GetTemplateChild("track"); + track.GetObservable(ValueProperty).Subscribe(x => this.Value = x); + this.GetObservable(ValueProperty).Subscribe(x => track.Value = x); + } + private bool CalculateIsVisible() { switch (this.Visibility) diff --git a/Perspex.Themes.Default/ScrollBarStyle.cs b/Perspex.Themes.Default/ScrollBarStyle.cs index 21c5ba10a7..452a7228b3 100644 --- a/Perspex.Themes.Default/ScrollBarStyle.cs +++ b/Perspex.Themes.Default/ScrollBarStyle.cs @@ -66,9 +66,9 @@ namespace Perspex.Themes.Default Background = Brushes.Silver, Content = new Track { + Id = "track", [~Track.MinimumProperty] = control[~ScrollBar.MinimumProperty], [~Track.MaximumProperty] = control[~ScrollBar.MaximumProperty], - [~~Track.ValueProperty] = control[~ScrollBar.ValueProperty], [~Track.ViewportSizeProperty] = control[~ScrollBar.ViewportSizeProperty], [~Track.OrientationProperty] = control[~ScrollBar.OrientationProperty], Thumb = new Thumb diff --git a/Perspex.Themes.Default/ScrollViewerStyle.cs b/Perspex.Themes.Default/ScrollViewerStyle.cs index 7a14e1cd65..00c65fcb69 100644 --- a/Perspex.Themes.Default/ScrollViewerStyle.cs +++ b/Perspex.Themes.Default/ScrollViewerStyle.cs @@ -31,7 +31,9 @@ namespace Perspex.Themes.Default private Control Template(ScrollViewer control) { - return new Grid + ScrollBar vert; + + var result = new Grid { ColumnDefinitions = new ColumnDefinitions { @@ -59,23 +61,27 @@ namespace Perspex.Themes.Default Id = "horizontalScrollBar", Orientation = Orientation.Horizontal, [~ScrollBar.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty], - [~~ScrollBar.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], + //[~~ScrollBar.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty], [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], [Grid.RowProperty] = 1, }, - new ScrollBar + (vert = new ScrollBar { Id = "verticalScrollBar", Orientation = Orientation.Vertical, [~ScrollBar.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty], - [~~ScrollBar.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], + //[~~ScrollBar.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty], [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], [Grid.ColumnProperty] = 1, - }, + }), }, }; + + vert[~~ScrollBar.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty]; + + return result; } } } From 1fe10092d9ba9e0a3f00140ba11a735540a0c9a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 12:37:22 +0200 Subject: [PATCH 09/12] Fix TextBox selection. --- Perspex.Controls/TextBox.cs | 28 +++++++++++++------------- Perspex.Themes.Default/TextBoxStyle.cs | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Perspex.Controls/TextBox.cs b/Perspex.Controls/TextBox.cs index 43258abb2b..ae1d313ba1 100644 --- a/Perspex.Controls/TextBox.cs +++ b/Perspex.Controls/TextBox.cs @@ -112,20 +112,8 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { this.presenter = this.GetTemplateChild("textPresenter"); - } - - protected override void OnGotFocus(GotFocusEventArgs e) - { - base.OnGotFocus(e); - this.presenter.ShowCaret(); - } - - protected override void OnLostFocus(RoutedEventArgs e) - { - base.OnLostFocus(e); - this.SelectionStart = 0; - this.SelectionEnd = 0; - this.presenter.HideCaret(); + this.presenter.GotFocus += PresenterGotFocus; + this.presenter.LostFocus += PresenterLostFocus; } protected override void OnKeyDown(KeyEventArgs e) @@ -451,5 +439,17 @@ namespace Perspex.Controls return i; } + + private void PresenterGotFocus(object sender, RoutedEventArgs e) + { + this.presenter.ShowCaret(); + } + + private void PresenterLostFocus(object sender, RoutedEventArgs e) + { + this.SelectionStart = 0; + this.SelectionEnd = 0; + this.presenter.HideCaret(); + } } } diff --git a/Perspex.Themes.Default/TextBoxStyle.cs b/Perspex.Themes.Default/TextBoxStyle.cs index c9ff7f6605..8b341f616d 100644 --- a/Perspex.Themes.Default/TextBoxStyle.cs +++ b/Perspex.Themes.Default/TextBoxStyle.cs @@ -60,7 +60,7 @@ namespace Perspex.Themes.Default [~TextPresenter.CaretIndexProperty] = control[~TextBox.CaretIndexProperty], [~TextPresenter.SelectionStartProperty] = control[~TextBox.SelectionStartProperty], [~TextPresenter.SelectionEndProperty] = control[~TextBox.SelectionEndProperty], - [~~TextPresenter.TextProperty] = control[~~TextBox.TextProperty], + [~TextPresenter.TextProperty] = control[~TextBox.TextProperty], [~TextPresenter.TextWrappingProperty] = control[~TextBox.TextWrappingProperty], } } From e56252e64f233a9781b53777ca89872fd6154cc5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 12:38:38 +0200 Subject: [PATCH 10/12] Stylecop! --- Perspex.Controls/TextBox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Perspex.Controls/TextBox.cs b/Perspex.Controls/TextBox.cs index ae1d313ba1..c008660292 100644 --- a/Perspex.Controls/TextBox.cs +++ b/Perspex.Controls/TextBox.cs @@ -112,8 +112,8 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { this.presenter = this.GetTemplateChild("textPresenter"); - this.presenter.GotFocus += PresenterGotFocus; - this.presenter.LostFocus += PresenterLostFocus; + this.presenter.GotFocus += this.PresenterGotFocus; + this.presenter.LostFocus += this.PresenterLostFocus; } protected override void OnKeyDown(KeyEventArgs e) From 50415da22aeaccdb9400603e4f644ddbf622df6d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 12:53:09 +0200 Subject: [PATCH 11/12] Undo last 2 commits - they were wrong. --- Perspex.Controls/TextBox.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Perspex.Controls/TextBox.cs b/Perspex.Controls/TextBox.cs index c008660292..43258abb2b 100644 --- a/Perspex.Controls/TextBox.cs +++ b/Perspex.Controls/TextBox.cs @@ -112,8 +112,20 @@ namespace Perspex.Controls protected override void OnTemplateApplied() { this.presenter = this.GetTemplateChild("textPresenter"); - this.presenter.GotFocus += this.PresenterGotFocus; - this.presenter.LostFocus += this.PresenterLostFocus; + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + this.presenter.ShowCaret(); + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + this.SelectionStart = 0; + this.SelectionEnd = 0; + this.presenter.HideCaret(); } protected override void OnKeyDown(KeyEventArgs e) @@ -439,17 +451,5 @@ namespace Perspex.Controls return i; } - - private void PresenterGotFocus(object sender, RoutedEventArgs e) - { - this.presenter.ShowCaret(); - } - - private void PresenterLostFocus(object sender, RoutedEventArgs e) - { - this.SelectionStart = 0; - this.SelectionEnd = 0; - this.presenter.HideCaret(); - } } } From d6b771bcea8ad793439b3477191a03766ac62f13 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 4 May 2015 13:00:26 +0200 Subject: [PATCH 12/12] Don't re-focus already focused control. This actually fixes the TextBox selection problem. --- Perspex.Input/KeyboardDevice.cs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Perspex.Input/KeyboardDevice.cs b/Perspex.Input/KeyboardDevice.cs index 999db93ee5..e31172507d 100644 --- a/Perspex.Input/KeyboardDevice.cs +++ b/Perspex.Input/KeyboardDevice.cs @@ -48,26 +48,29 @@ namespace Perspex.Input public void SetFocusedElement(IInputElement element, bool keyboardNavigated) { - var interactive = this.FocusedElement as IInteractive; - - if (interactive != null) + if (element != this.FocusedElement) { - interactive.RaiseEvent(new RoutedEventArgs + var interactive = this.FocusedElement as IInteractive; + + if (interactive != null) { - RoutedEvent = InputElement.LostFocusEvent, - }); - } + interactive.RaiseEvent(new RoutedEventArgs + { + RoutedEvent = InputElement.LostFocusEvent, + }); + } - this.FocusedElement = element; - interactive = element as IInteractive; + this.FocusedElement = element; + interactive = element as IInteractive; - if (interactive != null) - { - interactive.RaiseEvent(new GotFocusEventArgs + if (interactive != null) { - RoutedEvent = InputElement.GotFocusEvent, - KeyboardNavigated = keyboardNavigated, - }); + interactive.RaiseEvent(new GotFocusEventArgs + { + RoutedEvent = InputElement.GotFocusEvent, + KeyboardNavigated = keyboardNavigated, + }); + } } }