From 08ca269a093e4aff3cb37195c0d998bdb8ed8901 Mon Sep 17 00:00:00 2001 From: Nelson Carrillo Date: Sat, 25 Jul 2015 16:40:35 -0400 Subject: [PATCH 1/2] Correctly subtracted the minimum from the value and the divisor. --- Perspex.Controls/ProgressBar.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Perspex.Controls/ProgressBar.cs b/Perspex.Controls/ProgressBar.cs index 2afbd3bc87..f14751a0e7 100644 --- a/Perspex.Controls/ProgressBar.cs +++ b/Perspex.Controls/ProgressBar.cs @@ -16,13 +16,15 @@ namespace Perspex.Controls public class ProgressBar : RangeBase { /// + /// + private Border indicator; protected override Size ArrangeOverride(Size finalSize) { var size = base.ArrangeOverride(finalSize); - var b = this.Bounds; - - var indicator = this.GetTemplateChild("PART_Indicator"); - indicator.Width = Math.Max(this.Minimum, finalSize.Width * (this.Value / this.Maximum)); + this.indicator = this.indicator ?? this.GetTemplateChild("PART_Indicator"); + + double percent = this.Maximum == this.Minimum ? 1.0 : (this.Value - this.Minimum) / (this.Maximum - this.Minimum); + indicator.Width = finalSize.Width * percent; return size; } From 60b6f23aef452f8fe31051adf5f906e411989f2f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 25 Jul 2015 22:51:23 +0200 Subject: [PATCH 2/2] Refactored ISelectable behavior into a mixin. --- Perspex.Controls/ListBoxItem.cs | 25 +----- Perspex.Controls/MenuItem.cs | 8 +- Perspex.Controls/Mixins/SelectableMixin.cs | 70 +++++++++++++++++ Perspex.Controls/Perspex.Controls.csproj | 1 + Perspex.Controls/TabItem.cs | 16 +++- Perspex.Controls/TreeViewItem.cs | 37 ++++++--- Perspex.Themes.Default/ListBoxItemStyle.cs | 2 +- Perspex.Themes.Default/MenuItemStyle.cs | 2 +- Perspex.Themes.Default/TabItemStyle.cs | 2 +- Perspex.Themes.Default/TreeViewItemStyle.cs | 2 +- .../GlobalSuppressions.cs | 4 + .../Mixins/SelectableMixinTests.cs | 76 +++++++++++++++++++ .../Perspex.Controls.UnitTests.csproj | 1 + 13 files changed, 207 insertions(+), 39 deletions(-) create mode 100644 Perspex.Controls/Mixins/SelectableMixin.cs create mode 100644 Tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs diff --git a/Perspex.Controls/ListBoxItem.cs b/Perspex.Controls/ListBoxItem.cs index e567dc9de6..8cb00c18f4 100644 --- a/Perspex.Controls/ListBoxItem.cs +++ b/Perspex.Controls/ListBoxItem.cs @@ -1,17 +1,15 @@ // ----------------------------------------------------------------------- // -// Copyright 2014 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Controls { - using System; - using Perspex.Controls.Primitives; - using Perspex.Interactivity; + using Perspex.Controls.Mixins; /// - /// An selectable item in a . + /// A selectable item in a . /// public class ListBoxItem : ContentControl, ISelectable { @@ -26,8 +24,7 @@ namespace Perspex.Controls /// static ListBoxItem() { - Control.PseudoClass(IsSelectedProperty, ":selected"); - IsSelectedProperty.Changed.Subscribe(IsSelectedChanged); + SelectableMixin.Attach(IsSelectedProperty); } /// @@ -38,19 +35,5 @@ namespace Perspex.Controls get { return this.GetValue(IsSelectedProperty); } set { this.SetValue(IsSelectedProperty, value); } } - - /// - /// Called when the property changes on an object. - /// - /// The sender. - private static void IsSelectedChanged(PerspexPropertyChangedEventArgs e) - { - var interactive = e.Sender as IInteractive; - - if (interactive != null) - { - interactive.RaiseEvent(new RoutedEventArgs(SelectingItemsControl.IsSelectedChangedEvent)); - } - } } } diff --git a/Perspex.Controls/MenuItem.cs b/Perspex.Controls/MenuItem.cs index 393582c2d4..d1f901aefc 100644 --- a/Perspex.Controls/MenuItem.cs +++ b/Perspex.Controls/MenuItem.cs @@ -10,13 +10,14 @@ namespace Perspex.Controls using System.Collections.Generic; using System.Linq; using System.Windows.Input; + using Perspex.Controls.Mixins; + using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; + using Perspex.Controls.Templates; using Perspex.Input; using Perspex.Interactivity; - using Perspex.Controls.Templates; - using Perspex.Controls.Presenters; - using Perspex.VisualTree; using Perspex.Threading; + using Perspex.VisualTree; using Splat; /// @@ -87,6 +88,7 @@ namespace Perspex.Controls /// static MenuItem() { + SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ClickEvent.AddClassHandler(x => x.OnClick); SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); diff --git a/Perspex.Controls/Mixins/SelectableMixin.cs b/Perspex.Controls/Mixins/SelectableMixin.cs new file mode 100644 index 0000000000..95cf398677 --- /dev/null +++ b/Perspex.Controls/Mixins/SelectableMixin.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.Mixins +{ + using System; + using Perspex.Interactivity; + using Primitives; + + /// + /// Adds selectable functionality to control classes. + /// + /// + /// + /// The adds behavior to a control which can be + /// selected. It adds the following behavior: + /// + /// + /// + /// Raises an when the value if + /// the IsSelected property changes. + /// + /// + /// Adds a 'selected' class to selected controls. + /// + /// + /// + /// Mixins apply themselves to classes and not instances, and as such should be created in + /// a static constructor. + /// + /// + public static class SelectableMixin + { + /// + /// Initializes a new instance of the class. + /// + /// The control type. + /// The IsSelected property. + public static void Attach(PerspexProperty isSelected) + where TControl : class, IControl + { + Contract.Requires(isSelected != null); + + isSelected.Changed.Subscribe(x => + { + var sender = x.Sender as TControl; + + if (sender != null) + { + if ((bool)x.NewValue) + { + sender.Classes.Add("selected"); + } + else + { + sender.Classes.Remove("selected"); + } + + sender.RaiseEvent(new RoutedEventArgs + { + RoutedEvent = SelectingItemsControl.IsSelectedChangedEvent + }); + } + }); + } + } +} \ No newline at end of file diff --git a/Perspex.Controls/Perspex.Controls.csproj b/Perspex.Controls/Perspex.Controls.csproj index 5a4ab94ddc..8ee82358c7 100644 --- a/Perspex.Controls/Perspex.Controls.csproj +++ b/Perspex.Controls/Perspex.Controls.csproj @@ -39,6 +39,7 @@ + diff --git a/Perspex.Controls/TabItem.cs b/Perspex.Controls/TabItem.cs index 059995ae6e..e7a302e2dd 100644 --- a/Perspex.Controls/TabItem.cs +++ b/Perspex.Controls/TabItem.cs @@ -6,20 +6,32 @@ namespace Perspex.Controls { + using Mixins; using Perspex.Controls.Primitives; + /// + /// An item in a or . + /// public class TabItem : HeaderedContentControl, ISelectable { + /// + /// Defines the property. + /// public static readonly PerspexProperty IsSelectedProperty = ListBoxItem.IsSelectedProperty.AddOwner(); + /// + /// Initializes static members of the class. + /// static TabItem() { + SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(typeof(TabItem), true); - Control.AffectsRender(IsSelectedProperty); - Control.PseudoClass(IsSelectedProperty, ":selected"); } + /// + /// Gets or sets the selection state of the item. + /// public bool IsSelected { get { return this.GetValue(IsSelectedProperty); } diff --git a/Perspex.Controls/TreeViewItem.cs b/Perspex.Controls/TreeViewItem.cs index 51d0673f02..fe98cd4e79 100644 --- a/Perspex.Controls/TreeViewItem.cs +++ b/Perspex.Controls/TreeViewItem.cs @@ -1,6 +1,6 @@ // ----------------------------------------------------------------------- // -// Copyright 2014 MIT Licence. See licence.md for more information. +// Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- @@ -8,56 +8,75 @@ namespace Perspex.Controls { using System; using System.Linq; + using Mixins; using Perspex.Controls.Generators; using Perspex.Controls.Primitives; + using Perspex.Rendering; using Perspex.VisualTree; + /// + /// An item in a . + /// public class TreeViewItem : HeaderedItemsControl, ISelectable { + /// + /// Defines the property. + /// public static readonly PerspexProperty IsExpandedProperty = PerspexProperty.Register("IsExpanded"); + /// + /// Defines the property. + /// public static readonly PerspexProperty IsSelectedProperty = ListBoxItem.IsSelectedProperty.AddOwner(); private TreeView treeView; + /// + /// Initializes static members of the class. + /// static TreeViewItem() { - Control.PseudoClass(IsSelectedProperty, ":selected"); - } - - public TreeViewItem() - { - Control.AffectsRender(IsSelectedProperty); + SelectableMixin.Attach(IsSelectedProperty); } + /// + /// Gets or sets a value indicating whether the item is expanded to show its children. + /// public bool IsExpanded { get { return this.GetValue(IsExpandedProperty); } set { this.SetValue(IsExpandedProperty, value); } } + /// + /// Gets or sets the selection state of the item. + /// public bool IsSelected { get { return this.GetValue(IsSelectedProperty); } set { this.SetValue(IsSelectedProperty, value); } } + /// protected override ItemContainerGenerator CreateItemContainerGenerator() { if (this.treeView == null) { throw new InvalidOperationException( - "Cannot get the ItemContainerGenerator for a TreeViewItem " + + "Cannot get the ItemContainerGenerator for a TreeViewItem " + "before it is added to a TreeView."); } return this.treeView.ItemContainerGenerator; } - protected override void OnVisualParentChanged(Visual oldParent) + /// + protected override void OnAttachedToVisualTree(IRenderRoot root) { + base.OnAttachedToVisualTree(root); + if (this.GetVisualParent() != null) { this.treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); diff --git a/Perspex.Themes.Default/ListBoxItemStyle.cs b/Perspex.Themes.Default/ListBoxItemStyle.cs index 0553dc6886..73bc4883c0 100644 --- a/Perspex.Themes.Default/ListBoxItemStyle.cs +++ b/Perspex.Themes.Default/ListBoxItemStyle.cs @@ -25,7 +25,7 @@ namespace Perspex.Themes.Default new Setter(ListBoxItem.TemplateProperty, ControlTemplate.Create(this.Template)), }, }, - new Style(x => x.OfType().Class(":selected").Template().Name("border")) + new Style(x => x.OfType().Class("selected").Template().Name("border")) { Setters = new[] { diff --git a/Perspex.Themes.Default/MenuItemStyle.cs b/Perspex.Themes.Default/MenuItemStyle.cs index 45b975b7a3..65b6a7d53c 100644 --- a/Perspex.Themes.Default/MenuItemStyle.cs +++ b/Perspex.Themes.Default/MenuItemStyle.cs @@ -41,7 +41,7 @@ namespace Perspex.Themes.Default new Setter(MenuItem.TemplateProperty, ControlTemplate.Create(this.TopLevelTemplate)), }, }, - new Style(x => x.OfType().Class(":selected").Template().Name("root")) + new Style(x => x.OfType().Class("selected").Template().Name("root")) { Setters = new[] { diff --git a/Perspex.Themes.Default/TabItemStyle.cs b/Perspex.Themes.Default/TabItemStyle.cs index 0cb134136d..6a61ad9aa3 100644 --- a/Perspex.Themes.Default/TabItemStyle.cs +++ b/Perspex.Themes.Default/TabItemStyle.cs @@ -27,7 +27,7 @@ namespace Perspex.Themes.Default new Setter(TabItem.TemplateProperty, ControlTemplate.Create(this.Template)), }, }, - new Style(x => x.OfType().Class(":selected")) + new Style(x => x.OfType().Class("selected")) { Setters = new[] { diff --git a/Perspex.Themes.Default/TreeViewItemStyle.cs b/Perspex.Themes.Default/TreeViewItemStyle.cs index 77cf318ef7..0b24aa1fe3 100644 --- a/Perspex.Themes.Default/TreeViewItemStyle.cs +++ b/Perspex.Themes.Default/TreeViewItemStyle.cs @@ -35,7 +35,7 @@ namespace Perspex.Themes.Default new Setter(Border.PaddingProperty, new Thickness(2)), }, }, - new Style(x => x.OfType().Class(":selected").Template().Name("header")) + new Style(x => x.OfType().Class("selected").Template().Name("header")) { Setters = new[] { diff --git a/Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs b/Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs index a6e0896543..72d1f6b6a3 100644 --- a/Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs +++ b/Tests/Perspex.Controls.UnitTests/GlobalSuppressions.cs @@ -3,6 +3,10 @@ // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields must be private", + Justification = "PerspexProperty fields should not be private.")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1600:Elements must be documented", diff --git a/Tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs b/Tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs new file mode 100644 index 0000000000..dd0bc22228 --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/Mixins/SelectableMixinTests.cs @@ -0,0 +1,76 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests.Mixins +{ + using Perspex.Controls.Mixins; + using Perspex.Controls.Primitives; + using Xunit; + + public class SelectableMixinTests + { + [Fact] + public void Selected_Class_Should_Not_Initially_Be_Added() + { + var target = new TestControl(); + + Assert.Empty(target.Classes); + } + + [Fact] + public void Setting_IsSelected_Should_Add_Selected_Class() + { + var target = new TestControl(); + + target.IsSelected = true; + + Assert.Equal(new[] { "selected" }, target.Classes); + } + + [Fact] + public void Clearing_IsSelected_Should_Remove_Selected_Class() + { + var target = new TestControl(); + + target.IsSelected = true; + target.IsSelected = false; + + Assert.Empty(target.Classes); + } + + [Fact] + public void Setting_IsSelected_Should_Raise_IsSelectedChangedEvent() + { + var target = new TestControl(); + var raised = false; + + target.AddHandler( + SelectingItemsControl.IsSelectedChangedEvent, + (s, e) => raised = true); + + target.IsSelected = true; + + Assert.True(raised); + } + + private class TestControl : Control, ISelectable + { + public static readonly PerspexProperty IsSelectedProperty = + PerspexProperty.Register(nameof(IsSelected)); + + static TestControl() + { + SelectableMixin.Attach(IsSelectedProperty); + } + + public bool IsSelected + { + get { return this.GetValue(IsSelectedProperty); } + set { this.SetValue(IsSelectedProperty, value); } + } + } + } +} diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 34b970ffe1..c9e1613e1e 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -91,6 +91,7 @@ +