From ff50cce8c030e1471c1ce09f48c7f5513ca8ba51 Mon Sep 17 00:00:00 2001
From: Edward Brown <edward.brown@stfc.ac.uk>
Date: Tue, 10 Apr 2018 09:55:41 +0100
Subject: [PATCH] Added modified JobTreeView from batch_widget_prototype.

- Added a modified version of JobTreeView from the
  batch_widget_prototype branch
- Refactored out QtStandardItemTreeAdapter which can be tested
  separately from the view.
- Also transfered QtTreeCursorNavigation, the class used to implement
  the tab based navigation.

Re #22263
---
 qt/widgets/common/CMakeLists.txt              |   9 +
 .../Common/Batch/JobTreeView.h                |  84 ++++++++++
 .../Common/Batch/QtStandardItemTreeAdapter.h  |  55 +++++++
 .../Common/Batch/QtTreeCursorNavigation.h     |  35 ++++
 qt/widgets/common/src/Batch/JobTreeView.cpp   | 155 ++++++++++++++++++
 .../src/Batch/QtStandardItemTreeAdapter.cpp   | 124 ++++++++++++++
 .../src/Batch/QtTreeCursorNavigation.cpp      |  98 +++++++++++
 .../common/test/Batch/QtAdaptedModelTest.h    | 127 ++++++++++++++
 8 files changed, 687 insertions(+)
 create mode 100644 qt/widgets/common/inc/MantidQtWidgets/Common/Batch/JobTreeView.h
 create mode 100644 qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h
 create mode 100644 qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h
 create mode 100644 qt/widgets/common/src/Batch/JobTreeView.cpp
 create mode 100644 qt/widgets/common/src/Batch/QtStandardItemTreeAdapter.cpp
 create mode 100644 qt/widgets/common/src/Batch/QtTreeCursorNavigation.cpp
 create mode 100644 qt/widgets/common/test/Batch/QtAdaptedModelTest.h

