Add a MergedProxyModel that lets us merge two models into one...

This commit is contained in:
David Sansome 2010-05-09 15:51:04 +00:00
parent 1b00aaa8b3
commit 6b8d6c93f9
20 changed files with 467 additions and 18 deletions

View File

@ -84,6 +84,7 @@ set(CLEMENTINE-SOURCES
database.cpp
librarymodel.cpp
playlistbackend.cpp
mergedproxymodel.cpp
)
# Header files that have Q_OBJECT in
@ -153,6 +154,7 @@ set(CLEMENTINE-MOC-HEADERS
librarymodel.h
playlistbackend.h
database.h
mergedproxymodel.h
)
# lists of engine source files

View File

@ -47,7 +47,7 @@ const char* LastFMService::kAudioscrobblerClientId = "tng";
const char* LastFMService::kApiKey = "75d20fb472be99275392aefa2760ea09";
const char* LastFMService::kSecret = "d3072b60ae626be12be69448f5c46e70";
LastFMService::LastFMService(QObject* parent)
LastFMService::LastFMService(RadioModel* parent)
: RadioService(kServiceName, parent),
scrobbler_(NULL),
station_dialog_(new LastFMStationDialog),

View File

@ -47,7 +47,7 @@ class LastFMService : public RadioService {
Q_OBJECT
public:
LastFMService(QObject* parent = 0);
LastFMService(RadioModel* parent);
~LastFMService();
static const char* kServiceName;

View File

@ -16,6 +16,10 @@
#include "magnatuneservice.h"
#include "song.h"
#include "radiomodel.h"
#include "mergedproxymodel.h"
#include "librarymodel.h"
#include "librarybackend.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
@ -29,16 +33,24 @@ const char* MagnatuneService::kServiceName = "Magnatune";
const char* MagnatuneService::kDatabaseUrl =
"http://magnatune.com/info/song_info2_xml.gz";
MagnatuneService::MagnatuneService(QObject* parent)
MagnatuneService::MagnatuneService(RadioModel* parent)
: RadioService(kServiceName, parent),
root_(NULL),
library_backend_(new LibraryBackend(parent->db(), "songs", "", "", this)),
library_model_(new LibraryModel(library_backend_, this)),
network_(new QNetworkAccessManager(this))
{
library_model_->Init();
}
RadioItem* MagnatuneService::CreateRootItem(RadioItem *parent) {
root_ = new RadioItem(this, RadioItem::Type_Service, kServiceName, parent);
root_->icon = QIcon(":magnatune.png");
model()->merged_model()->AddSubModel(
model()->index(root_->row, 0, model()->ItemToIndex(parent)),
library_model_);
return root_;
}

View File

@ -23,11 +23,14 @@
class QNetworkAccessManager;
class LibraryBackend;
class LibraryModel;
class MagnatuneService : public RadioService {
Q_OBJECT
public:
MagnatuneService(QObject* parent = 0);
MagnatuneService(RadioModel* parent);
static const char* kServiceName;
static const char* kDatabaseUrl;
@ -46,6 +49,8 @@ class MagnatuneService : public RadioService {
private:
RadioItem* root_;
LibraryBackend* library_backend_;
LibraryModel* library_model_;
QNetworkAccessManager* network_;
};

View File

@ -47,6 +47,7 @@
#include "transcodedialog.h"
#include "playlistbackend.h"
#include "database.h"
#include "mergedproxymodel.h"
#include "globalshortcuts/globalshortcuts.h"
@ -93,7 +94,7 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
library_config_dialog_(new LibraryConfigDialog),
about_dialog_(new About),
database_(new Database(this)),
radio_model_(new RadioModel(this)),
radio_model_(new RadioModel(database_, this)),
playlist_backend_(new PlaylistBackend(database_, this)),
playlist_(new Playlist(playlist_backend_, this)),
player_(new Player(playlist_, radio_model_->GetLastFMService(), engine, this)),
@ -143,7 +144,7 @@ MainWindow::MainWindow(QNetworkAccessManager* network, Engine::Type engine, QWid
library_config_dialog_->SetModel(library_->model()->directory_model());
settings_dialog_->SetLibraryDirectoryModel(library_->model()->directory_model());
ui_.radio_view->setModel(radio_model_);
ui_.radio_view->setModel(radio_model_->merged_model());
cover_manager_->Init();

227
src/mergedproxymodel.cpp Normal file
View File

@ -0,0 +1,227 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "mergedproxymodel.h"
std::size_t hash_value(const QModelIndex& index) {
return qHash(index);
}
MergedProxyModel::MergedProxyModel(QObject* parent)
: QAbstractProxyModel(parent)
{
}
void MergedProxyModel::AddSubModel(const QModelIndex& source_parent,
const QAbstractItemModel* submodel) {
merge_points_.insert(submodel, source_parent);
connect(submodel, SIGNAL(modelReset()), this, SLOT(SubModelReset()));
connect(submodel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
this, SLOT(RowsAboutToBeInserted(QModelIndex,int,int)));
connect(submodel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
this, SLOT(RowsAboutToBeRemoved(QModelIndex,int,int)));
connect(submodel, SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(RowsInserted(QModelIndex,int,int)));
connect(submodel, SIGNAL(rowsRemoved(QModelIndex,int,int)),
this, SLOT(RowsRemoved(QModelIndex,int,int)));
}
void MergedProxyModel::setSourceModel(QAbstractItemModel* source_model) {
if (sourceModel()) {
disconnect(sourceModel(), SIGNAL(modelReset()), this, SLOT(SourceModelReset()));
disconnect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
this, SLOT(RowsAboutToBeInserted(QModelIndex,int,int)));
disconnect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
this, SLOT(RowsAboutToBeRemoved(QModelIndex,int,int)));
disconnect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(RowsInserted(QModelIndex,int,int)));
disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
this, SLOT(RowsRemoved(QModelIndex,int,int)));
}
QAbstractProxyModel::setSourceModel(source_model);
connect(sourceModel(), SIGNAL(modelReset()), this, SLOT(SourceModelReset()));
connect(sourceModel(), SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
this, SLOT(RowsAboutToBeInserted(QModelIndex,int,int)));
connect(sourceModel(), SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
this, SLOT(RowsAboutToBeRemoved(QModelIndex,int,int)));
connect(sourceModel(), SIGNAL(rowsInserted(QModelIndex,int,int)),
this, SLOT(RowsInserted(QModelIndex,int,int)));
connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)),
this, SLOT(RowsRemoved(QModelIndex,int,int)));
}
void MergedProxyModel::SourceModelReset() {
// Delete all mappings
MappingContainer::index<tag_by_pointer>::type::iterator begin =
mappings_.get<tag_by_pointer>().begin();
MappingContainer::index<tag_by_pointer>::type::iterator end =
mappings_.get<tag_by_pointer>().end();
qDeleteAll(begin, end);
// Clear the containers
mappings_.clear();
merge_points_.clear();
// Reset the proxy
reset();
}
void MergedProxyModel::SubModelReset() {
const QAbstractItemModel* submodel = static_cast<const QAbstractItemModel*>(sender());
// Delete all the mappings that reference the submodel
MappingContainer::index<tag_by_pointer>::type::iterator it =
mappings_.get<tag_by_pointer>().begin();
MappingContainer::index<tag_by_pointer>::type::iterator end =
mappings_.get<tag_by_pointer>().end();
while (it != end) {
if ((*it)->source_index.model() == submodel) {
delete *it;
it = mappings_.get<tag_by_pointer>().erase(it);
} else {
++it;
}
}
// Reset the proxy
reset();
}
QModelIndex MergedProxyModel::GetActualSourceParent(const QModelIndex& source_parent,
const QAbstractItemModel* model) const {
if (!source_parent.isValid() && model != sourceModel())
return merge_points_.value(model);
return source_parent;
}
void MergedProxyModel::RowsAboutToBeInserted(const QModelIndex& source_parent,
int start, int end) {
beginInsertRows(GetActualSourceParent(
source_parent, static_cast<const QAbstractItemModel*>(sender())),
start, end);
}
void MergedProxyModel::RowsInserted(const QModelIndex&, int, int) {
endInsertRows();
}
void MergedProxyModel::RowsAboutToBeRemoved(const QModelIndex& source_parent,
int start, int end) {
beginRemoveRows(GetActualSourceParent(
source_parent, static_cast<const QAbstractItemModel*>(sender())),
start, end);
}
void MergedProxyModel::RowsRemoved(const QModelIndex&, int, int) {
endRemoveRows();
}
QModelIndex MergedProxyModel::mapToSource(const QModelIndex& proxy_index) const {
if (!proxy_index.isValid())
return QModelIndex();
Mapping* mapping = static_cast<Mapping*>(proxy_index.internalPointer());
return mapping->source_index;
}
QModelIndex MergedProxyModel::mapFromSource(const QModelIndex& source_index) const {
if (!source_index.isValid())
return QModelIndex();
// Add a mapping if we don't have one already
MappingContainer::index<tag_by_source>::type::iterator it =
mappings_.get<tag_by_source>().find(source_index);
Mapping* mapping;
if (it != mappings_.get<tag_by_source>().end()) {
mapping = *it;
} else {
mapping = new Mapping(source_index);
const_cast<MergedProxyModel*>(this)->mappings_.insert(mapping);
}
return createIndex(source_index.row(), source_index.column(), mapping);
}
QModelIndex MergedProxyModel::index(int row, int column, const QModelIndex &parent) const {
QModelIndex source_index;
if (!parent.isValid()) {
source_index = sourceModel()->index(row, column, QModelIndex());
} else {
QModelIndex source_parent = mapToSource(parent);
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
if (child_model)
source_index = child_model->index(row, column, QModelIndex());
else
source_index = source_parent.model()->index(row, column, source_parent);
}
return mapFromSource(source_index);
}
QModelIndex MergedProxyModel::parent(const QModelIndex &child) const {
QModelIndex source_child = mapToSource(child);
if (source_child.model() == sourceModel())
return mapFromSource(source_child.parent());
if (!source_child.parent().isValid())
return mapFromSource(merge_points_.value(source_child.model()));
return mapFromSource(source_child.parent());
}
int MergedProxyModel::rowCount(const QModelIndex &parent) const {
if (!parent.isValid())
return sourceModel()->rowCount(QModelIndex());
QModelIndex source_parent = mapToSource(parent);
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
if (child_model)
return child_model->rowCount(QModelIndex());
return source_parent.model()->rowCount(source_parent);
}
int MergedProxyModel::columnCount(const QModelIndex &parent) const {
if (!parent.isValid())
return sourceModel()->columnCount(QModelIndex());
QModelIndex source_parent = mapToSource(parent);
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
if (child_model)
return child_model->columnCount(QModelIndex());
return source_parent.model()->columnCount(source_parent);
}
bool MergedProxyModel::hasChildren(const QModelIndex &parent) const {
if (!parent.isValid())
return sourceModel()->hasChildren(QModelIndex());
QModelIndex source_parent = mapToSource(parent);
const QAbstractItemModel* child_model = merge_points_.key(source_parent);
if (child_model)
return child_model->hasChildren(QModelIndex());
return source_parent.model()->hasChildren(source_parent);
}
QVariant MergedProxyModel::data(const QModelIndex &proxyIndex, int role) const {
QModelIndex source_index = mapToSource(proxyIndex);
return source_index.model()->data(source_index, role);
}

