Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions controls/dev/Generated/TableView.properties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ GlobalDependencyProperty TableViewProperties::s_SelectionModeProperty{ nullptr }
TableViewProperties::TableViewProperties()
: m_beginningEditEventSource{static_cast<TableView*>(this)}
, m_cellEditEndingEventSource{static_cast<TableView*>(this)}
, m_cellToolTipRequestedEventSource{static_cast<TableView*>(this)}
, m_selectionChangedEventSource{static_cast<TableView*>(this)}
, m_sortedEventSource{static_cast<TableView*>(this)}
, m_sortingEventSource{static_cast<TableView*>(this)}
Expand Down Expand Up @@ -538,6 +539,16 @@ void TableViewProperties::CellEditEnding(winrt::event_token const& token)
m_cellEditEndingEventSource.remove(token);
}

winrt::event_token TableViewProperties::CellToolTipRequested(winrt::TypedEventHandler<winrt::TableView, winrt::TableViewCellToolTipRequestedEventArgs> const& value)
{
return m_cellToolTipRequestedEventSource.add(value);
}

void TableViewProperties::CellToolTipRequested(winrt::event_token const& token)
{
m_cellToolTipRequestedEventSource.remove(token);
}

winrt::event_token TableViewProperties::SelectionChanged(winrt::TypedEventHandler<winrt::TableView, winrt::SelectionChangedEventArgs> const& value)
{
return m_selectionChangedEventSource.add(value);
Expand Down
3 changes: 3 additions & 0 deletions controls/dev/Generated/TableView.properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ class TableViewProperties
void BeginningEdit(winrt::event_token const& token);
winrt::event_token CellEditEnding(winrt::TypedEventHandler<winrt::TableView, winrt::TableViewCellEditEndingEventArgs> const& value);
void CellEditEnding(winrt::event_token const& token);
winrt::event_token CellToolTipRequested(winrt::TypedEventHandler<winrt::TableView, winrt::TableViewCellToolTipRequestedEventArgs> const& value);
void CellToolTipRequested(winrt::event_token const& token);
winrt::event_token SelectionChanged(winrt::TypedEventHandler<winrt::TableView, winrt::SelectionChangedEventArgs> const& value);
void SelectionChanged(winrt::event_token const& token);
winrt::event_token Sorted(winrt::TypedEventHandler<winrt::TableView, winrt::TableViewSortedEventArgs> const& value);
Expand All @@ -99,6 +101,7 @@ class TableViewProperties

event_source<winrt::TypedEventHandler<winrt::TableView, winrt::TableViewBeginningEditEventArgs>> m_beginningEditEventSource;
event_source<winrt::TypedEventHandler<winrt::TableView, winrt::TableViewCellEditEndingEventArgs>> m_cellEditEndingEventSource;
event_source<winrt::TypedEventHandler<winrt::TableView, winrt::TableViewCellToolTipRequestedEventArgs>> m_cellToolTipRequestedEventSource;
event_source<winrt::TypedEventHandler<winrt::TableView, winrt::SelectionChangedEventArgs>> m_selectionChangedEventSource;
event_source<winrt::TypedEventHandler<winrt::TableView, winrt::TableViewSortedEventArgs>> m_sortedEventSource;
event_source<winrt::TypedEventHandler<winrt::TableView, winrt::TableViewSortingEventArgs>> m_sortingEventSource;
Expand Down
124 changes: 123 additions & 1 deletion controls/dev/TableView/TableView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "TableViewRow.h"
#include "TableViewCellsPanel.h"
#include "TableViewAutomationPeer.h"
#include "TableViewCellToolTipRequestedEventArgs.h"
#include "TableViewSource.h"
#include "SortIndicator.h"
#include "RuntimeProfiler.h"
Expand All @@ -32,6 +33,10 @@ static constexpr std::wstring_view s_SortIndicatorName{ L"TableViewSortIndicator

namespace
{
// Consecutive follow-up tooltip passes a handler may request by calling InvalidateCellToolTips
// from inside CellToolTipRequested. Matches the row's rebuild drain cap.
constexpr int c_maxCellToolTipRefreshPasses = 3;

// cppwinrt's == compares raw ABI pointers, which can differ for the same object across a QI,
// so fall back to canonical IUnknown identity.
bool IsSameObject(winrt::IInspectable const& left, winrt::IInspectable const& right)
Expand Down Expand Up @@ -1298,6 +1303,120 @@ void TableView::OnRowElementPrepared(

}

winrt::com_ptr<TableViewCellToolTipRequestedEventArgs> TableView::RaiseCellToolTipRequested(
const winrt::TableViewColumn& column,
const winrt::IInspectable& item)
{
// Contained by the callers, which run from framework callbacks.
auto args = winrt::make_self<TableViewCellToolTipRequestedEventArgs>(item, column);
m_cellToolTipRequestedEventSource(*this, *args);
return args;
}

void TableView::InvalidateCellToolTips()
{
// Deferred because this is public: raising synchronously from an app handler would re-enter the
// pass and walk live children mid-mutation. Matches InvalidateItemsInfo.
if (m_cellToolTipRefreshQueued)
{
m_cellToolTipRefreshDirty = true;
return;
}

auto dispatcher = DispatcherQueue();
if (!dispatcher)
{
RefreshCellToolTipsSynchronously();
return;
}

m_cellToolTipRefreshQueued = true;
auto weakThis = get_weak();
if (!dispatcher.TryEnqueue([weakThis]()
{
if (auto strongThis = weakThis.get())
{
// Held across the pass so a handler calling back records a follow-up instead of
// queueing a callback every tick, and cleared on the throw path so a failed pass
// cannot disable every future invalidate.
strongThis->m_cellToolTipRefreshDirty = false;
try
{
strongThis->RefreshCellToolTipsOnRealizedRows();
}
catch (...)
{
TVDiag::LogRetailF(L"[TableView] Cell-tooltip re-resolve failed (HRESULT 0x%08X).",
static_cast<unsigned int>(winrt::to_hresult()));
}
strongThis->m_cellToolTipRefreshQueued = false;

if (strongThis->m_cellToolTipRefreshDirty)
{
strongThis->m_cellToolTipRefreshDirty = false;

// A handler invalidating on every raise would re-arm this forever, so it is
// bounded like the row's rebuild drain.
if (++strongThis->m_cellToolTipRefreshPasses < c_maxCellToolTipRefreshPasses)
{
strongThis->InvalidateCellToolTips();
return;
}

TVDiag::LogRetailF(L"[TableView] Dropping a cell-tooltip re-resolve after %d passes; "
L"a CellToolTipRequested handler is calling InvalidateCellToolTips unconditionally.",
c_maxCellToolTipRefreshPasses);
}

strongThis->m_cellToolTipRefreshPasses = 0;
}
}))
{
m_cellToolTipRefreshQueued = false;
RefreshCellToolTipsSynchronously();
}
}

// Holds the coalescing flag so a handler calling InvalidateCellToolTips records a follow-up instead
// of re-entering synchronously and recursing without bound. Terminal: there is no dispatcher to
// replay the follow-up on, so it is dropped with a diagnostic rather than silently.
void TableView::RefreshCellToolTipsSynchronously()
{
m_cellToolTipRefreshQueued = true;
auto queuedGuard = wil::scope_exit([this]()
{
m_cellToolTipRefreshQueued = false;
m_cellToolTipRefreshPasses = 0;
});

RefreshCellToolTipsOnRealizedRows();

if (m_cellToolTipRefreshDirty)
{
m_cellToolTipRefreshDirty = false;
TVDiag::LogRetailF(L"[TableView] Dropping a cell-tooltip re-resolve requested during a "
L"synchronous fallback pass; there is no dispatcher to replay it on.");
}
}

void TableView::RefreshCellToolTipsOnRealizedRows()
{
ForEachRealizedRow([](winrt::TableViewRow const& row)
{
try
{
winrt::get_self<TableViewRow>(row)->RefreshCellToolTips();
}
catch (...)
{
// Contained per row: an exception escaping a dispatcher callback fail-fasts, and one
// bad row must not abort the rest.
TVDiag::LogRetailF(L"[TableView] A row's cell tooltip pass failed (HRESULT 0x%08X). Continuing.",
static_cast<unsigned int>(winrt::to_hresult()));
}
});
}

void TableView::OnRowElementClearing(
const winrt::ItemsRepeater& /*sender*/,
const winrt::ItemsRepeaterElementClearingEventArgs& args)
Expand Down Expand Up @@ -1330,7 +1449,10 @@ void TableView::OnRowElementClearing(
}
}

winrt::get_self<TableViewRow>(row)->SetOwningTableViewInternal(nullptr);
auto const rowImpl = winrt::get_self<TableViewRow>(row);
// Release app-supplied tooltip content rather than pinning it in the recycle pool.
rowImpl->ReleaseCellToolTips();
rowImpl->SetOwningTableViewInternal(nullptr);
InvalidateMeasure();
}

Expand Down
23 changes: 23 additions & 0 deletions controls/dev/TableView/TableView.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ struct TableViewResourceCache
double lastFrozenColumnsHorizontalOffset{ 0.0 };
};

