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: -![](https://i.ytimg.com/vi/NJ9-hnmUbBM/hqdefault.jpg) +![](https://i.ytimg.com/vi/NJ9-hnmUbBM/hqdefault.jpg) ## 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