SmartTable.h

The widget itself. Every node, every panel field, and the C++ behind them.

Source/SmartTables/Public/SmartTable.h

A data table for UMG.

Columns are named once, in the Details panel or through SetColumns. A column binds by name to a property, or to a function that takes no input. Text, sorting and filtering all read that one binding. The header and the rows cannot end up reading different things.

Table->SetItems( Ships );
FSmartTableColumn Shield;             // a column that draws its own widget
Shield.ColumnId    = "Shield";
Shield.BindingName = "ShieldPercent";
Shield.CellClass   = UMyBarCell::StaticClass();

Most columns are one line and no code at all. A column with no CellClass gets the built-in text cell.

Rows arrive two ways. SetItems takes objects and builds a model inside the table. Everything else comes in through SetModel: a file, a server, a million rows.

The table itself never sorts. It says a sort was asked for and then draws whatever order the model hands back.

Class
USmartTable
Extends
UWidget
Members
399
Blueprint nodes
99
Panel fields
68

Constructor 1

ConstructorUSmartTable
USmartTable( const FObjectInitializer & ObjectInitializer )

Public functions 157

Smart Table 28

Set ItemsSetItemsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetItems( const TArray< UObject * > & InItems )

Replaces the whole set. It is fine before the widget is built, since a table on a tab not opened yet is the normal case.

Items in both sets keep their rows. The new ones arrive the way AddItem rows do, with the entrance and the scroll. A set that shares no item with the last one is a new set, and nothing in it arrives.

Add ItemAddItemnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void AddItem( UObject * Item )

Adds at the end. The row plays the entrance, and bStickToEnd with bScrollToAddedRow say where the view goes for it.

Remove ItemRemoveItemnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void RemoveItem( UObject * Item )

Takes the row out in the same call, while the table draws the items SetItems gave it. With any other model in force it takes nothing out and logs why. A row on its way out is not counted and not selected. That holds while the model sorts on the game thread. With a WorkDispatcher set, a sort already in flight can still count the row until it lands.

Clear ItemsClearItemsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void ClearItems()

Empties the built-in items model and makes it the one in force. A model handed to SetModel keeps everything it holds. It stops being the model this table draws, and nothing more.

Set Data TableSetDataTablenode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetDataTable( UDataTable * DataTable )

Takes a DataTable. Columns bind to the row struct fields by the same names, and nothing is copied. Rows get read where they sit.

Notify Item ChangedNotifyItemChangednode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void NotifyItemChanged( UObject * Item )

This item changed. Its row reads again. No other row does, and the order stays.

Is Scrolled To EndIsScrolledToEndpure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) bool IsScrolledToEnd() const

True when the view sat at the end of the list, as of the last tick.

Within a quarter of a row of the bottom counts as the end. Exactly zero would stop a table following over a drag that ended a pixel short.

Sampled once a tick, not read when asked. By the time a row is added the list has already grown, and a fresh read always says no.

if ( Table->IsScrolledToEnd() ) Table->ScrollRowIntoView( Row );
Refresh ItemsRefreshItemsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void RefreshItems()

Every visible row reads again. This is the one to call when a lot of items changed at once.

Get Selected ItemGetSelectedItempure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) UObject * GetSelectedItem() const

The item on the first selected row, or null when the model builds none. A DataTable model does build one, so this answers over a DataTable too.

Get Selected ItemsGetSelectedItemspure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) TArray< UObject * > GetSelectedItems() const

One entry per selected row that has an item. A DataTable model has one for every row: it builds a row proxy on request.

Get Selected RowsGetSelectedRowspure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) TArray< int32 > GetSelectedRows() const

The selection as rows. The only form a model with no items can use.

Is Row SelectedIsRowSelectedpure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) bool IsRowSelected( int32 NaturalRow ) const

Natural row, so it names the same row after any sort. False until the widget is built.

Set Row SelectedSetRowSelectednode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetRowSelected( int32 NaturalRow, bool bSelected = true )

Does nothing for a row a filter is hiding: only a drawn row can be selected.

Set Rows SelectedSetRowsSelectednode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetRowsSelected( const TArray< int32 > & NaturalRows, bool bSelected = true )

Select or clear a set of rows at once, and broadcast once for the lot.

A loop over SetRowSelected broadcasts per row, and every listener reads the whole selection back out each time. Rows a filter is hiding are dropped, and the log says how many.

Set Selected ItemsSetSelectedItemsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetSelectedItems( const TArray< UObject * > & Items, bool bSelected = true )

The same for items, through whatever the model answers for NaturalRowOfItem.

An item this model does not hold is dropped. A filter hiding the row drops the item too.

Set Selected ItemSetSelectedItemnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetSelectedItem( UObject * Item, bool bSelected = true )

Finds the drawn row holding that item, and does nothing when a filter is hiding it.

Clear SelectionClearSelectionnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void ClearSelection()

Drops every selected row. Safe before the widget is built, where it does nothing.

Scroll Item Into ViewScrollItemIntoViewnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void ScrollItemIntoView( UObject * Item )

Scrolls and never selects. Does nothing when a filter is hiding the row that holds the item.

Scroll Row Into ViewScrollRowIntoViewnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void ScrollRowIntoView( int32 NaturalRow )

Scrolls to a row by number, for a model that has no items to scroll to.

ScrollItemIntoView needs an object, and a struct model has none.

Scroll To EndScrollToEndnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void ScrollToEnd()

Scrolls to the last row the table is showing.

The end in presented space, and not the model's last row. Under a filter that row might not be drawn at all. The end here means the bottom of what is on screen.

bStartAtEnd is the same move at the moment the table is built.

Set Row HeightSetRowHeightnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetRowHeight( float InRowHeight )

A cell with more than one line of text needs tall rows. The default is one line.

Set Show HeaderSetShowHeadernode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetShowHeader( bool bInShowHeader )

With the header dropped, the same columns draw as a plain list of cells.

A runtime setting and not only a Details panel flag, so a table can drop its header and take it back while it is running.

Set Interactive CellsSetInteractiveCellsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetInteractiveCells( bool bInInteractiveCells )

Whether a click can reach the widget inside a cell. Rebuilds every row.

With it on, a control in one column works, and every other column still selects the row under it. It is not one or the other.

See bInteractiveCells

Are Cells InteractiveAreCellsInteractivepure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) bool AreCellsInteractive() const

Whether a click reaches inside a cell. Off, the row under it takes every click.

Set Height ModeSetHeightModenode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetHeightMode( ESmartTableHeight InHeightMode, int32 InMaxVisibleRows = 0 )

Whether the table takes the height it is given or asks for the height of its rows.

The runtime half of HeightMode, and the pair to SetShowHeader. A panel built in code that drops the header usually stops being a scrolling list too.

MaxVisibleRows is skipped unless the mode is FitRows.

Set Selection ModeSetSelectionModenode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void SetSelectionMode( ESmartTableSelectionMode InSelectionMode )

Whether clicking a row selects it, and how many rows can be selected.

None is for a table whose job is editing, such as a grid over a DataTable where the row marking only gets in the way of the field being changed.

It pairs with SetInteractiveCells( true ). That call lets the click reach the widget in the cell. This one stops the row lighting up behind it.

Get Selection ModeGetSelectionModepure node
UFUNCTION( BlueprintPure, Category = "Smart Table" ) ESmartTableSelectionMode GetSelectionMode() const

How many rows can be picked at once.

Notify Cell Value ChangedNotifyCellValueChangednode
UFUNCTION( BlueprintCallable, Category = "Smart Table" ) void NotifyCellValueChanged( UObject * Item, int32 NaturalRow, FName ColumnId, float Value )

Raises OnCellValueChanged. A cell calls this when a control inside it moved.

Reflected because a cell is a Widget Blueprint. Left as C++ only, it would give a cell graph no way to raise OnCellValueChanged at all.

Input 16

Focus TableFocusTablenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void FocusTable()

Gives the row list keyboard focus.

SetFocus goes to the widget root, which then passes the keyboard down. This skips the root and goes straight to the list. The engine's own key handling then covers the arrows, the D-pad and the stick.

Get Focused RowGetFocusedRowpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Input" ) int32 GetFocusedRow() const

The row the caret is on. Enter runs it, and MoveSelection moves from it.

With one row selected the caret is on it. With several selected the caret is on one of them, and that is the row Enter runs. INDEX_NONE when there is no caret at all.

Move SelectionMoveSelectionnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void MoveSelection( int32 Delta )

Moves the caret by Delta rows in presented order, and selects the row it reaches. With no caret yet, a move forward starts at the first row and a move back at the last.

Move Selection By PageMoveSelectionByPagenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void MoveSelectionByPage( int32 PageDelta )

The same, by whole screens. A page is as many rows as the list really built. That tracks the real size of the table instead of a number this plugin picked.

Select First RowSelectFirstRownode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void SelectFirstRow()

The first row as drawn, so a sort or a filter changes which row that is.

Select Last RowSelectLastRownode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void SelectLastRow()

The last row as drawn, which a sort or a filter also changes.

Activate Selected RowActivateSelectedRownode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void ActivateSelectedRow()

Raises OnRowActivated for the caret row, with no mouse. The pointer route is a click twice.

Cycle Column SortCycleColumnSortnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void CycleColumnSort( FName ColumnId, bool bAsSecondary = false )

One step of the sort cycle on a column: ascending, descending, then back to unsorted if the table lets it. What a header click does, and it saves the layout the same way.

Not SortByColumn, which sets an order at once and never saves.

Open Header MenuOpenHeaderMenunode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void OpenHeaderMenu( FName ColumnId )

Opens the built-in header menu on a column, under its header.

HeaderMenuOptions.bEnabled gates this call and not only the right click. Every other gate in the plugin takes the gesture and leaves the function.

Each entry the menu offers has a function of its own, so an author with the menu off reaches all of them from a graph.

Close MenuCloseMenunode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void CloseMenu()

Closes whichever menu this table has open, if any.

Nothing has to call this. A menu does not go when the table does. A watch on the table's own widget shuts the menu two frames after it leaves the screen.

This is for shutting one on purpose, or for a screen that cannot wait those two frames.

Open Table MenuOpenTableMenunode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void OpenTableMenu()

The menu with no column under it, which is the one that brings hidden columns back.

Begin Column ResizeBeginColumnResizenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void BeginColumnResize( FName ColumnId )

Starts a column resize that a held control drives.

Begin, Update and End instead of one call with a width, because a resize keeps going: a stick held against an edge moves the column the way a dragged mouse does.

A pointer press on a grip takes over a resize started this way, and its release ends the whole thing.

Update Column ResizeUpdateColumnResizenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void UpdateColumnResize( float DeltaPixels )

Widens or shrinks the column being resized by DeltaPixels.

Pixels, not stick units. Turning a stick push into pixels sits with the input settings, not here.

End Column ResizeEndColumnResizenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void EndColumnResize()

Ends it, and saves the layout once for the whole drag instead of once per step.

Set Allow Right Click Drag ScrollSetAllowRightClickDragScrollnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Input" ) void SetAllowRightClickDragScroll( bool bInAllowRightClickDragScroll )

Off leaves a right click a plain click, which is what opens the row menu.

Is Table FocusedIsTableFocusedpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Input" ) bool IsTableFocused() const

Whether this table, or any row or cell inside it, holds the keyboard.

Columns 11

Select ColumnSelectColumnnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void SelectColumn( FName ColumnId )

Selects one column. Every intent that gets no column id is then aimed at it.

Moving the aim and using it are separate calls, so one button can walk the selection while another acts on it.

The table also selects on its own whenever anything touches a column: its header, a cell in it, or a widget inside that cell.

Move Column SelectionMoveColumnSelectionnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void MoveColumnSelection( int32 Delta )

Walks the aim Delta columns along the shown order. With nothing aimed at yet, a move forward starts at the first shown column and a move back at the last.

Get Selected ColumnGetSelectedColumnpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Columns" ) FName GetSelectedColumn() const

NAME_None until something selects one.

Set ColumnsSetColumnsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void SetColumns( const TArray< FSmartTableColumn > & InColumns )

Replaces the whole set and rebuilds the header at once. Which columns a second level can order is only marked stale here, and worked out again the next time something asks.

Get ColumnsGetColumnspure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Columns" ) const TArray< FSmartTableColumn > & GetColumns() const

The columns as named, for a model that needs to know what it will be asked for.

The built-in models are handed them by SetColumns. A model written in C++ arrives through SetModel and is never reached into, so one that binds by column has to come and ask.

It is also the only way to read columns named in the Details panel instead of set from code.

Set Column VisibleSetColumnVisiblenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void SetColumnVisible( FName ColumnId, bool bVisible )

Written as a layout override, so ResetLayout puts back what the column was authored with.

Is Column VisibleIsColumnVisiblepure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Columns" ) bool IsColumnVisible( FName ColumnId ) const

Whether it is drawn now: the authored answer with any layout override over the top.

Set Stretch Last ColumnSetStretchLastColumnnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void SetStretchLastColumn( bool bInStretchLastColumn )

Gives the last shown column whatever width is left over, or stops giving it. The width pass runs at once, so a cell class that sizes itself from its column measures again.

Set Allow Horizontal ScrollSetAllowHorizontalScrollnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void SetAllowHorizontalScroll( bool bInAllowHorizontalScroll )

Off also puts the columns back to offset zero, so nothing is left scrolled out of sight.