class TableViewCellToolTipRequestedEventArgs;

namespace TabularShapingHelpers { class CustomSortRankAdapter; }

// The control's half of the TableViewSource sort axis. The projection is addressed by an opaque
Expand Down Expand Up @@ -196,6 +198,18 @@ class TableView :
// Pin rebuilt rows immediately when leading-frozen columns are active.
void PinFrozenColumnsForRow(const winrt::TableViewRow& row);

// Internal — true while at least one CellToolTipRequested handler is attached, so the cell
// realization path can skip all per-cell tooltip work when nobody is listening.
bool HasCellToolTipHandler() const { return static_cast<bool>(m_cellToolTipRequestedEventSource); }

// Internal — raises CellToolTipRequested for one cell and returns the args the handler filled in.
winrt::com_ptr<TableViewCellToolTipRequestedEventArgs> RaiseCellToolTipRequested(
const winrt::TableViewColumn& column,
const winrt::IInspectable& item);

// Queues a coalesced tooltip re-resolve for every realized cell (public, from the IDL).
void InvalidateCellToolTips();

// Requested by a cell panel (header/row) during measure when a realized cell's own measured width
// changed (grow or shrink). Invalidates our measure synchronously so ResolveColumnWidths re-runs in
// the same layout tick (no deferral -> no one-frame lag). Bridges the body ScrollViewer, which
Expand Down Expand Up @@ -712,6 +726,15 @@ class TableView :
// Set while a coalesced RebuildHeaders is pending on the dispatcher; collapses a burst of column
// changes into one rebuild. UI-thread only (all column callbacks arrive on the UI thread).
bool m_rebuildHeadersQueued{ false };
void RefreshCellToolTipsOnRealizedRows();
void RefreshCellToolTipsSynchronously();
// A coalesced cell-tooltip re-resolve is pending on the dispatcher (InvalidateCellToolTips).
bool m_cellToolTipRefreshQueued{ false };
// An invalidate arrived while a pass was queued or running; coalesced into one follow-up.
bool m_cellToolTipRefreshDirty{ false };
// Consecutive handler-requested follow-up passes, capped so an unconditionally-invalidating
// handler is dropped rather than pegging the UI thread.
int m_cellToolTipRefreshPasses{ 0 };

