diff --git a/readme.md b/readme.md
index c57bb7ba4e..6eec66c9ae 100644
--- a/readme.md
+++ b/readme.md
@@ -13,7 +13,7 @@ Desktop platforms:
Mobile platforms:
-
+
## NuGet
diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs
index 861192202b..ad959d7f8e 100644
--- a/src/Perspex.Controls/Control.cs
+++ b/src/Perspex.Controls/Control.cs
@@ -93,6 +93,15 @@ namespace Perspex.Controls
_nameScope = this as INameScope;
}
+ ///
+ /// Occurs when the property changes.
+ ///
+ ///
+ /// This event will be raised when the property has changed and
+ /// all subscribers to that change have been notified.
+ ///
+ public event EventHandler DataContextChanged;
+
///
/// Gets or sets the control's classes.
///
@@ -394,8 +403,9 @@ namespace Perspex.Controls
/// Called when the is changed and all subscribers to that change
/// have been notified.
///
- protected virtual void OnDataContextFinishedChanging()
+ protected virtual void OnDataContextChanged()
{
+ DataContextChanged?.Invoke(this, EventArgs.Empty);
}
///
@@ -419,6 +429,11 @@ namespace Perspex.Controls
if (control != null)
{
control.IsDataContextChanging = notifying;
+
+ if (!notifying)
+ {
+ control.OnDataContextChanged();
+ }
}
}
}
diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs
index 42886a0f41..b60af28fda 100644
--- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs
+++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs
@@ -287,7 +287,7 @@ namespace Perspex.Controls.Primitives
}
///
- protected override void OnDataContextFinishedChanging()
+ protected override void OnDataContextChanged()
{
if (_clearSelectedItemsAfterDataContextChanged == SelectedItems)
{
diff --git a/src/Perspex.Controls/Shapes/Shape.cs b/src/Perspex.Controls/Shapes/Shape.cs
index 58dc24a738..6e7934b0e7 100644
--- a/src/Perspex.Controls/Shapes/Shape.cs
+++ b/src/Perspex.Controls/Shapes/Shape.cs
@@ -90,13 +90,20 @@ namespace Perspex.Controls.Shapes
set { SetValue(StrokeThicknessProperty, value); }
}
+ public PenLineCap StrokeDashCap { get; set; } = PenLineCap.Flat;
+
+ public PenLineCap StrokeStartLineCap { get; set; } = PenLineCap.Flat;
+
+ public PenLineCap StrokeEndLineCap { get; set; } = PenLineCap.Flat;
+
public override void Render(DrawingContext context)
{
var geometry = RenderedGeometry;
if (geometry != null)
{
- var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray));
+ var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray),
+ StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap);
context.DrawGeometry(Fill, pen, geometry);
}
}
diff --git a/src/Perspex.SceneGraph/Media/Pen.cs b/src/Perspex.SceneGraph/Media/Pen.cs
index cc14ae29d6..489860a87a 100644
--- a/src/Perspex.SceneGraph/Media/Pen.cs
+++ b/src/Perspex.SceneGraph/Media/Pen.cs
@@ -31,12 +31,12 @@ namespace Perspex.Media
{
Brush = brush;
Thickness = thickness;
+ DashCap = dashCap;
StartLineCap = startLineCap;
EndLineCap = endLineCap;
LineJoin = lineJoin;
MiterLimit = miterLimit;
DashStyle = dashStyle;
- DashCap = dashCap;
}
///
diff --git a/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs b/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs
index fac270dd7c..65d3ce2dad 100644
--- a/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs
+++ b/src/Windows/Perspex.Direct2D1/PrimitiveExtensions.cs
@@ -71,26 +71,23 @@ namespace Perspex.Direct2D1
/// The Direct2D brush.
public static StrokeStyle ToDirect2DStrokeStyle(this Perspex.Media.Pen pen, SharpDX.Direct2D1.RenderTarget target)
{
- if (pen.DashStyle != null)
+ var properties = new StrokeStyleProperties
{
- if (pen.DashStyle.Dashes != null && pen.DashStyle.Dashes.Count > 0)
- {
- var properties = new StrokeStyleProperties
- {
- DashStyle = DashStyle.Custom,
- DashOffset = (float)pen.DashStyle.Offset,
- MiterLimit = (float)pen.MiterLimit,
- LineJoin = pen.LineJoin.ToDirect2D(),
- StartCap = pen.StartLineCap.ToDirect2D(),
- EndCap = pen.EndLineCap.ToDirect2D(),
- DashCap = pen.DashCap.ToDirect2D()
- };
-
- return new StrokeStyle(target.Factory, properties, pen.DashStyle?.Dashes.Select(x => (float)x).ToArray());
- }
+ DashStyle = DashStyle.Solid,
+ MiterLimit = (float)pen.MiterLimit,
+ LineJoin = pen.LineJoin.ToDirect2D(),
+ StartCap = pen.StartLineCap.ToDirect2D(),
+ EndCap = pen.EndLineCap.ToDirect2D(),
+ DashCap = pen.DashCap.ToDirect2D()
+ };
+ var dashes = new float[0];
+ if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
+ {
+ properties.DashStyle = DashStyle.Custom;
+ properties.DashOffset = (float)pen.DashStyle.Offset;
+ dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray();
}
-
- return null;
+ return new StrokeStyle(target.Factory, properties, dashes);
}
///
diff --git a/src/iOS/Perspex.iOS/Perspex.iOS.csproj b/src/iOS/Perspex.iOS/Perspex.iOS.csproj
index 1a5866908b..0d670ac5ed 100644
--- a/src/iOS/Perspex.iOS/Perspex.iOS.csproj
+++ b/src/iOS/Perspex.iOS/Perspex.iOS.csproj
@@ -42,6 +42,7 @@
+
diff --git a/src/iOS/Perspex.iOS/PerspexView.cs b/src/iOS/Perspex.iOS/PerspexView.cs
index 1bea986c98..4adfb98790 100644
--- a/src/iOS/Perspex.iOS/PerspexView.cs
+++ b/src/iOS/Perspex.iOS/PerspexView.cs
@@ -14,26 +14,41 @@ using Perspex.Media;
using Perspex.Platform;
using Perspex.Skia.iOS;
using UIKit;
+using Perspex.iOS.Specific;
+using ObjCRuntime;
namespace Perspex.iOS
{
+ [Adopts("UIKeyInput")]
class PerspexView : SkiaView, IWindowImpl
{
private readonly UIWindow _window;
private readonly UIViewController _controller;
private IInputRoot _inputRoot;
+ private readonly KeyboardEventsHelper _keyboardHelper;
public PerspexView(UIWindow window, UIViewController controller) : base(onFrame => PlatformThreadingInterface.Instance.Render = onFrame)
{
if (controller == null) throw new ArgumentNullException(nameof(controller));
_window = window;
_controller = controller;
+ _keyboardHelper = new KeyboardEventsHelper(this);
AutoresizingMask = UIViewAutoresizing.All;
AutoFit();
UIApplication.Notifications.ObserveDidChangeStatusBarOrientation(delegate { AutoFit(); });
UIApplication.Notifications.ObserveDidChangeStatusBarFrame(delegate { AutoFit(); });
}
+ [Export("hasText")]
+ bool HasText => _keyboardHelper.HasText();
+
+ [Export("insertText:")]
+ void InsertText(string text) => _keyboardHelper.InsertText(text);
+
+ [Export("deleteBackward")]
+ void DeleteBackward() => _keyboardHelper.DeleteBackward();
+
+ public override bool CanBecomeFirstResponder => _keyboardHelper.CanBecomeFirstResponder();
void AutoFit()
{
@@ -89,6 +104,7 @@ namespace Perspex.iOS
public void Show()
{
+ _keyboardHelper.ActivateAutoShowKeybord();
}
public Size MaxClientSize => Bounds.Size.ToPerspex();
@@ -152,8 +168,14 @@ namespace Perspex.iOS
RawMouseEventType.Move, location, InputModifiers.LeftMouseButton));
else
{
- Input?.Invoke(new RawMouseWheelEventArgs(PerspexAppDelegate.MouseDevice, (uint) touch.Timestamp,
- _inputRoot, location, location - _touchLastPoint, InputModifiers.LeftMouseButton));
+ double x = location.X - _touchLastPoint.X;
+ double y = location.Y - _touchLastPoint.Y;
+ double correction = 0.02;
+ var scale = PerspexLocator.Current.GetService().RenderScalingFactor;
+ scale = 1;
+
+ Input?.Invoke(new RawMouseWheelEventArgs(PerspexAppDelegate.MouseDevice, (uint)touch.Timestamp,
+ _inputRoot, location, new Vector(x * correction / scale, y * correction / scale), InputModifiers.LeftMouseButton));
}
_touchLastPoint = location;
}
diff --git a/src/iOS/Perspex.iOS/Specific/KeyboardEventsHelper.cs b/src/iOS/Perspex.iOS/Specific/KeyboardEventsHelper.cs
new file mode 100644
index 0000000000..872f9cd37d
--- /dev/null
+++ b/src/iOS/Perspex.iOS/Specific/KeyboardEventsHelper.cs
@@ -0,0 +1,147 @@
+using ObjCRuntime;
+using Perspex.Controls;
+using Perspex.Input;
+using Perspex.Input.Raw;
+using Perspex.Platform;
+using System;
+using System.ComponentModel;
+using System.Linq;
+using UIKit;
+
+namespace Perspex.iOS.Specific
+{
+ ///
+ /// In order to have properly handle of keyboard event in iOS View should already made some things in the View:
+ /// 1. Adopt the UIKeyInput protocol - add [Adopts("UIKeyInput")] to your view class
+ /// 2. Implement all the methods required by UIKeyInput:
+ /// 2.1 Implement HasText
+ /// example:
+ /// [Export("hasText")]
+ /// bool HasText => _keyboardHelper.HasText()
+ /// 2.2 Implement InsertText
+ /// example:
+ /// [Export("insertText:")]
+ /// void InsertText(string text) => _keyboardHelper.InsertText(text);
+ /// 2.3 Implement InsertText
+ /// example:
+ /// [Export("deleteBackward")]
+ /// void DeleteBackward() => _keyboardHelper.DeleteBackward();
+ /// 3.Let iOS know that this can become a first responder:
+ /// public override bool CanBecomeFirstResponder => _keyboardHelper.CanBecomeFirstResponder();
+ /// or
+ /// public override bool CanBecomeFirstResponder { get { return true; } }
+ ///
+ /// 4. To show keyboard:
+ /// view.BecomeFirstResponder();
+ /// 5. To hide keyboard
+ /// view.ResignFirstResponder();
+ ///
+ /// View that needs keyboard events and show/hide keyboard
+ internal class KeyboardEventsHelper where TView : UIView, IWindowImpl
+ {
+ private TView _view;
+ private IInputElement _lastFocusedElement;
+
+ public KeyboardEventsHelper(TView view)
+ {
+ _view = view;
+
+ var uiKeyInputAttribute = view.GetType().GetCustomAttributes(typeof(AdoptsAttribute), true).OfType().Where(a => a.ProtocolType == "UIKeyInput").FirstOrDefault();
+
+ if (uiKeyInputAttribute == null) throw new NotSupportedException($"View class {typeof(TView).Name} should have class attribute - [Adopts(\"UIKeyInput\")] in order to access keyboard events!");
+
+ HandleEvents = true;
+ }
+
+ ///
+ /// HandleEvents in order to suspend keyboard notifications or resume it
+ ///
+ public bool HandleEvents { get; set; }
+
+ public bool HasText() => false;
+
+ public bool CanBecomeFirstResponder() => true;
+
+ public void DeleteBackward()
+ {
+ HandleKey(Key.Back, RawKeyEventType.KeyDown);
+ HandleKey(Key.Back, RawKeyEventType.KeyUp);
+ }
+
+ public void InsertText(string text)
+ {
+ var rawTextEvent = new RawTextInputEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, text);
+ _view.Input(rawTextEvent);
+ }
+
+ private void HandleKey(Key key, RawKeyEventType type)
+ {
+ var rawKeyEvent = new RawKeyEventArgs(KeyboardDevice.Instance, (uint)DateTime.Now.Ticks, type, key, InputModifiers.None);
+ _view.Input(rawKeyEvent);
+ }
+
+ //currently not found a way to get InputModifiers state
+ //private static InputModifiers GetModifierKeys(object e)
+ //{
+ // var im = InputModifiers.None;
+ // //if (IsCtrlPressed) rv |= InputModifiers.Control;
+ // //if (IsShiftPressed) rv |= InputModifiers.Shift;
+
+ // return im;
+ //}
+
+ private bool NeedsKeyboard(IInputElement element)
+ {
+ //may be some other elements
+ return element is TextBox;
+ }
+
+ private void TryShowHideKeyboard(IInputElement element, bool value)
+ {
+ if (value)
+ {
+ _view.BecomeFirstResponder();
+ }
+ else
+ {
+ _view.ResignFirstResponder();
+ }
+ }
+
+ public void UpdateKeyboardState(IInputElement element)
+ {
+ var focusedElement = element;
+ bool oldValue = NeedsKeyboard(_lastFocusedElement);
+ bool newValue = NeedsKeyboard(focusedElement);
+
+ if (newValue != oldValue || newValue)
+ {
+ TryShowHideKeyboard(focusedElement, newValue);
+ }
+
+ _lastFocusedElement = element;
+ }
+
+ public void ActivateAutoShowKeybord()
+ {
+ var kbDevice = (KeyboardDevice.Instance as INotifyPropertyChanged);
+
+ //just in case we've called more than once the method
+ kbDevice.PropertyChanged -= KeyboardDevice_PropertyChanged;
+ kbDevice.PropertyChanged += KeyboardDevice_PropertyChanged;
+ }
+
+ private void KeyboardDevice_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(KeyboardDevice.FocusedElement))
+ {
+ UpdateKeyboardState(KeyboardDevice.Instance.FocusedElement);
+ }
+ }
+
+ public void Dispose()
+ {
+ HandleEvents = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
index d651216ca3..3d19e353ad 100644
--- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
+++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs
@@ -427,6 +427,34 @@ namespace Perspex.Controls.UnitTests.Primitives
// Clear DataContext and ensure that SelectedItems is still set in the VM.
target.DataContext = null;
Assert.Equal(new[] { "bar" }, vm.SelectedItems);
+
+ // Ensure target's SelectedItems is now clear.
+ Assert.Empty(target.SelectedItems);
+ }
+
+ [Fact]
+ public void Unbound_SelectedItems_Should_Be_Cleared_When_DataContext_Cleared()
+ {
+ var data = new
+ {
+ Items = new[] { "foo", "bar", "baz" },
+ };
+
+ var target = new TestSelector
+ {
+ DataContext = data,
+ Template = Template(),
+ };
+
+ var itemsBinding = new Binding { Path = "Items" };
+ itemsBinding.Bind(target, TestSelector.ItemsProperty);
+
+ Assert.Same(data.Items, target.Items);
+
+ target.SelectedItems.Add("bar");
+ target.DataContext = null;
+
+ Assert.Empty(target.SelectedItems);
}
private FuncControlTemplate Template()
diff --git a/tests/Perspex.RenderTests/Shapes/PathTests.cs b/tests/Perspex.RenderTests/Shapes/PathTests.cs
index 54cdf7e1cf..68b5956234 100644
Binary files a/tests/Perspex.RenderTests/Shapes/PathTests.cs and b/tests/Perspex.RenderTests/Shapes/PathTests.cs differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png
new file mode 100644
index 0000000000..d33068d62c
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Path_With_PenLineCap.expected.png differ