diff --git a/Samples/SimpleInk/cpp/SampleConfiguration.cpp b/Samples/SimpleInk/cpp/SampleConfiguration.cpp index a63859aff1..a26d385c19 100644 --- a/Samples/SimpleInk/cpp/SampleConfiguration.cpp +++ b/Samples/SimpleInk/cpp/SampleConfiguration.cpp @@ -31,6 +31,7 @@ Platform::Array^ MainPage::scenariosInner = ref new Platform::Array + +#define _USE_MATH_DEFINES +#include + +using namespace SDKTemplate; + +using namespace Platform; +using namespace Windows::Foundation; +using namespace Windows::Foundation::Numerics; +using namespace Windows::Foundation::Collections; +using namespace Windows::UI::Core; +using namespace Windows::UI::Input::Inking; +using namespace Windows::UI::Xaml; +using namespace Windows::UI::Xaml::Controls; +using namespace Windows::UI::Xaml::Media; +using namespace Windows::UI::Xaml::Shapes; + +Scenario13::Scenario13() +{ + InitializeComponent(); + LassoSelect = (Symbol)0xEF20; + + // Initialize the InkCanvas + inkCanvas->InkPresenter->InputDeviceTypes = CoreInputDeviceTypes::Mouse | CoreInputDeviceTypes::Pen; + + // Handlers to clear the selection when inking or erasing is detected + inkCanvas->InkPresenter->StrokeInput->StrokeStarted += ref new TypedEventHandler(this, &Scenario13::StrokeInput_StrokeStarted); + inkCanvas->InkPresenter->StrokeInput->StrokeEnded += ref new TypedEventHandler(this, &Scenario13::InkPresenter_StrokesErased); +} + +void Scenario13::StrokeInput_StrokeStarted(InkStrokeInput^ sender, PointerEventArgs^ args) +{ + ClearSelection(); + inkCanvas->InkPresenter->UnprocessedInput->PointerPressed -= pointerPressedToken; + inkCanvas->InkPresenter->UnprocessedInput->PointerMoved -= pointerMovedToken; + inkCanvas->InkPresenter->UnprocessedInput->PointerReleased -= pointerReleasedToken; +} + +void Scenario13::InkPresenter_StrokesErased(InkStrokeInput^ sender, PointerEventArgs^ args) +{ + ClearSelection(); + inkCanvas->InkPresenter->UnprocessedInput->PointerPressed -= pointerPressedToken; + inkCanvas->InkPresenter->UnprocessedInput->PointerMoved -= pointerMovedToken; + inkCanvas->InkPresenter->UnprocessedInput->PointerReleased -= pointerReleasedToken; +} + +void Scenario13::OnSizeChanged(Object^ sender, SizeChangedEventArgs^ e) +{ + HelperFunctions::UpdateCanvasSize(RootGrid, outputGrid, inkCanvas); +} + +void Scenario13::UnprocessedInput_PointerPressed(InkUnprocessedInput^ sender, PointerEventArgs^ args) +{ + if (SelectedBoudningBoxContainsPosition(args->CurrentPoint->Position)) + { + return; + } + + lasso = ref new Polyline(); + lasso->Stroke = ref new SolidColorBrush(Windows::UI::Colors::Blue); + lasso->StrokeThickness = 1; + lasso->StrokeDashArray = ref new DoubleCollection(); + lasso->StrokeDashArray->Append(5.0); + lasso->StrokeDashArray->Append(2.0); + lasso->Points->Append(args->CurrentPoint->RawPosition); + selectionCanvas->Children->Append(lasso); + isBoundRect = true; + + // Prevent most handlers along the event route from handling the same event again. + args->Handled = true; +} + +void Scenario13::UnprocessedInput_PointerMoved(InkUnprocessedInput^ sender, PointerEventArgs^ args) +{ + if (isBoundRect) + { + lasso->Points->Append(args->CurrentPoint->RawPosition); + } + + // Prevent most handlers along the event route from handling the same event again. + args->Handled = true; +} + +void Scenario13::UnprocessedInput_PointerReleased(InkUnprocessedInput^ sender, PointerEventArgs^ args) +{ + lasso->Points->Append(args->CurrentPoint->RawPosition); + + boundingRect = inkCanvas->InkPresenter->StrokeContainer->SelectWithPolyLine(lasso->Points); + isBoundRect = false; + DrawBoundingRect(); + + // Prevent most handlers along the event route from handling the same event again. + args->Handled = true; +} + +/// +/// ManipulationStarted happens when the mouse is pressed. We use this method to store what Manipulation we want to perform. +/// If it was None, then we don’t want to do anything. We were not even in the Selected Box +/// If the cursor was a SizeAll, then we wanted to move the InkStrokes. +/// Otherwise we have one of the Diagonal cursors so we want to do a resize. +/// +/// +/// +void Scenario13::SelectionRectangle_ManipulationStarted(Object^ sender, Windows::UI::Xaml::Input::ManipulationStartedRoutedEventArgs^ args) +{ + if (currentManipulationType != ManipulationTypes::None) + return; + + Windows::UI::Core::CoreWindow^ window = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + + if (window->PointerCursor->Type == CoreCursorType::SizeAll) + { + currentManipulationType = ManipulationTypes::Move; + } + else + { + currentManipulationType = ManipulationTypes::Size; + } +} + +/// +/// This is the main process for the InkStoke Manipulation. +/// Here we use the stored currentManipualtiaonType to determine what type of Action we want to preform (Move or Size) +/// If its move, then we call the MoveSelectedInkStrokes method passing the pointers Delta Translation. We also apply that translation to the SelectedRectangle's Translation X and Y. +/// This will move both the InkStrokes and the Selected Box as the mouse pointer moves. +/// If its Size, then we first need to calculate a Scaling Factor. We derive this based on the Pointers Delta Transform X but reducing it by 100 to help match the pointers scroll speed +/// The Delta X can be a Positive or Negative number, so when we add this to 1 we get a reasonable value for a Scale Factor. Typically this will fall between .90 to 1.10 +/// we then check that this is a valid number, as scaling down can cause issues if we go too small or attempt to create Negative height/width. +/// and finally we apply this scale factor to the Selection Box and call the ResizeSelectedInkStrokes. +/// +/// +/// +void Scenario13::SelectionRectangle_ManipulationDelta(Object^ sender, Windows::UI::Xaml::Input::ManipulationDeltaRoutedEventArgs^ e) +{ + CompositeTransform^ transform = (CompositeTransform^)selectionRectangle->RenderTransform; + + switch (currentManipulationType) + { + case ManipulationTypes::Move: + MoveSelectedInkStrokes(e->Delta.Translation); + transform->TranslateX += e->Delta.Translation.X; + transform->TranslateY += e->Delta.Translation.Y; + + break; + + case ManipulationTypes::Size: + //Example Scaling factor used to determine the speed at which the box will grow/shrink. + auto scale = std::abs(1 + (float)e->Delta.Translation.X / 100); + + //Restrict scaling to a Minimum value. this well prevent negative With amounts and app crashing. + if (selectionRectangle->Width <= 10 && scale < 1.0) + return; + + transform->ScaleX *= scale; + transform->ScaleY *= scale; + + auto offset = selectionRectangle->ActualOffset; + auto center = new Windows::Foundation::Numerics::float2(offset.x + (float)transform->TranslateX, offset.y + (float)transform->TranslateY); + + ResizeSelectedInkStrokes(scale, *center); + break; + } +} + +void Scenario13::SelectionRectangle_ManipulationCompleted(Object^ sender, Windows::UI::Xaml::Input::ManipulationCompletedRoutedEventArgs^ args) +{ + currentManipulationType = ManipulationTypes::None; + Windows::UI::Core::CoreWindow^ window = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + window->PointerCursor = ref new CoreCursor(CoreCursorType::Arrow, 0); +} + +void Scenario13::SelectionRectangle_PointerPressed(Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ args) +{ + if (currentManipulationType != ManipulationTypes::None) + return; + + Windows::UI::Core::CoreWindow^ window = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + + if (window->PointerCursor->Type == CoreCursorType::SizeAll) + { + currentManipulationType = ManipulationTypes::Move; + } + else + { + currentManipulationType = ManipulationTypes::Size; + + switch (resizePointerCornor) + { + case RectangleCorner::TopLeft: + case RectangleCorner::BottomRight: + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeNorthwestSoutheast, 0); + break; + case RectangleCorner::TopRight: + case RectangleCorner::BottomLeft: + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeNortheastSouthwest, 0); + break; + } + } +} + +void Scenario13::SelectionRectangle_PointerMoved(Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ args) +{ + if (currentManipulationType != ManipulationTypes::None) + return; + + Windows::UI::Core::CoreWindow^ window = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + Point mousePos = args->GetCurrentPoint(selectionRectangle)->Position; + + SetPointerCursorType(mousePos); +} + +void Scenario13::SelectionRectangle_PointerExited(Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ args) +{ + if (currentManipulationType != ManipulationTypes::None) + return; + + Windows::UI::Core::CoreWindow^ window = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + window->PointerCursor = ref new CoreCursor(CoreCursorType::Arrow, 0); +} + +/// +/// The method will take all Strokes from the inkCanvas that have been "Selected" as per the link statement. +/// Then we will apply a Matrix3x2 transform, based on the supplied scale and the topLeft point of the bounding box. +/// +/// Scaling factor to apply to the selected points +/// the "X,Y" coordinates of the Selection Box. +void Scenario13::ResizeSelectedInkStrokes(float scale, Windows::Foundation::Numerics::float2& topLeft) +{ + + auto allStrokes = inkCanvas->InkPresenter->StrokeContainer->GetStrokes(); + + if (allStrokes == nullptr) + return; + + float3x2 matrixSacale = make_float3x2_scale(scale, topLeft); + + for (auto stroke : allStrokes) + { + if (stroke->Selected) + { + stroke->PointTransform *= matrixSacale; + } + } +} + +void Scenario13::MoveSelectedInkStrokes(Point position) +{ + float3x2 matrix = make_float3x2_translation((float)position.X, (float)position.Y); + + auto allStrokes = inkCanvas->InkPresenter->StrokeContainer->GetStrokes(); + if (allStrokes == nullptr) + return; + + for (auto stroke : allStrokes) + { + if (stroke->Selected) + { + stroke->PointTransform *= matrix; + } + } + +} + +void Scenario13::DrawBoundingRect() +{ + selectionCanvas->Children->Clear(); + + if (boundingRect.Width <= 0 || boundingRect.Height <= 0) + { + return; + } + + selectionRectangle = ref new Rectangle(); + selectionRectangle->Name = "SelectedBox"; + selectionRectangle->Stroke = ref new SolidColorBrush(Windows::UI::Colors::Blue); + selectionRectangle->Fill = ref new SolidColorBrush(Windows::UI::Colors::Transparent); + selectionRectangle->StrokeThickness = 1; + selectionRectangle->StrokeDashArray = ref new DoubleCollection(); + selectionRectangle->ManipulationMode = Windows::UI::Xaml::Input::ManipulationModes::All; + selectionRectangle->RenderTransform = ref new CompositeTransform(); + selectionRectangle->StrokeDashArray->Append(5.0); + selectionRectangle->StrokeDashArray->Append(2.0); + selectionRectangle->Width = boundingRect.Width; + selectionRectangle->Height = boundingRect.Height; + + selectionCanvas->SetLeft(selectionRectangle, boundingRect.X); + selectionCanvas->SetTop(selectionRectangle, boundingRect.Y); + + manipulationStartedToken = selectionRectangle->ManipulationStarted += ref new Windows::UI::Xaml::Input::ManipulationStartedEventHandler(this, &Scenario13::SelectionRectangle_ManipulationStarted); + manipulationDeltaToken = selectionRectangle->ManipulationDelta += ref new Windows::UI::Xaml::Input::ManipulationDeltaEventHandler(this, &Scenario13::SelectionRectangle_ManipulationDelta); + manipulationCompletedToken = selectionRectangle->ManipulationCompleted += ref new Windows::UI::Xaml::Input::ManipulationCompletedEventHandler(this, &Scenario13::SelectionRectangle_ManipulationCompleted); + pointerPressedRectangleToken = selectionRectangle->PointerPressed += ref new Windows::UI::Xaml::Input::PointerEventHandler(this, &Scenario13::SelectionRectangle_PointerPressed); + pointerMovedRectangleToken = selectionRectangle->PointerMoved += ref new Windows::UI::Xaml::Input::PointerEventHandler(this, &Scenario13::SelectionRectangle_PointerMoved); + pointerExitedRectangleToken = selectionRectangle->PointerExited += ref new Windows::UI::Xaml::Input::PointerEventHandler(this, &Scenario13::SelectionRectangle_PointerExited); + + selectionCanvas->Children->Append(selectionRectangle); +} + +void Scenario13::ToolButton_Lasso(Object^ sender, RoutedEventArgs^ e) +{ + // By default, pen barrel button or right mouse button is processed for inking + // Set the configuration to instead allow processing these input on the UI thread + inkCanvas->InkPresenter->InputProcessingConfiguration->RightDragAction = InkInputRightDragAction::LeaveUnprocessed; + + pointerPressedToken = inkCanvas->InkPresenter->UnprocessedInput->PointerPressed += ref new TypedEventHandler(this, &Scenario13::UnprocessedInput_PointerPressed); + pointerMovedToken = inkCanvas->InkPresenter->UnprocessedInput->PointerMoved += ref new TypedEventHandler(this, &Scenario13::UnprocessedInput_PointerMoved); + pointerReleasedToken = inkCanvas->InkPresenter->UnprocessedInput->PointerReleased += ref new TypedEventHandler(this, &Scenario13::UnprocessedInput_PointerReleased); +} + +/// +/// We want to make sure that whenever we clear the SelectionBox we also unhook all out event handlers. +/// +void Scenario13::ClearDrawnBoundingRect() +{ + if (selectionCanvas->Children->Size > 0) + { + selectionCanvas->Children->Clear(); + + selectionRectangle->ManipulationStarted -= manipulationStartedToken; + selectionRectangle->ManipulationCompleted -= manipulationCompletedToken; + selectionRectangle->ManipulationDelta -= manipulationDeltaToken; + selectionRectangle->PointerExited -= pointerExitedRectangleToken; + selectionRectangle->PointerMoved -= pointerMovedRectangleToken; + selectionRectangle->PointerPressed -= pointerPressedRectangleToken; + + selectionRectangle = nullptr; + + boundingRect = Rect::Empty; + } +} + +void Scenario13::ClearSelection() +{ + auto strokes = inkCanvas->InkPresenter->StrokeContainer->GetStrokes(); + for (auto stroke : strokes) + { + stroke->Selected = false; + } + ClearDrawnBoundingRect(); +} + +bool Scenario13::SelectedBoudningBoxContainsPosition(Windows::Foundation::Point position) +{ + auto contains = !boundingRect.IsEmpty && boundingRect.Contains(position); + return contains; +} + +/// +/// Take the current pointer position and determine what cursor you want to display. +/// If the cursor is in a corner within the distance of an OffSet then display a diagonal cursor +/// If it is not in a corner, but still inside the Bounding Box, then display the 4 arrow cursor +/// otherwise it is outside the box so reset to default. +/// +/// +void Scenario13::SetPointerCursorType(Point mousePos) +{ + Windows::UI::Core::CoreWindow^ window = Windows::UI::Core::CoreWindow::GetForCurrentThread(); + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeAll, 0); + float offset = 5; + float recMinX, recMaxX, recMinY, recMaxY; + recMinX = 0; + recMinY = 0; + + recMaxX = (float)selectionRectangle->ActualWidth; + recMaxY = (float)selectionRectangle->ActualHeight; + + if (mousePos.X >= recMinX - offset && mousePos.X <= recMinX + offset && + mousePos.Y >= recMinY - offset && mousePos.Y <= recMinY + offset) + { + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeNorthwestSoutheast, 0); + resizePointerCornor = RectangleCorner::TopLeft; + return; + } + else if (mousePos.X >= recMaxX - offset && mousePos.X <= recMaxX + offset && + mousePos.Y >= recMaxY - offset && mousePos.Y <= recMaxY + offset) + { + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeNorthwestSoutheast, 0); + resizePointerCornor = RectangleCorner::BottomRight; + return; + } + else if (mousePos.X >= recMinX - offset && mousePos.X <= recMinX + offset && + mousePos.Y >= recMaxY - offset && mousePos.Y <= recMaxY + offset) + { + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeNortheastSouthwest, 0); + resizePointerCornor = RectangleCorner::BottomLeft; + return; + } + else if (mousePos.X >= recMaxX - offset && mousePos.X <= recMaxX + offset && + mousePos.Y >= recMinY - offset && mousePos.Y <= recMinY + offset) + { + window->PointerCursor = ref new CoreCursor(CoreCursorType::SizeNortheastSouthwest, 0); + resizePointerCornor = RectangleCorner::TopRight; + return; + } +} + diff --git a/Samples/SimpleInk/cpp/Scenario13.xaml.h b/Samples/SimpleInk/cpp/Scenario13.xaml.h new file mode 100644 index 0000000000..1c398c6523 --- /dev/null +++ b/Samples/SimpleInk/cpp/Scenario13.xaml.h @@ -0,0 +1,109 @@ +//********************************************************* +// +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF +// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY +// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR +// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. +// +//********************************************************* + +#pragma once + +#include "Scenario13.g.h" +#include "MainPage.xaml.h" + +namespace SDKTemplate +{ + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + [Windows::Foundation::Metadata::WebHostHidden] + public ref class Scenario13 sealed + { + internal: + /// + /// An enum used to help determine what Pointer symbol we should use. + /// We can use this enum to store which corner of the Selected Boudning Box our pointer was in at the time pointer was pressed down + /// + enum RectangleCorner + { + //We will use a Diagonal a cursor of CoreCursorType.SizeNorthwestSoutheast + TopLeft, + //We will use a Diagonal a cursor of CoreCursorType.SizeNortheastSouthwest + TopRight, + //We will use a Diagonal a cursor of CoreCursorType.SizeNortheastSouthwest + BottomLeft, + //We will use a Diagonal a cursor of CoreCursorType.SizeNorthwestSoutheast + BottomRight + }; + + /// + /// An enum used to help determine what type of Manipulation we are doing to the selected InkStrokes + /// + enum ManipulationTypes + { + //No current Manipulation started - default or base condition + None, + //we are planning or doing a Move manipulation + Move, + //we are planning or doing a Resize manipulation + Size + }; + + public: + Scenario13(); + + property Windows::UI::Xaml::Controls::Symbol LassoSelect; + + private: + + void StrokeInput_StrokeStarted(Windows::UI::Input::Inking::InkStrokeInput^ sender, Windows::UI::Core::PointerEventArgs^ args); + void InkPresenter_StrokesErased(Windows::UI::Input::Inking::InkStrokeInput^ sender, Windows::UI::Core::PointerEventArgs^ args); + + void OnSizeChanged(Platform::Object^ sender, Windows::UI::Xaml::SizeChangedEventArgs^ e); + void ToolButton_Lasso(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); + void MoveSelectedInkStrokes(Windows::Foundation::Point position); + void ResizeSelectedInkStrokes(float scale, Windows::Foundation::Numerics::float2& topLeft); + void DrawBoundingRect(); + void ClearDrawnBoundingRect(); + void ClearSelection(); + void SetPointerCursorType(Windows::Foundation::Point mousePos); + + void UnprocessedInput_PointerPressed(Windows::UI::Input::Inking::InkUnprocessedInput^ sender, Windows::UI::Core::PointerEventArgs^ args); + void UnprocessedInput_PointerMoved(Windows::UI::Input::Inking::InkUnprocessedInput^ sender, Windows::UI::Core::PointerEventArgs^ args); + void UnprocessedInput_PointerReleased(Windows::UI::Input::Inking::InkUnprocessedInput^ sender, Windows::UI::Core::PointerEventArgs^ args); + + void SelectionRectangle_ManipulationStarted(Object^ sender, Windows::UI::Xaml::Input::ManipulationStartedRoutedEventArgs^ args); + void SelectionRectangle_ManipulationDelta(Object^ sender, Windows::UI::Xaml::Input::ManipulationDeltaRoutedEventArgs^ args); + void SelectionRectangle_ManipulationCompleted(Object^ sender, Windows::UI::Xaml::Input::ManipulationCompletedRoutedEventArgs ^ args); + + void SelectionRectangle_PointerPressed(Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ args); + void SelectionRectangle_PointerMoved(Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ args); + void SelectionRectangle_PointerExited(Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ args); + + bool SelectedBoudningBoxContainsPosition(Windows::Foundation::Point position); + + MainPage^ rootPage = MainPage::Current; + Windows::UI::Xaml::Shapes::Polyline^ lasso; + Windows::Foundation::Rect boundingRect; + Windows::UI::Xaml::Shapes::Rectangle^ selectionRectangle; + ManipulationTypes currentManipulationType; + RectangleCorner resizePointerCornor; + bool isBoundRect; + + Windows::Foundation::EventRegistrationToken pointerPressedToken; + Windows::Foundation::EventRegistrationToken pointerMovedToken; + Windows::Foundation::EventRegistrationToken pointerReleasedToken; + + Windows::Foundation::EventRegistrationToken manipulationStartedToken; + Windows::Foundation::EventRegistrationToken manipulationCompletedToken; + Windows::Foundation::EventRegistrationToken manipulationDeltaToken; + Windows::Foundation::EventRegistrationToken pointerExitedRectangleToken; + Windows::Foundation::EventRegistrationToken pointerMovedRectangleToken; + Windows::Foundation::EventRegistrationToken pointerPressedRectangleToken; + + + }; +} diff --git a/Samples/SimpleInk/cpp/SimpleInk.vcxproj b/Samples/SimpleInk/cpp/SimpleInk.vcxproj index 1ee7db00d3..a19556db6b 100644 --- a/Samples/SimpleInk/cpp/SimpleInk.vcxproj +++ b/Samples/SimpleInk/cpp/SimpleInk.vcxproj @@ -180,6 +180,9 @@ ..\shared\Scenario12.xaml + + + ..\shared\Scenario13.xaml @@ -204,6 +207,7 @@ + @@ -262,6 +266,9 @@ ..\shared\Scenario12.xaml + + + ..\shared\Scenario13.xaml diff --git a/Samples/SimpleInk/cpp/SimpleInk.vcxproj.filters b/Samples/SimpleInk/cpp/SimpleInk.vcxproj.filters index 1c69e5bfef..a400440334 100644 --- a/Samples/SimpleInk/cpp/SimpleInk.vcxproj.filters +++ b/Samples/SimpleInk/cpp/SimpleInk.vcxproj.filters @@ -30,6 +30,7 @@ + @@ -49,6 +50,7 @@ + @@ -70,6 +72,7 @@ + diff --git a/Samples/SimpleInk/cs/SampleConfiguration.cs b/Samples/SimpleInk/cs/SampleConfiguration.cs index f7c76524ff..44bafc1892 100644 --- a/Samples/SimpleInk/cs/SampleConfiguration.cs +++ b/Samples/SimpleInk/cs/SampleConfiguration.cs @@ -34,6 +34,7 @@ public partial class MainPage : Page new Scenario() { Title="Scenario 10", ClassType=typeof(Scenario10)}, new Scenario() { Title="Scenario 11", ClassType=typeof(Scenario11)}, new Scenario() { Title="Scenario 12", ClassType=typeof(Scenario12)}, + new Scenario() { Title="Scenario 13", ClassType=typeof(Scenario13)}, }; } diff --git a/Samples/SimpleInk/cs/Scenario13.xaml.cs b/Samples/SimpleInk/cs/Scenario13.xaml.cs new file mode 100644 index 0000000000..beef6808c1 --- /dev/null +++ b/Samples/SimpleInk/cs/Scenario13.xaml.cs @@ -0,0 +1,453 @@ +//********************************************************* +// +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF +// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY +// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR +// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. +// +//********************************************************* + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Threading; +using Windows.Foundation; +using Windows.UI; +using Windows.UI.Core; +using Windows.UI.Input.Inking; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; +using Windows.UI.Xaml.Shapes; + +namespace SDKTemplate +{ + + /// + /// An enum used to help determine what Pointer symbol we should use. + /// We can use this enum to store which corner of the Selected Boudning Box our pointer was in at the time pointer was pressed down + /// + internal enum RectangleCorner + { + //We will use a Diagonal a cursor of CoreCursorType.SizeNorthwestSoutheast + TopLeft, + //We will use a Diagonal a cursor of CoreCursorType.SizeNortheastSouthwest + TopRight, + //We will use a Diagonal a cursor of CoreCursorType.SizeNortheastSouthwest + BottomLeft, + //We will use a Diagonal a cursor of CoreCursorType.SizeNorthwestSoutheast + BottomRight + } + + /// + /// An enum used to help determine what type of Manipulation we are doing to the selected InkStrokes + /// + internal enum ManipulationTypes + { + //No current Manipulation started - default or base condition + None, + //we are planning or doing a Move manipulation + Move, + //we are planning or doing a Resize manipulation + Size + } + + /// + /// This page shows the code to configure the InkToolbar. + /// + public sealed partial class Scenario13 : Page + { + private Polyline lasso; + private Rect boundingRect; + private bool isBoundRect; + + Symbol LassoSelect = (Symbol)0xEF20; + private Rectangle selectionRectangle; + private ManipulationTypes currentManipulationType; + private RectangleCorner resizePointerCornor; + + public Scenario13() + { + this.InitializeComponent(); + + // Initialize the InkCanvas + inkCanvas.InkPresenter.InputDeviceTypes = CoreInputDeviceTypes.Mouse | CoreInputDeviceTypes.Pen; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + // Registering handlers to clear the selection when inking or erasing is detected + inkCanvas.InkPresenter.StrokeInput.StrokeStarted += StrokeInput_StrokeStarted; + inkCanvas.InkPresenter.StrokesErased += InkPresenter_StrokesErased; + } + + protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) + { + // Unregistering handlers to clear the selection when inking or erasing is detected + inkCanvas.InkPresenter.StrokeInput.StrokeStarted -= StrokeInput_StrokeStarted; + inkCanvas.InkPresenter.StrokesErased -= InkPresenter_StrokesErased; + ClearSelection(); + } + + private void StrokeInput_StrokeStarted(InkStrokeInput sender, PointerEventArgs args) + { + ClearSelection(); + inkCanvas.InkPresenter.UnprocessedInput.PointerPressed -= UnprocessedInput_PointerPressed; + inkCanvas.InkPresenter.UnprocessedInput.PointerMoved -= UnprocessedInput_PointerMoved; + inkCanvas.InkPresenter.UnprocessedInput.PointerReleased -= UnprocessedInput_PointerReleased; + } + + private void InkPresenter_StrokesErased(InkPresenter sender, InkStrokesErasedEventArgs args) + { + ClearSelection(); + inkCanvas.InkPresenter.UnprocessedInput.PointerPressed -= UnprocessedInput_PointerPressed; + inkCanvas.InkPresenter.UnprocessedInput.PointerMoved -= UnprocessedInput_PointerMoved; + inkCanvas.InkPresenter.UnprocessedInput.PointerReleased -= UnprocessedInput_PointerReleased; + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + HelperFunctions.UpdateCanvasSize(RootGrid, outputGrid, inkCanvas); + } + + private void UnprocessedInput_PointerPressed(InkUnprocessedInput sender, PointerEventArgs args) + { + if (SelectedBoudningBoxContainsPosition(args.CurrentPoint.Position)) + { + return; + } + + lasso = new Polyline() + { + Stroke = new SolidColorBrush(Windows.UI.Colors.Blue), + StrokeThickness = 1, + StrokeDashArray = new DoubleCollection() { 5, 2 }, + }; + + lasso.Points.Add(args.CurrentPoint.RawPosition); + selectionCanvas.Children.Add(lasso); + isBoundRect = true; + + // Prevent most handlers along the event route from handling the same event again. + args.Handled = true; + } + + private void UnprocessedInput_PointerMoved(InkUnprocessedInput sender, PointerEventArgs args) + { + if (isBoundRect) + { + lasso.Points.Add(args.CurrentPoint.RawPosition); + } + + // Prevent most handlers along the event route from handling the same event again. + args.Handled = true; + } + + private void UnprocessedInput_PointerReleased(InkUnprocessedInput sender, PointerEventArgs args) + { + lasso.Points.Add(args.CurrentPoint.RawPosition); + + boundingRect = inkCanvas.InkPresenter.StrokeContainer.SelectWithPolyLine(lasso.Points); + isBoundRect = false; + DrawBoundingRect(); + + // Prevent most handlers along the event route from handling the same event again. + args.Handled = true; + } + + private void DrawBoundingRect() + { + selectionCanvas.Children.Clear(); + if (boundingRect.Width <= 0 || boundingRect.Height <= 0) + { + return; + } + + selectionRectangle = new Rectangle() + { + Name = "SelectedBox", + Stroke = new SolidColorBrush(Windows.UI.Colors.Blue), + Fill = new SolidColorBrush(Windows.UI.Colors.Transparent), + StrokeThickness = 1, + StrokeDashArray = new DoubleCollection() { 5, 2 }, + ManipulationMode = ManipulationModes.All, + RenderTransform = new CompositeTransform(), + Width = boundingRect.Width, + Height = boundingRect.Height + }; + + Canvas.SetLeft(selectionRectangle, boundingRect.X); + Canvas.SetTop(selectionRectangle, boundingRect.Y); + + selectionRectangle.ManipulationStarted += SelectionRectangle_ManipulationStarted; + selectionRectangle.ManipulationDelta += SelectionRectangle_ManipulationDelta; + selectionRectangle.ManipulationCompleted += SelectionRectangle_ManipulationCompleted; + selectionRectangle.PointerPressed += SelectionRectangle_PointerPressed; + selectionRectangle.PointerMoved += SelectionRectangle_PointerMoved; + selectionRectangle.PointerExited += SelectionRectangle_PointerExited; + + selectionCanvas.Children.Add(selectionRectangle); + } + + /// + /// ManipulationStarted happens when the mouse is pressed. We use this method to store what Manipulation we want to perform. + /// If it was None, then we don’t want to do anything. We were not even in the Selected Box + /// If the cursor was a SizeAll, then we wanted to move the InkStrokes. + /// Otherwise we have one of the Diagonal cursors so we want to do a resize. + /// + /// + /// + private void SelectionRectangle_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e) + { + if (currentManipulationType != ManipulationTypes.None) + return; + + if (Window.Current.CoreWindow.PointerCursor.Type == CoreCursorType.SizeAll) + { + currentManipulationType = ManipulationTypes.Move; + } + else + { + currentManipulationType = ManipulationTypes.Size; + } + } + + /// + /// This is the main process for the InkStoke Manipulation. + /// Here we use the stored currentManipualtiaonType to determine what type of Action we want to preform (Move or Size) + /// If its move, then we call the MoveSelectedInkStrokes method passing the pointers Delta Translation. We also apply that translation to the SelectedRectangle's Translation X and Y. + /// This will move both the InkStrokes and the Selected Box as the mouse pointer moves. + /// If its Size, then we first need to calculate a Scaling Factor. We derive this based on the Pointers Delta Transform X but reducing it by 100 to help match the pointers scroll speed + /// The Delta X can be a Positive or Negative number, so when we add this to 1 we get a reasonable value for a Scale Factor. Typically this will fall between .90 to 1.10 + /// we then check that this is a valid number, as scaling down can cause issues if we go too small or attempt to create Negative height/width. + /// and finally we apply this scale factor to the Selection Box and call the ResizeSelectedInkStrokes. + /// + /// + /// + private void SelectionRectangle_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) + { + e.Handled = true; + var transform = (selectionRectangle.RenderTransform as CompositeTransform); + + switch (currentManipulationType) + { + case ManipulationTypes.Move: + MoveSelectedInkStrokes(e.Delta.Translation); + transform.TranslateX += e.Delta.Translation.X; + transform.TranslateY += e.Delta.Translation.Y; + break; + + case ManipulationTypes.Size: + + //Example Scaling factor used to determine the speed at which the box will grow/shrink. + var scalingFactor = Math.Abs(1 + (float)e.Delta.Translation.X / 100); + + //Restrict scaling to a Minimum value. this well prevent negative With amounts and app crashing. + if (selectionRectangle.Width <= 10 && scalingFactor < 1f) + return; + + transform.ScaleX *= scalingFactor; + transform.ScaleY *= scalingFactor; + + var offset = selectionRectangle.ActualOffset; + + ResizeSelectedInkStrokes(scalingFactor, new Vector2(offset.X + (float)transform.TranslateX, offset.Y + (float)transform.TranslateY)); + break; + } + } + + /// + /// When we are done resizing/moving the InkStrokes, we reset the mouse pointer and clear the currentManipulationType placeholder. + /// + /// + /// + private void SelectionRectangle_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e) + { + currentManipulationType = ManipulationTypes.None; + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Arrow, 0); + } + + private void SelectionRectangle_PointerPressed(object sender, PointerRoutedEventArgs e) + { + if (currentManipulationType != ManipulationTypes.None) + return; + + if (Window.Current.CoreWindow.PointerCursor.Type == CoreCursorType.SizeAll) + { + currentManipulationType = ManipulationTypes.Move; + } + else + { + currentManipulationType = ManipulationTypes.Size; + + switch (resizePointerCornor) + { + case RectangleCorner.TopLeft: + case RectangleCorner.BottomRight: + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeNorthwestSoutheast, 0); + break; + case RectangleCorner.TopRight: + case RectangleCorner.BottomLeft: + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeNortheastSouthwest, 0); + break; + } + } + } + + private void SelectionRectangle_PointerMoved(object sender, PointerRoutedEventArgs e) + { + if (currentManipulationType != ManipulationTypes.None) + return; + + var mousePos = e.GetCurrentPoint(selectionRectangle).Position; + SetPointerCursorType(mousePos); + } + + private void SelectionRectangle_PointerExited(object sender, PointerRoutedEventArgs e) + { + if (currentManipulationType != ManipulationTypes.None) + return; + + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Arrow, 0); + } + + private void ToolButton_Lasso(object sender, RoutedEventArgs e) + { + // By default, pen barrel button or right mouse button is processed for inking + // Set the configuration to instead allow processing these input on the UI thread + inkCanvas.InkPresenter.InputProcessingConfiguration.RightDragAction = InkInputRightDragAction.LeaveUnprocessed; + + inkCanvas.InkPresenter.UnprocessedInput.PointerPressed += UnprocessedInput_PointerPressed; + inkCanvas.InkPresenter.UnprocessedInput.PointerMoved += UnprocessedInput_PointerMoved; + inkCanvas.InkPresenter.UnprocessedInput.PointerReleased += UnprocessedInput_PointerReleased; + } + + /// + /// We want to make sure that whenever we clear the SelectionBox we also unhook all out event handlers. + /// + private void ClearDrawnBoundingRect() + { + + if (selectionCanvas.Children.Count > 0) + { + selectionCanvas.Children.Clear(); + selectionRectangle.ManipulationStarted -= SelectionRectangle_ManipulationStarted; + selectionRectangle.ManipulationCompleted -= SelectionRectangle_ManipulationCompleted; + selectionRectangle.ManipulationDelta -= SelectionRectangle_ManipulationDelta; + selectionRectangle.PointerExited -= SelectionRectangle_PointerExited; + selectionRectangle.PointerMoved -= SelectionRectangle_PointerMoved; + selectionRectangle.PointerPressed -= SelectionRectangle_PointerPressed; + + selectionRectangle = null; + boundingRect = Rect.Empty; + } + } + + private void ClearSelection() + { + var strokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes(); + foreach (var stroke in strokes) + { + stroke.Selected = false; + } + ClearDrawnBoundingRect(); + } + + private bool SelectedBoudningBoxContainsPosition(Point position) + { + var contains = !boundingRect.IsEmpty && RectHelper.Contains(boundingRect, position); + return contains; + } + + /// + /// The method will take all Strokes from the inkCanvas that have been "Selected" as per the link statement. + /// Then we will apply a Matrix3x2 transform, based on the supplied scale and the topLeft point of the bounding box. + /// + /// Scaling factor to apply to the selected points + /// the "X,Y" coordinates of the Selection Box. + private void ResizeSelectedInkStrokes(float scale, Vector2 topLeft) + { + + IEnumerable selectedStrokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().Where(x => x.Selected); + if (selectedStrokes == null) + return; + + Matrix3x2 matrixSacale = Matrix3x2.CreateScale(scale, topLeft); + + foreach (InkStroke inkStroke in selectedStrokes) + { + inkStroke.PointTransform *= matrixSacale; + } + } + + private void MoveSelectedInkStrokes(Point pos) + { + Matrix3x2 matrix = Matrix3x2.CreateTranslation((float)pos.X, (float)pos.Y); + + if (!matrix.IsIdentity) + { + IEnumerable selectedStrokes = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().Where(x => x.Selected); + if (selectedStrokes == null) + return; + foreach (InkStroke stroke in selectedStrokes) + { + stroke.PointTransform *= matrix; + } + } + } + + /// + /// Take the current pointer position and determine what cursor you want to display. + /// If the cursor is in a corner within the distance of an OffSet then display a diagonal cursor + /// If it is not in a corner, but still inside the Bounding Box, then display the 4 arrow cursor + /// otherwise it is outside the box so reset to default. + /// + /// + private void SetPointerCursorType(Point mousePos) + { + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeAll, 0); + var offset = 5; + float recMinX, recMaxX, recMinY, recMaxY; + recMinX = 0; + recMinY = 0; + + recMaxX = (float)selectionRectangle.ActualWidth; + recMaxY = (float)selectionRectangle.ActualHeight; + + if (mousePos.X >= recMinX - offset && mousePos.X <= recMinX + offset && + mousePos.Y >= recMinY - offset && mousePos.Y <= recMinY + offset) + { + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeNorthwestSoutheast, 0); + resizePointerCornor = RectangleCorner.TopLeft; + return; + } + else if (mousePos.X >= recMaxX - offset && mousePos.X <= recMaxX + offset && + mousePos.Y >= recMaxY - offset && mousePos.Y <= recMaxY + offset) + { + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeNorthwestSoutheast, 0); + resizePointerCornor = RectangleCorner.BottomRight; + return; + } + else if (mousePos.X >= recMinX - offset && mousePos.X <= recMinX + offset && + mousePos.Y >= recMaxY - offset && mousePos.Y <= recMaxY + offset) + { + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeNortheastSouthwest, 0); + resizePointerCornor = RectangleCorner.BottomLeft; + return; + } + else if (mousePos.X >= recMaxX - offset && mousePos.X <= recMaxX + offset && + mousePos.Y >= recMinY - offset && mousePos.Y <= recMinY + offset) + { + Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.SizeNortheastSouthwest, 0); + resizePointerCornor = RectangleCorner.TopRight; + return; + } + } + } +} \ No newline at end of file diff --git a/Samples/SimpleInk/cs/SimpleInk.csproj b/Samples/SimpleInk/cs/SimpleInk.csproj index f551038e77..7bd52dfdcc 100644 --- a/Samples/SimpleInk/cs/SimpleInk.csproj +++ b/Samples/SimpleInk/cs/SimpleInk.csproj @@ -107,6 +107,7 @@ Scenario1.xaml + Scenario2.xaml @@ -162,6 +163,11 @@ MSBuild:Compile Designer + + Scenario13.xaml + MSBuild:Compile + Designer + Scenario2.xaml MSBuild:Compile diff --git a/Samples/SimpleInk/shared/Scenario13.xaml b/Samples/SimpleInk/shared/Scenario13.xaml new file mode 100644 index 0000000000..e4c6e1987b --- /dev/null +++ b/Samples/SimpleInk/shared/Scenario13.xaml @@ -0,0 +1,78 @@ + + + + + + + + + + + + This scenario demonstrates selecting InkStrokes and Moving/Resizing them. + Select the Stroke selection tool, i.e. lasso, and select a drawn Stroke. + When you hover over the Bounding Box your mouse pointer will change to represent the available Manipulation Options. + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SharedContent/cs/MainPage.xaml.cs b/SharedContent/cs/MainPage.xaml.cs index f75bbdd17d..4a6fd8787b 100644 --- a/SharedContent/cs/MainPage.xaml.cs +++ b/SharedContent/cs/MainPage.xaml.cs @@ -49,6 +49,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) { itemCollection.Add(new Scenario { Title = $"{i++}) {s.Title}", ClassType = s.ClassType }); } + ScenarioControl.ItemsSource = itemCollection; if (Window.Current.Bounds.Width < 640)