Get Column LabelGetColumnLabelpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Columns" ) FText GetColumnLabel( FName ColumnId ) const

The column header text as named, or its id when there is none.

Scroll Columns ByScrollColumnsBynode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Columns" ) void ScrollColumnsBy( float DeltaPixels )

Scrolls the columns sideways by a number of pixels. Negative goes left.

The mouse reaches the same thing through the scrollbar. This is the route a pad has to a far column.

Sorting 7

Sort By ColumnSortByColumnnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Sorting" ) void SortByColumn( FName ColumnId, ESmartTableSortMode SortMode )

Asks the model for this order. The header arrows follow what the model says, not this call.

It replaces the sort instead of adding to it, and None clears it.

See SmartTable::Sorting::SpecWithPrimary, the same rule as a value, for code that reads it.

Sort By Secondary ColumnSortBySecondaryColumnnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Sorting" ) void SortBySecondaryColumn( FName ColumnId, ESmartTableSortMode SortMode )

Keeps the current primary sort and orders its ties by this column. Shift clicking a header does this.

Two levels at most. On an unsorted table the column becomes the primary, and None takes the level off.

It refuses a level that would move no row. A second arrow over an order that did not change looks like a broken table. bAllowDeadSecondarySort turns the refusal off.

See SmartTable::Sorting::SpecWithSecondary, which has the rules in full.

Set Sort SpecSetSortSpecnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Sorting" ) void SetSortSpec( const FSmartTableSortSpec & Spec )

Asks the model, which can refuse. Then the header arrow goes back and no row moves.

Get Sort SpecGetSortSpecpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Sorting" ) FSmartTableSortSpec GetSortSpec() const

The order the model is holding, not the last one asked for. Empty when there is no model.

Set Allow Dead Secondary SortSetAllowDeadSecondarySortnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Sorting" ) void SetAllowDeadSecondarySort( bool bInAllowDeadSecondarySort )

Takes effect on the next sort. Nothing is worked out inside this call.

Refresh Dead Secondary ColumnsRefreshDeadSecondaryColumnsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Sorting" ) void RefreshDeadSecondaryColumns()

Brings IsDeadSecondaryColumn up to date with the rows as they stand right now.

It sits outside the getter because that getter is const and BlueprintPure, and a pure node can run any number of times in any order. An answer built on first read would show its own call count.

One pass over the presented rows, and only when something marked the answer stale. That gate marks this the DRAWING entry: the Shift held dimming and the greyed menu entry, both of which can be a beat behind. The one site that ACTS on the answer works it out again instead.

Is Dead Secondary ColumnIsDeadSecondaryColumnpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Sorting" ) bool IsDeadSecondaryColumn( FName ColumnId ) const

True when a second sort level on this column could reorder nothing, because the primary sort left it no ties to break. Always false while bAllowDeadSecondarySort is on.

It reads the last answer RefreshDeadSecondaryColumns worked out, and works out nothing itself.

Filtering 2

Set Filter TextSetFilterTextnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Filtering" ) void SetFilterText( const FText & InFilterText )

Searches every column, hidden ones too, so hiding a column never changes what the filter finds.

Get Filter TextGetFilterTextpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Filtering" ) FText GetFilterText() const

What the model is filtering by now. Empty when there is no model.

Model 2

Set ModelSetModelnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Model" ) void SetModel( USmartTableModel * InModel )

Borrowed, never owned. Closing a table must not take the data with it. Replaces the built-in one.

Get ModelGetModelpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Model" ) USmartTableModel * GetModel() const

Whatever the table reads now: one given to SetModel, or the built-in one SetItems fills.

Row Numbers 1

Set Show Row NumbersSetShowRowNumbersnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Row Numbers" ) void SetShowRowNumbers( bool bInShowRowNumbers )

Rebuilds the header and every row, so this is not free to turn on and off per frame.

Drag and Drop 10

Set Allow Cell Drag DropSetAllowCellDragDropnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Drag and Drop" ) void SetAllowCellDragDrop( bool bInAllowCellDragDrop )

Lets cells start drags and take drops. It does nothing else.

Off by default. A cell that can be dragged has to hit test itself, so every cell in the table takes the pointer before the row under it does.

Can Pointer Start DragCanPointerStartDragpure nodestatic
UFUNCTION( BlueprintPure, Category = "Smart Table|Drag and Drop" ) static bool CanPointerStartDrag( const FPointerEvent & PointerEvent )

Whether a drag may start from this pointer. True only while the pointer holds a cursor.

An FSlateUser standing for a world-space pointer holds no cursor, and IsVirtualUser is written as that test. NotifyPointerMoveComplete asserts on the cursor inside its drag branch, so a drag from such a pointer trips an engine assert for as long as it lasts.

The assert guards the shape of the cursor and nothing else, so a refusal costs the gesture and nothing more. A cell that starts a drag of its own asks this too.

Allows Cell Drag DropAllowsCellDragDroppure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Drag and Drop" ) bool AllowsCellDragDrop() const

Whether a cell may start a drag. Off, no cell ever takes the pointer from the row under it.

Get Cell Drag RoleGetCellDragRolepure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Drag and Drop" ) ESmartTableCellDragRole GetCellDragRole( int32 NaturalRow, FName ColumnId ) const

What this cell is to the drag going on now, or None when there is not one.

Is Cell Drag ActiveIsCellDragActivepure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Drag and Drop" ) bool IsCellDragActive() const

Whether a cell drag is out at all. The marking is an attribute on every visible cell and asks this first, so a frame with no drag in it pays one test per cell instead of a role lookup.

It keeps nothing of its own. The two drag rows are what it reads.

Move Rows ToMoveRowsTonode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Drag and Drop" ) bool MoveRowsTo( const TArray< int32 > & NaturalRows, int32 InsertBeforeNaturalRow )

Moves rows in front of another one. Every reorder route ends here, the row drag included.

InsertBeforeNaturalRow is a GAP: 0 is in front of every row, GetNumRows is past the last.

It clears the sort once the rows move. A live sort would otherwise put the row straight back.

False means the model refused, and then nothing moved and the sort is still in force.

Move Selected RowsMoveSelectedRowsnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Drag and Drop" ) bool MoveSelectedRows( int32 Delta )

Moves the selection that many rows. Delta counts PRESENTED rows, which is what the eye counts: one up means the row drawn above, filtered or not.

It stops at the ends and never wraps. False when the selection is already there, and false when a filter is hiding part of it.

Allows Row ReorderAllowsRowReorderpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Drag and Drop" ) bool AllowsRowReorder() const

Whether rows may be dragged into a new order. Off, a press on a row only ever selects.

Set Allow Row ReorderSetAllowRowReordernode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Drag and Drop" ) void SetAllowRowReorder( bool bInAllowRowReorder )

Rebuilds every row, because a row reads this once when it is built and never again.

Natural Gap For Presented GapNaturalGapForPresentedGappure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Drag and Drop" ) int32 NaturalGapForPresentedGap( int32 PresentedGap ) const

The natural gap that a gap between two DRAWN rows stands for. This is what MoveRowsTo wants.

The rule: a drop lands right after the drawn row above the gap. With a filter on, that row can move far in natural space while on screen it looks like one step.

Style 2

Set Style AssetSetStyleAssetnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Style" ) void SetStyleAsset( USmartTableStyle * InStyleAsset )

Uses a style asset and rebuilds what draws from it.

Get Style AssetGetStyleAssetpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Style" ) USmartTableStyle * GetStyleAsset() const

The style asset in force, or null while the table draws its own authored look.

Layout 15

Get LayoutGetLayoutpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Layout" ) FSmartTableLayout GetLayout() const

What was changed in the table: widths, hidden columns, sort. A width or a hidden flag left at its default stays out. The drawn order is kept apart from those, and is empty until something moves.

Apply LayoutApplyLayoutnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void ApplyLayout( const FSmartTableLayout & Layout )

Restores a stored layout. It does not raise OnLayoutChanged, since nothing in the table changed it.

Reset LayoutResetLayoutnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void ResetLayout()

Back to the named columns: widths, visibility and sort as the Details panel left them.

Set Table IdSetTableIdnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void SetTableId( FName InTableId )

Which table this is, so a layout can be kept for it.

Two tables with one id share a layout, which is almost never right. Setting this reads the store again, so one widget used for several sets of data keeps its own widths for each.

Set Layout StoreSetLayoutStorenode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void SetLayoutStore( USmartTableLayoutStore * InLayoutStore )

Reads the stored layout back at once when TableId is already set, which is the usual order: the id is authored on the widget and the store arrives at runtime.

Set Column WidthSetColumnWidthnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void SetColumnWidth( FName ColumnId, float Width )

Sets a column width in pixels and marks it as chosen, so no later resize takes it back.

Reset Column WidthResetColumnWidthnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void ResetColumnWidth( FName ColumnId )

Forgets a chosen width and gives the column back to its named sizing. A Fill column stretches with the window again, and a Fixed one goes back to its named pixels.

Shrinking a column does not rewrite its OWN chosen width. Widening one can rewrite a NEIGHBOUR'S, on a table with no sideways scroll, where the room has to come from somewhere. ResetLayout drops every chosen width at once.

Get Column WidthGetColumnWidthpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Layout" ) float GetColumnWidth( FName ColumnId ) const

The column width right now, stretch and all. Zero usually means the width is not known in pixels yet, as for a Fill column before the first layout. It is also the answer for an unknown ColumnId, and for a Fixed column whose author typed zero.

Size Column To ContentSizeColumnToContentnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void SizeColumnToContent( FName ColumnId )

Widens a column to the widest text in it, header and all, capped at MaxAutoSizeColumnWidth. The width counts as chosen afterwards, so no later fit pass takes it back.

Set Column OrderSetColumnOrdernode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void SetColumnOrder( const TArray< FName > & ColumnIds )

Left to right order. A column left out keeps its named place, after the ones named here.

SHeaderRow has no drag of its own, so the header cell carries one. Move Left and Move Right in the header menu drive this call, and so do MoveColumn and MoveColumnNextTo from a graph.

Get Column OrderGetColumnOrderpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Layout" ) TArray< FName > GetColumnOrder() const

The order as drawn: the authored order with any stored one folded into it. An id naming no column is dropped, and a column no id names keeps the place it was authored in.

Move ColumnMoveColumnnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void MoveColumn( FName ColumnId, int32 Delta )

Moves a column by Delta places. It is clamped, so a column at either end stays there.

Move Column Next ToMoveColumnNextTonode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Layout" ) void MoveColumnNextTo( FName Moved, FName Target, bool bAfter )

Puts Moved right before or right after Target. This is what dropping a dragged header does.

Is Header Menu EnabledIsHeaderMenuEnabledpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Layout" ) bool IsHeaderMenuEnabled() const

Whether a right click on the header opens the built-in menu at all.

Can Reorder ColumnsCanReorderColumnspure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Layout" ) bool CanReorderColumns() const

Whether a column may be DRAGGED by its header. The menu's Move Left and Move Right do not ask this one.

Busy 2

Set BusySetBusynode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Busy" ) void SetBusy( bool bInBusy )

For a model that owns its own work: a sort on a server, a query, a load.

The built-in background path drives this itself. What is left is work the table can not see. Nothing draws until BusyOverlayDelaySeconds passes, so setting and clearing it in one call shows nothing.

Is BusyIsBusypure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Busy" ) bool IsBusy() const

Whether the table is waiting on a sort. The overlay only shows after BusyOverlayDelaySeconds.

Rows 3

Open Row MenuOpenRowMenunode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Rows" ) void OpenRowMenu( int32 NaturalRow )

Opens the row menu on one row, the way a right click there would.

Reflected, so a pad button or a key binding opens it with no right click in the path. RowMenuOptions.bEnabled gates the right click and never this call.

Set Row Menu EntriesSetRowMenuEntriesnode
UFUNCTION( BlueprintCallable, Category = "Smart Table|Rows" ) void SetRowMenuEntries( const TArray< FSmartTableMenuEntry > & Entries )

Replaces the entries the next row menu draws.

It is for calling from OnRowMenuOpening, which is why it takes effect at once and not on the next open. The menu gets built from this list the moment that event returns.

Is Row Menu EnabledIsRowMenuEnabledpure node
UFUNCTION( BlueprintPure, Category = "Smart Table|Rows" ) bool IsRowMenuEnabled() const

Whether a right click on a row opens its menu. RowMenuOptions.bEnabled is where it is set.

C++ only 58

GetItems
TConstArrayView< TObjectPtr< UObject > > GetItems() const

Every item this table holds, in the order the model keeps them.

A view, not a copy. It points straight at the model's own array, so a big table does not copy its items just to be read.

The catch is what ends it. AddItem, RemoveItem, SetItems and ClearItems all move that array, and a view held across any of them points at free memory.

Empty unless the built-in items model is the one in force. A DataTable model keeps no array to point at, though its rows still have items: it builds a row proxy the first time one is asked for.

GetMenuDrivingUser
int32 GetMenuDrivingUser() const

The Slate "user" this table's open menu was pushed for. A menu no pointer opened belongs to the pointer over the table, or to the keyboard "user" when there is none. A flyout inside it is opened for the same one, or it dies when the focus of that "user" moves.

EndPointerResize
bool EndPointerResize( int32 UserIndex, uint32 PointerIndex )

Ends a pointer resize this exact "user" and pointer began, and says whether there was one.

Not a UFUNCTION: the header row calls it off a release and off a lost capture, and neither is a thing a graph can express. An intent hold is left alone. EndColumnResize is what ends one of those.