// Per-instance resource cache; replaces the former process-global map keyed by `this`.
TableViewResourceCache m_resourceCache{};
Expand Down
39 changes: 39 additions & 0 deletions controls/dev/TableView/TableView.idl
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,29 @@ runtimeclass TableViewCellEditEndingEventArgs
Boolean Cancel;
};

// Args for CellToolTipRequested.
[MUX_PREVIEW]
[webhosthidden]
runtimeclass TableViewCellToolTipRequestedEventArgs
{
// The data item of the row the cell belongs to.
Object Item { get; };

// The column of the cell. Always non-null.
MU_XC_NAMESPACE.TableViewColumn Column { get; };

// The tooltip content: a string, or any content a ToolTip can host. Null or an empty string
// means no tooltip. A ToolTip is not valid content -- the control owns the ToolTip and its
// placement. A UIElement is parented by that cell's ToolTip, so return a fresh element per
// raise: the same instance handed to a second cell cannot be parented twice and is dropped.
Object Content { get; set; };

// Accessible text, published as the cell's UIA HelpText unless the cell already reports that
// same text, so Narrator does not read it twice. Set it when Content is not a string; it
// overrides string Content, and is ignored when Content is null.
String AutomationHelpText { get; set; };
};

[MUX_PREVIEW]
[webhosthidden]
unsealed runtimeclass TableViewTextColumn : TableViewColumn
Expand Down Expand Up @@ -575,6 +598,22 @@ unsealed runtimeclass TableView : Microsoft.UI.Xaml.Controls.Control
// Raised AFTER a sort-state change has been applied. Not cancellable.
event Windows.Foundation.TypedEventHandler<MU_XC_NAMESPACE.TableView, MU_XC_NAMESPACE.TableViewSortedEventArgs> Sorted;

