diff --git a/korganizer/akonadicollectionview.cpp b/korganizer/akonadicollectionview.cpp index 3b0a8083db..c562255ceb 100644 --- a/korganizer/akonadicollectionview.cpp +++ b/korganizer/akonadicollectionview.cpp @@ -1,1049 +1,1059 @@ /* This file is part of KOrganizer. Copyright (c) 2003,2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (C) 2009 Sebastian Sauer Copyright (C) 2010 Laurent Montel Copyright (C) 2012 Sérgio Martins 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 "akonadicollectionview.h" #include "kocore.h" #include "kohelper.h" #include "koprefs.h" #include "koglobals.h" #include "views/collectionview/reparentingmodel.h" #include "views/collectionview/calendardelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static Akonadi::EntityTreeModel *findEtm(QAbstractItemModel *model) { QAbstractProxyModel *proxyModel; while (model) { proxyModel = qobject_cast(model); if (proxyModel && proxyModel->sourceModel()) { model = proxyModel->sourceModel(); } else { break; } } return qobject_cast(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 treeStateRestorer; QTreeView *mTreeView; bool mExpandAll; QString mTreeStateConfig; }; AkonadiCollectionViewFactory::AkonadiCollectionViewFactory( CalendarView *view ) : mView( view ), mAkonadiCollectionView( 0 ) { } namespace { static bool hasCompatibleMimeTypes( const Akonadi::Collection &collection ) { static QStringList goodMimeTypes; if ( goodMimeTypes.isEmpty() ) { goodMimeTypes << QLatin1String( "text/calendar" ) << KCalCore::Event::eventMimeType() << KCalCore::Todo::todoMimeType() << KCalCore::Journal::journalMimeType(); } for ( int i=0; i() && !collection.attribute()->iconName().isEmpty() ) { return collection.attribute()->icon(); } } } else if ( role == Qt::FontRole ) { const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index ); if ( !collection.contentMimeTypes().isEmpty() && KOHelper::isStandardCalendar( collection.id() ) && collection.rights() & Akonadi::Collection::CanCreateItem ) { QFont font = qvariant_cast( QSortFilterProxyModel::data( index, Qt::FontRole ) ); font.setBold( true ); if ( !mInitDefaultCalendar ) { mInitDefaultCalendar = true; CalendarSupport::KCalPrefs::instance()->setDefaultCalendarId( collection.id() ); } return font; } } return QSortFilterProxyModel::data( index, role ); } /* reimp */ Qt::ItemFlags flags( const QModelIndex &index ) const { return Qt::ItemIsSelectable | QSortFilterProxyModel::flags( index ); } private: 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(); CollectionIdentificationAttribute *attr = col.attribute(); //We filter the user folders because we insert person nodes for user folders. if (attr && ((attr->collectionNamespace() == "usertoplevel") || (attr->collectionNamespace() == "usertoplevel")) || (col.name().contains(QLatin1String("Other Users")))) { return false; } return true; } }; class EnabledModel : public QSortFilterProxyModel { public: explicit EnabledModel(QObject *parent=0) : QSortFilterProxyModel(parent) { } protected: virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { if (role == EnabledRole) { Akonadi::Collection col = index.data(Akonadi::EntityTreeModel::CollectionRole).value(); if (col.shouldList(Akonadi::Collection::ListDisplay)) { return Qt::Checked; } else { return Qt::Unchecked; } } return QSortFilterProxyModel::data(index, role); } }; class CalendarDelegateModel : public QSortFilterProxyModel { public: explicit CalendarDelegateModel(QObject *parent=0) : QSortFilterProxyModel(parent) { } protected: bool checkChildren(const QModelIndex &index, int role, const QVariant &value) const { const QModelIndex sourceIndex = mapToSource(index); for (int i = 0; i < sourceModel()->rowCount(sourceIndex); i++) { const QModelIndex child = sourceModel()->index(i, 0, sourceIndex); if (child.data(role) != value) { return false; } } return true; } virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { if (role == Qt::CheckStateRole) { if (sourceModel()->hasChildren(mapToSource(index)) && index.data(NodeTypeRole).toInt() == PersonNodeRole) { bool allChecked = checkChildren(index, role, Qt::Checked); bool allUnchecked = checkChildren(index, role, Qt::Unchecked); if (allChecked) { return Qt::Checked; } else if (allUnchecked) { return Qt::Unchecked; } else { return Qt::PartiallyChecked; } } } if (role == EnabledRole) { if (sourceModel()->hasChildren(mapToSource(index)) && index.data(NodeTypeRole).toInt() == PersonNodeRole) { bool allChecked = checkChildren(index, role, Qt::Checked); bool allUnchecked = checkChildren(index, role, Qt::Unchecked); // kDebug() << "person node " << index.data().toString() << allChecked << allUnchecked; if (allChecked) { return Qt::Checked; } else if (allUnchecked) { return Qt::Unchecked; } else { return Qt::PartiallyChecked; } } } return QSortFilterProxyModel::data(index, role); } void setChildren(const QModelIndex &sourceIndex, const QVariant &value, int role) const { if (!sourceIndex.isValid()) { return; } for (int i = 0; i < sourceModel()->rowCount(sourceIndex); i++) { const QModelIndex child = sourceModel()->index(i, 0, sourceIndex); sourceModel()->setData(child, value, role); setChildren(child, value, role); } } virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) { if (role == Qt::CheckStateRole) { if (sourceModel()->hasChildren(mapToSource(index)) && index.data(NodeTypeRole).toInt() == PersonNodeRole) { setChildren(mapToSource(index), value, role); } } return QSortFilterProxyModel::setData(index, value, role); } }; } // anonymous namespace CalendarViewExtension *AkonadiCollectionViewFactory::create( QWidget *parent ) { mAkonadiCollectionView = new AkonadiCollectionView( view(), true, parent ); QObject::connect( mAkonadiCollectionView, SIGNAL(resourcesChanged(bool)), mView, SLOT(resourcesChanged()) ); QObject::connect( mAkonadiCollectionView, SIGNAL(resourcesAddedRemoved()), mView, SLOT(resourcesChanged()) ); return mAkonadiCollectionView; } CalendarView *AkonadiCollectionViewFactory::view() const { return mView; } AkonadiCollectionView *AkonadiCollectionViewFactory::collectionView() const { return mAkonadiCollectionView; } AkonadiCollectionView::AkonadiCollectionView( CalendarView *view, bool hasContextMenu, QWidget *parent ) : CalendarViewExtension( parent ), mActionManager(0), mCollectionView(0), mBaseModel( 0 ), mSelectionProxyModel( 0 ), mNotSendAddRemoveSignal( false ), mWasDefaultCalendar( false ), mHasContextMenu( hasContextMenu ) { QVBoxLayout *topLayout = new QVBoxLayout( this ); 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 ); 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); EnabledModel *enabledModel = new EnabledModel(this); enabledModel->setSourceModel(userProxy); CalendarDelegateModel *calendarDelegateModel = new CalendarDelegateModel(this); calendarDelegateModel->setSourceModel(enabledModel); //Hide collections that are not required CollectionFilter *collectionFilter = new CollectionFilter( this ); collectionFilter->setSourceModel( calendarDelegateModel ); SortProxyModel *sortProxy = new SortProxyModel( this ); sortProxy->setSourceModel(collectionFilter); mCollectionView = new Akonadi::EntityTreeView( this ); mCollectionView->header()->hide(); mCollectionView->setRootIsDecorated( true ); // mCollectionView->setSorting( true ); { StyledCalendarDelegate *delegate = new StyledCalendarDelegate(mCollectionView); connect(delegate, SIGNAL(action(QModelIndex, int)), this, SLOT(onAction(QModelIndex, int))); mCollectionView->setItemDelegate( delegate ); } mCollectionView->setModel( sortProxy ); connect( mCollectionView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SLOT(updateMenu()) ); mNewNodeExpander = new NewNodeExpander(mCollectionView, false, QLatin1String("CollectionTreeView")); //Filter tree view. ReparentingModel *searchProxy = new ReparentingModel( this ); searchProxy->setSourceModel(collectionFilter); searchProxy->setObjectName(QLatin1String("searchProxy")); KRecursiveFilterProxyModel *filterTreeViewModel = new KRecursiveFilterProxyModel( this ); filterTreeViewModel->setDynamicSortFilter( true ); filterTreeViewModel->setSourceModel( searchProxy ); filterTreeViewModel->setFilterCaseSensitivity( Qt::CaseInsensitive ); // filterTreeViewModel->setObjectName( "Recursive filtering, for the search bar" ); connect( searchCol, SIGNAL(textChanged(QString)), filterTreeViewModel, SLOT(setFilterFixedString(QString)) ); SortProxyModel *searchSortProxy = new SortProxyModel( this ); searchSortProxy->setSourceModel(filterTreeViewModel); Akonadi::EntityTreeView *mSearchView = new Akonadi::EntityTreeView( this ); mSearchView->header()->hide(); mSearchView->setRootIsDecorated( true ); { StyledCalendarDelegate *delegate = new StyledCalendarDelegate(mCollectionView); connect(delegate, SIGNAL(action(QModelIndex, int)), this, SLOT(onAction(QModelIndex, int))); mSearchView->setItemDelegate( delegate ); } mSearchView->setModel( searchSortProxy ); 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 ); + KMessageWidget *msgWidget = new KMessageWidget(this); + msgWidget->setCloseButtonVisible(false); + msgWidget->setMessageType(KMessageWidget::Positive); + msgWidget->setObjectName(QLatin1String("msgwidget")); + msgWidget->setVisible(false); + msgWidget->setText(i18n("searching...")); + connect(mController, SIGNAL(searching(bool)), + msgWidget, SLOT(setVisible(bool))); + topLayout->addWidget(msgWidget); + connect( mBaseModel, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(rowsInserted(QModelIndex,int,int)) ); KXMLGUIClient *xmlclient = KOCore::self()->xmlguiClient( view ); if ( xmlclient ) { mCollectionView->setXmlGuiClient( xmlclient ); mActionManager = new Akonadi::StandardCalendarActionManager( xmlclient->actionCollection(), mCollectionView ); QList standardActions; standardActions << Akonadi::StandardActionManager::CreateCollection << Akonadi::StandardActionManager::CopyCollections << Akonadi::StandardActionManager::DeleteCollections << Akonadi::StandardActionManager::SynchronizeCollections << Akonadi::StandardActionManager::CollectionProperties << Akonadi::StandardActionManager::CopyItems << Akonadi::StandardActionManager::Paste << Akonadi::StandardActionManager::DeleteItems << Akonadi::StandardActionManager::CutItems << Akonadi::StandardActionManager::CutCollections << Akonadi::StandardActionManager::CreateResource << Akonadi::StandardActionManager::DeleteResources << Akonadi::StandardActionManager::ResourceProperties << Akonadi::StandardActionManager::SynchronizeResources << Akonadi::StandardActionManager::SynchronizeCollectionTree << Akonadi::StandardActionManager::SynchronizeCollectionsRecursive; Q_FOREACH( Akonadi::StandardActionManager::Type standardAction, standardActions ) { mActionManager->createAction( standardAction ); } QList calendarActions; calendarActions << Akonadi::StandardCalendarActionManager::CreateEvent << Akonadi::StandardCalendarActionManager::CreateTodo << Akonadi::StandardCalendarActionManager::CreateSubTodo << Akonadi::StandardCalendarActionManager::CreateJournal << Akonadi::StandardCalendarActionManager::EditIncidence; Q_FOREACH( Akonadi::StandardCalendarActionManager::Type calendarAction, calendarActions ) { mActionManager->createAction( calendarAction ); } mActionManager->setCollectionSelectionModel( mCollectionView->selectionModel() ); mActionManager->interceptAction( Akonadi::StandardActionManager::CreateResource ); mActionManager->interceptAction( Akonadi::StandardActionManager::DeleteResources ); mActionManager->interceptAction( Akonadi::StandardActionManager::DeleteCollections ); connect( mActionManager->action( Akonadi::StandardActionManager::CreateResource ), SIGNAL(triggered(bool)), this, SLOT(newCalendar()) ); connect( mActionManager->action( Akonadi::StandardActionManager::DeleteResources ), SIGNAL(triggered(bool)), this, SLOT(deleteCalendar()) ); connect( mActionManager->action( Akonadi::StandardActionManager::DeleteCollections ), SIGNAL(triggered(bool)), this, SLOT(deleteCalendar()) ); mActionManager->setContextText( Akonadi::StandardActionManager::CollectionProperties, Akonadi::StandardActionManager::DialogTitle, ki18nc( "@title:window", "Properties of Calendar Folder %1" ) ); const QStringList pages = QStringList() << QLatin1String( "CalendarSupport::CollectionGeneralPage" ) << QLatin1String( "Akonadi::CachePolicyPage" ) << QLatin1String( "PimCommon::CollectionAclPage" ); mActionManager->setCollectionPropertiesPageNames( pages ); mDisableColor = new KAction( mCollectionView ); mDisableColor->setText( i18n( "&Disable Color" ) ); mDisableColor->setEnabled( false ); xmlclient->actionCollection()->addAction( QString::fromLatin1( "disable_color" ), mDisableColor ); connect( mDisableColor, SIGNAL(triggered(bool)), this, SLOT(disableColor()) ); mAssignColor = new KAction( mCollectionView ); mAssignColor->setText( i18n( "&Assign Color..." ) ); mAssignColor->setEnabled( false ); xmlclient->actionCollection()->addAction( QString::fromLatin1( "assign_color" ), mAssignColor ); connect( mAssignColor, SIGNAL(triggered(bool)), this, SLOT(assignColor()) ); mDefaultCalendar = new KAction( mCollectionView ); mDefaultCalendar->setText( i18n( "Use as &Default Calendar" ) ); mDefaultCalendar->setEnabled( false ); xmlclient->actionCollection()->addAction( QString::fromLatin1( "set_standard_calendar" ), mDefaultCalendar ); //Disable a calendar or remove a referenced calendar QAction *disableAction = xmlclient->actionCollection()->addAction( QLatin1String("collection_disable"), this, SLOT(edit_disable()) ); disableAction->setText( i18n( "Remove from list" ) ); disableAction->setIcon(KIconLoader().loadIcon(QLatin1String("list-remove"), KIconLoader::Small)); //Enable (subscribe) to a calendar. mEnableAction = xmlclient->actionCollection()->addAction( QLatin1String("collection_enable"), this, SLOT(edit_enable()) ); mEnableAction->setText( i18n( "Add to list permanently" ) ); mEnableAction->setIcon(KIconLoader().loadIcon(QLatin1String("bookmarks"), KIconLoader::Small)); connect( mDefaultCalendar, SIGNAL(triggered(bool)), this, SLOT(setDefaultCalendar()) ); } } AkonadiCollectionView::~AkonadiCollectionView() { //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::onSearchIsActive(bool active) { if (!active) { mStackedWidget->setCurrentIndex(0); } else { mStackedWidget->setCurrentIndex(1); } } void AkonadiCollectionView::setDefaultCalendar() { QModelIndex index = mCollectionView->selectionModel()->currentIndex(); //selectedRows() Q_ASSERT( index.isValid() ); const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index ); CalendarSupport::KCalPrefs::instance()->setDefaultCalendarId( collection.id() ); CalendarSupport::KCalPrefs::instance()->usrWriteConfig(); updateMenu(); updateView(); emit defaultResourceChanged( collection ); } void AkonadiCollectionView::assignColor() { QModelIndex index = mCollectionView->selectionModel()->currentIndex(); //selectedRows() Q_ASSERT( index.isValid() ); const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index ); Q_ASSERT( collection.isValid() ); const QString identifier = QString::number( collection.id() ); const QColor defaultColor = KOPrefs::instance()->resourceColor( identifier ); QColor myColor; const int result = KColorDialog::getColor( myColor, defaultColor ); if ( result == KColorDialog::Accepted && myColor != defaultColor ) { KOPrefs::instance()->setResourceColor( identifier, myColor ); emit colorsChanged(); updateMenu(); updateView(); } } void AkonadiCollectionView::disableColor() { QModelIndex index = mCollectionView->selectionModel()->currentIndex(); //selectedRows() Q_ASSERT( index.isValid() ); const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index ); Q_ASSERT( collection.isValid() ); const QString identifier = QString::number( collection.id() ); KOPrefs::instance()->setResourceColor( identifier, QColor() ); updateMenu(); updateView(); emit colorsChanged(); } void AkonadiCollectionView::setCollectionSelectionProxyModel( KCheckableProxyModel *m ) { if ( mSelectionProxyModel == m ) { return; } mSelectionProxyModel = m; if ( !mSelectionProxyModel ) { return; } new NewCalendarChecker( m ); mBaseModel->setSourceModel( mSelectionProxyModel ); } KCheckableProxyModel *AkonadiCollectionView::collectionSelectionProxyModel() const { return mSelectionProxyModel; } Akonadi::EntityTreeView *AkonadiCollectionView::view() const { return mCollectionView; } void AkonadiCollectionView::updateView() { emit resourcesChanged( mSelectionProxyModel ? mSelectionProxyModel->selectionModel()->hasSelection() : false ); } void AkonadiCollectionView::updateMenu() { if ( !mHasContextMenu ) { return; } bool enableAction = mCollectionView->selectionModel()->hasSelection(); enableAction = enableAction && ( KOPrefs::instance()->agendaViewColors() != KOPrefs::CategoryOnly ); mAssignColor->setEnabled( enableAction ); QModelIndex index = mCollectionView->selectionModel()->currentIndex(); //selectedRows() bool disableStuff = true; if ( index.isValid() ) { //Returns an invalid collection on person nodes const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index ); 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 ); disableStuff = false; } if ( collection.isValid() && collection.shouldList(Akonadi::Collection::ListDisplay) ) { mEnableAction->setEnabled(false); } else { mEnableAction->setEnabled(true); } } if ( disableStuff ) { mDisableColor->setEnabled( false ); mDefaultCalendar->setEnabled( false ); mAssignColor->setEnabled( false ); } } void AkonadiCollectionView::newCalendar() { Akonadi::AgentTypeDialog dlg( this ); dlg.setWindowTitle( i18n( "Add Calendar" ) ); dlg.agentFilterProxyModel()->addMimeTypeFilter( QString::fromLatin1( "text/calendar" ) ); dlg.agentFilterProxyModel()->addCapabilityFilter( QLatin1String("Resource") ); // show only resources, no agents if ( dlg.exec() ) { mNotSendAddRemoveSignal = true; const Akonadi::AgentType agentType = dlg.agentType(); if ( agentType.isValid() ) { Akonadi::AgentInstanceCreateJob *job = new Akonadi::AgentInstanceCreateJob( agentType, this ); job->configure( this ); connect( job, SIGNAL(result(KJob*)), this, SLOT(newCalendarDone(KJob*)) ); job->start(); } } } void AkonadiCollectionView::newCalendarDone( KJob *job ) { Akonadi::AgentInstanceCreateJob *createjob = static_cast( job ); if ( createjob->error() ) { //TODO(AKONADI_PORT) // this should show an error dialog and should be merged // with the identical code in ActionManager kWarning() << "Create calendar failed:" << createjob->errorString(); mNotSendAddRemoveSignal = false; return; } mNotSendAddRemoveSignal = false; //TODO } void AkonadiCollectionView::deleteCalendar() { QModelIndex index = mCollectionView->selectionModel()->currentIndex(); //selectedRows() Q_ASSERT( index.isValid() ); const Akonadi::Collection collection = CalendarSupport::collectionFromIndex( index ); Q_ASSERT( collection.isValid() ); const QString displayname = index.model()->data( index, Qt::DisplayRole ).toString(); Q_ASSERT( !displayname.isEmpty() ); if ( KMessageBox::warningContinueCancel( this, i18n( "Do you really want to delete calendar %1?", displayname ), i18n( "Delete Calendar" ), KStandardGuiItem::del(), KStandardGuiItem::cancel(), QString(), KMessageBox::Dangerous ) == KMessageBox::Continue ) { bool isTopLevel = collection.parentCollection() == Akonadi::Collection::root(); mNotSendAddRemoveSignal = true; mWasDefaultCalendar = KOHelper::isStandardCalendar( collection.id() ); if ( !isTopLevel ) { // deletes contents Akonadi::CollectionDeleteJob *job = new Akonadi::CollectionDeleteJob( collection, this ); connect( job, SIGNAL(result(KJob*)), this, SLOT(deleteCalendarDone(KJob*)) ); } else { // deletes the agent, not the contents const Akonadi::AgentInstance instance = Akonadi::AgentManager::self()->instance( collection.resource() ); if ( instance.isValid() ) { Akonadi::AgentManager::self()->removeInstance( instance ); } } } } void AkonadiCollectionView::deleteCalendarDone( KJob *job ) { Akonadi::CollectionDeleteJob *deletejob = static_cast( job ); if ( deletejob->error() ) { kWarning() << "Delete calendar failed:" << deletejob->errorString(); mNotSendAddRemoveSignal = false; return; } if ( mWasDefaultCalendar ) { CalendarSupport::KCalPrefs::instance()->setDefaultCalendarId( Akonadi::Collection().id() ); } mNotSendAddRemoveSignal = false; //TODO } void AkonadiCollectionView::rowsInserted( const QModelIndex &, int, int ) { if ( !mNotSendAddRemoveSignal ) { emit resourcesAddedRemoved(); } } Akonadi::Collection AkonadiCollectionView::selectedCollection() const { Akonadi::Collection collection; QItemSelectionModel *selectionModel = mCollectionView->selectionModel(); if ( !selectionModel ) { return collection; } QModelIndexList indexes = selectionModel->selectedIndexes(); if ( !indexes.isEmpty() ) { collection = indexes.first().data( Akonadi::EntityTreeModel::CollectionRole ).value(); } return collection; } Akonadi::Collection::List AkonadiCollectionView::checkedCollections() const { Akonadi::Collection::List collections; if ( !mSelectionProxyModel ) { return collections; } QItemSelectionModel *selectionModel = mSelectionProxyModel->selectionModel(); if ( !selectionModel ) { return collections; } QModelIndexList indexes = selectionModel->selectedIndexes(); foreach( const QModelIndex &index, indexes ) { if ( index.isValid() ) { Akonadi::Collection collection = index.data( Akonadi::EntityTreeModel::CollectionRole ).value(); if ( collection.isValid() ) collections << collection; } } return collections; } bool AkonadiCollectionView::isChecked(const Akonadi::Collection &collection) const { if (!mSelectionProxyModel) return false; QItemSelectionModel *selectionModel = mSelectionProxyModel->selectionModel(); if (!selectionModel) return false; QModelIndexList indexes = selectionModel->selectedIndexes(); foreach(const QModelIndex &index, indexes) { if (index.isValid()) { Akonadi::Collection c = index.data(Akonadi::EntityTreeModel::CollectionRole).value(); if (c.id() == collection.id()) { return true; } } } return false; } Akonadi::EntityTreeModel *AkonadiCollectionView::entityTreeModel() const { QAbstractProxyModel *proxy = qobject_cast( mCollectionView->model() ); while( proxy ) { Akonadi::EntityTreeModel *etm = qobject_cast( proxy->sourceModel() ); if ( etm ) { return etm; } proxy = qobject_cast( proxy->sourceModel() ); } kWarning() << "Couldn't find EntityTreeModel"; return 0; } void AkonadiCollectionView::edit_disable() { Akonadi::Collection col = mCollectionView->currentIndex().data(Akonadi::EntityTreeModel::CollectionRole).value(); if (col.isValid()) { mController->setCollectionState(col, Controller::Disabled); } const QVariant var = mCollectionView->currentIndex().data(PersonRole); if (var.isValid()) { mController->removePerson(var.value()); } } void AkonadiCollectionView::edit_enable() { Akonadi::Collection col = mCollectionView->currentIndex().data(Akonadi::EntityTreeModel::CollectionRole).value(); kDebug() << col.name(); if (col.isValid()) { mController->setCollectionState(col, Controller::Enabled); } const QVariant var = mCollectionView->currentIndex().data(PersonRole); if (var.isValid()) { mController->addPerson(var.value()); } } void AkonadiCollectionView::onAction(const QModelIndex &index, int a) { const StyledCalendarDelegate::Action action = static_cast(a); switch (action) { case StyledCalendarDelegate::AddToList: { const Akonadi::Collection col = index.data(CollectionRole).value(); if (col.isValid()) { mController->setCollectionState(col, Controller::Referenced); } else { const QVariant var = index.data(PersonRole); if (var.isValid()) { mController->addPerson(var.value()); } } } break; case StyledCalendarDelegate::RemoveFromList: { const Akonadi::Collection col = CalendarSupport::collectionFromIndex(index); if (col.isValid()) { mController->setCollectionState(col, Controller::Disabled); } else { const QVariant var = index.data(PersonRole); if (var.isValid()) { mController->removePerson(var.value()); } } } break; case StyledCalendarDelegate::Enable: { const Akonadi::Collection col = CalendarSupport::collectionFromIndex(index); if (col.isValid()) { mController->setCollectionState(col, Controller::Enabled); } else { const QVariant var = index.data(PersonRole); if (var.isValid()) { mController->setCollectionState(Akonadi::Collection(var.value().rootCollection), Controller::Enabled, true); } } } break; } } #include "akonadicollectionview.moc" diff --git a/korganizer/views/collectionview/controller.cpp b/korganizer/views/collectionview/controller.cpp index 03f8949ed5..8f7808c908 100644 --- a/korganizer/views/collectionview/controller.cpp +++ b/korganizer/views/collectionview/controller.cpp @@ -1,554 +1,782 @@ /* Copyright (C) 2014 Christian Mollekopf 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 #include #include #include #include #include #include +#include #include #include #include #include #include CollectionNode::CollectionNode(ReparentingModel& personModel, const Akonadi::Collection& col) : Node(personModel), mCollection(col), mCheckState(Qt::Unchecked), isSearchNode(false) { } CollectionNode::~CollectionNode() { } bool CollectionNode::operator==(const ReparentingModel::Node &node) const { const CollectionNode *collectionNode = dynamic_cast(&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()) { return mCollection.attribute()->icon(); } return QVariant(); } if (role == Qt::CheckStateRole) { if (isSearchNode) { return QVariant(); } return mCheckState; } if (role == Qt::ToolTipRole) { - return QString(QLatin1String("Collection: ") + mCollection.name() + QString::number(mCollection.id())); + return QString(i18n("Collection: ") + mCollection.name() + QLatin1String(" ") + QString::number(mCollection.id())); } if (role == IsSearchResultRole) { return isSearchNode; } if (role == CollectionRole) { return QVariant::fromValue(mCollection); } if (role == NodeTypeRole) { return CollectionNodeRole; } return QVariant(); } bool CollectionNode::setData(const QVariant& value, int role) { if (role == Qt::CheckStateRole) { mCheckState = static_cast(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() == mCollection.id()); } PersonNode::PersonNode(ReparentingModel& personModel, const Person& person) : Node(personModel), mPerson(person), mCheckState(Qt::Unchecked), isSearchNode(false) { } PersonNode::~PersonNode() { } bool PersonNode::operator==(const Node &node) const { const PersonNode *personNode = dynamic_cast(&node); if (personNode) { - return (personNode->mPerson.name == mPerson.name); + return (personNode->mPerson.uid == mPerson.uid); } 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; + QString name = mPerson.name; + if (!mPerson.ou.isEmpty()) { + name += QLatin1String(" (") + mPerson.ou + QLatin1String(")"); + } + return name; } if (role == Qt::DecorationRole) { return KIcon(QLatin1String("meeting-participant")); } if (role == Qt::CheckStateRole) { if (isSearchNode) { return QVariant(); } return mCheckState; } if (role == Qt::ToolTipRole) { - return QString(QLatin1String("Person: ") + mPerson.name); + QString tooltip = i18n("Person: ") + mPerson.name; + if (!mPerson.mail.isEmpty()) { + tooltip += QLatin1String("\n") + i18n("Mail: ") + mPerson.mail; + } + if (!mPerson.ou.isEmpty()) { + tooltip += QLatin1String("\n") + i18n("Organization Unit: ") + mPerson.ou; + } + return tooltip; } if (role == PersonRole) { return QVariant::fromValue(mPerson); } if (role == IsSearchResultRole) { return isSearchNode; } if (role == NodeTypeRole) { return PersonNodeRole; } return QVariant(); } bool PersonNode::setData(const QVariant& value, int role) { if (role == Qt::CheckStateRole) { mCheckState = static_cast(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(); if (parent.id() == mPerson.rootCollection) { return true; } const Akonadi::Collection &col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); // 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().name == mPerson.name); } void PersonNodeManager::checkSourceIndex(const QModelIndex &sourceIndex) { const Akonadi::Collection col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); // kDebug() << col.displayName() << col.enabled(); if (col.isValid()) { CollectionIdentificationAttribute *attr = col.attribute(); if (attr && attr->collectionNamespace() == "usertoplevel") { kDebug() << "Found user folder, creating person node " << col.displayName(); Person person; person.name = col.displayName(); + person.mail = QString::fromUtf8(attr->mail()); + person.ou = QString::fromUtf8(attr->ou()); + person.uid = col.name(); person.rootCollection = col.id(); model.addNode(ReparentingModel::Node::Ptr(new PersonNode(model, person))); } } } void PersonNodeManager::checkSourceIndexRemoval(const QModelIndex &sourceIndex) { const Akonadi::Collection col = sourceIndex.data(Akonadi::EntityTreeModel::CollectionRole).value(); // kDebug() << col.displayName() << col.enabled(); if (col.isValid()) { CollectionIdentificationAttribute *attr = col.attribute(); if (attr && attr->collectionNamespace() == "usertoplevel") { kDebug() << "Found user folder, removing person node " << col.displayName(); Person person; person.name = col.displayName(); + person.mail = QString::fromUtf8(attr->mail()); + person.ou = QString::fromUtf8(attr->ou()); + person.uid = col.name(); person.rootCollection = col.id(); model.removeNode(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; + break; } } col.setParentCollection(parent); return col; } void CollectionSearchJob::onAncestorsFetched(KJob *job) { if (job->error()) { kWarning() << job->errorString(); } Akonadi::CollectionFetchJob *fetchJob = static_cast(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) { + connect(&mLdapSearch, SIGNAL(searchData(const QList &)), + SLOT(onLDAPSearchData(const QList &))); + + connect(&mLdapSearch, SIGNAL(searchDone()), + SLOT(onLDAPSearchDone())); +} + +PersonSearchJob::~PersonSearchJob() +{ + mLdapSearch.cancelSearch(); +} + +bool PersonSearchJob::kill(KJob::KillVerbosity verbosity) +{ + mLdapSearch.cancelSearch(); + return KJob::kill(verbosity); } 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(); + mCollectionSearchDone = false; + mLdapSearchDone = false; + + mLdapSearch.startSearch(QLatin1String("*") + mSearchString); + if (collections.isEmpty()) { //We didn't find anything - emitResult(); + mCollectionSearchDone = true; 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::onLDAPSearchData(const QList< KLDAP::LdapResultObject > &list) +{ + QList persons; + Q_FOREACH(const KLDAP::LdapResultObject &item, list) { + Person person; + person.name = QString::fromUtf8(item.object.value(QLatin1String("cn"))); + person.mail = QString::fromUtf8(item.object.value(QLatin1String("mail"))); + + const int depth = item.object.dn().depth(); + for ( int i = 0; i < depth; ++i ) { + const QString rdnStr = item.object.dn().rdnString(i); + if ( rdnStr.startsWith(QLatin1String("ou="), Qt::CaseInsensitive) ) { + person.ou = rdnStr.mid(3); + break; + } + } + const QStringList &parts = person.mail.split(QLatin1Char('@')); + if (parts.count() == 2) { + const QString &uid = parts.at(0); + person.uid = uid; + if (mMatches.contains(uid)) { + const Person &p = mMatches.value(uid); + if (p.mail != person.mail ) { + if (p.rootCollection > -1) { + person.rootCollection = p.rootCollection; + person.updateDisplayName = p.updateDisplayName; + updatePersonCollection(person); + mMatches.insert(uid, person); + } else { + kWarning() << "That should not happen: we found two times persons with the same uid ("<< uid << "), but differnet name:" << p.name << "vs" << person.name; + } + } + } else { //New person found + mMatches.insert(uid, person); + persons << person; + } + } else { + kWarning() << item.object.dn().toString() << ": invalid email address" << person.mail; + } + } + if (persons.count() > 0) { + emit personsFound(persons); + } +} + +void PersonSearchJob::onLDAPSearchDone() +{ + mLdapSearchDone = true; + if (mCollectionSearchDone) { + emitResult(); + } +} + void PersonSearchJob::onCollectionsReceived(const Akonadi::Collection::List &list) { + QList persons; Q_FOREACH(const Akonadi::Collection &col, list) { Person person; - person.name = col.displayName(); + const QString &uid = col.name(); + const CollectionIdentificationAttribute *const attr = col.attribute(); + const Akonadi::EntityDisplayAttribute *const displayname = col.attribute(); person.rootCollection = col.id(); - mMatches << person; + person.uid = uid; + if (attr) { + person.ou = QString::fromUtf8(attr->ou()); + person.mail = QString::fromUtf8(attr->mail()); + person.name = QString::fromUtf8(attr->identifier()); + if (!displayname || displayname->displayName().isEmpty() || displayname->displayName() == person.name) { + person.updateDisplayName = true; + } + } else { + person.name = col.displayName(); + if (!displayname || displayname->displayName().isEmpty()) { + person.updateDisplayName = true; + } + } + if (mMatches.contains(uid)) { + Person p = mMatches.value(uid); + if (p.rootCollection > -1) { + //two collection with the same uid ?! + kWarning() << "Two collections match to same person" << p.rootCollection << person.rootCollection; + } else if (p.mail != person.mail) { + p.rootCollection = person.rootCollection; + p.updateDisplayName = person.updateDisplayName; + updatePersonCollection(p); + } else { + mMatches.insert(uid, person); + emit personUpdate(person); + } + } else { + mMatches.insert(uid, person); + persons << person; + } + } + + if (persons.count() > 0) { + emit personsFound(persons); } } +void PersonSearchJob::updatePersonCollection(const Person &person) +{ + Akonadi::Collection c(person.rootCollection); + CollectionIdentificationAttribute *identification = c.attribute(Akonadi::Entity::AddIfMissing); + + if (person.updateDisplayName) { + Akonadi::EntityDisplayAttribute *displayname = c.attribute(Akonadi::Entity::AddIfMissing); + displayname->setDisplayName(person.name); + } + + //identification->setIdentifier("Other Users/" + person.uid); + identification->setIdentifier(person.name.toUtf8()); + identification->setName(person.name.toUtf8()); + identification->setCollectionNamespace("usertoplevel"); + identification->setMail(person.mail.toUtf8()); + identification->setOu(person.ou.toUtf8()); + + Akonadi::CollectionModifyJob *job = new Akonadi::CollectionModifyJob( c, this ); + connect(job, SIGNAL(result(KJob*)), this, SLOT(modifyResult(KJob*))); +} + void PersonSearchJob::onCollectionsFetched(KJob *job) { if (job->error()) { kWarning() << job->errorString(); } - emitResult(); + mCollectionSearchDone = true; + if (mLdapSearchDone) { + emitResult(); + } } QList PersonSearchJob::matches() const { - return mMatches; + return mMatches.values(); +} + +void PersonSearchJob::modifyResult(KJob *job) +{ + if (job->error()) { + kWarning() << job->errorString(); + return; + } + + const Akonadi::CollectionModifyJob *modifyJob = static_cast(job); + const Akonadi::Collection &col = modifyJob->collection(); + + const CollectionIdentificationAttribute *const attr = col.attribute(); + const Akonadi::EntityDisplayAttribute *const displayname = col.attribute(); + const QString &uid = col.name(); + Person &person = mMatches[col.name()]; + person.rootCollection = col.id(); + person.uid = uid; + if (attr) { + person.ou = QString::fromUtf8(attr->ou()); + person.mail = QString::fromUtf8(attr->mail()); + person.name = QString::fromUtf8(attr->identifier()); + if (!displayname || displayname->displayName().isEmpty() || displayname->displayName() == person.name) { + person.updateDisplayName = true; + } + } + kDebug() << "modified person to" << person.uid << person.name << person.rootCollection; + + mMatches.insert(person.uid, person); + emit personUpdate(person); } Controller::Controller(ReparentingModel* personModel, ReparentingModel* searchModel, QObject* parent) : QObject(parent), mPersonModel(personModel), mSearchModel(searchModel), mCollectionSearchJob(0), mPersonSearchJob(0) { Akonadi::AttributeFactory::registerAttribute(); } 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) { + emit searching(false); return; } + emit searching(true); + mPersonSearchJob = new PersonSearchJob(searchString, this); + connect(mPersonSearchJob, SIGNAL(personsFound(QList)), this, SLOT(onPersonsFound(QList))); + connect(mPersonSearchJob, SIGNAL(personUpdate(Person)), this, SLOT(onPersonUpdate(Person))); 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::onCollectionsFound(KJob* job) { + if (!mPersonSearchJob) { + emit searching(false); + } if (job->error()) { kWarning() << job->errorString(); mCollectionSearchJob = 0; return; } Q_ASSERT(mCollectionSearchJob == static_cast(job)); Q_FOREACH(const Akonadi::Collection &col, mCollectionSearchJob->matchingCollections()) { CollectionNode *collectionNode = new CollectionNode(*mSearchModel, col); collectionNode->isSearchNode = true; //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) +void Controller::onPersonsFound(const QList &persons) { - if (job->error()) { - kWarning() << job->errorString(); - mPersonSearchJob = 0; - return; - } - Q_ASSERT(mPersonSearchJob == static_cast(job)); - Q_FOREACH(const Person &p, mPersonSearchJob->matches()) { + Q_FOREACH(const Person &p, persons) { PersonNode *personNode = new PersonNode(*mSearchModel, p); personNode->isSearchNode = true; //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)); } +} + +void Controller::onPersonUpdate(const Person &person) +{ + PersonNode *personNode = new PersonNode(*mSearchModel, person); + personNode->isSearchNode = true; + mSearchModel->updateNode(ReparentingModel::Node::Ptr(personNode)); +} + +void Controller::onPersonsFound(KJob* job) +{ + if (!mCollectionSearchJob) { + emit searching(false); + } + if (job->error()) { + kWarning() << job->errorString(); + mPersonSearchJob = 0; + return; + } mPersonSearchJob = 0; } static Akonadi::EntityTreeModel *findEtm(QAbstractItemModel *model) { QAbstractProxyModel *proxyModel; while (model) { proxyModel = qobject_cast(model); if (proxyModel && proxyModel->sourceModel()) { model = proxyModel->sourceModel(); } else { break; } } return qobject_cast(model); } void Controller::setCollectionState(const Akonadi::Collection &collection, CollectionState collectionState, bool recursive) { //We removed the children first, so the children in the tree are removed before the parents if (recursive) { //We have to include all mimetypes since mimetypes are not available yet (they will be synced once the collectoins are referenced) Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(collection, Akonadi::CollectionFetchJob::Recursive, this); fetchJob->setProperty("collectionState", static_cast(collectionState)); fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter); connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onPersonCollectionsFetched(KJob*))); } { Akonadi::CollectionFetchJob *fetchJob = new Akonadi::CollectionFetchJob(collection, Akonadi::CollectionFetchJob::Base, this); fetchJob->setProperty("collectionState", static_cast(collectionState)); fetchJob->fetchScope().setListFilter(Akonadi::CollectionFetchScope::NoFilter); connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(onPersonCollectionsFetched(KJob*))); } } void Controller::onPersonCollectionsFetched(KJob* job) { if (job->error()) { kWarning() << "Failed to fetch collections " << job->errorString(); return; } Akonadi::EntityTreeModel *etm = findEtm(mPersonModel); if (!etm) { kWarning() << "Couldn't find etm"; return; } const CollectionState collectionState = static_cast(job->property("collectionState").toInt()); Q_FOREACH(const Akonadi::Collection &col, static_cast(job)->collections()) { // kDebug() << col.displayName() << "do enable " << enabled; Akonadi::Collection modifiedCollection = col; if (collectionState == Enabled) { modifiedCollection.setShouldList(Akonadi::Collection::ListDisplay, true); } if (collectionState == Disabled) { modifiedCollection.setShouldList(Akonadi::Collection::ListDisplay, false); } //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, collectionState == Referenced); } } void Controller::addPerson(const Person &person) { - kDebug() << person.name; + kDebug() << person.uid << person.name << person.rootCollection; + + if (person.rootCollection == -1) { + Baloo::PIM::CollectionQuery query; + query.setNamespace(QStringList() << QLatin1String("usertoplevel")); + query.pathMatches(QLatin1String("/Other Users/")+person.uid); + 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() << "for" << person.name; + //TODO: use the found collection and update attribute + } + PersonNode *personNode = new PersonNode(*mPersonModel, person); personNode->setChecked(true); mPersonModel->addNode(ReparentingModel::Node::Ptr(personNode)); - setCollectionState(Akonadi::Collection(person.rootCollection), Referenced, true); + if (person.rootCollection > -1) { + setCollectionState(Akonadi::Collection(person.rootCollection), Referenced, true); + } else { + kDebug() << "well this only a ldap search object without a collection"; + //TODO: use freebusy data into calendar + } } void Controller::removePerson(const Person &person) { - kDebug() << person.name; + kDebug() << person.uid << person.name << person.rootCollection; mPersonModel->removeNode(PersonNode(*mPersonModel, person)); - setCollectionState(Akonadi::Collection(person.rootCollection), Disabled, true); + if (person.rootCollection > -1) { + setCollectionState(Akonadi::Collection(person.rootCollection), Disabled, true); + } else { + kDebug() << "well this only a ldap search object without a collection"; + //TODO: delete freebusy data from calendar + } } diff --git a/korganizer/views/collectionview/controller.h b/korganizer/views/collectionview/controller.h index a90a851347..916078ec22 100644 --- a/korganizer/views/collectionview/controller.h +++ b/korganizer/views/collectionview/controller.h @@ -1,214 +1,238 @@ /* Copyright (C) 2014 Christian Mollekopf 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 #include #include #include #include "reparentingmodel.h" +#include + enum DataRoles { PersonRole = Akonadi::EntityTreeModel::UserRole + 1, IsSearchResultRole, CollectionRole, NodeTypeRole, EnabledRole }; enum NodeTypeRoles { SourceNodeRole, PersonNodeRole, CollectionNodeRole }; struct Person { - Person(): rootCollection(-1){}; + Person(): rootCollection(-1), updateDisplayName(false) {}; QString name; + QString uid; + QString ou; + QString mail; + bool updateDisplayName; Akonadi::Collection::Id rootCollection; //FIXME not sure we actually require those two QStringList folderPaths; QList 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: 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; bool isSearchNode; 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; bool isSearchNode; 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); void checkSourceIndexRemoval(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 ~PersonSearchJob(); virtual void start(); QList matches() const; +Q_SIGNALS: + void personsFound(const QList &persons); + void personUpdate(const Person &person); + +public Q_SLOTS: + bool kill(KillVerbosity verbosity=Quietly); + private Q_SLOTS: void onCollectionsReceived(const Akonadi::Collection::List &); void onCollectionsFetched(KJob *); + void onLDAPSearchData(const QList &); + void onLDAPSearchDone(); + void updatePersonCollection(const Person &person); + void modifyResult(KJob *job); private: QString mSearchString; - QList mMatches; + QHash mMatches; + KLDAP::LdapClientSearch mLdapSearch; + bool mCollectionSearchDone; + bool mLdapSearchDone; }; /** * 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); enum CollectionState { Disabled, Referenced, Enabled }; void setCollectionState(const Akonadi::Collection &collection, CollectionState collectionState, bool recursive = false); void addPerson(const Person &person); void removePerson(const Person &person); Q_SIGNALS: void searchIsActive(bool); + void searching(bool); public Q_SLOTS: void setSearchString(const QString &); private Q_SLOTS: void onCollectionsFound(KJob *job); + void onPersonsFound(const QList &persons); + void onPersonUpdate(const Person &person); void onPersonsFound(KJob *job); void onPersonCollectionsFetched(KJob *job); private: ReparentingModel *mPersonModel; ReparentingModel *mSearchModel; CollectionSearchJob *mCollectionSearchJob; PersonSearchJob *mPersonSearchJob; }; #endif diff --git a/korganizer/views/collectionview/reparentingmodel.cpp b/korganizer/views/collectionview/reparentingmodel.cpp index 6e73df0b1e..824f31c9fc 100644 --- a/korganizer/views/collectionview/reparentingmodel.cpp +++ b/korganizer/views/collectionview/reparentingmodel.cpp @@ -1,821 +1,847 @@ /* Copyright (C) 2014 Christian Mollekopf 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 /* * 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) : sourceIndex(srcIndex), parent(p), 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 QVector::iterator 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) { //We have to make this check before issuing the async method, //otherwise we run into the problem that while a node is being removed, //the async request could be triggered (due to a changed signal), //resulting in the node getting readded immediately after it had been removed. Q_FOREACH(const ReparentingModel::Node::Ptr &existing, mProxyNodes) { if (*existing == *node) { // kDebug() << "node is already existing"; return; } } mNodesToAdd << node; qRegisterMetaType("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; } } //If a datachanged call triggered this through checkSourceIndex, right after a person node has been removed. //We'd end-up re-inserting the node that has just been removed. Therefore removeNode can cancel the pending addNode //call through mNodesToAdd. bool addNodeAborted = true; for (int i = 0; i < mNodesToAdd.size(); i++) { if (*mNodesToAdd.at(i) == *node) { mNodesToAdd.remove(i); addNodeAborted = false; break; } } if (addNodeAborted) { return; } if (!isDuplicate(node)) { const int targetRow = mRootNode.children.size(); beginInsertRows(QModelIndex(), targetRow, targetRow); mProxyNodes << node; insertProxyNode(node); endInsertRows(); reparentSourceNodes(node); } else { mProxyNodes << node; } } +void ReparentingModel::updateNode(const ReparentingModel::Node::Ptr &node) +{ + Q_FOREACH(const ReparentingModel::Node::Ptr &existing, mProxyNodes) { + if (*existing == *node) { + node->parent = existing->parent; + int r = row(existing.data()); + existing->parent->children.replace(r, node); + const QModelIndex i = index(node.data()); + Q_ASSERT(i.row() == r); + emit dataChanged(i, i); + return; + } + } + + kWarning() << "no node to update"; +} + void ReparentingModel::removeNode(const ReparentingModel::Node& node) { //If there is an addNode in progress for that node, abort it. for (int i = 0; i < mNodesToAdd.size(); i++) { if (*mNodesToAdd.at(i) == node) { mNodesToAdd.remove(i); } } for (int i = 0; i < mProxyNodes.size(); i++) { if (*mProxyNodes.at(i) == node) { //TODO: this does not yet take care of un-reparenting reparented nodes. const Node &n = *mProxyNodes.at(i); Node *parentNode = n.parent; beginRemoveRows(index(parentNode), n.row(), n.row()); parentNode->children.remove(n.row()); //deletes node mProxyNodes.remove(i); endRemoveRows(); break; } } } void ReparentingModel::setNodes(const QList &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); //If the indexes have already been removed (e.g. by removeNode)this can indeed return an invalid index if (proxyIndex.isValid()) { 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(); } } //Allows the node manager to remove nodes that are no longer relevant for (int row = start; row <= end; row++) { mNodeManager->checkSourceIndexRemoval(sourceModel()->index(row, 0, parent)); } } 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(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") //Also QAbstractItemModel::match does all kinds of weird shit including passing row=-1 if (parentNode->children.size() <= row || row < 0) { return QModelIndex(); } 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); } } bool ReparentingModel::isDuplicate(const Node::Ptr &proxyNode) const { Q_FOREACH(const Node *n, mSourceNodes) { // kDebug() << index << index.data().toString(); if (proxyNode->isDuplicateOf(n->sourceIndex)) { return true; } } return false; } void ReparentingModel::insertProxyNode(const Node::Ptr &proxyNode) { // kDebug() << "checking " << proxyNode->data(Qt::DisplayRole).toString(); proxyNode->parent = &mRootNode; mRootNode.addChild(proxyNode); Q_ASSERT(validateNode(proxyNode.data())); } void ReparentingModel::reparentSourceNodes(const Node::Ptr &proxyNode) { //Reparent source nodes according to the provided rules Q_FOREACH(Node *n, mSourceNodes) { if (proxyNode->adopts(n->sourceIndex)) { const int oldRow = n->sourceIndex.row(); beginRemoveRows(index(n->parent), oldRow, oldRow); //We lie about the row being removed already, but the view can deal with that better than if we call endRemoveRows after beginInsertRows endRemoveRows(); const int newRow = proxyNode->children.size(); beginInsertRows(index(proxyNode.data()), newRow, newRow); proxyNode->reparent(n); endInsertRows(); Q_ASSERT(validateNode(n)); } } } 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 if (isDuplicate(proxyNode)) { continue; } insertProxyNode(proxyNode); reparentSourceNodes(proxyNode); } } 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 +int ReparentingModel::row(ReparentingModel::Node *node) const { Q_ASSERT(node); if (node == &mRootNode) { - return QModelIndex(); + return -1; } Q_ASSERT(validateNode(node)); int row = 0; Q_FOREACH(const Node::Ptr &c, node->parent->children) { if (c.data() == node) { - break; + return row; } row++; } - return createIndex(row, 0, node); + return -1; +} + +QModelIndex ReparentingModel::index(Node *node) const +{ + const int r = row(node); + if (r < 0) { + return QModelIndex(); + } + return createIndex(r, 0, node); } QModelIndex ReparentingModel::parent(const QModelIndex& child) const { // kDebug() << child << child.data().toString(); 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 index ad73ba8f3a..4f88c84846 100644 --- a/korganizer/views/collectionview/reparentingmodel.h +++ b/korganizer/views/collectionview/reparentingmodel.h @@ -1,148 +1,150 @@ /* Copyright (C) 2014 Christian Mollekopf 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 #include #include /** * 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 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 children; Node *parent; ReparentingModel &personModel; bool mIsSourceNode; }; struct NodeManager { typedef QSharedPointer 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 */){}; virtual void checkSourceIndexRemoval(const QModelIndex &/* sourceIndex */){}; }; public: explicit ReparentingModel(QObject* parent = 0); virtual ~ReparentingModel(); void setNodeManager(const NodeManager::Ptr &nodeManager); void addNode(const Node::Ptr &node); + void updateNode(const Node::Ptr &node); void removeNode(const Node &node); void setNodes(const QList &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()); bool isDuplicate(const Node::Ptr &proxyNode) const; void insertProxyNode(const Node::Ptr &proxyNode); void reparentSourceNodes(const Node::Ptr &proxyNode); void rebuildAll(); QModelIndex index(Node *node) const; + int row(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 mSourceNodes; QVector mProxyNodes; QVector mNodesToAdd; NodeManager::Ptr mNodeManager; // QModelIndexList mLayoutChangedProxyIndexes; // QList mLayoutChangedSourcePersistentModelIndexes; }; #endif diff --git a/korganizer/views/collectionview/tests/reparentingmodeltest.cpp b/korganizer/views/collectionview/tests/reparentingmodeltest.cpp index 4fe7dbbeea..f7ddd70574 100644 --- a/korganizer/views/collectionview/tests/reparentingmodeltest.cpp +++ b/korganizer/views/collectionview/tests/reparentingmodeltest.cpp @@ -1,635 +1,679 @@ /* Copyright (C) 2014 Christian Mollekopf 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 #include #include #include #include #include #include "reparentingmodel.h" class DummyNode : public ReparentingModel::Node { public: - DummyNode(ReparentingModel &personModel, const QString &name) + DummyNode(ReparentingModel &personModel, const QString &name, const QString &data=QString()) : ReparentingModel::Node(personModel), - mName(name) + mName(name), + mData(data) {} virtual ~DummyNode(){}; virtual bool operator==(const Node &node) const { const DummyNode *dummyNode = dynamic_cast(&node); if (dummyNode) { return (dummyNode->mName == mName); } return false; } private: virtual QVariant data(int role) const { if (role == Qt::DisplayRole) { return mName; + } else if (role == Qt::UserRole) { + return mData; } return QVariant(); } virtual bool setData(const QVariant& variant, int role){ + Q_UNUSED(variant); + Q_UNUSED(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; + QString mData; }; 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; + QModelIndex topLeft, bottomRight; 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) { + void onDataChanged(QModelIndex t,QModelIndex b) { mSignals << QLatin1String("dataChanged"); + topLeft = t; + bottomRight = b; } 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 testUpdateNode(); void testReparent(); void testReparentResetWithoutCrash(); void testAddReparentedSourceItem(); void testRemoveReparentedSourceItem(); void testNestedReparentedSourceItem(); void testAddNestedReparentedSourceItem(); void testSourceDataChanged(); void testSourceLayoutChanged(); void testInvalidLayoutChanged(); void testAddRemoveNodeByNodeManager(); void testRemoveNodeByNodeManagerWithDataChanged(); }; 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("rowsInserted") << QLatin1String("rowsRemoved")); } 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 } +/** + * updateNode should update the node datas + */ +void ReparentingModelTest::testUpdateNode() +{ + QStandardItemModel sourceModel; + ReparentingModel reparentingModel; + reparentingModel.setSourceModel(&sourceModel); + reparentingModel.addNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"), QLatin1String("blub")))); + + QTest::qWait(0); + + QModelIndex index = getIndex("proxy1", reparentingModel); + QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1); + QVERIFY(index.isValid()); + QCOMPARE(reparentingModel.data(index,Qt::UserRole).toString(), QLatin1String("blub")); + + ModelSignalSpy spy(reparentingModel); + reparentingModel.updateNode(ReparentingModel::Node::Ptr(new DummyNode(reparentingModel, QLatin1String("proxy1"), QLatin1String("new data")))); + QTest::qWait(0); + + QModelIndex i2 = getIndex("proxy1", reparentingModel); + QCOMPARE(i2.column(), index.column()); + QCOMPARE(i2.row(), index.row()); + + QCOMPARE(spy.mSignals.count(), 1); + QCOMPARE(spy.mSignals.takeLast(),QLatin1String("dataChanged")); + QCOMPARE(spy.topLeft, i2); + QCOMPARE(spy.bottomRight, i2); + + QCOMPARE(reparentingModel.rowCount(QModelIndex()), 1); + QCOMPARE(reparentingModel.data(i2,Qt::UserRole).toString(), QLatin1String("new data")); +} + 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() << 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() << 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()); } class DummyNodeManager : public ReparentingModel::NodeManager { public: DummyNodeManager(ReparentingModel &m) : ReparentingModel::NodeManager(m){}; private: void checkSourceIndex(const QModelIndex &sourceIndex) { if (sourceIndex.data().toString() == QLatin1String("personfolder")) { model.addNode(ReparentingModel::Node::Ptr(new DummyNode(model, QLatin1String("personnode")))); } } void checkSourceIndexRemoval(const QModelIndex &sourceIndex) { if (sourceIndex.data().toString() == QLatin1String("personfolder")) { model.removeNode(DummyNode(model, QLatin1String("personnode"))); } } }; void ReparentingModelTest::testAddRemoveNodeByNodeManager() { QStandardItemModel sourceModel; sourceModel.appendRow(new QStandardItem(QLatin1String("personfolder"))); ReparentingModel reparentingModel; reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel))); reparentingModel.setSourceModel(&sourceModel); QTest::qWait(0); QVERIFY(getIndex("personnode", reparentingModel).isValid()); QVERIFY(getIndex("personfolder", reparentingModel).isValid()); sourceModel.removeRows(0, 1, QModelIndex()); QTest::qWait(0); QVERIFY(!getIndex("personnode", reparentingModel).isValid()); QVERIFY(!getIndex("personfolder", reparentingModel).isValid()); } /* * This tests a special case that is caused by the delayed doAddNode call, * causing a removed node to be readded immediately if it's removed while * a doAddNode call is pending (that can be triggered by dataChanged). */ void ReparentingModelTest::testRemoveNodeByNodeManagerWithDataChanged() { QStandardItemModel sourceModel; QStandardItem *item = new QStandardItem(QLatin1String("personfolder")); sourceModel.appendRow(item); ReparentingModel reparentingModel; reparentingModel.setNodeManager(ReparentingModel::NodeManager::Ptr(new DummyNodeManager(reparentingModel))); reparentingModel.setSourceModel(&sourceModel); QTest::qWait(0); QVERIFY(getIndex("personnode", reparentingModel).isValid()); QVERIFY(getIndex("personfolder", reparentingModel).isValid()); //Trigger data changed item->setStatusTip(QLatin1String("sldkfjlfsj")); sourceModel.removeRows(0, 1, QModelIndex()); QTest::qWait(0); QVERIFY(!getIndex("personnode", reparentingModel).isValid()); QVERIFY(!getIndex("personfolder", reparentingModel).isValid()); } QTEST_MAIN(ReparentingModelTest) #include "reparentingmodeltest.moc"