diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..47addbf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required( VERSION 2.8 ) +project(qt-gui) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_CXX_FLAGS "${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS}") + +set( CMAKE_BUILD_TYPE Debug ) + +add_definitions ( -Wall ) + +find_package ( Qt5 REQUIRED Quick Gui Core Qml Widgets Test) + +include_directories ( + ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} + ${Qt5Quick_INCLUDE_DIRS} ${Qt5Gui_INCLUDE_DIRS} ${Qt5Core_INCLUDE_DIRS} ${Qt5Qml_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS} ${Qt5Test_INCLUDE_DIRS} + /usr/include/elektra + /usr/local/include/elektra) + +set ( qt-gui_HDRS + src/visitor.hpp + src/printvisitor.hpp + src/keysetvisitor.hpp + src/newkeycommand.hpp + src/editkeycommand.hpp + src/deletekeycommand.hpp + src/cutkeycommand.hpp + src/copykeycommand.hpp + ) + +set ( qt-gui_SRCS + src/main.cpp + src/treeviewmodel.cpp + src/confignode.cpp + src/printvisitor.cpp + src/keysetvisitor.cpp + modeltest/dynamictreemodel.cpp + modeltest/modeltest.cpp + src/undomanager.cpp + src/newkeycommand.cpp + src/deletekeycommand.cpp + src/editkeycommand.cpp + src/copykeycommand.cpp + src/cutkeycommand.cpp + ) + +set ( qt-gui_RSCS + resources.qrc + ) + +qt5_add_resources(RSCS ${qt-gui_RSCS}) + +add_executable ( qt-gui ${qt-gui_SRCS} ${UIS} ${RSCS} ${TRS} ) + +qt5_use_modules(qt-gui Quick Gui Core Qml Widgets) + + target_link_libraries ( qt-gui ${Qt5Quick_LIBRARIES} ${Qt5Gui_LIBRARIES} ${Qt5Core_LIBRARIES} ${Qt5Qml_LIBRARIES} ${Qt5Widgets_LIBRARIES} ${Qt5Test_LIBRARIES} elektra) diff --git a/modeltest/dynamictreemodel.cpp b/modeltest/dynamictreemodel.cpp new file mode 100644 index 0000000..f667fa3 --- /dev/null +++ b/modeltest/dynamictreemodel.cpp @@ -0,0 +1,402 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Stephen Kelly +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "dynamictreemodel.h" + +#include +#include +#include +#include + + +DynamicTreeModel::DynamicTreeModel(QObject *parent) + : QAbstractItemModel(parent), + nextId(1) +{ +} + +QModelIndex DynamicTreeModel::index(int row, int column, const QModelIndex &parent) const +{ +// if (column != 0) +// return QModelIndex(); + + + if ( column < 0 || row < 0 ) + return QModelIndex(); + + QList > childIdColumns = m_childItems.value(parent.internalId()); + + const qint64 grandParent = findParentId(parent.internalId()); + if (grandParent >= 0) { + QList > parentTable = m_childItems.value(grandParent); + if (parent.column() >= parentTable.size()) + qFatal("%s: parent.column() must be less than parentTable.size()", Q_FUNC_INFO); + QList parentSiblings = parentTable.at(parent.column()); + if (parent.row() >= parentSiblings.size()) + qFatal("%s: parent.row() must be less than parentSiblings.size()", Q_FUNC_INFO); + } + + if (childIdColumns.size() == 0) + return QModelIndex(); + + if (column >= childIdColumns.size()) + return QModelIndex(); + + QList rowIds = childIdColumns.at(column); + + if ( row >= rowIds.size()) + return QModelIndex(); + + qint64 id = rowIds.at(row); + + return createIndex(row, column, reinterpret_cast(id)); + +} + +qint64 DynamicTreeModel::findParentId(qint64 searchId) const +{ + if (searchId <= 0) + return -1; + + QHashIterator > > i(m_childItems); + while (i.hasNext()) + { + i.next(); + QListIterator > j(i.value()); + while (j.hasNext()) + { + QList l = j.next(); + if (l.contains(searchId)) + { + return i.key(); + } + } + } + return -1; +} + +QModelIndex DynamicTreeModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + qint64 searchId = index.internalId(); + qint64 parentId = findParentId(searchId); + // Will never happen for valid index, but what the hey... + if (parentId <= 0) + return QModelIndex(); + + qint64 grandParentId = findParentId(parentId); + if (grandParentId < 0) + grandParentId = 0; + + int column = 0; + QList childList = m_childItems.value(grandParentId).at(column); + + int row = childList.indexOf(parentId); + + return createIndex(row, column, reinterpret_cast(parentId)); + +} + +int DynamicTreeModel::rowCount(const QModelIndex &index ) const +{ + QList > cols = m_childItems.value(index.internalId()); + + if (cols.size() == 0 ) + return 0; + + if (index.column() > 0) + return 0; + + return cols.at(0).size(); +} + +int DynamicTreeModel::columnCount(const QModelIndex &index ) const +{ +// Q_UNUSED(index); + return m_childItems.value(index.internalId()).size(); +} + +QVariant DynamicTreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (Qt::DisplayRole == role) + { + return m_items.value(index.internalId()); + } + return QVariant(); +} + +void DynamicTreeModel::clear() +{ + beginResetModel(); + m_items.clear(); + m_childItems.clear(); + nextId = 1; + endResetModel(); +} + + +ModelChangeCommand::ModelChangeCommand( DynamicTreeModel *model, QObject *parent ) + : QObject(parent), m_model(model), m_numCols(1), m_startRow(-1), m_endRow(-1) +{ + +} + +QModelIndex ModelChangeCommand::findIndex(QList rows) +{ + const int col = 0; + QModelIndex parent = QModelIndex(); + QListIterator i(rows); + while (i.hasNext()) + { + parent = m_model->index(i.next(), col, parent); + if (!parent.isValid()) + qFatal("%s: parent must be valid", Q_FUNC_INFO); + } + return parent; +} + +ModelInsertCommand::ModelInsertCommand(DynamicTreeModel *model, QObject *parent ) + : ModelChangeCommand(model, parent) +{ + +} + +void ModelInsertCommand::doCommand() +{ + QModelIndex parent = findIndex(m_rowNumbers); + m_model->beginInsertRows(parent, m_startRow, m_endRow); + qint64 parentId = parent.internalId(); + for (int row = m_startRow; row <= m_endRow; row++) + { + for(int col = 0; col < m_numCols; col++ ) + { + if (m_model->m_childItems[parentId].size() <= col) + { + m_model->m_childItems[parentId].append(QList()); + } +// QString name = QUuid::createUuid().toString(); + qint64 id = m_model->newId(); + QString name = QString::number(id); + + m_model->m_items.insert(id, name); + m_model->m_childItems[parentId][col].insert(row, id); + + } + } + m_model->endInsertRows(); +} + + +ModelMoveCommand::ModelMoveCommand(DynamicTreeModel *model, QObject *parent) + : ModelChangeCommand(model, parent) +{ + +} +bool ModelMoveCommand::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) +{ + return m_model->beginMoveRows(srcParent, srcStart, srcEnd, destParent, destRow); +} + +void ModelMoveCommand::doCommand() +{ + QModelIndex srcParent = findIndex(m_rowNumbers); + QModelIndex destParent = findIndex(m_destRowNumbers); + + if (!emitPreSignal(srcParent, m_startRow, m_endRow, destParent, m_destRow)) + { + return; + } + + for (int column = 0; column < m_numCols; ++column) + { + QList l = m_model->m_childItems.value(srcParent.internalId())[column].mid(m_startRow, m_endRow - m_startRow + 1 ); + + for (int i = m_startRow; i <= m_endRow ; i++) + { + m_model->m_childItems[srcParent.internalId()][column].removeAt(m_startRow); + } + int d; + if (m_destRow < m_startRow) + d = m_destRow; + else + { + if (srcParent == destParent) + d = m_destRow - (m_endRow - m_startRow + 1); + else + d = m_destRow - (m_endRow - m_startRow) + 1; + } + + foreach(const qint64 id, l) + { + m_model->m_childItems[destParent.internalId()][column].insert(d++, id); + } + } + + emitPostSignal(); +} + +void ModelMoveCommand::emitPostSignal() +{ + m_model->endMoveRows(); +} + +ModelResetCommand::ModelResetCommand(DynamicTreeModel* model, QObject* parent) + : ModelMoveCommand(model, parent) +{ + +} + +ModelResetCommand::~ModelResetCommand() +{ + +} + +bool ModelResetCommand::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) +{ + Q_UNUSED(srcParent); + Q_UNUSED(srcStart); + Q_UNUSED(srcEnd); + Q_UNUSED(destParent); + Q_UNUSED(destRow); + + return true; +} + +void ModelResetCommand::emitPostSignal() +{ + //m_model->reset(); +} + +ModelResetCommandFixed::ModelResetCommandFixed(DynamicTreeModel* model, QObject* parent) + : ModelMoveCommand(model, parent) +{ + +} + +ModelResetCommandFixed::~ModelResetCommandFixed() +{ + +} + +bool ModelResetCommandFixed::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) +{ + Q_UNUSED(srcParent); + Q_UNUSED(srcStart); + Q_UNUSED(srcEnd); + Q_UNUSED(destParent); + Q_UNUSED(destRow); + + m_model->beginResetModel(); + return true; +} + +void ModelResetCommandFixed::emitPostSignal() +{ + m_model->endResetModel(); +} + +ModelChangeChildrenLayoutsCommand::ModelChangeChildrenLayoutsCommand(DynamicTreeModel* model, QObject* parent) + : ModelChangeCommand(model, parent) +{ + +} + +void ModelChangeChildrenLayoutsCommand::doCommand() +{ + const QPersistentModelIndex parent1 = findIndex(m_rowNumbers); + const QPersistentModelIndex parent2 = findIndex(m_secondRowNumbers); + + QList parents; + parents << parent1; + parents << parent2; + + emit m_model->layoutAboutToBeChanged(parents); + + int rowSize1 = -1; + int rowSize2 = -1; + + for (int column = 0; column < m_numCols; ++column) + { + { + QList &l = m_model->m_childItems[parent1.internalId()][column]; + rowSize1 = l.size(); + l.prepend(l.takeLast()); + } + { + QList &l = m_model->m_childItems[parent2.internalId()][column]; + rowSize2 = l.size(); + l.append(l.takeFirst()); + } + } + + // If we're changing one of the parent indexes, we need to ensure that we do that before + // changing any children of that parent. The reason is that we're keeping parent1 and parent2 + // around as QPersistentModelIndex instances, and we query idx.parent() in the loop. + QModelIndexList persistent = m_model->persistentIndexList(); + foreach (const QModelIndex &parent, parents) { + int idx = persistent.indexOf(parent); + if (idx != -1) + persistent.move(idx, 0); + } + + foreach (const QModelIndex &idx, persistent) { + if (idx.parent() == parent1) { + if (idx.row() == rowSize1 - 1) { + m_model->changePersistentIndex(idx, m_model->createIndex(0, idx.column(), idx.internalPointer())); + } else { + m_model->changePersistentIndex(idx, m_model->createIndex(idx.row() + 1, idx.column(), idx.internalPointer())); + } + } else if (idx.parent() == parent2) { + if (idx.row() == 0) { + m_model->changePersistentIndex(idx, m_model->createIndex(rowSize2 - 1, idx.column(), idx.internalPointer())); + } else { + m_model->changePersistentIndex(idx, m_model->createIndex(idx.row() - 1, idx.column(), idx.internalPointer())); + } + } + } + + emit m_model->layoutChanged(parents); +} diff --git a/modeltest/dynamictreemodel.h b/modeltest/dynamictreemodel.h new file mode 100644 index 0000000..8fd2a54 --- /dev/null +++ b/modeltest/dynamictreemodel.h @@ -0,0 +1,214 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Stephen Kelly +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef DYNAMICTREEMODEL_H +#define DYNAMICTREEMODEL_H + +#include + +#include +#include + + +class DynamicTreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + DynamicTreeModel(QObject *parent = 0); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; + int rowCount(const QModelIndex &index = QModelIndex()) const; + int columnCount(const QModelIndex &index = QModelIndex()) const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + void clear(); + +protected slots: + + /** + Finds the parent id of the string with id @p searchId. + + Returns -1 if not found. + */ + qint64 findParentId(qint64 searchId) const; + +private: + QHash m_items; + QHash > > m_childItems; + qint64 nextId; + qint64 newId() { return nextId++; }; + + QModelIndex m_nextParentIndex; + int m_nextRow; + + int m_depth; + int maxDepth; + + friend class ModelInsertCommand; + friend class ModelMoveCommand; + friend class ModelResetCommand; + friend class ModelResetCommandFixed; + friend class ModelChangeChildrenLayoutsCommand; + +}; + + +class ModelChangeCommand : public QObject +{ + Q_OBJECT +public: + + ModelChangeCommand( DynamicTreeModel *model, QObject *parent = 0 ); + + virtual ~ModelChangeCommand() {} + + void setAncestorRowNumbers(QList rowNumbers) { m_rowNumbers = rowNumbers; } + + QModelIndex findIndex(QList rows); + + void setStartRow(int row) { m_startRow = row; } + + void setEndRow(int row) { m_endRow = row; } + + void setNumCols(int cols) { m_numCols = cols; } + + virtual void doCommand() = 0; + +protected: + DynamicTreeModel* m_model; + QList m_rowNumbers; + int m_numCols; + int m_startRow; + int m_endRow; + +}; + +typedef QList ModelChangeCommandList; + +class ModelInsertCommand : public ModelChangeCommand +{ + Q_OBJECT + +public: + + ModelInsertCommand(DynamicTreeModel *model, QObject *parent = 0 ); + virtual ~ModelInsertCommand() {} + + virtual void doCommand(); +}; + + +class ModelMoveCommand : public ModelChangeCommand +{ + Q_OBJECT +public: + ModelMoveCommand(DynamicTreeModel *model, QObject *parent); + + virtual ~ModelMoveCommand() {} + + virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); + + virtual void doCommand(); + + virtual void emitPostSignal(); + + void setDestAncestors( QList rows ) { m_destRowNumbers = rows; } + + void setDestRow(int row) { m_destRow = row; } + +protected: + QList m_destRowNumbers; + int m_destRow; +}; + +/** + A command which does a move and emits a reset signal. +*/ +class ModelResetCommand : public ModelMoveCommand +{ + Q_OBJECT +public: + ModelResetCommand(DynamicTreeModel* model, QObject* parent = 0); + + virtual ~ModelResetCommand(); + + virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); + virtual void emitPostSignal(); + +}; + +/** + A command which does a move and emits a beginResetModel and endResetModel signals. +*/ +class ModelResetCommandFixed : public ModelMoveCommand +{ + Q_OBJECT +public: + ModelResetCommandFixed(DynamicTreeModel* model, QObject* parent = 0); + + virtual ~ModelResetCommandFixed(); + + virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); + virtual void emitPostSignal(); + +}; + +class ModelChangeChildrenLayoutsCommand : public ModelChangeCommand +{ + Q_OBJECT +public: + ModelChangeChildrenLayoutsCommand(DynamicTreeModel *model, QObject *parent); + + virtual ~ModelChangeChildrenLayoutsCommand() {} + + virtual void doCommand(); + + void setSecondAncestorRowNumbers( QList rows ) { m_secondRowNumbers = rows; } + +protected: + QList m_secondRowNumbers; + int m_destRow; +}; + +#endif diff --git a/modeltest/modeltest.cpp b/modeltest/modeltest.cpp new file mode 100644 index 0000000..a5233c4 --- /dev/null +++ b/modeltest/modeltest.cpp @@ -0,0 +1,598 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "modeltest.h" + +#include +#include + +/*! + Connect to all of the models signals. Whenever anything happens recheck everything. +*/ +ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false ) +{ + if (!model) + qFatal("%s: model must not be null", Q_FUNC_INFO); + + connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + + // Special checks for changes + connect(model, SIGNAL(layoutAboutToBeChanged()), + this, SLOT(layoutAboutToBeChanged()) ); + connect(model, SIGNAL(layoutChanged()), + this, SLOT(layoutChanged()) ); + + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex)) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(headerDataChanged(Qt::Orientation,int,int)) ); + + runAllTests(); +} + +void ModelTest::runAllTests() +{ + if ( fetchingMore ) + return; + nonDestructiveBasicTest(); + rowCount(); + columnCount(); + hasIndex(); + index(); + parent(); + data(); +} + +/*! + nonDestructiveBasicTest tries to call a number of the basic functions (not all) + to make sure the model doesn't outright segfault, testing the functions that makes sense. +*/ +void ModelTest::nonDestructiveBasicTest() +{ + QVERIFY( model->buddy ( QModelIndex() ) == QModelIndex() ); + model->canFetchMore ( QModelIndex() ); + QVERIFY( model->columnCount ( QModelIndex() ) >= 0 ); + QVERIFY( model->data ( QModelIndex() ) == QVariant() ); + fetchingMore = true; + model->fetchMore ( QModelIndex() ); + fetchingMore = false; + Qt::ItemFlags flags = model->flags ( QModelIndex() ); + QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 ); + model->hasChildren ( QModelIndex() ); + model->hasIndex ( 0, 0 ); + model->headerData ( 0, Qt::Horizontal ); + model->index ( 0, 0 ); + model->itemData ( QModelIndex() ); + QVariant cache; + model->match ( QModelIndex(), -1, cache ); + model->mimeTypes(); + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + QVERIFY( model->rowCount() >= 0 ); + QVariant variant; + model->setData ( QModelIndex(), variant, -1 ); + model->setHeaderData ( -1, Qt::Horizontal, QVariant() ); + model->setHeaderData ( 999999, Qt::Horizontal, QVariant() ); + QMap roles; + model->sibling ( 0, 0, QModelIndex() ); + model->span ( QModelIndex() ); + model->supportedDropActions(); +} + +/*! + Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() + + Models that are dynamically populated are not as fully tested here. + */ +void ModelTest::rowCount() +{ +// qDebug() << "rc"; + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + int rows = model->rowCount ( topIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( topIndex ) ); + + QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex ); + if ( secondLevelIndex.isValid() ) { // not the top level + // check a row count where parent is valid + rows = model->rowCount ( secondLevelIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( secondLevelIndex ) ); + } + + // The models rowCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() + */ +void ModelTest::columnCount() +{ + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->columnCount ( topIndex ) >= 0 ); + + // check a column count where parent is valid + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + if ( childIndex.isValid() ) + QVERIFY( model->columnCount ( childIndex ) >= 0 ); + + // columnCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::hasIndex() + */ +void ModelTest::hasIndex() +{ +// qDebug() << "hi"; + // Make sure that invalid values returns an invalid index + QVERIFY( !model->hasIndex ( -2, -2 ) ); + QVERIFY( !model->hasIndex ( -2, 0 ) ); + QVERIFY( !model->hasIndex ( 0, -2 ) ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + // check out of bounds + QVERIFY( !model->hasIndex ( rows, columns ) ); + QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) ); + + if ( rows > 0 ) + QVERIFY( model->hasIndex ( 0, 0 ) ); + + // hasIndex() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::index() + */ +void ModelTest::index() +{ +// qDebug() << "i"; + // Make sure that invalid values returns an invalid index + QVERIFY( model->index ( -2, -2 ) == QModelIndex() ); + QVERIFY( model->index ( -2, 0 ) == QModelIndex() ); + QVERIFY( model->index ( 0, -2 ) == QModelIndex() ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + if ( rows == 0 ) + return; + + // Catch off by one errors + QVERIFY( model->index ( rows, columns ) == QModelIndex() ); + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // Make sure that the same index is *always* returned + QModelIndex a = model->index ( 0, 0 ); + QModelIndex b = model->index ( 0, 0 ); + QVERIFY( a == b ); + + // index() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::parent() + */ +void ModelTest::parent() +{ +// qDebug() << "p"; + // Make sure the model won't crash and will return an invalid QModelIndex + // when asked for the parent of an invalid index. + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + + if ( model->rowCount() == 0 ) + return; + + // Column 0 | Column 1 | + // QModelIndex() | | + // \- topIndex | topIndex1 | + // \- childIndex | childIndex1 | + + // Common error test #1, make sure that a top level index has a parent + // that is a invalid QModelIndex. + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->parent ( topIndex ) == QModelIndex() ); + + // Common error test #2, make sure that a second level index has a parent + // that is the first level index. + if ( model->rowCount ( topIndex ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QVERIFY( model->parent ( childIndex ) == topIndex ); + } + + // Common error test #3, the second column should NOT have the same children + // as the first column in a row. + // Usually the second column shouldn't have children. + QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() ); + if ( model->rowCount ( topIndex1 ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 ); + QVERIFY( childIndex != childIndex1 ); + } + + // Full test, walk n levels deep through the model making sure that all + // parent's children correctly specify their parent. + checkChildren ( QModelIndex() ); +} + +/*! + Called from the parent() test. + + A model that returns an index of parent X should also return X when asking + for the parent of the index. + + This recursive function does pretty extensive testing on the whole model in an + effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() already work. + If they have a bug it will point it out, but the above tests should have already + found the basic bugs because it is easier to figure out the problem in + those tests then this one. + */ +void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) +{ + // First just try walking back up the tree. + QModelIndex p = parent; + while ( p.isValid() ) + p = p.parent(); + + // For models that are dynamically populated + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + + int rows = model->rowCount ( parent ); + int columns = model->columnCount ( parent ); + + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + // Some further testing against rows(), columns(), and hasChildren() + QVERIFY( rows >= 0 ); + QVERIFY( columns >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + //qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows + // << "columns:" << columns << "parent column:" << parent.column(); + + const QModelIndex topLeftChild = model->index( 0, 0, parent ); + + QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) ); + for ( int r = 0; r < rows; ++r ) { + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + QVERIFY( !model->hasIndex ( r, columns + 1, parent ) ); + for ( int c = 0; c < columns; ++c ) { + QVERIFY( model->hasIndex ( r, c, parent ) ); + QModelIndex index = model->index ( r, c, parent ); + // rowCount() and columnCount() said that it existed... + QVERIFY( index.isValid() ); + + // index() should always return the same index when called twice in a row + QModelIndex modifiedIndex = model->index ( r, c, parent ); + QVERIFY( index == modifiedIndex ); + + // Make sure we get the same index if we request it twice in a row + QModelIndex a = model->index ( r, c, parent ); + QModelIndex b = model->index ( r, c, parent ); + QVERIFY( a == b ); + + { + const QModelIndex sibling = model->sibling( r, c, topLeftChild ); + QVERIFY( index == sibling ); + } + { + const QModelIndex sibling = topLeftChild.sibling( r, c ); + QVERIFY( index == sibling ); + } + + // Some basic checking on the index that is returned + QVERIFY( index.model() == model ); + QCOMPARE( index.row(), r ); + QCOMPARE( index.column(), c ); + // While you can technically return a QVariant usually this is a sign + // of a bug in data(). Disable if this really is ok in your model. +// QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() ); + + // If the next test fails here is some somewhat useful debug you play with. + + if (model->parent(index) != parent) { + qDebug() << r << c << currentDepth << model->data(index).toString() + << model->data(parent).toString(); + qDebug() << index << parent << model->parent(index); +// And a view that you can even use to show the model. +// QTreeView view; +// view.setModel(model); +// view.show(); + } + + // Check that we can get back our real parent. + QCOMPARE( model->parent ( index ), parent ); + + // recursively go down the children + if ( model->hasChildren ( index ) && currentDepth < 10 ) { + //qDebug() << r << c << "has children" << model->rowCount(index); + checkChildren ( index, ++currentDepth ); + }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ + + // make sure that after testing the children that the index doesn't change. + QModelIndex newerIndex = model->index ( r, c, parent ); + QVERIFY( index == newerIndex ); + } + } +} + +/*! + Tests model's implementation of QAbstractItemModel::data() + */ +void ModelTest::data() +{ + // Invalid index should return an invalid qvariant + QVERIFY( !model->data ( QModelIndex() ).isValid() ); + + if ( model->rowCount() == 0 ) + return; + + // A valid index should have a valid QVariant data + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // shouldn't be able to set data on an invalid index + QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) ); + + // General Purpose roles that should return a QString + QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QSize + variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QFont + QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole ); + if ( fontVariant.isValid() ) { + QVERIFY( fontVariant.canConvert() ); + } + + // Check that the alignment is one we know about + QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole ); + if ( textAlignmentVariant.isValid() ) { + int alignment = textAlignmentVariant.toInt(); + QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) ); + } + + // General Purpose roles that should return a QColor + QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + // Check that the "check state" is one we know about. + QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole ); + if ( checkStateVariant.isValid() ) { + int state = checkStateVariant.toInt(); + QVERIFY( state == Qt::Unchecked || + state == Qt::PartiallyChecked || + state == Qt::Checked ); + } +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsInserted() + */ +void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */) +{ +// Q_UNUSED(end); +// qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString() +// << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) ); +// qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) ); + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( start, 0, parent ) ); + insert.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeInserted() + */ +void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end ) +{ + Changing c = insert.pop(); + QVERIFY( c.parent == parent ); +// qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize +// << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent ); + +// for (int ii=start; ii <= end; ii++) +// { +// qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent )); +// } +// qDebug(); + + QVERIFY( c.oldSize + ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + + if (c.next != model->data(model->index(end + 1, 0, c.parent))) { + qDebug() << start << end; + for (int i=0; i < model->rowCount(); ++i) + qDebug() << model->index(i, 0).data().toString(); + qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); + } + + QVERIFY( c.next == model->data ( model->index ( end + 1, 0, c.parent ) ) ); +} + +void ModelTest::layoutAboutToBeChanged() +{ + for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i ) + changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) ); +} + +void ModelTest::layoutChanged() +{ + for ( int i = 0; i < changing.count(); ++i ) { + QPersistentModelIndex p = changing[i]; + QVERIFY( p == model->index ( p.row(), p.column(), p.parent() ) ); + } + changing.clear(); +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsRemoved() + */ +void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end ) +{ +qDebug() << "ratbr" << parent << start << end; + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( end + 1, 0, parent ) ); + remove.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeRemoved() + */ +void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end ) +{ + qDebug() << "rr" << parent << start << end; + Changing c = remove.pop(); + QVERIFY( c.parent == parent ); + QVERIFY( c.oldSize - ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + QVERIFY( c.next == model->data ( model->index ( start, 0, c.parent ) ) ); +} + +void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QVERIFY(topLeft.isValid()); + QVERIFY(bottomRight.isValid()); + QModelIndex commonParent = bottomRight.parent(); + QVERIFY(topLeft.parent() == commonParent); + QVERIFY(topLeft.row() <= bottomRight.row()); + QVERIFY(topLeft.column() <= bottomRight.column()); + int rowCount = model->rowCount(commonParent); + int columnCount = model->columnCount(commonParent); + QVERIFY(bottomRight.row() < rowCount); + QVERIFY(bottomRight.column() < columnCount); +} + +void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end) +{ + QVERIFY(start >= 0); + QVERIFY(end >= 0); + QVERIFY(start <= end); + int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); + QVERIFY(start < itemCount); + QVERIFY(end < itemCount); +} + diff --git a/modeltest/modeltest.h b/modeltest/modeltest.h new file mode 100644 index 0000000..45c68f4 --- /dev/null +++ b/modeltest/modeltest.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef MODELTEST_H +#define MODELTEST_H + +#include +#include +#include + +class ModelTest : public QObject +{ + Q_OBJECT + +public: + ModelTest( QAbstractItemModel *model, QObject *parent = 0 ); + +private Q_SLOTS: + void nonDestructiveBasicTest(); + void rowCount(); + void columnCount(); + void hasIndex(); + void index(); + void parent(); + void data(); + +protected Q_SLOTS: + void runAllTests(); + void layoutAboutToBeChanged(); + void layoutChanged(); + void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end ); + void rowsInserted( const QModelIndex & parent, int start, int end ); + void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end ); + void rowsRemoved( const QModelIndex & parent, int start, int end ); + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void headerDataChanged(Qt::Orientation orientation, int start, int end); + +private: + void checkChildren( const QModelIndex &parent, int currentDepth = 0 ); + + QAbstractItemModel *model; + + struct Changing { + QModelIndex parent; + int oldSize; + QVariant last; + QVariant next; + }; + QStack insert; + QStack remove; + + bool fetchingMore; + + QList changing; +}; + +#endif diff --git a/modeltest/modeltest.pro b/modeltest/modeltest.pro new file mode 100644 index 0000000..0b8b651 --- /dev/null +++ b/modeltest/modeltest.pro @@ -0,0 +1,8 @@ +CONFIG += testcase +TARGET = tst_modeltest +QT += widgets testlib + +SOURCES += tst_modeltest.cpp modeltest.cpp dynamictreemodel.cpp +HEADERS += modeltest.h dynamictreemodel.h + +DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0 diff --git a/modeltest/modeltest.pro.user b/modeltest/modeltest.pro.user new file mode 100644 index 0000000..7e05525 --- /dev/null +++ b/modeltest/modeltest.pro.user @@ -0,0 +1,263 @@ + + + + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + true + false + 0 + true + 0 + 8 + true + 1 + true + true + true + false + + + + ProjectExplorer.Project.PluginSettings + + + + ProjectExplorer.Project.Target.0 + + Desktop Qt 5.3 GCC 64bit + Desktop Qt 5.3 GCC 64bit + qt.53.gcc_64_kit + 0 + 0 + 0 + + /home/raf/qtProjects/qt-gui/build-modeltest-Desktop_Qt_5_3_GCC_64bit-Debug + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + true + + false + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Debug + + Qt4ProjectManager.Qt4BuildConfiguration + 2 + true + + + /home/raf/qtProjects/qt-gui/build-modeltest-Desktop_Qt_5_3_GCC_64bit-Release + + + true + qmake + + QtProjectManager.QMakeBuildStep + false + true + + false + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + false + + + + 2 + Build + + ProjectExplorer.BuildSteps.Build + + + + true + Make + + Qt4ProjectManager.MakeStep + + -w + -r + + true + clean + + + 1 + Clean + + ProjectExplorer.BuildSteps.Clean + + 2 + false + + Release + + Qt4ProjectManager.Qt4BuildConfiguration + 0 + true + + 2 + + + 0 + Deploy + + ProjectExplorer.BuildSteps.Deploy + + 1 + Deploy locally + + ProjectExplorer.DefaultDeployConfiguration + + 1 + + + + false + false + false + false + true + 0.01 + 10 + true + 1 + 25 + + 1 + true + false + true + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + 2 + + modeltest + + Qt4ProjectManager.Qt4RunConfiguration:/home/raf/qtProjects/qt-gui/modeltest/modeltest.pro + + modeltest.pro + false + false + + 3768 + false + true + false + false + true + + 1 + + + + ProjectExplorer.Project.TargetCount + 1 + + + ProjectExplorer.Project.Updater.EnvironmentId + {90f31b8d-cdcb-475a-860e-16bdc879149a} + + + ProjectExplorer.Project.Updater.FileVersion + 15 + + diff --git a/modeltest/tst_modeltest.cpp b/modeltest/tst_modeltest.cpp new file mode 100644 index 0000000..131ad05 --- /dev/null +++ b/modeltest/tst_modeltest.cpp @@ -0,0 +1,342 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include +#include +#include + +#include "modeltest.h" +#include "dynamictreemodel.h" + + +class tst_ModelTest : public QObject +{ + Q_OBJECT + +public: + tst_ModelTest() {} + virtual ~tst_ModelTest() {} + +public slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + +private slots: + void stringListModel(); + void treeWidgetModel(); + void standardItemModel(); + void testInsertThroughProxy(); + void moveSourceItems(); + void testResetThroughProxy(); +}; + + + +void tst_ModelTest::initTestCase() +{ +} + +void tst_ModelTest::cleanupTestCase() +{ +} + +void tst_ModelTest::init() +{ + +} + +void tst_ModelTest::cleanup() +{ +} +/* + tests +*/ + +void tst_ModelTest::stringListModel() +{ + QStringListModel model; + QSortFilterProxyModel proxy; + + ModelTest t1(&model); + ModelTest t2(&proxy); + + proxy.setSourceModel(&model); + + model.setStringList(QStringList() << "2" << "3" << "1"); + model.setStringList(QStringList() << "a" << "e" << "plop" << "b" << "c" ); + + proxy.setDynamicSortFilter(true); + proxy.setFilterRegExp(QRegExp("[^b]")); +} + +void tst_ModelTest::treeWidgetModel() +{ + QTreeWidget widget; + + ModelTest t1(widget.model()); + + QTreeWidgetItem *root = new QTreeWidgetItem(&widget, QStringList("root")); + for (int i = 0; i < 20; ++i) { + new QTreeWidgetItem(root, QStringList(QString::number(i))); + } + QTreeWidgetItem *remove = root->child(2); + root->removeChild(remove); + QTreeWidgetItem *parent = new QTreeWidgetItem(&widget, QStringList("parent")); + new QTreeWidgetItem(parent, QStringList("child")); + widget.setItemHidden(parent, true); + + widget.sortByColumn(0); +} + +void tst_ModelTest::standardItemModel() +{ + QStandardItemModel model(10,10); + QSortFilterProxyModel proxy; + + + ModelTest t1(&model); + ModelTest t2(&proxy); + + proxy.setSourceModel(&model); + + model.insertRows(2, 5); + model.removeRows(4, 5); + + model.insertColumns(2, 5); + model.removeColumns(4, 5); + + model.insertRows(0,5, model.index(1,1)); + model.insertColumns(0,5, model.index(1,3)); +} + +void tst_ModelTest::testInsertThroughProxy() +{ + DynamicTreeModel *model = new DynamicTreeModel(this); + + QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this); + proxy->setSourceModel(model); + + new ModelTest(proxy, this); + + ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); + insertCommand->setNumCols(4); + insertCommand->setStartRow(0); + insertCommand->setEndRow(9); + // Parent is QModelIndex() + insertCommand->doCommand(); + + insertCommand = new ModelInsertCommand(model, this); + insertCommand->setNumCols(4); + insertCommand->setAncestorRowNumbers(QList() << 5); + insertCommand->setStartRow(0); + insertCommand->setEndRow(9); + insertCommand->doCommand(); + + ModelMoveCommand *moveCommand = new ModelMoveCommand(model, this); + moveCommand->setNumCols(4); + moveCommand->setStartRow(0); + moveCommand->setEndRow(0); + moveCommand->setDestRow(9); + moveCommand->setDestAncestors(QList() << 5); + moveCommand->doCommand(); +} + +/** + Makes the persistent index list publicly accessible +*/ +class AccessibleProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + AccessibleProxyModel(QObject *parent = 0) : QSortFilterProxyModel(parent) {} + + QModelIndexList persistent() + { + return persistentIndexList(); + } +}; + +class ObservingObject : public QObject +{ + Q_OBJECT +public: + ObservingObject(AccessibleProxyModel *proxy, QObject *parent = 0) + : QObject(parent) + , m_proxy(proxy) + , storePersistentFailureCount(0) + , checkPersistentFailureCount(0) + { + connect(m_proxy, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(storePersistent())); + connect(m_proxy, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(checkPersistent())); + } + +public slots: + + void storePersistent(const QModelIndex &parent) + { + for (int row = 0; row < m_proxy->rowCount(parent); ++row) { + QModelIndex proxyIndex = m_proxy->index(row, 0, parent); + QModelIndex sourceIndex = m_proxy->mapToSource(proxyIndex); + if (!proxyIndex.isValid()) { + qWarning("%s: Invalid proxy index", Q_FUNC_INFO); + ++storePersistentFailureCount; + } + if (!sourceIndex.isValid()) { + qWarning("%s: invalid source index", Q_FUNC_INFO); + ++storePersistentFailureCount; + } + m_persistentSourceIndexes.append(sourceIndex); + m_persistentProxyIndexes.append(proxyIndex); + if (m_proxy->hasChildren(proxyIndex)) + storePersistent(proxyIndex); + } + } + + void storePersistent() + { + // This method is called from rowsAboutToBeMoved. Persistent indexes should be valid + foreach(const QModelIndex &idx, m_persistentProxyIndexes) + if (!idx.isValid()) { + qWarning("%s: persistentProxyIndexes contains invalid index", Q_FUNC_INFO); + ++storePersistentFailureCount; + } + + if (!m_proxy->persistent().isEmpty()) { + qWarning("%s: proxy should have no persistent indexes when storePersistent called", + Q_FUNC_INFO); + ++storePersistentFailureCount; + } + storePersistent(QModelIndex()); + if (m_proxy->persistent().isEmpty()) { + qWarning("%s: proxy should have persistent index after storePersistent called", + Q_FUNC_INFO); + ++storePersistentFailureCount; + } + } + + void checkPersistent() + { + for (int row = 0; row < m_persistentProxyIndexes.size(); ++row) { + m_persistentProxyIndexes.at(row); + m_persistentSourceIndexes.at(row); + } + for (int row = 0; row < m_persistentProxyIndexes.size(); ++row) { + QModelIndex updatedProxy = m_persistentProxyIndexes.at(row); + QModelIndex updatedSource = m_persistentSourceIndexes.at(row); + if (m_proxy->mapToSource(updatedProxy) != updatedSource) { + qWarning("%s: check failed at row %d", Q_FUNC_INFO, row); + ++checkPersistentFailureCount; + } + } + m_persistentSourceIndexes.clear(); + m_persistentProxyIndexes.clear(); + } + +private: + AccessibleProxyModel *m_proxy; + QList m_persistentSourceIndexes; + QList m_persistentProxyIndexes; +public: + int storePersistentFailureCount; + int checkPersistentFailureCount; +}; + +void tst_ModelTest::moveSourceItems() +{ + DynamicTreeModel *model = new DynamicTreeModel(this); + AccessibleProxyModel *proxy = new AccessibleProxyModel(this); + proxy->setSourceModel(model); + + ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); + insertCommand->setStartRow(0); + insertCommand->setEndRow(2); + insertCommand->doCommand(); + + insertCommand = new ModelInsertCommand(model, this); + insertCommand->setAncestorRowNumbers(QList() << 1); + insertCommand->setStartRow(0); + insertCommand->setEndRow(2); + insertCommand->doCommand(); + + ObservingObject observer(proxy); + + ModelMoveCommand *moveCommand = new ModelMoveCommand(model, this); + moveCommand->setStartRow(0); + moveCommand->setEndRow(0); + moveCommand->setDestAncestors(QList() << 1); + moveCommand->setDestRow(0); + moveCommand->doCommand(); + + QCOMPARE(observer.storePersistentFailureCount, 0); + QCOMPARE(observer.checkPersistentFailureCount, 0); +} + +void tst_ModelTest::testResetThroughProxy() +{ + DynamicTreeModel *model = new DynamicTreeModel(this); + + ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); + insertCommand->setStartRow(0); + insertCommand->setEndRow(2); + insertCommand->doCommand(); + + QPersistentModelIndex persistent = model->index(0, 0); + + AccessibleProxyModel *proxy = new AccessibleProxyModel(this); + proxy->setSourceModel(model); + + ObservingObject observer(proxy); + observer.storePersistent(); + + ModelResetCommand *resetCommand = new ModelResetCommand(model, this); + resetCommand->setNumCols(0); + resetCommand->doCommand(); + + QCOMPARE(observer.storePersistentFailureCount, 0); + QCOMPARE(observer.checkPersistentFailureCount, 0); +} + + +QTEST_MAIN(tst_ModelTest) +#include "tst_modeltest.moc" diff --git a/qml/BasicRectangle.qml b/qml/BasicRectangle.qml index 9d1a02b..2f59256 100644 --- a/qml/BasicRectangle.qml +++ b/qml/BasicRectangle.qml @@ -4,5 +4,5 @@ Rectangle { color: activePalette.window radius: 5 border.width: 2 - border.color: activePalette.dark + border.color: activePalette.light } diff --git a/qml/BasicWindow.qml b/qml/BasicWindow.qml index d2fd790..625028a 100644 --- a/qml/BasicWindow.qml +++ b/qml/BasicWindow.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import QtQuick.Controls 1.2 +import QtQuick.Controls 1.1 import QtQuick.Window 2.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 diff --git a/qml/ButtonRow.qml b/qml/ButtonRow.qml index b68ffc8..6104a28 100644 --- a/qml/ButtonRow.qml +++ b/qml/ButtonRow.qml @@ -1,7 +1,7 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Window 2.1 -import QtQuick.Controls.Styles 1.2 +import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 diff --git a/qml/EditKeyWindow.qml b/qml/EditKeyWindow.qml new file mode 100644 index 0000000..ee404a7 --- /dev/null +++ b/qml/EditKeyWindow.qml @@ -0,0 +1,43 @@ +import QtQuick 2.2 + +KeyWindow { + + title: qsTr("Edit Key") + + path: treeView.currentNode === null ? "" : treeView.currentNode.path + keyName: keyAreaSelectedItem === null ? "" : keyAreaSelectedItem.name + keyValue: keyAreaSelectedItem === null ? "" : keyAreaSelectedItem.value + + function populateMetaArea() { + for(var i = 0; i < metaAreaModel.rowCount(); i++){ + qmlMetaKeyModel.append({"metaName" : metaAreaListView.model.get(i).name, "metaValue" : metaAreaListView.model.get(i).value}) + } + } + + function editAccepted() { + + var metaData = {}; + + //collect metadata in a map + for(var i = 0; i < qmlMetaKeyModel.count; i++){ + metaData[qmlMetaKeyModel.get(i).metaName] = qmlMetaKeyModel.get(i).metaValue + } + + //create undo command + if(isEdited) + undoManager.createEditKeyCommand(keyAreaView.model, keyAreaView.currentRow, keyName.toString(), keyValue.toString(), keyAreaSelectedItem.metaValue, + nameTextField.text, valueTextField.text, metaData) + +// //set key name & value +// keyAreaView.model.setData(keyAreaView.currentRow, nameTextField.text, "Name") +// keyAreaView.model.setData(keyAreaView.currentRow, valueTextField.text, "Value") + + +// console.log(keyAreaView.currentRow) + +// //set metaData +// keyAreaSelectedItem.node.setMeta(metaData) + + qmlMetaKeyModel.clear() + } +} diff --git a/qml/ExitDialog.qml b/qml/ExitDialog.qml new file mode 100644 index 0000000..6bc9e53 --- /dev/null +++ b/qml/ExitDialog.qml @@ -0,0 +1,13 @@ +import QtQuick 2.2 +import QtQuick.Dialogs 1.1 + +MessageDialog { + icon: StandardIcon.Warning + text: qsTr("The configuration has been modified.\nDo you want to save your changes?") + standardButtons: StandardButton.Save | StandardButton.Discard | StandardButton.Cancel + onAccepted: { + externTreeModel.synchronize() + Qt.quit() + } + onDiscard: Qt.quit() +} diff --git a/qml/KeyWindow.qml b/qml/KeyWindow.qml new file mode 100644 index 0000000..ca5bc0c --- /dev/null +++ b/qml/KeyWindow.qml @@ -0,0 +1,149 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Window 2.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 + +BasicWindow { + id: keyWindow + +// property alias valueLayout: valueLayout + property alias nameLabel: nameLabel + property alias addButton: addButton + property alias qmlMetaKeyModel: qmlMetaKeyModel + property alias nameTextField: nameTextField + property alias valueTextField: valueTextField + property string path: "" + property string keyName: "" + property string keyValue: "" + property bool isEdited: false + + contents: ColumnLayout { + anchors.fill: parent + anchors.margins: defaultMargins + anchors.centerIn: parent + spacing: defaultMargins + + Text{ + id: pathInfo + text: path + color: disabledPalette.text + } + GridLayout { + columns: 2 + Label { + id:nameLabel + text: qsTr("Key Name: ") + } + TextField { + id: nameTextField + Layout.fillWidth: true + focus: true + text: keyName + } + Label { + id: valueLabel + text: qsTr("Key Value: ") + } + TextField { + id: valueTextField + Layout.fillWidth: true + text: keyValue + } + } + // RowLayout { + // spacing: defaultSpacing + + // Label { + // id:nameLabel + // text: qsTr("Key Name: ") + // } + // TextField { + // id: nameTextField + // Layout.fillWidth: true + // focus: true + // text: keyName + // } + // } + // RowLayout { + // id:valueLayout + // spacing: defaultSpacing + + // Label { + // id: valueLabel + // text: qsTr("Key Value: ") + // } + // TextField { + // id: valueTextField + // Layout.fillWidth: true + // text: keyValue + // } + // } + BasicRectangle { + id: metaArea + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollView { + anchors.fill: parent + anchors.margins: defaultSpacing + ListView { + id: metaKeyListView + anchors.fill: parent + spacing: defaultMargins + model: qmlMetaKeyModel + delegate: metaKeyDelegate + } + } + + ListModel { + id: qmlMetaKeyModel + } + Component { + id: metaKeyDelegate + + NewMetaKey { + //check if user has edited metakeyname or metakeyvalue. This comparison can only happen here since + //"metaNameField.text" cannot be accessed outside the delegate. + metaNameField.onTextChanged: { + if(metaName !== metaNameField.text){ + qmlMetaKeyModel.set(index, {"metaName": metaNameField.text}) + isEdited = true + } + } + metaValueField.onTextChanged: { + if(metaValue !== metaValueField.text){ + qmlMetaKeyModel.set(index, {"metaValue": metaValueField.text}) + isEdited = true + } + } + } + } + } + Button { + id: addButton + anchors.horizontalCenter: parent.horizontalCenter + text: qsTr("New Meta Key") + onClicked: { + //add visual item + qmlMetaKeyModel.append({"metaName" : "", "metaValue" : ""}) + } + } + } + cancelButton.onClicked: { + keyWindow.visible = false + isEdited = false + nameTextField.undo() + valueTextField.undo() + qmlMetaKeyModel.clear() + } + okButton.onClicked: { + //check if user has edited keyname or keyvalue + if(keyName !== nameTextField.text || keyValue !== valueTextField.text) + isEdited = true + + keyWindow.visible = false + editAccepted() + } +} diff --git a/qml/MainMenuBar.qml b/qml/MainMenuBar.qml new file mode 100644 index 0000000..254ff4e --- /dev/null +++ b/qml/MainMenuBar.qml @@ -0,0 +1,209 @@ +import QtQuick 2.2 +import QtQuick.Controls 1.1 + +MenuBar { + + Menu { + id:dbdatabase + title: qsTr("&Database") + + MenuItem { + id:dbImport + action: importAction + } + MenuItem { + id:dbExport + action: exportAction + } + + MenuSeparator{} + + MenuItem { + id:dbCreateBackend + action: createBackendAction + } + MenuItem { + id:dbUnmountBackend + action: unmountBackendAction + } + + MenuSeparator{} + + MenuItem { + id:dbExit + text: qsTr("Exit") + shortcut: StandardKey.Quit + onTriggered: { + if(!undoManager.isClean()) + exitDialog.open() + else + Qt.quit() + } + } + } + + Menu { + id:edit + title: qsTr("&Edit") + + MenuItem { + id:edUndo + action: undoAction + } + MenuItem { + id:edRedo + action: redoAction + } + + MenuSeparator{} + + Menu { + id:edNew + title: qsTr("New") + + MenuItem { + id:edNewKey + action: newKeyAction + } + MenuItem { + id:edNewArray + action: newArrayAction + } + } + + MenuItem { + id:edEdit + action: editAction + } + + MenuSeparator{} + + MenuItem { + id:edCut + action: cutAction + } + MenuItem { + id:edCopy + action: copyAction + } + MenuItem { + id:edPaste + action: pasteAction + } + MenuItem { + id:edDelete + action: deleteAction + } + } + + Menu { + id:about + title: qsTr("&About") + MenuItem { + text: qsTr("Credits ") + } + } + + Menu { + id: treeContextMenu + //MenuItem { + //id:tcmDelete + //action: deleteAction + //} + } + + Menu { + id: keyContextMenu + + MenuItem { + id: kcmNewKey + action: newKeyAction + } + MenuItem { + id: kcmEdit + action: editAction + onTriggered: { + editKeyWindow.show() + editKeyWindow.populateMetaArea() + } + } + MenuItem { + id: kcmCut + + action: cutAction + onTriggered: { + console.log("cut") + keyAreaView.copyPasteIndex = keyAreaView.currentRow + keyAreaView.currentNodePath = treeView.currentNode.path + + undoManager.putToClipboard("cut", keyAreaView.model, keyAreaSelectedItem.node, keyAreaView.currentRow) + pasteCounter = 0 + } + } + MenuItem { + id: kcmCopy + + action: copyAction + + onTriggered: { + console.log("copy") + keyAreaView.copyPasteIndex = keyAreaView.currentRow + keyAreaView.currentNodePath = treeView.currentNode.path + + undoManager.putToClipboard("copy", keyAreaView.model, keyAreaSelectedItem.node, keyAreaView.currentRow) + } + } + MenuItem { + id: kcmPaste + + action: pasteAction + onTriggered: { + console.log("paste") + keyAreaView.copyPasteIndex = -1 + keyAreaView.currentNodePath = "" + + if(undoManager.clipboardType === "copy"){ + undoManager.createCopyKeyCommand(treeView.currentNode.node) + } + else if (undoManager.clipboardType === "cut"){ + + if(pasteCounter === 0){ + undoManager.createCutKeyCommand(treeView.currentNode.node) + pasteCounter++ + } + else{ + undoManager.createCopyKeyCommand(treeView.currentNode.node) + pasteCounter++ + } + } + } + } + MenuItem { + id: kcmDelete + + action: deleteAction + + onTriggered: { + + if(keyAreaSelectedItem !== null){ + undoManager.createDeleteKeyCommand("deleteKey", keyAreaView.model, keyAreaSelectedItem.node, keyAreaView.currentRow) + + keyAreaView.__decrementCurrentIndex() + keyAreaView.selection.clear() + keyAreaView.selection.select(keyAreaView.currentRow) + keyAreaSelectedItem = keyAreaView.model.get(keyAreaView.currentRow) + + if(keyAreaView.rowCount !== 0) + keyAreaSelectedItem = keyAreaView.model.get(keyAreaView.currentRow) + else + keyAreaSelectedItem = null + } + else if(treeView.currentNode !== null && keyAreaSelectedItem === null){ + undoManager.createDeleteKeyCommand("deleteBranch", treeView.currentNode.parentModel, treeView.currentNode.node, treeView.currentNode.index) + } + } + } + } +} + + diff --git a/qml/NewKeyWindow.qml b/qml/NewKeyWindow.qml index 546a045..2092f79 100644 --- a/qml/NewKeyWindow.qml +++ b/qml/NewKeyWindow.qml @@ -1,134 +1,24 @@ import QtQuick 2.2 -import QtQuick.Controls 1.1 -import QtQuick.Window 2.1 -import QtQuick.Controls.Styles 1.2 -import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 -BasicWindow { - id: editWindow +KeyWindow { - property alias valueLayout: valueLayout - property alias nameLabel: nameLabel - property alias addButton: addButton - property alias metaKeyModel: metaKeyModel - property string path: "" - property string keyName: "" - property string keyValue: "" - - cancelButton.onClicked: { - editWindow.visible = false - metaKeyModel.clear() - } - okButton.onClicked: { - editWindow.visible = false - editAccepted() - } - - contents: ColumnLayout { - anchors.fill: parent - anchors.margins: defaultMargins - anchors.centerIn: parent - spacing: defaultMargins - - Text{ - id: pathInfo - text: path - color: disabledPalette.text - } - RowLayout { - spacing: defaultSpacing - - Label { - id:nameLabel - text: qsTr("Key Name: ") - } - TextField { - id: nameTextField - Layout.fillWidth: true - focus: true - text: keyName - } - } - RowLayout { - id:valueLayout - spacing: defaultSpacing - - Label { - id: valueLabel - text: qsTr("Key Value: ") - } - TextField { - id: valueTextField - Layout.fillWidth: true - text: keyValue - } - } - BasicRectangle { - id: metaArea - Layout.fillWidth: true - Layout.fillHeight: true - - ScrollView { - anchors.fill: parent - anchors.margins: defaultSpacing - ListView { - id: metaKeyListView - anchors.fill: parent - spacing: defaultMargins - model: metaKeyModel - delegate: metaKeyDelegate - } - } - ListModel { - id: metaKeyModel - } - Component { - id: metaKeyDelegate - NewMetaKey { - metaNameField.onTextChanged: metaKeyModel.set(index, {"metaName": metaNameField.text}) - metaValueField.onTextChanged: metaKeyModel.set(index, {"metaValue": metaValueField.text}) - } - } - } - Button { - id: addButton - anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("New Meta Key") - onClicked: { - //add visual item - metaKeyModel.append({"metaName" : "", "metaValue" : ""}) - } - } - } - - function populateMetaArea() { - for(var i = 0; i < metaAreaModel.rowCount(); i++){ - metaKeyModel.append({"metaName" : metaAreaListView.model.get(i).name, "metaValue" : metaAreaListView.model.get(i).value}) - } - } + title: qsTr("Create new Key") + path: treeView.currentNode === null ? "" : treeView.currentNode.path function editAccepted() { - //set key name & value - keyAreaView.model.setDataValue(keyAreaView.currentRow, nameTextField.text, "Name") - keyAreaView.model.setDataValue(keyAreaView.currentRow, valueTextField.text, "Value") - - //delete metaKeys - for(var i = 0; i < metaAreaModel.rowCount(); i++) - metaAreaListView.model.get(i).node.deleteMeta(metaAreaListView.model.get(i).name) - //clear old meta nodes - metaAreaListView.model.clear() + var metaData = {}; - //insert new meta nodes - for(var i = 0; i < metaKeyModel.count; i++) - metaAreaListView.model.qmlInsertRow(i, keyAreaSelectedItem.node); + //collect metadata + for(var i = 0; i < qmlMetaKeyModel.count; i++) + metaData[qmlMetaKeyModel.get(i).metaName] = qmlMetaKeyModel.get(i).metaValue - //fill the meta nodes with provided names/values - for(var i = 0; i < metaKeyModel.count; i++) - metaAreaListView.model.setDataValue(i, [metaKeyModel.get(i).metaName, metaKeyModel.get(i).metaValue], "MetaValue") + //create UndoCommand + undoManager.createNewKeyCommand(externTreeModel, treeView.currentNode.path + "/" + nameTextField.text, valueTextField.text, metaData) - metaKeyModel.clear() + nameTextField.text = "" + valueTextField.text = "" + nameTextField.focus = true + qmlMetaKeyModel.clear() } - } diff --git a/qml/NewMetaKey.qml b/qml/NewMetaKey.qml index c634cee..34cdefb 100644 --- a/qml/NewMetaKey.qml +++ b/qml/NewMetaKey.qml @@ -1,11 +1,12 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.1 -import QtQuick.Controls.Styles 1.2 +import QtQuick.Controls.Styles 1.1 Item { id: metaInfoItem - width: metaArea.width - 3*defaultMargins + + width: parent.width height: metaNameField.height property alias metaNameField: metaNameField @@ -29,10 +30,19 @@ Item { } Button { id:deleteMetaButton - iconSource: "icons/application-exit.png" + + style: ButtonStyle { + background: Image { + anchors.centerIn: parent + height: metaNameField.height + width: height + source: "icons/application-exit.png" + } + } onClicked: { - metaKeyModel.remove(index)// remove the visual item + qmlMetaKeyModel.remove(index)// remove the visual item + isEdited = true } } } diff --git a/qml/Page4.qml b/qml/Page4.qml index 6e4975f..202e12d 100644 --- a/qml/Page4.qml +++ b/qml/Page4.qml @@ -77,7 +77,7 @@ Item { Layout.fillHeight: true ScrollView { - + //Markdown } } } diff --git a/qml/SearchField.qml b/qml/SearchField.qml index 41c4a30..4b78c0c 100644 --- a/qml/SearchField.qml +++ b/qml/SearchField.qml @@ -2,37 +2,37 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 TextField { -id: searchField -placeholderText: qsTr("Find Key...") -onAccepted: { -if(searchField.state == "hasText") -keyMetaColumn.state = "SHOW_SEARCH_RESULTS" -} + id: searchField + placeholderText: qsTr("Find Key...") + onAccepted: { + if(searchField.state == "hasText") + keyMetaColumn.state = "SHOW_SEARCH_RESULTS" + } -Image { -id: clear -anchors { -right: parent.right -rightMargin: defaultMargins -verticalCenter: parent.verticalCenter -} -source: "icons/clear.png" -opacity: 0 + Image { + id: clear + anchors { + right: parent.right + rightMargin: defaultMargins + verticalCenter: parent.verticalCenter + } + source: "icons/edit-clear.png" + opacity: 0 -MouseArea { -anchors.fill: parent -onClicked: { -searchField.text = '' -} -} -} -states: State { -name: "hasText" -when: searchField.text != '' + MouseArea { + anchors.fill: parent + onClicked: { + searchField.text = '' + } + } + } + states: State { + name: "hasText" + when: searchField.text != '' -PropertyChanges { -target: clear -opacity: 1 -} -} + PropertyChanges { + target: clear + opacity: 1 + } + } } diff --git a/qml/TreeView.qml b/qml/TreeView.qml index 604e8a0..20e7a36 100644 --- a/qml/TreeView.qml +++ b/qml/TreeView.qml @@ -1,5 +1,5 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 +import QtQuick 2.2 +import QtQuick.Controls 1.1 //treeView is based on code user "Jens" posted in the qt-project forum (http://qt-project.org/forums/viewthread/30521/#146845) @@ -19,6 +19,7 @@ ScrollView { text: model.name color: activePalette.windowText } + contentItem: Loader { id: content @@ -30,6 +31,7 @@ ScrollView { anchors.fill: parent Repeater { model: 1 + Math.max(treeView.contentItem.height, treeView.height) / treeView.rowHeight + Rectangle { color: activePalette.window width: treeView.width @@ -37,15 +39,20 @@ ScrollView { } } } + Component { id: treeBranch + Item { id: root + property bool isRoot: false implicitHeight: column.implicitHeight implicitWidth: column.implicitWidth + Column { id: column + x: 2 Item { height: isRoot ? 0 : treeView.rowHeight @@ -53,13 +60,17 @@ ScrollView { } Repeater { model: elements + Item { id: filler + width: Math.max(loader.width + treeView.columnIndent, row.width) height: Math.max(row.height, loader.height) property var _model: model + Rectangle { id: rowfill + x: treeView.mapToItem(rowfill, 0, 0).x width: treeView.width height: treeView.rowHeight @@ -82,6 +93,7 @@ ScrollView { } Row { id: row + Item { width: treeView.rowHeight height: treeView.rowHeight @@ -89,6 +101,7 @@ ScrollView { Image { id: expander + source: "icons/arrow-right.png" opacity: mouse.containsMouse ? 1 : 0.7 anchors.centerIn: parent @@ -101,6 +114,7 @@ ScrollView { } MouseArea { id: mouse + anchors.fill: parent hoverEnabled: true onClicked: { @@ -117,6 +131,7 @@ ScrollView { } Loader { id: loader + x: treeView.columnIndent height: expanded ? implicitHeight : 0 property var node: model diff --git a/qml/UnmountBackendWindow.qml b/qml/UnmountBackendWindow.qml index b6fe676..2d222b6 100644 --- a/qml/UnmountBackendWindow.qml +++ b/qml/UnmountBackendWindow.qml @@ -1,7 +1,7 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Window 2.1 -import QtQuick.Controls.Styles 1.2 +import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 diff --git a/qml/WizardTemplate.qml b/qml/WizardTemplate.qml index e8aaa0b..073bc3a 100644 --- a/qml/WizardTemplate.qml +++ b/qml/WizardTemplate.qml @@ -1,5 +1,5 @@ import QtQuick 2.2 -import QtQuick.Controls 1.2 +import QtQuick.Controls 1.1 import QtQuick.Window 2.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 diff --git a/qml/i18n/lang_de_DE.ts b/qml/i18n/lang_de_DE.ts new file mode 100644 index 0000000..b4285a4 --- /dev/null +++ b/qml/i18n/lang_de_DE.ts @@ -0,0 +1,333 @@ + + + + + ButtonRow + + + &Back + &Zurück + + + + &Next + &Vor + + + + &Finish + &Beenden + + + + &Cancel + &Abbrechen + + + + NewKeyWindow + + + Key Name: + Schlüssel Name: + + + + Key Value: + Schlüssel Wert: + + + + New Meta Key + Neuer Meta Schlüssel + + + + NewMetaKey + + + Meta Key Name... + Meta Schlüssel Name... + + + + Meta Key Value... + Meta Schlüssel Wert... + + + + Page1 + + + Backend name: + Backend Name: + + + + Page2 + + + Mount point: + Einhängepunkt: + + + + Page3 + + + Please enter a path to a file in the filesystem. This file is used by all plugins of this backend as fallback. For user or cascading mountpoints it must be a relative path. The actual path will be located dynamically by the resolver plugin. + + + + + Path: + Pfad: + + + + Page4 + + + Please select the plugins you want to include in the backend. + Bitte wählen Sie die Plugins die Sie in Ihr Backend einfügen möchten. + + + + Add Plugin + Plugin Einfügen + + + + Remove Plugin + Plugin Entfernen + + + + Included Plugins + Eingefügte Plugins + + + + Plugin Info + Plugin Information + + + + SearchField + + + Find Key... + Schlüssel finden... + + + + UnmountBackendWindow + + + Unmount Backend + Backend aushängen + + + + Mounted Backends + Eingehängte Backends + + + + Unmount + Aushängen + + + + WizardLoader + + + Create Backend + Backend erstellen + + + + main + + + Create new Key + Neuen Schlüssel erstellen + + + + Edit Key + Schlüssel editieren + + + + Create new Array Entry + Neuen Array Eintrag erstellen + + + + Array Name: + Array Name: + + + + New Array Entry + Neuer Array Eintrag + + + + Export to file + In Datei exportieren + + + + Key... + Schlüssel... + + + + New Key + Neuer Schlüssel + + + + Array Entry... + Array Eintrag... + + + + Delete + Löschen + + + + Import Configuration... + Konfiguration importieren... + + + + Import Configuration + Konfiguration importieren + + + + Export Configuration... + Konfiguration exportieren... + + + + Export Configuration + Konfiguration exportieren + + + + + Undo + Rückggängig + + + + + Redo + Wiederholen + + + + + Synchronize + Synchronisieren + + + + Create Backend... + Backend erstellen... + + + + Create Backend + Backend erstellen + + + + Unmount Backend... + Backend aushängen... + + + + Unmount Backend + Backend aushängen + + + + Edit... + Editieren... + + + + Edit + Editieren + + + + + Cut + Ausschneiden + + + + + Copy + Kopieren + + + + + Paste + Einfügen + + + + &Database + &Datenbank + + + + Exit + Beenden + + + + &Edit + &Editieren + + + + New + Neu + + + + &About + &Über + + + + Credits + + + + + Name + Name + + + + Value + Wert + + + + Close + Schließen + + + diff --git a/qml/icons/application-exit.png b/qml/icons/application-exit.png index 4839c61..dd76354 100644 Binary files a/qml/icons/application-exit.png and b/qml/icons/application-exit.png differ diff --git a/qml/icons/edit-clear.png b/qml/icons/edit-clear.png new file mode 100644 index 0000000..78d183f Binary files /dev/null and b/qml/icons/edit-clear.png differ diff --git a/qml/icons/edit-find.png b/qml/icons/edit-find.png new file mode 100644 index 0000000..ee69755 Binary files /dev/null and b/qml/icons/edit-find.png differ diff --git a/qml/main.qml b/qml/main.qml index a498b4f..18603eb 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -1,9 +1,9 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 +import QtQuick 2.2 +import QtQuick.Controls 1.1 import QtQuick.Window 2.0 -import QtQuick.Controls.Styles 1.2 +import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.2 +import QtQuick.Dialogs 1.1 ApplicationWindow { id: mainWindow @@ -14,6 +14,15 @@ ApplicationWindow { title: "Elektra Editor" + onClosing: { + if(!undoManager.isClean()){ + close.accepted = false + exitDialog.open() + } + else + Qt.quit() + } + property int deltaKeyAreaHeight: Math.round(keyArea.height - searchResultsArea.height*0.5 - defaultSpacing) property int deltaKeyAreaWidth: Math.round(mainRow.width*0.7 - defaultSpacing) property int deltaMetaAreaHeight: Math.round(metaArea.height - searchResultsArea.height*0.5) @@ -21,10 +30,13 @@ ApplicationWindow { //TreeViewModel property var metaAreaModel: (keyAreaSelectedItem === null ? null : keyAreaSelectedItem.metaValue) + property int pasteCounter: 0 + //Spacing & Margins recommended by KDE HIG property int defaultSpacing: 4 property int defaultMargins: 8 + //Get access to system colors SystemPalette { id: activePalette colorGroup: SystemPalette.Active @@ -40,24 +52,20 @@ ApplicationWindow { NewKeyWindow { id: newKeyWindow - title: qsTr("Create new Key") } - NewKeyWindow { + EditKeyWindow { id: editKeyWindow - title: qsTr("Edit Key") - path: treeView.currentNode === null ? "" : treeView.currentNode.path - keyName: keyAreaSelectedItem === null ? "" : keyAreaSelectedItem.name - keyValue: keyAreaSelectedItem === null ? "" : keyAreaSelectedItem.value } - NewKeyWindow { - id: newArrayWindow - title: qsTr("Create new Array Entry") - valueLayout.visible: false - nameLabel.text: qsTr("Array Name: ") - addButton.text: qsTr("New Array Entry") - } + + // NewKeyWindow { + // id: newArrayWindow + // title: qsTr("Create new Array Entry") + // valueLayout.visible: false + // nameLabel.text: qsTr("Array Name: ") + // addButton.text: qsTr("New Array Entry") + // } UnmountBackendWindow { id: unmountBackendWindow @@ -73,6 +81,10 @@ ApplicationWindow { selectExisting: false } + ExitDialog { + id: exitDialog + } + Action { id:newKeyAction text: qsTr("Key...") @@ -85,6 +97,7 @@ ApplicationWindow { id:newArrayAction text: qsTr("Array Entry...") onTriggered: newArrayWindow.show() + enabled: false } Action { @@ -101,6 +114,7 @@ ApplicationWindow { iconSource: "icons/import.png" tooltip: qsTr("Import Configuration") onTriggered: importDialog.open() + enabled: false } Action { @@ -109,6 +123,7 @@ ApplicationWindow { iconSource: "icons/export.png" tooltip: qsTr("Export Configuration") onTriggered: exportDialog.open() + enabled: false } Action { @@ -117,7 +132,24 @@ ApplicationWindow { iconSource: "icons/undo.png" tooltip: qsTr("Undo") shortcut: StandardKey.Undo - enabled: false + enabled: undoManager.canUndo + onTriggered: { + + if(undoManager.undoText === "deleteKey"){ + undoManager.undo() + keyAreaView.__incrementCurrentIndex() + keyAreaView.selection.clear() + keyAreaView.selection.select(keyAreaView.currentRow) + keyAreaSelectedItem = keyAreaView.model.get(keyAreaView.currentRow) + } + else if(undoManager.undoText === "cut"){ + pasteCounter-- + undoManager.undo() + } + else{ + undoManager.undo() + } + } } Action { @@ -126,7 +158,33 @@ ApplicationWindow { iconSource: "icons/redo.png" tooltip: qsTr("Redo") shortcut: StandardKey.Redo - enabled: false + enabled: undoManager.canRedo + onTriggered: { + + if(undoManager.redoText === "deleteKey"){ + + keyAreaView.__decrementCurrentIndex() + keyAreaView.selection.clear() + keyAreaView.selection.select(keyAreaView.currentRow) + keyAreaSelectedItem = keyAreaView.model.get(keyAreaView.currentRow) + + if(keyAreaView.rowCount !== 0) + keyAreaSelectedItem = keyAreaView.model.get(keyAreaView.currentRow) + else + keyAreaSelectedItem = null + } + else if(undoManager.redoText === "deleteBranch"){ + + if(metaAreaModel !== null) + metaAreaModel = null + } + else if(undoManager.redoText === "cut"){ + pasteCounter-- + } + + undoManager.redo() + } + } Action { @@ -135,8 +193,10 @@ ApplicationWindow { iconSource: "icons/synchronize.png" tooltip: qsTr("Synchronize") shortcut: StandardKey.Refresh - onTriggered: externTreeModel.synchronize() - enabled: false + onTriggered: { + externTreeModel.synchronize() + undoManager.setClean() + } } Action { @@ -144,6 +204,7 @@ ApplicationWindow { text: qsTr("Create Backend...") tooltip: qsTr("Create Backend") onTriggered: wizardLoader.show() + enabled: false } Action { @@ -151,6 +212,7 @@ ApplicationWindow { text: qsTr("Unmount Backend...") tooltip: qsTr("Unmount Backend") onTriggered: unmountBackendWindow.show() + enabled: false } Action { @@ -164,7 +226,6 @@ ApplicationWindow { text: qsTr("Cut") tooltip: qsTr("Cut") shortcut: StandardKey.Cut - enabled: false } Action { @@ -172,7 +233,6 @@ ApplicationWindow { text: qsTr("Copy") tooltip: qsTr("Copy") shortcut: StandardKey.Copy - enabled: false } Action { @@ -180,134 +240,10 @@ ApplicationWindow { text: qsTr("Paste") tooltip: qsTr("Paste") shortcut: StandardKey.Paste - enabled: false } - menuBar: MenuBar { + menuBar: MainMenuBar { id:mainMenuBar - - Menu { - id:dbdatabase - title: qsTr("&Database") - - MenuItem { - id:dbImport - action: importAction - } - MenuItem { - id:dbExport - action: exportAction - } - - MenuSeparator{} - - MenuItem { - id:dbCreateBackend - action: createBackendAction - } - MenuItem { - id:dbUnmountBackend - action: unmountBackendAction - } - - MenuSeparator{} - - MenuItem { - id:dbExit - text: qsTr("Exit") - shortcut: StandardKey.Quit - } - } - - Menu { - id:edit - title: qsTr("&Edit") - - MenuItem { - id:edUndo - action: undoAction - } - MenuItem { - id:edRedo - action: redoAction - } - - MenuSeparator{} - - Menu { - id:edNew - title: qsTr("New") - - MenuItem { - id:edNewKey - action: newKeyAction - } - MenuItem { - id:edNewArray - action: newArrayAction - } - } - - MenuItem { - id:edEdit - action: editAction - } - - MenuSeparator{} - - MenuItem { - id:edCut - action: cutAction - } - MenuItem { - id:edCopy - action: copyAction - } - MenuItem { - id:edPaste - action: pasteAction - } - MenuItem { - id:edDelete - action: deleteAction - } - } - - Menu { - id:about - title: qsTr("&About") - MenuItem { - text: qsTr("Credits ") - } - } - } - - Menu { - id: treeContextMenu - //MenuItem { - //id:tcmDelete - //action: deleteAction - //} - } - Menu { - id: keyContextMenu - MenuItem { - id: kcmDelete - action: deleteAction - onTriggered: { - keyAreaView.model.removeRow(keyAreaView.tableIndex) - keyAreaView.__decrementCurrentIndex() - keyAreaSelectedItem = keyAreaView.model.get(keyAreaView.currentRow) - } - } - MenuItem { - id: kcmEdit - action: editAction - onTriggered: { - editKeyWindow.show() - editKeyWindow.populateMetaArea() - } - } } toolBar: ToolBar { @@ -352,6 +288,10 @@ ApplicationWindow { height: 28 } + Image { + id: searchLogo + source: "icons/edit-find.png" + } SearchField { id: searchField Layout.fillWidth: true @@ -392,24 +332,41 @@ ApplicationWindow { } Column { id: keyMetaColumn + spacing: defaultSpacing BasicRectangle { id: keyArea + width: deltaKeyAreaWidth height: Math.round(mainRow.height*0.7 - defaultSpacing) + Component { + id: tableViewColumnDelegate + + Item { + anchors.fill: parent + anchors.margins: defaultSpacing + + Text{ + anchors.verticalCenter: parent.verticalCenter + text: styleData.value + color: (keyAreaView.copyPasteIndex === styleData.row && treeView.currentNode.path === keyAreaView.currentNodePath) ? disabledPalette.text : activePalette.text + } + } + } + TableView { id: keyAreaView + property int copyPasteIndex + property string currentNodePath + anchors.fill: parent anchors.margins: 2 frameVisible: false alternatingRowColors: false backgroundVisible: false - Component.onCompleted: currentRow = -1 - //QVariantMap - onClicked: keyAreaSelectedItem = model.get(currentRow) model:{ if(treeView.currentNode !== null) @@ -421,36 +378,62 @@ ApplicationWindow { role: "name" title: qsTr("Name") width: Math.round(keyArea.width*0.5) + delegate: tableViewColumnDelegate } TableViewColumn { role: "value" title: qsTr("Value") width: Math.round(keyArea.width*0.5) + delegate: tableViewColumnDelegate } - rowDelegate: Rectangle { - width: keyAreaView.width - color: styleData.selected ? activePalette.highlight : "transparent" - MouseArea { - propagateComposedEvents: true - anchors.fill: parent - acceptedButtons: Qt.RightButton - onClicked: keyContextMenu.popup() + + rowDelegate: Component { + + Rectangle { + width: keyAreaView.width + color: styleData.selected ? activePalette.highlight : "transparent" + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: { + keyAreaSelectedItem = model.get(styleData.row) + + if(mouse.button === Qt.RightButton) + keyContextMenu.popup() + else{ + keyAreaView.selection.clear() + keyAreaView.selection.select(styleData.row) + keyAreaView.currentRow = styleData.row + } + } + + onDoubleClicked: { + keyAreaSelectedItem = model.get(styleData.row) + editKeyWindow.show() + editKeyWindow.populateMetaArea() + } + } } } } } BasicRectangle { id: metaArea + width: deltaKeyAreaWidth height: Math.round(mainRow.height*0.3) ScrollView { id: metaAreaScrollView + anchors.fill: parent anchors.margins: defaultMargins ListView { id: metaAreaListView + model: metaAreaModel delegate: Text { @@ -462,11 +445,14 @@ ApplicationWindow { } BasicRectangle { id: searchResultsArea + width: deltaKeyAreaWidth height: Math.round(mainRow.height*0.2) visible: false Button { + id: searchResultsCloseButton + iconSource: "icons/dialog-close.png" anchors.right: parent.right anchors.top: parent.top @@ -483,11 +469,14 @@ ApplicationWindow { ScrollView { id: searchResultsScrollView + anchors.fill: parent anchors.margins: defaultMargins + anchors.rightMargin: searchResultsCloseButton.width ListView { id: searchResultsListView + focus: false highlight: Rectangle { @@ -531,8 +520,11 @@ ApplicationWindow { RowLayout { id: statusBarRow + Label { id: path + anchors.fill: parent + anchors.leftMargin: defaultMargins text: treeView.currentNode === null ? "" : treeView.currentNode.path + "/" + (keyAreaSelectedItem === null ? "" : keyAreaSelectedItem.name) } } diff --git a/qt-gui.pro b/qt-gui.pro index 32d9801..4c4b198 100644 --- a/qt-gui.pro +++ b/qt-gui.pro @@ -1,18 +1,34 @@ -QT += quick gui core qml widgets +QT += quick gui core qml widgets testlib SOURCES += \ - src/main.cpp \ - src/treeviewmodel.cpp \ - src/confignode.cpp \ + src/main.cpp \ + src/treeviewmodel.cpp \ + src/confignode.cpp \ src/printvisitor.cpp \ - src/keysetvisitor.cpp + src/keysetvisitor.cpp \ + modeltest/dynamictreemodel.cpp \ + modeltest/modeltest.cpp \ + src/undomanager.cpp \ + src/newkeycommand.cpp \ + src/deletekeycommand.cpp \ + src/editkeycommand.cpp \ + src/copykeycommand.cpp \ + src/cutkeycommand.cpp HEADERS += \ - src/treeviewmodel.hpp \ - src/confignode.hpp \ + src/treeviewmodel.hpp \ + src/confignode.hpp \ src/visitor.hpp \ src/printvisitor.hpp \ - src/keysetvisitor.hpp + src/keysetvisitor.hpp \ + modeltest/dynamictreemodel.h \ + modeltest/modeltest.h \ + src/undomanager.hpp \ + src/newkeycommand.hpp \ + src/editkeycommand.hpp \ + src/deletekeycommand.hpp \ + src/cutkeycommand.hpp \ + src/copykeycommand.hpp CONFIG += debug @@ -21,30 +37,38 @@ QML_IMPORT_PATH = INCLUDEPATH += /usr/include/elektra INCLUDEPATH += /usr/local/include/elektra -INCLUDEPATH += ../../Desktop/p4n81s-libelektra/src/tools/kdb/ # Default rules for deployment. include(deployment.pri) -RESOURCES += resources.qrc \ - i18n.qrc +RESOURCES += \ + resources.qrc + i18n.qrc OTHER_FILES += \ - qml/main.qml\ - qml/BasicWindow.qml\ - qml/ButtonRow.qml\ - qml/Page1.qml\ - qml/Page2.qml\ - qml/Page3.qml\ - qml/WizardLoader.qml\ - qml/WizardTemplate.qml\ - qml/Page4.qml\ - qml/NewMetaKey.qml\ - qml/BasicRectangle.qml \ - qml/UnmountBackendWindow.qml \ - qml/NewKeyWindow.qml \ - qml/NewArrayEntry.qml \ - qml/TreeView.qml + qml/main.qml\ + qml/BasicWindow.qml\ + qml/ButtonRow.qml\ + qml/Page1.qml\ + qml/Page2.qml\ + qml/Page3.qml\ + qml/WizardLoader.qml\ + qml/WizardTemplate.qml\ + qml/Page4.qml\ + qml/NewMetaKey.qml\ + qml/BasicRectangle.qml \ + qml/UnmountBackendWindow.qml \ + qml/NewArrayEntry.qml \ + qml/TreeView.qml \ + qml/KeyWindow.qml \ + qml/NewKeyWindow.qml \ + qml/EditKeyWindow.qml \ + qml/ExitDialog.qml \ + qml/MainMenuBar.qml unix: CONFIG += link_pkgconfig unix: PKGCONFIG += elektra + +SUBDIRS += \ + unittest/unittest.pro \ +# modeltest/modeltest.pro diff --git a/resources.qrc b/resources.qrc index 6edc086..3010fc1 100644 --- a/resources.qrc +++ b/resources.qrc @@ -36,5 +36,11 @@ qml/icons/arrow-right.png qml/TreeView.qml qml/BasicRectangle.qml + qml/icons/edit-find.png + qml/icons/edit-clear.png + qml/EditKeyWindow.qml + qml/KeyWindow.qml + qml/ExitDialog.qml + qml/MainMenuBar.qml diff --git a/src/confignode.cpp b/src/confignode.cpp index 0f66d79..b03d85a 100644 --- a/src/confignode.cpp +++ b/src/confignode.cpp @@ -3,37 +3,49 @@ using namespace kdb; -ConfigNode::ConfigNode(const QString& name, const QString& path, const Key &key) +ConfigNode::ConfigNode(const QString& name, const QString& path, const Key &key, TreeViewModel *parentModel) : m_name(name) , m_path(path) , m_key(key) , m_children(new TreeViewModel) , m_metaData(new TreeViewModel) + , m_parentModel(parentModel) { - // TODO: why not give full path? (or even better, pass Key - // with getBaseName() and getName()) - if (m_key && m_key.isString()) m_value = QVariant::fromValue(QString::fromStdString(m_key.getString())); - else if (m_key && m_key.isBinary()) m_value = QVariant::fromValue(QString::fromStdString(m_key.getBinary())); + + if(m_key) + populateMetaModel(); } ConfigNode::ConfigNode(const ConfigNode& other) : QObject() - , m_children(new TreeViewModel) - , m_metaData(new TreeViewModel) + , m_name(other.m_name) + , m_path(other.m_path) + , m_value(other.m_value) + , m_key(other.m_key.dup()) + , m_children(new TreeViewModel()) + , m_metaData(new TreeViewModel()) + , m_parentModel(other.m_parentModel) { - m_name = other.m_name; - m_path = other.m_path; - m_value = other.m_value; - m_key = other.m_key; + + foreach(ConfigNode *node, other.getChildren()->model()) + { + m_children->append(new ConfigNode(*node)); + } + + foreach(ConfigNode *node, other.getMetaKeys()->model()) + { + m_metaData->append(new ConfigNode(*node)); + } } ConfigNode::ConfigNode() : m_children(new TreeViewModel) , m_metaData(new TreeViewModel) + , m_parentModel(new TreeViewModel) { } @@ -41,6 +53,10 @@ ConfigNode::~ConfigNode() { delete m_children; delete m_metaData; + m_parentModel = NULL; + + if(m_key) + m_key.clear(); } int ConfigNode::getChildCount() const @@ -65,27 +81,69 @@ QVariant ConfigNode::getValue() const void ConfigNode::setName(const QString& name) { + // qDebug() << "ConfigNode::setName: Node with name " << m_name << " has new name " << name; m_name = name; - m_key.setBaseName(name.toStdString()); + + if(QString::fromStdString(m_key.getName()) != ""){ + // qDebug() << "ConfigNode::setName: Key with name " << QString::fromStdString(m_key.getName()) << " has new base name " << name; + m_key.setBaseName(name.toStdString()); + } } void ConfigNode::setValue(const QVariant& value) { m_value = value; - m_key.setString(value.toString().toStdString()); + + if(m_key) + m_key.setString(value.toString().toStdString()); } void ConfigNode::setMeta(const QString &name, const QVariant &value) { - deleteMeta(m_name); + // qDebug() << "ConfigNode::setMeta: metaNode " << m_name << " has new metaname " << name; m_name = name; m_value = value; - m_key.setMeta(name.toStdString(), value.toString().toStdString()); + + if(m_key) + { + // deleteMeta(m_name); + // qDebug() << "ConfigNode::setMeta: key " << QString::fromStdString(m_key.getName()) << " has new metakey " << name; + m_key.setMeta(name.toStdString(), value.toString().toStdString()); + } +} + +void ConfigNode::setMeta(const QVariantMap &metaData) +{ + for(int i = 0; i < m_metaData->model().size(); i++) + { + m_metaData->model().at(i)->deleteMeta(m_metaData->model().at(i)->getName()); + } + + m_metaData->clear(); + + for(int i = 0; i < metaData.size(); i++) + { + m_metaData->insertMetaRow(i, this); + } + + int counter = 0; + + for(QVariantMap::const_iterator iter = metaData.begin(); iter != metaData.end(); iter++) + { + QVariantList tmp; + tmp << iter.key() << iter.value(); + m_metaData->setData(counter, tmp, "MetaValue"); + counter++; + } } void ConfigNode::deleteMeta(const QString &name) { - m_key.delMeta(name.toStdString()); + if(m_key) + { + // qDebug() << "metakey " << name << " of node " << m_name << " deleted"; + m_key.delMeta(name.toStdString()); + } } void ConfigNode::accept(Visitor &visitor) @@ -96,35 +154,54 @@ void ConfigNode::accept(Visitor &visitor) node->accept(visitor); } -Key ConfigNode::getKey() +Key ConfigNode::getKey() const { return m_key; } -void ConfigNode::appendChild(ConfigNode* node) +void ConfigNode::invalidateKey() { - m_children->model().append(node); + // qDebug() << "ConfigNode::deleteKey: clearing key " << QString::fromStdString(m_key.getName()); + m_key.clear(); } -bool ConfigNode::hasChild(const QString& name) const +void ConfigNode::deletePath(QStringList &path) { - foreach (ConfigNode * node, m_children->model()) + if(path.count() == 0) + return; + + QString name = path.takeFirst(); + int index = getIndexByName(name); + ConfigNode *node = getChildByName(name); + + node->deletePath(path); + + if(node->getChildCount() == 0) + m_children->removeRow(index); +} + +int ConfigNode::getIndexByName(const QString &name) +{ + for(int i = 0; i < m_children->model().count(); i++) { - if (node->getName() == name) - { - return true; - } + if(m_children->model().at(i)->getName() == name) + return i; } - return false; + return -1; } -TreeViewModel* ConfigNode::getChildren() +TreeViewModel *ConfigNode::getParentModel() { - return m_children; + return m_parentModel; } -TreeViewModel* ConfigNode::getMetaValue() +void ConfigNode::setParentModel(TreeViewModel *parent) +{ + m_parentModel = parent; +} + +void ConfigNode::populateMetaModel() { if (m_key) { @@ -133,18 +210,60 @@ TreeViewModel* ConfigNode::getMetaValue() while (m_key.nextMeta()) { - ConfigNode* node = new ConfigNode("", "", m_key); - node->setName(QString::fromStdString(m_key.currentMeta().getName())); - node->setValue(QString::fromStdString(m_key.currentMeta().getString())); + // qDebug() << "ConfigNode::populateMetaModel: key " << QString::fromStdString(m_key.getName()) << " has metakey " << QString::fromStdString(m_key.currentMeta().getName()); + ConfigNode* node = new ConfigNode(); + + node->setName(QString::fromStdString(m_key.getName())); + node->setKey(m_key); + node->setMeta(QString::fromStdString(m_key.currentMeta().getName()), QVariant::fromValue(QString::fromStdString(m_key.currentMeta().getString()))); + m_metaData->model().append(node); } + } +} + +void ConfigNode::setKey(Key key) +{ + m_key = key; +} + +void ConfigNode::setKeyName(const QString &name) +{ + if(!m_key) + m_key = Key(); + + m_key.setName(name.toStdString()); +} + +void ConfigNode::appendChild(ConfigNode* node) +{ + m_children->append(node); +} +bool ConfigNode::hasChild(const QString& name) const +{ + foreach (ConfigNode * node, m_children->model()) + { + if (node->getName() == name) + { + return true; + } } + return false; +} + +TreeViewModel* ConfigNode::getChildren() const +{ + return m_children; +} + +TreeViewModel* ConfigNode::getMetaKeys() const +{ return m_metaData; } -ConfigNode* ConfigNode::getChildByName(QString& name) +ConfigNode* ConfigNode::getChildByName(QString& name) const { foreach (ConfigNode * node, m_children->model()) { @@ -157,7 +276,7 @@ ConfigNode* ConfigNode::getChildByName(QString& name) return NULL; } -ConfigNode* ConfigNode::getChildByIndex(int index) +ConfigNode* ConfigNode::getChildByIndex(int index) const { if (index >= 0 && index < m_children->model().length()) return m_children->model().at(index); @@ -165,6 +284,11 @@ ConfigNode* ConfigNode::getChildByIndex(int index) return NULL; } +void ConfigNode::setPath(const QString &path) +{ + m_path = path; +} + bool ConfigNode::childrenHaveNoChildren() const { int children = 0; diff --git a/src/confignode.hpp b/src/confignode.hpp index 69400ed..c624bea 100644 --- a/src/confignode.hpp +++ b/src/confignode.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "treeviewmodel.hpp" #include "printvisitor.hpp" @@ -17,8 +18,8 @@ class ConfigNode : public QObject Q_OBJECT public: - // TODO: why not pass the Key directly - explicit ConfigNode(const QString& name, const QString& path, const kdb::Key &key); + + explicit ConfigNode(const QString& name, const QString& path, const kdb::Key &key, TreeViewModel *parentModel); /// Needed by Qt ConfigNode(const ConfigNode& other); /// Needed by Qt @@ -78,13 +79,13 @@ class ConfigNode : public QObject * @brief Get the children of this ConfigNode. * @return The children of this ConfigNode as model. */ - TreeViewModel* getChildren(); + TreeViewModel* getChildren() const; /** * @brief Get the metakeys of this ConfigNode. * @return The metakeys of this ConfigNode as model. */ - TreeViewModel* getMetaValue(); + TreeViewModel* getMetaKeys() const; /** * @brief Returns if the children of this ConfigNode have any children themselves. @@ -97,20 +98,30 @@ class ConfigNode : public QObject * @param name The name of the child which is looked for. * @return The child with the given name if it is a child of this ConfigNode. */ - ConfigNode* getChildByName(QString& name); + ConfigNode* getChildByName(QString& name) const; /** * @brief Returns a child on a given index. * @param index The index of the wanted child. * @return The child on the given index. */ - Q_INVOKABLE ConfigNode* getChildByIndex(int index); + Q_INVOKABLE ConfigNode* getChildByIndex(int index) const; + + void setPath(const QString &path); - void setMeta(const QString &name, const QVariant &value); - Q_INVOKABLE void deleteMeta(const QString &name); + void setMeta(const QString &name, const QVariant &value); + Q_INVOKABLE void setMeta(const QVariantMap &metaData); + Q_INVOKABLE void deleteMeta(const QString &name); - void accept(Visitor &visitor); - kdb::Key getKey(); + void accept(Visitor &visitor); + kdb::Key getKey() const; + void setKey(kdb::Key key); + void setKeyName(const QString &name); + void invalidateKey(); + void deletePath(QStringList &path); + int getIndexByName(const QString &name); + TreeViewModel* getParentModel(); + void setParentModel(TreeViewModel *parent); private: // TODO: not needed if we hold the Key @@ -122,6 +133,9 @@ class ConfigNode : public QObject kdb::Key m_key; TreeViewModel* m_children; TreeViewModel* m_metaData; + TreeViewModel* m_parentModel; + + void populateMetaModel(); }; Q_DECLARE_METATYPE(ConfigNode) diff --git a/src/copykeycommand.cpp b/src/copykeycommand.cpp new file mode 100644 index 0000000..42b39a1 --- /dev/null +++ b/src/copykeycommand.cpp @@ -0,0 +1,24 @@ +#include "copykeycommand.hpp" +#include "treeviewmodel.hpp" + +CopyKeyCommand::CopyKeyCommand(ConfigNode *source, ConfigNode *target, QUndoCommand *parent) + : QUndoCommand(parent) + , m_source(*source) + , m_target(target) +{ + setText("copy"); +} + +void CopyKeyCommand::undo() +{ + m_target->getChildren()->removeRow(m_target->getIndexByName(m_source.getName())); +} + +void CopyKeyCommand::redo() +{ + QString newPath = m_target->getPath() + "/" + m_source.getName(); + m_source.setPath(newPath); + m_source.setKeyName(newPath); + + m_target->appendChild(new ConfigNode(m_source)); +} diff --git a/src/copykeycommand.hpp b/src/copykeycommand.hpp new file mode 100644 index 0000000..7425459 --- /dev/null +++ b/src/copykeycommand.hpp @@ -0,0 +1,23 @@ +#ifndef COPYKEYCOMMAND_H +#define COPYKEYCOMMAND_H + +#include +#include "confignode.hpp" + +#include + +class CopyKeyCommand : public QUndoCommand +{ +public: + explicit CopyKeyCommand(ConfigNode *source, ConfigNode *target, QUndoCommand *parent = 0); + + virtual void undo(); + virtual void redo(); + +private: + + ConfigNode m_source; + ConfigNode *m_target; +}; + +#endif // COPYKEYCOMMAND_H diff --git a/src/cutkeycommand.cpp b/src/cutkeycommand.cpp new file mode 100644 index 0000000..66c9891 --- /dev/null +++ b/src/cutkeycommand.cpp @@ -0,0 +1,28 @@ +#include "cutkeycommand.hpp" + +CutKeyCommand::CutKeyCommand(TreeViewModel *model, ConfigNode *source, ConfigNode *target, int index, QUndoCommand *parent) + : QUndoCommand(parent) + , m_model(model) + , m_source(*source) + , m_target(target) + , m_index(index) +{ + setText("cut"); +} + +void CutKeyCommand::undo() +{ + m_model->insertRow(m_index, new ConfigNode(m_source)); + m_target->getChildren()->removeRow(m_target->getIndexByName(m_source.getName())); +} + +void CutKeyCommand::redo() +{ + QString newPath = m_target->getPath() + "/" + m_source.getName(); + m_source.setPath(newPath); + m_source.setKeyName(newPath); + + m_target->appendChild(new ConfigNode(m_source)); + + m_model->removeRow(m_index); +} diff --git a/src/cutkeycommand.hpp b/src/cutkeycommand.hpp new file mode 100644 index 0000000..3153122 --- /dev/null +++ b/src/cutkeycommand.hpp @@ -0,0 +1,23 @@ +#ifndef CUTKEYCOMMAND_H +#define CUTKEYCOMMAND_H + +#include +#include "treeviewmodel.hpp" + +class CutKeyCommand : public QUndoCommand +{ +public: + explicit CutKeyCommand(TreeViewModel *model, ConfigNode *source, ConfigNode *target, int index, QUndoCommand *parent = 0); + + virtual void undo(); + virtual void redo(); + +private: + + TreeViewModel *m_model; + ConfigNode m_source; + ConfigNode *m_target; + int m_index; +}; + +#endif // CUTKEYCOMMAND_H diff --git a/src/deletekeycommand.cpp b/src/deletekeycommand.cpp new file mode 100644 index 0000000..6b1be0e --- /dev/null +++ b/src/deletekeycommand.cpp @@ -0,0 +1,20 @@ +#include "deletekeycommand.hpp" + +DeleteKeyCommand::DeleteKeyCommand(const QString &type, TreeViewModel *model, ConfigNode *node, int index, QUndoCommand *parent) + : QUndoCommand(parent) + , m_model(model) + , m_node(*node) + , m_index(index) +{ + setText(type); +} + +void DeleteKeyCommand::undo() +{ + m_model->insertRow(m_index, new ConfigNode(m_node)); +} + +void DeleteKeyCommand::redo() +{ + m_model->removeRow(m_index); +} diff --git a/src/deletekeycommand.hpp b/src/deletekeycommand.hpp new file mode 100644 index 0000000..74d168b --- /dev/null +++ b/src/deletekeycommand.hpp @@ -0,0 +1,24 @@ +#ifndef DELETEKEYCOMMAND_HPP +#define DELETEKEYCOMMAND_HPP + +#include +#include "treeviewmodel.hpp" + +class DeleteKeyCommand : public QUndoCommand +{ + +public: + explicit DeleteKeyCommand(const QString &type, TreeViewModel *model, ConfigNode *node, int index, QUndoCommand *parent = 0); + + virtual void undo(); + virtual void redo(); + +private: + + TreeViewModel *m_model; + ConfigNode m_node; + int m_index; + +}; + +#endif // DELETEKEYCOMMAND_HPP diff --git a/src/dynamictreemodel.cpp b/src/dynamictreemodel.cpp new file mode 100644 index 0000000..6a75a3d --- /dev/null +++ b/src/dynamictreemodel.cpp @@ -0,0 +1,402 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Stephen Kelly +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "dynamictreemodel.h" + +#include +#include +#include +#include + + +DynamicTreeModel::DynamicTreeModel(QObject *parent) + : QAbstractItemModel(parent), + nextId(1) +{ +} + +QModelIndex DynamicTreeModel::index(int row, int column, const QModelIndex &parent) const +{ +// if (column != 0) +// return QModelIndex(); + + + if ( column < 0 || row < 0 ) + return QModelIndex(); + + QList > childIdColumns = m_childItems.value(parent.internalId()); + + const qint64 grandParent = findParentId(parent.internalId()); + if (grandParent >= 0) { + QList > parentTable = m_childItems.value(grandParent); + if (parent.column() >= parentTable.size()) + qFatal("%s: parent.column() must be less than parentTable.size()", Q_FUNC_INFO); + QList parentSiblings = parentTable.at(parent.column()); + if (parent.row() >= parentSiblings.size()) + qFatal("%s: parent.row() must be less than parentSiblings.size()", Q_FUNC_INFO); + } + + if (childIdColumns.size() == 0) + return QModelIndex(); + + if (column >= childIdColumns.size()) + return QModelIndex(); + + QList rowIds = childIdColumns.at(column); + + if ( row >= rowIds.size()) + return QModelIndex(); + + qint64 id = rowIds.at(row); + + return createIndex(row, column, reinterpret_cast(id)); + +} + +qint64 DynamicTreeModel::findParentId(qint64 searchId) const +{ + if (searchId <= 0) + return -1; + + QHashIterator > > i(m_childItems); + while (i.hasNext()) + { + i.next(); + QListIterator > j(i.value()); + while (j.hasNext()) + { + QList l = j.next(); + if (l.contains(searchId)) + { + return i.key(); + } + } + } + return -1; +} + +QModelIndex DynamicTreeModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + qint64 searchId = index.internalId(); + qint64 parentId = findParentId(searchId); + // Will never happen for valid index, but what the hey... + if (parentId <= 0) + return QModelIndex(); + + qint64 grandParentId = findParentId(parentId); + if (grandParentId < 0) + grandParentId = 0; + + int column = 0; + QList childList = m_childItems.value(grandParentId).at(column); + + int row = childList.indexOf(parentId); + + return createIndex(row, column, reinterpret_cast(parentId)); + +} + +int DynamicTreeModel::rowCount(const QModelIndex &index ) const +{ + QList > cols = m_childItems.value(index.internalId()); + + if (cols.size() == 0 ) + return 0; + + if (index.column() > 0) + return 0; + + return cols.at(0).size(); +} + +int DynamicTreeModel::columnCount(const QModelIndex &index ) const +{ +// Q_UNUSED(index); + return m_childItems.value(index.internalId()).size(); +} + +QVariant DynamicTreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (Qt::DisplayRole == role) + { + return m_items.value(index.internalId()); + } + return QVariant(); +} + +void DynamicTreeModel::clear() +{ + beginResetModel(); + m_items.clear(); + m_childItems.clear(); + nextId = 1; + endResetModel(); +} + + +ModelChangeCommand::ModelChangeCommand( DynamicTreeModel *model, QObject *parent ) + : QObject(parent), m_model(model), m_numCols(1), m_startRow(-1), m_endRow(-1) +{ + +} + +QModelIndex ModelChangeCommand::findIndex(QList rows) +{ + const int col = 0; + QModelIndex parent = QModelIndex(); + QListIterator i(rows); + while (i.hasNext()) + { + parent = m_model->index(i.next(), col, parent); + if (!parent.isValid()) + qFatal("%s: parent must be valid", Q_FUNC_INFO); + } + return parent; +} + +ModelInsertCommand::ModelInsertCommand(DynamicTreeModel *model, QObject *parent ) + : ModelChangeCommand(model, parent) +{ + +} + +void ModelInsertCommand::doCommand() +{ + QModelIndex parent = findIndex(m_rowNumbers); + m_model->beginInsertRows(parent, m_startRow, m_endRow); + qint64 parentId = parent.internalId(); + for (int row = m_startRow; row <= m_endRow; row++) + { + for(int col = 0; col < m_numCols; col++ ) + { + if (m_model->m_childItems[parentId].size() <= col) + { + m_model->m_childItems[parentId].append(QList()); + } +// QString name = QUuid::createUuid().toString(); + qint64 id = m_model->newId(); + QString name = QString::number(id); + + m_model->m_items.insert(id, name); + m_model->m_childItems[parentId][col].insert(row, id); + + } + } + m_model->endInsertRows(); +} + + +ModelMoveCommand::ModelMoveCommand(DynamicTreeModel *model, QObject *parent) + : ModelChangeCommand(model, parent) +{ + +} +bool ModelMoveCommand::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) +{ + return m_model->beginMoveRows(srcParent, srcStart, srcEnd, destParent, destRow); +} + +void ModelMoveCommand::doCommand() +{ + QModelIndex srcParent = findIndex(m_rowNumbers); + QModelIndex destParent = findIndex(m_destRowNumbers); + + if (!emitPreSignal(srcParent, m_startRow, m_endRow, destParent, m_destRow)) + { + return; + } + + for (int column = 0; column < m_numCols; ++column) + { + QList l = m_model->m_childItems.value(srcParent.internalId())[column].mid(m_startRow, m_endRow - m_startRow + 1 ); + + for (int i = m_startRow; i <= m_endRow ; i++) + { + m_model->m_childItems[srcParent.internalId()][column].removeAt(m_startRow); + } + int d; + if (m_destRow < m_startRow) + d = m_destRow; + else + { + if (srcParent == destParent) + d = m_destRow - (m_endRow - m_startRow + 1); + else + d = m_destRow - (m_endRow - m_startRow) + 1; + } + + foreach(const qint64 id, l) + { + m_model->m_childItems[destParent.internalId()][column].insert(d++, id); + } + } + + emitPostSignal(); +} + +void ModelMoveCommand::emitPostSignal() +{ + m_model->endMoveRows(); +} + +ModelResetCommand::ModelResetCommand(DynamicTreeModel* model, QObject* parent) + : ModelMoveCommand(model, parent) +{ + +} + +ModelResetCommand::~ModelResetCommand() +{ + +} + +bool ModelResetCommand::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) +{ + Q_UNUSED(srcParent); + Q_UNUSED(srcStart); + Q_UNUSED(srcEnd); + Q_UNUSED(destParent); + Q_UNUSED(destRow); + + return true; +} + +void ModelResetCommand::emitPostSignal() +{ + m_model->reset(); +} + +ModelResetCommandFixed::ModelResetCommandFixed(DynamicTreeModel* model, QObject* parent) + : ModelMoveCommand(model, parent) +{ + +} + +ModelResetCommandFixed::~ModelResetCommandFixed() +{ + +} + +bool ModelResetCommandFixed::emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow) +{ + Q_UNUSED(srcParent); + Q_UNUSED(srcStart); + Q_UNUSED(srcEnd); + Q_UNUSED(destParent); + Q_UNUSED(destRow); + + m_model->beginResetModel(); + return true; +} + +void ModelResetCommandFixed::emitPostSignal() +{ + m_model->endResetModel(); +} + +ModelChangeChildrenLayoutsCommand::ModelChangeChildrenLayoutsCommand(DynamicTreeModel* model, QObject* parent) + : ModelChangeCommand(model, parent) +{ + +} + +void ModelChangeChildrenLayoutsCommand::doCommand() +{ + const QPersistentModelIndex parent1 = findIndex(m_rowNumbers); + const QPersistentModelIndex parent2 = findIndex(m_secondRowNumbers); + + QList parents; + parents << parent1; + parents << parent2; + + emit m_model->layoutAboutToBeChanged(parents); + + int rowSize1 = -1; + int rowSize2 = -1; + + for (int column = 0; column < m_numCols; ++column) + { + { + QList &l = m_model->m_childItems[parent1.internalId()][column]; + rowSize1 = l.size(); + l.prepend(l.takeLast()); + } + { + QList &l = m_model->m_childItems[parent2.internalId()][column]; + rowSize2 = l.size(); + l.append(l.takeFirst()); + } + } + + // If we're changing one of the parent indexes, we need to ensure that we do that before + // changing any children of that parent. The reason is that we're keeping parent1 and parent2 + // around as QPersistentModelIndex instances, and we query idx.parent() in the loop. + QModelIndexList persistent = m_model->persistentIndexList(); + foreach (const QModelIndex &parent, parents) { + int idx = persistent.indexOf(parent); + if (idx != -1) + persistent.move(idx, 0); + } + + foreach (const QModelIndex &idx, persistent) { + if (idx.parent() == parent1) { + if (idx.row() == rowSize1 - 1) { + m_model->changePersistentIndex(idx, m_model->createIndex(0, idx.column(), idx.internalPointer())); + } else { + m_model->changePersistentIndex(idx, m_model->createIndex(idx.row() + 1, idx.column(), idx.internalPointer())); + } + } else if (idx.parent() == parent2) { + if (idx.row() == 0) { + m_model->changePersistentIndex(idx, m_model->createIndex(rowSize2 - 1, idx.column(), idx.internalPointer())); + } else { + m_model->changePersistentIndex(idx, m_model->createIndex(idx.row() - 1, idx.column(), idx.internalPointer())); + } + } + } + + emit m_model->layoutChanged(parents); +} diff --git a/src/dynamictreemodel.h b/src/dynamictreemodel.h new file mode 100644 index 0000000..8fd2a54 --- /dev/null +++ b/src/dynamictreemodel.h @@ -0,0 +1,214 @@ +/**************************************************************************** +** +** Copyright (C) 2009 Stephen Kelly +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef DYNAMICTREEMODEL_H +#define DYNAMICTREEMODEL_H + +#include + +#include +#include + + +class DynamicTreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + DynamicTreeModel(QObject *parent = 0); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; + int rowCount(const QModelIndex &index = QModelIndex()) const; + int columnCount(const QModelIndex &index = QModelIndex()) const; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + void clear(); + +protected slots: + + /** + Finds the parent id of the string with id @p searchId. + + Returns -1 if not found. + */ + qint64 findParentId(qint64 searchId) const; + +private: + QHash m_items; + QHash > > m_childItems; + qint64 nextId; + qint64 newId() { return nextId++; }; + + QModelIndex m_nextParentIndex; + int m_nextRow; + + int m_depth; + int maxDepth; + + friend class ModelInsertCommand; + friend class ModelMoveCommand; + friend class ModelResetCommand; + friend class ModelResetCommandFixed; + friend class ModelChangeChildrenLayoutsCommand; + +}; + + +class ModelChangeCommand : public QObject +{ + Q_OBJECT +public: + + ModelChangeCommand( DynamicTreeModel *model, QObject *parent = 0 ); + + virtual ~ModelChangeCommand() {} + + void setAncestorRowNumbers(QList rowNumbers) { m_rowNumbers = rowNumbers; } + + QModelIndex findIndex(QList rows); + + void setStartRow(int row) { m_startRow = row; } + + void setEndRow(int row) { m_endRow = row; } + + void setNumCols(int cols) { m_numCols = cols; } + + virtual void doCommand() = 0; + +protected: + DynamicTreeModel* m_model; + QList m_rowNumbers; + int m_numCols; + int m_startRow; + int m_endRow; + +}; + +typedef QList ModelChangeCommandList; + +class ModelInsertCommand : public ModelChangeCommand +{ + Q_OBJECT + +public: + + ModelInsertCommand(DynamicTreeModel *model, QObject *parent = 0 ); + virtual ~ModelInsertCommand() {} + + virtual void doCommand(); +}; + + +class ModelMoveCommand : public ModelChangeCommand +{ + Q_OBJECT +public: + ModelMoveCommand(DynamicTreeModel *model, QObject *parent); + + virtual ~ModelMoveCommand() {} + + virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); + + virtual void doCommand(); + + virtual void emitPostSignal(); + + void setDestAncestors( QList rows ) { m_destRowNumbers = rows; } + + void setDestRow(int row) { m_destRow = row; } + +protected: + QList m_destRowNumbers; + int m_destRow; +}; + +/** + A command which does a move and emits a reset signal. +*/ +class ModelResetCommand : public ModelMoveCommand +{ + Q_OBJECT +public: + ModelResetCommand(DynamicTreeModel* model, QObject* parent = 0); + + virtual ~ModelResetCommand(); + + virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); + virtual void emitPostSignal(); + +}; + +/** + A command which does a move and emits a beginResetModel and endResetModel signals. +*/ +class ModelResetCommandFixed : public ModelMoveCommand +{ + Q_OBJECT +public: + ModelResetCommandFixed(DynamicTreeModel* model, QObject* parent = 0); + + virtual ~ModelResetCommandFixed(); + + virtual bool emitPreSignal(const QModelIndex &srcParent, int srcStart, int srcEnd, const QModelIndex &destParent, int destRow); + virtual void emitPostSignal(); + +}; + +class ModelChangeChildrenLayoutsCommand : public ModelChangeCommand +{ + Q_OBJECT +public: + ModelChangeChildrenLayoutsCommand(DynamicTreeModel *model, QObject *parent); + + virtual ~ModelChangeChildrenLayoutsCommand() {} + + virtual void doCommand(); + + void setSecondAncestorRowNumbers( QList rows ) { m_secondRowNumbers = rows; } + +protected: + QList m_secondRowNumbers; + int m_destRow; +}; + +#endif diff --git a/src/editkeycommand.cpp b/src/editkeycommand.cpp new file mode 100644 index 0000000..434e94d --- /dev/null +++ b/src/editkeycommand.cpp @@ -0,0 +1,30 @@ +#include "editkeycommand.hpp" + +EditKeyCommand::EditKeyCommand(TreeViewModel *model, int index, const QString &oldName, const QVariant &oldValue, const QVariantMap &oldMetaData, + const QString &newName, const QVariant &newValue, const QVariantMap &newMetaData, QUndoCommand *parent) + : QUndoCommand(parent) + , m_model(model) + , m_index(index) + , m_oldName(oldName) + , m_oldValue(oldValue) + , m_oldMetaData(oldMetaData) + , m_newName(newName) + , m_newValue(newValue) + , m_newMetaData(newMetaData) +{ + setText("edit"); +} + +void EditKeyCommand::undo() +{ + m_model->setData(m_index, m_oldName, "Name"); + m_model->setData(m_index, m_oldValue, "Value"); + m_model->model().at(m_index)->setMeta(m_oldMetaData); +} + +void EditKeyCommand::redo() +{ + m_model->setData(m_index, m_newName, "Name"); + m_model->setData(m_index, m_newValue, "Value"); + m_model->model().at(m_index)->setMeta(m_newMetaData); +} diff --git a/src/editkeycommand.hpp b/src/editkeycommand.hpp new file mode 100644 index 0000000..a1b1ef6 --- /dev/null +++ b/src/editkeycommand.hpp @@ -0,0 +1,30 @@ +#ifndef EDITKEYCOMMAND_HPP +#define EDITKEYCOMMAND_HPP + +#include +#include "treeviewmodel.hpp" + +class EditKeyCommand : public QUndoCommand +{ + +public: + explicit EditKeyCommand(TreeViewModel *model, int index, const QString &oldName, const QVariant &oldValue, const QVariantMap &oldMetaData, + const QString &newName, const QVariant &newValue, const QVariantMap &newMetaData, QUndoCommand *parent = 0); + virtual void undo(); + virtual void redo(); + +private: + + TreeViewModel* m_model; + int m_index; + + QString m_oldName; + QVariant m_oldValue; + QVariantMap m_oldMetaData; + + QString m_newName; + QVariant m_newValue; + QVariantMap m_newMetaData; +}; + +#endif // EDITKEYCOMMAND_HPP diff --git a/src/keysetvisitor.cpp b/src/keysetvisitor.cpp index 1eb0c39..7d260fd 100644 --- a/src/keysetvisitor.cpp +++ b/src/keysetvisitor.cpp @@ -1,14 +1,36 @@ #include "keysetvisitor.hpp" +using namespace kdb; + +KeySetVisitor::KeySetVisitor(KeySet &keySet) +{ + m_set = keySet; +} + void KeySetVisitor::visit(ConfigNode *node) { - set.append(node->getKey()); + Key key = node->getKey(); + + if(key && key.isValid()){ + qDebug() << "Appending key " << QString::fromStdString(key.getName()); + m_set.append(key); + } + else{ + qDebug() << "Key of node " << node->getName() << " is null or invalid"; + } } void KeySetVisitor::visit(TreeViewModel *model) { + qDebug() << "==================================="; foreach (ConfigNode *node, model->model()) node->accept(*this); + qDebug() << "==================================="; + + model->populateModel(); +} - qDebug() << "set size " << set.size(); +KeySet &KeySetVisitor::getKeySet() +{ + return m_set; } diff --git a/src/keysetvisitor.hpp b/src/keysetvisitor.hpp index 77d2b11..e0cf760 100644 --- a/src/keysetvisitor.hpp +++ b/src/keysetvisitor.hpp @@ -8,11 +8,13 @@ class KeySetVisitor : public Visitor { public: + explicit KeySetVisitor(kdb::KeySet &keySet); void visit(ConfigNode *node); void visit(TreeViewModel *model); + kdb::KeySet& getKeySet(); private: - kdb::KeySet set; + kdb::KeySet m_set; }; #endif // KEYSETVISITOR_HPP diff --git a/src/main.cpp b/src/main.cpp index ef80658..40fa9f7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,40 +2,45 @@ #include #include #include +#include +#include #include "treeviewmodel.hpp" #include "confignode.hpp" +#include "undomanager.hpp" +#include "modeltest/modeltest.h" int main(int argc, char* argv[]) { - QApplication app(argc, argv); + QApplication app(argc, argv); - qRegisterMetaType ("TreeViewModel"); - qRegisterMetaType ("ConfigNode"); + qRegisterMetaType ("TreeViewModel"); + qRegisterMetaType ("ConfigNode"); + qRegisterMetaType ("UndoManager"); - QString locale = QLocale::system().name(); + QString locale = QLocale::system().name(); - QTranslator tr; - tr.load(QString(":/qml/i18n/lang_") + locale + QString(".qm")); - app.installTranslator(&tr); + QTranslator tr; + tr.load(QString(":/qml/i18n/lang_") + locale + QString(".qm")); + app.installTranslator(&tr); - QQmlApplicationEngine engine; - QQmlContext* ctxt = engine.rootContext(); + QQmlApplicationEngine engine; + QQmlContext* ctxt = engine.rootContext(); - kdb::KDB kdb; - kdb::KeySet config; - kdb.get(config, "/"); + UndoManager manager; + kdb::KeySet set; - TreeViewModel* model = new TreeViewModel; - model->populateModel(config); - ctxt->setContextProperty("externTreeModel", QVariant::fromValue(model)); - engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); + TreeViewModel* model = new TreeViewModel(set); + //new ModelTest(model); - PrintVisitor printer; - model->accept(printer); + model->populateModel(); - KeySetVisitor ksVisit; - model->accept(ksVisit); + ctxt->setContextProperty("undoManager", &manager); + ctxt->setContextProperty("externTreeModel", model); + engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); - return app.exec(); +// PrintVisitor printer; +// model->accept(printer); + + return app.exec(); } diff --git a/src/newkeycommand.cpp b/src/newkeycommand.cpp new file mode 100644 index 0000000..3d8f83e --- /dev/null +++ b/src/newkeycommand.cpp @@ -0,0 +1,21 @@ +#include "newkeycommand.hpp" + +NewKeyCommand::NewKeyCommand(TreeViewModel *model, const QString &path, const QString &value, const QVariantMap &metaData, QUndoCommand *parent) + : QUndoCommand(parent) + , m_model(model) + , m_path(path) + , m_value(value) + , m_metaData(metaData) +{ + setText("new"); +} + +void NewKeyCommand::undo() +{ + m_model->deletePath(m_path); +} + +void NewKeyCommand::redo() +{ + m_model->createNewNode(m_path, m_value, m_metaData); +} diff --git a/src/newkeycommand.hpp b/src/newkeycommand.hpp new file mode 100644 index 0000000..606a07d --- /dev/null +++ b/src/newkeycommand.hpp @@ -0,0 +1,25 @@ +#ifndef NEWKEYCOMMAND_HPP +#define NEWKEYCOMMAND_HPP + +#include +#include "treeviewmodel.hpp" + +class NewKeyCommand : public QUndoCommand +{ + +public: + explicit NewKeyCommand(TreeViewModel *model, const QString &path, const QString &value, const QVariantMap &metaData, QUndoCommand *parent = 0); + + virtual void undo(); + virtual void redo(); + +private: + + TreeViewModel *m_model; + QString m_path; + QString m_value; + QVariantMap m_metaData; + +}; + +#endif // NEWKEYCOMMAND_HPP diff --git a/src/treeviewmodel.cpp b/src/treeviewmodel.cpp index fa50b66..5f07c14 100644 --- a/src/treeviewmodel.cpp +++ b/src/treeviewmodel.cpp @@ -5,7 +5,13 @@ using namespace kdb; TreeViewModel::TreeViewModel(QObject* parent) { - Q_UNUSED(parent) + Q_UNUSED(parent); +} + +TreeViewModel::TreeViewModel(KeySet &keySet) + : m_keySet(keySet) +{ + m_kdb.get(m_keySet, ""); } TreeViewModel::TreeViewModel(const TreeViewModel& other) @@ -30,22 +36,20 @@ QVariant TreeViewModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) { - qDebug() << "TreeVieModel::data: index not valid"; + qDebug() << "TreeViewModel::data: index not valid. Index = " << index.row() << " Model size = " << m_model.size(); // TODO: why is this function called with wrong index? return QVariant(); } if (index.row() > (m_model.size() - 1)) { - qDebug() << "TreeVieModel::data: row too high" << index.row(); + qDebug() << "TreeViewModel::data: row too high" << index.row(); // TODO: why is this function called with wrong index? return QVariant(); } ConfigNode* node = m_model.at(index.row()); - //qDebug() << "Role: " << role; - switch (role) { @@ -70,11 +74,17 @@ QVariant TreeViewModel::data(const QModelIndex& index, int role) const return QVariant::fromValue(node->childrenHaveNoChildren()); case MetaValueRole: - return QVariant::fromValue(node->getMetaValue()); + return QVariant::fromValue(node->getMetaKeys()); case NodeRole: return QVariant::fromValue(node); + case ParentModelRole: + return QVariant::fromValue(node->getParentModel()); + + case IndexRole: + return QVariant::fromValue(index.row()); + default: qDebug() << "Unknown role " << role; return QVariant(); @@ -87,7 +97,7 @@ bool TreeViewModel::setData(const QModelIndex& index, const QVariant& value, int { if (!index.isValid() || index.row() > (m_model.size() - 1)) { - qDebug() << "Wrong index called"; + qDebug() << "TreeViewModel::setData: Wrong index called"; return false; } @@ -116,11 +126,11 @@ bool TreeViewModel::setData(const QModelIndex& index, const QVariant& value, int // TODO: Why are there two implementations of setData needed? // Because QML cannot call setData() directly (see https://bugreports.qt-project.org/browse/QTBUG-7932) -void TreeViewModel::setDataValue(int index, const QVariant& value, const QString& role) +void TreeViewModel::setData(int index, const QVariant& value, const QString& role) { if (index < 0 || index > m_model.size() - 1) { - qDebug() << "setDataValue: Wrong index called. model.size = " << m_model.size() << " index = " << index; + qDebug() << "TreeViewModel::setData: Wrong index called. model.size = " << m_model.size() << " index = " << index; return; } @@ -142,6 +152,48 @@ void TreeViewModel::setDataValue(int index, const QVariant& value, const QString return; } +QString TreeViewModel::toString() +{ + QString model = "\n"; + + foreach(ConfigNode *node, m_model){ + model += node->getPath(); + model += "\n"; + } + + return model; +} + +void TreeViewModel::deletePath(const QString &path) +{ + QStringList splittedPath = path.split("/"); + + QString root = splittedPath.takeFirst(); + + if(root == "system") + { + m_model.at(0)->deletePath(splittedPath); + } + else if(root == "user") + { + m_model.at(1)->deletePath(splittedPath); + } + else + { + qDebug() << "TreeViewModel::deletePath: INVALID_PATH"; + } +} + +int TreeViewModel::getIndexByName(const QString &name) const +{ + for(int i = 0; i < m_model.count(); i++){ + if(m_model.at(i)->getName() == name) + return i; + } + + return -1; +} + Qt::ItemFlags TreeViewModel::flags(const QModelIndex& index) const { if (!index.isValid()) @@ -150,12 +202,13 @@ Qt::ItemFlags TreeViewModel::flags(const QModelIndex& index) const return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; } -// TODO: make recursion more elegant, pass Key void TreeViewModel::sink(ConfigNode* node, QStringList keys, QString path, Key key) { if (keys.length() == 0) return; + bool isLeaf = (keys.length() == 1); + // qDebug() << "in sink: " << keys << " with path: " << path; QString name = keys.takeFirst(); @@ -168,44 +221,49 @@ void TreeViewModel::sink(ConfigNode* node, QStringList keys, QString path, Key k } else { - // qDebug() << "new child: " << name << " with path: " << (path + "/" + name); - ConfigNode* newNode = new ConfigNode(name, (path + "/" + name), key); + ConfigNode* newNode; + + if(isLeaf) + newNode = new ConfigNode(name, (path + "/" + name), key, node->getChildren()); + else + newNode = new ConfigNode(name, (path + "/" + name), NULL, node->getChildren()); + node->appendChild(newNode); + sink(newNode, keys, node->getPath() + "/" + name, key); } } -void TreeViewModel::populateModel(kdb::KeySet const & config) +void TreeViewModel::populateModel() { - ConfigNode* system = new ConfigNode("system", "system", 0); - ConfigNode* user = new ConfigNode("user", "user", 0); + ConfigNode* system = new ConfigNode("system", "system", 0, this); + ConfigNode* user = new ConfigNode("user", "user", 0, this); m_model << system << user; - QStringList configData; + m_keySet.rewind(); - config.rewind(); - - while (config.next()) + while (m_keySet.next()) { - QString currentKey = QString::fromStdString(config.current().getName()); + QString currentKey = QString::fromStdString(m_keySet.current().getName()); + + // qDebug() << "TreeViewModel::populateModel: currentKey: " << currentKey; - QStringList splittedKey = currentKey.split("/"); + QStringList keys = currentKey.split("/"); + QString root = keys.takeFirst(); - if (splittedKey.at(0) == "system") + if (root == "system") { - splittedKey.removeFirst(); - sink(m_model.at(0), splittedKey, "system", config.current()); + sink(m_model.at(0), keys, "system", m_keySet.current()); } - else if (splittedKey.at(0) == "user") + else if (root == "user") { - splittedKey.removeFirst(); - sink(m_model.at(1), splittedKey, "user", config.current()); + sink(m_model.at(1), keys, "user", m_keySet.current()); } else { - qDebug() << "INVALID_KEY"; + qDebug() << "TreeViewModel::populateModel: INVALID_KEY: " << currentKey; } } @@ -240,7 +298,7 @@ QVariant TreeViewModel::find(const QString& term) if (searchResults->model().count() == 0) { - searchResults->model().append(new ConfigNode("NotfoundNode", "There were no results matching your query.", 0)); + searchResults->model().append(new ConfigNode("NotfoundNode", "There were no results matching your query.", 0, this)); } return QVariant::fromValue(searchResults); @@ -270,13 +328,14 @@ bool TreeViewModel::removeRow(int row, const QModelIndex& parent) { Q_UNUSED(parent); - if (row < 0 || row > m_model.size()-1) + if (row < 0 || row > m_model.size() - 1) { qDebug() << "Tried to remove row out of bounds. model.size = " << m_model.size() << ", index = " << row; return false; } beginRemoveRows(QModelIndex(), row, row); + m_model.at(row)->invalidateKey(); delete m_model.takeAt(row); endRemoveRows(); @@ -286,15 +345,27 @@ bool TreeViewModel::removeRow(int row, const QModelIndex& parent) bool TreeViewModel::insertRow(int row, const QModelIndex& parent) { Q_UNUSED(parent); - beginInsertRows(QModelIndex(), row, row); - m_model.insert(row, new ConfigNode("", "", m_metaModelParent)); + ConfigNode *node = new ConfigNode; + node->setName(QString::fromStdString(m_metaModelParent.getName())); + node->setKey(m_metaModelParent); + node->setParentModel(this); + + beginInsertRows(QModelIndex(), row, row); + m_model.insert(row, node); endInsertRows(); return true; } +void TreeViewModel::insertRow(int row, ConfigNode *node) +{ + beginInsertRows(QModelIndex(), row, row); + m_model.insert(row, node); + endInsertRows(); +} + -void TreeViewModel::qmlInsertRow(int row, ConfigNode *node) +void TreeViewModel::insertMetaRow(int row, ConfigNode *node) { m_metaModelParent = node->getKey(); @@ -305,6 +376,51 @@ void TreeViewModel::qmlInsertRow(int row, ConfigNode *node) qDebug() << "Key " << QString::fromStdString(node->getKey().getFullName()) << " not valid!"; } +void TreeViewModel::createNewNode(const QString &path, const QString &value, const QVariantMap metaData) +{ + qDebug() << "TreeViewModel::createNewNode: path = " << path << " value = " << value; + Key key; + key.setName(path.toStdString()); + key.setString(value.toStdString()); + + for(QVariantMap::const_iterator iter = metaData.begin(); iter != metaData.end(); iter++) + { + qDebug() << iter.key() << iter.value(); + key.setMeta(iter.key().toStdString(), iter.value().toString().toStdString()); + } + + QStringList splittedKey = path.split("/"); + + if (splittedKey.at(0) == "system") + { + splittedKey.removeFirst(); + sink(m_model.at(0), splittedKey, "system", key); + } + else if (splittedKey.at(0) == "user") + { + splittedKey.removeFirst(); + sink(m_model.at(1), splittedKey, "user", key); + } + else + { + qDebug() << "TreeViewModel::createNewNode: INVALID_KEY: " << path; + } +} + +void TreeViewModel::append(ConfigNode *node) +{ + insertRow(rowCount(), node); +} + +void TreeViewModel::synchronize() +{ + KeySetVisitor ksVisit(m_keySet); + accept(ksVisit); + m_keySet = ksVisit.getKeySet(); + m_kdb.set(m_keySet, "/"); + populateModel(); +} + void TreeViewModel::clear() { beginResetModel(); @@ -315,6 +431,7 @@ void TreeViewModel::clear() QHash TreeViewModel::roleNames() const { QHash roles; + roles[NameRole] = "name"; roles[PathRole] = "path"; roles[ValueRole] = "value"; @@ -323,5 +440,8 @@ QHash TreeViewModel::roleNames() const roles[ChildrenHaveNoChildrenRole] = "childrenHaveNoChildren"; roles[MetaValueRole] = "metaValue"; roles[NodeRole] = "node"; + roles[ParentModelRole] = "parentModel"; + roles[IndexRole] = "index"; + return roles; } diff --git a/src/treeviewmodel.hpp b/src/treeviewmodel.hpp index cf576b2..63e1f23 100644 --- a/src/treeviewmodel.hpp +++ b/src/treeviewmodel.hpp @@ -35,69 +35,72 @@ class TreeViewModel : public QAbstractListModel ChildrenRole, ///< The role QML can access the children model of a node at a specified index. ChildrenHaveNoChildrenRole, ///< The role QML can access if children of a node at a specified index do have children on their own. MetaValueRole, ///< The role QML can access the meta model of a node at a specified index. - NodeRole ///< The role QML can retrieve the node at a specified index. + NodeRole, ///< The role QML can retrieve the node at a specified index. + ParentModelRole, + IndexRole }; explicit TreeViewModel(QObject* parent = 0); + explicit TreeViewModel(kdb::KeySet &keySet); // Needed for Qt TreeViewModel(TreeViewModel const & other); ~TreeViewModel(); - /// @return the underlying model + // @return the underlying model QList& model() { return m_model; } //mandatory methods inherited from QAbstractItemModel - Q_INVOKABLE int rowCount(const QModelIndex& parent = QModelIndex()) const; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); - Q_INVOKABLE void setDataValue(int index, const QVariant& value, const QString& role); - Q_INVOKABLE bool insertRow(int row, const QModelIndex &parent = QModelIndex()); - Q_INVOKABLE bool removeRow(int row, const QModelIndex& parent = QModelIndex()); - Qt::ItemFlags flags(const QModelIndex& index) const; + Q_INVOKABLE int rowCount(const QModelIndex& parent = QModelIndex()) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole); + Q_INVOKABLE bool insertRow(int row, const QModelIndex &parent = QModelIndex()); + Q_INVOKABLE bool removeRow(int row, const QModelIndex& parent = QModelIndex()); + Qt::ItemFlags flags(const QModelIndex& index) const; - /// recursively populate the model - void populateModel(kdb::KeySet const & config); + // recursively populate the model + Q_INVOKABLE void populateModel(); - // TODO: add visitor in order to: - // print tree for debugging purposes - // get current KeySet (by appending all Keys in the ConfigNodes - // with this KeySet we can implement undo and save to storage - - void accept(Visitor &visitor); + void accept(Visitor &visitor); /** * @brief Get the roles of a ConfigNode at the specifies index. Needed to access roles from outside a delegate in QML. * @param idx The index of the ConfigNode. * @return A map of the roles of the ConfigNode at the specified index. */ - Q_INVOKABLE QVariantMap get(int idx) const; + Q_INVOKABLE QVariantMap get(int idx) const; /** * @brief Find a search term in the model. * @param term The search term of interest. * @return A model which includes all ConfigNodes that have the search term in their name or value. */ - Q_INVOKABLE QVariant find(const QString& term); - Q_INVOKABLE void qmlInsertRow(int row, ConfigNode *node); - Q_INVOKABLE void clear(); + Q_INVOKABLE QVariant find(const QString& term); + void insertMetaRow(int row, ConfigNode *node); + void insertRow(int row, ConfigNode* node); + Q_INVOKABLE void clear(); + Q_INVOKABLE void synchronize(); + Q_INVOKABLE void createNewNode(const QString &path, const QString &value, const QVariantMap metaData); + void append(ConfigNode *node); + Q_INVOKABLE void setData(int index, const QVariant& value, const QString& role); + QString toString(); + void deletePath(const QString &path); + Q_INVOKABLE int getIndexByName(const QString &name) const; private: - void sink(ConfigNode* node, QStringList keys, QString path, kdb::Key key); - void find(ConfigNode *node, TreeViewModel *searchResults, const QString term); + void sink(ConfigNode* node, QStringList keys, QString path, kdb::Key key); + void find(ConfigNode *node, TreeViewModel *searchResults, const QString term); - QList m_model; - kdb::Key m_metaModelParent; + QList m_model; + kdb::Key m_metaModelParent; + kdb::KDB m_kdb; + kdb::KeySet m_keySet; protected: - QHash roleNames() const; - -signals: - void modelChanged(); - + QHash roleNames() const; }; Q_DECLARE_METATYPE(TreeViewModel) diff --git a/src/tst_modeltest.cpp b/src/tst_modeltest.cpp new file mode 100644 index 0000000..131ad05 --- /dev/null +++ b/src/tst_modeltest.cpp @@ -0,0 +1,342 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include +#include +#include + +#include "modeltest.h" +#include "dynamictreemodel.h" + + +class tst_ModelTest : public QObject +{ + Q_OBJECT + +public: + tst_ModelTest() {} + virtual ~tst_ModelTest() {} + +public slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + +private slots: + void stringListModel(); + void treeWidgetModel(); + void standardItemModel(); + void testInsertThroughProxy(); + void moveSourceItems(); + void testResetThroughProxy(); +}; + + + +void tst_ModelTest::initTestCase() +{ +} + +void tst_ModelTest::cleanupTestCase() +{ +} + +void tst_ModelTest::init() +{ + +} + +void tst_ModelTest::cleanup() +{ +} +/* + tests +*/ + +void tst_ModelTest::stringListModel() +{ + QStringListModel model; + QSortFilterProxyModel proxy; + + ModelTest t1(&model); + ModelTest t2(&proxy); + + proxy.setSourceModel(&model); + + model.setStringList(QStringList() << "2" << "3" << "1"); + model.setStringList(QStringList() << "a" << "e" << "plop" << "b" << "c" ); + + proxy.setDynamicSortFilter(true); + proxy.setFilterRegExp(QRegExp("[^b]")); +} + +void tst_ModelTest::treeWidgetModel() +{ + QTreeWidget widget; + + ModelTest t1(widget.model()); + + QTreeWidgetItem *root = new QTreeWidgetItem(&widget, QStringList("root")); + for (int i = 0; i < 20; ++i) { + new QTreeWidgetItem(root, QStringList(QString::number(i))); + } + QTreeWidgetItem *remove = root->child(2); + root->removeChild(remove); + QTreeWidgetItem *parent = new QTreeWidgetItem(&widget, QStringList("parent")); + new QTreeWidgetItem(parent, QStringList("child")); + widget.setItemHidden(parent, true); + + widget.sortByColumn(0); +} + +void tst_ModelTest::standardItemModel() +{ + QStandardItemModel model(10,10); + QSortFilterProxyModel proxy; + + + ModelTest t1(&model); + ModelTest t2(&proxy); + + proxy.setSourceModel(&model); + + model.insertRows(2, 5); + model.removeRows(4, 5); + + model.insertColumns(2, 5); + model.removeColumns(4, 5); + + model.insertRows(0,5, model.index(1,1)); + model.insertColumns(0,5, model.index(1,3)); +} + +void tst_ModelTest::testInsertThroughProxy() +{ + DynamicTreeModel *model = new DynamicTreeModel(this); + + QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this); + proxy->setSourceModel(model); + + new ModelTest(proxy, this); + + ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); + insertCommand->setNumCols(4); + insertCommand->setStartRow(0); + insertCommand->setEndRow(9); + // Parent is QModelIndex() + insertCommand->doCommand(); + + insertCommand = new ModelInsertCommand(model, this); + insertCommand->setNumCols(4); + insertCommand->setAncestorRowNumbers(QList() << 5); + insertCommand->setStartRow(0); + insertCommand->setEndRow(9); + insertCommand->doCommand(); + + ModelMoveCommand *moveCommand = new ModelMoveCommand(model, this); + moveCommand->setNumCols(4); + moveCommand->setStartRow(0); + moveCommand->setEndRow(0); + moveCommand->setDestRow(9); + moveCommand->setDestAncestors(QList() << 5); + moveCommand->doCommand(); +} + +/** + Makes the persistent index list publicly accessible +*/ +class AccessibleProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + AccessibleProxyModel(QObject *parent = 0) : QSortFilterProxyModel(parent) {} + + QModelIndexList persistent() + { + return persistentIndexList(); + } +}; + +class ObservingObject : public QObject +{ + Q_OBJECT +public: + ObservingObject(AccessibleProxyModel *proxy, QObject *parent = 0) + : QObject(parent) + , m_proxy(proxy) + , storePersistentFailureCount(0) + , checkPersistentFailureCount(0) + { + connect(m_proxy, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(storePersistent())); + connect(m_proxy, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), SLOT(checkPersistent())); + } + +public slots: + + void storePersistent(const QModelIndex &parent) + { + for (int row = 0; row < m_proxy->rowCount(parent); ++row) { + QModelIndex proxyIndex = m_proxy->index(row, 0, parent); + QModelIndex sourceIndex = m_proxy->mapToSource(proxyIndex); + if (!proxyIndex.isValid()) { + qWarning("%s: Invalid proxy index", Q_FUNC_INFO); + ++storePersistentFailureCount; + } + if (!sourceIndex.isValid()) { + qWarning("%s: invalid source index", Q_FUNC_INFO); + ++storePersistentFailureCount; + } + m_persistentSourceIndexes.append(sourceIndex); + m_persistentProxyIndexes.append(proxyIndex); + if (m_proxy->hasChildren(proxyIndex)) + storePersistent(proxyIndex); + } + } + + void storePersistent() + { + // This method is called from rowsAboutToBeMoved. Persistent indexes should be valid + foreach(const QModelIndex &idx, m_persistentProxyIndexes) + if (!idx.isValid()) { + qWarning("%s: persistentProxyIndexes contains invalid index", Q_FUNC_INFO); + ++storePersistentFailureCount; + } + + if (!m_proxy->persistent().isEmpty()) { + qWarning("%s: proxy should have no persistent indexes when storePersistent called", + Q_FUNC_INFO); + ++storePersistentFailureCount; + } + storePersistent(QModelIndex()); + if (m_proxy->persistent().isEmpty()) { + qWarning("%s: proxy should have persistent index after storePersistent called", + Q_FUNC_INFO); + ++storePersistentFailureCount; + } + } + + void checkPersistent() + { + for (int row = 0; row < m_persistentProxyIndexes.size(); ++row) { + m_persistentProxyIndexes.at(row); + m_persistentSourceIndexes.at(row); + } + for (int row = 0; row < m_persistentProxyIndexes.size(); ++row) { + QModelIndex updatedProxy = m_persistentProxyIndexes.at(row); + QModelIndex updatedSource = m_persistentSourceIndexes.at(row); + if (m_proxy->mapToSource(updatedProxy) != updatedSource) { + qWarning("%s: check failed at row %d", Q_FUNC_INFO, row); + ++checkPersistentFailureCount; + } + } + m_persistentSourceIndexes.clear(); + m_persistentProxyIndexes.clear(); + } + +private: + AccessibleProxyModel *m_proxy; + QList m_persistentSourceIndexes; + QList m_persistentProxyIndexes; +public: + int storePersistentFailureCount; + int checkPersistentFailureCount; +}; + +void tst_ModelTest::moveSourceItems() +{ + DynamicTreeModel *model = new DynamicTreeModel(this); + AccessibleProxyModel *proxy = new AccessibleProxyModel(this); + proxy->setSourceModel(model); + + ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); + insertCommand->setStartRow(0); + insertCommand->setEndRow(2); + insertCommand->doCommand(); + + insertCommand = new ModelInsertCommand(model, this); + insertCommand->setAncestorRowNumbers(QList() << 1); + insertCommand->setStartRow(0); + insertCommand->setEndRow(2); + insertCommand->doCommand(); + + ObservingObject observer(proxy); + + ModelMoveCommand *moveCommand = new ModelMoveCommand(model, this); + moveCommand->setStartRow(0); + moveCommand->setEndRow(0); + moveCommand->setDestAncestors(QList() << 1); + moveCommand->setDestRow(0); + moveCommand->doCommand(); + + QCOMPARE(observer.storePersistentFailureCount, 0); + QCOMPARE(observer.checkPersistentFailureCount, 0); +} + +void tst_ModelTest::testResetThroughProxy() +{ + DynamicTreeModel *model = new DynamicTreeModel(this); + + ModelInsertCommand *insertCommand = new ModelInsertCommand(model, this); + insertCommand->setStartRow(0); + insertCommand->setEndRow(2); + insertCommand->doCommand(); + + QPersistentModelIndex persistent = model->index(0, 0); + + AccessibleProxyModel *proxy = new AccessibleProxyModel(this); + proxy->setSourceModel(model); + + ObservingObject observer(proxy); + observer.storePersistent(); + + ModelResetCommand *resetCommand = new ModelResetCommand(model, this); + resetCommand->setNumCols(0); + resetCommand->doCommand(); + + QCOMPARE(observer.storePersistentFailureCount, 0); + QCOMPARE(observer.checkPersistentFailureCount, 0); +} + + +QTEST_MAIN(tst_ModelTest) +#include "tst_modeltest.moc" diff --git a/src/undomanager.cpp b/src/undomanager.cpp new file mode 100644 index 0000000..e8db69d --- /dev/null +++ b/src/undomanager.cpp @@ -0,0 +1,121 @@ +#include +#include "undomanager.hpp" +#include "editkeycommand.hpp" +#include "deletekeycommand.hpp" +#include "newkeycommand.hpp" +#include "copykeycommand.hpp" +#include "cutkeycommand.hpp" + +UndoManager::UndoManager(QObject *parent) : + QObject(parent) + , m_undoStack(new QUndoStack(this)) +{ + connect(m_undoStack, SIGNAL(canRedoChanged(bool)), this, SIGNAL(canRedoChanged())); + connect(m_undoStack, SIGNAL(canUndoChanged(bool)), this, SIGNAL(canUndoChanged())); + connect(m_undoStack, SIGNAL(redoTextChanged(QString)), this, SIGNAL(redoTextChanged())); + connect(m_undoStack, SIGNAL(undoTextChanged(QString)), this, SIGNAL(undoTextChanged())); + + m_clipboard = QApplication::clipboard(); +} + +UndoManager::UndoManager(const UndoManager &other) +{ + +} + +UndoManager::~UndoManager() +{ + +} + +bool UndoManager::canUndo() const +{ + return m_undoStack->canUndo(); +} + +bool UndoManager::canRedo() const +{ + return m_undoStack->canRedo(); +} + +void UndoManager::createEditKeyCommand(TreeViewModel *model, int index, const QString &oldName, const QVariant &oldValue, const QVariant &oldMetaData, + const QString &newName, const QVariant &newValue, const QVariant &newMetaData) +{ + //convert TreeViewModel to QVariantMap + TreeViewModel *tmpModel = qvariant_cast(oldMetaData); + QVariantMap oldMDMap; + + foreach(ConfigNode *node, tmpModel->model()){ + oldMDMap.insert(node->getName(), node->getValue()); + } + + m_undoStack->push(new EditKeyCommand(model, index, oldName, oldValue, oldMDMap, newName, newValue, newMetaData.toMap())); +} + +void UndoManager::createDeleteKeyCommand(const QString &type, TreeViewModel *model, ConfigNode *node, int index) +{ + m_undoStack->push(new DeleteKeyCommand(type, model, node, index)); +} + +void UndoManager::createNewKeyCommand(TreeViewModel *model, const QString &name, const QString &value, const QVariantMap &metaData) +{ + m_undoStack->push(new NewKeyCommand(model, name, value, metaData)); +} + +void UndoManager::createCopyKeyCommand(ConfigNode *target) +{ + m_undoStack->push(new CopyKeyCommand(qvariant_cast(m_clipboard->property("node")), target)); +} + +void UndoManager::createCutKeyCommand(ConfigNode *target) +{ + m_undoStack->push(new CutKeyCommand(qvariant_cast(m_clipboard->property("model")), qvariant_cast(m_clipboard->property("node")), target, m_clipboard->property("index").toInt())); +} + +void UndoManager::setClean() +{ + m_undoStack->setClean(); +} + +bool UndoManager::isClean() +{ + return m_undoStack->isClean(); +} + +void UndoManager::undo() +{ + m_undoStack->undo(); +} + +void UndoManager::redo() +{ + m_undoStack->redo(); +} + +QString UndoManager::undoText() const +{ + return m_undoStack->undoText(); +} + +QString UndoManager::clipboardType() const +{ + return m_clipboardType; +} + +void UndoManager::putToClipboard(const QString &type, TreeViewModel *model, ConfigNode *node, int index) +{ + m_clipboardType = type; + + m_clipboard->clear(); + + m_clipboard->setProperty("model", QVariant::fromValue(model)); + m_clipboard->setProperty("node", QVariant::fromValue(new ConfigNode(*node))); + m_clipboard->setProperty("index", QVariant::fromValue(index)); + + emit clipboardTypeChanged(); +} + +QString UndoManager::redoText() const +{ + return m_undoStack->redoText(); +} diff --git a/src/undomanager.hpp b/src/undomanager.hpp new file mode 100644 index 0000000..6c6dc7d --- /dev/null +++ b/src/undomanager.hpp @@ -0,0 +1,69 @@ +#ifndef UNDOMANAGER_HPP +#define UNDOMANAGER_HPP + +#include +#include +#include +#include "treeviewmodel.hpp" + +class QUndoStack; + +class UndoManager : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool canUndo READ canUndo() NOTIFY canUndoChanged()) + Q_PROPERTY(bool canRedo READ canRedo() NOTIFY canRedoChanged()) + + Q_PROPERTY(QString redoText READ redoText() NOTIFY redoTextChanged()) + Q_PROPERTY(QString undoText READ undoText() NOTIFY undoTextChanged()) + Q_PROPERTY(QString clipboardType READ clipboardType() NOTIFY clipboardTypeChanged()) + +public: + + explicit UndoManager(QObject *parent = 0); + UndoManager(UndoManager const & other); + ~UndoManager(); + + bool canUndo() const; + bool canRedo() const; + + QString redoText() const; + QString undoText() const; + QString clipboardType() const; + + Q_INVOKABLE void putToClipboard(const QString &type, TreeViewModel *model, ConfigNode *node, int index); + + Q_INVOKABLE void createEditKeyCommand(TreeViewModel *model, int index, const QString &oldName, const QVariant &oldValue, const QVariant &oldMetaData, + const QString &newName, const QVariant &newValue, const QVariant &newMetaData); + + Q_INVOKABLE void createDeleteKeyCommand(const QString &type, TreeViewModel *model, ConfigNode *node, int index); + Q_INVOKABLE void createNewKeyCommand(TreeViewModel *model, const QString &name, const QString &value, const QVariantMap &metaData); + Q_INVOKABLE void createCopyKeyCommand(ConfigNode *target); + Q_INVOKABLE void createCutKeyCommand(ConfigNode *target); + Q_INVOKABLE void setClean(); + Q_INVOKABLE bool isClean(); + +Q_SIGNALS: + + void canUndoChanged(); + void canRedoChanged(); + void redoTextChanged(); + void undoTextChanged(); + void clipboardTypeChanged(); + +public Q_SLOTS: + + void undo(); + void redo(); + +private: + + QUndoStack *m_undoStack; + QClipboard *m_clipboard; + QString m_clipboardType; +}; + +Q_DECLARE_METATYPE(UndoManager) + +#endif // UNDOMANAGER_HPP diff --git a/unittest/confignodetest.cpp b/unittest/confignodetest.cpp new file mode 100644 index 0000000..44bbe02 --- /dev/null +++ b/unittest/confignodetest.cpp @@ -0,0 +1,6 @@ +#include "confignodetest.hpp" + +ConfigNodeTest::ConfigNodeTest(QObject *parent) : + QObject(parent) +{ +} diff --git a/unittest/confignodetest.hpp b/unittest/confignodetest.hpp new file mode 100644 index 0000000..fc6033d --- /dev/null +++ b/unittest/confignodetest.hpp @@ -0,0 +1,18 @@ +#ifndef CONFIGNODETEST_HPP +#define CONFIGNODETEST_HPP + +#include + +class ConfigNodeTest : public QObject +{ + Q_OBJECT +public: + explicit ConfigNodeTest(QObject *parent = 0); + +signals: + +public slots: + +}; + +#endif // CONFIGNODETEST_HPP diff --git a/unittest/treeviewtest.cpp b/unittest/treeviewtest.cpp new file mode 100644 index 0000000..0f72cbf --- /dev/null +++ b/unittest/treeviewtest.cpp @@ -0,0 +1,6 @@ +#include "treeviewtest.hpp" + +TreeViewTest::TreeViewTest(QObject *parent) : + QObject(parent) +{ +} diff --git a/unittest/treeviewtest.hpp b/unittest/treeviewtest.hpp new file mode 100644 index 0000000..e2decf7 --- /dev/null +++ b/unittest/treeviewtest.hpp @@ -0,0 +1,18 @@ +#ifndef TREEVIEWTEST_HPP +#define TREEVIEWTEST_HPP + +#include + +class TreeViewTest : public QObject +{ + Q_OBJECT +public: + explicit TreeViewTest(QObject *parent = 0); + +signals: + +public slots: + +}; + +#endif // TREEVIEWTEST_HPP