// ----- Tooltips -----
// Raised as each cell's tooltip is resolved: on realization, on cell rebuild, when a cell edit
// closes, and on InvalidateCellToolTips. May be raised repeatedly for the same cell, so handlers
// should be cheap and side-effect free. Opt-in -- not raised when no handler is attached. A
// tooltip the app sets inside a cell's own content template opens over that content; the
// control's tooltip covers the rest of the cell.
event Windows.Foundation.TypedEventHandler<MU_XC_NAMESPACE.TableView, MU_XC_NAMESPACE.TableViewCellToolTipRequestedEventArgs> CellToolTipRequested;

// Queues a coalesced re-resolve of every realized cell; it normally does not run before this
// call returns, falling back to running synchronously only when there is no dispatcher. Use it
// after attaching a handler to a TableView whose rows are already realized, or when the data
// behind the tooltips changes. Calling it from a CellToolTipRequested handler is
// re-entrancy-safe, but call it only when the state behind the tooltips has actually changed: a
// handler that invalidates on every raise is dropped after a few passes.
void InvalidateCellToolTips();

static Microsoft.UI.Xaml.DependencyProperty ItemsSourceProperty { get; };
static Microsoft.UI.Xaml.DependencyProperty ColumnsProperty { get; };
static Microsoft.UI.Xaml.DependencyProperty HeadersVisibilityProperty { get; };
Expand Down
2 changes: 2 additions & 0 deletions controls/dev/TableView/TableView.vcxitems
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewGroupInfo.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewBeginningEditEventArgs.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewCellEditEndingEventArgs.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewCellToolTipRequestedEventArgs.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewToolTipHelpers.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewSortingEventArgs.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)TableViewSortedEventArgs.h" />
</ItemGroup>
Expand Down
39 changes: 39 additions & 0 deletions controls/dev/TableView/TableViewAutomationHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,42 @@ inline std::optional<winrt::hstring> TryGetColumnHeaderString(winrt::TableViewCo
}
return std::nullopt;
}

// The text a realized cell reports to UIA: the generated TextBlock for a text column, otherwise the
// standard UIA name of the column-generated content. Shared by TableViewCellAutomationPeer (which
// composes it into the cell name) and the cell tooltip pass (which uses it to avoid publishing
// HelpText that would make Narrator read the same string twice), so the two cannot drift.
// allowPeerCreation is false on the tooltip path: resolving a non-text cell allocates an automation
// peer, and that must not happen on the scroll path with no UIA listener.
inline winrt::hstring GetCellValueTextFromWrapper(winrt::FrameworkElement const& cellWrapper, bool allowPeerCreation = true)
{
if (!cellWrapper)
{
return {};
}

winrt::FrameworkElement content{ nullptr };
if (auto const border = cellWrapper.try_as<winrt::Border>())
{
content = border.Child().try_as<winrt::FrameworkElement>();
}
if (!content)
{
content = cellWrapper;
}

if (auto const textBlock = content.try_as<winrt::TextBlock>())
{
return textBlock.Text();
}

if (allowPeerCreation)
{
if (auto const peer = winrt::FrameworkElementAutomationPeer::CreatePeerForElement(content))
{
return peer.GetName();
}
}

return {};
}
31 changes: 1 addition & 30 deletions controls/dev/TableView/TableViewCellAutomationPeer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,36 +97,7 @@ winrt::hstring TableViewCellAutomationPeer::GetColumnHeaderText()

winrt::hstring TableViewCellAutomationPeer::GetCellValueText()
{
auto const cell = Owner().try_as<winrt::FrameworkElement>();
if (!cell)
{
return {};
}

// The cell wrapper's child is the column-generated content.
winrt::FrameworkElement content{ nullptr };
if (auto const border = cell.try_as<winrt::Border>())
{
content = border.Child().try_as<winrt::FrameworkElement>();
}
if (!content)
{
content = cell;
}

// Common text-column case: read the generated TextBlock.
if (auto const textBlock = content.try_as<winrt::TextBlock>())
{
return textBlock.Text();
}

// Template content uses the standard UIA name computation.
if (auto const peer = winrt::FrameworkElementAutomationPeer::CreatePeerForElement(content))
{
return peer.GetName();
}

return {};
return GetCellValueTextFromWrapper(Owner().try_as<winrt::FrameworkElement>());
}

int32_t TableViewCellAutomationPeer::GetRowIndex()
Expand Down
Loading
Loading