Monday, May 14, 2018

Adding a new data element to a table and selecting it

I had no idea something so simple would take me so long to figure out. I want to have a menu item that adds a new data element to a table and selects it. See, sound easy, right?
Well, I also want to be able to undo it, which adds some complexity, but that's not the part that took me longest to figure out.

For one thing, there are a lot of objects involved in this. First you need a data model. I subclassed mine from the QAbstractTableModel since I'm going to display my data in a TableView. I really don't see much difference in the QAbstractTableModel and the QAbstractItemModel. I think they would work interchangeably for my case.

The TableView I created in Qt Designer, but since I want the table to be sortable and searchable, my data model needs a QSortFilterProxyModel. Right now I'm using the default one, but I think I'll need to create my own subclass of it when I get to the filtering part. So the constructor for my TableView looks like this:

ActionList::ActionList(QWidget *parent) :
ui(new Ui::ActionList)
proxy = new QSortFilterProxyModel(parent);

Not too bad, so far. Now I need a slot for my menu item. All it does is create an undo command and push it on the stack. The undo commands are really tedious because of all the object boiler plate required. I simplified mine somewhat by making the declaration and definition one in the same. This means I have to include the .cpp file into another .cpp file to compile it, which is ugly, but saves a lot of duplication. I'll see if it works out as the program gets more complicated.

undoStack->push (new AddActionCommand (actionModel, Action::Audio));

The QAbstractTableModel has a bunch of functions to add rows, columns and insert data, but I think these are all for edit-in-place tables. In my case the table is not editable, so I created a new function to add data in the form of my custom Action object.
QModelIndex ActionModel::addAction (Action *act)
int newRow = actions.count();
beginInsertRows(QModelIndex(), newRow, newRow);
return (index(newRow,0));

It stores the data inside the model and uses signals to alert the view to update the display. Note the return of the QModelIndex. It took me a long time to figure that out, and I'm still not sure it's the best way to do it but it allows me to select that ModelIndex in the View. Where it gets tricky is that the view may have been sorted, so the index(row) in the model is not necessarily the same in the view. Figuring out that I had to have a QModelIndex and map it between the original model and the proxy took me many hours. Finally making it work wasn't too bad, but even that has multiple ways to do it. So inside the undo command I do this:
AddActionCommand(ActionModel *model, Action::Type action_type, QUndoCommand *parent = 0)
: QUndoCommand(parent)
qDebug () << "AddActionCommand called"; action = new Action (action_type); actions = model; QModelIndex qmi = actions->addAction (action);

app->ui->action_list->setCurrentIndex (qmi);
setText(QObject::tr("Add thing"));

And the real magic happens in the actionlist:

ui->actionListView->setCurrentIndex (proxy->mapFromSource (qmi));
This all may be clear as mud since I left a lot out, but the summary is:
Keep track of your QSortFilterProxyModel
find the QItemIndex for your new data in the main model
use proxy->mapFromSource to turn your main model index into a proxy model index
finally call setCurrentIndex on your TableView with the proxy model index

Simple, right?

No comments:

Post a Comment