Branch 'kolab/integration/4.13.0' - akonadiconsole/browserwidget.cpp korganizer/actionmanager.cpp korganizer/akonadicollectionview.cpp korganizer/akonadicollectionview.h korganizer/CMakeLists.txt korganizer/korganizerui.rc korganizer/views

Christian Mollekopf mollekopf at kolabsys.com
Mon Aug 11 18:19:01 CEST 2014


 akonadiconsole/browserwidget.cpp                               |    1 
 korganizer/CMakeLists.txt                                      |    6 
 korganizer/actionmanager.cpp                                   |    9 
 korganizer/akonadicollectionview.cpp                           |  376 ++++
 korganizer/akonadicollectionview.h                             |   16 
 korganizer/korganizerui.rc                                     |    2 
 korganizer/views/collectionview/CMakeLists.txt                 |    1 
 korganizer/views/collectionview/calendardelegate.cpp           |  154 ++
 korganizer/views/collectionview/calendardelegate.h             |   49 
 korganizer/views/collectionview/controller.cpp                 |  528 ++++++
 korganizer/views/collectionview/controller.h                   |  198 ++
 korganizer/views/collectionview/reparentingmodel.cpp           |  757 ++++++++++
 korganizer/views/collectionview/reparentingmodel.h             |  143 +
 korganizer/views/collectionview/tests/CMakeLists.txt           |   16 
 korganizer/views/collectionview/tests/reparentingmodeltest.cpp |  567 +++++++
 15 files changed, 2765 insertions(+), 58 deletions(-)

New commits:
commit 18e9c21653e6b16ba21c159bae2e92d9f4877411
Author: Christian Mollekopf <chrigi_1 at fastmail.fm>
Date:   Mon Aug 11 18:18:24 2014 +0200

    New calendar selection, still work in progress.

diff --git a/akonadiconsole/browserwidget.cpp b/akonadiconsole/browserwidget.cpp
index b415c7c..8ed02a0 100644
--- a/akonadiconsole/browserwidget.cpp
+++ b/akonadiconsole/browserwidget.cpp
@@ -127,6 +127,7 @@ BrowserWidget::BrowserWidget(KXmlGuiWindow *xmlGuiWindow, QWidget * parent) :
   mBrowserModel = new AkonadiBrowserModel( mBrowserMonitor, this );
   mBrowserModel->setItemPopulationStrategy( EntityTreeModel::LazyPopulation );
   mBrowserModel->setShowSystemEntities( true );
+  mBrowserModel->setListFilter( CollectionFetchScope::Display );
 
 //   new ModelTest( mBrowserModel );
 
