SmartTableModel.h

Where the rows come from, and the one class to subclass when they come from you.

Source/SmartTables/Public/SmartTableModel.h

What a table draws.

Most tables never name one. USmartTable::SetItems builds one inside itself. This is for data that is not a set of UObjects, or is too big to hold as one.

There are two row spaces. Natural space is what a subclass sees: it never moves, so NotifyRowChanged( 7 ) still means row 7 after any amount of sorting. Presented space is what the table draws: natural space read through the permutation and the filter mask this base holds. A subclass can not override the readers for it.

That split is why sorting gives no row a new number. A selection is held in natural rows and lives through a sort untouched.

Three answers is a working model. Everything else has a default:

int32 UMyModel::GetNumRows_Implementation() { return Ships.Num(); }
FText UMyModel::GetCellText_Implementation( int32 Row, FName Column )
{
    return Column == "Name" ? FText::FromString( Ships[ Row ].Name ) : FText::AsNumber( Ships[ Row ].Shield );
}
FName UMyModel::GetRowId_Implementation( int32 Row ) { return Ships[ Row ].Id; }

Sorting and filtering fall out of those three. Such a model overrides neither SortRows nor ApplyTextFilter.

Class
USmartTableModel
Extends
UObject
Members
58
Blueprint nodes
21

Public functions 36

Model 25

Get Num RowsGetNumRowsoverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) int32 GetNumRows()

How many rows this model holds, in natural space. The one count a subclass has to answer.

Not const, and neither is any reader below it. A read may fill a cache, and both built-in models do. The items model resolves the binding of a column the first time it meets an item class. The DataTable model builds the stand-in for a row the first time a cell class asks.

Neither cache can be filled up front. A Blueprint compile swaps the reflected item array to a freshly built UClass while a cache beside it still names the old one. The fill stays late and the const comes off the signature instead. BlueprintPure does not ask for const, so an override sees no difference.

Get Cell TextGetCellTextoverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) FText GetCellText( int32 NaturalRow, FName ColumnId )

What one cell draws. Empty text is a fine answer for a column this model has no value for.

Get Cell EditorGetCellEditoroverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) ESmartTableCellEditor GetCellEditor( int32 NaturalRow, FName ColumnId )

Which editor the built-in editable cell offers for this value, if any.

None by default. A table takes no edits until a model says otherwise, and a model that already exists can not start accepting them because the plugin moved on.

Both built-in models answer from reflection. A table pointed at a row struct or at a UObject has a working editor per column with no editor named anywhere. Over data reflection can not read, rows out of a csv or a json file, Text is usually the only honest answer.

Set Cell TextSetCellTextoverride
UFUNCTION( BlueprintNativeEvent, Category = "Smart Tables|Model" ) bool SetCellText( int32 NaturalRow, FName ColumnId, const FText & Value )

Writes an edited value back, and gives back false when the model will not take it.

Text for every editor, the numeric and toggle ones too, because what comes back is what was typed or clicked. A model refusing it has to refuse that text, and never a number the plugin already read out of it.

A model that takes a write has to say so. Nothing here calls NotifyRowChanged. Left out, it shows up as one cell drawing again while its neighbours in the same row do not.

Get Cell ColorGetCellColoroverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) FLinearColor GetCellColor( int32 NaturalRow, FName ColumnId )

Alpha 0 hands the colour back to the table's own cell text style, so a model with nothing to say writes nothing.

Get Row ColorGetRowColoroverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) FLinearColor GetRowColor( int32 NaturalRow )

A tint over the whole row: every other row drawn darker, a colour that says how bad something is, a row that has gone stale.

Alpha 0 leaves the row style of the table to draw with no tint over it, the same as GetCellColor.

It is here so the row widget does not have to be replaced to carry a tint. That widget takes cells from the pool and gives them back, which is what lets a table scroll a million rows.

Get Cell Sort KeyGetCellSortKeyoverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) FSmartTableSortKey GetCellSortKey( int32 NaturalRow, FName ColumnId )

Falls back to the drawn text, so a model that writes only GetCellText still sorts. A numeric column overrides it. Text goes through CompareNatural, which reads a run of digits as one number. That still puts 1.25 after 1.5, because the run 25 is more than the run 5.

Get Row IdGetRowIdoverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) FName GetRowId( int32 NaturalRow )

A name for a row that lives through rows arriving and leaving. The selection is put back through it.

It falls back to the row number, which is right only while the row set only ever grows. A model that adds or drops a row in the middle needs a name of its own here.

Get Row ItemGetRowItemoverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) UObject * GetRowItem( int32 NaturalRow )