diff --git a/qt/widgets/common/CMakeLists.txt b/qt/widgets/common/CMakeLists.txt
index 1028f8cfc0b..4d738a7502a 100644
--- a/qt/widgets/common/CMakeLists.txt
+++ b/qt/widgets/common/CMakeLists.txt
@@ -78,6 +78,9 @@ set ( SRC_FILES
 	src/DataProcessorUI/QtDataProcessorOptionsDialog.cpp
 	src/DataProcessorUI/VectorString.cpp
   src/Batch/RowLocation.cpp
+  src/Batch/JobTreeView.cpp
+  src/Batch/QtStandardItemTreeAdapter.cpp
+  src/Batch/QtTreeCursorNavigation.cpp
 	src/DataSelector.cpp
 	src/DiagResults.cpp
 	src/DoubleSpinBox.cpp
@@ -178,6 +181,9 @@ set ( MOC_FILES
   inc/MantidQtWidgets/Common/AlgorithmSelectorWidget.h
   inc/MantidQtWidgets/Common/CheckboxHeader.h
   inc/MantidQtWidgets/Common/Batch/RowLocation.h
+  inc/MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h
+  inc/MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h
+  inc/MantidQtWidgets/Common/Batch/JobTreeView.h
   inc/MantidQtWidgets/Common/DataProcessorUI/AbstractTreeModel.h
   inc/MantidQtWidgets/Common/DataProcessorUI/QtCommandAdapter.h
   inc/MantidQtWidgets/Common/DataProcessorUI/GenericDataProcessorPresenter.h
@@ -532,6 +538,9 @@ set( TEST_FILES
   test/DataProcessorUI/TwoLevelTreeManagerTest.h
   test/DataProcessorUI/WhiteListTest.h
   test/DataProcessorUI/GenericDataProcessorPresenterTest.h
+
+  test/Batch/QtAdaptedModelTest.h
+
   test/ParseKeyValueStringTest.h
   test/DataProcessorUI/QOneLevelTreeModelTest.h
   test/DataProcessorUI/QTwoLevelTreeModelTest.h
diff --git a/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/JobTreeView.h b/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/JobTreeView.h
new file mode 100644
index 00000000000..9a901e70d15
--- /dev/null
+++ b/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/JobTreeView.h
@@ -0,0 +1,84 @@
+#ifndef MANTIDQTMANTIDWIDGETS_JOBTREEVIEW_H_
+#define MANTIDQTMANTIDWIDGETS_JOBTREEVIEW_H_
+#include "MantidQtWidgets/Common/DllOption.h"
+#include "MantidQtWidgets/Common/Batch/RowLocation.h"
+#include "MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h"
+#include "MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h"
+
+#include <QTreeView>
+
+namespace MantidQt {
+namespace MantidWidgets {
+namespace Batch {
+
+class IJobTreeViewSubscriber {
+public:
+  virtual void notifyCellChanged(RowLocation itemIndex, int column,
+                                 std::string newValue) = 0;
+  virtual void notifyRowInserted(RowLocation itemIndex) = 0;
+  virtual void notifyRowRemoved(RowLocation itemIndex) = 0;
+  virtual void
+  notifySelectedRowsChanged(std::vector<RowLocation> const &selection) = 0;
+};
+
+class EXPORT_OPT_MANTIDQT_COMMON JobTreeView : public QTreeView {
+  Q_OBJECT
+public:
+  // JobTreeView(QWidget *parent = nullptr);
+  JobTreeView(QStringList const &columnHeadings, QWidget *parent = nullptr);
+
+  void subscribe(IJobTreeViewSubscriber &subscriber);
+
+  void insertChildRowOf(RowLocation const &parent, int beforeRow,
+                        std::vector<std::string> const &rowText);
+  void insertChildRowOf(RowLocation const &parent, int beforeRow);
+  void appendChildRowOf(RowLocation const &parent);
+  void appendChildRowOf(RowLocation const &parentLocation,
+                        std::vector<std::string> const &rowText);
+
+  void removeRowAt(RowLocation const &location);
+
+  std::vector<std::string> rowTextAt(RowLocation const &location) const;
+  void setRowTextAt(RowLocation const &location,
+                    std::vector<std::string> const &rowText);
+
+  std::string textAt(RowLocation location, int column);
+  void setTextAt(RowLocation location, int column, std::string const &cellText);
+
+  QModelIndex moveCursor(CursorAction cursorAction,
+                         Qt::KeyboardModifiers modifiers) override;
+
+protected:
+  void keyPressEvent(QKeyEvent *event) override;
+  void setHeaderLabels(QStringList const &columnHeadings);
+
+private:
+  void make(QModelIndex const &){};
+
+  QModelIndex expanded(QModelIndex const &index);
+  QModelIndex editAt(QModelIndex const &index);
+
+  QModelIndex applyNavigationResult(QtTreeCursorNavigationResult const &result);
+  QModelIndex findOrMakeCellBelow(QModelIndex const &index);
+
+  QList<QStandardItem *>
+  rowFromRowText(std::vector<std::string> const &rowText) const;
+  std::vector<std::string> rowTextFromRow(QModelIndex firstCellIndex) const;
+
+  QModelIndex modelIndexAt(RowLocation const &location, int column = 0) const;
+  RowLocation rowLocationAt(QModelIndex const &index) const;
+  QStandardItem *modelItemAt(RowLocation const &location, int column = 0) const;
+
+  QStandardItem *modelItemFromIndex(QModelIndex const &location) const;
+
+  QtTreeCursorNavigation navigation() const;
+  QtStandardItemMutableTreeAdapter adaptedModel();
+  QtStandardItemTreeAdapter const adaptedModel() const;
+
+  IJobTreeViewSubscriber *m_notifyee;
+  QStandardItemModel m_model;
+};
+}
+}
+}
+#endif // MANTIDQTMANTIDWIDGETS_JOBTREEVIEW_H_
diff --git a/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h b/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h
new file mode 100644
index 00000000000..6c3487849c6
--- /dev/null
+++ b/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h
@@ -0,0 +1,55 @@
+#ifndef MANTIDQTMANTIDWIDGETS_TREEITEMMODELADAPTER_H_
+#define MANTIDQTMANTIDWIDGETS_TREEITEMMODELADAPTER_H_
+#include "MantidQtWidgets/Common/DllOption.h"
+#include <QStandardItemModel>
+
+namespace MantidQt {
+namespace MantidWidgets {
+namespace Batch {
+
+class EXPORT_OPT_MANTIDQT_COMMON QtStandardItemTreeAdapter {
+public:
+  QtStandardItemTreeAdapter(QStandardItemModel const &model);
+
+  QModelIndex rootModelIndex() const;
+
+  QStandardItem const *modelItemFromIndex(QModelIndex const &index) const;
+
+  QList<QStandardItem *>
+  rowFromRowText(std::vector<std::string> const &rowText) const;
+  std::vector<std::string> rowTextFromRow(QModelIndex firstCellIndex) const;
+
+  QList<QStandardItem *> emptyRow() const;
+
+private:
+  QStandardItemModel const &model() const;
+  QStandardItemModel const* m_model;
+};
+
+class EXPORT_OPT_MANTIDQT_COMMON QtStandardItemMutableTreeAdapter : public QtStandardItemTreeAdapter {
+public:
+  QtStandardItemMutableTreeAdapter(QStandardItemModel &model);
+
+  QModelIndex appendEmptySiblingRow(QModelIndex const &index);
+  QModelIndex appendSiblingRow(QModelIndex const &index,
+                               QList<QStandardItem *> cells);
+
+  QModelIndex appendEmptyChildRow(QModelIndex const &parent);
+  QModelIndex appendChildRow(QModelIndex const &parent,
+                             QList<QStandardItem *> cells);
+
+  QModelIndex insertChildRow(QModelIndex const &parent, int column,
+                             QList<QStandardItem *> cells);
+  QModelIndex insertEmptyChildRow(QModelIndex const &parent, int column);
+  QStandardItem *modelItemFromIndex(QModelIndex const &index);
+
+  void removeRowAt(QModelIndex const &index);
+
+private:
+  QStandardItemModel &model();
+  QStandardItemModel *m_model;
+};
+}
+}
+}
+#endif // MANTIDQTMANTIDWIDGETS_TREEITEMMODELADAPTER_H_
diff --git a/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h b/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h
new file mode 100644
index 00000000000..cf5a009f029
--- /dev/null
+++ b/qt/widgets/common/inc/MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h
@@ -0,0 +1,35 @@
+#ifndef MANTIDQTMANTIDWIDGETS_QTTREECURSORNAVIGATION_H_
+#define MANTIDQTMANTIDWIDGETS_QTTREECURSORNAVIGATION_H_
+#include <QModelIndex>
+#include <utility>
+namespace MantidQt {
+namespace MantidWidgets {
+
+using QtTreeCursorNavigationResult = std::pair<bool, QModelIndex>;
+class QtTreeCursorNavigation {
+public:
+  QtTreeCursorNavigation(QAbstractItemModel const* model);
+  QModelIndex moveCursorPrevious(QModelIndex const &currentIndex) const;
+  QtTreeCursorNavigationResult
+  moveCursorNext(QModelIndex const &currentIndex) const;
+
+  QModelIndex previousCellInThisRow(QModelIndex const &index) const;
+  QModelIndex lastCellInPreviousRow(QModelIndex const &index) const;
+  QModelIndex lastCellInParentRowElseNone(QModelIndex const &index) const;
+  QModelIndex firstCellOnNextRow(QModelIndex const &rowAbove) const;
+  QModelIndex nextCellOnThisRow(QModelIndex const &index) const;
+  QModelIndex lastRowInThisNode(QModelIndex const &index) const;
+
+  bool isNotLastCellOnThisRow(QModelIndex const &index) const;
+  bool isNotLastRowInThisNode(QModelIndex const &index) const;
+  bool isNotFirstCellInThisRow(QModelIndex const &index) const;
+  bool isNotFirstRowInThisNode(QModelIndex const &index) const;
+
+private:
+  QAbstractItemModel const* model;
+  QtTreeCursorNavigationResult withoutAppendedRow(QModelIndex const &index) const;
+  QtTreeCursorNavigationResult withAppendedRow(QModelIndex const& index) const;
+};
+}
+}
+#endif // MANTIDQTMANTIDWIDGETS_QTTREECURSORNAVIGATION_H_
diff --git a/qt/widgets/common/src/Batch/JobTreeView.cpp b/qt/widgets/common/src/Batch/JobTreeView.cpp
new file mode 100644
index 00000000000..db7bbdc74df
--- /dev/null
+++ b/qt/widgets/common/src/Batch/JobTreeView.cpp
@@ -0,0 +1,155 @@
+#include "MantidQtWidgets/Common/Batch/JobTreeView.h"
+#include "MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h"
+#include "MantidQtWidgets/Common/Batch/AssertOrThrow.h"
+#include <QKeyEvent>
+#include <QStandardItemModel>
+#include <QSortFilterProxyModel>
+namespace MantidQt {
+namespace MantidWidgets {
+namespace Batch {
+
+JobTreeView::JobTreeView(QStringList const &columnHeadings, QWidget *parent)
+    : QTreeView(parent), m_model(this) {
+  setModel(&m_model);
+  setHeaderLabels(columnHeadings);
+}
+
+void JobTreeView::setHeaderLabels(QStringList const &columnHeadings) {
+  m_model.setHorizontalHeaderLabels(columnHeadings);
+
+  for (auto i = 0; i < model()->columnCount(); ++i)
+    resizeColumnToContents(i);
+}
+
+void JobTreeView::subscribe(IJobTreeViewSubscriber &subscriber) {
+  m_notifyee = &subscriber;
+}
+
+QModelIndex JobTreeView::modelIndexAt(RowLocation const &location,
+                                      int column) const {
+  auto parentIndex = adaptedModel().rootModelIndex();
+  if (location.isRoot()) {
+    return parentIndex;
+  } else {
+    auto &path = location.path();
+    for (auto it = path.cbegin(); it != path.cend() - 1; ++it)
+      parentIndex = model()->index(*it, column, parentIndex);
+    assertOrThrow(
+        model()->hasIndex(location.rowRelativeToParent(), 0, parentIndex),
+        "modelIndexAt: Location refers to an index which does not "
+        "exist in the model.");
+    return model()->index(location.rowRelativeToParent(), column, parentIndex);
+  }
+}
+
+RowLocation JobTreeView::rowLocationAt(QModelIndex const &index) const {
+  if (index.isValid()) {
+    auto pathComponents = RowPath();
+    auto currentIndex = index;
+    while (currentIndex.isValid()) {
+      pathComponents.insert(pathComponents.begin(), currentIndex.row());
+      currentIndex = index.parent();
+    }
+    return RowLocation(pathComponents);
+  } else {
+    return RowLocation({});
+  }
+}
+
+void JobTreeView::removeRowAt(RowLocation const &location) {
+  adaptedModel().removeRowAt(modelIndexAt(location));
+}
+
+void JobTreeView::insertChildRowOf(RowLocation const &parent, int beforeRow) {
+  adaptedModel().insertEmptyChildRow(modelIndexAt(parent), beforeRow);
+}
+
+void JobTreeView::insertChildRowOf(RowLocation const &parent, int beforeRow,
+                                   std::vector<std::string> const &rowText) {
+  adaptedModel().insertChildRow(modelIndexAt(parent), beforeRow,
+                                adaptedModel().rowFromRowText(rowText));
+}
+
+void JobTreeView::appendChildRowOf(RowLocation const &parent) {
+  adaptedModel().appendEmptyChildRow(modelIndexAt(parent));
+}
+
+void JobTreeView::appendChildRowOf(RowLocation const &parent,
+                                   std::vector<std::string> const &rowText) {
+  adaptedModel().appendChildRow(modelIndexAt(parent),
+                                adaptedModel().rowFromRowText(rowText));
+}
+
+QModelIndex JobTreeView::editAt(QModelIndex const &index) {
+  clearSelection();
+  setCurrentIndex(index);
+  edit(index);
+  return index;
+}
+
+QModelIndex JobTreeView::expanded(QModelIndex const &index) {
+  auto expandAt = index;
+  while (expandAt.isValid()) {
+    setExpanded(expandAt, true);
+    expandAt = model()->parent(expandAt);
+  }
+  return index;
+}
+
+QtStandardItemMutableTreeAdapter JobTreeView::adaptedModel() {
+  return QtStandardItemMutableTreeAdapter(m_model);
+}
+
+QtStandardItemTreeAdapter const JobTreeView::adaptedModel() const {
+  return QtStandardItemTreeAdapter(m_model);
+}
+
+QtTreeCursorNavigation JobTreeView::navigation() const {
+  return QtTreeCursorNavigation(model());
+}
+
+QModelIndex JobTreeView::findOrMakeCellBelow(QModelIndex const &index) {
+  if (navigation().isNotLastRowInThisNode(index)) {
+    return moveCursor(QAbstractItemView::MoveDown, Qt::NoModifier);
+  } else {
+    return adaptedModel().appendEmptySiblingRow(index);
+  }
+}
+
+void JobTreeView::keyPressEvent(QKeyEvent *event) {
+  if (event->key() == Qt::Key_Return) {
+    event->accept();
+    if (event->modifiers() & Qt::ControlModifier)
+      editAt(adaptedModel().appendEmptyChildRow(currentIndex()));
+    else {
+      auto below = findOrMakeCellBelow(currentIndex());
+      editAt(expanded(below));
+    }
+  } else {
+    QTreeView::keyPressEvent(event);
+  }
+}
+
+QModelIndex
+JobTreeView::applyNavigationResult(QtTreeCursorNavigationResult const &result) {
+  auto shouldMakeNewRowBelow = result.first;
+  if (shouldMakeNewRowBelow)
+    return expanded(adaptedModel().appendEmptySiblingRow(result.second));
+  else
+    return result.second;
+}
+
+QModelIndex JobTreeView::moveCursor(CursorAction cursorAction,
+                                    Qt::KeyboardModifiers modifiers) {
+  if (cursorAction == QAbstractItemView::MoveNext) {
+    return applyNavigationResult(navigation().moveCursorNext(currentIndex()));
+  } else if (cursorAction == QAbstractItemView::MovePrevious) {
+    return navigation().moveCursorPrevious(currentIndex());
+  } else {
+    return QTreeView::moveCursor(cursorAction, modifiers);
+  }
+}
+
+} // namespace Batch
+} // namespace MantidWidgets
+} // namespace MantidQt
diff --git a/qt/widgets/common/src/Batch/QtStandardItemTreeAdapter.cpp b/qt/widgets/common/src/Batch/QtStandardItemTreeAdapter.cpp
new file mode 100644
index 00000000000..b33a22793f6
--- /dev/null
+++ b/qt/widgets/common/src/Batch/QtStandardItemTreeAdapter.cpp
@@ -0,0 +1,124 @@
+#include "MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h"
+#include "MantidQtWidgets/Common/Batch/AssertOrThrow.h"
+
+namespace MantidQt {
+namespace MantidWidgets {
+namespace Batch {
+
+QtStandardItemMutableTreeAdapter::QtStandardItemMutableTreeAdapter(
+    QStandardItemModel &model)
+    : QtStandardItemTreeAdapter(model), m_model(&model) {}
+
+QtStandardItemTreeAdapter::QtStandardItemTreeAdapter(
+    QStandardItemModel const &model)
+    : m_model(&model) {}
+
+QModelIndex QtStandardItemTreeAdapter::rootModelIndex() const {
+  return QModelIndex();
+}
+
+QStandardItem const *
+QtStandardItemTreeAdapter::modelItemFromIndex(QModelIndex const &index) const {
+  if (index.isValid()) {
+    auto *item = model().itemFromIndex(index);
+    assertOrThrow(item != nullptr,
+                  "modelItemFromIndex: Index must point to a valid item.");
+    return item;
+  } else
+    return model().invisibleRootItem();
+}
+
+QStandardItem *
+QtStandardItemMutableTreeAdapter::modelItemFromIndex(QModelIndex const &index) {
+  if (index.isValid()) {
+    auto *item = model().itemFromIndex(index);
+    assertOrThrow(item != nullptr,
+                  "modelItemFromIndex: Index must point to a valid item.");
+    return item;
+  } else
+    return model().invisibleRootItem();
+}
+
+void QtStandardItemMutableTreeAdapter::removeRowAt(QModelIndex const &index) {
+  if (index.isValid()) {
+    auto *parent = modelItemFromIndex(model().parent(index));
+    parent->removeRow(index.row());
+  } else {
+    model().removeRows(0, model().rowCount());
+  }
+}
+
+QModelIndex QtStandardItemMutableTreeAdapter::appendEmptySiblingRow(
+    QModelIndex const &index) {
+  return appendEmptyChildRow(model().parent(index));
+}
+
+QModelIndex QtStandardItemMutableTreeAdapter::appendSiblingRow(
+    QModelIndex const &index, QList<QStandardItem *> cells) {
+  return appendChildRow(model().parent(index), cells);
+}
+
+QModelIndex QtStandardItemMutableTreeAdapter::appendEmptyChildRow(
+    QModelIndex const &parent) {
+  return appendChildRow(parent, emptyRow());
+}
+
+QModelIndex
+QtStandardItemMutableTreeAdapter::appendChildRow(QModelIndex const &parent,
+                                                 QList<QStandardItem *> cells) {
+  auto *const parentItem = modelItemFromIndex(parent);
+  parentItem->appendRow(cells);
+  return model().index(model().rowCount(parent) - 1, 0, parent);
+}
+
+QModelIndex QtStandardItemMutableTreeAdapter::insertChildRow(
+    QModelIndex const &parent, int row, QList<QStandardItem *> cells) {
+  auto *const parentItem = modelItemFromIndex(parent);
+  parentItem->insertRow(row, cells);
+  return model().index(row, 0, parent);
+}
+
+QModelIndex
+QtStandardItemMutableTreeAdapter::insertEmptyChildRow(QModelIndex const &parent,
+                                                      int row) {
+  return insertChildRow(parent, row, emptyRow());
+}
+
+QList<QStandardItem *> QtStandardItemTreeAdapter::emptyRow() const {
+  auto cells = QList<QStandardItem *>();
+  for (auto i = 0; i < model().columnCount(); ++i)
+    cells.append(new QStandardItem(""));
+  return cells;
+}
+
+QList<QStandardItem *> QtStandardItemTreeAdapter::rowFromRowText(
+    std::vector<std::string> const &rowText) const {
+  auto rowCells = QList<QStandardItem *>();
+  for (auto &cellText : rowText)
+    rowCells.append(new QStandardItem(QString::fromStdString(cellText)));
+  return rowCells;
+}
+
+std::vector<std::string>
+QtStandardItemTreeAdapter::rowTextFromRow(QModelIndex firstCellIndex) const {
+  auto rowText = std::vector<std::string>();
+  rowText.reserve(model().columnCount());
+
+  for (auto i = 0; i < model().columnCount(); i++) {
+    auto *cell =
+        modelItemFromIndex(firstCellIndex.sibling(firstCellIndex.row(), i));
+    rowText.emplace_back(cell->text().toStdString());
+  }
+  return rowText;
+}
+
+QStandardItemModel const &QtStandardItemTreeAdapter::model() const {
+  return *m_model;
+}
+
+QStandardItemModel &QtStandardItemMutableTreeAdapter::model() {
+  return *m_model;
+}
+}
+}
+}
diff --git a/qt/widgets/common/src/Batch/QtTreeCursorNavigation.cpp b/qt/widgets/common/src/Batch/QtTreeCursorNavigation.cpp
new file mode 100644
index 00000000000..9a86691d343
--- /dev/null
+++ b/qt/widgets/common/src/Batch/QtTreeCursorNavigation.cpp
@@ -0,0 +1,98 @@
+#include "MantidQtWidgets/Common/Batch/QtTreeCursorNavigation.h"
+
+namespace MantidQt {
+namespace MantidWidgets {
+
+QtTreeCursorNavigation::QtTreeCursorNavigation(QAbstractItemModel const *model) : model(model) {}
+
+QtTreeCursorNavigationResult
+QtTreeCursorNavigation::withoutAppendedRow(QModelIndex const &index) const {
+  return std::make_pair(false, index);
+}
+
+QtTreeCursorNavigationResult
+QtTreeCursorNavigation::withAppendedRow(QModelIndex const &index) const {
+  return std::make_pair(true, index);
+}
+
+std::pair<bool, QModelIndex>
+QtTreeCursorNavigation::moveCursorNext(QModelIndex const &currentIndex) const {
+  if (currentIndex.isValid()) {
+    if (isNotLastCellOnThisRow(currentIndex))
+      return withoutAppendedRow(nextCellOnThisRow(currentIndex));
+    else if (isNotLastRowInThisNode(currentIndex))
+      return withoutAppendedRow(firstCellOnNextRow(currentIndex));
+    else
+      return withAppendedRow(currentIndex);
+  } else
+    return withoutAppendedRow(QModelIndex());
+}
+
+QModelIndex QtTreeCursorNavigation::moveCursorPrevious(
+    QModelIndex const &currentIndex) const {
+  if (currentIndex.isValid()) {
+    if (isNotFirstCellInThisRow(currentIndex))
+      return previousCellInThisRow(currentIndex);
+    else if (isNotFirstRowInThisNode(currentIndex))
+      return lastCellInPreviousRow(currentIndex);
+    else
+      return lastCellInParentRowElseNone(currentIndex);
+  } else
+    return QModelIndex();
+}
+
+bool QtTreeCursorNavigation::isNotFirstCellInThisRow(
+    QModelIndex const &index) const {
+  return index.column() > 0;
+}
+
+bool QtTreeCursorNavigation::isNotFirstRowInThisNode(
+    QModelIndex const &index) const {
+  return index.row() > 0;
+}
+
+QModelIndex
+QtTreeCursorNavigation::previousCellInThisRow(QModelIndex const &index) const {
+  return index.sibling(index.row(), index.column() - 1);
+}
+
+QModelIndex
+QtTreeCursorNavigation::lastCellInPreviousRow(QModelIndex const &index) const {
+  return index.sibling(index.row() - 1, model->columnCount() - 1);
+}
+
+QModelIndex
+QtTreeCursorNavigation::lastRowInThisNode(QModelIndex const &parent) const {
+  return model->index(model->rowCount(parent) - 1, 0, parent);
+}
+
+QModelIndex QtTreeCursorNavigation::lastCellInParentRowElseNone(
+    QModelIndex const &index) const {
+  auto parent = model->parent(index);
+  if (parent.isValid())
+    return parent.sibling(parent.row(), model->columnCount() - 1);
+  else
+    return QModelIndex();
+}
+
+QModelIndex
+QtTreeCursorNavigation::firstCellOnNextRow(QModelIndex const &index) const {
+  return index.sibling(index.row() + 1, 0);
+}
+
+QModelIndex
+QtTreeCursorNavigation::nextCellOnThisRow(QModelIndex const &index) const {
+  return index.sibling(index.row(), index.column() + 1);
+}
+
+bool QtTreeCursorNavigation::isNotLastCellOnThisRow(
+    QModelIndex const &index) const {
+  return index.column() + 1 < model->columnCount();
+}
+
+bool QtTreeCursorNavigation::isNotLastRowInThisNode(
+    QModelIndex const &index) const {
+  return index.row() + 1 < model->rowCount(index.parent());
+}
+}
+}
diff --git a/qt/widgets/common/test/Batch/QtAdaptedModelTest.h b/qt/widgets/common/test/Batch/QtAdaptedModelTest.h
new file mode 100644
index 00000000000..3b5ed7decc8
--- /dev/null
+++ b/qt/widgets/common/test/Batch/QtAdaptedModelTest.h
@@ -0,0 +1,127 @@
+#ifndef MANTID_MANTIDWIDGETS_QTADAPTEDMODELTEST_H
+#define MANTID_MANTIDWIDGETS_QTADAPTEDMODELTEST_H
+
+#include <cxxtest/TestSuite.h>
+#include <gtest/gtest.h>
+#include <QModelIndex>
+#include <QStandardItemModel>
+#include "MantidKernel/make_unique.h"
+#include "MantidQtWidgets/Common/Batch/QtStandardItemTreeAdapter.h"
+
+using namespace MantidQt::MantidWidgets;
+using namespace MantidQt::MantidWidgets::Batch;
+using namespace testing;
+
+class QtAdaptedModelTest : public CxxTest::TestSuite {
+public:
+  // This pair of boilerplate methods prevent the suite being created statically
+  // This means the constructor isn't called when running other tests
+  static QtAdaptedModelTest *createSuite() { return new QtAdaptedModelTest(); }
+  static void destroySuite(QtAdaptedModelTest *suite) { delete suite; }
+
+  QtStandardItemMutableTreeAdapter adapt(QStandardItemModel *model) {
+    return QtStandardItemMutableTreeAdapter(*model);
+  }
+
+  std::unique_ptr<QStandardItemModel> emptyModel() const {
+    return Mantid::Kernel::make_unique<QStandardItemModel>();
+  }
+
+  void testInvalidIndexIsRoot() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    TS_ASSERT_EQUALS(adaptedModel.rootModelIndex(), QModelIndex());
+  }
+
+  void testCanGetRootItemFromRootIndex() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    auto rootIndex = QModelIndex();
+    TS_ASSERT_EQUALS(adaptedModel.modelItemFromIndex(rootIndex),
+                     model->invisibleRootItem());
+  }
+
+  void testAppendChildNode() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    auto *expectedChildItem = new QStandardItem("Some Dummy Text");
+    adaptedModel.appendChildRow(QModelIndex(), {expectedChildItem});
+
+    TS_ASSERT_EQUALS(expectedChildItem, model->invisibleRootItem()->child(0));
+  }
+
+  void testInsertChildNodeBetweenTwoSiblings() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    auto *sibling0 = new QStandardItem("Sibling 0");
+    auto *sibling1 = new QStandardItem("Sibling 1");
+
+    auto *rootItem = model->invisibleRootItem();
+    rootItem->appendRow({sibling0});
+    rootItem->appendRow({sibling1});
+
+    auto *newSibling = new QStandardItem("Some Dummy Text");
+
+    adaptedModel.insertChildRow(QModelIndex(), 1, {newSibling});
+    TS_ASSERT_EQUALS(newSibling, model->invisibleRootItem()->child(1));
+  }
+
+  void testAppendSiblingNodeAfterSiblings() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    auto *sibling0 = new QStandardItem("Sibling 0");
+    auto *sibling1 = new QStandardItem("Sibling 1");
+
+    auto *rootItem = model->invisibleRootItem();
+    rootItem->appendRow({sibling0});
+    rootItem->appendRow({sibling1});
+
+    auto rootIndex = QModelIndex();
+    auto sibling0Index = model->index(/*row=*/0, /*column=*/0, rootIndex);
+    auto *newSibling = new QStandardItem("Some Dummy Text");
+
+    adaptedModel.appendSiblingRow(sibling0Index, {newSibling});
+    TS_ASSERT_EQUALS(newSibling, model->invisibleRootItem()->child(2));
+  }
+
+  void testCanRowTextCorrectForAppendedRow() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    auto const firstCellText = QString("First Cell");
+    auto *firstCell = new QStandardItem(firstCellText);
+    auto const secondCellText = QString("Second Cell");
+    auto *secondCell = new QStandardItem(secondCellText);
+
+    auto *rootItem = model->invisibleRootItem();
+    rootItem->appendRow({firstCell, secondCell});
+
+    auto rootIndex = QModelIndex();
+    auto childRowIndex = model->index(/*row=*/0, /*column=*/0, rootIndex);
+
+    auto rowText = adaptedModel.rowTextFromRow(childRowIndex);
+
+    TS_ASSERT_EQUALS(rowText.size(), 2u);
+    TS_ASSERT_EQUALS(firstCellText, QString::fromStdString(rowText[0]));
+    TS_ASSERT_EQUALS(secondCellText, QString::fromStdString(rowText[1]));
+  }
+
+  void testCanCreateCellsFromStringVector() {
+    auto model = emptyModel();
+    auto adaptedModel = adapt(model.get());
+
+    auto const rowText = std::vector<std::string>({"First Cell", "Second Cell"});
+    auto const row = adaptedModel.rowFromRowText(rowText);
+
+    TS_ASSERT_EQUALS(row.size(), 2u);
+    TS_ASSERT_EQUALS(rowText[0], row[0]->text().toStdString());
+    TS_ASSERT_EQUALS(rowText[1], row[1]->text().toStdString());
+  }
+};
+
+#endif // MANTID_MANTIDWIDGETS_QTADAPTEDMODELTEST_H
-- 
GitLab