Both halves of the pair are matched, so a release from one "user" cannot end a drag another began. A table on a world-space screen is driven by a virtual pointer while the real cursor still exists.

SelectColumnAt
void SelectColumnAt( float LocalX )

Selects the column that holds LocalX, measured in the header's own space.

This is how a click on a cell selects its column. The row asks for it from a preview press, so the answer is the same whether the click hit the cell drawing or a slider inside it.

GetSortSpecRef
const FSmartTableSortSpec & GetSortSpecRef() const

The same, without the copy. For C++ only, and empty while this table has no model.

GetRowInsertMarkerBrush
const FSlateBrush & GetRowInsertMarkerBrush() const
GetCellDragBrush
const FSlateBrush * GetCellDragBrush( ESmartTableCellDragRole Role ) const

The brush that marks a role. Null for None, and for a role with no brush set.

GetCellDragRoleOfRowId
ESmartTableCellDragRole GetCellDragRoleOfRowId( FName RowId, FName ColumnId ) const

GetCellDragRole for a row named by its id, which a built row already holds. It asks the model for nothing, and the marking reads it for every visible cell on every draw of a drag.

BeginCellDrag
void BeginCellDrag( int32 NaturalRow, FName ColumnId )

A cell calls these as a drag starts on it, comes over it, or leaves it. Only a cell does. They are the cell half of the work, public only because a cell is its own class.

SetCellDragTarget
void SetCellDragTarget( int32 NaturalRow, FName ColumnId )
ClearCellDragTarget
void ClearCellDragTarget( int32 NaturalRow, FName ColumnId )
EndCellDrag
void EndCellDrag()

Runs when the drag ends, no matter how it ended.

ColumnsPropertyNamestaticeditor only
static FName ColumnsPropertyName()

The names SmartTablesEditor needs to reach these properties.

Here and not as text over there, because GET_MEMBER_NAME_CHECKED can not see a protected member from outside the class. A plain TEXT( "Columns" ) in another module is a rename waiting to fail with no log line at runtime, instead of failing loudly at build time.

ColumnSourcePropertyNamestaticeditor only
static FName ColumnSourcePropertyName()
ExpectedItemClassPropertyNamestaticeditor only
static FName ExpectedItemClassPropertyName()
SourceTablePropertyNamestaticeditor only
static FName SourceTablePropertyName()
SourceFilePropertyNamestaticeditor only
static FName SourceFilePropertyName()
MergedColumnsFromSourceeditor only
TArray< FSmartTableColumn > MergedColumnsFromSource() const

What this table's columns would be after copying from the named source. Named ones stay, missing ones are added, and nothing is dropped.

It gives an answer back instead of writing one. The Details panel of a UWidget edits a preview copy, and the engine only moves a notified property edit onto the template the asset saves. A function writing straight to Columns reaches the one object thrown out on the next compile. SmartTablesEditor writes this through an IPropertyHandle, which is what the engine listens to.

DescribeSourcesLostBySwitchingToeditor only
FText DescribeSourcesLostBySwitchingTo( ESmartTableColumnSource NewSource ) const

Names the sources a move to NewSource would clear. Empty text when there is nothing to lose.

GetResizeGripWidth
float GetResizeGripWidth() const
CanResizeColumn
bool CanResizeColumn( FName ColumnId ) const

Whether the right edge of this column is a resize grip at all.

The one place that answers it. It reads the table's bAllowColumnResize, the column's own bResizable, and whether the column is shown at all. A locked column draws no resize cursor, and its click sorts.

ColumnLayout::FindEdgeAt skips a locked column inside its own walk. Not a second copy: it is a pure function over column sizes with no table in reach, so it can not ask.

IsResizingColumn
bool IsResizingColumn() const

True between the start of a resize and the release that ends it, whether a press on a column edge began it or a BeginColumnResize call did.

OpenRowMenuFromPointer
void OpenRowMenuFromPointer( int32 NaturalRow, const FVector2D & AbsolutePosition, int32 DrivingUserIndex )

The same menu, put where a pointer pressed instead of under the row, for the Slate "user" that pressed.

OpenRowMenu sits under the row, which is what a gamepad or a graph needs. It is a UFUNCTION and this is not: a graph has no "user" to name, so it opens a menu for the keyboard one. A pointer does, and on a table drawn off screen naming the wrong one loses the menu the moment the player's real click reaches the game.

HasRightClickMenu
bool HasRightClickMenu() const

Whether a right click opens a menu anywhere on this table: the header menu on a shown header, or the menu of a row.

IsRightDragScrolling
bool IsRightDragScrolling() const

Whether a right drag is scrolling this table either way. A row asks so it can tell a pan that just ended from a click. Asked of the table and not of the list, because the list is private.

AllowsHorizontalScroll
bool AllowsHorizontalScroll() const

Whether the columns can pan at all. The list asks before it starts counting a sideways right drag, since a drag that can scroll nothing must not end up hiding the row menu.

AcquireCell
USmartTableCell * AcquireCell( FName ColumnId, int32 NaturalRow )

Takes a cell from the pool, bound to this row and column.

This one and the ones after it are public because Slate widgets in this plugin call them across a class line, which is not the same as being part of the API.

Null when the column id is not one of this table's columns, and null when there is no model yet.

A model with no items is fine: GetItem() gives back null and a cell that reads the model text still works. A CellClass over such a model says so once per column, at Verbose. One with an ItemSetter draws nothing without the item, so it says it as a Warning, outside the designer.

ReleaseCell
void ReleaseCell( USmartTableCell * Cell )
IsFirstItemSetterWarningFor
bool IsFirstItemSetterWarningFor( const UClass & CellClass )

True the first time a cell of CellClass reports an ItemSetter mistake to this table, and false after, so a pool of broken cells says it once.

AcquireRowWidget
USmartTableRowWidget * AcquireRowWidget( int32 NaturalRow, const TSharedRef< SWidget > & ColumnsContent )

Takes a row wrapper from the pool, with Columns already in its named slot.

Null in three cases: no RowWidgetClass, a class the pool can not build, and a class with no named slot. The first is the default and says nothing. The last still draws the row, only unwrapped.

The two failures warn once per class, not once per table.

ReleaseRowWidget
void ReleaseRowWidget( USmartTableRowWidget * RowWidget )
FindColumn
const FSmartTableColumn * FindColumn( FName ColumnId ) const
NotifyCellDropped
void NotifyCellDropped( USmartTableCellDragDropOp * Payload, int32 TargetNaturalRow, FName TargetColumnId )

Raises OnCellDropped. The cell that was dropped on calls it, so the target is its own row.

CopyStyleClassDefaults
void CopyStyleClassDefaults( const USmartTableStyle & Defaults )

The style members taken from the style class defaults, which is the end of the constructor. Public because rebuilding those defaults after a palette edit has to run exactly this. That sweep is not a table's own behaviour, so it lives in SmartTableStyleRefresh.

ReapplyAfterPaletteChangeeditor only
void ReapplyAfterPaletteChange( const TSet< const UObject * > & Refreshed )

Runs again after a palette edit moved something under this table.

Refreshed holds every object the sweep really changed. Only a table can say whether any of them matters to it, because StyleAsset and the live widget are both out of reach from outside. That is why the check sits here and not in the sweep, and why a table nothing moved goes no further.

SmartTable::StyleRefresh::FromSettings calls it, and nothing else does.

MakeRowNumberWidget
TSharedRef< SWidget > MakeRowNumberWidget( TSharedRef< SSmartTableRow > Row ) const

The widget for the pinned number column. The row draws it instead of a cell, because it belongs to the table and not to the data, and no model should have to know it is there.

HandleHeaderGesture
void HandleHeaderGesture( FName ColumnId, bool bShiftDown )

A left click on a header. It sorts. Shift held adds a second level, but only while bAllowSecondarySort is on. With it off, Shift does what a plain click does.

MoveRowsFromDrop
void MoveRowsFromDrop( const TArray< FSmartTableKeptRow > & Rows, int32 InsertBeforeNaturalRow )

A row drag landing on this table. It moves the rows through MoveRowsTo, and saves the layout when that took a sort off, the way a header click saves. MoveRowsTo called from a graph saves nothing.

Rows is what the drag picked up, found again as it lands. A row that left while the drag was out stays out of the move. InsertBeforeNaturalRow counts the rows as they stand at the drop.

MakeHeaderMenu
TSharedRef< SWidget > MakeHeaderMenu( FName ColumnId )

The right click menu of a column, built new each time so it names what is sorted and hidden now.

PushMenuOnPath
void PushMenuOnPath( TSharedRef< SWidget > Anchor, const FWidgetPath & AnchorPath, TSharedRef< SWidget > Menu, FVector2D AbsolutePosition, int32 DrivingUserIndex )

The same, for a caller that already has the widget path and must not have one built for it.

A header cell pushes from a mouse event and has to give that event its own path. A menu pushed on a path that finds no owning window opens fine, and then can not push a second menu inside it. The sort entries work, and the Columns flyout does nothing at all.

AbsolutePosition is in the WIDGET TREE's absolute space: what a pointer event carries, and what LocalToAbsolute answers. The menu stack reads a different space in a window drawn by an FWidgetRenderer, and this is the one place the two are put back together.

PushMenuAtPoint
void PushMenuAtPoint( TSharedRef< SWidget > Anchor, TSharedRef< SWidget > Menu, FVector2D AbsolutePosition, int32 DrivingUserIndex )

The same at a named screen point, building the path from the anchor.

Every menu this table opens goes through this or PushMenuOnPath, which is why closing the last one and giving focus back are properties of the table. Opening one closes every menu Slate is holding open and not only this table's, since that is what the menu stack offers.

Public because a graph opens menus too, and a menu the table does not know about is one it can not close before freeing the style set it draws from.

GetHeaderStyle
const FHeaderRowStyle & GetHeaderStyle() const

The header's own background and label style. The widget a column drag draws under the pointer reads it: that widget is a floating copy of a header with no other way to look like one. A drag outlives the widget that started it, so it can not hold the table.

GetHeaderTextStyle
const FTextBlockStyle & GetHeaderTextStyle() const
GetInsertMarkerBrush
const FSlateBrush & GetInsertMarkerBrush() const
GetColumnDividerBrush
const FSlateBrush & GetColumnDividerBrush() const
GetColumnCaretBrush
const FSlateBrush & GetColumnCaretBrush() const
DoesColumnCaretNeedFocus
bool DoesColumnCaretNeedFocus() const
IsColumnCaretShown
bool IsColumnCaretShown() const
IsFocusOnPointerInteractionEnabled
bool IsFocusOnPointerInteractionEnabled() const
GetSortBrush
const FSlateBrush * GetSortBrush( FName ColumnId ) const

The arrow for the current sort on this column, or null when nothing sorts by it. Direction only, and the level is said by how many arrows the header draws.

A brush the style set to a Draw As of None comes back like any other. This answers what the sort is, and drawing it is the header's job. Null would say nothing sorts by the column, which is not true.

GetSortTint
FSlateColor GetSortTint() const

The colour both sort arrows draw in, which is the colour of the header labels.

Read from the label style and not from the header row, which carries a colour of its own. Two values for one idea drift apart.

IsSecondarySortColumn
bool IsSecondarySortColumn( FName ColumnId ) const

True when this column is the second sort level, which the header draws as a second arrow.

GetRowStyle
const FTableRowStyle & GetRowStyle() const
GetCellTextStyle
const FTextBlockStyle & GetCellTextStyle() const
GetEmptyTextStyle
const FTextBlockStyle & GetEmptyTextStyle() const

The style the nothing to show line draws in.

GetHeaderHeight
float GetHeaderHeight() const

How tall the header draws. Zero until a style reaches this table.

GetRowHeight
float GetRowHeight() const
MakeEmptyStateTextstatic
static FText MakeEmptyStateText( int32 NumRows, const FText & FilterText, const FText & InEmptyText, const FText & InNoMatchesFormat )

Which of the two empty messages a table like this needs.

It reads only its arguments, so the choice needs no built table. No rows at all and a filter that matched none look like the same blank box and mean two different things.

Protected functions 9

RebuildWidgetoverride
virtual TSharedRef< SWidget > RebuildWidget() override
ReleaseSlateResourcesoverride
virtual void ReleaseSlateResources( bool bReleaseChildren ) override
SynchronizePropertiesoverride
virtual void SynchronizeProperties() override
GetPaletteCategoryoverrideeditor only
virtual const FText GetPaletteCategory() override
PostEditChangePropertyoverrideeditor only
virtual void PostEditChangeProperty( FPropertyChangedEvent & PropertyChangedEvent ) override
ValidateCompiledDefaultsoverrideeditor only
virtual void ValidateCompiledDefaults( class IWidgetCompilerLog & CompileLog ) const override

Fails the Widget Blueprint compile when a column names something that is not there.

A binding is a name, resolved against a struct nothing compiles against. Rename a field and every column that named it goes blank. The first sign is a table of empty cells in a running game: the C++ still builds, the asset still loads, and the column is still well formed.

An error stops the compile, which is the point. A warning would scroll past.

Get Bindable NamesGetBindableNames
UFUNCTION() TArray< FName > GetBindableNames() const

Fills the binding dropdown on every column from ExpectedItemClass. Reflected because the Details panel calls it by name, and it is not part of the script API.

