diff --git a/src/controls/FeatureAreas.props b/src/controls/FeatureAreas.props index 06d66fa8e8..9c0d5fe0af 100644 --- a/src/controls/FeatureAreas.props +++ b/src/controls/FeatureAreas.props @@ -286,7 +286,7 @@ true true true - false + true true diff --git a/src/controls/dev/Generated/InkCanvas.properties.cpp b/src/controls/dev/Generated/InkCanvas.properties.cpp index 76174c5904..e3d6e7b704 100644 --- a/src/controls/dev/Generated/InkCanvas.properties.cpp +++ b/src/controls/dev/Generated/InkCanvas.properties.cpp @@ -13,15 +13,42 @@ namespace winrt::Microsoft::UI::Xaml::Controls #include "InkCanvas.g.cpp" +GlobalDependencyProperty InkCanvasProperties::s_AllowedInputTypesProperty{ nullptr }; +GlobalDependencyProperty InkCanvasProperties::s_DefaultDrawingAttributesProperty{ nullptr }; GlobalDependencyProperty InkCanvasProperties::s_IsEnabledProperty{ nullptr }; +GlobalDependencyProperty InkCanvasProperties::s_ModeProperty{ nullptr }; InkCanvasProperties::InkCanvasProperties() + : m_strokeCollectedEventSource{static_cast(this)} + , m_strokesErasedEventSource{static_cast(this)} { EnsureProperties(); } void InkCanvasProperties::EnsureProperties() { + if (!s_AllowedInputTypesProperty) + { + s_AllowedInputTypesProperty = + InitializeDependencyProperty( + L"AllowedInputTypes", + winrt::name_of(), + winrt::name_of(), + false /* isAttached */, + ValueHelper::BoxValueIfNecessary(winrt::InkInputType::Pen | winrt::InkInputType::Mouse), + winrt::PropertyChangedCallback(&OnAllowedInputTypesPropertyChanged)); + } + if (!s_DefaultDrawingAttributesProperty) + { + s_DefaultDrawingAttributesProperty = + InitializeDependencyProperty( + L"DefaultDrawingAttributes", + winrt::name_of(), + winrt::name_of(), + false /* isAttached */, + ValueHelper::BoxedDefaultValue(), + winrt::PropertyChangedCallback(&OnDefaultDrawingAttributesPropertyChanged)); + } if (!s_IsEnabledProperty) { s_IsEnabledProperty = @@ -33,11 +60,41 @@ void InkCanvasProperties::EnsureProperties() ValueHelper::BoxValueIfNecessary(true), winrt::PropertyChangedCallback(&OnIsEnabledPropertyChanged)); } + if (!s_ModeProperty) + { + s_ModeProperty = + InitializeDependencyProperty( + L"Mode", + winrt::name_of(), + winrt::name_of(), + false /* isAttached */, + ValueHelper::BoxValueIfNecessary(winrt::InkCanvasMode::Draw), + winrt::PropertyChangedCallback(&OnModePropertyChanged)); + } } void InkCanvasProperties::ClearProperties() { + s_AllowedInputTypesProperty = nullptr; + s_DefaultDrawingAttributesProperty = nullptr; s_IsEnabledProperty = nullptr; + s_ModeProperty = nullptr; +} + +void InkCanvasProperties::OnAllowedInputTypesPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto owner = sender.as(); + winrt::get_self(owner)->OnAllowedInputTypesPropertyChanged(args); +} + +void InkCanvasProperties::OnDefaultDrawingAttributesPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto owner = sender.as(); + winrt::get_self(owner)->OnDefaultDrawingAttributesPropertyChanged(args); } void InkCanvasProperties::OnIsEnabledPropertyChanged( @@ -48,6 +105,40 @@ void InkCanvasProperties::OnIsEnabledPropertyChanged( winrt::get_self(owner)->OnIsEnabledPropertyChanged(args); } +void InkCanvasProperties::OnModePropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto owner = sender.as(); + winrt::get_self(owner)->OnModePropertyChanged(args); +} + +void InkCanvasProperties::AllowedInputTypes(winrt::InkInputType const& value) +{ + [[gsl::suppress(con)]] + { + static_cast(this)->SetValue(s_AllowedInputTypesProperty, ValueHelper::BoxValueIfNecessary(value)); + } +} + +winrt::InkInputType InkCanvasProperties::AllowedInputTypes() +{ + return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_AllowedInputTypesProperty)); +} + +void InkCanvasProperties::DefaultDrawingAttributes(winrt::InkDrawingAttributes const& value) +{ + [[gsl::suppress(con)]] + { + static_cast(this)->SetValue(s_DefaultDrawingAttributesProperty, ValueHelper::BoxValueIfNecessary(value)); + } +} + +winrt::InkDrawingAttributes InkCanvasProperties::DefaultDrawingAttributes() +{ + return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_DefaultDrawingAttributesProperty)); +} + void InkCanvasProperties::IsEnabled(bool value) { [[gsl::suppress(con)]] @@ -60,3 +151,36 @@ bool InkCanvasProperties::IsEnabled() { return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_IsEnabledProperty)); } + +void InkCanvasProperties::Mode(winrt::InkCanvasMode const& value) +{ + [[gsl::suppress(con)]] + { + static_cast(this)->SetValue(s_ModeProperty, ValueHelper::BoxValueIfNecessary(value)); + } +} + +winrt::InkCanvasMode InkCanvasProperties::Mode() +{ + return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_ModeProperty)); +} + +winrt::event_token InkCanvasProperties::StrokeCollected(winrt::TypedEventHandler const& value) +{ + return m_strokeCollectedEventSource.add(value); +} + +void InkCanvasProperties::StrokeCollected(winrt::event_token const& token) +{ + m_strokeCollectedEventSource.remove(token); +} + +winrt::event_token InkCanvasProperties::StrokesErased(winrt::TypedEventHandler const& value) +{ + return m_strokesErasedEventSource.add(value); +} + +void InkCanvasProperties::StrokesErased(winrt::event_token const& token) +{ + m_strokesErasedEventSource.remove(token); +} diff --git a/src/controls/dev/Generated/InkCanvas.properties.h b/src/controls/dev/Generated/InkCanvas.properties.h index 4810987c05..fcbebf6bad 100644 --- a/src/controls/dev/Generated/InkCanvas.properties.h +++ b/src/controls/dev/Generated/InkCanvas.properties.h @@ -9,17 +9,52 @@ class InkCanvasProperties public: InkCanvasProperties(); + void AllowedInputTypes(winrt::InkInputType const& value); + winrt::InkInputType AllowedInputTypes(); + + void DefaultDrawingAttributes(winrt::InkDrawingAttributes const& value); + winrt::InkDrawingAttributes DefaultDrawingAttributes(); + void IsEnabled(bool value); bool IsEnabled(); + void Mode(winrt::InkCanvasMode const& value); + winrt::InkCanvasMode Mode(); + + static winrt::DependencyProperty AllowedInputTypesProperty() { return s_AllowedInputTypesProperty; } + static winrt::DependencyProperty DefaultDrawingAttributesProperty() { return s_DefaultDrawingAttributesProperty; } static winrt::DependencyProperty IsEnabledProperty() { return s_IsEnabledProperty; } + static winrt::DependencyProperty ModeProperty() { return s_ModeProperty; } + static GlobalDependencyProperty s_AllowedInputTypesProperty; + static GlobalDependencyProperty s_DefaultDrawingAttributesProperty; static GlobalDependencyProperty s_IsEnabledProperty; + static GlobalDependencyProperty s_ModeProperty; + + winrt::event_token StrokeCollected(winrt::TypedEventHandler const& value); + void StrokeCollected(winrt::event_token const& token); + winrt::event_token StrokesErased(winrt::TypedEventHandler const& value); + void StrokesErased(winrt::event_token const& token); + + event_source> m_strokeCollectedEventSource; + event_source> m_strokesErasedEventSource; static void EnsureProperties(); static void ClearProperties(); + static void OnAllowedInputTypesPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args); + + static void OnDefaultDrawingAttributesPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args); + static void OnIsEnabledPropertyChanged( winrt::DependencyObject const& sender, winrt::DependencyPropertyChangedEventArgs const& args); + + static void OnModePropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args); }; diff --git a/src/controls/dev/Generated/InkToolBarMenuButton.properties.cpp b/src/controls/dev/Generated/InkToolBarMenuButton.properties.cpp index 717df18d31..67256ae083 100644 --- a/src/controls/dev/Generated/InkToolBarMenuButton.properties.cpp +++ b/src/controls/dev/Generated/InkToolBarMenuButton.properties.cpp @@ -14,7 +14,6 @@ namespace winrt::Microsoft::UI::Xaml::Controls #include "InkToolBarMenuButton.g.cpp" GlobalDependencyProperty InkToolBarMenuButtonProperties::s_IsExtensionGlyphShownProperty{ nullptr }; -GlobalDependencyProperty InkToolBarMenuButtonProperties::s_MenuKindProperty{ nullptr }; InkToolBarMenuButtonProperties::InkToolBarMenuButtonProperties() { @@ -34,23 +33,11 @@ void InkToolBarMenuButtonProperties::EnsureProperties() ValueHelper::BoxedDefaultValue(), nullptr); } - if (!s_MenuKindProperty) - { - s_MenuKindProperty = - InitializeDependencyProperty( - L"MenuKind", - winrt::name_of(), - winrt::name_of(), - false /* isAttached */, - ValueHelper::BoxedDefaultValue(), - nullptr); - } } void InkToolBarMenuButtonProperties::ClearProperties() { s_IsExtensionGlyphShownProperty = nullptr; - s_MenuKindProperty = nullptr; } void InkToolBarMenuButtonProperties::IsExtensionGlyphShown(bool value) @@ -65,16 +52,3 @@ bool InkToolBarMenuButtonProperties::IsExtensionGlyphShown() { return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_IsExtensionGlyphShownProperty)); } - -void InkToolBarMenuButtonProperties::MenuKind(winrt::InkToolBarMenuKind const& value) -{ - [[gsl::suppress(con)]] - { - static_cast(this)->SetValue(s_MenuKindProperty, ValueHelper::BoxValueIfNecessary(value)); - } -} - -winrt::InkToolBarMenuKind InkToolBarMenuButtonProperties::MenuKind() -{ - return ValueHelper::CastOrUnbox(static_cast(this)->GetValue(s_MenuKindProperty)); -} diff --git a/src/controls/dev/Generated/InkToolBarMenuButton.properties.h b/src/controls/dev/Generated/InkToolBarMenuButton.properties.h index d5eaa85aea..dbea46501a 100644 --- a/src/controls/dev/Generated/InkToolBarMenuButton.properties.h +++ b/src/controls/dev/Generated/InkToolBarMenuButton.properties.h @@ -12,14 +12,9 @@ class InkToolBarMenuButtonProperties void IsExtensionGlyphShown(bool value); bool IsExtensionGlyphShown(); - void MenuKind(winrt::InkToolBarMenuKind const& value); - winrt::InkToolBarMenuKind MenuKind(); - static winrt::DependencyProperty IsExtensionGlyphShownProperty() { return s_IsExtensionGlyphShownProperty; } - static winrt::DependencyProperty MenuKindProperty() { return s_MenuKindProperty; } static GlobalDependencyProperty s_IsExtensionGlyphShownProperty; - static GlobalDependencyProperty s_MenuKindProperty; static void EnsureProperties(); static void ClearProperties(); diff --git a/src/controls/dev/InkCanvas/APITests/InkCanvasTests.cs b/src/controls/dev/InkCanvas/APITests/InkCanvasTests.cs new file mode 100644 index 0000000000..fdfeca7c4e --- /dev/null +++ b/src/controls/dev/InkCanvas/APITests/InkCanvasTests.cs @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using Common; +using Microsoft.UI.Xaml.Controls; +using MUXControlsTestApp.Utilities; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Markup; +using Windows.UI; +using Windows.UI.Input.Inking; + +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using WEX.Logging.Interop; + +namespace Microsoft.UI.Xaml.Tests.MUXControls.ApiTests +{ + [TestClass] + public class InkCanvasTests : ApiTestBase + { + [TestMethod] + public void InkCanvasDefaultPropertyValuesTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Verify default property values + Verify.IsTrue(inkCanvas.IsEnabled, "InkCanvas should be enabled by default."); + Verify.AreEqual(InkCanvasMode.Draw, inkCanvas.Mode, "Default mode should be Draw."); + Verify.AreEqual( + InkInputType.Pen | InkInputType.Mouse, + inkCanvas.AllowedInputTypes, + "Default AllowedInputTypes should be Pen | Mouse."); + }); + } + + [TestMethod] + public void InkCanvasModePropertyTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Default + Verify.AreEqual(InkCanvasMode.Draw, inkCanvas.Mode, "Default mode should be Draw."); + + // Set to Erase + inkCanvas.Mode = InkCanvasMode.Erase; + Verify.AreEqual(InkCanvasMode.Erase, inkCanvas.Mode, "Mode should be Erase after setting."); + + // Set to Select + inkCanvas.Mode = InkCanvasMode.Select; + Verify.AreEqual(InkCanvasMode.Select, inkCanvas.Mode, "Mode should be Select after setting."); + + // Set back to Draw + inkCanvas.Mode = InkCanvasMode.Draw; + Verify.AreEqual(InkCanvasMode.Draw, inkCanvas.Mode, "Mode should be Draw after setting back."); + }); + } + + [TestMethod] + public void InkCanvasAllowedInputTypesPropertyTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Default + Verify.AreEqual( + InkInputType.Pen | InkInputType.Mouse, + inkCanvas.AllowedInputTypes, + "Default should be Pen | Mouse."); + + // Set to Pen only + inkCanvas.AllowedInputTypes = InkInputType.Pen; + Verify.AreEqual(InkInputType.Pen, inkCanvas.AllowedInputTypes, "Should be Pen only."); + + // Set to Touch only + inkCanvas.AllowedInputTypes = InkInputType.Touch; + Verify.AreEqual(InkInputType.Touch, inkCanvas.AllowedInputTypes, "Should be Touch only."); + + // Set to all input types + inkCanvas.AllowedInputTypes = InkInputType.Pen | InkInputType.Touch | InkInputType.Mouse; + Verify.AreEqual( + InkInputType.Pen | InkInputType.Touch | InkInputType.Mouse, + inkCanvas.AllowedInputTypes, + "Should be Pen | Touch | Mouse."); + + // Set to None + inkCanvas.AllowedInputTypes = InkInputType.None; + Verify.AreEqual(InkInputType.None, inkCanvas.AllowedInputTypes, "Should be None."); + }); + } + + [TestMethod] + public void InkCanvasIsEnabledPropertyTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Default + Verify.IsTrue(inkCanvas.IsEnabled, "InkCanvas should be enabled by default."); + + // Disable + inkCanvas.IsEnabled = false; + Verify.IsFalse(inkCanvas.IsEnabled, "InkCanvas should be disabled after setting to false."); + + // Re-enable + inkCanvas.IsEnabled = true; + Verify.IsTrue(inkCanvas.IsEnabled, "InkCanvas should be enabled after setting to true."); + }); + } + + [TestMethod] + public void InkCanvasDefaultDrawingAttributesPropertyTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Set DefaultDrawingAttributes + var attrs = new InkDrawingAttributes(); + attrs.Color = Windows.UI.Colors.Red; + attrs.Size = new Windows.Foundation.Size(5, 5); + attrs.PenTip = PenTipShape.Circle; + + inkCanvas.DefaultDrawingAttributes = attrs; + + var retrieved = inkCanvas.DefaultDrawingAttributes; + Verify.IsNotNull(retrieved, "DefaultDrawingAttributes should not be null after setting."); + Verify.AreEqual(Windows.UI.Colors.Red, retrieved.Color, "Color should be Red."); + Verify.AreEqual(5.0, retrieved.Size.Width, "Width should be 5."); + Verify.AreEqual(5.0, retrieved.Size.Height, "Height should be 5."); + Verify.AreEqual(PenTipShape.Circle, retrieved.PenTip, "PenTip should be Circle."); + }); + } + + [TestMethod] + public void InkCanvasDrawingAttributesPenTipTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Rectangle pen tip + var attrs = new InkDrawingAttributes(); + attrs.PenTip = PenTipShape.Rectangle; + inkCanvas.DefaultDrawingAttributes = attrs; + Verify.AreEqual(PenTipShape.Rectangle, inkCanvas.DefaultDrawingAttributes.PenTip, + "PenTip should be Rectangle."); + + // Circle pen tip + attrs.PenTip = PenTipShape.Circle; + inkCanvas.DefaultDrawingAttributes = attrs; + Verify.AreEqual(PenTipShape.Circle, inkCanvas.DefaultDrawingAttributes.PenTip, + "PenTip should be Circle."); + }); + } + + [TestMethod] + public void InkCanvasStrokeContainerAccessTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // StrokeContainer may be null before InkPresenter is initialized (not loaded in tree) + // But the property should be accessible without throwing + var container = inkCanvas.StrokeContainer; + // After loading, StrokeContainer comes from InkPresenter, which is created on load. + // Here we just verify the getter doesn't throw. + Log.Comment("StrokeContainer getter is accessible (value may be null before load)."); + }); + } + + [TestMethod] + public void InkCanvasClearStrokesTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // ClearStrokes should not throw even when there are no strokes / not loaded + inkCanvas.ClearStrokes(); + Log.Comment("ClearStrokes did not throw when called before load."); + }); + } + + [TestMethod] + public void InkCanvasInVisualTreeTest() + { + RunOnUIThread.Execute(() => + { + var root = (Grid)XamlReader.Load( + @" + + "); + + Content = root; + Content.UpdateLayout(); + + var inkCanvas = (InkCanvas)root.FindName("TestInkCanvas"); + Verify.IsNotNull(inkCanvas, "InkCanvas should be found in visual tree."); + Verify.AreEqual(400.0, inkCanvas.Width, "Width should be 400."); + Verify.AreEqual(300.0, inkCanvas.Height, "Height should be 300."); + }); + } + + [TestMethod] + public void InkCanvasModeDependencyPropertyTest() + { + RunOnUIThread.Execute(() => + { + // Verify the dependency property is accessible + var dp = InkCanvas.ModeProperty; + Verify.IsNotNull(dp, "ModeProperty should not be null."); + + var inkCanvas = new InkCanvas(); + inkCanvas.SetValue(dp, InkCanvasMode.Erase); + Verify.AreEqual(InkCanvasMode.Erase, (InkCanvasMode)inkCanvas.GetValue(dp), + "Mode should be Erase via DependencyProperty."); + }); + } + + [TestMethod] + public void InkCanvasAllowedInputTypesDependencyPropertyTest() + { + RunOnUIThread.Execute(() => + { + var dp = InkCanvas.AllowedInputTypesProperty; + Verify.IsNotNull(dp, "AllowedInputTypesProperty should not be null."); + + var inkCanvas = new InkCanvas(); + inkCanvas.SetValue(dp, InkInputType.Touch); + Verify.AreEqual(InkInputType.Touch, (InkInputType)inkCanvas.GetValue(dp), + "AllowedInputTypes should be Touch via DependencyProperty."); + }); + } + + [TestMethod] + public void InkCanvasDefaultDrawingAttributesDependencyPropertyTest() + { + RunOnUIThread.Execute(() => + { + var dp = InkCanvas.DefaultDrawingAttributesProperty; + Verify.IsNotNull(dp, "DefaultDrawingAttributesProperty should not be null."); + + var attrs = new InkDrawingAttributes(); + attrs.Color = Windows.UI.Colors.Blue; + + var inkCanvas = new InkCanvas(); + inkCanvas.SetValue(dp, attrs); + + var retrieved = (InkDrawingAttributes)inkCanvas.GetValue(dp); + Verify.IsNotNull(retrieved, "Retrieved attributes should not be null."); + Verify.AreEqual(Windows.UI.Colors.Blue, retrieved.Color, "Color should be Blue."); + }); + } + + [TestMethod] + public void InkCanvasIsEnabledDependencyPropertyTest() + { + RunOnUIThread.Execute(() => + { + var dp = InkCanvas.IsEnabledProperty; + Verify.IsNotNull(dp, "IsEnabledProperty should not be null."); + + var inkCanvas = new InkCanvas(); + Verify.IsTrue((bool)inkCanvas.GetValue(dp), "Default IsEnabled should be true."); + + inkCanvas.SetValue(dp, false); + Verify.IsFalse((bool)inkCanvas.GetValue(dp), "IsEnabled should be false."); + }); + } + + [TestMethod] + public void InkCanvasModePropertyChangePersistsTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + + // Cycle through all modes and verify each persists + foreach (InkCanvasMode mode in new[] { InkCanvasMode.Draw, InkCanvasMode.Erase, InkCanvasMode.Select }) + { + inkCanvas.Mode = mode; + Verify.AreEqual(mode, inkCanvas.Mode, $"Mode should persist as {mode}."); + } + }); + } + + [TestMethod] + public void InkCanvasMultipleInstancesTest() + { + RunOnUIThread.Execute(() => + { + var canvas1 = new InkCanvas(); + var canvas2 = new InkCanvas(); + + // Set different properties on each + canvas1.Mode = InkCanvasMode.Erase; + canvas2.Mode = InkCanvasMode.Select; + + canvas1.AllowedInputTypes = InkInputType.Pen; + canvas2.AllowedInputTypes = InkInputType.Touch | InkInputType.Mouse; + + // Verify they are independent + Verify.AreEqual(InkCanvasMode.Erase, canvas1.Mode, "Canvas1 mode should be Erase."); + Verify.AreEqual(InkCanvasMode.Select, canvas2.Mode, "Canvas2 mode should be Select."); + Verify.AreEqual(InkInputType.Pen, canvas1.AllowedInputTypes, "Canvas1 should be Pen."); + Verify.AreEqual(InkInputType.Touch | InkInputType.Mouse, canvas2.AllowedInputTypes, + "Canvas2 should be Touch | Mouse."); + }); + } + + [TestMethod] + public void InkCanvasStrokeCollectedEventTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + bool eventFired = false; + + // Subscribe to event - verify it doesn't throw + inkCanvas.StrokeCollected += (sender, args) => + { + eventFired = true; + }; + + // Event won't fire without ink presenter activity, but subscription should work. + Log.Comment("StrokeCollected event subscription succeeded."); + }); + } + + [TestMethod] + public void InkCanvasStrokesErasedEventTest() + { + RunOnUIThread.Execute(() => + { + var inkCanvas = new InkCanvas(); + bool eventFired = false; + + // Subscribe to event - verify it doesn't throw + inkCanvas.StrokesErased += (sender, args) => + { + eventFired = true; + }; + + Log.Comment("StrokesErased event subscription succeeded."); + }); + } + + [TestMethod] + public void InkCanvasInVisualTreeWithPropertiesTest() + { + RunOnUIThread.Execute(() => + { + var root = (Grid)XamlReader.Load( + @" + + "); + + Content = root; + Content.UpdateLayout(); + + var inkCanvas = (InkCanvas)root.FindName("TestInkCanvas"); + Verify.IsNotNull(inkCanvas, "InkCanvas should be found."); + Verify.AreEqual(InkCanvasMode.Erase, inkCanvas.Mode, "Mode should be Erase from XAML."); + }); + } + } +} diff --git a/src/controls/dev/InkCanvas/APITests/InkCanvas_APITests.projitems b/src/controls/dev/InkCanvas/APITests/InkCanvas_APITests.projitems new file mode 100644 index 0000000000..60e8badd86 --- /dev/null +++ b/src/controls/dev/InkCanvas/APITests/InkCanvas_APITests.projitems @@ -0,0 +1,15 @@ + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + a1b2c3d4-e5f6-7890-abcd-ef1234567890 + + + InkCanvas_APITests + + + + + diff --git a/src/controls/dev/InkCanvas/APITests/InkCanvas_APITests.shproj b/src/controls/dev/InkCanvas/APITests/InkCanvas_APITests.shproj new file mode 100644 index 0000000000..3b85a8de20 --- /dev/null +++ b/src/controls/dev/InkCanvas/APITests/InkCanvas_APITests.shproj @@ -0,0 +1,13 @@ + + + + a1b2c3d4-e5f6-7890-abcd-ef1234567890 + 14.0 + + + + + + + + diff --git a/src/controls/dev/InkCanvas/InkCanvas.cpp b/src/controls/dev/InkCanvas/InkCanvas.cpp index 93605cc8a1..083a795d4b 100644 --- a/src/controls/dev/InkCanvas/InkCanvas.cpp +++ b/src/controls/dev/InkCanvas/InkCanvas.cpp @@ -5,6 +5,8 @@ #include #include "InkCanvas.h" #include "InkCanvasAutomationPeer.h" +#include "InkCanvasStrokeCollectedEventArgs.h" +#include "InkCanvasStrokesErasedEventArgs.h" #include "RuntimeProfiler.h" #include "Microsoft.UI.Xaml.xamlroot.h" #include "Microsoft.UI.Composition.h" @@ -188,6 +190,193 @@ void InkCanvas::OnIsEnabledPropertyChanged(winrt::DependencyPropertyChangedEvent }); } +void InkCanvas::OnModePropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args) +{ + UpdateInkPresenterMode(); +} + +void InkCanvas::OnAllowedInputTypesPropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args) +{ + UpdateInkPresenterInputTypes(); +} + +void InkCanvas::OnDefaultDrawingAttributesPropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto newAttrs = unbox_value(args.NewValue()); + if (newAttrs) + { + QueueInkPresenterWorkItem([newAttrs](auto presenter) + { + presenter.CopyDefaultDrawingAttributes(newAttrs); + }); + } +} + +void InkCanvas::UpdateInkPresenterMode() +{ + auto mode = Mode(); + QueueInkPresenterWorkItem([mode](auto presenter) + { + switch (mode) + { + case winrt::InkCanvasMode::Draw: + presenter.InputProcessingConfiguration().Mode(winrt::InkInputProcessingMode::Inking); + break; + case winrt::InkCanvasMode::Erase: + presenter.InputProcessingConfiguration().Mode(winrt::InkInputProcessingMode::Erasing); + break; + case winrt::InkCanvasMode::Select: + presenter.InputProcessingConfiguration().Mode(winrt::InkInputProcessingMode::None); + break; + } + }); +} + +void InkCanvas::UpdateInkPresenterInputTypes() +{ + auto allowedTypes = AllowedInputTypes(); + QueueInkPresenterWorkItem([allowedTypes](auto presenter) + { + winrt::CoreInputDeviceTypes types = winrt::CoreInputDeviceTypes::None; + if (static_cast(allowedTypes) & static_cast(winrt::InkInputType::Pen)) + { + types = types | winrt::CoreInputDeviceTypes::Pen; + } + if (static_cast(allowedTypes) & static_cast(winrt::InkInputType::Touch)) + { + types = types | winrt::CoreInputDeviceTypes::Touch; + } + if (static_cast(allowedTypes) & static_cast(winrt::InkInputType::Mouse)) + { + types = types | winrt::CoreInputDeviceTypes::Mouse; + } + presenter.InputDeviceTypes(types); + }); +} + +winrt::InkStrokeContainer InkCanvas::StrokeContainer() +{ + if (m_inkPresenter) + { + return m_inkPresenter.StrokeContainer(); + } + return nullptr; +} + +winrt::IAsyncAction InkCanvas::SaveAsync(winrt::Windows::Storage::Streams::IOutputStream stream) +{ + // Save all strokes to the provided stream + concurrency::task_completion_event taskComplete; + + auto callback = [stream, taskComplete, strongThis = get_strong()]() + { + try + { + if (strongThis->m_inkPresenter) + { + auto strokeContainer = strongThis->m_inkPresenter.StrokeContainer(); + auto strokes = strokeContainer.GetStrokes(); + if (strokes.Size() > 0) + { + // SaveAsync returns IAsyncOperationWithProgress, we block on the ink thread + auto saveOp = strokeContainer.SaveAsync(stream); + saveOp.get(); + } + } + taskComplete.set(); + } + catch (...) + { + taskComplete.set_exception(std::current_exception()); + } + }; + + winrt::check_hresult(m_threadData->m_inkHost->QueueWorkItem(winrt::make(callback).get())); + + auto inktask = concurrency::create_task(taskComplete, concurrency::task_continuation_context::get_current_winrt_context()); + co_await inktask; +} + +winrt::IAsyncAction InkCanvas::LoadAsync(winrt::Windows::Storage::Streams::IInputStream stream) +{ + // Load strokes from the provided stream + concurrency::task_completion_event taskComplete; + + auto callback = [stream, taskComplete, strongThis = get_strong()]() + { + try + { + if (strongThis->m_inkPresenter) + { + auto strokeContainer = strongThis->m_inkPresenter.StrokeContainer(); + auto loadOp = strokeContainer.LoadAsync(stream); + loadOp.get(); + } + taskComplete.set(); + } + catch (...) + { + taskComplete.set_exception(std::current_exception()); + } + }; + + winrt::check_hresult(m_threadData->m_inkHost->QueueWorkItem(winrt::make(callback).get())); + + auto inktask = concurrency::create_task(taskComplete, concurrency::task_continuation_context::get_current_winrt_context()); + co_await inktask; +} + +void InkCanvas::ClearStrokes() +{ + QueueInkPresenterWorkItem([](auto presenter) + { + presenter.StrokeContainer().Clear(); + }); +} + +void InkCanvas::SetupStrokeEvents() +{ + if (m_strokeEventsConnected || !m_inkPresenter) + { + return; + } + + // We set up these event subscriptions on the ink thread since the presenter + // fires events on that thread. + auto weakThis = get_weak(); + QueueInkPresenterWorkItem([weakThis](auto presenter) + { + presenter.StrokesCollected( + [weakThis](winrt::InkPresenter const& /*sender*/, winrt::InkStrokesCollectedEventArgs const& args) + { + auto strongThis = weakThis.get(); + if (strongThis) + { + auto strokes = args.Strokes(); + for (auto const& stroke : strokes) + { + auto eventArgs = winrt::make(); + // Fire through the generated event source + strongThis->m_strokeCollectedEventSource(*strongThis, eventArgs); + } + } + }); + + presenter.StrokesErased( + [weakThis](winrt::InkPresenter const& /*sender*/, winrt::InkStrokesErasedEventArgs const& args) + { + auto strongThis = weakThis.get(); + if (strongThis) + { + auto eventArgs = winrt::make(); + strongThis->m_strokesErasedEventSource(*strongThis, eventArgs); + } + }); + }); + + m_strokeEventsConnected = true; +} + winrt::AutomationPeer InkCanvas::OnCreateAutomationPeer() { return winrt::make(*this); @@ -270,6 +459,9 @@ void InkCanvas::CreateInkPresenter() strongThis->m_inkPresenter = inkPresenter; }); winrt::check_hresult(inkHost->QueueWorkItem(callback.get())); + + // Set up stroke events eagerly after the ink presenter work item is queued. + SetupStrokeEvents(); } void InkCanvas::UpdateInkPresenterSize() diff --git a/src/controls/dev/InkCanvas/InkCanvas.h b/src/controls/dev/InkCanvas/InkCanvas.h index 049e741917..70543ef7b1 100644 --- a/src/controls/dev/InkCanvas/InkCanvas.h +++ b/src/controls/dev/InkCanvas/InkCanvas.h @@ -26,14 +26,30 @@ class InkCanvas : void OnLoaded(winrt::IInspectable const& sender, winrt::RoutedEventArgs const& args); void OnUnloaded(winrt::IInspectable const& sender, winrt::RoutedEventArgs const& args); void OnIsEnabledPropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args); + void OnModePropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args); + void OnAllowedInputTypesPropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args); + void OnDefaultDrawingAttributesPropertyChanged(winrt::DependencyPropertyChangedEventArgs const& args); winrt::AutomationPeer OnCreateAutomationPeer(); winrt::IAsyncAction QueueInkPresenterWorkItem(winrt::DoInkPresenterWork workItem); + // New API surface - Mode, Input Types, Drawing Attributes + winrt::InkStrokeContainer StrokeContainer(); + + // Persistence + winrt::IAsyncAction SaveAsync(winrt::Windows::Storage::Streams::IOutputStream stream); + winrt::IAsyncAction LoadAsync(winrt::Windows::Storage::Streams::IInputStream stream); + + // Clear all strokes + void ClearStrokes(); + private: void CreateInkPresenter(); void UpdateInkPresenterSize(); + void UpdateInkPresenterMode(); + void UpdateInkPresenterInputTypes(); + void SetupStrokeEvents(); void AttachToVisualLink(); void DetachFromVisualLink(); @@ -51,6 +67,10 @@ class InkCanvas : winrt::XamlRoot::Changed_revoker m_xamlRootChangedRevoker{}; winrt::FrameworkElement::SizeChanged_revoker m_sizeChanged_revoker; + // Stroke event tokens for InkPresenter + winrt::event_token m_strokesCollectedToken{}; + bool m_strokeEventsConnected{ false }; + // These methods (and struct) are all in support of the Composition Target method of // doing things. They all can just go away and calls to them be removed when we // get the bug fixed for the visual link method and get that code path tested and enabled. diff --git a/src/controls/dev/InkCanvas/InkCanvas.idl b/src/controls/dev/InkCanvas/InkCanvas.idl index cb08f567fa..78696c84c5 100644 --- a/src/controls/dev/InkCanvas/InkCanvas.idl +++ b/src/controls/dev/InkCanvas/InkCanvas.idl @@ -3,19 +3,82 @@ [MUX_PREVIEW] delegate void DoInkPresenterWork(Windows.UI.Input.Inking.InkPresenter presenter); + [MUX_PREVIEW] + enum InkCanvasMode + { + Draw = 0, + Erase = 1, + Select = 2, + }; + + [MUX_PREVIEW] + [flags] + enum InkInputType + { + None = 0x0, + Pen = 0x1, + Touch = 0x2, + Mouse = 0x4, + }; + + [MUX_PREVIEW] + runtimeclass InkCanvasStrokeCollectedEventArgs + { + Windows.UI.Input.Inking.InkStroke Stroke{ get; }; + }; + + [MUX_PREVIEW] + runtimeclass InkCanvasStrokesErasedEventArgs + { + Windows.Foundation.Collections.IVectorView Strokes{ get; }; + }; [MUX_PREVIEW] unsealed runtimeclass InkCanvas : Microsoft.UI.Xaml.FrameworkElement { InkCanvas(); + // Core enable/disable [MUX_PROPERTY_CHANGED_CALLBACK(TRUE)] [MUX_DEFAULT_VALUE("true")] Boolean IsEnabled{ get; set; }; + // InkPresenter work queue (existing) Windows.Foundation.IAsyncAction QueueInkPresenterWorkItem(DoInkPresenterWork workItem); + // Drawing mode + [MUX_PROPERTY_CHANGED_CALLBACK(TRUE)] + [MUX_DEFAULT_VALUE("winrt::InkCanvasMode::Draw")] + InkCanvasMode Mode{ get; set; }; + + // Input type filtering + [MUX_PROPERTY_CHANGED_CALLBACK(TRUE)] + [MUX_DEFAULT_VALUE("winrt::InkInputType::Pen | winrt::InkInputType::Mouse")] + InkInputType AllowedInputTypes{ get; set; }; + + // Default stroke attributes + [MUX_PROPERTY_CHANGED_CALLBACK(TRUE)] + Windows.UI.Input.Inking.InkDrawingAttributes DefaultDrawingAttributes{ get; set; }; + + // Stroke container access + Windows.UI.Input.Inking.InkStrokeContainer StrokeContainer{ get; }; + + // Persistence: Save / Load ink strokes + Windows.Foundation.IAsyncAction SaveAsync(Windows.Storage.Streams.IOutputStream stream); + Windows.Foundation.IAsyncAction LoadAsync(Windows.Storage.Streams.IInputStream stream); + + // Clear all strokes + void ClearStrokes(); + + // Stroke events + event Windows.Foundation.TypedEventHandler StrokeCollected; + event Windows.Foundation.TypedEventHandler StrokesErased; + + // Dependency properties static Microsoft.UI.Xaml.DependencyProperty IsEnabledProperty{ get; }; + static Microsoft.UI.Xaml.DependencyProperty ModeProperty{ get; }; + static Microsoft.UI.Xaml.DependencyProperty AllowedInputTypesProperty{ get; }; + static Microsoft.UI.Xaml.DependencyProperty DefaultDrawingAttributesProperty{ get; }; }; } diff --git a/src/controls/dev/InkCanvas/InkCanvas.vcxitems b/src/controls/dev/InkCanvas/InkCanvas.vcxitems index 65ee2bb3e7..62c2772715 100644 --- a/src/controls/dev/InkCanvas/InkCanvas.vcxitems +++ b/src/controls/dev/InkCanvas/InkCanvas.vcxitems @@ -17,6 +17,8 @@ + + diff --git a/src/controls/dev/InkCanvas/InkCanvasStrokeCollectedEventArgs.h b/src/controls/dev/InkCanvas/InkCanvasStrokeCollectedEventArgs.h new file mode 100644 index 0000000000..0fa9061547 --- /dev/null +++ b/src/controls/dev/InkCanvas/InkCanvasStrokeCollectedEventArgs.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include "InkCanvasStrokeCollectedEventArgs.g.h" + +namespace winrt::implementation +{ + struct InkCanvasStrokeCollectedEventArgs : InkCanvasStrokeCollectedEventArgsT + { + InkCanvasStrokeCollectedEventArgs() = default; + InkCanvasStrokeCollectedEventArgs(winrt::Windows::UI::Input::Inking::InkStroke const& stroke) + : m_stroke(stroke) {} + + winrt::Windows::UI::Input::Inking::InkStroke Stroke() { return m_stroke; } + + private: + winrt::Windows::UI::Input::Inking::InkStroke m_stroke{ nullptr }; + }; +} diff --git a/src/controls/dev/InkCanvas/InkCanvasStrokesErasedEventArgs.h b/src/controls/dev/InkCanvas/InkCanvasStrokesErasedEventArgs.h new file mode 100644 index 0000000000..7249ccf3b4 --- /dev/null +++ b/src/controls/dev/InkCanvas/InkCanvasStrokesErasedEventArgs.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include "InkCanvasStrokesErasedEventArgs.g.h" + +namespace winrt::implementation +{ + struct InkCanvasStrokesErasedEventArgs : InkCanvasStrokesErasedEventArgsT + { + InkCanvasStrokesErasedEventArgs() = default; + InkCanvasStrokesErasedEventArgs(winrt::Windows::Foundation::Collections::IVectorView const& strokes) + : m_strokes(strokes) {} + + winrt::Windows::Foundation::Collections::IVectorView Strokes() { return m_strokes; } + + private: + winrt::Windows::Foundation::Collections::IVectorView m_strokes{ nullptr }; + }; +} diff --git a/src/controls/dev/InkCanvas/InteractionTests/InkCanvasTests.cs b/src/controls/dev/InkCanvas/InteractionTests/InkCanvasTests.cs new file mode 100644 index 0000000000..02a055c668 --- /dev/null +++ b/src/controls/dev/InkCanvas/InteractionTests/InkCanvasTests.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using Common; +using Microsoft.UI.Xaml.Tests.MUXControls.InteractionTests.Infra; +using Microsoft.UI.Xaml.Tests.MUXControls.InteractionTests.Common; + +using WEX.TestExecution; +using WEX.TestExecution.Markup; +using WEX.Logging.Interop; + +using Microsoft.Windows.Apps.Test.Automation; +using Microsoft.Windows.Apps.Test.Foundation; +using Microsoft.Windows.Apps.Test.Foundation.Controls; +using Microsoft.Windows.Apps.Test.Foundation.Waiters; +using MUXTestInfra.Shared.Infra; + +namespace Microsoft.UI.Xaml.Tests.MUXControls.InteractionTests +{ + [TestClass] + public class InkCanvasTests + { + [ClassInitialize] + [TestProperty("RunAs", "User")] + [TestProperty("Classification", "Integration")] + [TestProperty("Platform", "Any")] + [TestProperty("MUXControlsTestSuite", "SuiteB")] + public static void ClassInitialize(TestContext testContext) + { + TestEnvironment.Initialize(testContext); + } + + public void TestCleanup() + { + TestCleanupHelper.Cleanup(); + } + + [TestMethod] + public void InkCanvasRendersInVisualTree() + { + using (var setup = new TestSetupHelper("InkCanvas Tests")) + { + var inkCanvas = FindElement.ByName("TestInkCanvas"); + Verify.IsNotNull(inkCanvas, "InkCanvas should be present in the visual tree."); + } + } + + [TestMethod] + public void InkCanvasModeSwitching() + { + using (var setup = new TestSetupHelper("InkCanvas Tests")) + { + var modeSelector = FindElement.ByName("ModeSelector"); + Verify.IsNotNull(modeSelector, "ModeSelector should be present."); + + var modeText = FindElement.ByName("ModeText"); + Verify.IsNotNull(modeText, "ModeText should be present."); + + // Switch to Erase + modeSelector.SelectItemByName("Erase"); + Wait.ForIdle(); + + var statusText = FindElement.ByName("StatusText"); + Log.Comment($"Status after Erase: {statusText.GetText()}"); + + // Switch to Select + modeSelector.SelectItemByName("Select"); + Wait.ForIdle(); + + // Switch back to Draw + modeSelector.SelectItemByName("Draw"); + Wait.ForIdle(); + } + } + + [TestMethod] + public void InkCanvasInputTypeConfiguration() + { + using (var setup = new TestSetupHelper("InkCanvas Tests")) + { + var penInput = FindElement.ByName("PenInput"); + var mouseInput = FindElement.ByName("MouseInput"); + var touchInput = FindElement.ByName("TouchInput"); + + Verify.IsNotNull(penInput, "PenInput checkbox should be present."); + Verify.IsNotNull(mouseInput, "MouseInput checkbox should be present."); + Verify.IsNotNull(touchInput, "TouchInput checkbox should be present."); + + // Enable touch + touchInput.Check(); + Wait.ForIdle(); + + // Disable pen + penInput.Uncheck(); + Wait.ForIdle(); + + // Re-enable pen + penInput.Check(); + Wait.ForIdle(); + + var inputTypesText = FindElement.ByName("InputTypesText"); + Verify.IsNotNull(inputTypesText, "InputTypesText should be present."); + } + } + + [TestMethod] + public void InkCanvasClearStrokes() + { + using (var setup = new TestSetupHelper("InkCanvas Tests")) + { + var clearButton = FindElement.ByName