The item a cell class is given. Null means this model has none, and a CellClass column over it says so once at Verbose instead of drawing nothing, or as a Warning when that cell names an ItemSetter.

Natural Row Of ItemNaturalRowOfItemoverridepure node
UFUNCTION( BlueprintNativeEvent, BlueprintPure, Category = "Smart Tables|Model" ) int32 NaturalRowOfItem( UObject * Item )

Which row holds this object, or INDEX_NONE.

The other direction of GetRowItem. The default answer is expensive in a way the call site can not see: the base walks the rows asking GetRowItem.

A model that answers only that one still gets a working lookup out of the walk. A model whose GetRowItem BUILDS something pays for a whole set of them to answer no. The DataTable model does exactly that, so it overrides this and answers from the stand-ins it already gave out.

Both built-in models override it, and any model with a lookup of its own can do the same.

Sort RowsSortRowsoverride
UFUNCTION( BlueprintNativeEvent, Category = "Smart Tables|Model" ) bool SortRows( const FSmartTableSortSpec & Spec )

Put the rows in this order. Gives back false when this model does not sort that column.

The default pulls a key per row through GetCellSortKey and reorders. Every model sorts with no sort code of its own, a Blueprint subclass that wrote nothing but GetCellText included.

An override orders somewhere else: a sort on a server sends its query from here, gives back true, and notifies once the answer arrives.

False puts the header arrow back and logs. A header that flips while no row moves reads as a table that took the click and did nothing with it.

Apply Text FilterApplyTextFilteroverride
UFUNCTION( BlueprintNativeEvent, Category = "Smart Tables|Model" ) bool ApplyTextFilter( const FText & FilterText, const TArray< FName > & AllColumnIds )

Hide the rows that do not match. Empty text clears the filter.

It searches every column, the hidden ones too. Over the visible set only, hiding a column would change which rows the same words find, with no log line anywhere.

Move RowsMoveRowsoverride
UFUNCTION( BlueprintNativeEvent, Category = "Smart Tables|Model" ) bool MoveRows( const TArray< int32 > & NaturalRows, int32 InsertBeforeNaturalRow )

Put these rows in front of that one. False when this model does not reorder, which is the default.

InsertBeforeNaturalRow is a GAP, not a row: 0 is in front of everything, GetNumRows is past the end. Rows are numbered as they were before the move. PlanRowMove works that out.

An override moves data and nothing else. The table clears the sort and notifies around this call.

Notify Row ChangedNotifyRowChangednode
UFUNCTION( BlueprintCallable, Category = "Smart Tables|Model" ) void NotifyRowChanged( int32 NaturalRow )

The values of one row moved. That row reads again and nothing else does, unless a sort is already in flight. Then the whole presentation is rebuilt when it lands.

Notify Cell ChangedNotifyCellChangednode
UFUNCTION( BlueprintCallable, Category = "Smart Tables|Model" ) void NotifyCellChanged( int32 NaturalRow, FName ColumnId )

ONE value of one row moved, and only that cell reads again.

It holds only while no column of this model reads another. A bound pure function in another column can read the property just written, a total off a price. This tells the table that column has nothing to read again, so it goes on drawing the old total.

The built-in models say NotifyRowChanged for that reason. They resolve bindings by name and can not tell which column reads which. A model that can rule it out says this instead, and gets a value-changed animation on the one cell that moved.

Notify Rows ChangedNotifyRowsChangednode
UFUNCTION( BlueprintCallable, Category = "Smart Tables|Model" ) void NotifyRowsChanged()

The values of many rows moved, and there are still the same number of rows.

Notify Rows AddedNotifyRowsAddednode
UFUNCTION( BlueprintCallable, Category = "Smart Tables|Model" ) void NotifyRowsAdded( const TArray< int32 > & NaturalRows )

These rows joined the set. Each plays the entrance of the table, and the table scrolls for them as its own settings say.

The rows count as they stand now, after they went in.

Notify Rows RemovedNotifyRowsRemovednode
UFUNCTION( BlueprintCallable, Category = "Smart Tables|Model" ) void NotifyRowsRemoved( const TArray< FName > & RowIds )

The rows with these ids left the set. A view scrolled into the rows keeps the rows it shows.

Said after they went, so each id is read with GetRowId before its row goes.

Notify Num Rows ChangedNotifyNumRowsChangednode
UFUNCTION( BlueprintCallable, Category = "Smart Tables|Model" ) void NotifyNumRowsChanged()

Rows arrived or left, and this model does not say which. The selection is matched back up by GetRowId.

No row plays an entrance, and the view is neither followed nor held. NotifyRowsAdded and NotifyRowsRemoved say which rows.

