aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Moe Gustavsen <richard.gustavsen@qt.io>2022-11-18 12:37:56 +0100
committerRichard Moe Gustavsen <richard.gustavsen@qt.io>2022-12-01 12:01:46 +0100
commited83f0f795132ef20ee6fafbad911a3da0a6c481 (patch)
tree8b6301a2001f2aa5e723a282c9915d582a80fcb8
parent97f348f6b9345bb3592bc32553101c8f7f4e1202 (diff)
QQuickTableView: implement TableView.editDelegate
This patch will implement support for editing cells in TableView. It enables you to attach an edit delegate to the tableview delegate, using the attached property TableView.editDelegate. The application can initiate editing by calling TableView.edit() (and TableView.closeEditor()) explicitly, or implicitly by using edit triggers. The EditTriggers enum in TableView mirrors the EditTriggers in QTableView (Widgets). [ChangeLog][Quick][TableView] Added support for editing cells Fixes: QTBUG-108838 Change-Id: I25df93a7eeabf9d8a4c4c6248e020d8eba6d5bd7 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
-rw-r--r--src/quick/doc/snippets/qml/tableview/editdelegate.qml52
-rw-r--r--src/quick/items/qquicktableview.cpp575
-rw-r--r--src/quick/items/qquicktableview_p.h34
-rw-r--r--src/quick/items/qquicktableview_p_p.h8
-rw-r--r--tests/auto/quick/qquicktableview/data/editdelegate.qml56
-rw-r--r--tests/auto/quick/qquicktableview/testmodel.h12
-rw-r--r--tests/auto/quick/qquicktableview/tst_qquicktableview.cpp688
-rw-r--r--tests/manual/tableview/abstracttablemodel/main.cpp6
-rw-r--r--tests/manual/tableview/abstracttablemodel/main.qml164
9 files changed, 1517 insertions, 78 deletions
diff --git a/src/quick/doc/snippets/qml/tableview/editdelegate.qml b/src/quick/doc/snippets/qml/tableview/editdelegate.qml
new file mode 100644
index 0000000000..1fdf9e7b3a
--- /dev/null
+++ b/src/quick/doc/snippets/qml/tableview/editdelegate.qml
@@ -0,0 +1,52 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+import QtQuick
+import QtQuick.Window
+import QtQuick.Controls
+import QtQml.Models
+import Qt.labs.qmlmodels
+
+Window {
+ width: 480
+ height: 640
+ visible: true
+ visibility: Window.AutomaticVisibility
+
+//![0]
+ TableView {
+ id: tableView
+ anchors.fill: parent
+ clip: true
+
+ model: TableModel {
+ TableModelColumn { display: "name" }
+ rows: [ { "name": "Harry" }, { "name": "Hedwig" } ]
+ }
+
+ selectionModel: ItemSelectionModel {}
+
+ delegate: Rectangle {
+ implicitWidth: 100
+ implicitHeight: 50
+
+ Text {
+ anchors.centerIn: parent
+ text: display
+ }
+
+ TableView.editDelegate: TextField {
+ text: display
+ horizontalAlignment: TextInput.AlignHCenter
+ verticalAlignment: TextInput.AlignVCenter
+ Component.onCompleted: selectAll()
+
+ TableView.onCommit: {
+ let index = TableView.view.modelIndex(column, row)
+ TableView.view.model.setData(index, text, Qt.DisplayRole)
+ }
+ }
+ }
+ }
+//![0]
+}
diff --git a/src/quick/items/qquicktableview.cpp b/src/quick/items/qquicktableview.cpp
index 1a1fafa2a2..38807ba0b3 100644
--- a/src/quick/items/qquicktableview.cpp
+++ b/src/quick/items/qquicktableview.cpp
@@ -151,6 +151,36 @@
\snippet qml/tableview/tableviewwithprovider.qml 0
+ \section1 Editing cells
+
+ You can let the user edit table cells by providing an edit delegate. The
+ edit delegate will be instantiated according to the \l editTriggers, which
+ by default is when the user double taps on a cell, or presses e.g
+ \l Qt.Key_Enter or \l Qt.Key_Return. The edit delegate is set using
+ \l {TableView::editDelegate}, which is an attached property that you set
+ on the \l delegate. The following snippet shows how to do that:
+
+ \snippet qml/tableview/editdelegate.qml 0
+
+ If the user presses Qt.Key_Enter or Qt.Key_Return while the edit delegate
+ is active, TableView will emit the \l TableView::commit signal to the edit
+ delegate, so that it can write back the changed data to the model.
+
+ \note In order for a cell to be editable, the model needs to override
+ \l QAbstractItemModel::flags(), and return \c Qt::ItemIsEditable.
+ This flag is not enabled in QAbstractItemModel by default.
+ The override could for example look like this:
+
+ \code
+ Qt::ItemFlags QAbstractItemModelSubClass::flags(const QModelIndex &index) const override
+ {
+ Q_UNUSED(index)
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
+ }
+ \endcode
+
+ \sa TableView::editDelegate, TableView::commit, editTriggers, edit(), closeEditor()
+
\section1 Overlays and underlays
All new items that are instantiated from the delegate are parented to the
@@ -633,6 +663,58 @@
*/
/*!
+ \qmlproperty enumeration QtQuick::TableView::editTriggers
+ \since 6.5
+
+ This property holds the different ways the user can start to edit a cell.
+ It can be a combination of the following values:
+
+ \default TableView.DoubleTapped | TableView.EditKeyPressed.
+ \value TableView.NoEditTriggers - the user cannot trigger editing of cells.
+ When this value is set, TableView will neither \e {open or close}
+ the edit delegate as a response to any user interaction.
+ But the application can call \l edit() and \l closeEditor() manually.
+ \value TableView.SingleTapped - the user can edit a cell by single tapping it.
+ \value TableView.DoubleTapped - the user can edit a cell by double tapping it.
+ \value TableView.SelectedTapped - the user can edit the
+ \l {QItemSelectionModel::currentIndex()}{current cell} by tapping it.
+ \value TableView.EditKeyPressed - the user can edit the
+ \l {QItemSelectionModel::currentIndex()}{current cell} by pressing one
+ of the edit keys. The edit keys are decided by the OS, but are normally
+ \c Qt.Key_Enter and \c Qt.Key_Return.
+ \value TableView.AnyKeyPressed - the user can edit the
+ \l {TableView::current}{current cell} by pressing any key, other
+ than the cell navigation keys. The pressed key is also sent to the
+ focus object inside the \l {TableView::editDelegate}{edit delegate}.
+
+ For \c TableView.SelectedTapped, \c TableView.EditKeyPressed, and
+ \c TableView.AnyKeyPressed to have any effect, TableView needs to have a
+ \l {selectionModel}{selection model} assigned, since they depend on a
+ \l {QItemSelectionModel::currentIndex()}{current index} being set. To be
+ able to receive any key events at all, TableView will also need to have
+ \l QQuickItem::activeFocus.
+
+ When editing a cell, the user can press \c Qt.Key_Tab or \c Qt.Key_Backtab
+ to \l {TableView::commit}{commit} the data, and move editing to the next
+ cell. This behavior can be disabled by setting
+ \l QQuickItem::activeFocusOnTab on TableView to \c false.
+
+ \note In order for a cell to be editable, the \l delegate needs an
+ \l {TableView::editDelegate}{edit delegate} attached, and the model
+ needs to return \c Qt::ItemIsEditable from \l QAbstractItemModel::flags().
+
+ \code
+ Qt::ItemFlags QAbstractItemModelSubClass::flags(const QModelIndex &index) const override
+ {
+ Q_UNUSED(index)
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
+ }
+ \endcode
+
+ \sa TableView::editDelegate, TableView::commit, {Editing cells}
+*/
+
+/*!
\qmlmethod QtQuick::TableView::positionViewAtCell(point cell, PositionMode mode, point offset, rect subRect)
Positions \l {Flickable::}{contentX} and \l {Flickable::}{contentY} such
@@ -1088,6 +1170,34 @@
*/
/*!
+ \qmlmethod QtQuick::TableView::edit(QModelIndex modelIndex)
+ \since 6.5
+
+ This function starts an editing session for the cell that represents
+ \a modelIndex. If the user is already editing another cell, that session ends.
+
+ Normally you can specify the different ways of starting an edit session by
+ using \l editTriggers instead. If that isn't sufficient, you can use this
+ function. To take full control over cell editing and keep TableView from
+ interfering, set editTriggers to \c TableView.NoEditTriggers.
+
+ \note The \l {ItemSelectionModel::currentIndex}{current index} in the
+ \l {selectionModel}{selection model} will also change to \a modelIndex.
+
+ \sa closeEditor(), editTriggers, TableView::editDelegate, {Editing cells}
+*/
+
+/*!
+ \qmlmethod QtQuick::TableView::closeEditor()
+ \since 6.5
+
+ If the user is editing a cell, calling this function will
+ stop the editing, and destroy the edit delegate instance.
+
+ \sa edit(), TableView::editDelegate, {Editing cells}
+*/
+
+/*!
\qmlattachedproperty TableView QtQuick::TableView::view
This attached property holds the view that manages the delegate instance.
@@ -1125,6 +1235,66 @@
\sa {Reusing items}, reuseItems, pooled
*/
+/*!
+ \qmlattachedsignal QtQuick::TableView::commit
+ This signal is emitted by the \l {TableView::editDelegate}{edit delegate}
+
+ This attached signal is emitted when the \l {TableView::editDelegate}{edit delegate}
+ is active, and the user presses \l Qt.Key_Enter or \l Qt.Key_Return. It will also
+ be emitted if TableView has \l QQuickItem::activeFocusOnTab set, and the user
+ presses Qt.Key_Tab or Qt.Key_Backtab.
+
+ This signal will \e not be emitted if editing ends because of reasons other
+ than the ones mentioned. This includes e.g if the user presses
+ Qt.Key_Escape, taps outside the delegate, the row or column being
+ edited is deleted, or if the application calls \l closeEditor().
+
+ Upon receiving the signal, the edit delegate should write any modified data
+ back to the model.
+
+ \note This property should be attached to the
+ \l {TableView::editDelegate}{edit delegate}, and not to the \l delegate.
+
+ \sa TableView::editDelegate, editTriggers, {Editing cells}
+*/
+
+/*!
+ \qmlattachedproperty Component QtQuick::TableView::editDelegate
+
+ This attached property holds the edit delegate. It's instantiated
+ when editing begins, and will be resized and placed at the same location
+ as the cell it edits. It supports the same required properties as the
+ \l {\l delegate}{TableView delegate}, including \c index, \c row and \c column.
+ Properties of the model, like \c display and \c edit, are also available
+ (depending on the \l {QAbstractItemModel::roleNames()}{role names} exposed
+ by the model).
+
+ Editing starts when the actions specified by \l editTriggers are met, and
+ the current cell is editable.
+
+ \note In order for a cell to be editable, the model needs to override
+ \l QAbstractItemModel::flags(), and return \c Qt::ItemIsEditable.
+
+ You can also open and close the edit delegate manually by calling \l edit()
+ and \l closeEditor(), respectively. The \c Qt::ItemIsEditable flag will
+ then be ignored.
+
+ Editing ends when the user presses \c Qt.Key_Enter or \c Qt.Key_Return
+ (and also \c Qt.Key_Tab or \c Qt.Key_Backtab, if TableView has
+ \l QQuickItem::activeFocusOnTab set). In that case, the \l TableView::commit
+ signal will be emitted, so that the edit delegate can respond by writing any
+ modified data back to the model. If editing ends because of other reasons
+ (e.g if the user presses Qt.Key_Escape), the signal will not be emitted.
+ In any case will \l {Component::destruction}{destruction()} be emitted in the end.
+
+ When the edit delegate is instantiated, it will call \l QQuickItem::forceActiveFocus()
+ on the first item inside the delegate that has \l QQuickItem::activeFocusOnTab set to
+ \c true. You can override this by calling \l QQuickItem::forceActiveFocus() explicitly
+ on some other item inside the delegate from \l {Component::completed()}{Component.completed()}.
+
+ \sa editTriggers, TableView::commit, edit(), closeEditor(), {Editing cells}
+*/
+
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcTableViewDelegateLifecycle, "qt.quick.tableview.lifecycle")
@@ -1182,6 +1352,11 @@ QQuickTableViewPrivate::~QQuickTableViewPrivate()
delete fxTableItem;
}
+ if (editItem)
+ editModel->dispose(editItem);
+ if (editModel)
+ delete editModel;
+
if (tableModel)
delete tableModel;
}
@@ -1221,7 +1396,10 @@ void QQuickTableViewPrivate::dumpTable() const
void QQuickTableViewPrivate::setRequiredProperty(const char *property,
const QVariant &value, int serializedModelIndex, QObject *object, bool init)
{
- if (!qobject_cast<QQmlTableInstanceModel *>(model)) {
+ Q_Q(QQuickTableView);
+
+ QQmlTableInstanceModel *tableInstanceModel = qobject_cast<QQmlTableInstanceModel *>(model);
+ if (!tableInstanceModel) {
// TableView only supports using required properties when backed by
// a QQmlTableInstanceModel. This is almost always the case, except
// if you assign it an ObjectModel or a DelegateModel (which are really
@@ -1234,21 +1412,47 @@ void QQuickTableViewPrivate::setRequiredProperty(const char *property,
const QString propertyName = QString::fromUtf8(property);
if (init) {
- const bool wasRequired = model->setRequiredProperty(serializedModelIndex, propertyName, value);
+ bool wasRequired = false;
+ if (object == editItem) {
+ // Special case: the item that we should write to belongs to the edit
+ // model rather than 'model' (which is used for normal delegate items).
+ wasRequired = editModel->setRequiredProperty(serializedModelIndex, propertyName, value);
+ } else {
+ wasRequired = tableInstanceModel->setRequiredProperty(serializedModelIndex, propertyName, value);
+ }
if (wasRequired) {
QStringList propertyList = object->property(kRequiredProperties).toStringList();
object->setProperty(kRequiredProperties, propertyList << propertyName);
}
} else {
- const QStringList propertyList = object->property(kRequiredProperties).toStringList();
- if (!propertyList.contains(propertyName)) {
- // We only write to properties that are required
- return;
+ {
+ const QStringList propertyList = object->property(kRequiredProperties).toStringList();
+ if (propertyList.contains(propertyName)) {
+ const auto metaObject = object->metaObject();
+ const int propertyIndex = metaObject->indexOfProperty(property);
+ const auto metaProperty = metaObject->property(propertyIndex);
+ metaProperty.write(object, value);
+ }
}
- const auto metaObject = object->metaObject();
- const int propertyIndex = metaObject->indexOfProperty(property);
- const auto metaProperty = metaObject->property(propertyIndex);
- metaProperty.write(object, value);
+
+ if (editItem) {
+ // Whenever we're told to update a required property for a table item that has the
+ // same model index as the edit item, we also mirror that update to the edit item.
+ // As such, this function is never called for the edit item directly (except the
+ // first time when it needs to be initialized).
+ Q_TABLEVIEW_ASSERT(object != editItem, "");
+ const QModelIndex modelIndex = q->modelIndex(cellAtModelIndex(serializedModelIndex));
+ if (modelIndex == editIndex) {
+ const QStringList propertyList = editItem->property(kRequiredProperties).toStringList();
+ if (propertyList.contains(propertyName)) {
+ const auto metaObject = editItem->metaObject();
+ const int propertyIndex = metaObject->indexOfProperty(property);
+ const auto metaProperty = metaObject->property(propertyIndex);
+ metaProperty.write(editItem, value);
+ }
+ }
+ }
+
}
}
@@ -2891,6 +3095,9 @@ void QQuickTableViewPrivate::processLoadRequest()
emit q->bottomRowChanged();
break;
}
+
+ if (editIndex.isValid())
+ updateEditItem();
}
loadRequest.markAsDone();
@@ -2993,6 +3200,8 @@ void QQuickTableViewPrivate::processRebuildTable()
if (edgesBeforeRebuild.bottom() != q->bottomRow())
emit q->bottomRowChanged();
+ if (editIndex.isValid())
+ updateEditItem();
updateCurrentRowAndColumn();
qCDebug(lcTableViewDelegateLifecycle()) << "current table:" << tableLayoutToString();
@@ -3729,7 +3938,9 @@ void QQuickTableViewPrivate::initItemCallback(int modelIndex, QObject *object)
{
Q_Q(QQuickTableView);
- auto item = static_cast<QQuickItem*>(object);
+ auto item = qobject_cast<QQuickItem*>(object);
+ if (!item)
+ return;
item->setParentItem(q->contentItem());
item->setZ(1);
@@ -3760,6 +3971,9 @@ void QQuickTableViewPrivate::itemReusedCallback(int modelIndex, QObject *object)
setRequiredProperty(kRequiredProperty_current, QVariant::fromValue(current), modelIndex, object, false);
setRequiredProperty(kRequiredProperty_selected, QVariant::fromValue(selected), modelIndex, object, false);
+ if (auto item = qobject_cast<QQuickItem*>(object))
+ QQuickItemPrivate::get(item)->setCulled(false);
+
if (auto attached = getAttachedObject(object))
emit attached->reused();
}
@@ -4053,9 +4267,15 @@ void QQuickTableViewPrivate::rowsInsertedCallback(const QModelIndex &parent, int
void QQuickTableViewPrivate::rowsRemovedCallback(const QModelIndex &parent, int, int)
{
+ Q_Q(QQuickTableView);
+
if (parent != QModelIndex())
return;
+ // If editIndex was a part of the removed rows, it will now be invalid.
+ if (!editIndex.isValid() && editItem)
+ q->closeEditor();
+
scheduleRebuildTable(RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentHeight);
}
@@ -4074,9 +4294,15 @@ void QQuickTableViewPrivate::columnsInsertedCallback(const QModelIndex &parent,
void QQuickTableViewPrivate::columnsRemovedCallback(const QModelIndex &parent, int, int)
{
+ Q_Q(QQuickTableView);
+
if (parent != QModelIndex())
return;
+ // If editIndex was a part of the removed columns, it will now be invalid.
+ if (!editIndex.isValid() && editItem)
+ q->closeEditor();
+
scheduleRebuildTable(RebuildOption::ViewportOnly | RebuildOption::CalculateNewContentWidth);
}
@@ -4098,6 +4324,8 @@ void QQuickTableViewPrivate::fetchMoreData()
void QQuickTableViewPrivate::modelResetCallback()
{
+ Q_Q(QQuickTableView);
+ q->closeEditor();
scheduleRebuildTable(RebuildOption::All);
}
@@ -4345,13 +4573,22 @@ void QQuickTableViewPrivate::init()
handleTap(tapHandler->point().pressPosition());
});
- QObject::connect(tapHandler, &QQuickTapHandler::doubleTapped, [this, q] {
+ QObject::connect(tapHandler, &QQuickTapHandler::doubleTapped, [this, q, tapHandler] {
const bool resizeRow = resizableRows && hoverHandler->m_row != -1;
const bool resizeColumn = resizableColumns && hoverHandler->m_column != -1;
- if (resizeRow)
- q->setRowHeight(hoverHandler->m_row, -1);
- if (resizeColumn)
- q->setColumnWidth(hoverHandler->m_column, -1);
+
+ if (resizeRow || resizeColumn) {
+ if (resizeRow)
+ q->setRowHeight(hoverHandler->m_row, -1);
+ if (resizeColumn)
+ q->setColumnWidth(hoverHandler->m_column, -1);
+ } else if (editTriggers & QQuickTableView::DoubleTapped) {
+ const QPointF pos = tapHandler->point().pressPosition();
+ const QPoint cell = q->cellAtPosition(pos);
+ const QModelIndex index = q->modelIndex(cell);
+ if (canEdit(index, false))
+ q->edit(index);
+ }
});
}
@@ -4369,10 +4606,63 @@ void QQuickTableViewPrivate::handleTap(const QPointF &pos)
if (resizeHandler->state() != QQuickTableViewResizeHandler::Listening)
return;
- if (pointerNavigationEnabled) {
- clearSelection();
- setCurrentIndexFromTap(pos);
+ QModelIndex prevIndex;
+ if (selectionModel) {
+ prevIndex = selectionModel->currentIndex();
+ if (pointerNavigationEnabled) {
+ clearSelection();
+ setCurrentIndexFromTap(pos);
+ }
}
+
+ if (editTriggers != QQuickTableView::NoEditTriggers)
+ q->closeEditor();
+
+ const QModelIndex tappedIndex = q->modelIndex(q->cellAtPosition(pos));
+ if (canEdit(tappedIndex, false)) {
+ if (editTriggers & QQuickTableView::SingleTapped)
+ q->edit(tappedIndex);
+ else if ((editTriggers & QQuickTableView::SelectedTapped) && tappedIndex == prevIndex)
+ q->edit(tappedIndex);
+ }
+}
+
+bool QQuickTableViewPrivate::canEdit(const QModelIndex tappedIndex, bool warn)
+{
+ // Check that a call to edit(tappedIndex) would not
+ // result in warnings being printed.
+ Q_Q(QQuickTableView);
+
+ if (!tappedIndex.isValid()) {
+ if (warn)
+ qmlWarning(q) << "cannot edit: index is not valid!";
+ return false;
+ }
+
+ if (auto const qaim = model->abstractItemModel()) {
+ if (!(qaim->flags(tappedIndex) & Qt::ItemIsEditable)) {
+ if (warn)
+ qmlWarning(q) << "cannot edit: QAbstractItemModel::flags(index) doesn't contain Qt::ItemIsEditable";
+ return false;
+ }
+ }
+
+ const QPoint cell = q->cellAtIndex(tappedIndex);
+ const QQuickItem *cellItem = q->itemAtCell(cell);
+ if (!cellItem) {
+ if (warn)
+ qmlWarning(q) << "cannot edit: the cell to edit is not inside the viewport!";
+ return false;
+ }
+
+ auto attached = getAttachedObject(cellItem);
+ if (!attached || !attached->editDelegate()) {
+ if (warn)
+ qmlWarning(q) << "cannot edit: no TableView.editDelegate set!";
+ return false;
+ }
+
+ return true;
}
void QQuickTableViewPrivate::syncViewportPosRecursive()
@@ -4427,6 +4717,9 @@ bool QQuickTableViewPrivate::setCurrentIndexFromKeyEvent(QKeyEvent *e)
{
Q_Q(QQuickTableView);
+ if (!selectionModel || !selectionModel->model())
+ return false;
+
const QModelIndex currentIndex = selectionModel->currentIndex();
const QPoint currentCell = q->cellAtIndex(currentIndex);
const bool select = (e->modifiers() & Qt::ShiftModifier) && (e->key() != Qt::Key_Backtab);
@@ -4619,6 +4912,60 @@ bool QQuickTableViewPrivate::setCurrentIndexFromKeyEvent(QKeyEvent *e)
return true;
}
+bool QQuickTableViewPrivate::editFromKeyEvent(QKeyEvent *e)
+{
+ Q_Q(QQuickTableView);
+
+ if (editTriggers == QQuickTableView::NoEditTriggers)
+ return false;
+ if (!selectionModel || !selectionModel->model())
+ return false;
+
+ const QModelIndex index = selectionModel->currentIndex();
+ const QPoint cell = q->cellAtIndex(index);
+ const QQuickItem *cellItem = q->itemAtCell(cell);
+ if (!cellItem)
+ return false;
+
+ auto attached = getAttachedObject(cellItem);
+ if (!attached || !attached->editDelegate())
+ return false;
+
+ const bool anyKeyAccepted = editTriggers & QQuickTableView::AnyKeyPressed;
+ bool editKeyPressed = false;
+ if (editTriggers & QQuickTableView::EditKeyPressed) {
+ switch (e->key()) {
+ case Qt::Key_Return:
+ case Qt::Key_Enter:
+#ifndef Q_OS_MACOS
+ case Qt::Key_F2:
+#endif
+ editKeyPressed = true;
+ break;
+ }
+ }
+
+ if (!(editKeyPressed || anyKeyAccepted))
+ return false;
+
+ if (!canEdit(index, false)) {
+ // If canEdit() returns false at this point (e.g because currentIndex is not
+ // editable), we still want to eat the key event, to keep a consistent behavior
+ // when some cells are editable, but others not.
+ return true;
+ }
+
+ q->edit(index);
+
+ if (editIndex.isValid() && anyKeyAccepted && !editKeyPressed) {
+ // Replay the key event to the focus object (which should at this point
+ // be the edit item, or an item inside the edit item).
+ QGuiApplication::sendEvent(QGuiApplication::focusObject(), e);
+ }
+
+ return true;
+}
+
void QQuickTableViewPrivate::updateCursor()
{
int row = resizableRows ? hoverHandler->m_row : -1;
@@ -4654,6 +5001,31 @@ void QQuickTableViewPrivate::updateCursor()
}
}
+void QQuickTableViewPrivate::updateEditItem()
+{
+ Q_Q(QQuickTableView);
+ Q_ASSERT(editIndex.isValid());
+ Q_ASSERT(editItem);
+
+ const QPoint cell = q->cellAtIndex(editIndex);
+ auto cellItem = q->itemAtCell(cell);
+
+ if (cellItem) {
+ editItem->setX(cellItem->x());
+ editItem->setY(cellItem->y());
+ editItem->setWidth(cellItem->width());
+ editItem->setHeight(cellItem->height());
+ // Temporarily hide the tableview delegate so that it
+ // doesn't shine through if the edit delegate is semi-transparent.
+ QQuickItemPrivate::get(cellItem)->setCulled(true);
+ } else {
+ // 'Hide' the edit item, without losing active focus. Simply
+ // culling it will not work, since some platforms (macOS) will
+ // overlay the focus item with a focus rect.
+ editItem->setX(q->contentWidth() + 10000);
+ }
+}
+
QQuickTableView::QQuickTableView(QQuickItem *parent)
: QQuickFlickable(*(new QQuickTableViewPrivate), parent)
{
@@ -4799,6 +5171,8 @@ QVariant QQuickTableView::model() const
void QQuickTableView::setModel(const QVariant &newModel)
{
Q_D(QQuickTableView);
+
+ closeEditor();
d->setModelImpl(newModel);
if (d->selectionModel)
@@ -4822,6 +5196,22 @@ void QQuickTableView::setDelegate(QQmlComponent *newDelegate)
emit delegateChanged();
}
+QQuickTableView::EditTriggers QQuickTableView::editTriggers() const
+{
+ return d_func()->editTriggers;
+}
+
+void QQuickTableView::setEditTriggers(QQuickTableView::EditTriggers editTriggers)
+{
+ Q_D(QQuickTableView);
+ if (editTriggers == d->editTriggers)
+ return;
+
+ d->editTriggers = editTriggers;
+
+ emit editTriggersChanged();
+}
+
bool QQuickTableView::reuseItems() const
{
return bool(d_func()->reusableFlag == QQmlTableInstanceModel::Reusable);
@@ -5509,6 +5899,111 @@ void QQuickTableView::forceLayout()
d_func()->forceLayout(true);
}
+void QQuickTableView::edit(const QModelIndex &index)
+{
+ Q_D(QQuickTableView);
+
+ if (!d->canEdit(index, true))
+ return;
+
+ if (d->editIndex == index)
+ return;
+
+ if (!d->tableModel)
+ return;
+
+ if (!d->editModel) {
+ d->editModel = new QQmlTableInstanceModel(qmlContext(this));
+ d->editModel->useImportVersion(d->resolveImportVersion());
+ QObject::connect(d->editModel, &QQmlInstanceModel::initItem,
+ [this, d] (int serializedModelIndex, QObject *object) {
+ // initItemCallback will call setRequiredProperty for each required property in the
+ // delegate, both for this class, but also also for any subclasses. setRequiredProperty
+ // is currently dependent of the QQmlTableInstanceModel that was used to create the object
+ // in order to initialize required properties, so we need to set the editItem variable
+ // early on, so that we can use it in setRequiredProperty.
+ d->editIndex = modelIndex(d->cellAtModelIndex(serializedModelIndex));
+ d->editItem = qmlobject_cast<QQuickItem*>(object);
+ d->initItemCallback(serializedModelIndex, object);
+ });
+ }
+
+ if (d->selectionModel)
+ d->selectionModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
+
+ if (d->editIndex.isValid())
+ closeEditor();
+
+ const auto cellItem = itemAtCell(cellAtIndex(index));
+ Q_ASSERT(cellItem);
+ const auto attached = d->getAttachedObject(cellItem);
+ Q_ASSERT(attached);
+
+ d->editModel->setModel(d->tableModel->model());
+ d->editModel->setDelegate(attached->editDelegate());
+
+ const int cellIndex = d->modelIndexToCellIndex(index);
+ QObject* object = d->editModel->object(cellIndex, QQmlIncubator::Synchronous);
+ if (!object) {
+ d->editIndex = QModelIndex();
+ d->editItem = nullptr;
+ qmlWarning(this) << "cannot edit: TableView.editDelegate could not be instantiated!";
+ return;
+ }
+
+ // Note: at this point, editIndex and editItem has been set from initItem!
+
+ if (!d->editItem) {
+ qmlWarning(this) << "cannot edit: TableView.editDelegate is not an Item!";
+ d->editItem = nullptr;
+ d->editIndex = QModelIndex();
+ d->editModel->release(object, QQmlInstanceModel::NotReusable);
+ return;
+ }
+
+ d->editItem->setZ(2);
+ d->updateEditItem();
+
+ // Find the first child inside the delegate that wants focus. But
+ // we only transfer focus if the edit item didn't already set
+ // it to some other internal item explicitly upon construction.
+ QObject *focusObject = d->editItem->window()->focusObject();
+ QQuickItem *focusItem = qobject_cast<QQuickItem *>(focusObject);
+ if (!d->editItem->isAncestorOf(focusItem)) {
+ QQuickItem *newFocusItem = d->editItem;
+ if (!newFocusItem->activeFocusOnTab())
+ newFocusItem = d->editItem->nextItemInFocusChain(true);
+ if (newFocusItem == d->editItem || d->editItem->isAncestorOf(newFocusItem)) {
+ // Set activeFocusOnTab to false to ensure that QQuickTableView::keyPressEvent()
+ // receives the tab keys, instead of QQuickItemPrivate::focusNextPrev().
+ // TableView knows better which cell is next in the focus chain.
+ newFocusItem->setActiveFocusOnTab(false);
+ newFocusItem->forceActiveFocus(Qt::MouseFocusReason);
+ }
+ }
+}
+
+void QQuickTableView::closeEditor()
+{
+ Q_D(QQuickTableView);
+
+ if (!d->editItem)
+ return;
+
+ d->editModel->release(d->editItem, QQmlInstanceModel::NotReusable);
+ d->editItem = nullptr;
+
+ if (d->editIndex.isValid()) {
+ // Note: we can have an invalid editIndex, even when we
+ // have an editItem, if the model has changed (e.g been reset)!
+ const QPoint cell = cellAtIndex(d->editIndex);
+ if (auto cellItem = itemAtCell(cell))
+ QQuickItemPrivate::get(cellItem)->setCulled(false);
+
+ d->editIndex = QModelIndex();
+ }
+}
+
QQuickTableViewAttached *QQuickTableView::qmlAttachedProperties(QObject *obj)
{
return new QQuickTableViewAttached(obj);
@@ -5573,7 +6068,7 @@ void QQuickTableView::keyPressEvent(QKeyEvent *e)
{
Q_D(QQuickTableView);
- if (!d->keyNavigationEnabled || !d->selectionModel || !d->selectionModel->model()) {
+ if (!d->keyNavigationEnabled) {
QQuickFlickable::keyPressEvent(e);
return;
}
@@ -5581,19 +6076,30 @@ void QQuickTableView::keyPressEvent(QKeyEvent *e)
if (d->tableSize.isEmpty())
return;
- const QModelIndex currentIndex = d->selectionModel->currentIndex();
- const QPoint currentCell = cellAtIndex(currentIndex);
-
- if (!d->cellIsValid(currentCell)) {
- switch (e->key()) {
- case Qt::Key_Up:
- case Qt::Key_Down:
- case Qt::Key_Left:
- case Qt::Key_Right:
- // Special case: the current index doesn't map to a cell in the view (perhaps
- // because it isn't set yet). In that case, we set it to be the top-left cell.
- const QModelIndex topLeftIndex = modelIndex(leftColumn(), topRow());
- d->selectionModel->setCurrentIndex(topLeftIndex, QItemSelectionModel::NoUpdate);
+ if (d->editIndex.isValid()) {
+ // While editing, we limit the keys that we
+ // handle to not interfere with editing.
+ if (d->editTriggers != QQuickTableView::NoEditTriggers) {
+ switch (e->key()) {
+ case Qt::Key_Enter:
+ case Qt::Key_Return:
+ if (auto attached = d->getAttachedObject(d->editItem))
+ emit attached->commit();
+ closeEditor();
+ break;
+ case Qt::Key_Escape:
+ closeEditor();
+ break;
+ case Qt::Key_Tab:
+ case Qt::Key_Backtab:
+ if (activeFocusOnTab()) {
+ if (auto attached = d->getAttachedObject(d->editItem))
+ emit attached->commit();
+ if (d->setCurrentIndexFromKeyEvent(e))
+ edit(d->selectionModel->currentIndex());
+ }
+ break;
+ }
}
return;
}
@@ -5601,6 +6107,9 @@ void QQuickTableView::keyPressEvent(QKeyEvent *e)
if (d->setCurrentIndexFromKeyEvent(e))
return;
+ if (d->editFromKeyEvent(e))
+ return;
+
QQuickFlickable::keyPressEvent(e);
}
diff --git a/src/quick/items/qquicktableview_p.h b/src/quick/items/qquicktableview_p.h
index c2e935a287..0366c9d250 100644
--- a/src/quick/items/qquicktableview_p.h
+++ b/src/quick/items/qquicktableview_p.h
@@ -23,6 +23,7 @@ QT_REQUIRE_CONFIG(quick_tableview);
#include <QtQuick/private/qquickflickable_p.h>
#include <QtQml/private/qqmlnullablevalue_p.h>
#include <QtQml/private/qqmlfinalizer_p.h>
+#include <QtQml/private/qqmlguard_p.h>
QT_BEGIN_NAMESPACE
@@ -62,6 +63,7 @@ class Q_QUICK_PRIVATE_EXPORT QQuickTableView : public QQuickFlickable, public QQ
Q_PROPERTY(SelectionBehavior selectionBehavior READ selectionBehavior WRITE setSelectionBehavior NOTIFY selectionBehaviorChanged REVISION(6, 4) FINAL)
Q_PROPERTY(bool resizableColumns READ resizableColumns WRITE setResizableColumns NOTIFY resizableColumnsChanged REVISION(6, 5) FINAL)
Q_PROPERTY(bool resizableRows READ resizableRows WRITE setResizableRows NOTIFY resizableRowsChanged REVISION(6, 5) FINAL)
+ Q_PROPERTY(EditTriggers editTriggers READ editTriggers WRITE setEditTriggers NOTIFY editTriggersChanged REVISION(6, 5) FINAL)
QML_NAMED_ELEMENT(TableView)
QML_ADDED_IN_VERSION(2, 12)
@@ -90,6 +92,17 @@ public:
};
Q_ENUM(SelectionBehavior)
+ enum EditTrigger {
+ NoEditTriggers = 0x0,
+ SingleTapped = 0x1,
+ DoubleTapped = 0x2,
+ SelectedTapped = 0x4,
+ EditKeyPressed = 0x8,
+ AnyKeyPressed = 0x10,
+ };
+ Q_DECLARE_FLAGS(EditTriggers, EditTrigger)
+ Q_FLAG(EditTriggers)
+
QQuickTableView(QQuickItem *parent = nullptr);
~QQuickTableView() override;
int rows() const;
@@ -156,6 +169,9 @@ public:
bool resizableRows() const;
void setResizableRows(bool enabled);
+ EditTriggers editTriggers() const;
+ void setEditTriggers(EditTriggers editTriggers);
+
Q_INVOKABLE void forceLayout();
Q_INVOKABLE void positionViewAtCell(const QPoint &cell, PositionMode mode, const QPointF &offset = QPointF(), const QRectF &subRect = QRectF());
Q_INVOKABLE void positionViewAtCell(int column, int row, PositionMode mode, const QPointF &offset = QPointF(), const QRectF &subRect = QRectF());
@@ -194,6 +210,9 @@ public:
Q_REVISION(6, 5) Q_INVOKABLE void clearRowHeights();
Q_REVISION(6, 5) Q_INVOKABLE qreal explicitRowHeight(int row) const;
+ Q_REVISION(6, 5) Q_INVOKABLE void edit(const QModelIndex &index);
+ Q_REVISION(6, 5) Q_INVOKABLE void closeEditor();
+
static QQuickTableViewAttached *qmlAttachedProperties(QObject *);
Q_SIGNALS:
@@ -222,6 +241,7 @@ Q_SIGNALS:
Q_REVISION(6, 4) void selectionBehaviorChanged();
Q_REVISION(6, 5) void resizableColumnsChanged();
Q_REVISION(6, 5) void resizableRowsChanged();
+ Q_REVISION(6, 5) void editTriggersChanged();
protected:
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override;
@@ -247,6 +267,7 @@ class Q_QUICK_PRIVATE_EXPORT QQuickTableViewAttached : public QObject
{
Q_OBJECT
Q_PROPERTY(QQuickTableView *view READ view NOTIFY viewChanged)
+ Q_PROPERTY(QQmlComponent *editDelegate READ editDelegate WRITE setEditDelegate NOTIFY editDelegateChanged)
public:
QQuickTableViewAttached(QObject *parent)
@@ -260,18 +281,31 @@ public:
Q_EMIT viewChanged();
}
+ QQmlComponent *editDelegate() const { return m_editDelegate; }
+ void setEditDelegate(QQmlComponent *newEditDelegate)
+ {
+ if (m_editDelegate == newEditDelegate)
+ return;
+ m_editDelegate = newEditDelegate;
+ emit editDelegateChanged();
+ }
+
Q_SIGNALS:
void viewChanged();
void pooled();
void reused();
+ void editDelegateChanged();
+ void commit();
private:
QPointer<QQuickTableView> m_view;
+ QQmlGuard<QQmlComponent> m_editDelegate;
friend class QQuickTableViewPrivate;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(QQuickTableView::PositionMode)
+Q_DECLARE_OPERATORS_FOR_FLAGS(QQuickTableView::EditTriggers)
QT_END_NAMESPACE
diff --git a/src/quick/items/qquicktableview_p_p.h b/src/quick/items/qquicktableview_p_p.h
index c1099529eb..54730fbc64 100644
--- a/src/quick/items/qquicktableview_p_p.h
+++ b/src/quick/items/qquicktableview_p_p.h
@@ -372,6 +372,11 @@ public:
QQuickTableViewHoverHandler *hoverHandler = nullptr;
QQuickTableViewResizeHandler *resizeHandler = nullptr;
+ QQmlTableInstanceModel *editModel = nullptr;
+ QQuickItem *editItem = nullptr;
+ QPersistentModelIndex editIndex;
+ QQuickTableView::EditTriggers editTriggers = QQuickTableView::DoubleTapped | QQuickTableView::EditKeyPressed;
+
#ifdef QT_DEBUG
QString forcedIncubationMode = qEnvironmentVariable("QT_TABLEVIEW_INCUBATION_MODE");
#endif
@@ -475,6 +480,7 @@ public:
void scheduleRebuildTable(QQuickTableViewPrivate::RebuildOptions options);
void updateCursor();
+ void updateEditItem();
QTypeRevision resolveImportVersion();
void createWrapperModel();
@@ -546,6 +552,8 @@ public:
void setCurrentIndexFromTap(const QPointF &pos);
void setCurrentIndex(const QPoint &cell);
bool setCurrentIndexFromKeyEvent(QKeyEvent *e);
+ bool canEdit(const QModelIndex tappedIndex, bool warn);
+ bool editFromKeyEvent(QKeyEvent *e);
// QQuickSelectable
QQuickItem *selectionPointerHandlerTarget() const override;
diff --git a/tests/auto/quick/qquicktableview/data/editdelegate.qml b/tests/auto/quick/qquicktableview/data/editdelegate.qml
new file mode 100644
index 0000000000..3fbaf1fae9
--- /dev/null
+++ b/tests/auto/quick/qquicktableview/data/editdelegate.qml
@@ -0,0 +1,56 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Window
+
+Item {
+ width: 640
+ height: 450
+
+ property alias tableView: tableView
+
+ TableView {
+ id: tableView
+ anchors.fill: parent
+ clip: true
+
+ property Item editItem: null
+ property var editIndex
+
+ selectionModel: ItemSelectionModel {}
+
+ delegate: Rectangle {
+ implicitWidth: 100
+ implicitHeight: 50
+
+ Text {
+ anchors.centerIn: parent
+ text: display
+ }
+
+ TableView.editDelegate: TextInput {
+ id: editRoot
+ text: display
+ horizontalAlignment: TextInput.AlignHCenter
+ verticalAlignment: TextInput.AlignVCenter
+ activeFocusOnTab: true
+
+ required property bool current
+ required property bool selected
+
+ Component.onCompleted: {
+ tableView.editItem = editRoot
+ tableView.editIndex = tableView.modelIndex(column, row)
+ selectAll()
+ }
+
+ Component.onDestruction: {
+ tableView.editItem = null
+ tableView.editIndex = tableView.modelIndex(-1, -1)
+ }
+ }
+ }
+ }
+
+}
diff --git a/tests/auto/quick/qquicktableview/testmodel.h b/tests/auto/quick/qquicktableview/testmodel.h
index 55ac5ef93d..02a3478bdd 100644
--- a/tests/auto/quick/qquicktableview/testmodel.h
+++ b/tests/auto/quick/qquicktableview/testmodel.h
@@ -159,6 +159,17 @@ public:
insertRow(row, QModelIndex());
}
+ Qt::ItemFlags flags(const QModelIndex &index) const override
+ {
+ Q_UNUSED(index)
+ return m_flags;
+ }
+
+ void setFlags(Qt::ItemFlags flags)
+ {
+ m_flags = flags;
+ }
+
signals:
void rowCountChanged();
void columnCountChanged();
@@ -168,6 +179,7 @@ private:
int m_columns = 0;
bool m_dataCanBeFetched = false;
QHash<int, QString> modelData;
+ Qt::ItemFlags m_flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
};
#define TestModelAsVariant(...) QVariant::fromValue(QSharedPointer<TestModel>(new TestModel(__VA_ARGS__)))
diff --git a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp
index 08cd1b81fb..12c50185ea 100644
--- a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp
+++ b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp
@@ -9,6 +9,7 @@
#include <QtQuick/private/qquicktableview_p_p.h>
#include <QtQuick/private/qquickloader_p.h>
#include <QtQuick/private/qquickdraghandler_p.h>
+#include <QtQuick/private/qquicktextinput_p.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcontext.h>
@@ -42,6 +43,7 @@ Q_DECLARE_METATYPE(QMarginsF);
#define LOAD_TABLEVIEW(fileName) \
view->setSource(testFileUrl(fileName)); \
view->show(); \
+ view->requestActivate(); \
QVERIFY(QTest::qWaitForWindowActive(view)); \
GET_QML_TABLEVIEW(tableView)
@@ -249,6 +251,19 @@ private slots:
void dragFromCellCenter();
void tapOnResizeArea_data();
void tapOnResizeArea();
+ void editUsingEditTriggers_data();
+ void editUsingEditTriggers();
+ void editUsingTab();
+ void editOnNonEditableCell_data();
+ void editOnNonEditableCell();
+ void noEditDelegate_data();
+ void noEditDelegate();
+ void editAndCloseEditor();
+ void editWarning_noEditDelegate();
+ void editWarning_invalidIndex();
+ void editWarning_nonEditableModelItem();
+ void attachedPropertiesOnEditDelegate();
+ void requiredPropertiesOnEditDelegate();
};
tst_QQuickTableView::tst_QQuickTableView()
@@ -6365,6 +6380,679 @@ void tst_QQuickTableView::tapOnResizeArea()
QCOMPARE(tableView->selectionModel()->currentIndex(), model.index(1, 1));
}
+void tst_QQuickTableView::editUsingEditTriggers_data()
+{
+ QTest::addColumn<QQuickTableView::EditTriggers>("editTriggers");
+ QTest::addColumn<bool>("interactive");
+
+ // We need to test both with and without interactive, since SingleTapped
+ // actions will happen already on press in a TableView that is not interactive!
+ for (bool interactive : {true, false}) {
+ QTest::newRow("NoEditTriggers") << QQuickTableView::EditTriggers(QQuickTableView::NoEditTriggers) << interactive;
+ QTest::newRow("SingleTapped") << QQuickTableView::EditTriggers(QQuickTableView::SingleTapped) << interactive;
+ QTest::newRow("DoubleTapped") << QQuickTableView::EditTriggers(QQuickTableView::DoubleTapped) << interactive;
+ QTest::newRow("SelectedTapped") << QQuickTableView::EditTriggers(QQuickTableView::SelectedTapped) << interactive;
+ QTest::newRow("EditKeyPressed") << QQuickTableView::EditTriggers(QQuickTableView::EditKeyPressed) << interactive;
+ QTest::newRow("AnyKeyPressed") << QQuickTableView::EditTriggers(QQuickTableView::EditKeyPressed) << interactive;
+ QTest::newRow("DoubleTapped | EditKeyPressed")
+ << QQuickTableView::EditTriggers(QQuickTableView::DoubleTapped | QQuickTableView::EditKeyPressed) << interactive;
+ QTest::newRow("SingleTapped | AnyKeyPressed")
+ << QQuickTableView::EditTriggers(QQuickTableView::SingleTapped | QQuickTableView::AnyKeyPressed) << interactive;
+ }
+}
+
+void tst_QQuickTableView::editUsingEditTriggers()
+{
+ // Check that you can start to edit in TableView
+ // using the available edit triggers.
+ QFETCH(QQuickTableView::EditTriggers, editTriggers);
+ QFETCH(bool, interactive);
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->setInteractive(interactive);
+ tableView->forceActiveFocus();
+
+ QCOMPARE(tableView->editTriggers(), QQuickTableView::DoubleTapped | QQuickTableView::EditKeyPressed);
+ tableView->setEditTriggers(editTriggers);
+
+ const char kEditItem[] = "editItem";
+ const char kEditIndex[] = "editIndex";
+
+ WAIT_UNTIL_POLISHED;
+
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+
+ const QPoint cell1(1, 1);
+ const QPoint cell2(2, 1);
+ const QModelIndex index1 = tableView->modelIndex(cell1);
+ const QModelIndex index2 = tableView->modelIndex(cell2);
+ const auto item1 = tableView->itemAtCell(cell1);
+ const auto item2 = tableView->itemAtCell(cell2);
+ QVERIFY(item1);
+ QVERIFY(item2);
+
+ QQuickWindow *window = tableView->window();
+
+ const QPoint localPos = QPoint(item1->width() - 1, item1->height() - 1);
+ const QPoint localPosOutside = QPoint(tableView->contentWidth() + 10, tableView->contentHeight() + 10);
+ const QPoint tapPos1 = window->contentItem()->mapFromItem(item1, localPos).toPoint();
+ const QPoint tapPos2 = window->contentItem()->mapFromItem(item2, localPos).toPoint();
+ const QPoint tapOutsideContentItem = window->contentItem()->mapFromItem(item2, localPosOutside).toPoint();
+
+ if (editTriggers & QQuickTableView::SingleTapped) {
+ // edit cell 1
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+ const auto editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+ QVERIFY(editItem1->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+
+ // edit cell 2 (without closing the previous edit session first)
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos2);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ const auto editItem2 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem2);
+ QVERIFY(editItem2->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index2);
+
+ // single tap outside content item should close the editor
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapOutsideContentItem);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ }
+
+ if (editTriggers & QQuickTableView::DoubleTapped) {
+ // edit cell 1
+ QTest::mouseDClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+ const auto editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+ QVERIFY(editItem1->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+
+ // edit cell 2 (without closing the previous edit session first)
+ QTest::mouseDClick(window, Qt::LeftButton, Qt::NoModifier, tapPos2);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ const auto editItem2 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem2);
+ QVERIFY(editItem2->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index2);
+
+ // single tap outside the edit item should close the editor
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+
+ if (!(editTriggers & QQuickTableView::SingleTapped)) {
+ // single tap on a cell should not open the editor
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ // single tap outside content item should make sure editing ends
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapOutsideContentItem);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::SelectedTapped) {
+ // select cell first, then tap on it
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+ const auto editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+ QVERIFY(editItem1->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+
+ // tap on a non-selected cell. This should close the editor, and move
+ // the current index, but not begin to edit the cell.
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos2);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+
+ // tap on a non-selected cell while no editor is active
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos2);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ }
+
+ if (editTriggers & QQuickTableView::EditKeyPressed) {
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_Return);
+ const auto editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+ QVERIFY(editItem1->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+
+ // Pressing escape should close the editor
+ QTest::keyClick(window, Qt::Key_Escape);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+
+ // Pressing Enter to open the editor again
+ QTest::keyClick(window, Qt::Key_Enter);
+ const auto editItem2 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem2);
+ QVERIFY(editItem2->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+
+ // single tap outside the edit item should close the editor
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos2);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::AnyKeyPressed) {
+ // Pressing key x should start to edit. And in case of AnyKeyPressed, we
+ // also replay the key event to the focus object.
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_X);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+ auto textInput1 = tableView->property(kEditItem).value<QQuickTextInput *>();
+ QVERIFY(textInput1);
+ QVERIFY(textInput1->hasActiveFocus());
+ QCOMPARE(textInput1->text(), "x");
+
+ // Pressing escape should close the editor
+ QTest::keyClick(window, Qt::Key_Escape);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+
+ // Pressing enter should also start to edit. But this is a
+ // special case, we don't replay enter into the focus object.
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_Enter);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+ auto textInput2 = tableView->property(kEditItem).value<QQuickTextInput *>();
+ QVERIFY(textInput2);
+ QVERIFY(textInput2->hasActiveFocus());
+ QCOMPARE(textInput2->text(), "1");
+
+ if (!(editTriggers & QQuickTableView::SingleTapped)) {
+ // single tap outside the edit item should close the editor
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos2);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ // single tap outside content item should make sure editing ends
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapOutsideContentItem);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers == QQuickTableView::NoEditTriggers) {
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::mouseDClick(window, Qt::LeftButton, Qt::NoModifier, tapPos1);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_Return);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_X);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+}
+
+void tst_QQuickTableView::editUsingTab()
+{
+ // Check that the you can commit and start to edit
+ // the next cell by pressing tab and backtab.
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->forceActiveFocus();
+
+ const char kEditItem[] = "editItem";
+ const char kEditIndex[] = "editIndex";
+
+ WAIT_UNTIL_POLISHED;
+
+ const QPoint cell1(1, 1);
+ const QPoint cell2(2, 1);
+ const QModelIndex index1 = tableView->modelIndex(cell1);
+ const QModelIndex index2 = tableView->modelIndex(cell2);
+
+ QQuickWindow *window = tableView->window();
+
+ // Edit cell 1
+ tableView->edit(index1);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+ const QQuickItem *editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+
+ // Press Tab to edit cell 2
+ QTest::keyClick(window, Qt::Key_Tab);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index2);
+ const QQuickItem *editItem2 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem2);
+
+ // Press Backtab to edit cell 1
+ QTest::keyClick(window, Qt::Key_Backtab);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+ const QQuickItem *editItem3 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem3);
+}
+
+void tst_QQuickTableView::editOnNonEditableCell_data()
+{
+ QTest::addColumn<QQuickTableView::EditTriggers>("editTriggers");
+
+ QTest::newRow("SingleTapped") << QQuickTableView::EditTriggers(QQuickTableView::SingleTapped);
+ QTest::newRow("DoubleTapped") << QQuickTableView::EditTriggers(QQuickTableView::DoubleTapped);
+ QTest::newRow("SelectedTapped") << QQuickTableView::EditTriggers(QQuickTableView::SelectedTapped);
+ QTest::newRow("EditKeyPressed") << QQuickTableView::EditTriggers(QQuickTableView::EditKeyPressed);
+ QTest::newRow("AnyKeyPressed") << QQuickTableView::EditTriggers(QQuickTableView::EditKeyPressed);
+}
+
+void tst_QQuickTableView::editOnNonEditableCell()
+{
+ // Check that the user cannot edit a non-editable cell from the edit triggers.
+ // Note: we don't want TableView to print out warnings in this case, since
+ // the user is not doing anything wrong. We only want to print out warnings if
+ // the application is calling edit() explicitly on a cell that cannot be edited
+ // (separate test below).
+ QFETCH(QQuickTableView::EditTriggers, editTriggers);
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ // set flags that exclude Qt::ItemIsEditable
+ model.setFlags(Qt::ItemIsEnabled);
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->setEditTriggers(editTriggers);
+ tableView->forceActiveFocus();
+
+ const char kEditItem[] = "editItem";
+ const char kEditIndex[] = "editIndex";
+
+ WAIT_UNTIL_POLISHED;
+
+ const QPoint cell(1, 1);
+ const QModelIndex index1 = tableView->modelIndex(cell);
+ const auto item = tableView->itemAtCell(cell);
+ QVERIFY(item);
+
+ QQuickWindow *window = tableView->window();
+
+ const QPoint localPos = QPoint(item->width() - 1, item->height() - 1);
+ const QPoint tapPos = window->contentItem()->mapFromItem(item, localPos).toPoint();
+
+ if (editTriggers & QQuickTableView::SingleTapped) {
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::DoubleTapped) {
+ QTest::mouseDClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::SelectedTapped) {
+ // select cell first, then tap on it
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::EditKeyPressed) {
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_Return);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::AnyKeyPressed) {
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_X);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+}
+
+void tst_QQuickTableView::noEditDelegate_data()
+{
+ QTest::addColumn<QQuickTableView::EditTriggers>("editTriggers");
+
+ QTest::newRow("NoEditTriggers") << QQuickTableView::EditTriggers(QQuickTableView::NoEditTriggers);
+ QTest::newRow("SingleTapped") << QQuickTableView::EditTriggers(QQuickTableView::SingleTapped);
+ QTest::newRow("DoubleTapped") << QQuickTableView::EditTriggers(QQuickTableView::DoubleTapped);
+ QTest::newRow("SelectedTapped") << QQuickTableView::EditTriggers(QQuickTableView::SelectedTapped);
+ QTest::newRow("EditKeyPressed") << QQuickTableView::EditTriggers(QQuickTableView::EditKeyPressed);
+ QTest::newRow("AnyKeyPressed") << QQuickTableView::EditTriggers(QQuickTableView::EditKeyPressed);
+}
+
+void tst_QQuickTableView::noEditDelegate()
+{
+ // Check that you cannot start to edit if
+ // no edit delegate has been set.
+ QFETCH(QQuickTableView::EditTriggers, editTriggers);
+ LOAD_TABLEVIEW("tableviewwithselected2.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->setEditTriggers(editTriggers);
+ tableView->forceActiveFocus();
+
+ const char kEditItem[] = "editItem";
+ const char kEditIndex[] = "editIndex";
+
+ WAIT_UNTIL_POLISHED;
+
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+
+ const QPoint cell(1, 1);
+ const QModelIndex index1 = tableView->modelIndex(cell);
+ const auto item = tableView->itemAtCell(cell);
+ QVERIFY(item);
+
+ QQuickWindow *window = tableView->window();
+
+ const QPoint localPos = QPoint(item->width() - 1, item->height() - 1);
+ const QPoint tapPos = window->contentItem()->mapFromItem(item, localPos).toPoint();
+
+ if (editTriggers & QQuickTableView::SingleTapped) {
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::DoubleTapped) {
+ QTest::mouseDClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::SelectedTapped) {
+ // select cell first, then tap on it
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::EditKeyPressed) {
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_Return);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers & QQuickTableView::AnyKeyPressed) {
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_X);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+
+ if (editTriggers == QQuickTableView::NoEditTriggers) {
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::mouseDClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ tableView->selectionModel()->setCurrentIndex(index1, QItemSelectionModel::NoUpdate);
+ QTest::keyClick(window, Qt::Key_Return);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QTest::keyClick(window, Qt::Key_X);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ }
+}
+
+void tst_QQuickTableView::editAndCloseEditor()
+{
+ // Check that the application can call edit() and closeEditor()
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->forceActiveFocus();
+
+ const char kEditItem[] = "editItem";
+ const char kEditIndex[] = "editIndex";
+
+ WAIT_UNTIL_POLISHED;
+
+ const QPoint cell1(1, 1);
+ const QPoint cell2(2, 2);
+ const QModelIndex index1 = tableView->modelIndex(cell1);
+ const QModelIndex index2 = tableView->modelIndex(cell2);
+
+ // Edit cell 1
+ tableView->edit(index1);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index1);
+ const QQuickItem *editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+ QVERIFY(editItem1->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index1);
+
+ // Edit cell 2
+ tableView->edit(index2);
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ const QQuickItem *editItem2 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem2);
+ QVERIFY(editItem2->hasActiveFocus());
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index2);
+
+ // Close the editor
+ tableView->closeEditor();
+ QCOMPARE(tableView->selectionModel()->currentIndex(), index2);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+}
+
+void tst_QQuickTableView::editWarning_noEditDelegate()
+{
+ // Check that the TableView will print out a warning if the
+ // application calls edit() on a cell that has no editDelegate.
+ LOAD_TABLEVIEW("tableviewwithselected2.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+
+ WAIT_UNTIL_POLISHED;
+
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*cannot edit: no TableView.editDelegate set!"));
+ tableView->edit(tableView->modelIndex(1, 1));
+}
+
+void tst_QQuickTableView::editWarning_invalidIndex()
+{
+ // Check that the TableView will print out a warning if the
+ // application calls edit() on an invalid index.
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+
+ WAIT_UNTIL_POLISHED;
+
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*cannot edit: index is not valid!"));
+ tableView->edit(tableView->modelIndex(-1, -1));
+}
+
+void tst_QQuickTableView::editWarning_nonEditableModelItem()
+{
+ // Check that the TableView will print out a warning if the
+ // application calls edit() on cell that cannot, according
+ // to the model flags, be edited.
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+ // set flags that exclude Qt::ItemIsEditable
+ model.setFlags(Qt::ItemIsEnabled);
+
+ WAIT_UNTIL_POLISHED;
+
+ QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*cannot edit:.*flags.*Qt::ItemIsEditable"));
+ tableView->edit(tableView->modelIndex(1, 1));
+}
+
+void tst_QQuickTableView::attachedPropertiesOnEditDelegate()
+{
+ // Check that the TableView.commit signal is emitted when
+ // the user presses enter or return, but not when e.g pressing escape.
+ // Also check that TableView.view is correct.
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ auto model = TestModel(4, 4);
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->forceActiveFocus();
+
+ const char kEditItem[] = "editItem";
+ const char kEditIndex[] = "editIndex";
+
+ WAIT_UNTIL_POLISHED;
+
+ const QPoint cell(1, 1);
+ const QModelIndex index = tableView->modelIndex(cell);
+ QQuickWindow *window = tableView->window();
+
+ // Open the edit
+ tableView->edit(index);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index);
+ QQuickItem *editItem1 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem1);
+ const auto attached1 = getAttachedObject(editItem1);
+ QVERIFY(attached1);
+ QSignalSpy commitSpy1(attached1, &QQuickTableViewAttached::commit);
+
+ // Check that TableView has been assigned to TableView.view
+ QCOMPARE(attached1->view(), tableView);
+
+ // Accept and close the edit, check commit signal
+ QTest::keyClick(window, Qt::Key_Enter);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(commitSpy1.count(), 1);
+
+ // Repeat once more, but use Key_Return to accept instead
+ tableView->edit(index);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index);
+ QQuickItem *editItem2 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem2);
+ const auto attached2 = getAttachedObject(editItem2);
+ QVERIFY(attached2);
+ QSignalSpy commitSpy2(attached2, &QQuickTableViewAttached::commit);
+
+ QTest::keyClick(window, Qt::Key_Return);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(commitSpy1.count(), 1);
+ QCOMPARE(commitSpy2.count(), 1);
+
+ // Repeat once more, but use Key_Escape instead.
+ // This should close the edit, but without an accepted signal.
+ tableView->edit(index);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index);
+ QQuickItem *editItem3 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem3);
+ const auto attached3 = getAttachedObject(editItem3);
+ QVERIFY(editItem3);
+ QSignalSpy commitSpy3(attached3, &QQuickTableViewAttached::commit);
+
+ QTest::keyClick(window, Qt::Key_Escape);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(commitSpy3.count(), 0);
+
+ // Repeat once more, but tap outside the edit item.
+ // This should close the edit, but without an accepted signal.
+ tableView->edit(index);
+ QCOMPARE(tableView->property(kEditIndex).value<QModelIndex>(), index);
+ QQuickItem *editItem4 = tableView->property(kEditItem).value<QQuickItem *>();
+ QVERIFY(editItem4);
+ const auto attached4 = getAttachedObject(editItem4);
+ QVERIFY(editItem4);
+ QSignalSpy commitSpy4(attached4, &QQuickTableViewAttached::commit);
+
+ const QPoint tapPos = window->contentItem()->mapFromItem(editItem4, QPointF(-10, -10)).toPoint();
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, tapPos);
+ QVERIFY(!tableView->property(kEditItem).value<QQuickItem *>());
+ QVERIFY(!tableView->property(kEditIndex).value<QModelIndex>().isValid());
+ QCOMPARE(commitSpy4.count(), 0);
+}
+
+void tst_QQuickTableView::requiredPropertiesOnEditDelegate()
+{
+ // Check that all expected required properties on the edit
+ // delegate (like row, column, current) has correct values.
+ LOAD_TABLEVIEW("editdelegate.qml");
+
+ TestModel model(4, 4);
+ QItemSelectionModel selectionModel(&model);
+
+ tableView->setModel(QVariant::fromValue(&model));
+ tableView->setSelectionModel(&selectionModel);
+
+ const char kEditItem[] = "editItem";
+
+ WAIT_UNTIL_POLISHED;
+
+ const QPoint cell(1, 1);
+ const QModelIndex index1 = tableView->modelIndex(cell);
+ const QModelIndex index2 = tableView->modelIndex(2, 2);
+
+ tableView->edit(index1);
+
+ auto textInput = tableView->property(kEditItem).value<QQuickTextInput *>();
+ QVERIFY(textInput);
+ // Check that "text: display" in the edit delegate works
+ QCOMPARE(textInput->text(), "1");
+
+ QCOMPARE(textInput->property("current").toBool(), true);
+ QCOMPARE(textInput->property("selected").toBool(), false);
+ selectionModel.select(index1, QItemSelectionModel::Select);
+ QCOMPARE(textInput->property("selected").toBool(), true);
+ selectionModel.setCurrentIndex(index2, QItemSelectionModel::Select);
+ QCOMPARE(textInput->property("current").toBool(), false);
+}
+
QTEST_MAIN(tst_QQuickTableView)
#include "tst_qquicktableview.moc"
diff --git a/tests/manual/tableview/abstracttablemodel/main.cpp b/tests/manual/tableview/abstracttablemodel/main.cpp
index dadd992ef7..a765f66272 100644
--- a/tests/manual/tableview/abstracttablemodel/main.cpp
+++ b/tests/manual/tableview/abstracttablemodel/main.cpp
@@ -203,6 +203,12 @@ public:
return true;
}
+ Qt::ItemFlags flags(const QModelIndex &index) const override
+ {
+ Q_UNUSED(index)
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
+ }
+
signals:
void rowCountChanged();
void columnCountChanged();
diff --git a/tests/manual/tableview/abstracttablemodel/main.qml b/tests/manual/tableview/abstracttablemodel/main.qml
index 36f27afda3..5f030039f5 100644
--- a/tests/manual/tableview/abstracttablemodel/main.qml
+++ b/tests/manual/tableview/abstracttablemodel/main.qml
@@ -53,7 +53,7 @@ ApplicationWindow {
id: flickingMode
checkable: true
checked: true
- text: "Enable flicking"
+ text: "Interactive"
}
CheckBox {
@@ -126,6 +126,36 @@ ApplicationWindow {
checked: false
text: "Highlight row/col"
}
+
+ ComboBox {
+ id: selectionCombo
+ model: [
+ "SelectionDisabled",
+ "SelectCells",
+ "SelectRows",
+ "SelectColumns",
+ ]
+ }
+ ComboBox {
+ id: selectionModeCombo
+ model: [
+ "Auto",
+ "Drag",
+ "PressAndHold",
+ ]
+ }
+ ComboBox {
+ id: editCombo
+ currentIndex: 2
+ model: [
+ "NoEditTriggers",
+ "SingleTapped",
+ "DoubleTapped",
+ "SelectedTapped",
+ "EditKeyPressed",
+ "AnyKeyPressed",
+ ]
+ }
}
}
@@ -156,7 +186,7 @@ ApplicationWindow {
id: marginsSpinBox
from: 0
to: 100
- value: 1
+ value: 0
editable: true
}
}
@@ -231,36 +261,6 @@ ApplicationWindow {
Layout.rightMargin: menu.menuMargin
Layout.leftMargin: menu.menuMargin
ColumnLayout {
- RadioButton {
- id: selectionDisabled
- text: "SelectionDisabled"
- }
- RadioButton {
- id: selectCells
- text: "SelectCells"
- checked: true
- }
- RadioButton {
- id: selectRows
- text: "SelectRows"
- }
- RadioButton {
- id: selectColumns
- text: "SelectColumns"
- }
- Label {
- width: parent.width
- font.pixelSize: 10
- text: "(SelectionMode: " + (tableView.interactive ? "PressAndHold)" : "Drag)")
- }
- }
- }
-
- GroupBox {
- Layout.minimumWidth: menu.availableWidth - (menu.menuMargin * 2)
- Layout.rightMargin: menu.menuMargin
- Layout.leftMargin: menu.menuMargin
- ColumnLayout {
Button {
text: "Current to top-left"
enabled: currentIndex.valid
@@ -312,6 +312,35 @@ ApplicationWindow {
Layout.minimumWidth: menu.availableWidth - (menu.menuMargin * 2)
Layout.rightMargin: menu.menuMargin
Layout.leftMargin: menu.menuMargin
+ ColumnLayout {
+ Button {
+ text: "Open editor"
+ enabled: currentIndex.valid
+ onClicked: {
+ tableView.edit(currentIndex, true)
+ }
+ }
+ Button {
+ text: "Close editor"
+ enabled: currentIndex.valid
+ onClicked: {
+ tableView.closeEditor()
+ }
+ }
+ Button {
+ text: "Set current index"
+ onClicked: {
+ let index = tableView.modelIndex(1, 1);
+ tableView.selectionModel.setCurrentIndex(index, ItemSelectionModel.NoUpdate)
+ }
+ }
+ }
+ }
+
+ GroupBox {
+ Layout.minimumWidth: menu.availableWidth - (menu.menuMargin * 2)
+ Layout.rightMargin: menu.menuMargin
+ Layout.leftMargin: menu.menuMargin
Layout.bottomMargin: menu.menuMargin
ColumnLayout {
Button {
@@ -328,6 +357,11 @@ ApplicationWindow {
leftHeader.contentY += tableView.height * 1.2
}
}
+
+ Button {
+ text: "ForceLayout()"
+ onClicked: tableView.forceLayout()
+ }
}
}
@@ -354,8 +388,8 @@ ApplicationWindow {
delegate: Rectangle {
implicitHeight: topHeader.height
implicitWidth: 20
- color: "lightgray"
- Text {
+ color: window.palette.alternateBase
+ Label {
anchors.centerIn: parent
visible: drawText.checked
text: column
@@ -388,8 +422,8 @@ ApplicationWindow {
delegate: Rectangle {
implicitHeight: 50
implicitWidth: leftHeader.width
- color: "lightgray"
- Text {
+ color: window.palette.alternateBase
+ Label {
anchors.centerIn: parent
visible: drawText.checked
text: row
@@ -411,6 +445,8 @@ ApplicationWindow {
anchors.right: parent.right
anchors.top: topHeader.bottom
anchors.bottom: parent.bottom
+ anchors.topMargin: 1
+ anchors.leftMargin: 1
anchors.rightMargin: 10
anchors.bottomMargin: 10
@@ -428,10 +464,26 @@ ApplicationWindow {
resizableRows: resizableRowsEnabled.checked
resizableColumns: resizableColumnsEnabled.checked
animate: enableAnimation.checked
- selectionBehavior: selectCells.checked ? TableView.SelectCells
- : selectColumns.checked ? TableView.SelectColumns
- : selectRows.checked ? TableView.SelectRows
- : TableView.SelectionDisabled
+ selectionBehavior: {
+ switch (selectionCombo.currentText) {
+ case "SelectCells": return TableView.SelectCells
+ case "SelectRows": return TableView.SelectRows
+ case "SelectColumns": return TableView.SelectColumns
+ }
+ return TableView.SelectionDisabled
+ }
+ editTriggers: {
+ switch (editCombo.currentText) {
+ case "NoEditTriggers": return TableView.NoEditTriggers
+ case "SingleTapped": return TableView.SingleTapped
+ case "DoubleTapped": return TableView.DoubleTapped
+ case "SelectedTapped": return TableView.SelectedTapped
+ case "EditKeyPressed": return TableView.EditKeyPressed
+ case "AnyKeyPressed": return TableView.AnyKeyPressed
+ }
+ return TableView.SelectionDisabled
+ }
+
leftMargin: marginsSpinBox.value
topMargin: marginsSpinBox.value
rightMargin: marginsSpinBox.value
@@ -451,6 +503,13 @@ ApplicationWindow {
SelectionRectangle {
target: tableView
+ selectionMode: {
+ switch (selectionModeCombo.currentText) {
+ case "Drag": return SelectionRectangle.Drag
+ case "PressAndHold": return SelectionRectangle.PressAndHold
+ }
+ return SelectionRectangle.Auto
+ }
}
Component {
@@ -459,14 +518,14 @@ ApplicationWindow {
id: delegate
implicitWidth: useLargeCells.checked ? 1000 : 50
implicitHeight: useLargeCells.checked ? 1000 : 30
- border.width: current ? 2 : 0
- border.color: "darkgreen"
+ border.width: current ? 3 : 0
+ border.color: window.palette.highlight
property var randomColor: Qt.rgba(0.6 + (0.4 * Math.random()), 0.6 + (0.4 * Math.random()), 0.6 + (0.4 * Math.random()), 1)
- color: selected ? "lightgreen"
+ color: selected ? window.palette.highlight
: (highlightCurrentRow.checked && (row === tableView.currentRow || column === tableView.currentColumn)) ? "lightgray"
: useRandomColor.checked ? randomColor
: model.display === "added" ? "lightblue"
- : "white"
+ : window.palette.window.lighter(1.3)
required property bool selected
required property bool current
@@ -480,11 +539,26 @@ ApplicationWindow {
visible: useSubRect.checked
}
- Text {
+ Label {
anchors.centerIn: parent
visible: drawText.checked
text: model.display
}
+
+ TableView.editDelegate: TextField {
+ horizontalAlignment: TextInput.AlignHCenter
+ verticalAlignment: TextInput.AlignVCenter
+ text: display
+
+ TableView.onCommit: {
+ let modelIndex = TableView.view.modelIndex(column, row)
+ TableView.view.model.setData(modelIndex, text, Qt.DisplayRole)
+ }
+
+ Component.onCompleted: {
+ selectAll()
+ }
+ }
}
}