ResolveColumnSchema
const UStruct * ResolveColumnSchema( FText & OutSchemaName, FText & OutProblem ) const

ColumnSource, ExpectedItemClass and SourceTable, put to SmartTable::ResolveColumnSchema. SourceFile is left out of it, since the column builder reads that file straight.

ChooseRowMenuEntry
void ChooseRowMenuEntry( FName EntryId, int32 EntryIndex, FSmartTableKeptRow Row )

An entry of the row menu was picked. The row is found again and OnRowMenuEntryChosen names it, with the item it holds now.

A menu stays open over frames, and a live feed adds and drops rows while it does. A row that left while the menu was open says nothing.

Private functions 85

HandleGenerateRow
TSharedRef< ITableRow > HandleGenerateRow( int32 NaturalRow, const TSharedRef< STableViewBase > & OwnerTable )
HandleSelectionChanged
void HandleSelectionChanged( int32 NaturalRow, ESelectInfo::Type SelectInfo )
HandleRowActivated
void HandleRowActivated( int32 NaturalRow )
RebuildHeader
void RebuildHeader()
GetEmptyStateText
FText GetEmptyStateText() const
GetEmptyStateVisibility
EVisibility GetEmptyStateVisibility() const
GetFitRowsHeight
FOptionalSize GetFitRowsHeight() const

Unset in Fill, and unset in FitRows while RowHeight is zero or less. Either one lets the parent height straight through.

GetBusyOverlayVisibility
EVisibility GetBusyOverlayVisibility() const
HandleModelBusyChanged
void HandleModelBusyChanged( bool bInBusy )
MakeMenuBuilder
FMenuBuilder MakeMenuBuilder()

One builder for every menu this table opens, over the style resolved for this table.

HostMenu
TSharedRef< SWidget > HostMenu( FMenuBuilder & Menu )
AppendTableWideEntries
void AppendTableWideEntries( FMenuBuilder & Menu )

Show Columns and Reset Columns, which both menus offer and neither owns.

MakeTableMenu
TSharedRef< SWidget > MakeTableMenu()
HeaderLayoutChanged
void HeaderLayoutChanged()
RefreshVisibleRowCells
void RefreshVisibleRowCells( FName ColumnId )

Sets one column cell again in every row on screen, which is what gets a wrapping cell to work out its line breaks again.

No cell this plugin ships wraps. The stock text cell is a bare STextBlock with no wrap set. This is here for a cell class of its own that does.

NotifyVisibleCellsOfDragRoles
void NotifyVisibleCellsOfDragRoles()

Says to every cell on screen what it now is to the drag, so a cell that draws its own marking hears about it. The built-in marking needs none of this, being an attribute the row reads every frame.

TickTable
EActiveTimerReturnType TickTable( double CurrentTime, float DeltaTime )

Everything this table does once per frame, in one place.

One timer on the root, never one per header cell. Ten columns still means one tick a frame, and a table drawing no header at all goes on ticking.

GetRowNumberWidth
float GetRowNumberWidth() const

The width the pinned number column needs, which grows with the row count.

LeadingColumnOffset
float LeadingColumnOffset() const

What sits left of the first column without being one: the row number gutter, when it is shown. Every walk across the columns starts here. A walk that skips it names the wrong column.

GetAvailableColumnWidth
float GetAvailableColumnWidth() const

How much width the columns really have to share, less the pinned number column.

The scroll box is what it reads, never the header row. A header inside a scroll box sits at whatever width its own columns asked for. Measure a stretched column against that and it can only grow.

The scroll box takes its size from its parent instead, which is the one number in the sum that does not depend on the answer.

ReconcileColumnWidths
void ReconcileColumnWidths()

Works out every shown column pixel width again, then reads the size of exactly the cells that moved.

TickTable runs it, and it refuses cheaply: nothing happens unless the inputs are dirty or the free width moved more than half a pixel. Running it twice gives the same answer.

It is also the one place the header is rebuilt for width reasons. Twice is usual and not a ceiling: once when the first pass turns resizable columns Manual, and once when the gutter passes the header.

The gutter cannot be bound at all, since FixedWidth is a Slate argument and not an attribute. It is sized from the row count, so a model still filling in costs one more rebuild per digit.

MarkColumnWidthsDirty
void MarkColumnWidthsDirty()

The next ReconcileColumnWidths runs even when the free width has not moved. For the callers that changed the inputs: a column shown or hidden, a layout restored, a chosen width.

RefreshRowsForNewColumnWidths
void RefreshRowsForNewColumnWidths()

Asks the list to read its row sizes again, and to build any rows the new widths left room for.

HandleColumnWidthChanged
void HandleColumnWidthChanged( float NewWidth, FName ColumnId )

Every resize route ends here: the header row grip and the SHeaderRow one.

SetColumnWidthWhileDragging
void SetColumnWidthWhileDragging( FName ColumnId, float Width )

The width during a drag. Every column is measured again on every move, and the only thing that waits for the mouse coming up is the layout being saved.

SettleGripWidths
void SettleGripWidths()

Clears what the SHeaderRow sizing grip leaves behind, once a frame from the timer.

That grip settles a width without ever saying a gesture began, so it fills the width snapshot and owes a layout save, and nothing else would ever drop either. Every gesture this class starts ends at its own release instead, so a live one is left alone here.

FinishResize
void FinishResize()

Ends whatever resize is going on, whichever route opened it. The gesture goes back to rest, and the one layout save the whole drag owes is written.

Both routes come through here, so there is one reset and not two. Reset half of it and the gesture still names a column with no press behind it, which every later read takes for a live pointer drag.

UpdateResizeAutoScroll
void UpdateResizeAutoScroll()

Keeps a live resize going when the cursor leaves the visible part, by scrolling to follow it.

TakeWidthFromColumnsRightOf
float TakeWidthFromColumnsRightOf( FName DraggedColumnId, float DesiredWidth )

Moves width between DraggedColumnId and the columns to its right, and gives back the widest the dragged column can be.

Fit mode only. There the widths fill the space exactly, so without this a drag that grows a column has nowhere to grow into, and refusing it flat reads as a broken resize.

It does not fit the columns again. Every frame works from the widths the drag started with, so one pointer position always gives the same answer. Fitting again each frame would stack instead, and a pointer held still would go on taking width off the neighbour.

WidthAtResizeStart
float WidthAtResizeStart( FName ColumnId )

What ColumnId measured when the drag began. It takes the snapshot when there is not one yet.

Late on purpose. The pointer has three ways in and this table owns only two of them. SHeaderRow puts its own sizing grip over the right edge of every manual column and captures the mouse itself. A drag on those few pixels arrives through HandleColumnWidthChanged, which runs neither of the two.

Taking the snapshot on first use covers all three. Every route reads a width before it writes one, so the late snapshot is the press time one.

HandleHeaderRowMouseDown
FReply HandleHeaderRowMouseDown( const FGeometry & Geometry, const FPointerEvent & MouseEvent )
HandleHeaderRowMouseMove
FReply HandleHeaderRowMouseMove( const FGeometry & Geometry, const FPointerEvent & MouseEvent )
FindColumnEdgeAt
FName FindColumnEdgeAt( float LocalX, float & OutColumnLeft ) const

The column whose right edge is under LocalX, or None. It also says where that column starts.

MakeColumnVisibilityMenu
TSharedRef< SWidget > MakeColumnVisibilityMenu()

The Show Columns flyout, as a widget instead of entries poured into a builder the engine built.

FMenuStack draws the panel behind every pushed menu from the Menu.Background of FCoreStyle, hard coded inside FMenuStack. A named background reaches a menu only through a wrapper on top of the content, and that wrapper is SSmartTableMenuHost. A submenu built by AddSubMenu gives nowhere to put one, so its flyout draws in the engine grey right beside a parent using the table style.

The flyout goes through MakeMenuBuilder, so its search box matches the parent.

ToggleColumnVisible
void ToggleColumnVisible( FName ColumnId )
ColumnView
SmartTable::FColumnView ColumnView() const

What the columns are right now, read off Columns, ActiveLayout and ResolvedWidths. It lives for one call. Every read of the columns goes through it.

LayoutFor
FSmartTableColumnLayout & LayoutFor( FName ColumnId )

The stored override for this column, built the first time it is asked for.

WarnUnknownColumn
void WarnUnknownColumn( const TCHAR * Caller, FName ColumnId ) const

The one warning for a ColumnId that names no column, said the same way by every function taking one.

Caller is the function name, so the message still says which call did nothing. The guards stay at the call sites. Half read FindColumn and half read the order array, so one shared check would answer the wrong question for half the callers.

PushMenuAt
void PushMenuAt( TSharedRef< SWidget > Anchor, TSharedRef< SWidget > Menu, bool bBelow )

Opens a menu tied to a widget of this table, closing whatever was already up.

The widget path is built here instead of by the caller. It gives a true refusal when the table is off screen, and it is what lets a submenu find its way.

ApplySelection
void ApplySelection( TConstArrayView< int32 > NaturalRows, bool bSelected )

The one route to the list's selection. Every caller gives the whole set, so the list signals once whatever number of rows moved.

PresentedIndexOfCaret
int32 PresentedIndexOfCaret() const

Where the caret sits in presented order, or INDEX_NONE when there is none: no caret at all, or a row a filter is hiding right now.

MoveCaretTo
void MoveCaretTo( int32 PresentedRow )

Puts the caret on a presented row, selects it, and scrolls it into view. The one place a MOVE writes both carets together. A rebuild puts FocusedRow back on its own, without going through here.

IsRowDrawn
bool IsRowDrawn( int32 NaturalRow ) const

Whether this row is on screen right now. False for a row a filter took away, and for every row while this table has no model.

PresentedRowOf
int32 PresentedRowOf( int32 NaturalRow ) const

Where that row draws, or INDEX_NONE when it does not.

CanMoveCaret
bool CanMoveCaret( const TCHAR * Caller ) const

Whether the caret can move at all. It says why not in the log, so every mover refuses the same way.

HandleListKeyDown
FReply HandleListKeyDown( const FGeometry & Geometry, const FKeyEvent & KeyEvent )

Enter, and nothing else.

It gives back Unhandled for every other key on purpose, including an Enter with nothing to run. Arrows, the D-pad and the stick only start moving the list once a key event ends unhandled, so a flat Handled() here would kill that with no error anywhere to say why.

LayoutChangedByUser
void LayoutChangedByUser()

Saves through the store when there is one, and says so to listeners. Only for changes in the table.

ApplyDesignTimePreview
void ApplyDesignTimePreview()

Fills the table in the designer through the same model route the game uses, so the preview can not differ from what the game draws. With a source set those are the REAL rows, read from the DataTable or the class defaults. Placeholders are what it falls back to with no source.

RebuildRowIndices
void RebuildRowIndices()

Throws the row index array away and builds it again from the model presented rows. Nothing is reused: the selected and focused row ids are written down first, and put back afterwards, which is what carries a selection through rows being added or dropped.

HandlePresentationChanged
void HandlePresentationChanged( const FSmartTablePresentationChange & Change )
CanSortBySecondaryColumn
bool CanSortBySecondaryColumn( FName ColumnId ) const

Whether the header menu offers "Then Sort By This".

Three things decide it: bAllowSecondarySort, a sort level on some other column, and this column not being a dead secondary. The middle one is what the Shift gesture never asks. With nothing sorted at all, SpecWithSecondary turns the column into the primary. That is right for a click and wrong for an entry starting with Then, which would name a sort that is not there.

RecomputeDeadSecondaryColumns
void RecomputeDeadSecondaryColumns()

Works the dead-secondary answer out again, whatever the dirty flag says.

For the one site that ACTS on it, the refusal inside SortBySecondaryColumn. That site cannot trust the flag. A row tick changes values without re-sorting, and the pass reads live keys over the order in force, so one value moving inside a tie group frees a column with nothing marking it stale.

Refreshing on the flag is right everywhere else, because everywhere else is drawing.

SortFromMenu
void SortFromMenu( FName ColumnId, ESmartTableSortMode SortMode, bool bSecondary )

A sort picked from the header menu. It comes through here instead of binding the public sort calls straight, so a menu sort saves the layout exactly as a header click does.

LoadStoredLayout
bool LoadStoredLayout()

Reads whatever is filed under the id of this table into ActiveLayout. It loads only, and uses nothing.

True when a stored layout was found and taken. False covers all three ways there is nothing to take: no store, no id, and a store with nothing filed under this id. A caller needs them apart, because an empty ActiveLayout that came off disk means unsorted and an empty one that was never filled means nothing at all.

Split out because the callers really do differ about when the header and the model should hear about it. Building the widget has to load before the header is there, so a restored width is what the columns first draw at instead of a flash of the named one.

RestoreStoredLayout
void RestoreStoredLayout()

Reads whatever is filed under the id of this table again, and uses it.

One body for both ways in: a new id, and a new store. They are the same question asked twice.

GetOrCreateItemsModel
USmartTableObjectModel & GetOrCreateItemsModel()

The model SetItems fills. Built the first time it is needed, so a table with no items builds none.

ItemForRow
UObject * ItemForRow( int32 NaturalRow ) const
RowIdAt
FName RowIdAt( int32 NaturalRow ) const

The id of this row, or None with no model or no row.

ReasonForRow
ESmartTableAssignReason ReasonForRow( int32 NaturalRow ) const

Whether this row reads Added or Scrolled, for the cells and wrapper built for it.

UpdateArrivals
void UpdateArrivals()