diff --git a/korganizer/CMakeLists.txt b/korganizer/CMakeLists.txt
index 277c8e7..b1e0bb3 100644
--- a/korganizer/CMakeLists.txt
+++ b/korganizer/CMakeLists.txt
@@ -173,6 +173,9 @@ set(korganizerprivate_LIB_SRCS
     aboutdata.cpp
     actionmanager.cpp
     akonadicollectionview.cpp
+    views/collectionview/reparentingmodel.cpp
+    views/collectionview/controller.cpp
+    views/collectionview/calendardelegate.cpp
     calendarview.cpp
     datechecker.cpp
     datenavigator.cpp
@@ -276,6 +279,7 @@ set(korganizerprivate_LIB_SRCS
     ${KDE4_KNEWSTUFF3_LIBS}
     ${KDE4_KPRINTUTILS_LIBS}
     ${ZLIB_LIBRARIES}
+    ${BALOO_LIBRARIES}
   )
 
   set_target_properties(korganizerprivate PROPERTIES
@@ -346,3 +350,5 @@ set(korganizerprivate_LIB_SRCS
   )
 
 endif()
+
+add_subdirectory(views/collectionview)
diff --git a/korganizer/actionmanager.cpp b/korganizer/actionmanager.cpp
index 6e8e610..2732cf4 100644
--- a/korganizer/actionmanager.cpp
+++ b/korganizer/actionmanager.cpp
@@ -327,6 +327,15 @@ void ActionManager::initActions()
 
   /************************** EDIT MENU *********************************/
 
+  //Disable a calendar or remove a referenced calendar
+  QAction *disableAction = mACollection->addAction( QLatin1String("collection_disable"), mCollectionView, SLOT(edit_disable()) );
+  disableAction->setText( i18n( "Disable Calendar" ) );
+
+  //Enable (subscribe) to a calendar.
+  QAction *enableAction = mACollection->addAction( QLatin1String("collection_enable"), mCollectionView, SLOT(edit_enable()) );
+  enableAction->setText( i18n( "Enable Calendar" ) );
+  //TODO: hide option on enabled collections
+
   QAction *pasteAction;
   Akonadi::History *history = mCalendarView->history();
   if ( mIsPart ) {
diff --git a/korganizer/akonadicollectionview.cpp b/korganizer/akonadicollectionview.cpp
index 7f88b89..4471cfa 100644
--- a/korganizer/akonadicollectionview.cpp
+++ b/korganizer/akonadicollectionview.cpp
@@ -31,6 +31,8 @@
 #include "kohelper.h"
 #include "koprefs.h"
 #include "koglobals.h"
+#include "views/collectionview/reparentingmodel.h"
+#include "views/collectionview/calendardelegate.h"
 
 #include <calendarsupport/kcalprefs.h>
 #include <calendarsupport/utils.h>
@@ -46,6 +48,7 @@
 #include <Akonadi/EntityTreeModel>
 #include <Akonadi/ETMViewStateSaver>
 #include <Akonadi/Calendar/StandardCalendarActionManager>
+#include <akonadi/collectionidentificationattribute.h>
 
 #include <KAction>
 #include <KActionCollection>
@@ -53,11 +56,170 @@
 #include <KColorDialog>
 #include <KMessageBox>
 #include <KRecursiveFilterProxyModel>
+#include <KLineEdit>
 
 #include <QHeaderView>
 #include <QPainter>
 #include <QStyledItemDelegate>
 #include <QVBoxLayout>
+#include <QStackedWidget>
+
+static Akonadi::EntityTreeModel *findEtm(QAbstractItemModel *model)
+{
+    QAbstractProxyModel *proxyModel;
+    while (model) {
+        proxyModel = qobject_cast<QAbstractProxyModel*>(model);
+        if (proxyModel && proxyModel->sourceModel()) {
+            model = proxyModel->sourceModel();
+        } else {
+            break;
+        }
+    }
+    return qobject_cast<Akonadi::EntityTreeModel*>(model);
+}
+
+/**
+* Automatically checks new calendar entries
+*/
+class NewCalendarChecker : public QObject {
+    Q_OBJECT
+public:
+    NewCalendarChecker(QAbstractItemModel *model)
+        :QObject(model),
+        mCheckableProxy(model)
+    {
+        connect(model, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(onSourceRowsInserted(QModelIndex, int, int)));
+    }
+
+private slots:
+    void onSourceRowsInserted(const QModelIndex &parent, int start, int end)
+    {
+        Akonadi::EntityTreeModel *etm = findEtm(mCheckableProxy);
+        //Only check new collections and not during initial population
+        if (!etm || !etm->isCollectionTreeFetched()) {
+            return;
+        }
+        for (int i = start; i <= end; i++) {
+            kDebug() << "checking " << mCheckableProxy->index(i, 0, parent).data().toString();
+            const QModelIndex index = mCheckableProxy->index(i, 0, parent);
+            mCheckableProxy->setData(index, Qt::Checked, Qt::CheckStateRole);
+            if (mCheckableProxy->hasChildren(index)) {
+                onSourceRowsInserted(index, 0, mCheckableProxy->rowCount(index) - 1);
+            }
+        }
+    }
+
+private:
+    QAbstractItemModel *mCheckableProxy;
+};
+
+
+/**
+* Handles expansion state of a treeview
+*
+* Persists state, and automatically expands new entries.
+* With expandAll enabled this class simply ensures that all indexes are fully expanded.
+*/
+class NewNodeExpander : public QObject {
+    Q_OBJECT
+public:
+    NewNodeExpander(QTreeView *view, bool expandAll, const QString &treeStateConfig)
+        :QObject(view),
+        mTreeView(view),
+        mExpandAll(expandAll),
+        mTreeStateConfig(treeStateConfig)
+    {
+        connect(view->model(), SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(onSourceRowsInserted(QModelIndex, int, int)));
+        connect(view->model(), SIGNAL(layoutChanged()), this, SLOT(onLayoutChanged()));
+        connect(view->model(), SIGNAL(modelReset()), this, SLOT(onModelReset()));
+        restoreTreeState();
+    }
+
+    virtual ~NewNodeExpander()
+    {
+        //Ideally we'd automatically save the treestate of the parent view here,
+        //but that unfortunately doesn't seem to work
+    }
+
+public Q_SLOTS:
+    void saveState()
+    {
+        saveTreeState();
+    }
+
+private Q_SLOTS:
+    void onSourceRowsInserted(const QModelIndex &parent, int start, int end)
+    {
+        //The initial expansion is handled by the state saver
+        if (!mExpandAll) {
+            Akonadi::EntityTreeModel *etm = findEtm(mTreeView->model());
+            if (!etm || !etm->isCollectionTreeFetched()) {
+                restoreTreeState();
+                return;
+            }
+        }
+        for (int i = start; i <= end; i++) {
+            const QModelIndex index = mTreeView->model()->index(i, 0, parent);
+            // kDebug() << "expanding " << index.data().toString();
+            mTreeView->expand(index);
+            if (mTreeView->model()->hasChildren(index)) {
+                onSourceRowsInserted(index, 0, mTreeView->model()->rowCount(index) - 1);
+            }
+        }
+    }
+
+    void onLayoutChanged()
+    {
+        if (mExpandAll) {
+            onSourceRowsInserted(QModelIndex(), 0, mTreeView->model()->rowCount(QModelIndex()) - 1);
+        }
+    }
+
+    void onModelReset()
+    {
+        if (mExpandAll) {
+            onSourceRowsInserted(QModelIndex(), 0, mTreeView->model()->rowCount(QModelIndex()) - 1);
+        }
+    }
+
+private:
+    void saveTreeState()
+    {
+        Akonadi::ETMViewStateSaver treeStateSaver;
+        KConfigGroup group(KOGlobals::self()->config(), mTreeStateConfig);
+        treeStateSaver.setView(mTreeView);
+        treeStateSaver.setSelectionModel(0); // we only save expand state
+        treeStateSaver.saveState(group);
+    }
+
+    void restoreTreeState()
+    {
+        if (mTreeStateConfig.isEmpty()) {
+            return;
+        }
+        //Otherwise ETMViewStateSaver crashes
+        if (!findEtm(mTreeView->model())) {
+            return;
+        }
+        if ( treeStateRestorer ) {// We don't need more than one to be running at the same time
+            delete treeStateRestorer;
+        }
+        kDebug() << "Restore tree state";
+        treeStateRestorer = new Akonadi::ETMViewStateSaver(); // not a leak
+        KConfigGroup group( KOGlobals::self()->config(), mTreeStateConfig );
+        treeStateRestorer->setView( mTreeView );
+        treeStateRestorer->setSelectionModel( 0 ); // we only restore expand state
+        treeStateRestorer->restoreState( group );
+    }
+
+    QPointer<Akonadi::ETMViewStateSaver> treeStateRestorer;
+    QTreeView *mTreeView;
+    bool mExpandAll;
+    QString mTreeStateConfig;
+};
+
+
+
 
 AkonadiCollectionViewFactory::AkonadiCollectionViewFactory( CalendarView *view )
   : mView( view ), mAkonadiCollectionView( 0 )
@@ -99,7 +261,7 @@ class ColorDelegate : public QStyledItemDelegate
       QStyledItemDelegate::paint( painter, option, index );
       QStyleOptionViewItemV4 v4 = option;
       initStyleOption( &v4, index );
-      if ( v4.checkState == Qt::Checked ) {
+      if ( v4.checkState ) {
         const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index );
         QColor color = KOHelper::resourceColor( collection );
         if ( color.isValid() ) {
@@ -121,6 +283,32 @@ class ColorDelegate : public QStyledItemDelegate
     }
 };
 
+class SortProxyModel : public QSortFilterProxyModel
+{
+  public:
+    explicit SortProxyModel( QObject *parent=0 )
+      : QSortFilterProxyModel( parent )
+    {
+    }
+
+     bool lessThan(const QModelIndex &left,
+                                       const QModelIndex &right) const
+    {
+        QVariant leftPerson = left.data(PersonNode::PersonRole);
+        QVariant rightPerson = right.data(PersonNode::PersonRole);
+        if (leftPerson.isValid() && !rightPerson.isValid()) {
+            return true;
+        }
+        QString leftString = left.data().toString();
+        QString rightString = right.data().toString();
+        if (leftPerson.isValid() && rightPerson.isValid()) {
+            leftString = leftPerson.value<Person>().name;
+            rightString = rightPerson.value<Person>().name;
+        }
+        return QString::localeAwareCompare(leftString, rightString) < 0;
+    }
+};
+
 class ColorProxyModel : public QSortFilterProxyModel
 {
   public:
@@ -172,6 +360,28 @@ class ColorProxyModel : public QSortFilterProxyModel
     mutable bool mInitDefaultCalendar;
 };
 
+class CollectionFilter : public QSortFilterProxyModel
+{
+  public:
+    explicit CollectionFilter( QObject *parent=0 )
+      : QSortFilterProxyModel( parent )
+    {
+        setDynamicSortFilter(true);
+    }
+
+  protected:
+    virtual bool filterAcceptsRow(int row, const QModelIndex &sourceParent) const {
+        const QModelIndex sourceIndex = sourceParent.child(row, 0);
+        const Akonadi::Collection &col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+        CollectionIdentificationAttribute *attr = col.attribute<CollectionIdentificationAttribute>();
+        //We filter the user folders because we insert person nodes for user folders.
+        if (attr && ((attr->collectionNamespace() == "usertoplevel") || (attr->collectionNamespace() == "usertoplevel"))) {
+            return false;
+        }
+        return true;
+    }
+};
+
 } // anonymous namespace
 
 CalendarViewExtension *AkonadiCollectionViewFactory::create( QWidget *parent )
@@ -209,44 +419,84 @@ AkonadiCollectionView::AkonadiCollectionView( CalendarView *view, bool hasContex
   topLayout->setMargin( 0 );
   topLayout->setSpacing( KDialog::spacingHint() );
 
-  //KLineEdit *searchCol = new KLineEdit( this );
-  //searchCol->setClearButtonShown( true );
-  //searchCol->setClickMessage( i18nc( "@info/plain Displayed grayed-out inside the "
-  //                                                   "textbox, verb to search", "Search" ) );
-  //topLayout->addWidget( searchCol );
+  KLineEdit *searchCol = new KLineEdit( this );
+  searchCol->setClearButtonShown( true );
+  searchCol->setClickMessage( i18nc( "@info/plain Displayed grayed-out inside the "
+                                                    "textbox, verb to search", "Search" ) );
+  topLayout->addWidget( searchCol );
 
   ColorProxyModel *colorProxy = new ColorProxyModel( this );
   colorProxy->setObjectName( QLatin1String("Show calendar colors") );
   colorProxy->setDynamicSortFilter( true );
   mBaseModel = colorProxy;
 
+
+  //Model that displays users
+  ReparentingModel *userProxy = new ReparentingModel( this );
+  userProxy->setNodeManager(ReparentingModel::NodeManager::Ptr(new PersonNodeManager(*userProxy)));
+  userProxy->setSourceModel(colorProxy);
+
+  SortProxyModel *sortProxy = new SortProxyModel( this );
+  // sortProxy->setObjectName( QLatin1String("Show calendar colors") );
+  sortProxy->setDynamicSortFilter( true );
+  sortProxy->setSourceModel(userProxy);
+
+  //Hide collections that are not required
+  CollectionFilter *collectionFilter = new CollectionFilter( this );
+  collectionFilter->setDynamicSortFilter( true );
+  collectionFilter->setSourceModel( sortProxy );
+
   mCollectionView = new Akonadi::EntityTreeView( this );
-  topLayout->addWidget( mCollectionView );
   mCollectionView->header()->hide();
   mCollectionView->setRootIsDecorated( true );
-  mCollectionView->setItemDelegate( new ColorDelegate( this ) );
-
-  //Filter tree view.
-  //KRecursiveFilterProxyModel *filterTreeViewModel = new KRecursiveFilterProxyModel( this );
-  //filterTreeViewModel->setDynamicSortFilter( true );
-  //filterTreeViewModel->setSourceModel( colorProxy );
-  //filterTreeViewModel->setFilterCaseSensitivity( Qt::CaseInsensitive );
-  //filterTreeViewModel->setObjectName( "Recursive filtering, for the search bar" );
-  mCollectionView->setModel( colorProxy );
+  {
+    StyledCalendarDelegate *delegate = new StyledCalendarDelegate(mCollectionView);
+    connect(delegate, SIGNAL(enabled(QModelIndex, bool)), this, SLOT(onCalendarEnabled(QModelIndex, bool)));
+    mCollectionView->setItemDelegate( delegate );
+  }
+  mCollectionView->setModel( collectionFilter );
   connect( mCollectionView->selectionModel(),
            SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
            SLOT(updateMenu()) );
+  mNewNodeExpander = new NewNodeExpander(mCollectionView, false, QLatin1String("CollectionTreeView"));
 
-  connect( mCollectionView->model(), SIGNAL(rowsInserted(QModelIndex,int,int)),
-           SLOT(checkNewCalendar(QModelIndex,int,int)) );
 
-  //connect( searchCol, SIGNAL(textChanged(QString)),
-  //         filterTreeViewModel, SLOT(setFilterFixedString(QString)) );
+  //Filter tree view.
+  KRecursiveFilterProxyModel *filterTreeViewModel = new KRecursiveFilterProxyModel( this );
+  filterTreeViewModel->setDynamicSortFilter( true );
+  filterTreeViewModel->setSourceModel( userProxy );
+  filterTreeViewModel->setFilterCaseSensitivity( Qt::CaseInsensitive );
+//   filterTreeViewModel->setObjectName( "Recursive filtering, for the search bar" );
+  connect( searchCol, SIGNAL(textChanged(QString)),
+          filterTreeViewModel, SLOT(setFilterFixedString(QString)) );
+
+  ReparentingModel *searchProxy = new ReparentingModel( this );
+  searchProxy->setSourceModel(filterTreeViewModel);
+
+
+  Akonadi::EntityTreeView *mSearchView = new Akonadi::EntityTreeView( this );
+  mSearchView->header()->hide();
+  mSearchView->setRootIsDecorated( true );
+  mSearchView->setItemDelegate( new ColorDelegate( this ) );
+  mSearchView->setModel( searchProxy );
+  new NewNodeExpander(mSearchView, true, QString());
+
+  mController = new Controller(userProxy, searchProxy, this);
+  connect( searchCol, SIGNAL(textChanged(QString)),
+          mController, SLOT(setSearchString(QString)) );
+  connect( mController, SIGNAL(searchIsActive(bool)),
+          this, SLOT(onSearchIsActive(bool)) );
+
+  mStackedWidget = new QStackedWidget(this);
+  mStackedWidget->addWidget(mCollectionView);
+  mStackedWidget->addWidget(mSearchView);
+  mStackedWidget->setCurrentWidget(mCollectionView);
+
+  topLayout->addWidget( mStackedWidget );
 
   connect( mBaseModel, SIGNAL(rowsInserted(QModelIndex,int,int)),
            this, SLOT(rowsInserted(QModelIndex,int,int)) );
 
-  //mCollectionView->setSelectionMode( QAbstractItemView::NoSelection );
   KXMLGUIClient *xmlclient = KOCore::self()->xmlguiClient( view );
   if ( xmlclient ) {
     mCollectionView->setXmlGuiClient( xmlclient );
@@ -337,24 +587,17 @@ AkonadiCollectionView::AkonadiCollectionView( CalendarView *view, bool hasContex
 
 AkonadiCollectionView::~AkonadiCollectionView()
 {
-  Akonadi::ETMViewStateSaver treeStateSaver;
-  KConfigGroup group( KOGlobals::self()->config(), "CollectionTreeView" );
-  treeStateSaver.setView( mCollectionView );
-  treeStateSaver.setSelectionModel( 0 ); // we only save expand state
-  treeStateSaver.saveState( group );
+    //Necessary because it's apparently impossible to detect in the note expander when to save the state before view get's deleted
+    mNewNodeExpander->saveState();
 }
 
-void AkonadiCollectionView::restoreTreeState()
+void AkonadiCollectionView::onSearchIsActive(bool active)
 {
-  static QPointer<Akonadi::ETMViewStateSaver> treeStateRestorer;
-  if ( treeStateRestorer ) {// We don't need more than one to be running at the same time
-    delete treeStateRestorer;
-  }
-  treeStateRestorer = new Akonadi::ETMViewStateSaver(); // not a leak
-  KConfigGroup group( KOGlobals::self()->config(), "CollectionTreeView" );
-  treeStateRestorer->setView( mCollectionView );
-  treeStateRestorer->setSelectionModel( 0 ); // we only restore expand state
-  treeStateRestorer->restoreState( group );
+    if (!active) {
+        mStackedWidget->setCurrentIndex(0);
+    } else {
+        mStackedWidget->setCurrentIndex(1);
+    }
 }
 
 void AkonadiCollectionView::setDefaultCalendar()
@@ -413,6 +656,7 @@ void AkonadiCollectionView::setCollectionSelectionProxyModel( KCheckableProxyMod
     return;
   }
 
+  new NewCalendarChecker( m );
   mBaseModel->setSourceModel( mSelectionProxyModel );
 }
 
@@ -444,26 +688,22 @@ void AkonadiCollectionView::updateMenu()
   mAssignColor->setEnabled( enableAction );
   QModelIndex index = mCollectionView->selectionModel()->currentIndex(); //selectedRows()
 
-  bool disableStuff = false;
+  bool disableStuff = true;
 
   if ( index.isValid() ) {
+    //Returns an invalid collection on person nodes
     const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index );
-    Q_ASSERT( collection.isValid() );
 
-    if ( !collection.contentMimeTypes().isEmpty() ) {
+    if ( collection.isValid() && !collection.contentMimeTypes().isEmpty() ) {
       const QString identifier = QString::number( collection.id() );
       const QColor defaultColor = KOPrefs::instance()->resourceColor( identifier );
       enableAction = enableAction && defaultColor.isValid();
       mDisableColor->setEnabled( enableAction );
       mDefaultCalendar->setEnabled( !KOHelper::isStandardCalendar( collection.id() ) &&
                                     collection.rights() & Akonadi::Collection::CanCreateItem );
-    } else {
-      disableStuff = true;
+      disableStuff = false;
     }
-  } else {
-    disableStuff = true;
   }
-
   if ( disableStuff ) {
     mDisableColor->setEnabled( false );
     mDefaultCalendar->setEnabled( false );
@@ -563,7 +803,6 @@ void AkonadiCollectionView::rowsInserted( const QModelIndex &, int, int )
   if ( !mNotSendAddRemoveSignal ) {
     emit resourcesAddedRemoved();
   }
-  restoreTreeState();
 }
 
 Akonadi::Collection AkonadiCollectionView::selectedCollection() const
@@ -635,19 +874,44 @@ Akonadi::EntityTreeModel *AkonadiCollectionView::entityTreeModel() const
   return 0;
 }
 
-void AkonadiCollectionView::checkNewCalendar( const QModelIndex &parent, int begin, int end )
+void AkonadiCollectionView::edit_disable()
 {
-  // HACK: Check newly created calendars
-  Akonadi::EntityTreeModel *etm = entityTreeModel();
-  if ( etm && entityTreeModel()->isCollectionTreeFetched() ) {
-    for( int row=begin; row<=end; ++row ) {
-      QModelIndex index = mCollectionView->model()->index( row, 0, parent );
-      if ( index.isValid() )
-        mCollectionView->model()->setData( index, Qt::Checked, Qt::CheckStateRole );
+    Akonadi::Collection col = mCollectionView->currentIndex().data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+    if (col.isValid()) {
+        mController->setCollection(col, false, false);
     }
-    if ( parent.isValid() ) {
-      mCollectionView->setExpanded( parent, true );
+}
+
+void AkonadiCollectionView::edit_enable()
+{
+    Akonadi::Collection col = mCollectionView->currentIndex().data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+    if (col.isValid()) {
+        mController->setCollection(col, true, false);
+    }
+}
+
+void AkonadiCollectionView::onCalendarEnabled(const QModelIndex &index, bool enabled)
+{
+    const Akonadi::Collection col = CalendarSupport::collectionFromIndex(index);
+    if (col.isValid()) {
+        mController->setCollection(col, enabled, false);
+    } else {
+        if (!enabled) {
+            //Disable all child collections
+            const QAbstractItemModel *model = index.model();
+            for (int row = 0; row < model->rowCount(index); row++) {
+                const Akonadi::Collection col = CalendarSupport::collectionFromIndex(model->index(row, 0, index));
+                if (col.isValid()) {
+                    mController->setCollection(col, false, false);
+                }
+            }
+        }
+        
+        const QVariant var = index.data(PersonNode::PersonRole);
+        if (var.isValid()) {
+            mController->setPersonDisabled(var.value<Person>());
+        }
     }
-  }
 }
 
+#include "akonadicollectionview.moc"
diff --git a/korganizer/akonadicollectionview.h b/korganizer/akonadicollectionview.h
index b4d34cd..8c68155 100644
--- a/korganizer/akonadicollectionview.h
+++ b/korganizer/akonadicollectionview.h
@@ -28,7 +28,10 @@
 #define KORG_AKONADICOLLECTIONVIEW_H
 
 #include "calendarview.h"
+#include "views/collectionview/reparentingmodel.h"
+#include "views/collectionview/controller.h"
 #include <Akonadi/Collection>
+#include <kidentityproxymodel.h>
 
 class AkonadiCollectionView;
 
@@ -61,6 +64,8 @@ class AkonadiCollectionViewFactory : public CalendarViewExtension::Factory
     AkonadiCollectionView *mAkonadiCollectionView;
 };
 
+class NewNodeExpander;
+
 /**
  * This class provides a view of calendar resources.
  */
@@ -80,6 +85,9 @@ class AkonadiCollectionView : public CalendarViewExtension
     Akonadi::Collection selectedCollection() const;
     Akonadi::Collection::List checkedCollections() const;
     bool isChecked(const Akonadi::Collection &) const;
+  public Q_SLOTS:
+    void edit_disable();
+    void edit_enable();
 
   Q_SIGNALS:
     void resourcesChanged( bool enabled );
@@ -90,8 +98,6 @@ class AkonadiCollectionView : public CalendarViewExtension
   private Q_SLOTS:
     void updateView();
     void updateMenu();
-    void restoreTreeState();
-    void checkNewCalendar( const QModelIndex &parent, int begin, int end );
 
     void newCalendar();
     void newCalendarDone( KJob * );
@@ -102,12 +108,15 @@ class AkonadiCollectionView : public CalendarViewExtension
     void assignColor();
     void disableColor();
     void setDefaultCalendar();
+    void onSearchIsActive(bool);
+    void onCalendarEnabled(const QModelIndex &, bool);
 
   private:
     Akonadi::EntityTreeModel *entityTreeModel() const;
 
     Akonadi::StandardCalendarActionManager *mActionManager;
     Akonadi::EntityTreeView *mCollectionView;
+    QStackedWidget *mStackedWidget;
     QAbstractProxyModel *mBaseModel;
     KCheckableProxyModel *mSelectionProxyModel;
     KAction *mAssignColor;
@@ -116,6 +125,9 @@ class AkonadiCollectionView : public CalendarViewExtension
     bool mNotSendAddRemoveSignal;
     bool mWasDefaultCalendar;
     bool mHasContextMenu;
+    Controller *mController;
+    NewNodeExpander *mNewNodeExpander;
   };
 
+
 #endif
diff --git a/korganizer/korganizerui.rc b/korganizer/korganizerui.rc
index 632f154..e5545b1 100644
--- a/korganizer/korganizerui.rc
+++ b/korganizer/korganizerui.rc
@@ -166,6 +166,8 @@
   <Menu name="akonadi_collectionview_contextmenu">
     <Action name="akonadi_collection_create"/>
     <Action name="akonadi_collection_delete"/>
+    <Action name="collection_disable"/><text>Disable Calendar</text>
+    <Action name="collection_enable"/><text>Enable Calendar</text>
     <Separator/>
       <Menu name="calendar_color"><text>Calendar Colors</text>
       <Action name="assign_color"/>
diff --git a/korganizer/views/collectionview/CMakeLists.txt b/korganizer/views/collectionview/CMakeLists.txt
new file mode 100644
index 0000000..571a126
--- /dev/null
+++ b/korganizer/views/collectionview/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(tests)
\ No newline at end of file
diff --git a/korganizer/views/collectionview/calendardelegate.cpp b/korganizer/views/collectionview/calendardelegate.cpp
new file mode 100644
index 0000000..a19e14f
--- /dev/null
+++ b/korganizer/views/collectionview/calendardelegate.cpp
@@ -0,0 +1,154 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library 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 Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+#include "calendardelegate.h"
+
+#include <KIcon>
+#include <KIconLoader>
+
+#include <QApplication>
+#include <QPainter>
+#include <QMouseEvent>
+
+#include <calendarsupport/utils.h>
+#include <kohelper.h>
+
+StyledCalendarDelegate::StyledCalendarDelegate(QObject * parent)
+    : QStyledItemDelegate(parent)
+{
+}
+
+StyledCalendarDelegate::~StyledCalendarDelegate()
+{
+
+}
+
+static QRect enableButtonRect(const QRect &rect)
+{
+    QRect r = rect;
+    const int h = r.height()- 4;
+    r.adjust(r.width()- h*2 - 2*2, 2, -2 - h - 2, -2);
+    return r;
+}
+
+static QStyle *style(const QStyleOptionViewItem &option)
+{
+    QWidget const *widget = 0;
+    if (const QStyleOptionViewItemV3 *v3 = qstyleoption_cast<const QStyleOptionViewItemV3 *>(&option)) {
+        widget = v3->widget;
+    }
+    QStyle *style = widget ? widget->style() : QApplication::style();
+    return style;
+
+}
+
+void StyledCalendarDelegate::paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
+{
+    Q_ASSERT(index.isValid());
+
+    QStyledItemDelegate::paint( painter, option, index );
+    QStyleOptionViewItemV4 opt = option;
+    initStyleOption(&opt, index);
+
+    QStyle *s = style(option);
+
+    const Akonadi::Collection col = CalendarSupport::collectionFromIndex(index);
+
+    //Favorite button
+    {
+        static QPixmap enablePixmap = KIconLoader().loadIcon(QLatin1String("bookmarks"), KIconLoader::Small);
+        static QPixmap disablePixmap = KIconLoader().loadIcon(QLatin1String("window-close"), KIconLoader::Small);
+        QStyleOptionButton buttonOpt;
+        if (!col.shouldList(Akonadi::Collection::ListDisplay)) {
+            buttonOpt.icon = enablePixmap;
+        } else {
+            buttonOpt.icon = disablePixmap;
+        }
+        QRect r = opt.rect;
+        const int h = r.height()- 4;
+        buttonOpt.rect = enableButtonRect(r);
+        buttonOpt.state = QStyle::State_Active | QStyle::State_Enabled;
+        buttonOpt.iconSize = QSize(h, h);
+
+        s->drawControl(QStyle::CE_PushButton, &buttonOpt, painter, 0);
+    }
+
+    //Color indicator
+    if (opt.checkState){
+        QColor color = KOHelper::resourceColor(col);
+        if (color.isValid()){
+            QRect r = opt.rect;
+            const int h = r.height()- 4;
+            r.adjust(r.width()- h - 2, 2, - 2, -2);
+            painter->save();
+            painter->setRenderHint(QPainter::Antialiasing);
+            QPen pen = painter->pen();
+            pen.setColor(color);
+            QPainterPath path;
+            path.addRoundedRect(r, 5, 5);
+            color.setAlpha(200);
+            painter->fillPath(path, color);
+            painter->strokePath(path, pen);
+            painter->restore();
+        }
+    }
+}
+
+bool StyledCalendarDelegate::editorEvent(QEvent *event,
+                                QAbstractItemModel *model,
+                                const QStyleOptionViewItem &option,
+                                const QModelIndex &index)
+{
+    Q_ASSERT(event);
+    Q_ASSERT(model);
+
+    // make sure that we have the right event type
+    if ((event->type() == QEvent::MouseButtonRelease)
+        || (event->type() == QEvent::MouseButtonDblClick)
+        || (event->type() == QEvent::MouseButtonPress)) {
+
+        QRect buttonRect = enableButtonRect(option.rect);
+
+        QMouseEvent *me = static_cast<QMouseEvent*>(event);
+        if (me->button() != Qt::LeftButton || !buttonRect.contains(me->pos())) {
+            return QStyledItemDelegate::editorEvent(event, model, option, index);
+        }
+
+        if ((event->type() == QEvent::MouseButtonPress)
+            || (event->type() == QEvent::MouseButtonDblClick)) {
+            return true;
+        }
+    } else {
+        return QStyledItemDelegate::editorEvent(event, model, option, index);
+    }
+
+    onEnableButtonClicked(index);
+    return true;
+}
+
+void StyledCalendarDelegate::onEnableButtonClicked(const QModelIndex &index)
+{
+    const Akonadi::Collection col = CalendarSupport::collectionFromIndex(index);
+    emit enabled(index, !col.enabled());
+}
+
+QSize StyledCalendarDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const
+{
+    return QStyledItemDelegate::sizeHint(option, index);
+}
+
diff --git a/korganizer/views/collectionview/calendardelegate.h b/korganizer/views/collectionview/calendardelegate.h
new file mode 100644
index 0000000..a43bdd6
--- /dev/null
+++ b/korganizer/views/collectionview/calendardelegate.h
@@ -0,0 +1,49 @@
+/*
+    Copyright (c) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+    This library is free software; you can redistribute it and/or modify it
+    under the terms of the GNU Library General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.
+
+    This library 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 Library General Public
+    License for more details.
+
+    You should have received a copy of the GNU Library General Public License
+    along with this library; see the file COPYING.LIB.  If not, write to the
+    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+    02110-1301, USA.
+*/
+#ifndef CALENDARDELEGATE_H
+#define CALENDARDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+class StyledCalendarDelegate : public QStyledItemDelegate
+{
+    Q_OBJECT
+
+public:
+    StyledCalendarDelegate(QObject * parent);
+    virtual ~StyledCalendarDelegate();
+
+    void paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const;
+    QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const;
+
+Q_SIGNALS:
+    void enabled(const QModelIndex &, bool);
+
+protected:
+    bool editorEvent(QEvent *event,
+                    QAbstractItemModel *model,
+                    const QStyleOptionViewItem &option,
+                    const QModelIndex &index);
+
+private:
+    void onEnableButtonClicked(const QModelIndex &index);
+};
+    
+#endif
+
diff --git a/korganizer/views/collectionview/controller.cpp b/korganizer/views/collectionview/controller.cpp
new file mode 100644
index 0000000..cea7f2c
--- /dev/null
+++ b/korganizer/views/collectionview/controller.cpp
@@ -0,0 +1,528 @@
+/*
+  Copyright (C) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+  This program 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 2 of the License, or
+  (at your option) any later version.
+
+  This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+#include "controller.h"
+
+#include <Akonadi/EntityTreeModel>
+#include <Akonadi/EntityDisplayAttribute>
+#include <Akonadi/CollectionModifyJob>
+#include <Akonadi/CollectionFetchJob>
+#include <Akonadi/CollectionFetchScope>
+#include <Akonadi/AttributeFactory>
+#include <KIcon>
+#include <KCalCore/Event>
+#include <KCalCore/Journal>
+#include <KCalCore/Todo>
+#include <baloo/pim/collectionquery.h>
+#include <akonadi/collectionidentificationattribute.h>
+
+CollectionNode::CollectionNode(ReparentingModel& personModel, const Akonadi::Collection& col)
+:   Node(personModel),
+    mCollection(col),
+    mCheckState(Qt::Unchecked)
+{
+}
+
+CollectionNode::~CollectionNode()
+{
+
+}
+
+bool CollectionNode::operator==(const ReparentingModel::Node &node) const
+{
+    const CollectionNode *collectionNode = dynamic_cast<const CollectionNode*>(&node);
+    if (collectionNode) {
+        return (collectionNode->mCollection == mCollection);
+    }
+    return false;
+}
+
+QVariant CollectionNode::data(int role) const
+{
+    if (role == Qt::DisplayRole) {
+        QStringList path;
+        Akonadi::Collection c = mCollection;
+        while (c.isValid()) {
+            path.prepend(c.name());
+            c = c.parentCollection();
+        }
+        return path.join(QLatin1String("/"));
+    }
+    if (role == Qt::DecorationRole) {
+        if (mCollection.hasAttribute<Akonadi::EntityDisplayAttribute>()) {
+            return mCollection.attribute<Akonadi::EntityDisplayAttribute>()->icon();
+        }
+        return QVariant();
+    }
+    if (role == Qt::CheckStateRole) {
+        return mCheckState;
+    }
+    if (role == Qt::ToolTipRole) {
+        return QString(QLatin1String("Collection: ") + mCollection.name() + QString::number(mCollection.id()));
+    }
+    return QVariant();
+}
+
+bool CollectionNode::setData(const QVariant& value, int role)
+{
+    if (role == Qt::CheckStateRole) {
+        mCheckState = static_cast<Qt::CheckState>(value.toInt());
+        emitter.emitEnabled(mCheckState == Qt::Checked, mCollection);
+        return true;
+    }
+    return false;
+}
+
+bool CollectionNode::isDuplicateOf(const QModelIndex& sourceIndex)
+{
+    return (sourceIndex.data(Akonadi::EntityTreeModel::CollectionIdRole).value<Akonadi::Collection::Id>() == mCollection.id());
+}
+
+
+PersonNode::PersonNode(ReparentingModel& personModel, const Person& person)
+:   Node(personModel),
+    mPerson(person),
+    mCheckState(Qt::Unchecked)
+{
+
+}
+
+PersonNode::~PersonNode()
+{
+
+}
+
+bool PersonNode::operator==(const Node &node) const
+{
+    const PersonNode *personNode = dynamic_cast<const PersonNode*>(&node);
+    if (personNode) {
+        return (personNode->mPerson.name == mPerson.name);
+    }
+    return false;
+}
+
+void PersonNode::setChecked(bool enabled)
+{
+    if (enabled) {
+        mCheckState = Qt::Checked;
+    } else {
+        mCheckState = Qt::Unchecked;
+    }
+}
+
+QVariant PersonNode::data(int role) const
+{
+    if (role == Qt::DisplayRole) {
+        return mPerson.name;
+    }
+    if (role == Qt::DecorationRole) {
+        return KIcon(QLatin1String("meeting-participant"));
+    }
+    if (role == Qt::CheckStateRole) {
+        return mCheckState;
+    }
+    if (role == Qt::ToolTipRole) {
+        return QString(QLatin1String("Person: ") + mPerson.name);
+    }
+    if (role == PersonRole) {
+        return QVariant::fromValue(mPerson);
+    }
+    return QVariant();
+}
+
+bool PersonNode::setData(const QVariant& value, int role)
+{
+    if (role == Qt::CheckStateRole) {
+        mCheckState = static_cast<Qt::CheckState>(value.toInt());
+        emitter.emitEnabled(mCheckState == Qt::Checked, mPerson);
+        return true;
+    }
+    return false;
+}
+
+bool PersonNode::adopts(const QModelIndex& sourceIndex)
+{
+    const Akonadi::Collection &parent = sourceIndex.data(Akonadi::EntityTreeModel::ParentCollectionRole).value<Akonadi::Collection>();
+    if (parent.id() == mPerson.rootCollection) {
+        return true;
+    }
+
+    const Akonadi::Collection &col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+    // kDebug() << col.displayName();
+    //FIXME: we need a way to compare the path we get from LDAP to the folder in akonadi.
+    //TODO: get it from the folder attribute
+    if ((col.isValid() && mPerson.folderPaths.contains(col.displayName())) || mPerson.collections.contains(col.id())) {
+        // kDebug() << "reparenting " << col.displayName() << " to " << mPerson.name;
+        return true;
+    }
+    return false;
+}
+
+bool PersonNode::isDuplicateOf(const QModelIndex& sourceIndex)
+{
+    return (sourceIndex.data(PersonRole).value<Person>().name == mPerson.name);
+}
+
+void PersonNodeManager::checkSourceIndex(const QModelIndex &sourceIndex)
+{
+    const Akonadi::Collection col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value<Akonadi::Collection>();
+    kDebug() << col.displayName() << col.enabled();
+    if (col.isValid()) {
+        CollectionIdentificationAttribute *attr = col.attribute<CollectionIdentificationAttribute>();
+        if (attr && attr->collectionNamespace() == "usertoplevel") {
+            kDebug() << "Found user folder, creating person node";
+            Person person;
+            person.name = col.displayName();
+            person.rootCollection = col.id();
+
+            model.addNode(ReparentingModel::Node::Ptr(new PersonNode(model, person)));
+        }
+    }
+}
+
+CollectionSearchJob::CollectionSearchJob(const QString& searchString, QObject* parent)
+    : KJob(parent),
+    mSearchString(searchString)
+{
+}
+
+void CollectionSearchJob::start()
+{
+    Baloo::PIM::CollectionQuery query;
+    //We exclude the other users namespace
+    query.setNamespace(QStringList() << QLatin1String("shared") << QLatin1String(""));
+    query.pathMatches(mSearchString);
+    query.setMimetype(QStringList() << QLatin1String("text/calendar"));
+    query.setLimit(200);
+    Baloo::PIM::ResultIterator it = query.exec();
+    Akonadi::Collection::List collections;
+    while (it.next()) {
+        collections << Akonadi::Collection(it.id());
+    }
+    kDebug() << "Found collections " << collections.size();
+    
+    if (collections.isEmpty()) {
+        //We didn't find anything
+        emitResult();
+        return;
+    }
+
+    Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(collections, Akonadi::CollectionFetchJob::Base, this);
+    fetchJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+    fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
+    connect(fetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(onCollectionsReceived(Akonadi::Collection::List)));
+    connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onCollectionsFetched(KJob*)));
+}
+
+void CollectionSearchJob::onCollectionsReceived(const Akonadi::Collection::List &list)
+{
+    Q_FOREACH(const Akonadi::Collection &col, list) {
+        if (col.name().contains(mSearchString)) {
+            mMatchingCollections << col;
+            Akonadi::Collection ancestor = col.parentCollection();
+            while (ancestor.isValid() && (ancestor != Akonadi::Collection::root())) {
+                if (!mAncestors.contains(ancestor)) {
+                    mAncestors << ancestor;
+                }
+                ancestor = ancestor.parentCollection();
+            }
+        }
+    }
+}
+
+void CollectionSearchJob::onCollectionsFetched(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+    }
+    if (!mAncestors.isEmpty()) {
+        Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(mAncestors, Akonadi::CollectionFetchJob::Base, this);
+        fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
+        connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onAncestorsFetched(KJob*)));
+    } else {
+        //We didn't find anything
+        emitResult();
+    }
+}
+
+static Akonadi::Collection replaceParent(Akonadi::Collection col, const Akonadi::Collection::List &ancestors)
+{
+    if (!col.isValid()) {
+        return col;
+    }
+    const Akonadi::Collection parent = replaceParent(col.parentCollection(), ancestors);
+    Q_FOREACH (const Akonadi::Collection &c, ancestors) {
+        if (col == c) {
+            col = c;
+        }
+    }
+    col.setParentCollection(parent);
+    return col;
+}
+
+void CollectionSearchJob::onAncestorsFetched(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+    }
+    Akonadi::CollectionFetchJob *fetchJob = static_cast<Akonadi::CollectionFetchJob*>(job);
+    Akonadi::Collection::List matchingCollections;
+    Q_FOREACH (const Akonadi::Collection &c, mMatchingCollections) {
+        //We need to replace the parents with the version that contains the name, so we can display it accordingly
+        matchingCollections << replaceParent(c, fetchJob->collections());
+    }
+    mMatchingCollections = matchingCollections;
+    emitResult();
+}
+
+Akonadi::Collection::List CollectionSearchJob::matchingCollections() const
+{
+    return mMatchingCollections;
+}
+
+
+PersonSearchJob::PersonSearchJob(const QString& searchString, QObject* parent)
+    : KJob(parent),
+    mSearchString(searchString)
+{
+}
+
+void PersonSearchJob::start()
+{
+    Baloo::PIM::CollectionQuery query;
+    query.setNamespace(QStringList() << QLatin1String("usertoplevel"));
+    query.nameMatches(mSearchString);
+    query.setLimit(200);
+    Baloo::PIM::ResultIterator it = query.exec();
+    Akonadi::Collection::List collections;
+    while (it.next()) {
+        collections << Akonadi::Collection(it.id());
+    }
+    kDebug() << "Found persons " << collections.size();
+
+    if (collections.isEmpty()) {
+        //We didn't find anything
+        emitResult();
+        return;
+    }
+
+    Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(collections, Akonadi::CollectionFetchJob::Base, this);
+    fetchJob->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::All);
+    fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
+    connect(fetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(onCollectionsReceived(Akonadi::Collection::List)));
+    connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onCollectionsFetched(KJob*)));
+    //TODO query ldap for available persons and their folders.
+    //TODO identify imap folders as person folders and list them here (after indexing them in baloo).
+    //
+    //The IMAP resource should add a "Person" attribute to the collections in the person namespace,
+    //the ldap query can then be used to update the name (entitydisplayattribute) for the person.
+}
+
+void PersonSearchJob::onCollectionsReceived(const Akonadi::Collection::List &list)
+{
+    Q_FOREACH(const Akonadi::Collection &col, list) {
+        Person person;
+        person.name = col.displayName();
+        person.rootCollection = col.id();
+        mMatches << person;
+    }
+}
+
+void PersonSearchJob::onCollectionsFetched(KJob *job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+    }
+    emitResult();
+}
+
+QList<Person> PersonSearchJob::matches() const
+{
+    return mMatches;
+}
+
+
+Controller::Controller(ReparentingModel* personModel, ReparentingModel* searchModel, QObject* parent)
+    : QObject(parent),
+    mPersonModel(personModel),
+    mSearchModel(searchModel),
+    mCollectionSearchJob(0),
+    mPersonSearchJob(0)
+{
+    Akonadi::AttributeFactory::registerAttribute<CollectionIdentificationAttribute>();
+}
+
+void Controller::setSearchString(const QString &searchString)
+{
+    if (mCollectionSearchJob) {
+        disconnect(mCollectionSearchJob, 0, this, 0);
+        mCollectionSearchJob->kill(KJob::Quietly);
+        mCollectionSearchJob = 0;
+    }
+    if (mPersonSearchJob) {
+        disconnect(mPersonSearchJob, 0, this, 0);
+        mPersonSearchJob->kill(KJob::Quietly);
+        mPersonSearchJob = 0;
+    }
+    //TODO: Delay and abort when results are found
+    mSearchModel->clear();
+    emit searchIsActive(!searchString.isEmpty());
+    if (searchString.size() < 2) {
+        return;
+    }
+
+    mPersonSearchJob = new PersonSearchJob(searchString, this);
+    connect(mPersonSearchJob, SIGNAL(result(KJob*)), this, SLOT(onPersonsFound(KJob*)));
+    mPersonSearchJob->start();
+
+    mCollectionSearchJob = new CollectionSearchJob(searchString, this);
+    connect(mCollectionSearchJob, SIGNAL(result(KJob*)), this, SLOT(onCollectionsFound(KJob*)));
+    mCollectionSearchJob->start();
+}
+
+void Controller::onPersonEnabled(bool enabled, const Person& person)
+{
+    // kDebug() << person.name << enabled;
+    if (enabled) {
+        PersonNode *personNode = new PersonNode(*mPersonModel, person);
+        personNode->setChecked(true);
+        mPersonModel->addNode(ReparentingModel::Node::Ptr(personNode));
+        Akonadi::Collection rootCollection(person.rootCollection);
+        if (rootCollection.isValid()) {
+            //Reference the persons collections if available
+            Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(rootCollection, Akonadi::CollectionFetchJob::Recursive, this);
+            fetchJob->setProperty("enable", enabled);
+            fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter);
+            fetchJob->fetchScope().setContentMimeTypes(QStringList() << QLatin1String("text/calendar"));
+            connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onPersonCollectionsFetched(KJob*)));
+        }
+    } else {
+        //If we accidentally added a person and want to remove it again
+        mPersonModel->removeNode(PersonNode(*mPersonModel, person));
+        //Dereference subcollections
+    }
+
+}
+
+void Controller::onPersonCollectionsFetched(KJob* job)
+{
+    if (job->error()) {
+        kWarning() << "Failed to fetch collections " << job->errorString();
+        return;
+    }
+    const bool enable = job->property("enable").toBool();
+    Q_FOREACH(const Akonadi::Collection &col, static_cast<Akonadi::CollectionFetchJob*>(job)->collections()) {
+        setCollectionReferenced(enable, col);
+    }
+}
+
+void Controller::onCollectionsFound(KJob* job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+        mCollectionSearchJob = 0;
+        return;
+    }
+    Q_ASSERT(mCollectionSearchJob == static_cast<CollectionSearchJob*>(job));
+    Q_FOREACH(const Akonadi::Collection &col, mCollectionSearchJob->matchingCollections()) {
+        CollectionNode *collectionNode = new CollectionNode(*mSearchModel, col);
+        //toggled by the checkbox, results in collection getting monitored
+        connect(&collectionNode->emitter, SIGNAL(enabled(bool, Akonadi::Collection)), this, SLOT(onCollectionEnabled(bool, Akonadi::Collection)));
+        mSearchModel->addNode(ReparentingModel::Node::Ptr(collectionNode));
+    }
+    mCollectionSearchJob = 0;
+}
+
+void Controller::onPersonsFound(KJob* job)
+{
+    if (job->error()) {
+        kWarning() << job->errorString();
+        mPersonSearchJob = 0;
+        return;
+    }
+    Q_ASSERT(mPersonSearchJob == static_cast<PersonSearchJob*>(job));
+    Q_FOREACH(const Person &p, mPersonSearchJob->matches()) {
+        PersonNode *personNode = new PersonNode(*mSearchModel, p);
+        //toggled by the checkbox, results in person getting added to main model
+        connect(&personNode->emitter, SIGNAL(enabled(bool, Person)), this, SLOT(onPersonEnabled(bool, Person)));
+        mSearchModel->addNode(ReparentingModel::Node::Ptr(personNode));
+    }
+    mPersonSearchJob = 0;
+}
+
+static Akonadi::EntityTreeModel *findEtm(QAbstractItemModel *model)
+{
+    QAbstractProxyModel *proxyModel;
+    while (model) {
+        proxyModel = qobject_cast<QAbstractProxyModel*>(model);
+        if (proxyModel && proxyModel->sourceModel()) {
+            model = proxyModel->sourceModel();
+        } else {
+            break;
+        }
+    }
+    return qobject_cast<Akonadi::EntityTreeModel*>(model);
+}
+
+void Controller::setCollectionReferenced(bool enabled, const Akonadi::Collection& collection)
+{
+    kDebug() << collection.displayName() << "do reference " << enabled;
+    kDebug() << "current " << collection.referenced();
+    Akonadi::EntityTreeModel *etm = findEtm(mPersonModel);
+    Q_ASSERT(etm);
+    etm->setCollectionReferenced(collection, enabled);
+}
+
+void Controller::setCollectionEnabled(bool enabled, const Akonadi::Collection& collection)
+{
+    kDebug() << collection.displayName() << "do enable " << enabled;
+    kDebug() << "current " << collection.enabled();
+
+    Akonadi::Collection modifiedCollection = collection;
+    modifiedCollection.setShouldList(Akonadi::Collection::ListDisplay, enabled);
+    new Akonadi::CollectionModifyJob(modifiedCollection);
+}
+
+void Controller::onCollectionEnabled(bool enabled, const Akonadi::Collection& collection)
+{
+    setCollectionReferenced(enabled, collection);
+}
+
+void Controller::setCollection(const Akonadi::Collection &collection, bool enabled, bool referenced)
+{
+    Akonadi::EntityTreeModel *etm = findEtm(mPersonModel);
+    if (!etm) {
+        kWarning() << "Couldn't find etm";
+        return;
+    }
+    kDebug() << collection.displayName() << "do enable " << enabled;
+    Akonadi::Collection modifiedCollection = collection;
+    modifiedCollection.setShouldList(Akonadi::Collection::ListDisplay, enabled);
+    //HACK: We have no way of getting to the correct session as used by the etm,
+    //and two concurrent jobs end up overwriting the enabled state of each other.
+    etm->setCollectionReferenced(modifiedCollection, referenced);
+}
+
+void Controller::setPersonDisabled(const Person &person)
+{
+    mPersonModel->removeNode(PersonNode(*mPersonModel, person));
+}
+
diff --git a/korganizer/views/collectionview/controller.h b/korganizer/views/collectionview/controller.h
new file mode 100644
index 0000000..a63747d
--- /dev/null
+++ b/korganizer/views/collectionview/controller.h
@@ -0,0 +1,198 @@
+/*
+  Copyright (C) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+  This program 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 2 of the License, or
+  (at your option) any later version.
+
+  This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#ifndef KORG_CONTROLLER_H
+#define KORG_CONTROLLER_H
+
+#include <QObject>
+#include <QStringList>
+#include <Akonadi/EntityTreeModel>
+#include <Akonadi/Collection>
+#include "reparentingmodel.h"
+
+struct Person
+{
+    Person(): rootCollection(-1){};
+    QString name;
+    Akonadi::Collection::Id rootCollection;
+    
+    //FIXME not sure we actually require those two
+    QStringList folderPaths;
+    QList<Akonadi::Collection::Id> collections;
+};
+
+Q_DECLARE_METATYPE(Person);
+
+/**
+ * We need to emit signals in the subclass but don't want to make the parent a QObject
+ */
+class Emitter : public QObject {
+  Q_OBJECT
+public:
+    void emitEnabled(bool state, const Person &person)
+    {
+        emit enabled(state, person);
+    }
+
+    void emitEnabled(bool state, const Akonadi::Collection &collection)
+    {
+        emit enabled(state, collection);
+    }
+
+  Q_SIGNALS:
+    void enabled(bool, Person);
+    void enabled(bool, Akonadi::Collection);
+};
+
+/**
+ * A node representing a person
+ */
+class PersonNode : public ReparentingModel::Node
+{
+public:
+    enum DataRole {
+        PersonRole = Akonadi::EntityTreeModel::UserRole + 1
+    };
+
+    PersonNode(ReparentingModel &personModel, const Person &person);
+    virtual ~PersonNode();
+    virtual bool operator==(const Node &) const;
+
+    void setChecked(bool);
+
+    virtual QVariant data(int role) const;
+
+    Emitter emitter;
+private:
+    virtual bool setData(const QVariant& variant, int role);
+    virtual bool adopts(const QModelIndex& sourceIndex);
+    virtual bool isDuplicateOf(const QModelIndex& sourceIndex);
+
+    Person mPerson;
+    Qt::CheckState mCheckState;
+};
+
+class CollectionNode : public ReparentingModel::Node
+{
+public:
+    CollectionNode(ReparentingModel &personModel, const Akonadi::Collection &col);
+    virtual ~CollectionNode();
+    virtual bool operator==(const Node &) const;
+
+    Emitter emitter;
+
+private:
+    virtual QVariant data(int role) const;
+    virtual bool setData(const QVariant& variant, int role);
+    virtual bool isDuplicateOf(const QModelIndex& sourceIndex);
+    Akonadi::Collection mCollection;
+    Qt::CheckState mCheckState;
+};
+
+class PersonNodeManager : public ReparentingModel::NodeManager
+{
+public:
+    PersonNodeManager(ReparentingModel &personModel) : ReparentingModel::NodeManager(personModel){};
+private:
+    void checkSourceIndex(const QModelIndex &sourceIndex);
+};
+
+class CollectionSearchJob : public KJob
+{
+    Q_OBJECT
+public:
+    explicit CollectionSearchJob(const QString &searchString, QObject* parent = 0);
+
+    virtual void start();
+
+    Akonadi::Collection::List matchingCollections() const;
+
+private Q_SLOTS:
+    void onCollectionsReceived(const Akonadi::Collection::List &);
+    void onCollectionsFetched(KJob *);
+    void onAncestorsFetched(KJob *);
+
+private:
+    QString mSearchString;
+    Akonadi::Collection::List mMatchingCollections;
+    Akonadi::Collection::List mAncestors;
+};
+
+class PersonSearchJob : public KJob
+{
+    Q_OBJECT
+public:
+    explicit PersonSearchJob(const QString &searchString, QObject* parent = 0);
+
+    virtual void start();
+
+    QList<Person> matches() const;
+
+private Q_SLOTS:
+    void onCollectionsReceived(const Akonadi::Collection::List &);
+    void onCollectionsFetched(KJob *);
+
+private:
+    QString mSearchString;
+    QList<Person> mMatches;
+};
+
+/**
+ * Add search results to the search model, and use the selection to add results to the person model.
+ */
+class Controller : public QObject
+{
+    Q_OBJECT
+public:
+    explicit Controller(ReparentingModel *personModel, ReparentingModel *searchModel, QObject* parent = 0);
+    /**
+     *  This model will be used to select the collections that are available in the ETM
+     */
+    void setEntityTreeModel(Akonadi::EntityTreeModel *etm);
+
+    void setCollectionReferenced(bool enabled, const Akonadi::Collection &collection);
+    void setCollectionEnabled(bool enabled, const Akonadi::Collection &collection);
+    void setCollection(const Akonadi::Collection &collection, bool enabled, bool referenced);
+    void setPersonDisabled(const Person &person);
+
+Q_SIGNALS:
+    void searchIsActive(bool);
+
+public Q_SLOTS:
+    void setSearchString(const QString &);
+
+private Q_SLOTS:
+    void onCollectionsFound(KJob *job);
+    void onPersonsFound(KJob *job);
+    void onPersonEnabled(bool enabled, const Person &person);
+    // void onPersonCollectionsReceived(Akonadi::Collection::List);
+    void onPersonCollectionsFetched(KJob *job);
+    void onCollectionEnabled(bool enabled, const Akonadi::Collection &collection);
+
+private:
+    ReparentingModel *mPersonModel;
+    ReparentingModel *mSearchModel;
+    CollectionSearchJob *mCollectionSearchJob;
+    PersonSearchJob *mPersonSearchJob;
+};
+
+#endif
diff --git a/korganizer/views/collectionview/reparentingmodel.cpp b/korganizer/views/collectionview/reparentingmodel.cpp
new file mode 100644
index 0000000..f21485b
--- /dev/null
+++ b/korganizer/views/collectionview/reparentingmodel.cpp
@@ -0,0 +1,757 @@
+/*
+  Copyright (C) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+  This program 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 2 of the License, or
+  (at your option) any later version.
+
+  This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+#include "reparentingmodel.h"
+
+#include <KDebug>
+
+/*
+ * Notes:
+ * * layoutChanged must never add or remove nodes.
+ * * rebuildAll can therefore only be called if it doesn't introduce new nodes or within a reset.
+ * * The node memory management is done using the node tree, nodes are deleted by being removed from the node tree.
+ */
+
+ReparentingModel::Node::Node(ReparentingModel& model)
+:   parent(0),
+    personModel(model),
+    mIsSourceNode(false)
+{
+
+}
+
+ReparentingModel::Node::Node(ReparentingModel& model, ReparentingModel::Node* p, const QModelIndex& srcIndex)
+:   parent(p),
+    sourceIndex(srcIndex),
+    personModel(model),
+    mIsSourceNode(true)
+{
+    if(sourceIndex.isValid()) {
+        personModel.mSourceNodes.append(this);
+    }
+    Q_ASSERT(parent);
+}
+
+ReparentingModel::Node::~Node()
+{
+    //The source index may be invalid meanwhile (it's a persistent index)
+    personModel.mSourceNodes.removeOne(this);
+}
+
+bool ReparentingModel::Node::operator==(const ReparentingModel::Node &node) const
+{
+    return (this == &node);
+}
+
+void ReparentingModel::Node::reparent(ReparentingModel::Node *node)
+{
+    Node::Ptr nodePtr;
+    if (node->parent) {
+        //Reparent node
+        Ptr *it = node->parent->children.begin();
+        for (;it != node->parent->children.end(); it++) {
+            if (it->data() == node) {
+                //Reuse smart pointer
+                nodePtr = *it;
+                node->parent->children.erase(it);
+                break;
+            }
+        }
+        Q_ASSERT(nodePtr);
+    } else {
+        nodePtr = Node::Ptr(node);
+    }
+    addChild(nodePtr);
+}
+
+void ReparentingModel::Node::addChild(const ReparentingModel::Node::Ptr &node)
+{
+    node->parent = this;
+    children.append(node);
+}
+
+void ReparentingModel::Node::clearHierarchy()
+{
+    parent = 0;
+    children.clear();
+}
+
+bool ReparentingModel::Node::setData(const QVariant& value, int role)
+{
+    return false;
+}
+
+QVariant ReparentingModel::Node::data(int role) const
+{
+    if(sourceIndex.isValid()) {
+        return sourceIndex.data(role);
+    }
+    return QVariant();
+}
+
+bool ReparentingModel::Node::adopts(const QModelIndex& sourceIndex)
+{
+    return false;
+}
+
+bool ReparentingModel::Node::isDuplicateOf(const QModelIndex& sourceIndex)
+{
+    return false;
+}
+
+bool ReparentingModel::Node::isSourceNode() const
+{
+    return mIsSourceNode;
+}
+
+int ReparentingModel::Node::row() const
+{
+    Q_ASSERT(parent);
+    int row = 0;
+    Q_FOREACH(const Node::Ptr &node, parent->children) {
+        if (node.data() == this) {
+            return row;
+        }
+        row++;
+    }
+    return -1;
+}
+
+
+
+ReparentingModel::ReparentingModel(QObject* parent)
+:   QAbstractProxyModel(parent),
+    mRootNode(*this),
+    mNodeManager(NodeManager::Ptr(new NodeManager(*this)))
+{
+
+}
+
+ReparentingModel::~ReparentingModel()
+{
+    //Otherwise we cannot guarantee that the nodes reference to *this is always valid
+    mRootNode.children.clear();
+    mProxyNodes.clear();
+    mSourceNodes.clear();
+}
+
+bool ReparentingModel::validateNode(const Node *node) const
+{
+    //Expected:
+    // * Each node tree starts at mRootNode
+    // * Each node is listed in the children of it's parent
+    // * Root node never leaves the model and thus should never enter this function
+    if (!node) {
+        kWarning() << "nullptr";
+        return false;
+    }
+    if (node == &mRootNode) {
+        kWarning() << "is root node";
+        return false;
+    }
+    const Node *n = node;
+    int depth = 0;
+    while (n) {
+        if (!n) {
+            kWarning() << "nullptr" << depth;
+            return false;
+        }
+        if ((long)(n) < 1000) {
+            //Detect corruptions with unlikely pointers
+            kWarning() << "corrupt pointer" << depth;
+            return false;
+        }
+        if (!n->parent) {
+            kWarning() << "nullptr parent" << depth << n->isSourceNode();
+            return false;
+        }
+        if (n->parent == n) {
+            kWarning() << "loop" << depth;
+            return false;
+        }
+
+        bool found = false;
+        Q_FOREACH(const Node::Ptr &child, n->parent->children) {
+            if (child.data() == n) {
+                found = true;
+            }
+        }
+        if (!found) {
+            kWarning() << "not linked as child" << depth;
+            return false;
+        }
+        depth++;
+        if (depth > 1000) {
+            kWarning() << "loop detected" << depth;
+            return false;
+        }
+
+        if (n->parent == &mRootNode) {
+            return true;
+        }
+        //If the parent isn't root there is at least one more level
+        if (!n->parent->parent) {
+            kWarning() << "missing parent parent" << depth;
+            return false;
+        }
+        if (n->parent->parent == n) {
+            kWarning() << "parent parent loop" << depth;
+            return false;
+        }
+        n = n->parent;
+    }
+    kWarning() << "not linked to root" << depth;
+    return false;
+}
+
+void ReparentingModel::addNode(const ReparentingModel::Node::Ptr& node)
+{
+    qRegisterMetaType<Node::Ptr>("Node::Ptr");
+    QMetaObject::invokeMethod(this, "doAddNode", Qt::QueuedConnection, QGenericReturnArgument(), Q_ARG(Node::Ptr, node));
+}
+
+void ReparentingModel::doAddNode(const Node::Ptr &node)
+{
+    Q_FOREACH(const ReparentingModel::Node::Ptr &existing, mProxyNodes) {
+        if (*existing == *node) {
+            // kDebug() << "node is already existing";
+            return;
+        }
+    }
+
+    beginResetModel();
+    mProxyNodes << node;
+    rebuildAll();
+    endResetModel();
+}
+
+void ReparentingModel::removeNode(const ReparentingModel::Node& node)
+{
+    beginResetModel();
+    for (int i = 0; i < mProxyNodes.size(); i++) {
+        if (*mProxyNodes.at(i) == node) {
+            mProxyNodes.remove(i);
+            break;
+        }
+    }
+    rebuildAll();
+    endResetModel();
+}
+
+
+void ReparentingModel::setNodes(const QList<Node::Ptr> &nodes)
+{
+    Q_FOREACH(const ReparentingModel::Node::Ptr &node, nodes) {
+        addNode(node);
+    }
+    Q_FOREACH(const ReparentingModel::Node::Ptr &node, mProxyNodes) {
+        if (!nodes.contains(node)) {
+            removeNode(*node);
+        }
+    }
+}
+
+void ReparentingModel::clear()
+{
+    beginResetModel();
+    mProxyNodes.clear();
+    rebuildAll();
+    endResetModel();
+}
+
+void ReparentingModel::setNodeManager(const NodeManager::Ptr &nodeManager)
+{
+    mNodeManager = nodeManager;
+}
+
+void ReparentingModel::setSourceModel(QAbstractItemModel* sourceModel)
+{
+    beginResetModel();
+    QAbstractProxyModel::setSourceModel(sourceModel);
+    if (sourceModel) {
+        connect(sourceModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
+                SLOT(onSourceRowsAboutToBeInserted(QModelIndex,int,int)));
+        connect(sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)),
+                SLOT(onSourceRowsInserted(QModelIndex,int,int)));
+        connect(sourceModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
+                SLOT(onSourceRowsAboutToBeRemoved(QModelIndex,int,int)));
+        connect(sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)),
+                SLOT(onSourceRowsRemoved(QModelIndex,int,int)));
+        connect(sourceModel, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)),
+                SLOT(onSourceRowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)));
+        connect(sourceModel, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
+                SLOT(onSourceRowsMoved(QModelIndex,int,int,QModelIndex,int)));
+        connect(sourceModel, SIGNAL(modelAboutToBeReset()),
+                SLOT(onSourceModelAboutToBeReset()));
+        connect(sourceModel, SIGNAL(modelReset()),
+                SLOT(onSourceModelReset()));
+        connect(sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
+                SLOT(onSourceDataChanged(QModelIndex,QModelIndex)));
+//       connect(sourceModel, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
+//               SLOT(_k_sourceHeaderDataChanged(Qt::Orientation,int,int)));
+        connect(sourceModel, SIGNAL(layoutAboutToBeChanged()),
+                SLOT(onSourceLayoutAboutToBeChanged()));
+        connect(sourceModel, SIGNAL(layoutChanged()),
+                SLOT(onSourceLayoutChanged()));
+//       connect(sourceModel, SIGNAL(destroyed()),
+//               SLOT(onSourceModelDestroyed()));
+    }
+
+    rebuildAll();
+    endResetModel();
+}
+
+void ReparentingModel::onSourceRowsAboutToBeInserted(QModelIndex parent, int start, int end)
+{
+    Q_UNUSED(parent);
+    Q_UNUSED(start);
+    Q_UNUSED(end);
+}
+
+ReparentingModel::Node *ReparentingModel::getReparentNode(const QModelIndex &sourceIndex)
+{
+    Q_FOREACH(const Node::Ptr &proxyNode, mProxyNodes) {
+        //Reparent source nodes according to the provided rules
+        //The proxy can be ignored if it is a duplicate, so only reparent to proxies that are in the model
+        if (proxyNode->parent && proxyNode->adopts(sourceIndex)) {
+            Q_ASSERT(validateNode(proxyNode.data()));
+            return proxyNode.data();
+        }
+    }
+    return 0;
+}
+
+ReparentingModel::Node *ReparentingModel::getParentNode(const QModelIndex &sourceIndex)
+{
+    if (Node *node = getReparentNode(sourceIndex)) {
+        return node;
+    }
+    const QModelIndex proxyIndex = mapFromSource(sourceIndex.parent());
+    if (proxyIndex.isValid()) {
+        return extractNode(proxyIndex);
+    }
+    return 0;
+}
+
+void ReparentingModel::appendSourceNode(Node *parentNode, const QModelIndex &sourceIndex, const QModelIndexList &skip)
+{
+    mNodeManager->checkSourceIndex(sourceIndex);
+
+    Node::Ptr node(new Node(*this, parentNode, sourceIndex));
+    parentNode->children.append(node);
+    Q_ASSERT(validateNode(node.data()));
+    rebuildFromSource(node.data(), sourceIndex, skip);
+}
+
+QModelIndexList ReparentingModel::descendants(const QModelIndex &sourceIndex)
+{
+    if (!sourceModel()) {
+        return QModelIndexList();
+    }
+    QModelIndexList list;
+    if (sourceModel()->hasChildren(sourceIndex)) {
+        for (int i = 0; i < sourceModel()->rowCount(sourceIndex); i++) {
+            const QModelIndex index = sourceModel()->index(i, 0, sourceIndex);
+            list << index;
+            list << descendants(index);
+        }
+    }
+    return list;
+}
+
+void ReparentingModel::removeDuplicates(const QModelIndex &sourceIndex)
+{
+    QModelIndexList list;
+    list << sourceIndex << descendants(sourceIndex);
+    Q_FOREACH(const QModelIndex &descendant, list) {
+        Q_FOREACH(const Node::Ptr &proxyNode, mProxyNodes) {
+            if (proxyNode->isDuplicateOf(descendant)) {
+                //Removenode from proxy
+                if (!proxyNode->parent) {
+                    kWarning() << "Found proxy that is already not part of the model " << proxyNode->data(Qt::DisplayRole).toString();
+                    continue;
+                }
+                const int targetRow = proxyNode->row();
+                beginRemoveRows(index(proxyNode->parent), targetRow, targetRow);
+                proxyNode->parent->children.remove(targetRow);
+                proxyNode->parent = 0;
+                endRemoveRows();
+            }
+        }
+    }
+}
+
+void ReparentingModel::onSourceRowsInserted(QModelIndex parent, int start, int end)
+{
+    kDebug() << parent << start << end;
+    for (int row = start; row <= end; row++) {
+        QModelIndex sourceIndex = sourceModel()->index(row, 0, parent);
+        Q_ASSERT(sourceIndex.isValid());
+        Node *parentNode = getParentNode(sourceIndex);
+        if (!parentNode) {
+            parentNode = &mRootNode;
+        } else {
+            Q_ASSERT(validateNode(parentNode));
+        }
+        Q_ASSERT(parentNode);
+        
+        //Remove any duplicates that we are going to replace
+        removeDuplicates(sourceIndex);
+
+        QModelIndexList reparented;
+        //Check for children to reparent
+        {
+            Q_FOREACH(const QModelIndex &descendant, descendants(sourceIndex)) {
+                if (Node *proxyNode = getReparentNode(descendant)) {
+                    kDebug() << "reparenting " << descendant.data().toString();
+                    int targetRow = proxyNode->children.size();
+                    beginInsertRows(index(proxyNode), targetRow, targetRow);
+                    appendSourceNode(proxyNode, descendant);
+                    reparented << descendant;
+                    endInsertRows();
+                }
+            }
+        }
+
+        if (parentNode->isSourceNode()) {
+            int targetRow = parentNode->children.size();
+            beginInsertRows(mapFromSource(parent), targetRow, targetRow);
+            appendSourceNode(parentNode, sourceIndex, reparented);
+            endInsertRows();
+        } else { //Reparented
+            int targetRow = parentNode->children.size();
+            beginInsertRows(index(parentNode), targetRow, targetRow);
+            appendSourceNode(parentNode, sourceIndex);
+            endInsertRows();
+        }
+    }
+}
+
+void ReparentingModel::onSourceRowsAboutToBeRemoved(QModelIndex parent, int start, int end)
+{
+    // kDebug() << parent << start << end;
+    //we remove in reverse order as otherwise the indexes in parentNode->children wouldn't be correct
+    for (int row = end; row >= start; row--) {
+        QModelIndex sourceIndex = sourceModel()->index(row, 0, parent);
+        Q_ASSERT(sourceIndex.isValid());
+
+        const QModelIndex proxyIndex = mapFromSource(sourceIndex);
+        const Node *node = extractNode(proxyIndex);
+        Node *parentNode = node->parent;
+        Q_ASSERT(parentNode);
+        const int targetRow = node->row();
+        beginRemoveRows(index(parentNode), targetRow, targetRow);
+        parentNode->children.remove(targetRow); //deletes node
+        endRemoveRows();
+    }
+}
+
+void ReparentingModel::onSourceRowsRemoved(QModelIndex parent, int start, int end)
+{
+}
+
+void ReparentingModel::onSourceRowsAboutToBeMoved(QModelIndex sourceParent, int sourceStart, int sourceEnd, QModelIndex destParent, int dest)
+{
+    kWarning() << "not implemented";
+    //TODO
+    beginResetModel();
+}
+
+void ReparentingModel::onSourceRowsMoved(QModelIndex sourceParent, int sourceStart, int sourceEnd, QModelIndex destParent, int dest)
+{
+    kWarning() << "not implemented";
+    //TODO
+    endResetModel();
+}
+
+void ReparentingModel::onSourceLayoutAboutToBeChanged()
+{
+    // layoutAboutToBeChanged();
+    // Q_FOREACH(const QModelIndex &proxyPersistentIndex, persistentIndexList()) {
+    //     Q_ASSERT(proxyPersistentIndex.isValid());
+    //     const QPersistentModelIndex srcPersistentIndex = mapToSource(proxyPersistentIndex);
+    //     // TODO also update the proxy persistent indexes
+    //     //Skip indexes that are not in the source model
+    //     if (!srcPersistentIndex.isValid()) {
+    //         continue;
+    //     }
+    //     mLayoutChangedProxyIndexes << proxyPersistentIndex;
+    //     mLayoutChangedSourcePersistentModelIndexes << srcPersistentIndex;
+    // }
+}
+
+void ReparentingModel::onSourceLayoutChanged()
+{
+    //By ignoring this we miss structural changes in the sourcemodel, which is mostly ok.
+    //Before we can re-enable this we need to properly deal with skipped duplicates, because
+    //a layout change MUST NOT add/remove new nodes (only shuffling allowed)
+    //
+    //Our source indexes are not endagered since we use persistend model indexes anyways
+ 
+    // rebuildAll();
+
+    // for (int i = 0; i < mLayoutChangedProxyIndexes.size(); ++i) {
+    //     const QModelIndex oldProxyIndex = mLayoutChangedProxyIndexes.at(i);
+    //     const QModelIndex newProxyIndex = mapFromSource(mLayoutChangedSourcePersistentModelIndexes.at(i));
+    //     if (oldProxyIndex != newProxyIndex) {
+    //         changePersistentIndex(oldProxyIndex, newProxyIndex);
+    //     }
+    // }
+
+    // mLayoutChangedProxyIndexes.clear();
+    // mLayoutChangedSourcePersistentModelIndexes.clear();
+
+    // layoutChanged();
+}
+
+void ReparentingModel::onSourceDataChanged(QModelIndex begin, QModelIndex end)
+{
+    for (int row = begin.row(); row <= end.row(); row++) {
+        mNodeManager->checkSourceIndex(sourceModel()->index(row, begin.column(), begin.parent()));
+    }
+    emit dataChanged(mapFromSource(begin), mapFromSource(end));
+}
+
+void ReparentingModel::onSourceModelAboutToBeReset()
+{
+    beginResetModel();
+}
+
+void ReparentingModel::onSourceModelReset()
+{
+    rebuildAll();
+    endResetModel();
+}
+
+ReparentingModel::Node *ReparentingModel::extractNode(const QModelIndex &index) const
+{
+    Node *node = static_cast<Node*>(index.internalPointer());
+    Q_ASSERT(node);
+    Q_ASSERT(validateNode(node));
+    return node;
+}
+
+QModelIndex ReparentingModel::index(int row, int column, const QModelIndex& parent) const
+{
+    // kDebug() << parent << row;
+    const Node *parentNode;
+    if (parent.isValid()) {
+        parentNode = extractNode(parent);
+    } else {
+        parentNode = &mRootNode;
+    }
+    //At least QAbstractItemView expects that we deal with this properly (see rowsAboutToBeRemoved "find the next visible and enabled item")
+    if (parentNode->children.size() <= row) {
+        return QModelIndex();
+    }
+    Q_ASSERT(parentNode->children.size() > row);
+    Node *node = parentNode->children.at(row).data();
+    Q_ASSERT(validateNode(node));
+    return createIndex(row, column, node);
+}
+
+QModelIndex ReparentingModel::mapToSource(const QModelIndex &idx) const
+{
+    if (!idx.isValid() || !sourceModel()) {
+        return QModelIndex();
+    }
+    Node *node = extractNode(idx);
+    if (!node->isSourceNode()) {
+        return QModelIndex();
+    }
+    Q_ASSERT(node->sourceIndex.model() == sourceModel());
+    return node->sourceIndex;
+}
+
+ReparentingModel::Node *ReparentingModel::getSourceNode(const QModelIndex &sourceIndex) const
+{
+    Q_FOREACH (Node *n, mSourceNodes) {
+        if (n->sourceIndex == sourceIndex) {
+            return n;
+        }
+    }
+    return 0;
+}
+
+QModelIndex ReparentingModel::mapFromSource(const QModelIndex& sourceIndex) const
+{
+    kDebug() << sourceIndex << sourceIndex.data().toString();
+    if (!sourceIndex.isValid()) {
+        return QModelIndex();
+    }
+    Node *node = getSourceNode(sourceIndex);
+    if (!node) {
+        //This can happen if a source nodes is hidden (person collections)
+        return QModelIndex();
+    }
+    Q_ASSERT(validateNode(node));
+    return index(node);
+}
+
+void ReparentingModel::rebuildFromSource(Node *parentNode, const QModelIndex &sourceParent, const QModelIndexList &skip)
+{
+    Q_ASSERT(parentNode);
+    if (!sourceModel()) {
+        return;
+    }
+    for (int i = 0; i < sourceModel()->rowCount(sourceParent); i++) {
+        const QModelIndex &sourceIndex = sourceModel()->index(i, 0, sourceParent);
+        //Skip indexes that should be excluded because they have been reparented
+        if (skip.contains(sourceIndex)) {
+            continue;
+        }
+        appendSourceNode(parentNode, sourceIndex, skip);
+    }
+}
+
+void ReparentingModel::rebuildAll()
+{
+    mRootNode.children.clear();
+    Q_FOREACH(const Node::Ptr &proxyNode, mProxyNodes) {
+        proxyNode->clearHierarchy();
+    }
+    Q_ASSERT(mSourceNodes.isEmpty());
+    mSourceNodes.clear();
+    rebuildFromSource(&mRootNode, QModelIndex());
+    Q_FOREACH(const Node::Ptr &proxyNode, mProxyNodes) {
+        // kDebug() << "checking " << proxyNode->data(Qt::DisplayRole).toString();
+        //Avoid inserting a node that is already part of the source model
+        bool isDuplicate = false;
+        Q_FOREACH(const Node *n, mSourceNodes) {
+            // kDebug() << index << index.data().toString();
+            if (proxyNode->isDuplicateOf(n->sourceIndex)) {
+                isDuplicate = true;
+                break;
+            }
+        }
+        if (isDuplicate) {
+            continue;
+        }
+
+        proxyNode->parent = &mRootNode;
+        mRootNode.addChild(proxyNode);
+        Q_ASSERT(validateNode(proxyNode.data()));
+
+        //Reparent source nodes according to the provided rules
+        Q_FOREACH(Node *n, mSourceNodes) {
+            if (proxyNode->adopts(n->sourceIndex)) {
+                Node *reparentNode = n;
+                proxyNode->reparent(reparentNode);
+                Q_ASSERT(validateNode(reparentNode));
+            }
+        }
+    }
+}
+
+QVariant ReparentingModel::data(const QModelIndex& proxyIndex, int role) const
+{
+    Q_ASSERT(proxyIndex.isValid());
+    const Node *node = extractNode(proxyIndex);
+    if (node->isSourceNode()) {
+        return sourceModel()->data(mapToSource(proxyIndex), role);
+    }
+    return node->data(role);
+}
+
+bool ReparentingModel::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+    Q_ASSERT(index.isValid());
+    if (!sourceModel()) {
+        return false;
+    }
+    Node *node = extractNode(index);
+    if (node->isSourceNode()) {
+        return sourceModel()->setData(mapToSource(index), value, role);
+    }
+    return node->setData(value, role);
+}
+
+Qt::ItemFlags ReparentingModel::flags(const QModelIndex& index) const
+{
+    if (!index.isValid() || !sourceModel()) {
+        return Qt::NoItemFlags;
+    }
+    Node *node = extractNode(index);
+    if (!node->isSourceNode()) {
+        return Qt::ItemIsEnabled | Qt::ItemIsUserCheckable;
+    }
+    return sourceModel()->flags(mapToSource(index));
+}
+
+QModelIndex ReparentingModel::index(Node *node) const
+{
+    Q_ASSERT(node);
+    if (node == &mRootNode) {
+        return QModelIndex();
+    }
+    Q_ASSERT(validateNode(node));
+    int row = 0;
+    Q_FOREACH(const Node::Ptr &c, node->parent->children) {
+        if (c.data() == node) {
+            break;
+        }
+        row++;
+    }
+    return createIndex(row, 0, node);
+}
+
+QModelIndex ReparentingModel::parent(const QModelIndex& child) const
+{
+    // kDebug() << child << child.data().toString();
+    Q_ASSERT(child.isValid());
+    if (!child.isValid()) {
+        return QModelIndex();
+    }
+    const Node *node = extractNode(child);
+    return index(node->parent);
+}
+
+QModelIndex ReparentingModel::buddy(const QModelIndex& index) const
+{
+    if (!index.isValid() || !sourceModel()) {
+        return QModelIndex();
+    }
+    Node *node = extractNode(index);
+    if (node->isSourceNode()) {
+        return mapFromSource(sourceModel()->buddy(mapToSource(index)));
+    }
+    return index;
+}
+
+int ReparentingModel::rowCount(const QModelIndex& parent) const
+{
+    if (!parent.isValid()) {
+        return mRootNode.children.size();
+    }
+    Node *node = extractNode(parent);
+    return node->children.size();
+}
+
+bool ReparentingModel::hasChildren(const QModelIndex& parent) const
+{
+    return (rowCount(parent) != 0);
+}
+
+int ReparentingModel::columnCount(const QModelIndex& parent) const
+{
+    return 1;
+}
+
diff --git a/korganizer/views/collectionview/reparentingmodel.h b/korganizer/views/collectionview/reparentingmodel.h
new file mode 100644
index 0000000..43556e8
--- /dev/null
+++ b/korganizer/views/collectionview/reparentingmodel.h
@@ -0,0 +1,143 @@
+/*
+  Copyright (C) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+  This program 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 2 of the License, or
+  (at your option) any later version.
+
+  This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+
+#ifndef KORG_REPARENTINGMODEL_H
+#define KORG_REPARENTINGMODEL_H
+
+#include <QAbstractProxyModel>
+#include <QSharedPointer>
+#include <QVector>
+
+/**
+ * A model that can hold an extra set of nodes which can "adopt" (reparent),
+ * source nodes.
+ */
+class ReparentingModel : public QAbstractProxyModel
+{
+    Q_OBJECT
+public:
+    struct Node {
+        typedef QSharedPointer<Node> Ptr;
+        virtual ~Node();
+        virtual bool operator==(const Node &) const;
+
+    protected:
+        Node(ReparentingModel &personModel);
+
+    private:
+        friend class ReparentingModel;
+        Node(ReparentingModel &personModel, Node *parent, const QModelIndex &sourceIndex);
+        virtual QVariant data(int role) const;
+        virtual bool setData(const QVariant &variant, int role);
+        virtual bool adopts(const QModelIndex &sourceIndex);
+        virtual bool isDuplicateOf(const QModelIndex &sourceIndex);
+
+        bool isSourceNode() const;
+        void reparent(Node *node);
+        void addChild(const Node::Ptr &node);
+        int row() const;
+        void clearHierarchy();
+
+        QPersistentModelIndex sourceIndex;
+        QVector<Ptr> children;
+        Node *parent;
+        ReparentingModel &personModel;
+        bool mIsSourceNode;
+    };
+
+    struct NodeManager {
+        typedef QSharedPointer<NodeManager> Ptr;
+
+        NodeManager(ReparentingModel &m) :model(m){};
+        virtual ~NodeManager(){};
+
+    protected:
+        ReparentingModel &model;
+
+    private:
+        friend class ReparentingModel;
+
+        //Allows the implementation to create proxy nodes as necessary
+        virtual void checkSourceIndex(const QModelIndex &sourceIndex){};
+    };
+
+public:
+    explicit ReparentingModel(QObject* parent = 0);
+    virtual ~ReparentingModel();
+
+    void setNodeManager(const NodeManager::Ptr &nodeManager);
+    void addNode(const Node::Ptr &node);
+    void removeNode(const Node &node);
+    void setNodes(const QList<Node::Ptr> &nodes);
+    void clear();
+
+    virtual int rowCount(const QModelIndex& parent = QModelIndex()) const;
+    virtual int columnCount(const QModelIndex& parent = QModelIndex()) const;
+    virtual QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const;
+    virtual QModelIndex parent(const QModelIndex& child) const;
+    virtual QVariant data(const QModelIndex& proxyIndex, int role = Qt::DisplayRole) const;
+    virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole);
+    virtual Qt::ItemFlags flags(const QModelIndex& index) const;
+    virtual bool hasChildren(const QModelIndex& parent = QModelIndex()) const;
+    virtual QModelIndex buddy(const QModelIndex& index) const;
+
+    virtual void setSourceModel(QAbstractItemModel* sourceModel);
+    virtual QModelIndex mapFromSource(const QModelIndex& sourceIndex) const;
+    virtual QModelIndex mapToSource(const QModelIndex& proxyIndex) const;
+
+private Q_SLOTS:
+    void onSourceRowsAboutToBeInserted(QModelIndex,int,int);
+    void onSourceRowsInserted(QModelIndex,int,int);
+    void onSourceRowsAboutToBeRemoved(QModelIndex,int,int);
+    void onSourceRowsRemoved(QModelIndex,int,int);
+    void onSourceRowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int);
+    void onSourceRowsMoved(QModelIndex,int,int,QModelIndex,int);
+    void onSourceDataChanged(QModelIndex,QModelIndex);
+    void onSourceLayoutAboutToBeChanged();
+    void onSourceLayoutChanged();
+    void onSourceModelAboutToBeReset();
+    void onSourceModelReset();
+    void doAddNode(const Node::Ptr &node);
+
+private:
+    void rebuildFromSource(Node *parentNode, const QModelIndex &idx, const QModelIndexList &skip = QModelIndexList());
+    void rebuildAll();
+    QModelIndex index(Node *node) const;
+    Node *getReparentNode(const QModelIndex &sourceIndex);
+    Node *getParentNode(const QModelIndex &sourceIndex);
+    bool validateNode(const Node *node) const;
+    Node *extractNode(const QModelIndex &index) const;
+    void appendSourceNode(Node *parentNode, const QModelIndex &sourceIndex, const QModelIndexList &skip = QModelIndexList());
+    QModelIndexList descendants(const QModelIndex &sourceIndex);
+    void removeDuplicates(const QModelIndex &sourceIndex);
+    Node *getSourceNode(const QModelIndex &sourceIndex) const;
+
+    Node mRootNode;
+    QList<Node*> mSourceNodes;
+    QVector<Node::Ptr> mProxyNodes;
+    NodeManager::Ptr mNodeManager;
+    // QModelIndexList mLayoutChangedProxyIndexes;
+    // QList<QPersistentModelIndex> mLayoutChangedSourcePersistentModelIndexes;
+};
+
+
+#endif
diff --git a/korganizer/views/collectionview/tests/CMakeLists.txt b/korganizer/views/collectionview/tests/CMakeLists.txt
new file mode 100644
index 0000000..1053df8
--- /dev/null
+++ b/korganizer/views/collectionview/tests/CMakeLists.txt
@@ -0,0 +1,16 @@
+include_directories(
+  ${CMAKE_CURRENT_SOURCE_DIR}/..
+)
+
+set(reparentingmodeltest_SRCS
+    reparentingmodeltest.cpp
+    ../reparentingmodel.cpp
+)
+
+kde4_add_unit_test(reparentingmodeltest NOGUI ${reparentingmodeltest_SRCS})
+target_link_libraries(reparentingmodeltest
+    ${QT_QTTEST_LIBRARY}
+    ${QT_QTCORE_LIBRARY}
+    ${QT_QTGUI_LIBRARY}
+    ${KDE4_KDECORE_LIBS}
+)
diff --git a/korganizer/views/collectionview/tests/reparentingmodeltest.cpp b/korganizer/views/collectionview/tests/reparentingmodeltest.cpp
new file mode 100644
index 0000000..c28abd3
--- /dev/null
+++ b/korganizer/views/collectionview/tests/reparentingmodeltest.cpp
@@ -0,0 +1,567 @@
+/*
+  Copyright (C) 2014 Christian Mollekopf <mollekopf at kolabsys.com>
+
+  This program 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 2 of the License, or
+  (at your option) any later version.
+
+  This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+  As a special exception, permission is given to link this program
+  with any edition of Qt, and distribute the resulting executable,
+  without including the source code for Qt in the source distribution.
+*/
+#include <QObject>
+#include <QTest>
+#include <QSignalSpy>
+#include <QStandardItemModel>
+#include <QSortFilterProxyModel>
+#include <KDebug>
+#include "reparentingmodel.h"
+
+class DummyNode : public ReparentingModel::Node
+{
+public:
+    DummyNode(ReparentingModel &personModel, const QString &name)
+    : ReparentingModel::Node(personModel),
+    mName(name)
+    {}
+
+    virtual ~DummyNode(){};
+
+    virtual bool operator==(const Node &node) const {
+        const DummyNode *dummyNode = dynamic_cast<const DummyNode*>(&node);
+        if (dummyNode) {
+            return (dummyNode->mName == mName);
+        }
+        return false;
+    }
+
+private:
+    virtual QVariant data(int role) const {
+        if (role == Qt::DisplayRole) {
+            return mName;
+        }
+        return QVariant();
+    }
+    virtual bool setData(const QVariant& variant, int role){
+        return false;
+    }
+    virtual bool isDuplicateOf(const QModelIndex& sourceIndex) {
+        return (sourceIndex.data().toString() == mName);
+    }
+
+    virtual bool adopts(const QModelIndex& sourceIndex) {
+        return sourceIndex.data().toString().contains(QLatin1String("orphan"));
+    }
+
+    QString mName;
+};
+
+class ModelSignalSpy : public QObject {
+    Q_OBJECT
+public:
+    explicit ModelSignalSpy(QAbstractItemModel &model) {
+        connect(&model, SIGNAL(rowsInserted(QModelIndex, int, int)), this, SLOT(onRowsInserted(QModelIndex,int,int)));
+        connect(&model, SIGNAL(rowsRemoved(QModelIndex, int, int)), this, SLOT(onRowsRemoved(QModelIndex,int,int)));
+        connect(&model, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int)), this, SLOT(onRowsMoved(QModelIndex,int,int, QModelIndex, int)));
+        connect(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(onDataChanged(QModelIndex,QModelIndex)));
+        connect(&model, SIGNAL(layoutChanged()), this, SLOT(onLayoutChanged()));
+        connect(&model, SIGNAL(modelReset()), this, SLOT(onModelReset()));
+    }
+
+    QStringList mSignals;
+    QModelIndex parent;
+    int start;
+    int end;
+
+public Q_SLOTS:
+    void onRowsInserted(QModelIndex p, int s, int e) {
+        mSignals << QLatin1String("rowsInserted");
+        parent = p;
+        start = s;
+        end = e;
+    }
+    void onRowsRemoved(QModelIndex p, int s, int e) {
+        mSignals << QLatin1String("rowsRemoved");
+        parent = p;
+        start = s;
+        end = e;
+    }
+    void onRowsMoved(QModelIndex,int,int,QModelIndex,int) {
+        mSignals << QLatin1String("rowsMoved");
+    }
+    void onDataChanged(QModelIndex,QModelIndex) {
+        mSignals << QLatin1String("dataChanged");
+    }
+    void onLayoutChanged() {
+        mSignals << QLatin1String("layoutChanged");
+    }
+    void onModelReset() {
+        mSignals << QLatin1String("modelReset");
+    }
+};
+
+QModelIndex getIndex(char *string, const QAbstractItemModel &model)
+{
+    QModelIndexList list = model.match(model.index(0, 0), Qt::DisplayRole, QString::fromLatin1(string), 1, Qt::MatchRecursive);
+    if (list.isEmpty()) {
+        return QModelIndex();
+    }
+    return list.first();
+}
+
+QModelIndexList getIndexList(char *string, const QAbstractItemModel &model)
+{
+    return model.match(model.index(0, 0), Qt::DisplayRole, QString::fromLatin1(string), 1, Qt::MatchRecursive);
+}
+
+class ReparentingModelTest : public QObject
+{
+    Q_OBJECT
+private Q_SLOTS:
+    void testPopulation();
+    void testAddRemoveSourceItem();
+    void testInsertSourceRow();
+    void testInsertSourceRowSubnode();
+    void testAddRemoveProxyNode();
+    void testDeduplicate();
+    void testDeduplicateNested();
+    void testDeduplicateProxyNodeFirst();
+    void testNestedDeduplicateProxyNodeFirst();
+    void testReparent();
+    void testReparentResetWithoutCrash();
+    void testAddReparentedSourceItem();
+    void testRemoveReparentedSourceItem();
+    void testNestedReparentedSourceItem();
+    void testAddNestedReparentedSourceItem();
+    void testSourceDataChanged();
+    void testSourceLayoutChanged();
+    void testInvalidLayoutChanged();
+};
+
+void ReparentingModelTest::testPopulation()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row2")));
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(getIndex("row2", reparentingModel).isValid());
+}
+
+void ReparentingModelTest::testAddRemoveSourceItem()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+    ModelSignalSpy spy(reparentingModel);
+
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row2")));
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(getIndex("row2", reparentingModel).isValid());
+    QCOMPARE(spy.parent, QModelIndex());
+    QCOMPARE(spy.start, 1);
+    QCOMPARE(spy.end, 1);
+
+    sourceModel.removeRows(1, 1, QModelIndex());
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(!getIndex("row2", reparentingModel).isValid());
+    QCOMPARE(spy.parent, QModelIndex());
+    QCOMPARE(spy.start, 1);
+    QCOMPARE(spy.end, 1);
+
+    QCOMPARE(spy.mSignals, QStringList() << QLatin1String("rowsInserted") << QLatin1String("rowsRemoved"));
+}
+
+//Ensure the model can deal with rows that are inserted out of order
+void ReparentingModelTest::testInsertSourceRow()
+{
+    QStandardItemModel sourceModel;
+    QStandardItem *row2 = new QStandardItem(QLatin1String("row2"));
+    sourceModel.appendRow(row2);
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+    ModelSignalSpy spy(reparentingModel);
+
+    QStandardItem *row1 = new QStandardItem(QLatin1String("row1"));
+    sourceModel.insertRow(0, row1);
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(getIndex("row2", reparentingModel).isValid());
+
+    //The model does not try to reorder. First come, first serve.
+    QCOMPARE(getIndex("row1", reparentingModel).row(), 1);
+    QCOMPARE(getIndex("row2", reparentingModel).row(), 0);
+    reparentingModel.setData(reparentingModel.index(1, 0, QModelIndex()), QLatin1String("row1foo"), Qt::DisplayRole);
+    reparentingModel.setData(reparentingModel.index(0, 0, QModelIndex()), QLatin1String("row2foo"), Qt::DisplayRole);
+    QCOMPARE(row1->data(Qt::DisplayRole).toString(), QLatin1String("row1foo"));
+    QCOMPARE(row2->data(Qt::DisplayRole).toString(), QLatin1String("row2foo"));
+}
+
+//Ensure the model can deal with rows that are inserted out of order in a subnode
+void ReparentingModelTest::testInsertSourceRowSubnode()
+{
+    QStandardItem *parent = new QStandardItem(QLatin1String("parent"));
+    
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(parent);
+    QStandardItem *row2 = new QStandardItem(QLatin1String("row2"));
+    parent->appendRow(row2);
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+    ModelSignalSpy spy(reparentingModel);
+
+    QStandardItem *row1 = new QStandardItem(QLatin1String("row1"));
+    parent->insertRow(0, row1);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(getIndex("row2", reparentingModel).isValid());
+    //The model does not try to reorder. First come, first serve.
+    QCOMPARE(getIndex("row1", reparentingModel).row(), 1);
+    QCOMPARE(getIndex("row2", reparentingModel).row(), 0);
+    reparentingModel.setData(reparentingModel.index(1, 0, getIndex("parent", reparentingModel)), QLatin1String("row1foo"), Qt::DisplayRole);
+    reparentingModel.setData(reparentingModel.index(0, 0, getIndex("parent", reparentingModel)), QLatin1String("row2foo"), Qt::DisplayRole);
+    QCOMPARE(row1->data(Qt::DisplayRole).toString(), QLatin1String("row1foo"));
+    QCOMPARE(row2->data(Qt::DisplayRole).toString(), QLatin1String("row2foo"));
+}
+
+void ReparentingModelTest::testAddRemoveProxyNode()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    ModelSignalSpy spy(reparentingModel);
+
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+
+    QTest::qWait(0);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(getIndex("proxy1", reparentingModel).isValid());
+
+    reparentingModel.removeNode(DummyNode(reparentingModel, QLatin1String("proxy1")));
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    QVERIFY(!getIndex("proxy1", reparentingModel).isValid());
+
+    QCOMPARE(spy.mSignals, QStringList() << QLatin1String("modelReset") << QLatin1String("modelReset"));
+}
+
+void ReparentingModelTest::testDeduplicate()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("row1"))));
+
+    QTest::qWait(0);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QCOMPARE(getIndexList("row1", reparentingModel).size(), 1);
+    //TODO ensure we actually have the source index and not the proxy index
+}
+
+/**
+ * rebuildAll detects and handles nested duplicates
+ */
+void ReparentingModelTest::testDeduplicateNested()
+{
+    QStandardItemModel sourceModel;
+    QStandardItem *item = new QStandardItem(QLatin1String("row1"));
+    item->appendRow(new QStandardItem(QLatin1String("child1")));
+    sourceModel.appendRow(item);
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("child1"))));
+
+    QTest::qWait(0);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QCOMPARE(getIndexList("child1", reparentingModel).size(), 1);
+}
+
+/**
+ * onSourceRowsInserted detects and removes duplicates
+ */
+void ReparentingModelTest::testDeduplicateProxyNodeFirst()
+{
+    QStandardItemModel sourceModel;
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("row1"))));
+
+    QTest::qWait(0);
+
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QCOMPARE(getIndexList("row1", reparentingModel).size(), 1);
+    //TODO ensure we actually have the source index and not the proxy index
+}
+
+/**
+ * onSourceRowsInserted detects and removes nested duplicates
+ */
+void ReparentingModelTest::testNestedDeduplicateProxyNodeFirst()
+{
+    QStandardItemModel sourceModel;
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("child1"))));
+
+    QTest::qWait(0);
+
+    QStandardItem *item = new QStandardItem(QLatin1String("row1"));
+    item->appendRow(new QStandardItem(QLatin1String("child1")));
+    sourceModel.appendRow(item);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QCOMPARE(getIndexList("child1", reparentingModel).size(), 1);
+    //TODO ensure we actually have the source index and not the proxy index
+}
+
+void ReparentingModelTest::testReparent()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("orphan")));
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+
+    QTest::qWait(0);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QVERIFY(getIndex("proxy1", reparentingModel).isValid());
+    QCOMPARE(reparentingModel.rowCount(getIndex("proxy1", reparentingModel)), 1);
+}
+
+/*
+ * This test ensures we properly deal with reparented source nodes if the model is reset.
+ * This is important since source nodes are removed during the model reset while the proxy nodes (to which the source nodes have been reparented) remain.
+ *
+ * Note that this test is only useful with the model internal asserts.
+ */
+void ReparentingModelTest::testReparentResetWithoutCrash()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("orphan")));
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+    QTest::qWait(0);
+
+    reparentingModel.setSourceModel(&sourceModel);
+
+    QTest::qWait(0);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+}
+
+void ReparentingModelTest::testAddReparentedSourceItem()
+{
+    QStandardItemModel sourceModel;
+
+    ReparentingModel reparentingModel;
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+    reparentingModel.setSourceModel(&sourceModel);
+
+    QTest::qWait(0);
+
+    ModelSignalSpy spy(reparentingModel);
+
+    sourceModel.appendRow(new QStandardItem(QLatin1String("orphan")));
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QVERIFY(getIndex("proxy1", reparentingModel).isValid());
+    QCOMPARE(spy.mSignals, QStringList() << QLatin1String("rowsInserted"));
+    QCOMPARE(spy.parent, getIndex("proxy1", reparentingModel));
+    QCOMPARE(spy.start, 0);
+    QCOMPARE(spy.end, 0);
+}
+
+void ReparentingModelTest::testRemoveReparentedSourceItem()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("orphan")));
+    ReparentingModel reparentingModel;
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+    reparentingModel.setSourceModel(&sourceModel);
+
+    QTest::qWait(0);
+
+    ModelSignalSpy spy(reparentingModel);
+
+    sourceModel.removeRows(0, 1, QModelIndex());
+
+    QTest::qWait(0);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1);
+    QVERIFY(getIndex("proxy1", reparentingModel).isValid());
+    QVERIFY(!getIndex("orphan", reparentingModel).isValid());
+    QCOMPARE(spy.mSignals, QStringList() << QLatin1String("rowsRemoved"));
+    QCOMPARE(spy.parent, getIndex("proxy1", reparentingModel));
+    QCOMPARE(spy.start, 0);
+    QCOMPARE(spy.end, 0);
+}
+
+void ReparentingModelTest::testNestedReparentedSourceItem()
+{
+    QStandardItemModel sourceModel;
+    QStandardItem *item = new QStandardItem(QLatin1String("parent"));
+    item->appendRow(QList<QStandardItem*>() << new QStandardItem(QLatin1String("orphan")));
+    sourceModel.appendRow(item);
+
+    ReparentingModel reparentingModel;
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+    reparentingModel.setSourceModel(&sourceModel);
+
+    QTest::qWait(0);
+
+    //toplevel should have both parent and proxy
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("orphan", reparentingModel).isValid());
+    QCOMPARE(getIndex("orphan", reparentingModel).parent(), getIndex("proxy1", reparentingModel));
+}
+
+void ReparentingModelTest::testAddNestedReparentedSourceItem()
+{
+    QStandardItemModel sourceModel;
+
+    ReparentingModel reparentingModel;
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"))));
+    reparentingModel.setSourceModel(&sourceModel);
+
+    QTest::qWait(0);
+
+    ModelSignalSpy spy(reparentingModel);
+
+    QStandardItem *item = new QStandardItem(QLatin1String("parent"));
+    item->appendRow(QList<QStandardItem*>() << new QStandardItem(QLatin1String("orphan")));
+    sourceModel.appendRow(item);
+
+    QTest::qWait(0);
+
+    //toplevel should have both parent and proxy
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("orphan", reparentingModel).isValid());
+    QCOMPARE(getIndex("orphan", reparentingModel).parent(), getIndex("proxy1", reparentingModel));
+    QCOMPARE(spy.mSignals, QStringList() << QLatin1String("rowsInserted") << QLatin1String("rowsInserted"));
+}
+
+void ReparentingModelTest::testSourceDataChanged()
+{
+    QStandardItemModel sourceModel;
+    QStandardItem *item = new QStandardItem(QLatin1String("row1"));
+    sourceModel.appendRow(item);
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&sourceModel);
+
+    item->setText(QLatin1String("rowX"));
+
+    QVERIFY(!getIndex("row1", reparentingModel).isValid());
+    QVERIFY(getIndex("rowX", reparentingModel).isValid());
+}
+
+
+void ReparentingModelTest::testSourceLayoutChanged()
+{
+    QStandardItemModel sourceModel;
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row2")));
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+
+    QSortFilterProxyModel filter;
+    filter.setSourceModel(&sourceModel);
+
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&filter);
+    ModelSignalSpy spy(reparentingModel);
+
+    QPersistentModelIndex index1 = reparentingModel.index(0, 0, QModelIndex());
+    QPersistentModelIndex index2 = reparentingModel.index(1, 0, QModelIndex());
+
+    //Emits layout changed and sorts the items the other way around
+    filter.sort(0, Qt::AscendingOrder);
+
+    QCOMPARE(reparentingModel.rowCount(QModelIndex()), 2);
+    QVERIFY(getIndex("row1", reparentingModel).isValid());
+    //Right now we don't even care about the order
+    // QCOMPARE(spy.mSignals, QStringList() << QLatin1String("layoutChanged"));
+    QCOMPARE(index1.data().toString(), QLatin1String("row2"));
+    QCOMPARE(index2.data().toString(), QLatin1String("row1"));
+}
+
+/*
+ * This is a very implementation specific test that tries to crash the model
+ */
+//Test for invalid implementation of layoutChanged
+//*have proxy node in model
+//*insert duplicate from source
+//*issue layout changed so the model get's rebuilt
+//*access node (which is not actually existing anymore)
+// => crash
+void ReparentingModelTest::testInvalidLayoutChanged()
+{
+    QStandardItemModel sourceModel;
+    QSortFilterProxyModel filter;
+    filter.setSourceModel(&sourceModel);
+    ReparentingModel reparentingModel;
+    reparentingModel.setSourceModel(&filter);
+    reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("row1"))));
+
+    QTest::qWait(0);
+
+    //Take reference to proxy node
+    QPersistentModelIndex persistentIndex = getIndexList("row1", reparentingModel).first();
+    QVERIFY(persistentIndex.isValid());
+
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row1")));
+    sourceModel.appendRow(new QStandardItem(QLatin1String("row2")));
+
+    //This rebuilds the model and invalidates the reference
+    //Emits layout changed and sorts the items the other way around
+    filter.sort(0, Qt::AscendingOrder);
+
+    //This fails because the persistenIndex is no longer valid
+    persistentIndex.data().toString();
+    QVERIFY(!persistentIndex.isValid());
+}
+
+
+QTEST_MAIN(ReparentingModelTest)
+
+#include "reparentingmodeltest.moc"




More information about the commits mailing list