Get Active Sort SpecGetActiveSortSpecpure node
UFUNCTION( BlueprintPure, Category = "Smart Tables|Model" ) FSmartTableSortSpec GetActiveSortSpec() const

The sort in force right now. Empty is unsorted.

Get Active Filter TextGetActiveFilterTextpure node
UFUNCTION( BlueprintPure, Category = "Smart Tables|Model" ) FText GetActiveFilterText() const

The filter text in force. Empty means every row is shown.

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

True while a sort this model dispatched has not come back yet.

Get Num Presented RowsGetNumPresentedRowspure node
UFUNCTION( BlueprintPure, Category = "Smart Tables|Model" ) int32 GetNumPresentedRows() const

How many rows really draw: natural space less whatever the filter hides.

Not virtual, on purpose, and neither are the two below it. Presented space is the answer of this base class. A subclass able to override it could differ from the rows it gave over a moment earlier.

All three are BlueprintPure, because the two row spaces are the main idea here. With no pure node nothing outside C++ could ask how many rows draw, what draws at a place, or where a row ended up.

Presented To Natural RowPresentedToNaturalRowpure node
UFUNCTION( BlueprintPure, Category = "Smart Tables|Model" ) int32 PresentedToNaturalRow( int32 PresentedRow ) const

The row number this model uses, for the row drawn at this place.

Natural To Presented RowNaturalToPresentedRowpure node
UFUNCTION( BlueprintPure, Category = "Smart Tables|Model" ) int32 NaturalToPresentedRow( int32 NaturalRow ) const

Where a natural row draws right now, or INDEX_NONE while the filter hides it.

C++ only 11

KeepRow
FSmartTableKeptRow KeepRow( int32 NaturalRow )

This row by its number and its id, for something that acts on it in a later frame.

FindKeptRow
int32 FindKeptRow( const FSmartTableKeptRow & Row )

Where a kept row is now, or INDEX_NONE once it left. See FSmartTableKeptRow::FindNow

PlanRowMovestatic
static TArray< int32 > PlanRowMove( int32 NumRows, const TArray< int32 > & NaturalRows, int32 InsertBeforeNaturalRow )

The order rows end up in, as a permutation: entry i held that number before the move.

Not inside one model, because every model needs the same maths. An array, a cursor, a file, all the same.

Two things here are easy to get wrong. Moved rows keep their order against each other. And the gap counts rows that are about to leave, so row 1 dropped after row 5 lands after 5, not 6.

Empty means refused: out of range, a duplicate, or every row moving.

NotifyValuesChanged
void NotifyValuesChanged( const FSmartTablePresentationChange & Change )

The one route out for a values-only change, so the rule about a rebuild in flight is written once and both public functions above cannot drift apart about it.

NotifyRowSetChanged
void NotifyRowSetChanged( const FSmartTableRowSetDiff & Diff )

The one route for a change to the row set. The three above build a diff and come down here.

ActiveSortSpecRef
const FSmartTableSortSpec & ActiveSortSpecRef() const

The same, without the copy. For C++ only.

A spec holds a TArray, so the reflected reader above builds a new one on every call, and a header asks for the sort several times per column per draw. UHT refuses a reference return, so the two live side by side: a graph takes the copy, and anything drawing takes this.

GetPresentedRows
TConstArrayView< int32 > GetPresentedRows() const

The drawn order itself. A view and not a copy, valid until the next sort or filter lands.

OnPresentationChanged
FOnSmartTablePresentationChanged & OnPresentationChanged()

Fires when a rebuild lands and was not superseded, carrying what it says changed. Landing is the test, and not whether the order came out any different.

OnBusyChanged
FOnSmartTableModelBusy & OnBusyChanged()

Fires when this model starts and stops holding a sort it dispatched.

SetWorkDispatcher
void SetWorkDispatcher( FSmartTableWorkDispatcher InDispatcher )

Where the ordering work runs. Unset runs it here and now, and that is the default on purpose.

Only the sort moves. The keys are pulled here, on the game thread, as plain data, so what leaves is an array of numbers and text. A model is never called from a worker thread on this path.

RebuildPresentation
void RebuildPresentation( const FSmartTablePresentationChange & WhenDone = FSmartTablePresentationChange::Order() )

Puts the sort and the filter in force back over the rows. This base calls it whenever the data moves, and a table calls it once more when a new model arrives.

WhenDone goes out once the answer is in, and never now. A dispatched sort comes back from here having changed nothing, so a caller broadcasting its own change at once would be naming a presentation that does not exist yet at all.

Protected functions 2

ApplySortSpecToPresentation
void ApplySortSpecToPresentation( const FSmartTableSortSpec & Spec )

