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