Plays the entrances of any new rows now on screen, and drops the ones that ran out of time. Runs once a frame, and gives up at once while nothing is new.

SampleViewAtEnd
void SampleViewAtEnd()

Writes down whether the view is at the end, once a frame, before anything can add to the list.

ReassignChangedRows
void ReassignChangedRows()

Reads again any built row whose number now holds a different row than the one it drew.

The list keys its widgets by row number, and dropping a row retires the last number, not the dropped one.

So when the item set comes out the same length it went in, the list finds a widget for every number and asks none of them to read again. A capped feed adding one row and dropping one in the same frame is exactly that, and the table looks frozen until a scroll forces new widgets.

The row id spots the change. A row whose id has not moved is left alone.

ArmArrivalIfOwed
void ArmArrivalIfOwed( int32 NaturalRow, USmartTableRowWidget * Wrapper )

Hides a wrapper that is owed an entrance nothing has seen yet.

Two call sites. AcquireRowWidget arms whenever the pool gives a wrapper out, new or reused. ReassignChangedRows arms when a wrapper that already exists takes a different row.

MakeRowMenu
TSharedRef< class SWidget > MakeRowMenu( const FSmartTableKeptRow & Row )

The row menu as a widget, from RowMenuEntries as they stand once OnRowMenuOpening has run.

OpenRowMenuInternal
void OpenRowMenuInternal( int32 NaturalRow, const TOptional< FVector2D > & AbsolutePosition, int32 DrivingUserIndex )

Both routes in one place. An unset position means under the row, the answer with no pointer.

BeginScrollToRow
void BeginScrollToRow( int32 PresentedRow )

Starts the smooth move that every scroll to a row goes through. With no distance to cover it places the view on the row instead.

A presented row, never a natural one. Every caller already knows where it points in drawn order, and taking that keeps the search out of here.

StepScrollAnimation
void StepScrollAnimation( float DeltaTime )

One step of that move, off the timer the table already runs. The step that arrives hands over to PlaceScrollOnRow, since only the list can put the view exactly on a row.

StartAtEndIfOwed
void StartAtEndIfOwed()

Puts the view at the end once, for a table with bStartAtEnd set.

From the timer and never the constructor, which has neither of the two things it needs. The rows, which a model may not have given over yet, and a list that has been laid out.

Against a viewport of no height, far enough down to show the last row works out as the top.

RetargetScrollAfterStructuralChange
void RetargetScrollAfterStructuralChange()

Aims a move that is already going at wherever its row went, or stops it when the row is gone. RebuildRowIndices calls it, and that runs on every change to the row set there is.

TopRowOnScreen
FSmartTableHeldRow TopRowOnScreen()

The row at the top of the view, read before the rows are rebuilt. After the rebuild the built rows draw the new rows, and nothing is left that knows what the view showed.

RememberTopRow
void RememberTopRow()

Writes down the row at the top of the view once the rows are rebuilt, for the next change in this frame. See FSmartTableScrollMove::LeftAtTop

ScrollAfterRowSetChange
void ScrollAfterRowSetChange( const FSmartTableRowSetDiff & Diff, const FSmartTableHeldRow & Held, int32 PresentedBefore, bool bWasAtEnd )

Scrolls the view once the rows are rebuilt after a row set change, whichever route the rows took. FSmartTableScrollMove::ScrollAfterRowSet decides. This carries it out.

HoldRowAtTop
void HoldRowAtTop( const FSmartTableHeldRow & Held, const FSmartTableRowSetDiff & Diff )

Puts the held row back at the top of the view. Nothing moves when the row left the set.

LastDrawnArrival
int32 LastDrawnArrival( const FSmartTableRowSetDiff & Diff ) const

The drawn place of the arrived row drawn last, or INDEX_NONE when none is drawn. A filter can hide every one of them.

PlaceScrollOnRow
void PlaceScrollOnRow( int32 NaturalRow )

Puts the view exactly on a row the move has already reached.

The last row is the odd one out, and the reason this is here. The target worked out is the row plus one, less the viewport over RowHeight, which is the true bottom only while every row is exactly RowHeight tall. One taller row and the target sits short of it.

RequestScrollIntoView will not fix that: it only promises the row is visible, and at that offset it already is. So for the last row the list gets asked where its own bottom is.

WarnIfSizedByContent
void WarnIfSizedByContent()

Says so, once, when this table is sized by its content instead of by its parent.

A list virtualises against the height it is given. Put one where the parent asks for a desired size instead, and the answer is the sum of every row. Every row then gets built.

Three parents do it: a Vertical Box slot left on Auto, a Scroll Box, a Size Box with no override. The table still draws correctly, and nothing on screen says anything is wrong.

WarnAboutDuplicateColumnIds
void WarnAboutDuplicateColumnIds( const TArray< FSmartTableColumn > & InColumns )

Says so, once per id, when a column set names the same ColumnId twice.

The Blueprint compiler says this too, but a table built only from C++ never compiles a widget. That is exactly the case most likely to build its columns in a loop.

See ValidateCompiledDefaults

MirrorLastColumnStyle
void MirrorLastColumnStyle()

Writes HeaderStyle.LastColumnStyle from HeaderStyle.ColumnStyle. Called wherever HeaderStyle is written, which is every copy of the look.

ApplyStyleAsset
void ApplyStyleAsset()

Copies either the style asset or the named values into the inline style properties.

The copy is what lets every getter and Slate argument in this class point at one member and know nothing about style assets. It writes over the named values. Those go into AuthoredStyle on the first call and never again, or clearing the asset would leave the last style in place.

RefreshAppliedStyle
void RefreshAppliedStyle()

Pushes whatever the inline properties now hold out to the live Slate.

SetStyleAsset and the palette refresh both use it, because a style that comes in without this is only half done. STextBlock::SetTextStyle copies instead of keeping the pointer, so a cell built under the last look keeps that look until something sets it again.

MeasureRowNumberWidth
void MeasureRowNumberWidth()

Measures the row number gutter. Called wherever its inputs move: the presented row count, the text style the numbers draw in, and whether the gutter is shown at all.

GetFocusBorderVisibility
EVisibility GetFocusBorderVisibility() const

Collapsed while bShowFocusBorder is off, and collapsed while neither this table nor anything inside it holds the keyboard.

ForEachGeneratedRow
void ForEachGeneratedRow( TFunctionRef< void( SSmartTableRow & ) > Visit )

Walks every row that still exists, and drops the ones that do not.

The clearing out is why this is one function and not a loop written at each call site. A row the list has thrown out leaves a dead weak pointer behind, and nothing else clears them, so every walk must.

ColumnForIntent
FName ColumnForIntent( FName ColumnId )

The column an intent should work on, selecting it on the way through. Every column intent comes through here, so touching a column selects it is one rule in one place.

ColumnSelectionChanged
void ColumnSelectionChanged( bool bChanged )

Broadcasts and redraws when bChanged is true. The one place a selection change becomes visible.

Events 10

Columns 1

On Column Selection ChangedOnColumnSelectionChangedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Columns" ) FOnSmartTableColumnSelectionChanged OnColumnSelectionChanged

Fires only when the aim really moves. Selecting the column already aimed at raises nothing.

Events 7

On Selection ChangedOnSelectionChangedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableSelectionChanged OnSelectionChanged

The whole selection, every time it changes. An empty array is what a cleared selection sends.

On Row ActivatedOnRowActivatedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableRowActivated OnRowActivated

A click twice, or Enter on a focused row. Item is null when the model builds no objects.

On Sort ChangedOnSortChangedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableSortChanged OnSortChanged

The order the model settled on, which is not always the one that was asked for.

On Layout ChangedOnLayoutChangedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableLayoutChanged OnLayoutChanged

Fires only on a change the table itself did: a resize, a hide, a sort, a reset. Never on ApplyLayout, so code that saves on it can not loop against its own restore.

On Cell Value ChangedOnCellValueChangedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableCellValueChanged OnCellValueChanged

A control inside a cell moved.

The table says so and writes nothing. It gives back the item, the column and the new value. Writing that value and calling NotifyItemChanged is the owner's work.

On Row Menu OpeningOnRowMenuOpeningevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableRowMenuOpening OnRowMenuOpening

Fires before the entries are read, so SetRowMenuEntries from here builds the menu for this row.

On Row Menu Entry ChosenOnRowMenuEntryChosenevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Events" ) FOnSmartTableRowMenuEntry OnRowMenuEntryChosen

Which entry was picked, and on which row. The table itself does nothing with either.

Drag and Drop 2

On Cell DroppedOnCellDroppedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Drag and Drop" ) FOnSmartTableCellDropped OnCellDropped

A cell was dropped on another and nothing has moved. Moving the data is the owner's to do.

On Rows DroppedOnRowsDroppedevent
UPROPERTY( BlueprintAssignable, Category = "Smart Table|Drag and Drop" ) FOnSmartTableRowsDropped OnRowsDropped

Rows were dropped and the table has already moved them.

Public variables 3

OnExtendHeaderMenu
FOnSmartTableExtendHeaderMenu OnExtendHeaderMenu

Adds to the right click menu of a column, after the built-in entries.

A plain C++ delegate, not a Blueprint one, because an FMenuBuilder payload has no reflected form.

OnExtendRowMenu
FOnSmartTableExtendRowMenu OnExtendRowMenu

The same way out for the row menu, added after the named entries.

RowNumberColumnIdstatic
static const FName RowNumberColumnId

The pinned index column. Nothing stops a column being authored with this id, and nothing warns. The odd name is the only guard.

Protected variables 68

Smart Table 16

ColumnsColumnspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "The columns this table draws, in the order they are named here. Reorder them with the arrows on each entry.", TitleProperty = "ColumnId" ) ) TArray< FSmartTableColumn > Columns

The columns, in the order they were named. TitleProperty so a closed row reads as its ColumnId and not as Index 13, which says nothing until it is opened.

Column SourceColumnSourcepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "Where the column list is written against: an item class, a data table, or a file. The other two are hidden once one is picked." ) ) ESmartTableColumnSource ColumnSource = ESmartTableColumnSource::None

Which of the three fields below the columns of this table are written against.

The other two are hidden, not greyed out, and cleared when the mode moves off them. Two sources half set is not a pair to keep: whichever was read first would win, with nothing to say which.

Expected Item ClassExpectedItemClasspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "The class each row stands for. Optional, and the most useful field here: it fills the binding dropdown, checks bindings as they are typed, and draws real values in the designer.", EditCondition = "ColumnSource == ESmartTableColumnSource::ItemClass", EditConditionHides ) ) TSubclassOf< UObject > ExpectedItemClass
Editable when
ColumnSource == ESmartTableColumnSource::ItemClass

The class the columns are written against. Optional, and the single most useful thing to set.

It fills the binding dropdown on every column, checks the bindings while they are being typed, and lets the designer preview draw real values from the class defaults.

A Blueprint class works the same as a C++ one, since bindings resolve by name.

Source TableSourceTablepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "The data table the columns are written against. Naming it checks every column against the row struct and lets the column list be built from it.", EditCondition = "ColumnSource == ESmartTableColumnSource::DataTable", EditConditionHides ) ) TSoftObjectPtr< class UDataTable > SourceTable
Editable when
ColumnSource == ESmartTableColumnSource::DataTable

The DataTable the columns are written against. The other half of ExpectedItemClass.

Naming it changes nothing at runtime. What it does is check every column against the row struct when the asset compiles, and let the whole column set be built from it instead of typed out by hand.

Soft on purpose. A hard reference to a missing asset turns into null with no log line, which reads as never setting one. A soft path outlives the asset, so the table can say it is gone.

Source FileSourceFilepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "A csv or json whose shape the columns are written against, for data that is not an asset. Read only when the button below is pressed.", EditCondition = "ColumnSource == ESmartTableColumnSource::File", EditConditionHides, FilePathFilter = "Data files (*.csv; *.json)|*.csv;*.json", RelativeToGameDir ) ) FFilePath SourceFile
Editable when
ColumnSource == ESmartTableColumnSource::File

A .csv or .json whose shape the columns were written against, for data that is not an asset.

Read only when the button below is pressed. A packaged game never opens this path, so moving the file breaks nothing at runtime.

The picker stores a file inside the project relative to the project folder, and the button reads it from there. The same path then names the same file in every copy of the project.

Only the header row of a CSV and the first object of a JSON get read. Neither is a model, and the plugin ships no model for them.

Selection ModeSelectionModepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table" ) ESmartTableSelectionMode SelectionMode = ESmartTableSelectionMode::Single

Whether clicking a row selects it, and how many rows can be selected. None is for a table whose job is editing, where the row mark only gets in the way of the field being changed.

Interactive CellsbInteractiveCellspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "Let a click reach the widget inside a cell. Off, the row takes every click." ) ) bool bInteractiveCells = false

Whether a click can reach the widget inside a cell.

Off, a cell is hit test invisible and the row under it takes every click. That is what a table only ever selected from needs, and it is why a button inside a cell does nothing until this is on.

On, what a cell holds takes input first. Only the ORDER is decided here. A control that does not take the click lets it through to the row, so selection goes on working.

bAllowCellDragDrop makes cells hit testable whatever this says.

Row HeightRowHeightpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "The least height every row gets, in pixels. Zero lets each row grow to the height of its own cells.", ClampMin = "0.0" ) ) float RowHeight = 24.0f
Minimum
0.0