92
src/mergedproxymodel.h Normal file
View File

@ -0,0 +1,92 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef MERGEDPROXYMODEL_H
#define MERGEDPROXYMODEL_H
#include <QAbstractProxyModel>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/hashed_index.hpp>
using boost::multi_index::multi_index_container;
using boost::multi_index::indexed_by;
using boost::multi_index::hashed_unique;
using boost::multi_index::ordered_unique;
using boost::multi_index::tag;
using boost::multi_index::member;
using boost::multi_index::identity;
std::size_t hash_value(const QModelIndex& index);
class MergedProxyModel : public QAbstractProxyModel {
Q_OBJECT
public:
MergedProxyModel(QObject* parent = 0);
void AddSubModel(const QModelIndex& source_parent, const QAbstractItemModel* submodel);
// QAbstractItemModel
QModelIndex index(int row, int column, const QModelIndex &parent) const;
QModelIndex parent(const QModelIndex &child) const;
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const;
bool hasChildren(const QModelIndex &parent) const;
// QAbstractProxyModel
QModelIndex mapFromSource(const QModelIndex &sourceIndex) const;
QModelIndex mapToSource(const QModelIndex &proxyIndex) const;
void setSourceModel(QAbstractItemModel *sourceModel);
private slots:
void SourceModelReset();
void SubModelReset();
void RowsAboutToBeInserted(const QModelIndex& source_parent, int start, int end);
void RowsInserted(const QModelIndex& source_parent, int start, int end);
void RowsAboutToBeRemoved(const QModelIndex& source_parent, int start, int end);
void RowsRemoved(const QModelIndex& source_parent, int start, int end);
private:
QModelIndex GetActualSourceParent(const QModelIndex& source_parent,
const QAbstractItemModel* model) const;
struct Mapping {
Mapping(const QModelIndex& _source_index) : source_index(_source_index) {}
QModelIndex source_index;
};
struct tag_by_source {};
struct tag_by_pointer {};
typedef multi_index_container<
Mapping*,
indexed_by<
hashed_unique<tag<tag_by_source>,
member<Mapping, QModelIndex, &Mapping::source_index> >,
ordered_unique<tag<tag_by_pointer>,
identity<Mapping*> >
>
> MappingContainer;
MappingContainer mappings_;
QMap<const QAbstractItemModel*, QModelIndex> merge_points_;
};
#endif // MERGEDPROXYMODEL_H

