mirror of
https://github.com/microsoft/microsoft-ui-xaml.git
synced 2026-10-28 16:35:19 +08:00
Brings in #12307 (TableView: templated column headers, UIA resize, sort fixes, recycle perf, theming) and 11 other commits from main. Conflicts, resolved by keeping both sides: - TableView_Editing.cpp (BeginEdit): the TiP try/catch now wraps #12307's unwind-on-throw guard and ResetPendingBeginEdit. - TableView_Sort.cpp: the typed Sorted rethrow and failure capture keep #12307's explicitClear, keptSourceSort and reconcile changes, and the comparer tally reset follows its early return for a source another table owns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 53d996ee-1cb6-468a-bfc9-e11af03b1f80
3306 lines
112 KiB
C++
3306 lines
112 KiB
C++
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License. See LICENSE in the project root for license information.
|
|
|
|
#include "pch.h"
|
|
#include "common.h"
|
|
#include "TableViewRow.h"
|
|
#include "TableViewToolTipHelpers.h"
|
|
#include "TableViewFocusHelpers.h"
|
|
#include "TableView.h"
|
|
#include "TableViewColumn.h"
|
|
#include "TableViewCellsPanel.h"
|
|
#include "TableViewRowAutomationPeer.h"
|
|
#include "TableViewCellAutomationPeer.h"
|
|
#include "TableViewAutomationHelpers.h"
|
|
#include "TableViewCell.h"
|
|
#include "SharedHelpers.h"
|
|
#include "TVDiag.h"
|
|
|
|
#include <algorithm>
|
|
#include <vector>
|
|
|
|
static constexpr std::wstring_view s_CellsHostPartName{ L"PART_CellsHost"sv };
|
|
static constexpr std::wstring_view s_GridLineBorderPartName{ L"PART_GridLineBorder"sv };
|
|
static constexpr std::wstring_view s_RowExpanderGutterPartName{ L"PART_RowExpanderGutter"sv };
|
|
|
|
// Used only when there is no owner to ask; mirrors the TableViewRowExpanderSize resource.
|
|
static constexpr double c_defaultRowExpanderSize{ 24.0 };
|
|
|
|
// Mirrors the TableViewRowIndentSize resource. Used only when there is no owner to ask.
|
|
static constexpr double c_defaultRowIndentSize{ 16.0 };
|
|
|
|
namespace
|
|
{
|
|
constexpr winrt::Thickness s_verticalThickness{ 0, 0, 1, 0 };
|
|
constexpr winrt::Thickness s_zeroThickness{ 0, 0, 0, 0 };
|
|
|
|
bool WantsHorizontalLines(winrt::TableViewGridLinesVisibility visibility) noexcept
|
|
{
|
|
return visibility == winrt::TableViewGridLinesVisibility::Horizontal ||
|
|
visibility == winrt::TableViewGridLinesVisibility::All;
|
|
}
|
|
|
|
bool WantsVerticalLines(winrt::TableViewGridLinesVisibility visibility) noexcept
|
|
{
|
|
return visibility == winrt::TableViewGridLinesVisibility::Vertical ||
|
|
visibility == winrt::TableViewGridLinesVisibility::All;
|
|
}
|
|
|
|
// Failure path only: the element a column factory returned already sits in another parent. The logical
|
|
// parent is checked first: an element held by a container outside the live tree has no visual parent yet.
|
|
bool HasForeignParent(winrt::FrameworkElement const& element, winrt::DependencyObject const& host) noexcept
|
|
{
|
|
try
|
|
{
|
|
if (!element) { return false; }
|
|
auto parent = element.Parent();
|
|
if (!parent) { parent = winrt::VisualTreeHelper::GetParent(element); }
|
|
return parent && parent != host;
|
|
}
|
|
catch (...) { return false; }
|
|
}
|
|
|
|
// These run from OnDataContextChanged inside the rows list measure. A redundant write still
|
|
// invalidates measure, which repeated per row per pass trips the layout-cycle failfast, so every
|
|
// layout-affecting write on that path must be a no-op when the value is already in place.
|
|
void ClearLocalValueIfSet(winrt::DependencyObject const& target, winrt::DependencyProperty const& property)
|
|
{
|
|
if (target.ReadLocalValue(property) != winrt::DependencyProperty::UnsetValue())
|
|
{
|
|
target.ClearValue(property);
|
|
}
|
|
}
|
|
|
|
void SetThicknessIfChanged(
|
|
winrt::DependencyObject const& target,
|
|
winrt::DependencyProperty const& property,
|
|
winrt::Thickness const& value)
|
|
{
|
|
auto const current = target.ReadLocalValue(property);
|
|
if (current == winrt::DependencyProperty::UnsetValue() ||
|
|
winrt::unbox_value_or<winrt::Thickness>(current, winrt::Thickness{ -1, -1, -1, -1 }) != value)
|
|
{
|
|
target.SetValue(property, winrt::box_value(value));
|
|
}
|
|
}
|
|
|
|
// Slop allowed between the two presses of a double-click, in DIPs.
|
|
//
|
|
// SM_CXDOUBLECLK is in PHYSICAL pixels, so it must be divided by the element's rasterization
|
|
// scale to be comparable with the DIP-space positions this gesture works in. Without that the
|
|
// effective tolerance halves at 200% - a user whose two presses land 6 DIPs apart is inside the
|
|
// system tolerance but outside ours, and the double-click is silently dropped.
|
|
//
|
|
// Read per press rather than cached: the metric changes with the mouse control panel and the
|
|
// scale changes when the window moves between monitors, and a cached value never sees either.
|
|
// GetSystemMetrics is a cheap cached read in user32, not a round trip.
|
|
double GetDoubleClickSlop(winrt::UIElement const& element)
|
|
{
|
|
const auto cx = static_cast<double>(::GetSystemMetrics(SM_CXDOUBLECLK));
|
|
const auto cy = static_cast<double>(::GetSystemMetrics(SM_CYDOUBLECLK));
|
|
const double physical = (std::max)((std::max)(cx, cy), 4.0) / 2.0;
|
|
|
|
double scale = 1.0;
|
|
if (element)
|
|
{
|
|
if (auto const root = element.XamlRoot())
|
|
{
|
|
const auto rasterization = root.RasterizationScale();
|
|
if (rasterization > 0.0)
|
|
{
|
|
scale = rasterization;
|
|
}
|
|
}
|
|
}
|
|
|
|
return physical / scale;
|
|
}
|
|
|
|
// PointerPoint.Timestamp is in microseconds; the system double-click time is in milliseconds.
|
|
// Not cached, so a change made in the mouse control panel takes effect immediately.
|
|
uint64_t GetDoubleClickIntervalMicroseconds()
|
|
{
|
|
return static_cast<uint64_t>(::GetDoubleClickTime()) * 1000ull;
|
|
}
|
|
|
|
bool IsKeyDown(winrt::VirtualKey key)
|
|
{
|
|
return (winrt::InputKeyboardSource::GetKeyStateForCurrentThread(key) &
|
|
winrt::CoreVirtualKeyStates::Down) == winrt::CoreVirtualKeyStates::Down;
|
|
}
|
|
|
|
// Collects tab stops BELOW a cell wrapper (the wrapper is owned by SetCellsTabStopInternal).
|
|
// Keeps descending past a found tab stop: a focusable container can hold more.
|
|
constexpr int32_t c_maxCellContentTabStopDepth = 16;
|
|
|
|
void AppendContentTabStops(
|
|
winrt::DependencyObject const& parent, std::vector<winrt::UIElement>& tabStops, int32_t depthBudget)
|
|
{
|
|
if (depthBudget <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const int32_t count = winrt::VisualTreeHelper::GetChildrenCount(parent);
|
|
for (int32_t i = 0; i < count && i < c_maxCellContentChildrenPerLevel; ++i)
|
|
{
|
|
auto const child = winrt::VisualTreeHelper::GetChild(parent, i);
|
|
if (!child)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (auto const element = child.try_as<winrt::UIElement>(); element && element.IsTabStop())
|
|
{
|
|
tabStops.push_back(element);
|
|
}
|
|
|
|
AppendContentTabStops(child, tabStops, depthBudget - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
TableViewRow::TableViewRow()
|
|
{
|
|
SetDefaultStyleKey(this);
|
|
|
|
auto weakRow = get_weak();
|
|
|
|
// handledEventsToo: a row that participates in selection marks the press handled, which also
|
|
// suppresses XAML's gesture recognizer - this registration is what keeps begin-edit reachable.
|
|
m_editingPointerPressedHandler = winrt::PointerEventHandler(
|
|
[weakRow](winrt::IInspectable const& sender, winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->OnPointerPressedForEditing(sender, args);
|
|
}
|
|
});
|
|
AddHandler(winrt::UIElement::PointerPressedEvent(), winrt::box_value(m_editingPointerPressedHandler), true /* handledEventsToo */);
|
|
|
|
// auto_revoke owns this self-event subscription until row destruction.
|
|
m_dataContextChangedRevoker = DataContextChanged(
|
|
winrt::auto_revoke,
|
|
[weakRow](winrt::FrameworkElement const& sender, winrt::DataContextChangedEventArgs const& args)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->OnDataContextChanged(sender, args);
|
|
}
|
|
});
|
|
|
|
// Keep CommonStates VSM in sync with IsEnabled so the Disabled
|
|
// state activates when consumers toggle row IsEnabled at runtime.
|
|
// auto_revoke owns this self-event subscription until row destruction.
|
|
m_isEnabledChangedRevoker = IsEnabledChanged(
|
|
winrt::auto_revoke,
|
|
[weakRow](winrt::IInspectable const& sender, winrt::DependencyPropertyChangedEventArgs const& args)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->OnIsEnabledChanged(sender, args);
|
|
}
|
|
});
|
|
|
|
m_gettingFocusRevoker = GettingFocus(
|
|
winrt::auto_revoke,
|
|
[weakRow](winrt::UIElement const& sender, winrt::Microsoft::UI::Xaml::Input::GettingFocusEventArgs const& args)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->OnRowGettingFocus(sender, args);
|
|
}
|
|
});
|
|
|
|
m_gotFocusRevoker = GotFocus(
|
|
winrt::auto_revoke,
|
|
[weakRow](winrt::IInspectable const&, winrt::RoutedEventArgs const&)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->OnRowGotFocus();
|
|
}
|
|
});
|
|
|
|
// Don't strand cell content out of the tab order if unloaded before the restore turn runs.
|
|
m_unloadedRevoker = Unloaded(
|
|
winrt::auto_revoke,
|
|
[weakRow](winrt::IInspectable const&, winrt::RoutedEventArgs const&)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->RestoreCellContentTabStopsInternal();
|
|
}
|
|
});
|
|
}
|
|
|
|
void TableViewRow::OnIsEnabledChanged(
|
|
const winrt::IInspectable& /*sender*/,
|
|
const winrt::DependencyPropertyChangedEventArgs& /*args*/)
|
|
{
|
|
UpdateVisualState(true /* useTransitions */);
|
|
}
|
|
|
|
void TableViewRow::OnApplyTemplate()
|
|
{
|
|
m_cellsHostContextRequestedRevoker.revoke();
|
|
m_templateRootContextRequestedRevoker.revoke();
|
|
__super::OnApplyTemplate();
|
|
|
|
ResetCellAutomationNames();
|
|
auto const host = GetTemplateChild(hstring{ s_CellsHostPartName }).try_as<winrt::Panel>();
|
|
m_cellsHost.set(host);
|
|
m_gridLineBorder.set(GetTemplateChild(hstring{ s_GridLineBorderPartName }).try_as<winrt::Border>());
|
|
m_rowExpanderGutter.set(GetTemplateChild(hstring{ s_RowExpanderGutterPartName }).try_as<winrt::FrameworkElement>());
|
|
|
|
// Revoke unconditionally so a re-template that drops the gutter leaves no stale subscription.
|
|
m_gutterPointerPressedRevoker.revoke();
|
|
m_gutterPointerReleasedRevoker.revoke();
|
|
m_gutterPointerExitedRevoker.revoke();
|
|
m_gutterPointerCaptureLostRevoker.revoke();
|
|
m_isGutterPressed = false;
|
|
|
|
// Subscribe per template apply, not per prepare (pooled container would fan one tap into N
|
|
// toggles). The press is handled so the row's release-time selection latch never arms; the
|
|
// toggle itself fires on release-inside, matching TableViewGroupHeader.
|
|
if (auto const gutter = m_rowExpanderGutter.get())
|
|
{
|
|
m_gutterPointerPressedRevoker = gutter.PointerPressed(
|
|
winrt::auto_revoke,
|
|
{ this, &TableViewRow::OnExpanderGutterPointerPressed });
|
|
m_gutterPointerReleasedRevoker = gutter.PointerReleased(
|
|
winrt::auto_revoke,
|
|
{ this, &TableViewRow::OnExpanderGutterPointerReleased });
|
|
m_gutterPointerExitedRevoker = gutter.PointerExited(
|
|
winrt::auto_revoke,
|
|
{ this, &TableViewRow::OnExpanderGutterPointerCanceled });
|
|
m_gutterPointerCaptureLostRevoker = gutter.PointerCaptureLost(
|
|
winrt::auto_revoke,
|
|
{ this, &TableViewRow::OnExpanderGutterPointerCanceled });
|
|
}
|
|
|
|
if (host)
|
|
{
|
|
// Match PART_HeaderHost: scope TabFocusNavigation at the host, not the shared
|
|
// TableViewCellsPanel primitive.
|
|
host.TabFocusNavigation(winrt::KeyboardNavigationMode::Once);
|
|
}
|
|
|
|
if (auto const cellsPanel = host.try_as<winrt::TableViewCellsPanel>())
|
|
{
|
|
winrt::get_self<TableViewCellsPanel>(cellsPanel)->SetOwningRowInternal(*this);
|
|
}
|
|
|
|
// Every body request is handled below the row, so the framework never shows a row style or
|
|
// template ContextFlyout on its own and skips the focus and current-cell handling. Cell
|
|
// requests are caught at the cells host; padding and chevron requests at the template root.
|
|
if (host)
|
|
{
|
|
m_cellsHostContextRequestedRevoker = host.ContextRequested(winrt::auto_revoke,
|
|
[weakThis = get_weak()](auto&&, winrt::ContextRequestedEventArgs const& args)
|
|
{
|
|
if (auto self = weakThis.get())
|
|
{
|
|
self->OnCellsHostContextRequested(args);
|
|
}
|
|
});
|
|
}
|
|
|
|
m_templateRootContextRequestedRevoker = TableViewDetails::HookTemplateRootContextRequested(*this,
|
|
[weakThis = get_weak()](auto&&, winrt::ContextRequestedEventArgs const& args)
|
|
{
|
|
if (auto self = weakThis.get())
|
|
{
|
|
self->OnTemplateRootContextRequested(args);
|
|
}
|
|
});
|
|
|
|
RebuildCells();
|
|
|
|
ApplyHierarchyAffordance();
|
|
|
|
UpdateVisualState(false /* useTransitions */);
|
|
}
|
|
|
|
void TableViewRow::SetTerminalGridLineSuppression(TerminalGridLineSuppressionState state, bool refresh)
|
|
{
|
|
m_suppressTrailingGridLine = state.suppressTrailing;
|
|
m_suppressBottomGridLine = state.suppressBottom;
|
|
if (refresh)
|
|
{
|
|
RefreshGridLines();
|
|
}
|
|
}
|
|
|
|
winrt::AutomationPeer TableViewRow::OnCreateAutomationPeer()
|
|
{
|
|
return winrt::make<TableViewRowAutomationPeer>(*this);
|
|
}
|
|
|
|
// Typed accessor for the owning TableView.
|
|
winrt::TableView TableViewRow::GetOwningTableView()
|
|
{
|
|
// Keep the owner weak; callers acquire a strong ref only for synchronous work.
|
|
return m_owningTableView.get();
|
|
}
|
|
|
|
// Row-side half of TableView::IsContextMenuTargetCurrent, the only caller: it has already checked
|
|
// the target kind, row, anchor load state, column, and owning table.
|
|
bool TableViewRow::IsContextMenuTargetCurrent(
|
|
TableViewDetails::ContextMenuTarget const& target) const
|
|
{
|
|
if (!target.Row.IsLoaded() || !target.ScopeRoot || target.ScopeRoot != m_cellsHost.get())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const owner = m_owningTableView.get();
|
|
if (!owner)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto* ownerImpl = winrt::get_self<TableView>(owner);
|
|
if (!TableView::SameInspectableIdentity(
|
|
ownerImpl->UnwrapEditingDataItem(target.Row.DataContext()),
|
|
target.Item))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const repeater = ownerImpl->GetRowsRepeaterInternal();
|
|
if (!repeater)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const index = repeater.GetElementIndex(target.Row);
|
|
auto const view = repeater.ItemsSourceView();
|
|
if (!view || index < 0 || index >= view.Count() ||
|
|
!TableView::SameInspectableIdentity(
|
|
ownerImpl->UnwrapEditingDataItem(view.GetAt(index)),
|
|
target.Item))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!target.Column)
|
|
{
|
|
return target.Anchor == target.Row;
|
|
}
|
|
|
|
return TableViewDetails::IsLiveCellAnchor(target.Anchor, target.ScopeRoot, target.Column);
|
|
}
|
|
|
|
winrt::TableViewColumn TableViewRow::GetCellOwningColumn(const winrt::UIElement& cellElement) const
|
|
{
|
|
if (auto cellFE = cellElement.try_as<winrt::FrameworkElement>())
|
|
{
|
|
if (auto column = cellFE.Tag().try_as<winrt::TableViewColumn>())
|
|
{
|
|
return column;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// ----- Cell-level keyboard focus -----
|
|
|
|
int32_t TableViewRow::GetVisibleCellCountInternal() const
|
|
{
|
|
int32_t count = 0;
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
auto const children = host.Children();
|
|
const uint32_t size = children.Size();
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
{
|
|
auto const child = children.GetAt(i);
|
|
// Same predicate as TableViewCellAutomationPeer::Column and the row peer's children, so
|
|
// the keyboard coordinate space and the UIA one cannot drift apart.
|
|
if (child && IsVisibleColumn(GetCellOwningColumn(child)))
|
|
{
|
|
++count;
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
winrt::UIElement TableViewRow::GetVisibleCellInternal(int32_t visibleColumnIndex) const
|
|
{
|
|
if (visibleColumnIndex < 0)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
auto const children = host.Children();
|
|
const uint32_t size = children.Size();
|
|
int32_t visible = 0;
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
{
|
|
auto const child = children.GetAt(i);
|
|
if (!child || !IsVisibleColumn(GetCellOwningColumn(child)))
|
|
{
|
|
continue;
|
|
}
|
|
if (visible == visibleColumnIndex)
|
|
{
|
|
return child;
|
|
}
|
|
++visible;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int32_t TableViewRow::GetVisibleCellIndexInternal(const winrt::UIElement& cell) const
|
|
{
|
|
if (!cell)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
auto const children = host.Children();
|
|
const uint32_t size = children.Size();
|
|
int32_t visible = 0;
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
{
|
|
auto const child = children.GetAt(i);
|
|
if (!child || !IsVisibleColumn(GetCellOwningColumn(child)))
|
|
{
|
|
continue;
|
|
}
|
|
if (child == cell)
|
|
{
|
|
return visible;
|
|
}
|
|
++visible;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
winrt::UIElement TableViewRow::FindOwnCellInternal(
|
|
const winrt::DependencyObject& element, bool requireExact) const
|
|
{
|
|
auto const host = m_cellsHost.get();
|
|
if (!element || !host)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
winrt::DependencyObject current = element;
|
|
while (current)
|
|
{
|
|
if (current == host.try_as<winrt::DependencyObject>())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (auto const candidate = current.try_as<winrt::UIElement>())
|
|
{
|
|
if (winrt::VisualTreeHelper::GetParent(candidate) == host.try_as<winrt::DependencyObject>() &&
|
|
IsVisibleColumn(GetCellOwningColumn(candidate)))
|
|
{
|
|
return (!requireExact || candidate == element.try_as<winrt::UIElement>())
|
|
? candidate : nullptr;
|
|
}
|
|
}
|
|
|
|
if (requireExact)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
current = winrt::VisualTreeHelper::GetParent(current);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
winrt::FrameworkElement TableViewRow::GetLastVisibleCellInternal() const
|
|
{
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
const auto children = host.Children();
|
|
for (uint32_t i = children.Size(); i > 0; --i)
|
|
{
|
|
if (auto cell = children.GetAt(i - 1).try_as<winrt::FrameworkElement>();
|
|
cell &&
|
|
cell.Visibility() == winrt::Visibility::Visible &&
|
|
cell.ActualWidth() > 0.0)
|
|
{
|
|
return cell;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool TableViewRow::FocusVisibleCellInternal(int32_t visibleColumnIndex, winrt::FocusState state)
|
|
{
|
|
if (auto const cell = GetVisibleCellInternal(visibleColumnIndex))
|
|
{
|
|
if (auto const cellFE = cell.try_as<winrt::FrameworkElement>())
|
|
{
|
|
cellFE.StartBringIntoView();
|
|
}
|
|
|
|
// Drill in BEFORE focusing: at row level the cells are not tab stops, and
|
|
// CUIElement::IsFocusable requires IsTabStop even for a programmatic Focus().
|
|
SetCellLevelInternal(true);
|
|
|
|
if (cell.Focus(state))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// The cell refused (collapsed column, disabled subtree, a focus operation already in
|
|
// flight). Undo the drill-in rather than leaving the row in a state where neither level is
|
|
// a tab stop, which would strand the body with no reachable focus target at all.
|
|
SetCellLevelInternal(false);
|
|
}
|
|
|
|
return Focus(state);
|
|
}
|
|
|
|
// ----- Two-level focus: ROW level vs CELL level -----
|
|
//
|
|
// Body focus is either the row or one of its cells, never both. XAML tab search enters children
|
|
// before consulting TabFocusNavigation, so a focusable row with focusable cells creates extra
|
|
// forward/reverse tab stops.
|
|
//
|
|
// Gating IsTabStop at both ends keeps the body one tab stop while still allowing row/cell arrow
|
|
// navigation. Cells default to IsTabStop(true); row policy stamps the current level.
|
|
void TableViewRow::SetCellLevelInternal(bool isCellLevel)
|
|
{
|
|
m_isCellLevel = isCellLevel;
|
|
ApplyFocusLevelInternal();
|
|
}
|
|
|
|
// Re-applies the current level to the live cells. Called after any rebuild, because new cell
|
|
// wrappers arrive with IsTabStop(true) and would otherwise re-open the row-level Tab leak.
|
|
void TableViewRow::ApplyFocusLevelInternal()
|
|
{
|
|
// Make the incoming level focusable before clearing the outgoing one, or the row has no focus
|
|
// target during the handoff.
|
|
if (m_isCellLevel)
|
|
{
|
|
SetCellsTabStopInternal(true);
|
|
if (IsTabStop())
|
|
{
|
|
IsTabStop(false);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!IsTabStop())
|
|
{
|
|
IsTabStop(true);
|
|
}
|
|
SetCellsTabStopInternal(false);
|
|
}
|
|
}
|
|
|
|
void TableViewRow::SetCellsTabStopInternal(bool isTabStop)
|
|
{
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
auto const children = host.Children();
|
|
const uint32_t size = children.Size();
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
{
|
|
if (auto const child = children.GetAt(i))
|
|
{
|
|
if (child.IsTabStop() != isTabStop)
|
|
{
|
|
child.IsTabStop(isTabStop);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Focus inside a cell's content (drilled in via Enter, or an editor) is not grid level: that
|
|
// content owns its own keys, including Tab.
|
|
bool TableViewRow::IsGridLevelFocusInternal()
|
|
{
|
|
auto const root = XamlRoot();
|
|
if (!root)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const focused = winrt::FocusManager::GetFocusedElement(root).try_as<winrt::DependencyObject>();
|
|
if (!focused)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
winrt::DependencyObject const selfObject = *this;
|
|
if (focused == selfObject)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return FindOwnCellInternal(focused, true /* requireExact */) != nullptr;
|
|
}
|
|
|
|
// Takes authored cell content out of the tab order for ONE Tab walk. CFocusManager::GetNextTabStop
|
|
// searches the focused element's children before honouring TabFocusNavigation, so cell content
|
|
// would otherwise become extra tab stops. It can't stay suppressed: IsFocusable requires IsTabStop,
|
|
// so pointer/programmatic focus would break too. Restored on the next dispatcher turn (after XAML's
|
|
// default Tab move), and only if the local value is still the false written here.
|
|
void TableViewRow::SuppressCellContentTabStopsForTabWalkInternal()
|
|
{
|
|
// Restore a pending suppression first, or our false would be recorded as the original.
|
|
RestoreCellContentTabStopsInternal();
|
|
|
|
std::vector<winrt::UIElement> candidates;
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
auto const children = host.Children();
|
|
const uint32_t size = children.Size();
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
{
|
|
if (auto const cell = children.GetAt(i))
|
|
{
|
|
AppendContentTabStops(cell, candidates, c_maxCellContentTabStopDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (candidates.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const isTabStopProperty = winrt::UIElement::IsTabStopProperty();
|
|
auto const unsetValue = winrt::DependencyProperty::UnsetValue();
|
|
auto const suppressedValue = winrt::box_value(false);
|
|
|
|
for (auto const& element : candidates)
|
|
{
|
|
auto const local = element.ReadLocalValue(isTabStopProperty);
|
|
auto const binding = local.try_as<winrt::Microsoft::UI::Xaml::Data::BindingExpression>();
|
|
const bool isRestorable =
|
|
local == unsetValue ||
|
|
local.try_as<winrt::IReference<bool>>() ||
|
|
(binding && binding.ParentBinding());
|
|
if (!isRestorable)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (binding)
|
|
{
|
|
// Detach first: SetValue over a TwoWay binding would write through to the app's source.
|
|
element.ClearValue(isTabStopProperty);
|
|
}
|
|
|
|
element.SetValue(isTabStopProperty, suppressedValue);
|
|
m_suppressedContentTabStops.push_back({ winrt::make_weak(element), local });
|
|
}
|
|
|
|
if (m_suppressedContentTabStops.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto weakRow = get_weak();
|
|
auto const queue = DispatcherQueue();
|
|
if (!queue || !queue.TryEnqueue([weakRow]()
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
try
|
|
{
|
|
strongRow->RestoreCellContentTabStopsInternal();
|
|
}
|
|
catch (...)
|
|
{
|
|
// Best-effort restore; never fail-fast the dispatcher.
|
|
}
|
|
}
|
|
}))
|
|
{
|
|
// No dispatcher: never leave content out of the tab order, even at the cost of this press.
|
|
RestoreCellContentTabStopsInternal();
|
|
}
|
|
}
|
|
|
|
// Idempotent. Elements that have left this row are still restored (the false is our write); the
|
|
// "still our false" check preserves any newer value.
|
|
void TableViewRow::RestoreCellContentTabStopsInternal()
|
|
{
|
|
if (m_suppressedContentTabStops.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const suppressed = std::move(m_suppressedContentTabStops);
|
|
m_suppressedContentTabStops.clear();
|
|
|
|
auto const isTabStopProperty = winrt::UIElement::IsTabStopProperty();
|
|
auto const unsetValue = winrt::DependencyProperty::UnsetValue();
|
|
|
|
for (auto const& entry : suppressed)
|
|
{
|
|
auto const element = entry.element.get();
|
|
if (!element)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
auto const current = element.ReadLocalValue(isTabStopProperty).try_as<winrt::IReference<bool>>();
|
|
if (!current || current.Value())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const& original = entry.originalLocalValue;
|
|
if (original == unsetValue)
|
|
{
|
|
element.ClearValue(isTabStopProperty);
|
|
}
|
|
else if (auto const binding = original.try_as<winrt::Microsoft::UI::Xaml::Data::BindingExpression>())
|
|
{
|
|
winrt::Microsoft::UI::Xaml::Data::BindingOperations::SetBinding(
|
|
element, isTabStopProperty, binding.ParentBinding());
|
|
}
|
|
else
|
|
{
|
|
element.SetValue(isTabStopProperty, original);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
// One element failing must not strand the rest out of the tab order.
|
|
}
|
|
}
|
|
}
|
|
|
|
// The body is one tab stop; Enter is the route into cell content. The move is left to XAML:
|
|
// FocusManager::TryMoveFocus ignores ItemsRepeater's KeyboardNavigationMode::Once, so Shift+Tab
|
|
// would land in the previous row's content. Multiply expands the subtree (as TreeViewItem);
|
|
// unhandled Left/Right go to TableView::TryHandleRowLevelDrillKey here, ahead of PART_BodyScroller,
|
|
// and Space stays row selection.
|
|
void TableViewRow::OnKeyDown(winrt::KeyRoutedEventArgs const& args)
|
|
{
|
|
if (!args.Handled() &&
|
|
args.Key() == winrt::VirtualKey::Tab &&
|
|
// Ctrl+Tab is app-level navigation and Alt is the resize chord; neither is a band move.
|
|
!IsKeyDown(winrt::VirtualKey::Control) &&
|
|
!IsKeyDown(winrt::VirtualKey::Menu) &&
|
|
IsGridLevelFocusInternal())
|
|
{
|
|
SuppressCellContentTabStopsForTabWalkInternal();
|
|
}
|
|
|
|
// Unmodified only: Ctrl+arrow is focus-without-select; Shift/Alt chords belong to the app.
|
|
if (!args.Handled() && Level() > 0 && IsRowItselfFocused() &&
|
|
!IsKeyDown(winrt::VirtualKey::Control) &&
|
|
!IsKeyDown(winrt::VirtualKey::Shift) &&
|
|
!IsKeyDown(winrt::VirtualKey::Menu))
|
|
{
|
|
auto const owner = GetOwningTableView();
|
|
if (args.Key() == winrt::VirtualKey::Multiply && IsExpandable() && owner)
|
|
{
|
|
winrt::get_self<TableView>(owner)->ExpandRowSubtree(*this);
|
|
args.Handled(true);
|
|
}
|
|
}
|
|
|
|
if (!args.Handled() &&
|
|
(args.Key() == winrt::VirtualKey::Left || args.Key() == winrt::VirtualKey::Right))
|
|
{
|
|
if (auto const owner = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(owner)->HandleBodyHorizontalKeyFromRowInternal(args);
|
|
}
|
|
}
|
|
|
|
__super::OnKeyDown(args);
|
|
}
|
|
|
|
// Pop-out must make the row focusable before clearing the focused cell from tab order.
|
|
void TableViewRow::EnableRowFocusInternal()
|
|
{
|
|
IsTabStop(true);
|
|
}
|
|
|
|
void TableViewRow::OnRowGettingFocus(
|
|
const winrt::UIElement& /*sender*/,
|
|
const winrt::Microsoft::UI::Xaml::Input::GettingFocusEventArgs& args)
|
|
{
|
|
winrt::DependencyObject const selfObject = *this;
|
|
auto const newFocus = args.NewFocusedElement();
|
|
if (!newFocus)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Tab / Shift+Tab only. Arrow navigation, an edit-close restore and a pointer press all name
|
|
// the row they mean; redirecting those would move the user somewhere they did not ask for.
|
|
auto const direction = args.Direction();
|
|
const bool isTabEntry =
|
|
direction == winrt::FocusNavigationDirection::Next ||
|
|
direction == winrt::FocusNavigationDirection::Previous;
|
|
if (!isTabEntry)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const ownerImpl = winrt::get_self<TableView>(owner);
|
|
|
|
// An open editor owns focus; redirecting the row focus the editor teardown performs would
|
|
// fight the row's own "move focus off the editor before it leaves the tree" step.
|
|
if (ownerImpl->IsEditing() && m_editingElement.get())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Focus LEAVING this row must never be pulled back, or Tab can never exit the table. Only
|
|
// focus arriving from outside the row is an entry that wants resolving.
|
|
auto const oldFocus = args.OldFocusedElement();
|
|
if (oldFocus == selfObject ||
|
|
SharedHelpers::IsAncestor(oldFocus, selfObject, false /* checkVisibility */))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// When returning from another band inside the table, stale cell-level state can make XAML aim
|
|
// the body's single tab stop at a cell. Body band entry is still row-level.
|
|
winrt::DependencyObject const ownerObject = owner;
|
|
const bool focusCameFromWithinTable =
|
|
oldFocus == ownerObject ||
|
|
SharedHelpers::IsAncestor(oldFocus, ownerObject, false /* checkVisibility */);
|
|
|
|
if (focusCameFromWithinTable && ownerImpl->TryRedirectBandEntryToViewport(*this, args))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The bands share one column cursor: a cell-level cursor re-enters on the cell in that column.
|
|
if (focusCameFromWithinTable && ownerImpl->IsCellCursorActiveInternal())
|
|
{
|
|
auto targetRow = ownerImpl->ResolveFocusEntryRow(*this, oldFocus);
|
|
if (!targetRow)
|
|
{
|
|
targetRow = *this;
|
|
}
|
|
auto const targetImpl = winrt::get_self<TableViewRow>(targetRow);
|
|
|
|
if (const int32_t cellCount = targetImpl->GetVisibleCellCountInternal(); cellCount > 0)
|
|
{
|
|
const int32_t column =
|
|
std::clamp(ownerImpl->CurrentColumnCursorInternal(), 0, cellCount - 1);
|
|
|
|
// Row-level cells are not focusable.
|
|
targetImpl->SetCellLevelInternal(true);
|
|
|
|
if (auto const cell = targetImpl->GetVisibleCellInternal(column))
|
|
{
|
|
if (cell.try_as<winrt::DependencyObject>() != newFocus &&
|
|
!args.TrySetNewFocusedElement(cell))
|
|
{
|
|
// Unless XAML already targets a cell of this row, undo the drill-in so the row
|
|
// isn't left with no tab stop.
|
|
if (!targetImpl->FindOwnCellInternal(newFocus, false /* requireExact */))
|
|
{
|
|
targetImpl->SetCellLevelInternal(false);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
targetImpl->SetCellLevelInternal(false);
|
|
}
|
|
}
|
|
|
|
if (focusCameFromWithinTable)
|
|
{
|
|
if (newFocus == selfObject || !FindOwnCellInternal(newFocus, false /* requireExact */))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Commit the cursor change only once the focus manager accepts the redirect.
|
|
const bool wasCellLevel = IsCellLevelInternal();
|
|
SetCellLevelInternal(false);
|
|
if (args.TrySetNewFocusedElement(selfObject))
|
|
{
|
|
ownerImpl->SetCellCursorActiveInternal(false);
|
|
}
|
|
else
|
|
{
|
|
SetCellLevelInternal(wasCellLevel);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const auto correlationId = args.CorrelationId();
|
|
if (ownerImpl->HasRedirectedFocusEntry(correlationId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto entry = ownerImpl->ResolveFocusEntryTarget(*this, oldFocus, direction);
|
|
if (!entry.Element || !entry.Row)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Redirecting to an ancestor cell of the proposed element starts a loop. A row target still
|
|
// takes focus from content hosted inside it.
|
|
winrt::DependencyObject targetObject = entry.Element;
|
|
if (newFocus == targetObject ||
|
|
(entry.CellLevel && SharedHelpers::IsAncestor(newFocus, targetObject, false /* checkVisibility */)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Arm the target's level first (only one level is a tab stop); undo on failure.
|
|
auto targetRowImpl = winrt::get_self<TableViewRow>(entry.Row);
|
|
bool targetWasCellLevel = targetRowImpl->IsCellLevelInternal();
|
|
targetRowImpl->SetCellLevelInternal(entry.CellLevel);
|
|
|
|
if (!TableView::IsEntryRedirectTargetFocusable(entry.Element, ownerObject))
|
|
{
|
|
targetRowImpl->SetCellLevelInternal(targetWasCellLevel);
|
|
|
|
// The remembered target can't take focus; the viewport edge row still avoids a cache row.
|
|
auto const edgeRow = ownerImpl->FindViewportEdgeRowInternal(direction == winrt::FocusNavigationDirection::Previous);
|
|
winrt::DependencyObject const edgeObject = edgeRow;
|
|
if (!edgeRow || edgeObject == newFocus || edgeObject == targetObject)
|
|
{
|
|
return;
|
|
}
|
|
|
|
entry.Row = edgeRow;
|
|
entry.Element = edgeRow;
|
|
entry.CellLevel = false;
|
|
targetObject = edgeObject;
|
|
targetRowImpl = winrt::get_self<TableViewRow>(edgeRow);
|
|
targetWasCellLevel = targetRowImpl->IsCellLevelInternal();
|
|
targetRowImpl->SetCellLevelInternal(false);
|
|
if (!TableView::IsEntryRedirectTargetFocusable(edgeRow, ownerObject))
|
|
{
|
|
targetRowImpl->SetCellLevelInternal(targetWasCellLevel);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!args.TrySetNewFocusedElement(targetObject))
|
|
{
|
|
targetRowImpl->SetCellLevelInternal(targetWasCellLevel);
|
|
return;
|
|
}
|
|
|
|
ownerImpl->NoteFocusEntryRedirect(correlationId);
|
|
if (entry.CellLevel)
|
|
{
|
|
ownerImpl->ArmCellLevelRowInternal(entry.Row);
|
|
}
|
|
else
|
|
{
|
|
ownerImpl->SetCellCursorActiveInternal(false);
|
|
}
|
|
}
|
|
|
|
void TableViewRow::OnRowGotFocus()
|
|
{
|
|
if (auto const owner = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(owner)->OnRowCellFocusChanged(*this);
|
|
}
|
|
}
|
|
|
|
void TableViewRow::DetachColumnsSubscription()
|
|
{
|
|
if (m_observedColumns.get())
|
|
{
|
|
m_columnsVectorChangedRevoker = {};
|
|
m_observedColumns = nullptr;
|
|
}
|
|
}
|
|
|
|
void TableViewRow::AttachColumnsSubscription(winrt::TableView const& owner)
|
|
{
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Cast the ABI vector to its observable backing type for VectorChanged.
|
|
if (auto observable = owner.Columns().try_as<winrt::IObservableVector<winrt::TableViewColumn>>())
|
|
{
|
|
auto weakRow = get_weak();
|
|
m_columnsVectorChangedRevoker = observable.VectorChanged(
|
|
winrt::auto_revoke,
|
|
[weakRow](winrt::IObservableVector<winrt::TableViewColumn> const& sender, winrt::IVectorChangedEventArgs const& args)
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->OnColumnsVectorChanged(sender, args);
|
|
}
|
|
});
|
|
m_observedColumns = observable;
|
|
}
|
|
}
|
|
|
|
bool TableViewRow::SetOwningTableViewInternal(winrt::TableView const& owner)
|
|
{
|
|
auto const currentOwner = GetOwningTableView();
|
|
// Compare through the typed accessor so unchanged weak refs hit the no-op path.
|
|
if (currentOwner == owner)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Single-writer: the recycle cycle clears the owner to null between tables; a direct
|
|
// table-A -> table-B switch (no intervening clear) is a cross-table leak, so reject it.
|
|
if (currentOwner && owner)
|
|
{
|
|
// Runs inside an ItemsRepeater callback; an escaping throw would failfast.
|
|
// Assert in debug, no-op in retail (keep current owner, skip re-attach).
|
|
MUX_ASSERT_MSG(false, "TableViewRow re-owned without an intervening clear");
|
|
return false;
|
|
}
|
|
|
|
DetachColumnsSubscription();
|
|
|
|
if (owner)
|
|
{
|
|
m_owningTableView = winrt::make_weak(owner);
|
|
}
|
|
else
|
|
{
|
|
ResetCellAutomationNames();
|
|
m_owningTableView = nullptr;
|
|
// Reset transient interaction state so a row recycled while hovered/pressed
|
|
// re-enters the pool in Normal state (ListViewItem parity), not a stale tint.
|
|
m_isPointerOver = false;
|
|
m_isPressed = false;
|
|
m_selectOnPointerRelease = false;
|
|
|
|
// Selection belongs to the item, not the container: a pooled row must not carry selected
|
|
// chrome onto its next item. RefreshRowSelectionState restamps it on the way back in.
|
|
IsSelected(false);
|
|
|
|
// Begin-edit gesture state must go too. A row recycled away and back to the SAME item
|
|
// inside the double-click interval would otherwise turn the next single click into an
|
|
// edit, and the stale trackers keep the previous item and column alive.
|
|
ResetPressState();
|
|
|
|
// Recycled rows return at row level; otherwise a drilled row can reappear unreachable by Tab.
|
|
m_isCellLevel = false;
|
|
|
|
RestoreCellContentTabStopsInternal();
|
|
|
|
UpdateVisualState(false);
|
|
}
|
|
|
|
// Observe the new owner's Columns vector (no-op on recycle-out when owner is null).
|
|
AttachColumnsSubscription(owner);
|
|
|
|
return RebuildCells();
|
|
}
|
|
|
|
bool TableViewRow::EnsureOwningTableViewInternal(winrt::TableView const& owner)
|
|
{
|
|
if (!owner)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m_owningTableView.get() != owner)
|
|
{
|
|
return SetOwningTableViewInternal(owner);
|
|
}
|
|
|
|
auto const rowPeer = winrt::FrameworkElementAutomationPeer::FromElement(*this)
|
|
.try_as<winrt::TableViewRowAutomationPeer>();
|
|
auto* const peerImpl = rowPeer ? winrt::get_self<TableViewRowAutomationPeer>(rowPeer) : nullptr;
|
|
if (peerImpl && !peerImpl->CanReuseForRowItem(*this, owner))
|
|
{
|
|
// Cell providers are bound to their element, so replace the cells only if a cell peer exists;
|
|
// otherwise keep them and let them restamp from the inherited DataContext.
|
|
bool chromeRefreshed = false;
|
|
if (HasCellAutomationPeers())
|
|
{
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
ResetCellAutomationNames();
|
|
host.Children().Clear();
|
|
}
|
|
peerImpl->DropCellPeerCache();
|
|
chromeRefreshed = RebuildCells(false /* updateExistingCellPeerItems */);
|
|
}
|
|
else
|
|
{
|
|
peerImpl->DropCellPeerCache();
|
|
chromeRefreshed = RebuildCells();
|
|
}
|
|
peerImpl->TrackCurrentRowItem(*this, owner);
|
|
return chromeRefreshed;
|
|
}
|
|
|
|
const bool chromeRefreshed = RebuildCells();
|
|
|
|
if (peerImpl)
|
|
{
|
|
peerImpl->TrackCurrentRowItem(*this, owner);
|
|
}
|
|
|
|
return chromeRefreshed;
|
|
}
|
|
|
|
// Rewire realized rows when Columns changes but the owner identity does not.
|
|
void TableViewRow::RefreshColumnsSubscriptionInternal()
|
|
{
|
|
DetachColumnsSubscription();
|
|
|
|
auto owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AttachColumnsSubscription(owner);
|
|
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
ResetCellAutomationNames();
|
|
host.Children().Clear();
|
|
}
|
|
|
|
RebuildCells();
|
|
}
|
|
|
|
void TableViewRow::OnDataContextChanged(
|
|
const winrt::FrameworkElement& /*sender*/,
|
|
const winrt::DataContextChangedEventArgs& /*args*/)
|
|
{
|
|
// On recycle ItemsRepeater updates the row's DataContext; cells pick up the new item reactively
|
|
// via inheritance (they are not restamped). RebuildCells is still called so index-dependent visuals
|
|
// (alternating-row banding, frozen pinning) refresh for the new position. Guard against re-entry
|
|
// from child DataContext propagation.
|
|
if (m_isRebuildingCells)
|
|
{
|
|
return;
|
|
}
|
|
RebuildCells();
|
|
}
|
|
|
|
void TableViewRow::OnColumnsVectorChanged(
|
|
const winrt::IObservableVector<winrt::TableViewColumn>& /*sender*/,
|
|
const winrt::IVectorChangedEventArgs& /*args*/)
|
|
{
|
|
// Mutating Columns triggers a cell rebuild against the new column set. Coalesce a burst of column
|
|
// changes into a single rebuild on the next tick. No explicit Children().Clear() is needed:
|
|
// RebuildCells' restamp fast-path forces a full rebuild (which clears) whenever the column set
|
|
// actually changed, and keeping the old cells until the tick avoids an empty-cell flash.
|
|
QueueRebuildCells();
|
|
}
|
|
|
|
void TableViewRow::QueueRebuildCells()
|
|
{
|
|
// No cell host yet (template not applied); the owner-set / ApplyTemplate path builds cells.
|
|
if (!m_cellsHost.get())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// A rebuild is already scheduled for this tick -- collapse the burst into one.
|
|
if (m_rebuildCellsQueued)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto dispatcher = DispatcherQueue();
|
|
if (!dispatcher)
|
|
{
|
|
// No dispatcher (teardown) -- rebuild synchronously so cells are not left stale.
|
|
RebuildCells();
|
|
return;
|
|
}
|
|
|
|
m_rebuildCellsQueued = true;
|
|
auto weakRow = get_weak();
|
|
if (!dispatcher.TryEnqueue([weakRow]()
|
|
{
|
|
if (auto strongRow = weakRow.get())
|
|
{
|
|
strongRow->m_rebuildCellsQueued = false;
|
|
try
|
|
{
|
|
strongRow->RebuildCells();
|
|
}
|
|
catch (...)
|
|
{
|
|
// Coalesced cell rebuild is best-effort; never fail-fast the dispatcher.
|
|
}
|
|
}
|
|
}))
|
|
{
|
|
// Enqueue failed -- fall back to a synchronous rebuild so cells are not left stale.
|
|
m_rebuildCellsQueued = false;
|
|
RebuildCells();
|
|
}
|
|
}
|
|
|
|
void TableViewRow::OnPointerPressed(winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
if (args.Handled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto pointerPoint = args.GetCurrentPoint(*this);
|
|
auto props = pointerPoint.Properties();
|
|
|
|
// Mouse/pen: primary button only - a right-click opens a context menu and must not select.
|
|
// Touch reports no pressed button, so it is admitted on device type instead, matching the
|
|
// begin-edit gesture. Without this a touch press never arms the release-time selection.
|
|
const auto deviceType = args.Pointer().PointerDeviceType();
|
|
if (deviceType != winrt::Microsoft::UI::Input::PointerDeviceType::Touch &&
|
|
!props.IsLeftButtonPressed())
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_isPressed = true;
|
|
UpdateVisualState(true);
|
|
|
|
// A cell press enters cell level so keyboard/UIA focus names that cell, not the whole row.
|
|
winrt::UIElement pressedCell{ nullptr };
|
|
if (auto const source = args.OriginalSource().try_as<winrt::DependencyObject>())
|
|
{
|
|
pressedCell = FindOwnCellInternal(source, false /* requireExact */);
|
|
}
|
|
if (pressedCell)
|
|
{
|
|
// Drill in before focusing; row-level cells are not focusable.
|
|
SetCellLevelInternal(true);
|
|
if (!pressedCell.Focus(winrt::FocusState::Pointer))
|
|
{
|
|
SetCellLevelInternal(false);
|
|
Focus(winrt::FocusState::Pointer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Empty strip clicks land on the row, the body's row-level focus target.
|
|
SetCellLevelInternal(false);
|
|
Focus(winrt::FocusState::Pointer);
|
|
}
|
|
|
|
m_selectOnPointerRelease = true;
|
|
m_selectPointerId = args.Pointer().PointerId();
|
|
}
|
|
|
|
void TableViewRow::OnPointerEntered(winrt::PointerRoutedEventArgs const& /*args*/)
|
|
{
|
|
m_isPointerOver = true;
|
|
UpdateVisualState(true);
|
|
}
|
|
|
|
void TableViewRow::OnPointerExited(winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
m_isPointerOver = false;
|
|
m_isPressed = false;
|
|
|
|
// The contact left the row, so a deferred selection is no longer a tap on it. Only the pointer
|
|
// that armed it may disarm it: with two contacts on the same row, the other one leaving must
|
|
// not cancel this one's pending selection. Matches the check in OnPointerReleased.
|
|
if (args.Pointer().PointerId() == m_selectPointerId)
|
|
{
|
|
m_selectOnPointerRelease = false;
|
|
}
|
|
|
|
UpdateVisualState(true);
|
|
}
|
|
|
|
void TableViewRow::OnPointerReleased(winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
m_isPressed = false;
|
|
|
|
// Deferred selection: the pointer came up on this row without a pan or a capture loss taking
|
|
// it away, so it was a tap. Only the pointer that armed it can commit or disarm it.
|
|
const bool isArmingPointer = args.Pointer().PointerId() == m_selectPointerId;
|
|
const bool selectNow = m_selectOnPointerRelease && isArmingPointer;
|
|
if (isArmingPointer)
|
|
{
|
|
m_selectOnPointerRelease = false;
|
|
}
|
|
|
|
if (selectNow)
|
|
{
|
|
if (auto const owner = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(owner)->OnRowPointerSelect(*this);
|
|
}
|
|
}
|
|
|
|
UpdateVisualState(true);
|
|
}
|
|
|
|
void TableViewRow::OnPointerCaptureLost(winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
m_isPressed = false;
|
|
|
|
// A ScrollViewer took the pointer for a pan; the gesture was a scroll, not a tap. Only the
|
|
// arming pointer disarms, so a second contact panning does not cancel the first one's tap.
|
|
if (args.Pointer().PointerId() == m_selectPointerId)
|
|
{
|
|
m_selectOnPointerRelease = false;
|
|
}
|
|
|
|
UpdateVisualState(true);
|
|
}
|
|
|
|
void TableViewRow::OnPointerCanceled(winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
// Palm rejection or a system gesture. Can arrive without a preceding PointerCaptureLost, which
|
|
// would otherwise leave the latch set and let the next unrelated release select this row.
|
|
m_isPressed = false;
|
|
|
|
if (args.Pointer().PointerId() == m_selectPointerId)
|
|
{
|
|
m_selectOnPointerRelease = false;
|
|
}
|
|
|
|
UpdateVisualState(true);
|
|
}
|
|
|
|
void TableViewRow::UpdateVisualState(bool useTransitions)
|
|
{
|
|
// One GoToState into one group: the selected states share CommonStates so nothing depends on
|
|
// call order (TreeViewItem / ItemContainer pattern). Disabled wins, for ListViewItem parity.
|
|
std::wstring_view state;
|
|
if (IsSelected())
|
|
{
|
|
state = !IsEnabled() ? L"SelectedDisabled"sv
|
|
: m_isPressed ? L"SelectedPressed"sv
|
|
: m_isPointerOver ? L"SelectedPointerOver"sv
|
|
: L"Selected"sv;
|
|
}
|
|
else
|
|
{
|
|
state = !IsEnabled() ? L"Disabled"sv
|
|
: m_isPressed ? L"Pressed"sv
|
|
: m_isPointerOver ? L"PointerOver"sv
|
|
: L"Normal"sv;
|
|
}
|
|
|
|
winrt::VisualStateManager::GoToState(*this, hstring{ state }, useTransitions);
|
|
}
|
|
|
|
void TableViewRow::SetIsSelectedInternal(bool isSelected)
|
|
{
|
|
if (IsSelected() == isSelected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IsSelected(isSelected);
|
|
UpdateVisualState(true /* useTransitions */);
|
|
}
|
|
|
|
void TableViewRow::SetHierarchyStateInternal(int32_t level, bool isExpandable, bool isExpanded)
|
|
{
|
|
// Snapshot for the automation event (a recycled row moving to another node is a real transition).
|
|
const winrt::ExpandCollapseState oldState = HierarchyExpandCollapseState();
|
|
|
|
// Guarded: setters box before SetValue, and re-preparing onto the same node is the common case.
|
|
if (Level() != level)
|
|
{
|
|
Level(level);
|
|
}
|
|
|
|
if (IsExpandable() != isExpandable)
|
|
{
|
|
IsExpandable(isExpandable);
|
|
}
|
|
|
|
if (IsExpanded() != isExpanded)
|
|
{
|
|
IsExpanded(isExpanded);
|
|
}
|
|
|
|
// Unconditional: indent also depends on the indent resource and grouping. Writes inside are
|
|
// equality-guarded, so no layout re-invalidation.
|
|
ApplyHierarchyAffordance();
|
|
|
|
RaiseExpandCollapseStateChanged(oldState, HierarchyExpandCollapseState());
|
|
|
|
// Only an existing peer can have handed a client cached set metadata.
|
|
if (winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::PropertyChanged))
|
|
{
|
|
if (auto const rowPeer = winrt::FrameworkElementAutomationPeer::FromElement(*this).try_as<winrt::TableViewRowAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewRowAutomationPeer>(rowPeer)->RaiseHierarchyPositionChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Computed here, not on the peer, so transitions are measurable without forcing a peer.
|
|
winrt::ExpandCollapseState TableViewRow::HierarchyExpandCollapseState()
|
|
{
|
|
if (Level() <= 0 || !IsExpandable())
|
|
{
|
|
return winrt::ExpandCollapseState::LeafNode;
|
|
}
|
|
|
|
return IsExpanded()
|
|
? winrt::ExpandCollapseState::Expanded
|
|
: winrt::ExpandCollapseState::Collapsed;
|
|
}
|
|
|
|
void TableViewRow::RaiseExpandCollapseStateChanged(
|
|
winrt::ExpandCollapseState oldState,
|
|
winrt::ExpandCollapseState newState)
|
|
{
|
|
if (oldState == newState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Runs on every prepare; avoid forcing peers when no client listens.
|
|
if (!winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::PropertyChanged))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// XAML caches the peer per element, so the fallback yields the same instance a client holds.
|
|
auto peer = winrt::FrameworkElementAutomationPeer::FromElement(*this);
|
|
if (!peer)
|
|
{
|
|
peer = winrt::FrameworkElementAutomationPeer::CreatePeerForElement(*this);
|
|
}
|
|
|
|
if (auto const rowPeer = peer ? peer.try_as<winrt::TableViewRowAutomationPeer>() : nullptr)
|
|
{
|
|
winrt::get_self<TableViewRowAutomationPeer>(rowPeer)->RaiseExpandCollapseAutomationEvent(
|
|
oldState, newState);
|
|
}
|
|
}
|
|
|
|
// Keys bubbling from cell content (editors, ComboBox) must not toggle the row.
|
|
bool TableViewRow::IsRowItselfFocused()
|
|
{
|
|
auto const root = XamlRoot();
|
|
if (!root)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const focused = winrt::FocusManager::GetFocusedElement(root).try_as<winrt::UIElement>();
|
|
return focused && focused == *this;
|
|
}
|
|
|
|
// Places the chevron and reserves room in the lead cell. Separate so template apply and cell
|
|
// rebuild (fresh gutter/wrappers) can re-apply against unchanged state.
|
|
void TableViewRow::ApplyHierarchyAffordance()
|
|
{
|
|
const int32_t level = Level();
|
|
const bool isHierarchical = level > 0;
|
|
|
|
winrt::VisualStateManager::GoToState(
|
|
*this, isHierarchical ? L"RowHierarchical" : L"RowNotHierarchical", false /* useTransitions */);
|
|
winrt::VisualStateManager::GoToState(
|
|
*this, IsExpandable() ? L"RowExpandable" : L"RowNotExpandable", false /* useTransitions */);
|
|
winrt::VisualStateManager::GoToState(
|
|
*this, IsExpanded() ? L"RowExpanded" : L"RowCollapsed", false /* useTransitions */);
|
|
|
|
const double indent = HierarchyIndent();
|
|
|
|
if (auto const gutter = m_rowExpanderGutter.get())
|
|
{
|
|
// Set directly: a visual state undoing the Collapsed base value would only revert to it.
|
|
const auto visibility = isHierarchical ? winrt::Visibility::Visible : winrt::Visibility::Collapsed;
|
|
if (gutter.Visibility() != visibility)
|
|
{
|
|
gutter.Visibility(visibility);
|
|
}
|
|
|
|
const winrt::Thickness gutterMargin{ indent, 0, 0, 0 };
|
|
if (gutter.Margin().Left != gutterMargin.Left)
|
|
{
|
|
gutter.Margin(gutterMargin);
|
|
}
|
|
}
|
|
|
|
// The clip is in gutter space, which the margin just moved.
|
|
UpdateExpanderGutterClip();
|
|
|
|
ApplyHierarchyIndentToCells(indent);
|
|
|
|
// A Level change without re-prepare skips RefreshFrozenColumnLayout; sync the pinned lead
|
|
// cell's Translation here.
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
auto const gutter = m_rowExpanderGutter.get();
|
|
if (gutter && (isHierarchical || gutter.Translation().x != 0.0f))
|
|
{
|
|
SyncExpanderGutterWithLeadCell(host);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Level is 1-based (UIA); a root gets no indent beyond the gutter.
|
|
double TableViewRow::HierarchyIndent()
|
|
{
|
|
const int32_t level = Level();
|
|
if (level <= 0)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
double indentSize = c_defaultRowIndentSize;
|
|
double baseIndent = 0.0;
|
|
if (auto const owner = GetOwningTableView())
|
|
{
|
|
auto const ownerImpl = winrt::get_self<TableView>(owner);
|
|
|
|
indentSize = ownerImpl->GetRowIndentSize();
|
|
|
|
// Under a group header, roots are indented by its gutter + spacing (not its Padding);
|
|
// ungrouped roots stay flush.
|
|
if (ownerImpl->IsTableViewSourceGrouped())
|
|
{
|
|
baseIndent = ownerImpl->GetGroupHeaderContentOffset();
|
|
}
|
|
}
|
|
|
|
return baseIndent + (level - 1) * indentSize;
|
|
}
|
|
|
|
// Reserves chevron space via lead cell WRAPPER padding (column content may be any app element).
|
|
// Separate so column-visibility changes can move it without re-running visual states.
|
|
void TableViewRow::ApplyHierarchyIndentToCells()
|
|
{
|
|
ApplyHierarchyIndentToCells(HierarchyIndent());
|
|
}
|
|
|
|
void TableViewRow::ApplyHierarchyIndentToCells(double indent)
|
|
{
|
|
auto const host = m_cellsHost.get();
|
|
if (!host)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const bool isHierarchical = Level() > 0;
|
|
|
|
// From the resource, not the element: gutter may not be measured yet.
|
|
double expanderSize = c_defaultRowExpanderSize;
|
|
if (isHierarchical)
|
|
{
|
|
if (auto const owner = GetOwningTableView())
|
|
{
|
|
expanderSize = winrt::get_self<TableView>(owner)->GetRowExpanderSize();
|
|
}
|
|
}
|
|
|
|
auto const children = host.Children();
|
|
bool leadAssigned = false;
|
|
for (uint32_t i = 0; i < children.Size(); ++i)
|
|
{
|
|
auto const wrapper = children.GetAt(i).try_as<winrt::Grid>();
|
|
if (!wrapper || !TableViewCellsPanel::ColumnForCell(wrapper))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Lead = first VISIBLE cell; reset all others so hiding/showing columns never strands or
|
|
// duplicates the reservation.
|
|
const bool isLead =
|
|
!leadAssigned && wrapper.Visibility() == winrt::Visibility::Visible;
|
|
leadAssigned = leadAssigned || isLead;
|
|
|
|
const double reserved = (isLead && isHierarchical) ? indent + expanderSize : 0.0;
|
|
auto padding = wrapper.Padding();
|
|
if (padding.Left != reserved)
|
|
{
|
|
padding.Left = reserved;
|
|
wrapper.Padding(padding);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TableViewRow::OnExpanderGutterPointerPressed(
|
|
winrt::IInspectable const& /*sender*/,
|
|
winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
if (!IsExpandable())
|
|
{
|
|
// Unhandled so a press on a leaf's gutter selects like the cell.
|
|
return;
|
|
}
|
|
|
|
// Primary only (incl. touch/pen) so context-menu presses reach the row.
|
|
if (!args.GetCurrentPoint(*this).Properties().IsLeftButtonPressed())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Keep the press from arming the row's release-time selection latch.
|
|
args.Handled(true);
|
|
m_isGutterPressed = true;
|
|
}
|
|
|
|
void TableViewRow::OnExpanderGutterPointerReleased(
|
|
winrt::IInspectable const& /*sender*/,
|
|
winrt::PointerRoutedEventArgs const& args)
|
|
{
|
|
// Released over the gutter (no capture is taken), so a press that drags off never toggles.
|
|
const bool wasPressed = std::exchange(m_isGutterPressed, false);
|
|
if (!wasPressed || !IsExpandable())
|
|
{
|
|
return;
|
|
}
|
|
|
|
args.Handled(true);
|
|
|
|
if (auto const owner = GetOwningTableView())
|
|
{
|
|
// Shared with group headers: resolves identity now, defers the reshape.
|
|
winrt::get_self<TableView>(owner)->ToggleGroupExpansion(*this);
|
|
}
|
|
}
|
|
|
|
void TableViewRow::OnExpanderGutterPointerCanceled(
|
|
winrt::IInspectable const& /*sender*/,
|
|
winrt::PointerRoutedEventArgs const& /*args*/)
|
|
{
|
|
m_isGutterPressed = false;
|
|
}
|
|
|
|
bool TableViewRow::IsWithinExpanderGutter(const winrt::IInspectable& source) const
|
|
{
|
|
auto const gutter = m_rowExpanderGutter.get();
|
|
if (!gutter)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
auto const self = static_cast<winrt::DependencyObject>(*this);
|
|
for (auto current = source.try_as<winrt::DependencyObject>(); current && current != self;
|
|
current = winrt::VisualTreeHelper::GetParent(current))
|
|
{
|
|
if (current == gutter)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void TableViewRow::OnCellsArrangedInternal(double leadLeft, double leadWidth, double height)
|
|
{
|
|
if (m_cellsArranged && leadLeft == m_leadCellLeft && leadWidth == m_leadCellWidth && height == m_cellsArrangedHeight)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_cellsArranged = true;
|
|
m_leadCellLeft = leadLeft;
|
|
m_leadCellWidth = leadWidth;
|
|
m_cellsArrangedHeight = height;
|
|
UpdateExpanderGutterClip();
|
|
}
|
|
|
|
// Confine the chevron to the lead cell's slot so it never paints over or takes presses from the next
|
|
// column. Gutter and host share origin and Translation, so subtracting the gutter's left margin maps
|
|
// the slot into gutter space (holds under frozen pinning and RTL). Clip also bounds hit-testing.
|
|
void TableViewRow::UpdateExpanderGutterClip()
|
|
{
|
|
auto const gutter = m_rowExpanderGutter.get();
|
|
if (!gutter)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Every column hidden: clip away and disable hit-testing until the next arrange.
|
|
if (Level() > 0 && m_cellsArranged && m_leadCellWidth < 0.0)
|
|
{
|
|
auto geometry = gutter.Clip();
|
|
if (!geometry)
|
|
{
|
|
geometry = winrt::RectangleGeometry();
|
|
gutter.Clip(geometry);
|
|
}
|
|
const winrt::Rect empty{ 0.0f, 0.0f, 0.0f, 0.0f };
|
|
auto const current = geometry.Rect();
|
|
if (current.Width != 0.0f || current.Height != 0.0f || current.X != 0.0f || current.Y != 0.0f)
|
|
{
|
|
geometry.Rect(empty);
|
|
}
|
|
if (gutter.IsHitTestVisible())
|
|
{
|
|
gutter.IsHitTestVisible(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Not arranged yet (or flat): leave the gutter unclipped until the first arrange reports a slot.
|
|
if (Level() <= 0 || m_leadCellWidth < 0.0 || m_cellsArrangedHeight <= 0.0)
|
|
{
|
|
if (gutter.Clip())
|
|
{
|
|
gutter.Clip(nullptr);
|
|
}
|
|
if (!gutter.IsHitTestVisible())
|
|
{
|
|
gutter.IsHitTestVisible(true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const double gutterLeft = gutter.Margin().Left;
|
|
const double visibleLeft = (std::max)(0.0, m_leadCellLeft - gutterLeft);
|
|
const double visibleWidth = (std::max)(0.0, m_leadCellLeft + m_leadCellWidth - gutterLeft - visibleLeft);
|
|
|
|
auto geometry = gutter.Clip();
|
|
if (!geometry)
|
|
{
|
|
geometry = winrt::RectangleGeometry();
|
|
gutter.Clip(geometry);
|
|
}
|
|
const winrt::Rect rect{
|
|
static_cast<float>(visibleLeft),
|
|
0.0f,
|
|
static_cast<float>(visibleWidth),
|
|
static_cast<float>(m_cellsArrangedHeight) };
|
|
auto const current = geometry.Rect();
|
|
if (current.X != rect.X || current.Y != rect.Y || current.Width != rect.Width || current.Height != rect.Height)
|
|
{
|
|
geometry.Rect(rect);
|
|
}
|
|
|
|
const bool hitTestable = visibleWidth > 0.0;
|
|
if (gutter.IsHitTestVisible() != hitTestable)
|
|
{
|
|
gutter.IsHitTestVisible(hitTestable);
|
|
}
|
|
}
|
|
|
|
void TableViewRow::RefreshDensity()
|
|
{
|
|
// Re-read density metrics and restamp built-in cell padding.
|
|
RebuildCells();
|
|
}
|
|
|
|
void TableViewRow::RefreshCells()
|
|
{
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
ResetCellAutomationNames();
|
|
host.Children().Clear();
|
|
}
|
|
|
|
RebuildCells();
|
|
}
|
|
|
|
void TableViewRow::RefreshColumnVisibility(const winrt::TableViewColumn& column, winrt::Visibility visibility)
|
|
{
|
|
if (!column)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (auto cell = TableViewCellsPanel::CellForColumn(m_cellsHost.get(), column))
|
|
{
|
|
cell.Visibility(visibility);
|
|
}
|
|
|
|
// Lead cell may have changed. Indent only: runs from a column property callback, so no
|
|
// visual-state transitions.
|
|
ApplyHierarchyIndentToCells();
|
|
}
|
|
|
|
double TableViewRow::MeasuredWidthForColumn(const winrt::TableViewColumn& column) const
|
|
{
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
if (auto cellsPanel = host.try_as<winrt::TableViewCellsPanel>())
|
|
{
|
|
return winrt::get_self<TableViewCellsPanel>(cellsPanel)->MeasuredWidthForColumn(column);
|
|
}
|
|
}
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
void TableViewRow::InvalidateCells()
|
|
{
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
host.InvalidateMeasure();
|
|
}
|
|
}
|
|
|
|
void TableViewRow::ResetCellAutomationNames()
|
|
{
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
for (auto const& cell : host.Children())
|
|
{
|
|
try
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cell)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewCellAutomationPeer>(peer)->ResetEditName();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional released-cell name state could not be reset.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TableViewRow::HasCellAutomationPeers() const
|
|
{
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
for (auto const& cell : host.Children())
|
|
{
|
|
try
|
|
{
|
|
if (TableViewCell::TryGetExistingPeer(cell))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
// Unknown provider state: take the replace path, which is always correct.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TableViewRow::RaiseCellAutomationNamesChangedInternal(winrt::UIElement const& skipCell)
|
|
{
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
for (auto const& cell : host.Children())
|
|
{
|
|
if (skipCell && cell == skipCell)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cell)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
if (auto const oldName = winrt::get_self<TableViewCellAutomationPeer>(peer)->TakeLastNameInternal())
|
|
{
|
|
if (auto const newName = peer.GetName(); newName != *oldName)
|
|
{
|
|
peer.RaisePropertyChangedEvent(winrt::AutomationElementIdentifiers::NameProperty(),
|
|
winrt::box_value(*oldName), winrt::box_value(newName));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional cell name change could not be raised.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TableViewRow::RebuildCells(bool updateExistingCellPeerItems)
|
|
{
|
|
RestoreCellContentTabStopsInternal();
|
|
|
|
auto host = m_cellsHost.get();
|
|
if (!host)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Re-entry guard. See OnDataContextChanged.
|
|
if (m_isRebuildingCells)
|
|
{
|
|
return false;
|
|
}
|
|
m_isRebuildingCells = true;
|
|
// Synchronous RAII guard: captures this only until RebuildCells returns.
|
|
auto rebuildGuard = wil::scope_exit([this]() { m_isRebuildingCells = false; });
|
|
|
|
auto owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
// Keep realized cells on recycle-out so the next ElementPrepared restamps
|
|
// instead of re-generating every cell (the restamp fast-path exists for this).
|
|
return false;
|
|
}
|
|
|
|
// Density: apply the owning TableView's row min-height (Fluent Standard = 40,
|
|
// Compact = 30, Comfortable = 48; resolved from the TableViewRowMinHeight* resources).
|
|
const auto ownerImpl = winrt::get_self<TableView>(owner);
|
|
const auto rowMinHeight = ownerImpl->GetDensityRowMinHeight();
|
|
if (MinHeight() != rowMinHeight)
|
|
{
|
|
MinHeight(rowMinHeight);
|
|
}
|
|
|
|
auto columns = owner.Columns();
|
|
auto isOwnedColumn = [&owner](winrt::TableViewColumn const& column)
|
|
{
|
|
return column && winrt::get_self<TableViewColumn>(column)->GetOwningTableView() == owner;
|
|
};
|
|
if (!columns)
|
|
{
|
|
// Columns went away entirely. Clearing here without closing the edit first would rip a live
|
|
// editor out of the tree while the control still believed it was editing - and it wedges
|
|
// permanently: the next pass sees zero children AND zero columns, takes the restamp
|
|
// fast-path below, and returns before ever reaching the teardown. IsEditing would stay true
|
|
// for the lifetime of the control, with no editor and no way for the app to recover.
|
|
if (m_editingCellWrapper.get())
|
|
{
|
|
if (auto const editOwner = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(editOwner)->TerminateEditWithoutVisualRestore(true /* insideLayoutPass */);
|
|
}
|
|
else
|
|
{
|
|
AbandonCellEdit();
|
|
}
|
|
|
|
// Refused to close (an edit still in its Beginning window). Leave the cells alone
|
|
// rather than orphan the editor; the next pass retries once the edit has closed.
|
|
if (m_editingCellWrapper.get())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ResetCellAutomationNames();
|
|
host.Children().Clear();
|
|
return false;
|
|
}
|
|
|
|
auto dataContext = DataContext();
|
|
winrt::IInspectable dataItem = dataContext;
|
|
const auto children = host.Children();
|
|
if (updateExistingCellPeerItems)
|
|
{
|
|
try
|
|
{
|
|
for (auto const& cell : children)
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cell)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewCellAutomationPeer>(peer)->UpdateNameItem(dataItem);
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional recycled-cell name state could not be refreshed.");
|
|
}
|
|
}
|
|
|
|
uint32_t nonNullColumnCount = 0;
|
|
for (auto const& column : columns)
|
|
{
|
|
if (isOwnedColumn(column))
|
|
{
|
|
++nonNullColumnCount;
|
|
}
|
|
}
|
|
|
|
bool canRestampCells = children.Size() == nonNullColumnCount;
|
|
|
|
// The fast-path reuses cells and deliberately does not close an open edit. That is only sound
|
|
// while the editor is still one of those cells. If it has been orphaned - its wrapper is no
|
|
// longer a child - reusing them would leave the control editing an element outside the tree,
|
|
// so force the rebuild path, which tears the edit down.
|
|
if (canRestampCells)
|
|
{
|
|
if (auto const editingWrapper = m_editingCellWrapper.get())
|
|
{
|
|
bool stillAttached = false;
|
|
for (uint32_t i = 0; i < children.Size(); ++i)
|
|
{
|
|
if (children.GetAt(i) == editingWrapper)
|
|
{
|
|
stillAttached = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!stillAttached)
|
|
{
|
|
canRestampCells = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t childIndex = 0;
|
|
if (canRestampCells)
|
|
{
|
|
for (auto const& column : columns)
|
|
{
|
|
if (!isOwnedColumn(column))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto cellWrapper = children.GetAt(childIndex).try_as<winrt::Grid>();
|
|
if (!cellWrapper || cellWrapper.Tag().try_as<winrt::TableViewColumn>() != column)
|
|
{
|
|
canRestampCells = false;
|
|
break;
|
|
}
|
|
++childIndex;
|
|
}
|
|
}
|
|
|
|
if (canRestampCells)
|
|
{
|
|
const auto cellPadding = ownerImpl->GetDensityCellPadding();
|
|
childIndex = 0;
|
|
for (auto const& column : columns)
|
|
{
|
|
if (!isOwnedColumn(column))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto cellWrapper = children.GetAt(childIndex).as<winrt::Grid>();
|
|
// Do NOT re-push data during ItemsRepeater measure: live DataContext/Content mutation re-entered layout and hit 0xc0000420 on scroll.
|
|
// Cells must update reactively from inherited DataContext; only equal-valued visual restamps are safe here.
|
|
if (cellWrapper.Visibility() != column.Visibility())
|
|
{
|
|
cellWrapper.Visibility(column.Visibility());
|
|
}
|
|
if (cellWrapper.MinHeight() != rowMinHeight)
|
|
{
|
|
cellWrapper.MinHeight(rowMinHeight);
|
|
}
|
|
|
|
if (auto cellElement = TableViewCell::Child(cellWrapper).try_as<winrt::FrameworkElement>())
|
|
{
|
|
if (auto textBlock = cellElement.try_as<winrt::TextBlock>())
|
|
{
|
|
if (textBlock.Padding() != cellPadding)
|
|
{
|
|
textBlock.Padding(cellPadding);
|
|
}
|
|
}
|
|
}
|
|
|
|
++childIndex;
|
|
}
|
|
|
|
// Pin immediately so recycled frozen cells do not wait for the next scroll.
|
|
if (auto owningView = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(owningView)->PinFrozenColumnsForRow(*this);
|
|
}
|
|
|
|
ApplyFocusLevelInternal();
|
|
|
|
RefreshGridLines();
|
|
RefreshRowBackground();
|
|
ApplyHierarchyAffordance();
|
|
return true;
|
|
}
|
|
|
|
// Column adds/removes/reorders or template changes rebuild cells; width-only changes are picked
|
|
// up automatically by the cell panel's arrange (it reads each column's resolved ActualWidth), so
|
|
// no cell rebuild is needed for those.
|
|
//
|
|
// This path destroys every cell, so an open editor cannot survive it. The restamp fast-path above
|
|
// deliberately does NOT tear the edit down: it reuses the cells, so an editor opened on this row
|
|
// stays valid through the routine re-measure that installing it provokes.
|
|
//
|
|
// The teardown must not restore the display child - RebuildCells can run inside the repeater's
|
|
// measure pass. See AbandonCellEdit.
|
|
if (m_editingCellWrapper.get())
|
|
{
|
|
if (auto const editOwner = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(editOwner)->TerminateEditWithoutVisualRestore(true /* insideLayoutPass */);
|
|
}
|
|
else
|
|
{
|
|
AbandonCellEdit();
|
|
}
|
|
|
|
// If the edit still refuses to close, clearing the children would rip the live editor out
|
|
// of the tree while the control still believes it is editing. Leave the cells alone; the
|
|
// rebuild happens on the next pass once the edit has actually closed.
|
|
if (m_editingCellWrapper.get())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ResetCellAutomationNames();
|
|
host.Children().Clear();
|
|
|
|
int32_t visibleColumnIndex = 0;
|
|
for (auto const& column : columns)
|
|
{
|
|
if (!isOwnedColumn(column))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const cellWrapper = TableViewCell::Create(*this, column, visibleColumnIndex);
|
|
if (column.Visibility() == winrt::Visibility::Visible)
|
|
{
|
|
++visibleColumnIndex;
|
|
}
|
|
cellWrapper.Tag(column);
|
|
cellWrapper.Visibility(column.Visibility());
|
|
cellWrapper.MinHeight(rowMinHeight);
|
|
ownerImpl->AttachFocusCornerRadius(cellWrapper);
|
|
|
|
// A null Background does not hit-test; Transparent keeps the full cell pressable so clicks
|
|
// in padding still set current cell and support double-click-to-edit.
|
|
if (!m_cellWrapperBackground)
|
|
{
|
|
m_cellWrapperBackground = winrt::SolidColorBrush{ winrt::Colors::Transparent() };
|
|
}
|
|
cellWrapper.Background(m_cellWrapperBackground);
|
|
|
|
// No local DataContext anywhere on the cell path: cells inherit the row item so recycled
|
|
// rows update reactively. Custom columns must also bind to inherited DataContext, not bake
|
|
// in the initial dataItem.
|
|
if (auto cellElement = column.GenerateElement(dataItem))
|
|
{
|
|
AttachCellContent(cellWrapper, cellElement);
|
|
}
|
|
|
|
// Opt-in per-cell tooltip. Set once; the binding then tracks the row's inherited DataContext.
|
|
if (auto const toolTipBinding = column.CellToolTipBinding())
|
|
{
|
|
TableViewDetails::ApplyCellToolTipBinding(cellWrapper, toolTipBinding);
|
|
}
|
|
|
|
host.Children().Append(cellWrapper);
|
|
}
|
|
|
|
// Pin immediately so rebuilt frozen cells do not wait for the next scroll.
|
|
if (auto owningView = GetOwningTableView())
|
|
{
|
|
winrt::get_self<TableView>(owningView)->PinFrozenColumnsForRow(*this);
|
|
}
|
|
|
|
// New wrappers default to IsTabStop(true); restamp the current level so rebuilds do not reopen
|
|
// the row-to-first-cell Tab leak.
|
|
ApplyFocusLevelInternal();
|
|
|
|
RefreshGridLines();
|
|
RefreshRowBackground();
|
|
ApplyHierarchyAffordance();
|
|
return true;
|
|
}
|
|
|
|
// Recycle-out. Always walks: the bound value can revive a tooltip at any time, so a cached
|
|
// per-row flag would go stale and strand app content in the pool.
|
|
void TableViewRow::ReleaseCellToolTips()
|
|
{
|
|
if (auto const host = m_cellsHost.get())
|
|
{
|
|
ClearOwnedCellToolTips(host);
|
|
}
|
|
}
|
|
|
|
// Recycle-in. ReleaseCellToolTips neutralized the tooltips, and a bound value that did not change
|
|
// (an explicit Source, or equal across the DataContext reset) raises no callback to revive them.
|
|
void TableViewRow::RefreshCellToolTips()
|
|
{
|
|
auto const host = m_cellsHost.get();
|
|
if (!host)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const editingWrapper = m_editingCellWrapper.get();
|
|
auto const children = host.Children();
|
|
const uint32_t count = children.Size();
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
if (auto const cellWrapper = children.GetAt(i).try_as<winrt::Grid>();
|
|
cellWrapper && cellWrapper != editingWrapper)
|
|
{
|
|
// Usually the value changed with the DataContext and the tooltip is already live.
|
|
auto const record = TableViewDetails::GetRecord(cellWrapper);
|
|
if (!record || !record->ToolTip || record->ToolTip.IsEnabled())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
TableViewDetails::RefreshOwnedToolTip(cellWrapper);
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Re-applying a cell tooltip failed (HRESULT 0x%08X).",
|
|
static_cast<unsigned int>(winrt::to_hresult()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void TableViewRow::ClearOwnedCellToolTips(const winrt::Panel& host)
|
|
{
|
|
auto const children = host.Children();
|
|
const uint32_t count = children.Size();
|
|
for (uint32_t i = 0; i < count; ++i)
|
|
{
|
|
if (auto const cellWrapper = children.GetAt(i).try_as<winrt::Grid>())
|
|
{
|
|
TableViewDetails::ClearOwnedToolTip(cellWrapper);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Installs a generated display element as a cell's content, including the ContentPresenter wiring a
|
|
// template column needs. Shared by the cell rebuild and by the post-commit refresh, because
|
|
// GenerateElement alone is NOT a complete cell - forgetting the second half leaves a template
|
|
// column's Content unbound and the cell blank.
|
|
void TableViewRow::AttachCellContent(const winrt::Grid& cellWrapper, const winrt::FrameworkElement& cellElement)
|
|
{
|
|
if (!cellWrapper || !cellElement)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TableViewCell::Child(cellWrapper, cellElement);
|
|
|
|
// Bind Content to the wrapper's inherited DataContext so recycled template cells track the new
|
|
// item; binding to the presenter itself would freeze stale content.
|
|
if (auto presenter = cellElement.try_as<winrt::ContentPresenter>())
|
|
{
|
|
if (presenter.ContentTemplate())
|
|
{
|
|
winrt::Microsoft::UI::Xaml::Data::Binding contentBinding;
|
|
contentBinding.Source(cellWrapper);
|
|
contentBinding.Path(winrt::PropertyPath{ L"DataContext" });
|
|
winrt::BindingOperations::SetBinding(
|
|
presenter,
|
|
winrt::ContentPresenter::ContentProperty(),
|
|
contentBinding);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Grid lines only: the row's horizontal bottom line and the vertical per-cell separators (driven by
|
|
// GridLinesVisibility). Row background / alternating banding lives in RefreshRowBackground.
|
|
void TableViewRow::RefreshGridLines()
|
|
{
|
|
auto owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto visibility = owner.GridLinesVisibility();
|
|
if (WantsHorizontalLines(visibility))
|
|
{
|
|
ClearLocalValueIfSet(*this, winrt::Control::BorderThicknessProperty());
|
|
}
|
|
else
|
|
{
|
|
SetThicknessIfChanged(*this, winrt::Control::BorderThicknessProperty(), s_zeroThickness);
|
|
}
|
|
|
|
if (auto gridLineBorder = m_gridLineBorder.get())
|
|
{
|
|
auto thickness = BorderThickness();
|
|
if (m_suppressBottomGridLine)
|
|
{
|
|
thickness.Bottom = 0.0;
|
|
}
|
|
if (gridLineBorder.BorderThickness() != thickness)
|
|
{
|
|
gridLineBorder.BorderThickness(thickness);
|
|
}
|
|
}
|
|
|
|
auto host = m_cellsHost.get();
|
|
if (!host)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const bool wantVertical = WantsVerticalLines(visibility);
|
|
winrt::Brush gridLineBrush{ nullptr };
|
|
// FlowDirection mirrors the whole subtree, so the logical right edge is trailing in RTL too.
|
|
const auto verticalThickness = s_verticalThickness;
|
|
if (wantVertical)
|
|
{
|
|
gridLineBrush = winrt::get_self<TableView>(owner)->GetGridLineBrush();
|
|
}
|
|
|
|
const auto children = host.Children();
|
|
const uint32_t childCount = children.Size();
|
|
uint32_t lastVisibleCell = childCount;
|
|
// The cell wrapper is a composed Grid. Border is sealed and cannot host a custom
|
|
// automation peer, so both loops in RefreshGridLines must cast cell wrappers to Grid.
|
|
for (uint32_t i = childCount; i > 0; --i)
|
|
{
|
|
if (auto cellWrapper = children.GetAt(i - 1).try_as<winrt::Grid>())
|
|
{
|
|
const auto column = cellWrapper.Tag().try_as<winrt::TableViewColumn>();
|
|
if (cellWrapper.Visibility() == winrt::Visibility::Visible &&
|
|
column &&
|
|
column.ActualWidth() > 0.0)
|
|
{
|
|
lastVisibleCell = i - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (uint32_t i = 0; i < childCount; ++i)
|
|
{
|
|
if (auto cellWrapper = children.GetAt(i).try_as<winrt::Grid>())
|
|
{
|
|
if (wantVertical)
|
|
{
|
|
// RTL-aware thickness, not the LTR-only static: the separator must sit on the
|
|
// trailing edge in both flow directions.
|
|
if (cellWrapper.BorderThickness() != verticalThickness)
|
|
{
|
|
cellWrapper.BorderThickness(verticalThickness);
|
|
}
|
|
// Keep the separator's layout thickness stable and suppress only its brush when
|
|
// the terminal cell actually meets the outer border.
|
|
auto const brush =
|
|
m_suppressTrailingGridLine &&
|
|
cellWrapper.Visibility() == winrt::Visibility::Visible &&
|
|
i == lastVisibleCell
|
|
? nullptr
|
|
: gridLineBrush;
|
|
if (cellWrapper.BorderBrush() != brush)
|
|
{
|
|
cellWrapper.BorderBrush(brush);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearLocalValueIfSet(cellWrapper, winrt::Grid::BorderThicknessProperty());
|
|
ClearLocalValueIfSet(cellWrapper, winrt::Grid::BorderBrushProperty());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Row background / alternating banding only. Index-dependent (parity), so it must refresh when the
|
|
// row's position changes on recycle, and when RowBackground / AlternatingRowBackground change.
|
|
void TableViewRow::RefreshRowBackground()
|
|
{
|
|
auto owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Resolve the target first and write only on change, so recycled rows drop stale banding fills
|
|
// without a clear-then-set churn on every DataContext change.
|
|
winrt::Brush background{ nullptr };
|
|
if (owner.RowBackground() != nullptr || owner.AlternatingRowBackground() != nullptr)
|
|
{
|
|
auto rowIndex = -1;
|
|
if (auto repeater = winrt::get_self<TableView>(owner)->GetRowsRepeaterInternal())
|
|
{
|
|
rowIndex = repeater.GetElementIndex(*this);
|
|
}
|
|
if (rowIndex >= 0)
|
|
{
|
|
// RowBackground is the base for every row; AlternatingRowBackground overrides
|
|
// odd rows only when set (WPF DataGrid parity). Setting RowBackground alone
|
|
// must fill all rows uniformly, not stripe odd rows transparent.
|
|
background = owner.RowBackground();
|
|
if ((rowIndex % 2) != 0 && owner.AlternatingRowBackground() != nullptr)
|
|
{
|
|
background = owner.AlternatingRowBackground();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (background)
|
|
{
|
|
if (ReadLocalValue(winrt::Control::BackgroundProperty()) != background)
|
|
{
|
|
Background(background);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearLocalValueIfSet(*this, winrt::Control::BackgroundProperty());
|
|
}
|
|
}
|
|
|
|
void TableViewRow::RefreshFrozenColumnLayout(double horizontalOffset, double leadingFrozenWidth)
|
|
{
|
|
if (auto host = m_cellsHost.get())
|
|
{
|
|
TableViewCellsPanel::ApplyFrozenColumnLayout(host, horizontalOffset, leadingFrozenWidth);
|
|
|
|
// Same gate as ApplyHierarchyAffordance: flat rows skip this on every scroll pass, but a
|
|
// row leaving a tree still resets its gutter Translation.
|
|
auto const gutter = m_rowExpanderGutter.get();
|
|
if (gutter && (Level() > 0 || gutter.Translation().x != 0.0f))
|
|
{
|
|
SyncExpanderGutterWithLeadCell(host);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gutter sits outside the cells host, so mirror the lead visible wrapper's Translation (whatever
|
|
// frozen pinning decided, including none) to keep the chevron over its cell.
|
|
void TableViewRow::SyncExpanderGutterWithLeadCell(winrt::Panel const& host)
|
|
{
|
|
auto const gutter = m_rowExpanderGutter.get();
|
|
if (!gutter)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float offset = 0.0f;
|
|
if (Level() > 0)
|
|
{
|
|
auto const children = host.Children();
|
|
for (uint32_t i = 0; i < children.Size(); ++i)
|
|
{
|
|
auto const wrapper = children.GetAt(i);
|
|
if (TableViewCellsPanel::ColumnForCell(wrapper) && wrapper.Visibility() == winrt::Visibility::Visible)
|
|
{
|
|
offset = wrapper.Translation().x;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
winrt::ElementCompositionPreview::SetIsTranslationEnabled(gutter, true);
|
|
auto const translation = gutter.Translation();
|
|
if (translation.x != offset || translation.y != 0.0f || translation.z != 0.0f)
|
|
{
|
|
gutter.Translation({ offset, 0.0f, 0.0f });
|
|
}
|
|
if (winrt::Canvas::GetZIndex(gutter) != 2)
|
|
{
|
|
winrt::Canvas::SetZIndex(gutter, 2);
|
|
}
|
|
}
|
|
|
|
// ----- Editing -----
|
|
//
|
|
// The row owns its cells, so the control delegates the display/editor swap here. The swap replaces
|
|
// Child on the existing cell wrapper Border: keeping the wrapper means column width, visibility,
|
|
// frozen pinning and grid lines keep applying while the cell is edited, with no layout re-plumbing.
|
|
|
|
bool TableViewRow::BeginCellEdit(const winrt::TableViewColumn& column, const winrt::IInspectable& dataItem)
|
|
{
|
|
if (!column)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// An edit already open on this row is closed first. Committing it is the control's job, not
|
|
// the row's; by this point the control has already ended it, so anything still open here is
|
|
// stale visual state.
|
|
EndCellEdit(winrt::TableViewEditAction::Cancel);
|
|
|
|
auto const host = m_cellsHost.get();
|
|
if (!host)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
winrt::Grid cellWrapper{ nullptr };
|
|
for (auto const& child : host.Children())
|
|
{
|
|
if (auto const cell = child.try_as<winrt::Grid>())
|
|
{
|
|
if (cell.Tag().try_as<winrt::TableViewColumn>() == column)
|
|
{
|
|
cellWrapper = cell;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!cellWrapper)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// A throw past here must restore the display element and drop the half-captured edit state, or
|
|
// the cell is left blank and the row half-editing.
|
|
auto const displayElement = TableViewCell::Child(cellWrapper);
|
|
auto const focusRoot = XamlRoot();
|
|
auto const restoreFocusState = [&]()
|
|
{
|
|
auto const focused = focusRoot ? winrt::FocusManager::GetFocusedElement(focusRoot) : nullptr;
|
|
if (!TableViewDetails::IsWithinElementPopupAware(focused, cellWrapper, focusRoot))
|
|
{
|
|
return winrt::FocusState::Unfocused;
|
|
}
|
|
auto const focusedElement = focused.try_as<winrt::UIElement>();
|
|
return focusedElement && focusedElement.FocusState() != winrt::FocusState::Unfocused
|
|
? focusedElement.FocusState() : winrt::FocusState::Programmatic;
|
|
}();
|
|
auto unwindOnThrow = wil::scope_exit([this, cellWrapper, displayElement, focusRoot, restoreFocusState]() noexcept
|
|
{
|
|
try
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cellWrapper)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewCellAutomationPeer>(peer)->ResetEditName();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional abandoned-cell name state could not be reset.");
|
|
}
|
|
try
|
|
{
|
|
if (TableViewCell::Child(cellWrapper) != displayElement)
|
|
{
|
|
TableViewCell::Child(cellWrapper, displayElement);
|
|
TableViewDetails::RefreshOwnedToolTip(cellWrapper);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] A failed BeginCellEdit could not restore the display cell.");
|
|
}
|
|
m_editingAutomationPeer = nullptr;
|
|
m_editingAutomationItem.set(nullptr);
|
|
m_editingAutomationValue = {};
|
|
m_editingAutomationName = {};
|
|
m_editingRowAutomationName.reset();
|
|
m_editingColumn.set(nullptr);
|
|
m_editingCellWrapper.set(nullptr);
|
|
m_editingElement.set(nullptr);
|
|
m_editingDisplayElement.set(nullptr);
|
|
|
|
// Not a layout pass (unlike AbandonCellEdit's callers), so focus the editor took or dropped
|
|
// can go back to the cell.
|
|
try
|
|
{
|
|
if (restoreFocusState != winrt::FocusState::Unfocused &&
|
|
!TableViewDetails::IsWithinElementPopupAware(
|
|
focusRoot ? winrt::FocusManager::GetFocusedElement(focusRoot) : nullptr, cellWrapper, focusRoot))
|
|
{
|
|
SetCellLevelInternal(true);
|
|
if (!cellWrapper.Focus(restoreFocusState))
|
|
{
|
|
SetCellLevelInternal(false);
|
|
Focus(restoreFocusState);
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] A failed BeginCellEdit could not restore focus to the cell.");
|
|
}
|
|
});
|
|
|
|
auto const editingElement = winrt::get_self<TableViewColumn>(column)->GenerateEditingElement(dataItem);
|
|
if (!editingElement)
|
|
{
|
|
unwindOnThrow.release();
|
|
return false;
|
|
}
|
|
|
|
// No local DataContext on the editing element, for the same reason the display cell sets none:
|
|
// it inherits from the wrapper, which tracks the item across row recycle.
|
|
// Observe only a provider a client already obtained. Creating peers here would turn every
|
|
// ordinary edit into a UIA-tree allocation and could give the event a different identity.
|
|
try
|
|
{
|
|
// Gate the snapshot work, not the edit: this function's return value starts the edit.
|
|
auto const peer = winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::PropertyChanged)
|
|
? TableViewCell::TryGetExistingPeer(cellWrapper).try_as<winrt::TableViewCellAutomationPeer>()
|
|
: nullptr;
|
|
if (peer && peer.GetPattern(winrt::PatternInterface::Value))
|
|
{
|
|
auto const value = winrt::get_self<TableViewCellAutomationPeer>(peer)->Value();
|
|
auto const name = winrt::get_self<TableViewCellAutomationPeer>(peer)->ReadNameForEdit();
|
|
auto const weakPeer = winrt::make_weak(peer);
|
|
m_editingAutomationItem.set(dataItem);
|
|
m_editingAutomationValue = value;
|
|
m_editingAutomationName = name;
|
|
m_editingAutomationPeer = weakPeer;
|
|
}
|
|
m_editingRowAutomationName.reset();
|
|
if (winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::PropertyChanged))
|
|
{
|
|
if (auto const rowPeer = winrt::FrameworkElementAutomationPeer::FromElement(*this))
|
|
{
|
|
m_editingRowAutomationName = rowPeer.GetName();
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
m_editingAutomationPeer = nullptr;
|
|
m_editingAutomationItem.set(nullptr);
|
|
m_editingAutomationValue = {};
|
|
m_editingAutomationName = {};
|
|
m_editingRowAutomationName.reset();
|
|
TVDiag::LogRetailF(L"[TableView] Optional pre-edit UIA snapshot could not be captured.");
|
|
}
|
|
try
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cellWrapper)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewCellAutomationPeer>(peer)->BeginEditName();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional stable cell-name capture failed.");
|
|
}
|
|
m_editingDisplayElement.set(displayElement);
|
|
m_pendingEditingCell = winrt::make_weak(cellWrapper.as<winrt::UIElement>());
|
|
auto pendingCellGuard = wil::scope_exit([this]() noexcept { m_pendingEditingCell = nullptr; });
|
|
TableViewCell::Child(cellWrapper, editingElement);
|
|
|
|
m_editingColumn.set(column);
|
|
m_editingCellWrapper.set(cellWrapper);
|
|
TableViewDetails::ClearOwnedToolTip(cellWrapper);
|
|
m_editingElement.set(editingElement);
|
|
|
|
unwindOnThrow.release();
|
|
return true;
|
|
}
|
|
|
|
void TableViewRow::EndCellEdit(winrt::TableViewEditAction action, std::optional<HRESULT>* displayFailure, bool* applicationFault)
|
|
{
|
|
auto const cellWrapper = m_editingCellWrapper.get();
|
|
auto const weakPeer = m_editingAutomationPeer;
|
|
winrt::IInspectable originalItem{ nullptr };
|
|
winrt::hstring oldValue;
|
|
winrt::hstring oldName;
|
|
try
|
|
{
|
|
originalItem = m_editingAutomationItem.get();
|
|
oldValue = std::move(m_editingAutomationValue);
|
|
oldName = std::move(m_editingAutomationName);
|
|
}
|
|
catch (...)
|
|
{
|
|
originalItem = nullptr;
|
|
TVDiag::LogRetailF(L"[TableView] Optional edit UIA snapshot could not be retrieved.");
|
|
}
|
|
m_editingAutomationPeer = nullptr;
|
|
m_editingAutomationItem.set(nullptr);
|
|
m_editingAutomationValue = {};
|
|
m_editingAutomationName = {};
|
|
auto const oldRowName = std::move(m_editingRowAutomationName);
|
|
m_editingRowAutomationName.reset();
|
|
if (!cellWrapper)
|
|
{
|
|
m_editingColumn.set(nullptr);
|
|
m_editingElement.set(nullptr);
|
|
m_editingDisplayElement.set(nullptr);
|
|
return;
|
|
}
|
|
|
|
// Move focus off the editor before it leaves the tree. Dropping a focused element causes XAML
|
|
// to fall back to whatever it can find, which can scroll the list; the row is the correct
|
|
// landing spot and is where keyboard navigation expects focus to be.
|
|
if (auto const editingElement = m_editingElement.get())
|
|
{
|
|
bool editorHasFocus = false;
|
|
auto restoreState = winrt::FocusState::Programmatic;
|
|
if (auto const root = XamlRoot())
|
|
{
|
|
auto const focused = winrt::FocusManager::GetFocusedElement(root);
|
|
editorHasFocus = TableViewDetails::IsWithinElementPopupAware(focused, editingElement, root);
|
|
if (editorHasFocus)
|
|
{
|
|
// Read from the focused element: inside a dropdown the editor itself reports Unfocused.
|
|
if (auto const focusedElement = focused.try_as<winrt::UIElement>();
|
|
focusedElement && focusedElement.FocusState() != winrt::FocusState::Unfocused)
|
|
{
|
|
restoreState = focusedElement.FocusState();
|
|
}
|
|
}
|
|
}
|
|
|
|
TableViewDetails::CloseOpenDropDowns(editingElement);
|
|
|
|
if (editorHasFocus)
|
|
{
|
|
// Restore focus to the cell, not row, and preserve Keyboard focus state so the focus
|
|
// rectangle survives Enter-commit.
|
|
// Closing edit returns to cell level, so re-arm cells before restoring focus.
|
|
SetCellLevelInternal(true);
|
|
|
|
if (!cellWrapper.Focus(restoreState))
|
|
{
|
|
SetCellLevelInternal(false);
|
|
Focus(restoreState);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regenerate the display cell on commit rather than re-parenting the one that was parked when
|
|
// the edit opened.
|
|
//
|
|
// The parked element still holds the OneWay binding it was created with, and a OneWay binding
|
|
// only re-reads its source when that source raises PropertyChanged. For an item that does not
|
|
// implement INotifyPropertyChanged the committed value would therefore never appear - the user
|
|
// types, presses Enter, the item is updated, and the cell keeps showing the old text. A freshly
|
|
// generated element evaluates its binding immediately against the current value, so the commit
|
|
// is visible for plain POCOs too. This is what WPF's DataGrid does for the same reason.
|
|
//
|
|
// Only on Commit: a cancel wrote nothing, so the parked element is still correct and
|
|
// regenerating it would be pure churn.
|
|
winrt::UIElement displayElement = m_editingDisplayElement.get();
|
|
if (action == winrt::TableViewEditAction::Commit)
|
|
{
|
|
if (auto const column = m_editingColumn.get())
|
|
{
|
|
bool factoryReturned = false;
|
|
winrt::FrameworkElement refreshed{ nullptr };
|
|
try
|
|
{
|
|
refreshed = column.GenerateElement(DataContext());
|
|
factoryReturned = true;
|
|
if (refreshed)
|
|
{
|
|
AttachCellContent(cellWrapper, refreshed);
|
|
displayElement = nullptr;
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
if (displayFailure) { *displayFailure = winrt::to_hresult(); }
|
|
// An app-derived column's factory threw, or returned an element that already has a parent: both
|
|
// are the app's. TableView's own columns build a new element.
|
|
if (applicationFault)
|
|
{
|
|
*applicationFault = TableViewDetails::IsAppDerivedColumn(column) &&
|
|
(!factoryReturned || HasForeignParent(refreshed, cellWrapper));
|
|
}
|
|
// A column that throws while regenerating must not strand the row in edit mode;
|
|
// fall back to the parked element, which is stale but present.
|
|
displayElement = m_editingDisplayElement.get();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (displayElement)
|
|
{
|
|
TableViewCell::Child(cellWrapper, displayElement);
|
|
}
|
|
|
|
m_editingColumn.set(nullptr);
|
|
m_editingCellWrapper.set(nullptr);
|
|
m_editingElement.set(nullptr);
|
|
m_editingDisplayElement.set(nullptr);
|
|
|
|
// The bound value did not change, so only an explicit re-apply restores what the edit retracted.
|
|
TableViewDetails::RefreshOwnedToolTip(cellWrapper);
|
|
|
|
try
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cellWrapper)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewCellAutomationPeer>(peer)->EndEditName();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional final cell-name invalidation could not be prepared.");
|
|
}
|
|
|
|
try
|
|
{
|
|
if (action == winrt::TableViewEditAction::Commit && originalItem &&
|
|
TableView::SameInspectableIdentity(DataContext(), originalItem))
|
|
{
|
|
if (auto const peer = weakPeer.get())
|
|
{
|
|
auto const newValue = winrt::get_self<TableViewCellAutomationPeer>(peer)->Value();
|
|
auto const newName = winrt::get_self<TableViewCellAutomationPeer>(peer)->ReadNameForEdit();
|
|
if (oldValue != newValue || oldName != newName)
|
|
{
|
|
// Publish after Ending; reject recycled cells and superseded values.
|
|
if (auto const queue = DispatcherQueue())
|
|
{
|
|
auto const weakThis = get_weak();
|
|
auto const weakCell = winrt::make_weak(cellWrapper);
|
|
if (!queue.TryEnqueue([weakThis, weakPeer, weakCell, originalItem, oldValue, newValue, newName]()
|
|
{
|
|
try
|
|
{
|
|
auto const row = weakThis.get();
|
|
auto const currentPeer = weakPeer.get();
|
|
auto const cell = weakCell.get();
|
|
if (!row || !currentPeer || !cell ||
|
|
currentPeer.Owner() != cell ||
|
|
winrt::VisualTreeHelper::GetParent(cell).try_as<winrt::Panel>() != row->GetCellsHostPanelInternal() ||
|
|
!TableView::SameInspectableIdentity(row->DataContext(), originalItem))
|
|
{
|
|
return;
|
|
}
|
|
auto const peerImpl = winrt::get_self<TableViewCellAutomationPeer>(currentPeer);
|
|
if (peerImpl->GetRowIndexInternal() < 0 || peerImpl->Value() != newValue || peerImpl->ReadNameForEdit() != newName)
|
|
{
|
|
return;
|
|
}
|
|
if (oldValue != newValue)
|
|
{
|
|
currentPeer.RaisePropertyChangedEvent(winrt::ValuePatternIdentifiers::ValueProperty(),
|
|
winrt::box_value(oldValue), winrt::box_value(newValue));
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] A committed cell's UIA notification could not be delivered.");
|
|
}
|
|
}))
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional committed-cell UIA notification queue rejected delivery.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional committed-cell UIA notification has no dispatcher.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional committed-cell UIA notification could not be prepared.");
|
|
}
|
|
|
|
// Other cells and the row Name can derive from the edited value (a template column bound to a
|
|
// computed property). Their peers compute names live, but clients that cache need the change.
|
|
try
|
|
{
|
|
if (action == winrt::TableViewEditAction::Commit &&
|
|
winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::PropertyChanged))
|
|
{
|
|
if (auto const queue = DispatcherQueue())
|
|
{
|
|
auto const weakThis = get_weak();
|
|
auto const weakCell = winrt::make_weak(cellWrapper.as<winrt::UIElement>());
|
|
auto const item = DataContext();
|
|
if (!queue.TryEnqueue(winrt::DispatcherQueuePriority::Low, [weakThis, weakCell, item, oldRowName]()
|
|
{
|
|
try
|
|
{
|
|
auto const row = weakThis.get();
|
|
if (!row || !TableView::SameInspectableIdentity(row->DataContext(), item))
|
|
{
|
|
return;
|
|
}
|
|
row->RaiseCellAutomationNamesChangedInternal(weakCell.get());
|
|
if (oldRowName)
|
|
{
|
|
if (auto const rowPeer = winrt::FrameworkElementAutomationPeer::FromElement(*row))
|
|
{
|
|
if (auto const newRowName = rowPeer.GetName(); newRowName != *oldRowName)
|
|
{
|
|
rowPeer.RaisePropertyChangedEvent(winrt::AutomationElementIdentifiers::NameProperty(),
|
|
winrt::box_value(*oldRowName), winrt::box_value(newRowName));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional committed-row name notification could not be delivered.");
|
|
}
|
|
}))
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional committed-row name notification queue rejected delivery.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional committed-row name notification could not be prepared.");
|
|
}
|
|
}
|
|
|
|
void TableViewRow::AbandonCellEdit()
|
|
{
|
|
try
|
|
{
|
|
if (auto const cell = m_editingCellWrapper.get())
|
|
{
|
|
if (auto const peer = TableViewCell::TryGetExistingPeer(cell)
|
|
.try_as<winrt::TableViewCellAutomationPeer>())
|
|
{
|
|
winrt::get_self<TableViewCellAutomationPeer>(peer)->ResetEditName();
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
TVDiag::LogRetailF(L"[TableView] Optional abandoned-cell name state could not be reset.");
|
|
}
|
|
m_editingAutomationPeer = nullptr;
|
|
m_editingAutomationItem.set(nullptr);
|
|
m_editingAutomationValue = {};
|
|
m_editingAutomationName = {};
|
|
// Restores the display child, but deliberately does NOT touch focus. Callers run inside a layout
|
|
// pass, where moving focus re-enters the framework and trips the re-entrancy guard. Replacing
|
|
// the child does not - and it must happen, or the row keeps showing a TextBox after the edit
|
|
// closed, and over a different item once recycled.
|
|
auto const cellWrapper = m_editingCellWrapper.get();
|
|
if (cellWrapper)
|
|
{
|
|
TableViewCell::Child(cellWrapper, m_editingDisplayElement.get());
|
|
}
|
|
|
|
m_editingColumn.set(nullptr);
|
|
m_editingCellWrapper.set(nullptr);
|
|
m_editingElement.set(nullptr);
|
|
m_editingDisplayElement.set(nullptr);
|
|
|
|
// The cell is a display cell again; restore the tooltip the edit retracted.
|
|
TableViewDetails::RefreshOwnedToolTip(cellWrapper);
|
|
}
|
|
|
|
void TableViewRow::ResetPressState()
|
|
{
|
|
m_lastPressTimestamp = 0;
|
|
m_lastPressPosition = {};
|
|
m_lastPressColumn.set(nullptr);
|
|
m_lastPressItem.set(nullptr);
|
|
}
|
|
|
|
void TableViewRow::ProcessContextRequest(
|
|
TableViewDetails::ContextMenuTarget const& target,
|
|
winrt::ContextRequestedEventArgs const& args)
|
|
{
|
|
auto const owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (winrt::get_self<TableView>(owner)->ProcessContextMenuRequest(target, args))
|
|
{
|
|
args.Handled(true);
|
|
}
|
|
}
|
|
|
|
// Cell requests (pointer, keyboard, cell peer). Requests on the host's empty area carry no column
|
|
// and go on to the template root as row requests.
|
|
void TableViewRow::OnCellsHostContextRequested(winrt::ContextRequestedEventArgs const& args)
|
|
{
|
|
if (args.Handled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (auto const target = ResolveContextMenuTarget(args); target && target->Column)
|
|
{
|
|
ProcessContextRequest(*target, args);
|
|
}
|
|
}
|
|
|
|
// Row requests from anywhere under the template root but outside a cell: padding, the chevron,
|
|
// the cells host's empty area. A ContextFlyout on the template root itself is shown by the
|
|
// framework before this handler runs.
|
|
void TableViewRow::OnTemplateRootContextRequested(winrt::ContextRequestedEventArgs const& args)
|
|
{
|
|
if (args.Handled())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// A cell request the cells host left Unhandled (nothing to show) keeps bubbling.
|
|
if (auto const target = ResolveContextMenuTarget(args); target && !target->Column)
|
|
{
|
|
ProcessContextRequest(*target, args);
|
|
}
|
|
}
|
|
|
|
bool TableViewRow::ShowContextMenuFromPeer()
|
|
{
|
|
auto const owner = GetOwningTableView();
|
|
auto const target = MakeRowContextMenuTarget();
|
|
if (!owner || !target)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return winrt::get_self<TableView>(owner)->ProcessContextMenuRequest(*target, nullptr);
|
|
}
|
|
|
|
std::optional<TableViewDetails::ContextMenuTarget> TableViewRow::MakeRowContextMenuTarget()
|
|
{
|
|
auto const owner = GetOwningTableView();
|
|
auto const host = m_cellsHost.get();
|
|
if (!owner || !host)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
TableViewDetails::ContextMenuTarget target;
|
|
target.Row = *this;
|
|
target.Item = winrt::get_self<TableView>(owner)->UnwrapEditingDataItem(DataContext());
|
|
target.Anchor = *this;
|
|
target.ScopeRoot = host;
|
|
return target;
|
|
}
|
|
|
|
std::optional<TableViewDetails::ContextMenuTarget> TableViewRow::ResolveContextMenuTarget(
|
|
winrt::ContextRequestedEventArgs const& args)
|
|
{
|
|
auto target = MakeRowContextMenuTarget();
|
|
if (!target)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
winrt::TableViewRow const self = *this;
|
|
auto const source = args.OriginalSource().try_as<winrt::DependencyObject>();
|
|
if (!TableViewDetails::IsOwnRequestSource(source, self))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
// The column comes from the cell wrapper the request started in, never from a Tag on the
|
|
// cell's content and never from where the pointer is: the chevron overlays the first cell but
|
|
// belongs to the row.
|
|
auto const cell = FindOwnCellInternal(source, false /* requireExact */);
|
|
auto column = cell ? GetCellOwningColumn(cell) : nullptr;
|
|
if (column && !winrt::get_self<TableView>(GetOwningTableView())->IsContextMenuColumn(column))
|
|
{
|
|
column = nullptr;
|
|
}
|
|
|
|
auto const anchor = column ? TableViewCellsPanel::CellForColumn(target->ScopeRoot, column) : nullptr;
|
|
if (anchor)
|
|
{
|
|
target->Column = column;
|
|
target->Anchor = anchor;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
// Pointer entry point for editing, and the only place a pointer establishes the current cell.
|
|
//
|
|
// Lives on the row because the row owns its cells: resolving which cell a press landed on is a
|
|
// question only the row can answer cheaply. The control keeps the edit state machine, so this
|
|
// handler translates a gesture into SetCurrentCell / BeginEdit and nothing more. Mirrors WPF,
|
|
// where DataGridCell handles the gesture and calls DataGrid.BeginEdit.
|
|
//
|
|
// PointerPressed with click counting, not DoubleTapped: marking a press handled suppresses XAML's
|
|
// gesture recognizer entirely, and a row that participates in selection must mark it handled. A
|
|
// DoubleTapped handler would work today and silently break when selection lands.
|
|
void TableViewRow::OnPointerPressedForEditing(
|
|
const winrt::IInspectable& /*sender*/,
|
|
const winrt::PointerRoutedEventArgs& args)
|
|
{
|
|
auto const owner = GetOwningTableView();
|
|
if (!owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto ownerImpl = winrt::get_self<TableView>(owner);
|
|
|
|
// Editing is opt-in and read-only by default, so a read-only table pays nothing beyond this.
|
|
if (ownerImpl->IsReadOnly())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// A chevron press is a toggle, not a lead-cell press; otherwise two quick toggles would read as
|
|
// a double-click and open an editor. Leaf gutters (no toggle) fall through.
|
|
if (IsExpandable() && IsWithinExpanderGutter(args.OriginalSource()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const pointerPoint = args.GetCurrentPoint(*this);
|
|
if (!pointerPoint)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Mouse/pen: primary button only - a right-click opens a context menu and must not begin an
|
|
// edit. Touch reports no pressed button, so it is admitted on device type instead.
|
|
const auto deviceType = args.Pointer().PointerDeviceType();
|
|
if (deviceType != winrt::Microsoft::UI::Input::PointerDeviceType::Touch &&
|
|
!pointerPoint.Properties().IsLeftButtonPressed())
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const column = ResolvePressedColumn(args.OriginalSource(), args.GetCurrentPoint(nullptr).Position());
|
|
if (!column)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto const item = ownerImpl->UnwrapEditingDataItem(DataContext());
|
|
if (!item)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto timestamp = pointerPoint.Timestamp();
|
|
const auto position = pointerPoint.Position();
|
|
|
|
const double slop = GetDoubleClickSlop(*this);
|
|
|
|
// Device type is part of the repeat identity so a mouse click following a pen tap is not read
|
|
// as one double-click. Pointer ID deliberately is NOT: every touch contact gets a fresh id, so
|
|
// requiring equality would mean the second tap never matches and touch could never begin an
|
|
// edit at all.
|
|
const bool isRepeatPress =
|
|
m_lastPressColumn.get() == column &&
|
|
TableView::SameInspectableIdentity(m_lastPressItem.get(), item) &&
|
|
m_lastPressDeviceType == deviceType &&
|
|
timestamp >= m_lastPressTimestamp &&
|
|
(timestamp - m_lastPressTimestamp) <= GetDoubleClickIntervalMicroseconds() &&
|
|
std::abs(position.X - m_lastPressPosition.X) <= slop &&
|
|
std::abs(position.Y - m_lastPressPosition.Y) <= slop;
|
|
|
|
// A press inside the cell already being edited belongs to the editor: it is the user placing
|
|
// the caret, and must not be read as a navigation move or a fresh edit.
|
|
const bool pressInsideOpenEdit =
|
|
ownerImpl->IsEditing() &&
|
|
m_editingColumn.get() == column &&
|
|
m_editingElement.get() != nullptr;
|
|
|
|
if (!pressInsideOpenEdit)
|
|
{
|
|
// Move the current cell even when the edit is refused (read-only, or BeginningEdit cancels):
|
|
// the user pointed here, so a later F2 must not edit a different cell.
|
|
//
|
|
// This is also what makes keyboard editing reachable at all - pointer focus lands on the
|
|
// row, not a tagged cell, so without this the current column stays null.
|
|
ownerImpl->SetCurrentCell(item, column);
|
|
|
|
if (isRepeatPress)
|
|
{
|
|
ownerImpl->BeginEdit(item, column);
|
|
|
|
// Consume the press so a third click starts a fresh first press rather than
|
|
// re-entering begin-edit on a cell that is already open.
|
|
m_lastPressTimestamp = 0;
|
|
m_lastPressColumn.set(nullptr);
|
|
m_lastPressItem.set(nullptr);
|
|
return;
|
|
}
|
|
}
|
|
|
|
m_lastPressTimestamp = timestamp;
|
|
m_lastPressPosition = position;
|
|
m_lastPressColumn.set(column);
|
|
m_lastPressItem.set(item);
|
|
m_lastPressDeviceType = deviceType;
|
|
}
|
|
|
|
// Which of this row's cells a press landed on. Walks up from OriginalSource to the cell wrapper
|
|
// Grid, whose Tag carries the owning column (set in RebuildCells). Once the row itself has focus
|
|
// a press can arrive with the row as OriginalSource and no tagged cell on the chain, so fall back
|
|
// to hit-testing this row's subtree.
|
|
winrt::TableViewColumn TableViewRow::ResolvePressedColumn(
|
|
const winrt::IInspectable& originalSource,
|
|
const winrt::Point& hostPoint)
|
|
{
|
|
// A nested TableView's press bubbles through this row, and the walk would reach the INNER
|
|
// table's tagged Border before it ever reached this row - so filter on ownership rather than
|
|
// trying to stop the walk. A column this table does not own is not ours to act on.
|
|
auto const owner = GetOwningTableView();
|
|
auto const ownedByThisTable = [&owner](winrt::TableViewColumn const& candidate)
|
|
{
|
|
if (!candidate || !owner)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (auto const columns = owner.Columns())
|
|
{
|
|
for (auto const& column : columns)
|
|
{
|
|
if (column == candidate)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
auto current = originalSource.try_as<winrt::DependencyObject>();
|
|
while (current)
|
|
{
|
|
if (auto const cell = current.try_as<winrt::Grid>())
|
|
{
|
|
if (auto const tagged = cell.Tag().try_as<winrt::TableViewColumn>())
|
|
{
|
|
return ownedByThisTable(tagged) ? tagged : nullptr;
|
|
}
|
|
}
|
|
|
|
current = winrt::VisualTreeHelper::GetParent(current);
|
|
}
|
|
|
|
for (auto const& hit : winrt::VisualTreeHelper::FindElementsInHostCoordinates(hostPoint, *this))
|
|
{
|
|
if (auto const cell = hit.try_as<winrt::Grid>())
|
|
{
|
|
if (auto const tagged = cell.Tag().try_as<winrt::TableViewColumn>())
|
|
{
|
|
return ownedByThisTable(tagged) ? tagged : nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|