One each and not one shared: the default SortRows works through ApplySortSpecToPresentation, and the default ApplyTextFilter through ApplyFilterToPresentation. An override of either still gets the ordering rules by calling back into its own.

ApplyFilterToPresentation
void ApplyFilterToPresentation( const FText & FilterText, const TArray< FName > & AllColumnIds )

Private functions 5

BroadcastPresentationChanged
void BroadcastPresentationChanged( const FSmartTablePresentationChange & Change )
CollectFilteredRows
TArray< int32 > CollectFilteredRows()

The natural rows that live through the filter in force, in natural order. It reads the model, so it only ever runs on the game thread.

ExtractSortKeys
void ExtractSortKeys( TArray< TArray< FSmartTableSortKey > > & OutLevels, TArray< ESmartTableSortMode > & OutModes )

One array of keys per sort level, indexed by natural row. It reads the model, so it only ever runs on the game thread.

ApplySortedRows
void ApplySortedRows( int32 Token, TArray< int32 > && Rows, bool bWasDispatched )

Drops a result whose question has since moved, and uses the one still being asked.

bWasDispatched is given and never worked out. SortsInFlight > 0 asks a different question.

A sort running here and now, whether from an empty spec or a model with no dispatcher, would take the hold a dispatched one still has on the counter and drop busy early. Only the caller knows which of the three routes it came in on.

SetBusy
void SetBusy( bool bInBusy )

Protected variables 5

PresentedToNatural
TArray< int32 > PresentedToNatural

The natural row for every presented row. It is the identity map while nothing is sorted and nothing is filtered, and no row ever gets a new number as a sort comes and goes.

NaturalToPresented
TArray< int32 > NaturalToPresented

The other way round: natural row to where it draws, INDEX_NONE for a row the filter took away.

Written only in ApplySortedRows, beside the array above, so the two can never disagree.

It exists because the caret, a click, a menu and a scroll all ask it, and answering by walking the order costs the row count every time.

Active Sort SpecActiveSortSpec
UPROPERTY( Transient ) FSmartTableSortSpec ActiveSortSpec
Transient
Never saved.

The sort in force. Transient, because an order is a session thing and the layout store keeps it.

Active Filter TextActiveFilterText
UPROPERTY( Transient ) FText ActiveFilterText
Transient
Never saved.

The filter text in force, kept so a rebuild can put it back.

FilterColumnIds
TArray< FName > FilterColumnIds

Kept so RebuildPresentation can filter again without the table naming the columns a second time.

Private variables 8

PresentationChanged
FOnSmartTablePresentationChanged PresentationChanged
BusyChanged
FOnSmartTableModelBusy BusyChanged
WorkDispatcher
FSmartTableWorkDispatcher WorkDispatcher
SortTokenpure virtual
int32 SortToken = 0

One question still out, and not a count of how many there have been.

A result arriving after the spec moved on answers a question no longer being asked, and using it would put the table in an order the header does not show.

PendingChange

What the rebuild still out will say when it arrives.

Folded in and never replaced. A row set that moved while a sort was still out is still a row set that moved, so the table has to map identity again when the rows arrive. Losing that to a later, narrower change would drop a selection with no log line.

Add ORs the parts, so a later change can only ever add to what is waiting. A stale-cells tick and a row set change can arrive apart and still go out together.

bBusy
bool bBusy = false
SortsInFlightpure virtual
int32 SortsInFlight = 0

How many dispatched sorts have not come back yet.

bBusy is worked out from this and never set beside it, so a dropped or superseded result can not leave the overlay up. Every dispatch takes a hold and every result gives one back, whether or not its rows were still needed.

LastDispatchedToken
int32 LastDispatchedToken = INDEX_NONE

The token of the newest dispatched sort, or INDEX_NONE when the newest sort ran here and now.

It exists so NotifyRowChanged can ask the one question that matters to it: is a broadcast coming? SortsInFlight > 0 is not that question. A superseded sort comes back early and broadcasts nothing, so a row tick folded into PendingChange while only superseded sorts were out would sit there with nothing left to carry it.

When this equals SortToken the newest sort is dispatched and still out, so a broadcast is coming and a row tick folded into PendingChange goes out with it.

Delegate types 2

FOnSmartTablePresentationChanged
DECLARE_MULTICAST_DELEGATE_OneParam( FOnSmartTablePresentationChanged, const FSmartTablePresentationChange & )
FOnSmartTableModelBusy
DECLARE_MULTICAST_DELEGATE_OneParam( FOnSmartTableModelBusy, bool )

Next: SmartTableObjectModel.h