View File

@ -21,18 +21,22 @@
#include "radiomimedata.h"
#include "savedradio.h"
#include "magnatuneservice.h"
#include "mergedproxymodel.h"
#include <QMimeData>
#include <QtDebug>
QMap<QString, RadioService*> RadioModel::sServices;
RadioModel::RadioModel(QObject* parent)
: SimpleTreeModel<RadioItem>(new RadioItem(this), parent)
RadioModel::RadioModel(Database* db, QObject* parent)
: SimpleTreeModel<RadioItem>(new RadioItem(this), parent),
db_(db),
merged_model_(new MergedProxyModel(this))
{
Q_ASSERT(sServices.isEmpty());
root_->lazy_loaded = true;
merged_model_->setSourceModel(this);
AddService(new LastFMService(this));
AddService(new SomaFMService(this));

View File

@ -24,12 +24,14 @@
class RadioService;
class LastFMService;
class Song;
class MergedProxyModel;
class Database;
class RadioModel : public SimpleTreeModel<RadioItem> {
Q_OBJECT
public:
RadioModel(QObject* parent = 0);
RadioModel(Database* db, QObject* parent = 0);
enum {
Role_Type = Qt::UserRole + 1,
@ -52,6 +54,9 @@ class RadioModel : public SimpleTreeModel<RadioItem> {
void ShowContextMenu(RadioItem* item, const QPoint& global_pos);
void ReloadSettings();
Database* db() const { return db_; }
MergedProxyModel* merged_model() const { return merged_model_; }
signals:
void TaskStarted(MultiLoadingIndicator::TaskType);
void TaskFinished(MultiLoadingIndicator::TaskType);
@ -71,6 +76,8 @@ class RadioModel : public SimpleTreeModel<RadioItem> {
private:
static QMap<QString, RadioService*> sServices;
Database* db_;
MergedProxyModel* merged_model_;
};
#endif // RADIOMODEL_H

View File

@ -15,9 +15,11 @@
*/
#include "radioservice.h"
#include "radiomodel.h"
RadioService::RadioService(const QString& name, QObject *parent)
: QObject(parent),
RadioService::RadioService(const QString& name, RadioModel* model)
: QObject(model),
model_(model),
name_(name)
{
}

View File

@ -25,15 +25,17 @@
#include "multiloadingindicator.h"
class Song;
class RadioModel;
class RadioService : public QObject {
Q_OBJECT
public:
RadioService(const QString& name, QObject* parent = 0);
RadioService(const QString& name, RadioModel* model);
virtual ~RadioService() {}
QString name() const { return name_; }
RadioModel* model() const { return model_; }
virtual RadioItem* CreateRootItem(RadioItem* parent) = 0;
virtual void LazyPopulate(RadioItem* item) = 0;
@ -67,6 +69,7 @@ class RadioService : public QObject {
void AddItemToPlaylist(RadioItem* item);
private:
RadioModel* model_;
QString name_;
};

View File

@ -16,6 +16,7 @@
#include "radioview.h"
#include "radiomodel.h"
#include "mergedproxymodel.h"
#include <QContextMenuEvent>
@ -29,6 +30,10 @@ void RadioView::contextMenuEvent(QContextMenuEvent* e) {
if (!index.isValid())
return;
RadioModel* radio_model = static_cast<RadioModel*>(model());
radio_model->ShowContextMenu(radio_model->IndexToItem(index), e->globalPos());
MergedProxyModel* merged_model = static_cast<MergedProxyModel*>(model());
RadioModel* radio_model = static_cast<RadioModel*>(merged_model->sourceModel());
radio_model->ShowContextMenu(
radio_model->IndexToItem(merged_model->mapToSource(index)),
e->globalPos());
}

View File

@ -22,7 +22,7 @@
const char* SavedRadio::kServiceName = "SavedRadio";
const char* SavedRadio::kSettingsGroup = "SavedRadio";
SavedRadio::SavedRadio(QObject* parent)
SavedRadio::SavedRadio(RadioModel* parent)
: RadioService(kServiceName, parent),
root_(NULL),
context_menu_(new QMenu)

View File

@ -25,7 +25,7 @@ class SavedRadio : public RadioService {
Q_OBJECT
public:
SavedRadio(QObject* parent = 0);
SavedRadio(RadioModel* parent);
~SavedRadio();
enum ItemType {

View File

@ -50,6 +50,7 @@ class SimpleTreeItem {
T* parent;
QList<T*> children;
QAbstractItemModel* child_model;
SimpleTreeModel<T>* model;
};
@ -60,6 +61,7 @@ SimpleTreeItem<T>::SimpleTreeItem(int _type, SimpleTreeModel<T>* _model)
row(0),
lazy_loaded(true),
parent(NULL),
child_model(NULL),
model(_model)
{
}
@ -70,6 +72,7 @@ SimpleTreeItem<T>::SimpleTreeItem(int _type, const QString& _key, T* _parent)
key(_key),
lazy_loaded(false),
parent(_parent),
child_model(NULL),
model(_parent ? _parent->model : NULL)
{
if (parent) {
@ -83,6 +86,7 @@ SimpleTreeItem<T>::SimpleTreeItem(int _type, T* _parent)
: type(_type),
lazy_loaded(false),
parent(_parent),
child_model(NULL),
model(_parent ? _parent->model : NULL)
{
if (parent) {

View File

@ -31,7 +31,7 @@ const char* SomaFMService::kServiceName = "SomaFM";
const char* SomaFMService::kChannelListUrl = "http://somafm.com/channels.xml";
const char* SomaFMService::kHomepage = "http://somafm.com";
SomaFMService::SomaFMService(QObject* parent)
SomaFMService::SomaFMService(RadioModel* parent)
: RadioService(kServiceName, parent),
root_(NULL),
context_menu_(new QMenu),

View File

@ -28,7 +28,7 @@ class SomaFMService : public RadioService {
Q_OBJECT
public:
SomaFMService(QObject* parent = 0);
SomaFMService(RadioModel* parent);
~SomaFMService();
enum ItemType {

View File

@ -102,3 +102,4 @@ add_test_file(translations_test.cpp false)
add_test_file(playlist_test.cpp true)
add_test_file(scopedtransaction_test.cpp false)
add_test_file(fileformats_test.cpp false)
add_test_file(mergedproxymodel_test.cpp false)

View File

@ -0,0 +1,84 @@
/* This file is part of Clementine.
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "gtest/gtest.h"
#include "test_utils.h"
#include "mergedproxymodel.h"
#include <QStandardItemModel>
class MergedProxyModelTest : public ::testing::Test {
protected:
void SetUp() {
merged_.setSourceModel(&source_);
}
QStandardItemModel source_;
MergedProxyModel merged_;
};
TEST_F(MergedProxyModelTest, Flat) {
source_.appendRow(new QStandardItem("one"));
source_.appendRow(new QStandardItem("two"));
ASSERT_EQ(2, merged_.rowCount(QModelIndex()));
QModelIndex one_i = merged_.index(0, 0, QModelIndex());
QModelIndex two_i = merged_.index(1, 0, QModelIndex());
EXPECT_EQ("one", one_i.data().toString());
EXPECT_EQ("two", two_i.data().toString());
EXPECT_FALSE(merged_.parent(one_i).isValid());
EXPECT_FALSE(merged_.hasChildren(one_i));
}
TEST_F(MergedProxyModelTest, Tree) {
QStandardItem* one = new QStandardItem("one");
QStandardItem* two = new QStandardItem("two");
source_.appendRow(one);
one->appendRow(two);
ASSERT_EQ(1, merged_.rowCount(QModelIndex()));
QModelIndex one_i = merged_.index(0, 0, QModelIndex());
ASSERT_EQ(1, merged_.rowCount(one_i));
QModelIndex two_i = merged_.index(0, 0, one_i);
EXPECT_EQ("one", one_i.data().toString());
EXPECT_EQ("two", two_i.data().toString());
EXPECT_EQ("one", two_i.parent().data().toString());
}
TEST_F(MergedProxyModelTest, Merged) {
source_.appendRow(new QStandardItem("one"));
QStandardItemModel submodel;
submodel.appendRow(new QStandardItem("two"));
merged_.AddModel(source_.index(0, 0, QModelIndex()), &submodel);
ASSERT_EQ(1, merged_.rowCount(QModelIndex()));
QModelIndex one_i = merged_.index(0, 0, QModelIndex());
EXPECT_EQ("one", merged_.data(one_i).toString());
EXPECT_TRUE(merged_.hasChildren(one_i));
ASSERT_EQ(1, merged_.rowCount(one_i));
QModelIndex two_i = merged_.index(0, 0, one_i);
EXPECT_EQ("two", merged_.data(two_i).toString());
EXPECT_EQ(0, merged_.rowCount(two_i));
EXPECT_FALSE(merged_.hasChildren(two_i));
}