The least height every row gets, in pixels. Zero lets each row grow to the height of its own cells.

Arrival Window SecondsArrivalWindowSecondspanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table", meta = ( ToolTip = "How long a new row waits to be seen before the table gives up on its entrance. Keep it above Scroll To Row Seconds.", ClampMin = "0.0" ) ) float ArrivalWindowSeconds = 2.0f
Minimum
0.0
AdvancedDisplay
The panel hides it under the arrow.

How long a new row waits to be seen before the table drops its entrance. Seconds.

A new row can be off screen, and its entrance is still right a moment later. A minute later it is not.

Keep this above ScrollToRowSeconds, or a row still being scrolled to runs out of time on the way.

Stick To EndbStickToEndpanel
UPROPERTY( EditAnywhere, Category = "Smart Table", meta = ( ToolTip = "Keep a view that sits at the end at the end as rows arrive. Off, the table scrolls to each new row wherever it lands." ) ) bool bStickToEnd = true

Keep a view that sits at the end at the end as rows arrive.

What a chat window does: scroll up and the table stays put, scroll back down and it follows again. Staying put holds with it off too. The row at the top of the view stays there while rows arrive below it or leave above it.

Off, the table scrolls to each new row instead, wherever the view sits and wherever the sort put the row. Neither happens with bScrollToAddedRow off.

Start At EndbStartAtEndpanel
UPROPERTY( EditAnywhere, Category = "Smart Table", meta = ( ToolTip = "Open at the end of the rows instead of the top. For a log or a chat that gets all its rows at once." ) ) bool bStartAtEnd = false

Open at the end of the rows instead of the top.

For a log or a chat that gets all its rows at once. It fires once, on the first laid out frame that has rows. A table filled from empty never uses it, because its first row is already the end.

bStickToEnd can not do this: a table built holding more rows than fit opens at the top, which reads as scrolled up, so following never starts. ScrollToEnd is the same move for rows that come in later.

Reveal FractionRevealFractionpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table", meta = ( ToolTip = "How much of a row has to be on screen before its entrance plays. Keep it below one.", ClampMin = "0.05", ClampMax = "1.0" ) ) float RevealFraction = 0.75f
Range
0.05 to 1.0
AdvancedDisplay
The panel hides it under the arrow.

How much of a row has to be on screen before its entrance plays. A fraction of the row height.

Not 1.0. A row is never exactly whole once rounding and a part pixel viewport are in play, so asking for all of it gives entrances that never fire. Not near zero either. A strip of a row two pixels high is what the fraction is here to rule out.

Row Widget ClassRowWidgetClasspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "A widget wrapped around every row, so a row can be animated in Sequencer. Empty builds no wrapper." ) ) TSubclassOf< USmartTableRowWidget > RowWidgetClass

A widget wrapped around the cells of every row, so a row can be animated in Sequencer.

Empty by default: no wrapper is built and nothing is pooled, so a big table pays nothing for it. Name a class and each visible row gets one, reused as the list scrolls.

See USmartTableRowWidget for the named slot the cells go into.

Height ModeHeightModepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "How tall the table is. Fill takes the height it is given and scrolls, and Fit Rows asks for every row." ) ) ESmartTableHeight HeightMode = ESmartTableHeight::Fill

How tall the table is. Fill is a list: it takes the height it is given and scrolls.

FitRows is a block of rows. It asks for all of them, so a parent that sizes to fit grows to show every one. Asking is all it does, and a parent that gives less still wins. The table goes back to scrolling, so no row ends up out of reach.

Max Visible RowsMaxVisibleRowspanelpure virtual
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "Fit Rows only. Zero asks for every row, and anything else shows up to that many and then scrolls.", ClampMin = "0", EditCondition = "HeightMode == ESmartTableHeight::FitRows" ) ) int32 MaxVisibleRows = 0
Minimum
0
Editable when
HeightMode == ESmartTableHeight::FitRows

Fit Rows only. Zero asks for every row, and anything else shows up to that many and then scrolls.

Show HeaderbShowHeaderpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table", meta = ( ToolTip = "Draw the header row. Off, the same columns draw as a plain list of cells, with no labels, no click to sort and no resizing." ) ) bool bShowHeader = true

Draw the header row. Off, the same columns draw as a plain list of cells, with no labels, no click to sort and no resizing.

Drag and Drop 6

Allow Cell Drag DropbAllowCellDragDroppanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Drag and Drop", meta = ( ToolTip = "Let cells start drags and take drops. The table moves nothing on its own: it reports the drop and the owner decides." ) ) bool bAllowCellDragDrop = false

Lets cells start drags and take drops. Off unless a table really moves things around.

The plugin does no dragging of its own. OnCellDropped says what happened and the owner decides.

See SetAllowCellDragDrop

Allow Row ReorderbAllowRowReorderpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Drag and Drop", meta = ( ToolTip = "Let a row be dragged into a new place. Needs a model that can move rows, which the items model can and a data table one cannot." ) ) bool bAllowRowReorder = false

Lets a row be dragged into a new place in the order. Needs a model that answers MoveRows. The built-in items model does, a DataTable one can not.

A press that goes nowhere still selects: a drag only starts past the engine's drag distance. Drag a row that is selected with several others and all of them move.

See MoveRowsTo

Mark Drag CellsbMarkDragCellspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Drag and Drop", meta = ( ToolTip = "Tint the cell a drag started on and the cell it is over. Off for a cell class that draws its own drag marking." ) ) bool bMarkDragCells = true

Whether the table marks the source and target cells itself during a drag.

On, a cell class gets the marking without drawing a thing itself. Off, the two brushes are never drawn, which is what a cell class drawing its own marking from USmartTableCell::OnCellDragRoleChanged needs.

Drag Source BrushDragSourceBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Drag and Drop", meta = ( ToolTip = "Drawn over the cell a drag started on. See through dark by default, so the cell still shows through.", EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush DragSourceBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

Drawn over the cell the drag started on. See through dark by default, since what it held is under the cursor now, and a cell that still looks full while its contents are moving looks like a copy.

Drag Target BrushDragTargetBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Drag and Drop", meta = ( ToolTip = "Drawn over the cell a drag is hovering, never instead of it. The cell goes on drawing itself underneath.", EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush DragTargetBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

Drawn over the cell a drag is hovering, never instead of it. The cell goes on drawing itself underneath.

Row Insert Marker BrushRowInsertMarkerBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Drag and Drop", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush RowInsertMarkerBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

Drawn in the gap a release would drop rows into. Its height is how thick the bar draws.

Row Numbers 3

Show Row NumbersbShowRowNumberspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Row Numbers", meta = ( ToolTip = "A numbered column pinned to the far left. It counts what is drawn, so after a sort the numbers still read one, two, three down the screen." ) ) bool bShowRowNumbers = false

A numbered column pinned to the far left.

It counts what is drawn, not the rows underneath. After a sort the numbers still read 1, 2, 3 down the screen, because a column of numbers that jumps around can not be used to point at the fourth one.

Row Number Column WidthRowNumberColumnWidthpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Row Numbers", meta = ( ClampMin = "8.0" ) ) float RowNumberColumnWidth = 48.0f
Minimum
8.0

The least width the pinned number column gets, in pixels. It grows past this on its own once the row count needs more digits.

Row Number Text StyleRowNumberTextStylepanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Row Numbers", meta = ( ToolTip = "The text in the pinned number column. Kept apart from the cell text style.", EditCondition = "StyleAsset == nullptr" ) ) FTextBlockStyle RowNumberTextStyle
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

The text in the pinned number column. Its own style, because a number column usually reads quieter than the data beside it.

Columns 8

Stretch Last ColumnbStretchLastColumnpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ToolTip = "Give the last visible column whatever width is left over, so there is no ragged right edge. Drag it wider and it stays there." ) ) bool bStretchLastColumn = false

The last visible column takes whatever width is left over.

Off by default, since a column whose width means something should keep the width it was given. A bar is one, and a fixed width code is another. On, the table has no ragged right edge, which is what a notes column needs.

A floor, not an override: drag that column wider than the leftover and it stays there.

Allow Horizontal ScrollbAllowHorizontalScrollpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ToolTip = "What happens when the columns want more width than there is. Off they shrink to fit, on they keep their widths and a sideways scrollbar shows up." ) ) bool bAllowHorizontalScroll = false

What happens when the columns need more width than the table has.

Off, the columns fit. Widening one takes width from those to its right, down to MinColumnWidth each, and nothing is ever off screen. On, they keep their widths and a sideways scrollbar shows up. That is what a table with thirty columns needs.

bStretchLastColumn is not switched off here. Once the columns run past the edge it finds no leftover to hand out.

Allow Column ReorderbAllowColumnReorderpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ToolTip = "Drag a header onto another to move it. Off, no drag moves a column, and the menu entries Move Left and Move Right go on working." ) ) bool bAllowColumnReorder = true

Drag a header onto another to move it. Off, no drag moves a column. The menu entries Move Left and Move Right go on working, and so do MoveColumn and MoveColumnNextTo.

Allow Column ResizebAllowColumnResizepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ToolTip = "Let the right edge of a column be dragged to resize it. The engine draws its own grip whatever this says, so a locked column still shows a resize cursor and the drag does nothing." ) ) bool bAllowColumnResize = true

Dragging the right edge of a column resizes it.

Off, no drag moves a width. A fixed HUD table needs that without having to clear bResizable on every column it names. SetColumnWidth, SizeColumnToContent and BeginColumnResize still work, so the game and a pad still set widths.

SHeaderRow draws its own sizing grip on every Manual column and offers no way to turn it off. The last few pixels of a locked column still show a resize cursor and still take the press, and the drag writes nothing. HandleColumnWidthChanged refuses it.

Header Menu OptionsHeaderMenuOptionspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns" ) FSmartTableHeaderMenuOptions HeaderMenuOptions

Which entries the built-in right click menu offers, and whether it opens at all.

Resize Grip WidthResizeGripWidthpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ToolTip = "How much of a column's right edge counts as the resize grip, in pixels. Wider is simpler to hit and takes more of the column beside it.", ClampMin = "2.0" ) ) float ResizeGripWidth = 0.0f
Minimum
2.0

How much of the right edge of a column counts as the resize grip, in pixels. Wider is simpler to hit and takes more of the column beside it.

The declaration carries no value on purpose, and the same goes for the two below. The constructor sets all three from USmartTableSettings before anything reads them, so a number written on those lines is written over first. The project settings decide what a new table opens with.

Min Column WidthMinColumnWidthpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ToolTip = "How narrow any column may go, in pixels. It stops a column being taken down to nothing while its neighbours pull width off it.", ClampMin = "1.0" ) ) float MinColumnWidth = 0.0f
Minimum
1.0

How narrow any column may go, in pixels. It is what stops a column being taken down to nothing while its neighbours pull width off it. The project settings decide what a new table opens with.

Max Auto Size Column WidthMaxAutoSizeColumnWidthpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Columns", meta = ( ClampMin = "0.0" ) ) float MaxAutoSizeColumnWidth = 0.0f
Minimum
0.0

The widest a size to content column may go, in pixels. Zero lets it fit the longest value, no matter how wide that is.

Preview 1

Preview Row CountPreviewRowCountpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Preview", meta = ( ClampMin = "0", ClampMax = "24" ) ) int32 PreviewRowCount = 6
Range
0 to 24

How many rows the designer draws with no data, so the widget is not an empty box in the palette. Zero previews the empty message, which is the only way to see it without running the game.

Style 16

Style AssetStyleAssetpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "A style shared with every other table using it. Set, it wins over every style field here and greys each one out." ) ) TObjectPtr< USmartTableStyle > StyleAsset

A style shared with every other table that uses it.

Set, it wins over every style property this table has, and each one greys out in the panel. Six of them are filed under Row Numbers, Empty State, Drag and Drop and Busy, not under Style.

A greyed value is not lost. It is what the table goes back to when the asset is cleared.

Restyling a project is then one asset instead of one Details panel per table.

Background BrushBackgroundBrushpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "Drawn behind the rows, under everything else the table draws.", EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush BackgroundBrush
Editable when
StyleAsset == nullptr

Drawn behind the rows, under everything else the table draws.

This one and the styles under it are the look a table always has, which is why they stay in plain sight. The brushes for one feature or another sit behind Advanced. A style asset writes over the lot of them either way.

Row StyleRowStylepanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FTableRowStyle RowStyle
Editable when
StyleAsset == nullptr

One row: its background, and how it draws while hovered, selected and striped.

Header StyleHeaderStylepanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FHeaderRowStyle HeaderStyle
Editable when
StyleAsset == nullptr

The header row: its background, the splitter, and the sort arrows.

Scroll Bar StyleScrollBarStylepanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FScrollBarStyle ScrollBarStyle
Editable when
StyleAsset == nullptr

Both scrollbars, the one down the side and the one along the bottom.

Header Text StyleHeaderTextStylepanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "The text of the header labels. Kept apart from the cell text style.", EditCondition = "StyleAsset == nullptr" ) ) FTextBlockStyle HeaderTextStyle
Editable when
StyleAsset == nullptr

The text of the header labels. Kept apart from the cell text style.

Cell Text StyleCellTextStylepanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "The text in every built-in cell. Only the header style can be overridden per column.", EditCondition = "StyleAsset == nullptr" ) ) FTextBlockStyle CellTextStyle
Editable when
StyleAsset == nullptr

