From f3da94ff2019fe8b9d9dc9d86f8e78cb23c501a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Brid?= <36547063+RBrid@users.noreply.github.com> Date: Tue, 9 Jun 2020 19:18:55 -0700 Subject: [PATCH] NavigationViewItem input handling fixes (#2625) * NavigationViewItem fixes including WinUI3 26317023 * Applying PR feedback & merging with master. --- .../NavigationViewItemBase.properties.cpp | 10 +- .../NavigationViewItemBase.properties.h | 4 + dev/NavigationView/NavigationView.h | 3 - dev/NavigationView/NavigationView.idl | 3 + dev/NavigationView/NavigationViewHelper.h | 2 +- dev/NavigationView/NavigationViewItem.cpp | 328 ++++++++++++++---- dev/NavigationView/NavigationViewItem.h | 61 ++-- dev/NavigationView/NavigationViewItemBase.cpp | 25 +- dev/NavigationView/NavigationViewItemBase.h | 20 +- .../NavigationViewItemSeparator.cpp | 10 +- .../NavigationViewItemSeparator.h | 2 +- .../NavigationViewTests.cs | 67 +++- .../TestUI/NavigationViewPage.xaml | 1 + .../TestUI/NavigationViewPage.xaml.cs | 25 ++ dev/ResourceHelper/Utils.cpp | 2 +- dev/ResourceHelper/Utils.h | 4 +- dev/TabView/TestUI/TabViewPage.xaml | 14 +- dev/inc/RoutedEventHelpers.h | 34 +- 18 files changed, 474 insertions(+), 141 deletions(-) diff --git a/dev/Generated/NavigationViewItemBase.properties.cpp b/dev/Generated/NavigationViewItemBase.properties.cpp index d49c742af0..f5df02ce06 100644 --- a/dev/Generated/NavigationViewItemBase.properties.cpp +++ b/dev/Generated/NavigationViewItemBase.properties.cpp @@ -31,7 +31,7 @@ void NavigationViewItemBaseProperties::EnsureProperties() winrt::name_of(), false /* isAttached */, ValueHelper::BoxedDefaultValue(), - nullptr); + winrt::PropertyChangedCallback(&OnIsSelectedPropertyChanged)); } } @@ -40,6 +40,14 @@ void NavigationViewItemBaseProperties::ClearProperties() s_IsSelectedProperty = nullptr; } +void NavigationViewItemBaseProperties::OnIsSelectedPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args) +{ + auto owner = sender.as(); + winrt::get_self(owner)->OnPropertyChanged(args); +} + void NavigationViewItemBaseProperties::IsSelected(bool value) { static_cast(this)->SetValue(s_IsSelectedProperty, ValueHelper::BoxValueIfNecessary(value)); diff --git a/dev/Generated/NavigationViewItemBase.properties.h b/dev/Generated/NavigationViewItemBase.properties.h index bb70b955b4..8e22af3fcc 100644 --- a/dev/Generated/NavigationViewItemBase.properties.h +++ b/dev/Generated/NavigationViewItemBase.properties.h @@ -18,4 +18,8 @@ class NavigationViewItemBaseProperties static void EnsureProperties(); static void ClearProperties(); + + static void OnIsSelectedPropertyChanged( + winrt::DependencyObject const& sender, + winrt::DependencyPropertyChangedEventArgs const& args); }; diff --git a/dev/NavigationView/NavigationView.h b/dev/NavigationView/NavigationView.h index 630aa3e87d..4a0db32571 100644 --- a/dev/NavigationView/NavigationView.h +++ b/dev/NavigationView/NavigationView.h @@ -93,8 +93,6 @@ class NavigationView : private: - void OnRepeaterGettingFocus(const winrt::IInspectable& sender, const winrt::GettingFocusEventArgs& args); - // Selection handling functions void OnNavigationViewItemIsSelectedPropertyChanged(const winrt::DependencyObject& sender, const winrt::DependencyProperty& args); void OnSelectionModelSelectionChanged(const winrt::SelectionModel& selectionModel, const winrt::SelectionModelSelectionChangedEventArgs& e); @@ -261,7 +259,6 @@ class NavigationView : void OnNavigationViewItemOnGotFocus(const winrt::IInspectable& sender, const winrt::RoutedEventArgs& e); void OnNavigationViewItemExpandedPropertyChanged(const winrt::DependencyObject& sender, const winrt::DependencyProperty& args); - void OnOverflowItemSelectionChanged(const winrt::IInspectable& sender, const winrt::SelectionChangedEventArgs& args); void RaiseSelectionChangedEvent(winrt::IInspectable const& nextItem, bool isSettingsItem, NavigationRecommendedTransitionDirection recommendedDirection = NavigationRecommendedTransitionDirection::Default); diff --git a/dev/NavigationView/NavigationView.idl b/dev/NavigationView/NavigationView.idl index 7f6475afcb..c9d6d95309 100644 --- a/dev/NavigationView/NavigationView.idl +++ b/dev/NavigationView/NavigationView.idl @@ -301,12 +301,15 @@ unsealed runtimeclass NavigationView : Windows.UI.Xaml.Controls.ContentControl [webhosthidden] [WUXC_INTERFACE_NAME("INavigationViewItemBase", edf04eb1-37d1-471f-8570-3829ee5b2bc6)] [WUXC_CONSTRUCTOR_NAME("INavigationViewItemBaseFactory", eb014cef-7890-4ebb-8245-02e8510f321d)] +[MUX_PROPERTY_CHANGED_CALLBACK_METHODNAME("OnPropertyChanged")] [default_interface] unsealed runtimeclass NavigationViewItemBase : Windows.UI.Xaml.Controls.ContentControl { [WUXC_VERSION_MUXONLY] { + [MUX_PROPERTY_CHANGED_CALLBACK(TRUE)] Boolean IsSelected{ get; set; }; + static Windows.UI.Xaml.DependencyProperty IsSelectedProperty { get; }; } } diff --git a/dev/NavigationView/NavigationViewHelper.h b/dev/NavigationView/NavigationViewHelper.h index 019a08a2a5..bcd1a69292 100644 --- a/dev/NavigationView/NavigationViewHelper.h +++ b/dev/NavigationView/NavigationViewHelper.h @@ -36,7 +36,7 @@ class NavigationViewItemHelper { } - winrt::UIElement GetSelectionIndicator() { return m_selectionIndicator.get(); } + winrt::UIElement GetSelectionIndicator() const { return m_selectionIndicator.get(); } void Init(const winrt::IControlProtected & controlProtected) { diff --git a/dev/NavigationView/NavigationViewItem.cpp b/dev/NavigationView/NavigationViewItem.cpp index e561a8282b..c929f3d918 100644 --- a/dev/NavigationView/NavigationViewItem.cpp +++ b/dev/NavigationView/NavigationViewItem.cpp @@ -27,15 +27,15 @@ static constexpr auto c_chevronHidden = L"ChevronHidden"sv; static constexpr auto c_chevronVisibleOpen = L"ChevronVisibleOpen"sv; static constexpr auto c_chevronVisibleClosed = L"ChevronVisibleClosed"sv; -void NavigationViewItem::UpdateVisualStateNoTransition() +NavigationViewItem::NavigationViewItem() { - UpdateVisualState(false /*useTransition*/); + SetDefaultStyleKey(this); + SetValue(s_MenuItemsProperty, winrt::make>()); } -void NavigationViewItem::OnNavigationViewRepeaterPositionChanged() +void NavigationViewItem::UpdateVisualStateNoTransition() { - UpdateVisualStateNoTransition(); - ReparentRepeater(); + UpdateVisualState(false /*useTransition*/); } void NavigationViewItem::OnNavigationViewItemBaseDepthChanged() @@ -44,17 +44,24 @@ void NavigationViewItem::OnNavigationViewItemBaseDepthChanged() PropagateDepthToChildren(Depth() + 1); } -NavigationViewItem::NavigationViewItem() +void NavigationViewItem::OnNavigationViewItemBaseIsSelectedChanged() { - SetDefaultStyleKey(this); - SetValue(s_MenuItemsProperty, winrt::make>()); + UpdateVisualStateForPointer(); +} + +void NavigationViewItem::OnNavigationViewItemBasePositionChanged() +{ + UpdateVisualStateNoTransition(); + ReparentRepeater(); } void NavigationViewItem::OnApplyTemplate() { - // Stop UpdateVisualState before template is applied. Otherwise the visual may not the same as we expect + // Stop UpdateVisualState before template is applied. Otherwise the visuals may be unexpected m_appliedTemplate = false; - + + UnhookEventsAndClearFields(); + NavigationViewItemBase::OnApplyTemplate(); // Find selection indicator @@ -70,26 +77,11 @@ void NavigationViewItem::OnApplyTemplate() { m_flyoutClosingRevoker = flyoutBase.Closing(winrt::auto_revoke, { this, &NavigationViewItem::OnFlyoutClosing }); } - } - winrt::UIElement const presenter = [this, controlProtected]() - { - if (auto presenter = GetTemplateChildT(c_navigationViewItemPresenterName, controlProtected)) - { - m_navigationViewItemPresenter.set(presenter); - return presenter.try_as(); - } - // We don't have a presenter, so we are our own presenter. - return this->try_as(); - }(); + HookInputEvents(controlProtected); - m_presenterPointerPressedRevoker = presenter.PointerPressed(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerPressed }); - m_presenterPointerReleasedRevoker = presenter.PointerReleased(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerReleased }); - m_presenterPointerEnteredRevoker = presenter.PointerEntered(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerEntered }); - m_presenterPointerExitedRevoker = presenter.PointerExited(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerExited }); - m_presenterPointerCanceledRevoker = presenter.PointerCanceled(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerCanceled }); - m_presenterPointerCaptureLostRevoker = presenter.PointerCaptureLost(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerCaptureLost }); + m_isEnabledChangedRevoker = IsEnabledChanged(winrt::auto_revoke, { this, &NavigationViewItem::OnIsEnabledChanged }); m_toolTip.set(GetTemplateChildT(L"ToolTip"sv, controlProtected)); @@ -126,6 +118,7 @@ void NavigationViewItem::OnApplyTemplate() m_flyoutContentGrid.set(GetTemplateChildT(c_flyoutContentGrid, controlProtected)); m_appliedTemplate = true; + UpdateItemIndentation(); UpdateVisualStateNoTransition(); ReparentRepeater(); @@ -155,7 +148,7 @@ void NavigationViewItem::UpdateRepeaterItemsSource() } } -winrt::UIElement NavigationViewItem::GetSelectionIndicator() +winrt::UIElement NavigationViewItem::GetSelectionIndicator() const { auto selectIndicator = m_helper.GetSelectionIndicator(); if (auto presenter = GetPresenter()) @@ -269,23 +262,6 @@ void NavigationViewItem::OnHasUnrealizedChildrenPropertyChanged(const winrt::Dep UpdateVisualStateForChevron(); } -bool NavigationViewItem::ShowSelectionIndicatorIfRequired() -{ - if (!IsSelected()) - { - if (!IsRepeaterVisible() && IsChildSelected()) - { - ShowSelectionIndicator(true); - return true; - } - else - { - ShowSelectionIndicator(false); - } - } - return false; -} - void NavigationViewItem::ShowSelectionIndicator(bool visible) { if (auto const selectionIndicator = GetSelectionIndicator()) @@ -433,15 +409,14 @@ void NavigationViewItem::UpdateVisualStateForPointer() // update the states for the item itself. if (auto const presenter = m_navigationViewItemPresenter.get()) { - winrt::VisualStateManager::GoToState(m_navigationViewItemPresenter.get(), enabledStateValue, true); - winrt::VisualStateManager::GoToState(m_navigationViewItemPresenter.get(), selectedStateValue, true); + winrt::VisualStateManager::GoToState(presenter, enabledStateValue, true); + winrt::VisualStateManager::GoToState(presenter, selectedStateValue, true); } else { winrt::VisualStateManager::GoToState(*this, enabledStateValue, true); winrt::VisualStateManager::GoToState(*this, selectedStateValue, true); } - } void NavigationViewItem::UpdateVisualState(bool useTransitions) @@ -461,7 +436,7 @@ void NavigationViewItem::UpdateVisualState(bool useTransitions) if (auto const presenter = m_navigationViewItemPresenter.get()) { // Backward Compatibility with RS4-, new implementation prefer IconOnLeft/IconOnly/ContentOnly - winrt::VisualStateManager::GoToState(m_navigationViewItemPresenter.get(), shouldShowIcon ? L"IconVisible" : L"IconCollapsed", useTransitions); + winrt::VisualStateManager::GoToState(presenter, shouldShowIcon ? L"IconVisible" : L"IconCollapsed", useTransitions); } } @@ -480,7 +455,7 @@ void NavigationViewItem::UpdateVisualStateForChevron() if (auto const presenter = m_navigationViewItemPresenter.get()) { auto const chevronState = HasChildren() && !(m_isClosedCompact && ShouldRepeaterShowInFlyout()) ? ( IsExpanded() ? c_chevronVisibleOpen : c_chevronVisibleClosed) : c_chevronHidden; - winrt::VisualStateManager::GoToState(m_navigationViewItemPresenter.get(), chevronState, true); + winrt::VisualStateManager::GoToState(presenter, chevronState, true); } } @@ -494,7 +469,7 @@ bool NavigationViewItem::ShouldShowIcon() return static_cast(Icon()); } -bool NavigationViewItem::ShouldEnableToolTip() +bool NavigationViewItem::ShouldEnableToolTip() const { // We may enable Tooltip for IconOnly in the future, but not now return IsOnLeftNav() && m_isClosedCompact; @@ -505,19 +480,31 @@ bool NavigationViewItem::ShouldShowContent() return static_cast(Content()); } -bool NavigationViewItem::IsOnLeftNav() +bool NavigationViewItem::IsOnLeftNav() const { return Position() == NavigationViewRepeaterPosition::LeftNav; } -bool NavigationViewItem::IsOnTopPrimary() +bool NavigationViewItem::IsOnTopPrimary() const { return Position() == NavigationViewRepeaterPosition::TopPrimary; } -NavigationViewItemPresenter * NavigationViewItem::GetPresenter() +winrt::UIElement const NavigationViewItem::GetPresenterOrItem() const +{ + if (auto const presenter = m_navigationViewItemPresenter.get()) + { + return presenter.try_as(); + } + else + { + return this->try_as(); + } +} + +NavigationViewItemPresenter* NavigationViewItem::GetPresenter() const { - NavigationViewItemPresenter * presenter = nullptr; + NavigationViewItemPresenter* presenter = nullptr; if (m_navigationViewItemPresenter) { presenter = winrt::get_self(m_navigationViewItemPresenter.get()); @@ -590,14 +577,18 @@ void NavigationViewItem::ReparentRepeater() } } -bool NavigationViewItem::ShouldRepeaterShowInFlyout() +bool NavigationViewItem::ShouldRepeaterShowInFlyout() const { return (m_isClosedCompact && IsTopLevelItem()) || IsOnTopPrimary(); } -bool NavigationViewItem::IsRepeaterVisible() +bool NavigationViewItem::IsRepeaterVisible() const { - return m_repeater.get().Visibility() == winrt::Visibility::Visible; + if (auto const repeater = m_repeater.get()) + { + return repeater.Visibility() == winrt::Visibility::Visible; + } + return false; } void NavigationViewItem::UpdateItemIndentation() @@ -690,48 +681,241 @@ void NavigationViewItem::OnLostFocus(winrt::RoutedEventArgs const& e) } } -void NavigationViewItem::OnPresenterPointerPressed(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args) +void NavigationViewItem::ResetTrackedPointerId() { + m_trackedPointerId = 0; +} + +// Returns False when the provided pointer Id matches the currently tracked Id. +// When there is no currently tracked Id, sets the tracked Id to the provided Id and returns False. +// Returns True when the provided pointer Id does not match the currently tracked Id. +bool NavigationViewItem::IgnorePointerId(const winrt::PointerRoutedEventArgs& args) +{ + uint32_t pointerId = args.Pointer().PointerId(); + + if (m_trackedPointerId == 0) + { + m_trackedPointerId = pointerId; + } + else if (m_trackedPointerId != pointerId) + { + return true; + } + return false; +} + +void NavigationViewItem::OnPresenterPointerPressed(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) +{ + if (IgnorePointerId(args)) + { + return; + } + + MUX_ASSERT(!m_isPressed); + MUX_ASSERT(!m_capturedPointer); + // TODO: Update to look at presenter instead auto pointerProperties = args.GetCurrentPoint(*this).Properties(); m_isPressed = pointerProperties.IsLeftButtonPressed() || pointerProperties.IsRightButtonPressed(); + auto pointer = args.Pointer(); + auto presenter = GetPresenterOrItem(); + + MUX_ASSERT(presenter); + + if (presenter.CapturePointer(pointer)) + { + m_capturedPointer = pointer; + } + UpdateVisualState(true); } -void NavigationViewItem::OnPresenterPointerReleased(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args) +void NavigationViewItem::OnPresenterPointerReleased(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) { - m_isPressed = false; - UpdateVisualState(true); + if (IgnorePointerId(args)) + { + return; + } + + if (m_isPressed) + { + m_isPressed = false; + + if (m_capturedPointer) + { + auto presenter = GetPresenterOrItem(); + + MUX_ASSERT(presenter); + + presenter.ReleasePointerCapture(m_capturedPointer); + } + + UpdateVisualState(true); + } } -void NavigationViewItem::OnPresenterPointerEntered(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args) +void NavigationViewItem::OnPresenterPointerEntered(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) { - m_isPointerOver = true; - UpdateVisualState(true); + ProcessPointerOver(args); +} + +void NavigationViewItem::OnPresenterPointerMoved(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) +{ + ProcessPointerOver(args); } -void NavigationViewItem::OnPresenterPointerExited(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args) +void NavigationViewItem::OnPresenterPointerExited(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) { + if (IgnorePointerId(args)) + { + return; + } + m_isPointerOver = false; + + if (!m_capturedPointer) + { + ResetTrackedPointerId(); + } + UpdateVisualState(true); } -void NavigationViewItem::OnPresenterPointerCanceled(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args) +void NavigationViewItem::OnPresenterPointerCanceled(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) { - m_isPressed = false; - m_isPointerOver = false; + ProcessPointerCanceled(args); +} + +void NavigationViewItem::OnPresenterPointerCaptureLost(const winrt::IInspectable&, const winrt::PointerRoutedEventArgs& args) +{ + ProcessPointerCanceled(args); +} + +void NavigationViewItem::OnIsEnabledChanged(const winrt::IInspectable&, const winrt::DependencyPropertyChangedEventArgs&) +{ + if (!IsEnabled()) + { + m_isPressed = false; + m_isPointerOver = false; + + if (m_capturedPointer) + { + auto presenter = GetPresenterOrItem(); + + MUX_ASSERT(presenter); + + presenter.ReleasePointerCapture(m_capturedPointer); + m_capturedPointer = nullptr; + } + + ResetTrackedPointerId(); + } + UpdateVisualState(true); } -void NavigationViewItem::OnPresenterPointerCaptureLost(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args) +void NavigationViewItem::RotateExpandCollapseChevron(bool isExpanded) +{ + if (auto presenter = GetPresenter()) + { + presenter->RotateExpandCollapseChevron(isExpanded); + } +} + +void NavigationViewItem::ProcessPointerCanceled(const winrt::PointerRoutedEventArgs& args) { + if (IgnorePointerId(args)) + { + return; + } + m_isPressed = false; m_isPointerOver = false; + m_capturedPointer = nullptr; + ResetTrackedPointerId(); UpdateVisualState(true); } -void NavigationViewItem::RotateExpandCollapseChevron(bool isExpanded) +void NavigationViewItem::ProcessPointerOver(const winrt::PointerRoutedEventArgs& args) +{ + if (IgnorePointerId(args)) + { + return; + } + + if (!m_isPointerOver) + { + m_isPointerOver = true; + UpdateVisualState(true); + } +} + +void NavigationViewItem::HookInputEvents(const winrt::IControlProtected& controlProtected) { - winrt::get_self(m_navigationViewItemPresenter.get())->RotateExpandCollapseChevron(isExpanded); + winrt::UIElement const presenter = [this, controlProtected]() + { + if (auto presenter = GetTemplateChildT(c_navigationViewItemPresenterName, controlProtected)) + { + m_navigationViewItemPresenter.set(presenter); + return presenter.try_as(); + } + // We don't have a presenter, so we are our own presenter. + return this->try_as(); + }(); + + MUX_ASSERT(presenter); + + // Handlers that set flags are skipped when args.Handled is already True. + m_presenterPointerPressedRevoker = presenter.PointerPressed(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerPressed }); + m_presenterPointerEnteredRevoker = presenter.PointerEntered(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerEntered }); + m_presenterPointerMovedRevoker = presenter.PointerMoved(winrt::auto_revoke, { this, &NavigationViewItem::OnPresenterPointerMoved }); + + // Handlers that reset flags are not skipped when args.Handled is already True to avoid broken states. + m_presenterPointerReleasedRevoker = AddRoutedEventHandler( + presenter, + { this, &NavigationViewItem::OnPresenterPointerReleased }, + true /*handledEventsToo*/); + m_presenterPointerExitedRevoker = AddRoutedEventHandler( + presenter, + { this, &NavigationViewItem::OnPresenterPointerExited }, + true /*handledEventsToo*/); + m_presenterPointerCanceledRevoker = AddRoutedEventHandler( + presenter, + { this, &NavigationViewItem::OnPresenterPointerCanceled }, + true /*handledEventsToo*/); + m_presenterPointerCaptureLostRevoker = AddRoutedEventHandler( + presenter, + { this, &NavigationViewItem::OnPresenterPointerCaptureLost }, + true /*handledEventsToo*/); +} + +void NavigationViewItem::UnhookInputEvents() +{ + m_presenterPointerPressedRevoker.revoke(); + m_presenterPointerEnteredRevoker.revoke(); + m_presenterPointerMovedRevoker.revoke(); + m_presenterPointerReleasedRevoker.revoke(); + m_presenterPointerExitedRevoker.revoke(); + m_presenterPointerCanceledRevoker.revoke(); + m_presenterPointerCaptureLostRevoker.revoke(); +} + +void NavigationViewItem::UnhookEventsAndClearFields() +{ + UnhookInputEvents(); + + m_flyoutClosingRevoker.revoke(); + m_splitViewIsPaneOpenChangedRevoker.revoke(); + m_splitViewDisplayModeChangedRevoker.revoke(); + m_splitViewCompactPaneLengthChangedRevoker.revoke(); + m_repeaterElementPreparedRevoker.revoke(); + m_repeaterElementClearingRevoker.revoke(); + m_isEnabledChangedRevoker.revoke(); + + m_rootGrid.set(nullptr); + m_navigationViewItemPresenter.set(nullptr); + m_toolTip.set(nullptr); + m_repeater.set(nullptr); + m_flyoutContentGrid.set(nullptr); } diff --git a/dev/NavigationView/NavigationViewItem.h b/dev/NavigationView/NavigationViewItem.h index 67bafcd456..99978e0117 100644 --- a/dev/NavigationView/NavigationViewItem.h +++ b/dev/NavigationView/NavigationViewItem.h @@ -32,8 +32,7 @@ class NavigationViewItem : void OnMenuItemsSourcePropertyChanged(const winrt::DependencyPropertyChangedEventArgs& args); void OnHasUnrealizedChildrenPropertyChanged(const winrt::DependencyPropertyChangedEventArgs& args); - winrt::UIElement GetSelectionIndicator(); - winrt::ToolTip GetToolTip(); + winrt::UIElement GetSelectionIndicator() const; // IUIElement / IUIElementOverridesHelper winrt::AutomationPeer OnCreateAutomationPeer() override; @@ -50,35 +49,37 @@ class NavigationViewItem : // It provides a chance for NavigationViewItemPresenter to request visualstate refresh void UpdateVisualStateNoTransition(); - void OnPresenterPointerPressed(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); - void OnPresenterPointerReleased(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); - void OnPresenterPointerEntered(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); - void OnPresenterPointerExited(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); - void OnPresenterPointerCanceled(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); - void OnPresenterPointerCaptureLost(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); - void ShowHideChildren(); - bool ShouldRepeaterShowInFlyout(); - - winrt::ItemsRepeater GetRepeater() { return m_repeater.get(); }; + bool ShouldRepeaterShowInFlyout() const; - NavigationViewItemPresenter* GetPresenter(); + winrt::ItemsRepeater GetRepeater() const { return m_repeater.get(); }; void OnExpandCollapseChevronTapped(const winrt::IInspectable& sender, const winrt::TappedRoutedEventArgs& args); - bool ShowSelectionIndicatorIfRequired(); void RotateExpandCollapseChevron(bool isExpanded); - bool IsRepeaterVisible(); + bool IsRepeaterVisible() const; void PropagateDepthToChildren(int depth); private: + winrt::UIElement const GetPresenterOrItem() const; + NavigationViewItemPresenter* GetPresenter() const; + void UpdateNavigationViewItemToolTip(); void SuggestedToolTipChanged(winrt::IInspectable const& newContent); - void OnNavigationViewRepeaterPositionChanged() override; void OnNavigationViewItemBaseDepthChanged() override; + void OnNavigationViewItemBaseIsSelectedChanged() override; + void OnNavigationViewItemBasePositionChanged() override; - void OnLoaded(const winrt::IInspectable& sender, const winrt::RoutedEventArgs& args); - void OnUnloaded(const winrt::IInspectable& sender, const winrt::RoutedEventArgs& args); + void OnPresenterPointerPressed(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnPresenterPointerReleased(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnPresenterPointerEntered(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnPresenterPointerMoved(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnPresenterPointerExited(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnPresenterPointerCanceled(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnPresenterPointerCaptureLost(const winrt::IInspectable& sender, const winrt::PointerRoutedEventArgs& args); + void OnIsEnabledChanged(const winrt::IInspectable& sender, const winrt::DependencyPropertyChangedEventArgs& args); + void ResetTrackedPointerId(); + bool IgnorePointerId(const winrt::PointerRoutedEventArgs& args); void OnSplitViewPropertyChanged(const winrt::DependencyObject& sender, const winrt::DependencyProperty& args); void UpdateCompactPaneLength(); void UpdateIsClosedCompact(); @@ -93,33 +94,39 @@ class NavigationViewItem : void UpdateVisualState(bool useTransitions); bool ShouldShowIcon(); bool ShouldShowContent(); - bool ShouldEnableToolTip(); - bool IsOnLeftNav(); - bool IsOnTopPrimary(); + bool ShouldEnableToolTip() const; + bool IsOnLeftNav() const; + bool IsOnTopPrimary() const; bool HasChildren(); void UpdateRepeaterItemsSource(); void ReparentRepeater(); - void ReparentContent(); void OnFlyoutClosing(const winrt::IInspectable& sender, const winrt::FlyoutBaseClosingEventArgs& args); void UpdateItemIndentation(); void ShowSelectionIndicator(bool visible); + void ProcessPointerCanceled(const winrt::PointerRoutedEventArgs& args); + void ProcessPointerOver(const winrt::PointerRoutedEventArgs& args); + void HookInputEvents(const winrt::IControlProtected& controlProtected); + void UnhookInputEvents(); + void UnhookEventsAndClearFields(); PropertyChanged_revoker m_splitViewIsPaneOpenChangedRevoker{}; PropertyChanged_revoker m_splitViewDisplayModeChangedRevoker{}; PropertyChanged_revoker m_splitViewCompactPaneLengthChangedRevoker{}; winrt::UIElement::PointerPressed_revoker m_presenterPointerPressedRevoker{}; - winrt::UIElement::PointerReleased_revoker m_presenterPointerReleasedRevoker{}; winrt::UIElement::PointerEntered_revoker m_presenterPointerEnteredRevoker{}; - winrt::UIElement::PointerExited_revoker m_presenterPointerExitedRevoker{}; - winrt::UIElement::PointerCanceled_revoker m_presenterPointerCanceledRevoker{}; - winrt::UIElement::PointerCaptureLost_revoker m_presenterPointerCaptureLostRevoker{}; + winrt::UIElement::PointerMoved_revoker m_presenterPointerMovedRevoker{}; + RoutedEventHandler_revoker m_presenterPointerReleasedRevoker{}; + RoutedEventHandler_revoker m_presenterPointerExitedRevoker{}; + RoutedEventHandler_revoker m_presenterPointerCanceledRevoker{}; + RoutedEventHandler_revoker m_presenterPointerCaptureLostRevoker{}; winrt::ItemsRepeater::ElementPrepared_revoker m_repeaterElementPreparedRevoker{}; winrt::ItemsRepeater::ElementClearing_revoker m_repeaterElementClearingRevoker{}; winrt::FlyoutBase::Closing_revoker m_flyoutClosingRevoker{}; + winrt::Control::IsEnabledChanged_revoker m_isEnabledChangedRevoker{}; tracker_ref m_toolTip{ this }; NavigationViewItemHelper m_helper{ this }; @@ -135,6 +142,8 @@ class NavigationViewItem : bool m_hasKeyboardFocus{ false }; // Visual state tracking + winrt::Pointer m_capturedPointer{ nullptr }; + uint32_t m_trackedPointerId{ 0 }; bool m_isPressed{ false }; bool m_isPointerOver{ false }; diff --git a/dev/NavigationView/NavigationViewItemBase.cpp b/dev/NavigationView/NavigationViewItemBase.cpp index d28e719b35..c8b9d289e7 100644 --- a/dev/NavigationView/NavigationViewItemBase.cpp +++ b/dev/NavigationView/NavigationViewItemBase.cpp @@ -7,7 +7,7 @@ #include "NavigationView.h" #include "IndexPath.h" -NavigationViewRepeaterPosition NavigationViewItemBase::Position() +NavigationViewRepeaterPosition NavigationViewItemBase::Position() const { return m_position; } @@ -17,27 +17,30 @@ void NavigationViewItemBase::Position(NavigationViewRepeaterPosition value) if (m_position != value) { m_position = value; - OnNavigationViewRepeaterPositionChanged(); + OnNavigationViewItemBasePositionChanged(); } } -winrt::NavigationView NavigationViewItemBase::GetNavigationView() +winrt::NavigationView NavigationViewItemBase::GetNavigationView() const { return m_navigationView.get(); } void NavigationViewItemBase::Depth(int depth) { - m_depth = depth; - OnNavigationViewItemBaseDepthChanged(); + if (m_depth != depth) + { + m_depth = depth; + OnNavigationViewItemBaseDepthChanged(); + } } -int NavigationViewItemBase::Depth() +int NavigationViewItemBase::Depth() const { return m_depth; } -winrt::SplitView NavigationViewItemBase::GetSplitView() +winrt::SplitView NavigationViewItemBase::GetSplitView() const { winrt::SplitView splitView{ nullptr }; auto navigationView = GetNavigationView(); @@ -52,3 +55,11 @@ void NavigationViewItemBase::SetNavigationViewParent(winrt::NavigationView const { m_navigationView = winrt::make_weak(navigationView); } + +void NavigationViewItemBase::OnPropertyChanged(const winrt::DependencyPropertyChangedEventArgs& args) +{ + if (args.Property() == s_IsSelectedProperty) + { + OnNavigationViewItemBaseIsSelectedChanged(); + } +} diff --git a/dev/NavigationView/NavigationViewItemBase.h b/dev/NavigationView/NavigationViewItemBase.h index 478131d622..d9aba6d09a 100644 --- a/dev/NavigationView/NavigationViewItemBase.h +++ b/dev/NavigationView/NavigationViewItemBase.h @@ -43,23 +43,27 @@ class NavigationViewItemBase : __super::OnLostFocus(e); } - NavigationViewRepeaterPosition Position(); + void OnPropertyChanged(const winrt::DependencyPropertyChangedEventArgs& args); + + NavigationViewRepeaterPosition Position() const; void Position(NavigationViewRepeaterPosition value); - virtual void OnNavigationViewRepeaterPositionChanged() {} + virtual void OnNavigationViewItemBasePositionChanged() {} void Depth(int depth); - int Depth(); + int Depth() const; virtual void OnNavigationViewItemBaseDepthChanged() {} - - winrt::NavigationView GetNavigationView(); - winrt::SplitView GetSplitView(); + + virtual void OnNavigationViewItemBaseIsSelectedChanged() {} + + winrt::NavigationView GetNavigationView() const; + winrt::SplitView GetSplitView() const; void SetNavigationViewParent(winrt::NavigationView const& navigationView); - // TODO: Constant is a temporary mesure. Potentially expose using TemplateSettings. + // TODO: Constant is a temporary measure. Potentially expose using TemplateSettings. static constexpr int c_itemIndentation = 25; void IsTopLevelItem(bool isTopLevelItem) { m_isTopLevelItem = isTopLevelItem; }; - bool IsTopLevelItem() { return m_isTopLevelItem; }; + bool IsTopLevelItem() const { return m_isTopLevelItem; }; protected: diff --git a/dev/NavigationView/NavigationViewItemSeparator.cpp b/dev/NavigationView/NavigationViewItemSeparator.cpp index 5f14046b5b..6df9405dc3 100644 --- a/dev/NavigationView/NavigationViewItemSeparator.cpp +++ b/dev/NavigationView/NavigationViewItemSeparator.cpp @@ -26,7 +26,7 @@ void NavigationViewItemSeparator::UpdateVisualState(bool useTransitions) : L"HorizontalLine"sv : L"VerticalLine"sv; - VisualStateUtil::GotToStateIfGroupExists(*this, groupName, stateName, false /*useTransitions*/); + VisualStateUtil::GoToStateIfGroupExists(*this, groupName, stateName, false /*useTransitions*/); } } @@ -56,14 +56,14 @@ void NavigationViewItemSeparator::OnApplyTemplate() UpdateItemIndentation(); } -void NavigationViewItemSeparator::OnNavigationViewRepeaterPositionChanged() +void NavigationViewItemSeparator::OnNavigationViewItemBaseDepthChanged() { - UpdateVisualState(false /*useTransition*/); + UpdateItemIndentation(); } -void NavigationViewItemSeparator::OnNavigationViewItemBaseDepthChanged() +void NavigationViewItemSeparator::OnNavigationViewItemBasePositionChanged() { - UpdateItemIndentation(); + UpdateVisualState(false /*useTransition*/); } void NavigationViewItemSeparator::OnSplitViewPropertyChanged(const winrt::DependencyObject& /*sender*/, const winrt::DependencyProperty& /*args*/) diff --git a/dev/NavigationView/NavigationViewItemSeparator.h b/dev/NavigationView/NavigationViewItemSeparator.h index 9ea2031c73..21b864d253 100644 --- a/dev/NavigationView/NavigationViewItemSeparator.h +++ b/dev/NavigationView/NavigationViewItemSeparator.h @@ -16,7 +16,7 @@ class NavigationViewItemSeparator : void OnApplyTemplate() override; private: - void OnNavigationViewRepeaterPositionChanged() override; + void OnNavigationViewItemBasePositionChanged() override; void OnNavigationViewItemBaseDepthChanged() override; void UpdateVisualState(bool useTransitions); void UpdateItemIndentation(); diff --git a/dev/NavigationView/NavigationView_ApiTests/NavigationViewTests.cs b/dev/NavigationView/NavigationView_ApiTests/NavigationViewTests.cs index 893263b993..a3f1c295c5 100644 --- a/dev/NavigationView/NavigationView_ApiTests/NavigationViewTests.cs +++ b/dev/NavigationView/NavigationView_ApiTests/NavigationViewTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. using MUXControlsTestApp.Utilities; @@ -28,6 +28,7 @@ using NavigationViewBackButtonVisible = Microsoft.UI.Xaml.Controls.NavigationViewBackButtonVisible; using System.Collections.ObjectModel; using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Composition; namespace Windows.UI.Xaml.Tests.MUXControls.ApiTests { @@ -416,13 +417,19 @@ public void VerifyPaneProperties() [TestMethod] public void VerifySingleSelection() { + string navItemPresenter1CurrentState = string.Empty; + string navItemPresenter2CurrentState = string.Empty; + NavigationView navView = null; + NavigationViewItem menuItem1 = null; + NavigationViewItem menuItem2 = null; + RunOnUIThread.Execute(() => { - var navView = new NavigationView(); + navView = new NavigationView(); Content = navView; - var menuItem1 = new NavigationViewItem(); - var menuItem2 = new NavigationViewItem(); + menuItem1 = new NavigationViewItem(); + menuItem2 = new NavigationViewItem(); menuItem1.Content = "Item 1"; menuItem2.Content = "Item 2"; @@ -431,23 +438,69 @@ public void VerifySingleSelection() navView.Width = 1008; // forces the control into Expanded mode so that the menu renders Content.UpdateLayout(); + var menuItemLayoutRoot = VisualTreeHelper.GetChild(menuItem1, 0) as FrameworkElement; + var navItemPresenter = VisualTreeHelper.GetChild(menuItemLayoutRoot, 0) as FrameworkElement; + var navItemPresenterLayoutRoot = VisualTreeHelper.GetChild(navItemPresenter, 0) as FrameworkElement; + var statesGroups = VisualStateManager.GetVisualStateGroups(navItemPresenterLayoutRoot); + + foreach (var visualStateGroup in statesGroups) + { + Log.Comment($"VisualStateGroup1: Name={visualStateGroup.Name}, CurrentState={visualStateGroup.CurrentState.Name}"); + + visualStateGroup.CurrentStateChanged += (object sender, VisualStateChangedEventArgs e) => + { + Log.Comment($"VisualStateChangedEventArgs1: Name={e.Control.Name}, OldState={e.OldState.Name}, NewState={e.NewState.Name}"); + navItemPresenter1CurrentState = e.NewState.Name; + }; + } + + menuItemLayoutRoot = VisualTreeHelper.GetChild(menuItem2, 0) as FrameworkElement; + navItemPresenter = VisualTreeHelper.GetChild(menuItemLayoutRoot, 0) as FrameworkElement; + navItemPresenterLayoutRoot = VisualTreeHelper.GetChild(navItemPresenter, 0) as FrameworkElement; + statesGroups = VisualStateManager.GetVisualStateGroups(navItemPresenterLayoutRoot); + + foreach (var visualStateGroup in statesGroups) + { + Log.Comment($"VisualStateGroup2: Name={visualStateGroup.Name}, CurrentState={visualStateGroup.CurrentState.Name}"); + + visualStateGroup.CurrentStateChanged += (object sender, VisualStateChangedEventArgs e) => + { + Log.Comment($"VisualStateChangedEventArgs2: Name={e.Control.Name}, OldState={e.OldState.Name}, NewState={e.NewState.Name}"); + navItemPresenter2CurrentState = e.NewState.Name; + }; + } + Verify.IsFalse(menuItem1.IsSelected); Verify.IsFalse(menuItem2.IsSelected); - Verify.AreEqual(navView.SelectedItem, null); + Verify.AreEqual(null, navView.SelectedItem); menuItem1.IsSelected = true; Content.UpdateLayout(); + }); + IdleSynchronizer.Wait(); + + RunOnUIThread.Execute(() => + { + Verify.AreEqual("Selected", navItemPresenter1CurrentState); + Verify.AreEqual(string.Empty, navItemPresenter2CurrentState); Verify.IsTrue(menuItem1.IsSelected); Verify.IsFalse(menuItem2.IsSelected); - Verify.AreEqual(navView.SelectedItem, menuItem1); + Verify.AreEqual(menuItem1, navView.SelectedItem); menuItem2.IsSelected = true; Content.UpdateLayout(); + }); + IdleSynchronizer.Wait(); + + RunOnUIThread.Execute(() => + { + Verify.AreEqual("Normal", navItemPresenter1CurrentState); + Verify.AreEqual("Selected", navItemPresenter2CurrentState); Verify.IsTrue(menuItem2.IsSelected); Verify.IsFalse(menuItem1.IsSelected, "MenuItem1 should have been deselected when MenuItem2 was selected"); - Verify.AreEqual(navView.SelectedItem, menuItem2); + Verify.AreEqual(menuItem2, navView.SelectedItem); }); } diff --git a/dev/NavigationView/TestUI/NavigationViewPage.xaml b/dev/NavigationView/TestUI/NavigationViewPage.xaml index e79c338beb..7bbf2d7685 100644 --- a/dev/NavigationView/TestUI/NavigationViewPage.xaml +++ b/dev/NavigationView/TestUI/NavigationViewPage.xaml @@ -186,6 +186,7 @@ + diff --git a/dev/NavigationView/TestUI/NavigationViewPage.xaml.cs b/dev/NavigationView/TestUI/NavigationViewPage.xaml.cs index 250f0a1f9a..8770783f27 100644 --- a/dev/NavigationView/TestUI/NavigationViewPage.xaml.cs +++ b/dev/NavigationView/TestUI/NavigationViewPage.xaml.cs @@ -202,6 +202,22 @@ private void SelectedItemCombobox_SelectionChanged(object sender, SelectionChang } } + private void SelectedItemIsEnabledCheckbox_Checked(object sender, RoutedEventArgs e) + { + if (NavView.SelectedItem != null) + { + (NavView.SelectedItem as Control).IsEnabled = true; + } + } + + private void SelectedItemIsEnabledCheckbox_Unchecked(object sender, RoutedEventArgs e) + { + if (NavView.SelectedItem != null) + { + (NavView.SelectedItem as Control).IsEnabled = false; + } + } + private void SettingsItemVisibilityCheckbox_Checked(object sender, RoutedEventArgs e) { NavView.IsSettingsVisible = true; @@ -397,6 +413,15 @@ private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelec { SelectionChangedItemContainerType.Text = args.SelectedItemContainer.GetType().ToString(); } + + if (NavView.SelectedItem == null) + { + SelectedItemIsEnabledCheckbox.IsChecked = null; + } + else + { + SelectedItemIsEnabledCheckbox.IsChecked = (NavView.SelectedItem as Control).IsEnabled; + } } private void ClearSelectionChangeBlock(object sender,RoutedEventArgs e) diff --git a/dev/ResourceHelper/Utils.cpp b/dev/ResourceHelper/Utils.cpp index 1b4b5636ce..a0ebf5cab6 100644 --- a/dev/ResourceHelper/Utils.cpp +++ b/dev/ResourceHelper/Utils.cpp @@ -96,7 +96,7 @@ bool VisualStateUtil::VisualStateGroupExists(const winrt::FrameworkElement& cont return static_cast(GetVisualStateGroup(control, groupName)); } -void VisualStateUtil::GotToStateIfGroupExists(const winrt::Control& control, const std::wstring_view& groupName, const std::wstring_view& stateName, bool useTransitions) +void VisualStateUtil::GoToStateIfGroupExists(const winrt::Control& control, const std::wstring_view& groupName, const std::wstring_view& stateName, bool useTransitions) { auto visualStateGroup = GetVisualStateGroup(control, groupName); if (visualStateGroup) diff --git a/dev/ResourceHelper/Utils.h b/dev/ResourceHelper/Utils.h index e332c772f1..6964ac2af2 100644 --- a/dev/ResourceHelper/Utils.h +++ b/dev/ResourceHelper/Utils.h @@ -25,7 +25,7 @@ class VisualStateUtil public: static winrt::VisualStateGroup GetVisualStateGroup(const winrt::FrameworkElement& control, const std::wstring_view& groupName); static bool VisualStateGroupExists(const winrt::FrameworkElement& control, const std::wstring_view& groupName); - static void GotToStateIfGroupExists(const winrt::Control& control, const std::wstring_view& groupName, const std::wstring_view& stateName, bool useTransitions); + static void GoToStateIfGroupExists(const winrt::Control& control, const std::wstring_view& groupName, const std::wstring_view& stateName, bool useTransitions); }; namespace LayoutUtils @@ -55,4 +55,4 @@ namespace Util { return visible ? winrt::Visibility::Visible : winrt::Visibility::Collapsed; } -} \ No newline at end of file +} diff --git a/dev/TabView/TestUI/TabViewPage.xaml b/dev/TabView/TestUI/TabViewPage.xaml index abf9084f67..5d515fbf89 100644 --- a/dev/TabView/TestUI/TabViewPage.xaml +++ b/dev/TabView/TestUI/TabViewPage.xaml @@ -153,12 +153,14 @@ - - + + + + diff --git a/dev/inc/RoutedEventHelpers.h b/dev/inc/RoutedEventHelpers.h index 1f2154d622..e8c96acc4f 100644 --- a/dev/inc/RoutedEventHelpers.h +++ b/dev/inc/RoutedEventHelpers.h @@ -81,7 +81,11 @@ enum class RoutedEventType GettingFocus, LosingFocus, KeyDown, - PointerPressed + PointerPressed, + PointerReleased, + PointerExited, + PointerCanceled, + PointerCaptureLost }; template @@ -117,6 +121,34 @@ struct RoutedEventTraits using HandlerT = winrt::PointerEventHandler; }; +template <> +struct RoutedEventTraits +{ + static winrt::RoutedEvent Event() { return winrt::UIElement::PointerReleasedEvent(); } + using HandlerT = winrt::PointerEventHandler; +}; + +template <> +struct RoutedEventTraits +{ + static winrt::RoutedEvent Event() { return winrt::UIElement::PointerExitedEvent(); } + using HandlerT = winrt::PointerEventHandler; +}; + +template <> +struct RoutedEventTraits +{ + static winrt::RoutedEvent Event() { return winrt::UIElement::PointerCanceledEvent(); } + using HandlerT = winrt::PointerEventHandler; +}; + +template <> +struct RoutedEventTraits +{ + static winrt::RoutedEvent Event() { return winrt::UIElement::PointerCaptureLostEvent(); } + using HandlerT = winrt::PointerEventHandler; +}; + template> inline RoutedEventHandler_revoker AddRoutedEventHandler(winrt::UIElement const& object, typename traits::HandlerT const& callback, bool handledEventsToo) {