SmartTableModel.h
Where the rows come from, and the one class to subclass when they come from you.
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
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.
What one cell draws. Empty text is a fine answer for a column this model has no value for.
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.
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.
Alpha 0 hands the colour back to the table's own cell text style, so a model with nothing to say writes nothing.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
The values of many rows moved, and there are still the same number of rows.
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.
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.
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.
The sort in force right now. Empty is unsorted.
The filter text in force. Empty means every row is shown.
True while a sort this model dispatched has not come back yet.
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.
The row number this model uses, for the row drawn at this place.
Where a natural row draws right now, or INDEX_NONE while the filter hides it.
C++ only 11
This row by its number and its id, for something that acts on it in a later frame.
Where a kept row is now, or INDEX_NONE once it left. See FSmartTableKeptRow::FindNow
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.
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.
The one route for a change to the row set. The three above build a diff and come down here.
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.
The drawn order itself. A view and not a copy, valid until the next sort or filter lands.
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.
Fires when this model starts and stops holding a sort it dispatched.
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.
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
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.
Private functions 5
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.
One array of keys per sort level, indexed by natural row. It reads the model, so it only ever runs on the game thread.
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.
Protected variables 5
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.
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.
- Transient
- Never saved.
The sort in force. Transient, because an order is a session thing and the layout store keeps it.
- Transient
- Never saved.
The filter text in force, kept so a rebuild can put it back.
Kept so RebuildPresentation can filter again without the table naming the columns a second time.
Private variables 8
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.
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.
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.
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
Next: SmartTableObjectModel.h