The text in every built-in text cell, and the editable cell's read only and typing text with them. No column overrides it. That pairing exists for the header style and not for this one. A cell class of its own is free to read it or not.

Header HeightHeaderHeightpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "Header height in pixels. Zero takes it from the style asset.", EditCondition = "StyleAsset == nullptr", ClampMin = "0.0" ) ) float HeaderHeight = 0.0f
Minimum
0.0
Editable when
StyleAsset == nullptr

Header height in pixels. Zero means take it from the style.

The declaration carries no value on purpose. The constructor reads the height off the style class with the rest of the look, so a number written on that line is written over before anything reads it. It would still look exactly like the live one.

See USmartTableStyle::HeaderHeight, where it lives once.

Insert Marker BrushInsertMarkerBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush InsertMarkerBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

The bar that shows where a dragged column will end up.

Column Divider BrushColumnDividerBrushpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush ColumnDividerBrush
Editable when
StyleAsset == nullptr

The line between two columns. The header cell draws it, not the splitter, so its width is paid by the header and by no row.

Column Caret BrushColumnCaretBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush ColumnCaretBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

The bar under the column that column input is aimed at.

Focus Border BrushFocusBorderBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Style", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush FocusBorderBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

Drawn around the whole table while it holds the keyboard.

Show Focus BorderbShowFocusBorderpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "Draw the border that says the keyboard is on this table." ) ) bool bShowFocusBorder = true

Draw the focus border at all.

On by default. It is the table-wide answer to where the keyboard went, and focus moves for reasons nothing else shows: a menu closing, or a click taken by the widget above the table. The header's column caret says the same thing for one column, and only while a column is selected.

Show Column CaretbShowColumnCaretpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "Draw the bar under the selected column. Off hides the bar only: the column is still selected and still answers every intent." ) ) bool bShowColumnCaret = true

Draw the bar under the selected column at all.

It gates the drawing only. The selected column is still tracked and still answers every intent given no column id. This is for a UI that already shows the target some other way.

Column Caret Needs FocusbColumnCaretNeedsFocuspanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Style", meta = ( ToolTip = "Hide the column caret while the keyboard is somewhere else. On is for a screen walked by tab, where the caret says which widget the keys go to." ) ) bool bColumnCaretNeedsFocus = false

Hide the column caret while the keyboard is somewhere else.

Off, because a key bound through an input context reaches this table while the focus sits on the viewport, which is where it sits for most of a game. On is for a screen walked by tab, where the caret is saying which of several widgets the keys go to.

The focus border is not covered by this. It draws what focus is doing, so it tests focus always.

Empty State 3

Empty TextEmptyTextpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Empty State" ) FText EmptyText

Shown when the table has no rows at all.

No Matches Text FormatNoMatchesTextFormatpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Empty State", meta = ( ToolTip = "Shown when a filter matches no row. The first argument is the filter text." ) ) FText NoMatchesTextFormat

Shown when a filter matches no row. {0} is the filter text.

A different message from the one above on purpose. Nothing here and nothing matching look like the same blank box and are two different problems.

Empty Text StyleEmptyTextStylepanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Empty State", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FTextBlockStyle EmptyTextStyle
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

The text of both messages above.

Layout 2

Layout StoreLayoutStorepanel
UPROPERTY( EditAnywhere, Instanced, BlueprintReadOnly, Category = "Smart Table|Layout", meta = ( ToolTip = "Set one and this table's column layout lives past the session, filed under the table id below. Left unset, nothing is written anywhere." ) ) TObjectPtr< USmartTableLayoutStore > LayoutStore

Set one and this table's column layout lives past the session, filed under the table id below. Left unset, nothing is written anywhere.

Table IdTableIdpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Layout", meta = ( ToolTip = "What a stored layout is filed under. Taken from the widget name in the designer, and editable after that." ) ) FName TableId

What a stored layout is filed under. Taken from the widget name in the designer, and editable after that.

It is taken again on ANY Details panel edit while a store is set and this is empty. So clearing it and touching another property fills it straight back in.

There is no quiet fallback to the widget path. A rename would either lose the stored layout or, worse, give a renamed table the layout of a different one.

Sorting 4

Allow User SortingbAllowUserSortingpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Sorting", meta = ( ToolTip = "Let a header click sort by that column. Sorting from a graph still works." ) ) bool bAllowUserSorting = true

Clicking a header sorts by it.

Off, no header click or drag to sort does anything. One switch, instead of clearing bSortable on every column. SortByColumn and SetSortSpec still work, so a table can be sorted from the game UI alone.

The gates in this panel work the same way. Each covers the pointer and key routes only, and leaves the function itself open.

Allow Secondary SortbAllowSecondarySortpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Sorting", meta = ( ToolTip = "Hold Shift and click to add a second sort level. Off, the header menu still lists Then Sort By This and greys it out." ) ) bool bAllowSecondarySort = true

Shift clicking a header adds a second sort level. The header menu lists Then Sort By This either way. Off, the entry is drawn greyed.

Off is for a table where one order is the only one that means anything, and a second arrow would look like a bug.

Allow Sort NonebAllowSortNonepanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Sorting" ) bool bAllowSortNone = true

A third click clears the sort and puts the rows back in the model's own order.

Allow Dead Secondary SortbAllowDeadSecondarySortpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Sorting", meta = ( ToolTip = "Let a second sort level go onto a column that cannot move a row. Off refuses the click and dims the header instead." ) ) bool bAllowDeadSecondarySort = false

Let a second sort level go onto a column that could not move a row.

A second level only breaks ties in the first, so a primary with no ties leaves every other column with nothing to do. On, the click goes through and no row moves. Off, it is refused and the header dims.

A column that can still break a tie is never refused, and on an unsorted table the gesture becomes the primary. It costs one pass over the sorted rows, once Shift is held.

Rows 4

Scroll To Added RowbScrollToAddedRowpanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Rows", meta = ( ToolTip = "Scroll when rows arrive, to the end or to the new row as Stick To End says. It scrolls and nothing else, so the selection stays where it was put." ) ) bool bScrollToAddedRow = true

Scroll when rows arrive: to the end, or to the new row, as bStickToEnd says.

On, because a row added at the end sits past the bottom and the only sign of it is a shorter scrollbar. It scrolls and nothing else, so the selection stays where it was put.

Off for a table filled in the background, where pulling the view on every new row fights the reader.

Rows arrive through AddItem, through SetItems, and through a model that says NotifyRowsAdded. A set given all at once, or given to a table that drew no row yet, scrolls nothing.

Scroll To Row SecondsScrollToRowSecondspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Rows", meta = ( ToolTip = "How long any scroll to a row takes, in seconds. Zero jumps straight there.", ClampMin = "0.0" ) ) float ScrollToRowSeconds = 0.25f
Minimum
0.0

How long any scroll to a row takes. Seconds. Zero jumps straight there.

It covers the one above, ScrollRowIntoView and ScrollItemIntoView alike. They move instead of jumping, because a view that changes between two frames says the list moved without saying where from.

bScrollToAddedRow does not gate it: a table that never scrolls by itself still scrolls when asked.

Row Menu OptionsRowMenuOptionspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Rows", meta = ( ToolTip = "Whether a right click on a row opens a menu, and what that menu looks like." ) ) FSmartTableRowMenuOptions RowMenuOptions

Whether a right click on a row opens anything. Off by default.

See FSmartTableRowMenuOptions

Row Menu EntriesRowMenuEntriespanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Rows", meta = ( ToolTip = "What the row menu offers, in order. A menu that is the same on every row needs no code.", TitleProperty = "Id" ) ) TArray< FSmartTableMenuEntry > RowMenuEntries

What that menu offers, in order.

Named here instead of built in code, so a menu that is the same on every row needs no C++. SetRowMenuEntries is the other half, for a menu whose entries depend on the row.

Input 2

Focus On Pointer InteractionbFocusOnPointerInteractionpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Input", meta = ( ToolTip = "Take the keyboard when a pointer touches this table. Off, a click on a header or a gap can leave the arrow keys dead until focus is set from a graph." ) ) bool bFocusOnPointerInteraction = true

Take the keyboard when a pointer touches this table.

On by default. Slate alone gives the keyboard to the nearest widget above the click that can take focus, which for a header or a gap is outside the table. Its arrows and Enter then go dead.

It gates the built-in binding only. FocusTable stays public, so a game can drive focus from its own UI.

Allow Right Click Drag ScrollbAllowRightClickDragScrollpanel
UPROPERTY( EditAnywhere, Category = "Smart Table|Input", meta = ( ToolTip = "Let the right mouse button drag the rows up and down. Turn it off where the right button already means something, such as a cell that takes drops." ) ) bool bAllowRightClickDragScroll = true

Let the right mouse button drag the rows up and down.

Right for a long list on a touchpad, wrong wherever the right button already means something. A table whose cells take drops spends every right press working out if it was a scroll.

Off, only the right drag goes. ScrollRowIntoView, the wheel and touch all keep working.

Busy 3

Busy Overlay Delay SecondsBusyOverlayDelaySecondspanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Busy", meta = ( ToolTip = "How long work has to run before the table says it is busy, in seconds. Work that finishes first shows no spinner at all.", ClampMin = "0.0" ) ) float BusyOverlayDelaySeconds = 0.15f
Minimum
0.0

How long work has to run before the table says it is busy. Seconds.

Not zero by default, since a spinner that only flashes up reads as a bug. Work that finishes inside the wait draws no spinner at all. A busy and not busy pair inside one call is the usual case of that.

Block Input While BusybBlockInputWhileBusypanel
UPROPERTY( EditAnywhere, BlueprintReadOnly, Category = "Smart Table|Busy" ) bool bBlockInputWhileBusy = true

The overlay takes the clicks instead of letting them reach a row that is moving. A table sorting again is about to change whatever row sits under the cursor.

Busy Overlay BrushBusyOverlayBrushpanel
UPROPERTY( EditAnywhere, AdvancedDisplay, Category = "Smart Table|Busy", meta = ( EditCondition = "StyleAsset == nullptr" ) ) FSlateBrush BusyOverlayBrush
Editable when
StyleAsset == nullptr
AdvancedDisplay
The panel hides it under the arrow.

Drawn over the whole table while it is busy.

Private variables 48

CellDrag
LastReconciledWidth
float LastReconciledWidth = -1.0f

What GetAvailableColumnWidth() was the last time the widths were worked out, so TickTable can see when it moves. Negative until the first read, which no real width can be.

Read every frame instead of waiting on a delegate. The width has several causes: the parent resizing, a column being hidden, and the list's own scrollbar. That bar takes its width the moment the rows run past the bottom, which is after the first cells were set.

A wrapping cell is why it matters. The header reads its width every frame, but a cell works out its line breaks once, so a later move leaves it wrapped for a width that is gone.

ResolvedWidths
TMap< FName, float > ResolvedWidths

The width every shown column has right now. ReconcileColumnWidths works them out, RebuildWidget empties the map when the Slate is built again, and everything else only reads: the header width attributes, the edge hit tests, the cell sizing.

Not a UPROPERTY and never written to disk, on purpose: these are worked out from other numbers, and a worked out number kept anywhere goes stale. A column not in here has not been worked out yet.

ModelModel
UPROPERTY( Transient ) TObjectPtr< USmartTableModel > Model
Transient
Never saved.
Items ModelItemsModel
UPROPERTY( Transient ) TObjectPtr< USmartTableObjectModel > ItemsModel
Transient
Never saved.

Owned, because items built in a Blueprint are held nowhere else. A table that only pointed at them would empty itself at the next garbage collection, in no fixed order.

Data Table ModelDataTableModel
UPROPERTY( Transient ) TObjectPtr< USmartTableDataTableModel > DataTableModel
Transient
Never saved.
Preview ModelPreviewModel
UPROPERTY( Transient ) TObjectPtr< USmartTablePreviewModel > PreviewModel
Transient
Never saved.
Background StyleBackgroundStyle
UPROPERTY( Transient ) FTableViewStyle BackgroundStyle
Transient
Never saved.

SListView needs a style and not a brush. FTableViewStyle holds nothing but that brush, so naming it is naming the whole style and not part of one.

Active LayoutActiveLayout
UPROPERTY( Transient ) FSmartTableLayout ActiveLayout
Transient
Never saved.

The stored overrides on top of the named columns. Empty means exactly as named.

EmptyStateText
TSharedPtr< class STextBlock > EmptyStateText

The nothing to show line, kept so a style change can restyle it.

STextBlock copies the style it is given instead of keeping the pointer, so this one widget can not be refreshed by setting a member again the way a brush can. Something has to set it.

It is also the only surface of the table that nothing else rebuilds. Rows, cells and even the no columns placeholder all come back through RebuildHeader.

Cell PoolCellPool
UPROPERTY( Transient ) FUserWidgetPool CellPool
Transient
Never saved.

The engine's own pool, keyed by class. Held here instead of writing a second one.

Row Widget PoolRowWidgetPool
UPROPERTY( Transient ) FUserWidgetPool RowWidgetPool
Transient
Never saved.

The same pool again, for row wrappers. Kept apart from CellPool only because the two hold different classes and are freed at different moments. Stays empty while RowWidgetClass is empty.

RowWidgetSlotWarnedFor
TWeakObjectPtr< const UClass > RowWidgetSlotWarnedFor

The RowWidgetClass this table has already warned about. Null when it has warned about none.

Keyed by the class and not by a flag, because the class is the only input the answer depends on and it moves. Pointing RowWidgetClass at a second broken Widget Blueprint has to speak again, and a flag that has been set stays set for the life of the table.

A wrapper with no Columns slot is wrong for every row, so saying it per row would bury the log.

Arrivals

The new rows this table still owes an entrance to.

The rules are all in the type, which never holds this table, so each of them can be read with no widget and no model.

See USmartTableModel::NotifyRowsAdded, which is how a model says a row is new. See FSmartTableArrivals

bWarnedSizedByContent
bool bWarnedSizedByContent = false

Whether the height warning has already been given once for this table.

SetHeightMode and SetRowHeight reset it, because those are the two inputs the warning's own advice names: set MaxVisibleRows, or use HeightMode Fill. A second wrong answer has to speak again.

One input can not be in the key, and it is named here instead of hidden: whether the parent sizes to its content. That lives in a Slate slot owned by something else, with no moment this class can watch, so moving the table under another such panel resets nothing.

Scroll

The move going on, where the view sits, and whether the opening scroll is paid.

The rules are all in the type, which never holds this table, so each of them can be read with no widget, no list and no model.

See FSmartTableScrollMove

MeasuredRowNumberWidth
float MeasuredRowNumberWidth = 0.0f

What the gutter last measured, before RowNumberColumnWidth is taken as a least against it. Written only by MeasureRowNumberWidth, which every path that can move it calls.

Applied Style AssetAppliedStyleAsset
UPROPERTY( Transient ) TObjectPtr< USmartTableStyle > AppliedStyleAsset
Transient
Never saved.

The style asset the inline properties hold a copy of right now.

SynchronizeProperties runs after every Details panel edit, and copying the asset there every time would put the named snapshot back over an inline style property just typed in. Reading this is what separates a change of asset from any other change on the table.

Authored StyleAuthoredStyle
UPROPERTY( Transient ) TObjectPtr< USmartTableStyle > AuthoredStyle
Transient
Never saved.

The named look, taken once before any style asset is copied over it.

FramesSinceSizeCheckpure virtual
int32 FramesSinceSizeCheck = 0

Ticks since WarnIfSizedByContent last looked. It walks to the window, which is a parent chain, and the shape it looks for arrives with a resize or a new layout, never inside one frame.

bWarnedEmptyRowMenu
bool bWarnedEmptyRowMenu = false

Whether the row menu has already said it has nothing to show. Cleared by SetRowMenuEntries.

Two things make the menu whole and only one of them is keyed here. Binding OnExtendRowMenu is the other, and a delegate offers no moment to clear a flag from, so a table that binds it and then drops the binding says nothing about the second empty menu.

RowIndices
TArray< int32 > RowIndices
GeneratedRows
TArray< TWeakPtr< SSmartTableRow > > GeneratedRows

The rows the list has really built. Weak, and cleared out as they go.

ListView
TSharedPtr< class SSmartTableListView > ListView
VerticalScrollBar
TSharedPtr< class SScrollBar > VerticalScrollBar

Pinned outside the sideways scroll box. A list builds a bar of its own inside the scrolled content. On a table wider than its viewport that one sits off to the right, out of reach until the view has scrolled that far.

HorizontalScrollBox
TSharedPtr< class SScrollBox > HorizontalScrollBox

The viewport the columns are read against, and what a resize scrolls when it runs off the edge.

ItemlessColumnsWarned
TSet< FName > ItemlessColumnsWarned

So the no item column warning is said once per column, and not once per cell, per scroll.

CellClassFailuresWarned
TMap< FName, TWeakObjectPtr< const UClass > > CellClassFailuresWarned

The class each column was last warned about, so a broken CellClass is said once per column and not once per cell, per scroll.

Two inputs decide the answer and both are in the key: the column, and the class it names. A Details panel edit reaches neither SetColumns nor any other reset, so the column alone would go quiet the moment CellClass moved to a second broken class.

See RowWidgetSlotWarnedFor, the same member for the row wrapper, for the same reason.

ItemSetterClassesWarned
TSet< TWeakObjectPtr< const UClass > > ItemSetterClassesWarned

The cell classes whose ItemSetter mistake this table has said. Keyed by the class alone, since the mistake belongs to the class. A Blueprint compiled again is a new class and is said again.

bMissingTableIdWarned
bool bMissingTableIdWarned = false

The missing TableId warning is about the table, not about the change that set it off. Every drag, hide and sort would otherwise say it again for the rest of the session.

HeaderRow
TSharedPtr< SHeaderRow > HeaderRow
bApplyingPresentation
bool bApplyingPresentation = false

Guards the rebuild and the broadcast. Code that sorts from inside a sort handler would otherwise call back into itself, and a model with no work dispatcher set sorts inside the same call. Nothing in the plugin sets one, so that is what every table does until a game says otherwise.

bHoldSelectionBroadcast
bool bHoldSelectionBroadcast = false

Set while MoveRowsTo runs its rebuilds. Each rebuild puts the selection back, and a sorted drop runs two of them. HandleSelectionChanged holds its broadcast back while this is set.

bSelectionBroadcastOwed
bool bSelectionBroadcastOwed = false

A broadcast the hold above kept back. MoveRowsTo sends it once, after the rows are in place.

bWalkingRowCells
bool bWalkingRowCells = false

A row is part way through walking its own cells right now. FCellWalkScope is the only writer, so there is nothing to key and nothing to leave set.

A cell event is a Blueprint event. OnCellAssigned or OnCellReleased can call SetColumnVisible, and SHeaderRow::ClearColumns broadcasts straight back into GenerateWidgetForColumn on the row that is still walking. The copy a walk takes is safe to READ, and that is all. Inside the broadcast a cell can be released, given to another row, then given this row's number by a walk on a stale copy.

Not bApplyingPresentation, which guards a model broadcast re-entering itself.

bHeaderRebuildPending
bool bHeaderRebuildPending = false

A header rebuild was turned back by that guard and is still owed.

There is nothing to key it by. A rebuild reads every input again when it runs, and the one fact worth remembering is that a rebuild is owed at all.

TickTable never stops, so the owed rebuild cannot be dropped on the floor. bColumnWidthsDirty and bLayoutSavePending are the same shape.

DeadSecondaries

RecomputeDeadSecondaryColumns is the only writer. RefreshDeadSecondaryColumns is a dirty gated wrapper over it for the drawing readers, and neither is const. Every reader runs one of them first, which is what lets the BlueprintPure getter stay a plain read.

bBusy
bool bBusy = false
bColumnWidthsDirty
bool bColumnWidthsDirty = true
bHeaderWidthsLive
bool bHeaderWidthsLive = false

False until the first pass rebuilt the header with every resizable column in Manual mode.

Before that the header holds the named Fixed and Fill modes, because a bound attribute would answer in fill weights until a pass has run. The first frame would be one pixel wide.

It is not a flag on the pass itself, which keeps running for the whole life of the table. Once per SLATE rebuild, though, and not once per object: RebuildWidget puts this back to false, and a table can be rebuilt more than once.

HeaderGutterWidth
float HeaderGutterWidth = 0.0f

The row number width the header was built with. FixedWidth is a Slate argument and not an attribute, so a gutter that grows past it can only be widened by a rebuild.

bLayoutSavePending
bool bLayoutSavePending = false
FocusedRow
int32 FocusedRow = INDEX_NONE

The row the caret is on, in natural space so it lives through a sort the way a selection does.

This is a second caret. SListView keeps its own, and the two have to agree, or Space and a pad move would work around different rows.

MoveSelection keeps them together by selecting with ESelectInfo::OnNavigation. The engine only moves its own caret for a selection that is not Direct, which is why SetRowSelected leaves both alone.

ColumnSelection

The aim, and the rules that move it. A plain value type with no widget in reach.

See FSmartTableColumnSelection

DuplicateColumnIdsWarned
TSet< FName > DuplicateColumnIdsWarned

The doubled column ids this table has already warned about.

Keyed by the ids themselves and not by a flag, because those ids are the whole input the answer depends on. Fix one and leave another, and the table has something new to say. SetColumns is the only writer, so an entry can not outlive the column set it is about.

Resize

The resize going on now. Both routes into a resize write it, and one Reset ends either.

See FSmartTableResizeGesture

BusyStartSeconds
double BusyStartSeconds = 0.0

When busy was set. GetBusyOverlayVisibility reads it against BusyOverlayDelaySeconds every time the overlay is asked to draw, so the wait needs no timer of its own.

Delegate types 12

FOnSmartTableSelectionChangedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSmartTableSelectionChanged, const TArray< int32 > &, SelectedRows )

The selection, once it changed. One broadcast per change, carrying the whole set.

Rows and not items. A model that builds no items gives back null for every one of them, and GetSelectedItems is one call away where items do exist.

A cleared selection arrives as an empty array. Nothing else marks it.

FOnSmartTableRowActivatedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FOnSmartTableRowActivated, UObject *, Item, int32, NaturalRow )

A click twice, or Enter on a focused row. Item is null over a model that builds none, so the row comes with it.

FOnSmartTableSortChangedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSmartTableSortChanged, const FSmartTableSortSpec &, SortSpec )
FOnSmartTableLayoutChangedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSmartTableLayoutChanged, const FSmartTableLayout &, Layout )
FOnSmartTableColumnSelectionChangedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FOnSmartTableColumnSelectionChanged, FName, ColumnId )

The column the table is aimed at changed. NAME_None means the aim was cleared.

FOnSmartTableCellValueChangedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams( FOnSmartTableCellValueChanged, UObject *, Item, int32, NaturalRow, FName, ColumnId, float, Value )

A control inside a cell moved. The table writes nothing, because where the value lives is not something it can know.

It carries the row as well as the item. A model that builds no items still has rows to edit, and Item is null for every one of them.

FOnSmartTableCellDroppedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams( FOnSmartTableCellDropped, USmartTableCellDragDropOp *, Payload, int32, TargetNaturalRow, FName, TargetColumnId )

A cell was dropped onto another cell, and nothing has moved.

The table says what happened and the owner moves the data, the same way a sort works. A drop might mean a swap, a new row, or nothing at all, and only the owner knows which.

Both rows are natural, numbered as the rows stand when the drop lands.

The payload's SourceTable may not be this table. A cell drop from ANOTHER Smart Table is taken: a cell names a column and not a place in an order. A ROW drop across tables is refused, because two tables over the same rows mean their numbers differently. SourceTable says which kind arrived.

FOnSmartTableRowsDroppedevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FOnSmartTableRowsDropped, const TArray< int32 > &, MovedNaturalRows, int32, InsertBeforeNaturalRow )

Rows were dropped and the table has ALREADY moved them. This is the opposite of OnCellDropped, which moves nothing at all: a reorder goes through MoveRows, and the table can run that itself.

Both numbers name rows as they stood before the move. An owner keeping its own copy of the order needs them that way round.

FOnSmartTableRowMenuOpeningevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams( FOnSmartTableRowMenuOpening, UObject *, Item, int32, NaturalRow )

A row's menu is about to be built. SetRowMenuEntries runs from here for a menu that depends on the row.

It fires before the entries are read, not after. One named list can then serve a menu whose entries differ per row. The menu is built from the list as it stands when the event returns.

FOnSmartTableRowMenuEntryevent
DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams( FOnSmartTableRowMenuEntry, FName, EntryId, int32, EntryIndex, UObject *, Item, int32, NaturalRow )

Which entry was picked, on which row. The table runs nothing off it.

The row and its item are read as the entry is picked. A row that moved while the menu was open comes at its new number, and a row that left raises nothing.

Both the id and the index arrive, because a handler written as a switch takes one or the other.

EntryIndex is the place in RowMenuEntries as named, so it holds still when an entry hides itself. A handler keyed on the drawn place works on the wrong entry the moment a row hides one.

FOnSmartTableExtendHeaderMenu
DECLARE_DELEGATE_TwoParams( FOnSmartTableExtendHeaderMenu, FName, FMenuBuilder & )

The column the menu was opened on, and the builder to add entries to.

FOnSmartTableExtendRowMenu
DECLARE_DELEGATE_TwoParams( FOnSmartTableExtendRowMenu, const FSmartTableKeptRow &, FMenuBuilder & )

The C++ way out for a row menu that needs more than a list of entries: submenus, icons, checks.

The row comes kept. Its NaturalRow is right while the menu is built, and an entry picked later finds the row again with FindKeptRow on the model.

FCellWalkScope struct

Held by a row for as long as it is walking its own cells, so a header rebuild knows to wait.

Not part of the API. Only this plugin's own rows hold one, and it is public only because those rows are Slate widgets and not members of this class.

A scope, never a pair of calls. An early return, a throw, or a cell event that never comes back would each leave a plain flag set, and the header frozen for the life of the table. Two of them can sit inside each other, because it puts back what it found instead of clearing.

Constructors and destructor 4

ConstructorFCellWalkScope
explicit FCellWalkScope( USmartTable * InTable )
Destructor~FCellWalkScope
Copy constructorFCellWalkScopedeleted
FCellWalkScope( const FCellWalkScope & ) = delete
Copy assignmentoperator=deleted
FCellWalkScope & operator=( const FCellWalkScope & ) = delete

Private variables 2

Table
TWeakObjectPtr< USmartTable > Table

Weak, because a row outlives its table by a frame during teardown and ReleaseCells runs then.

bPrevious
bool bPrevious = false

Next: SmartTableTypes.h