/* Copyright (c) 2009 Kevin Ottens 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. */ #include "widget.h" #include #include #include #include #include "storagemodel.h" #include "core/messageitem.h" #include "core/view.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/groupheaderitem.h" #include #include #include #include #include namespace MessageList { class Widget::Private { public: Private( Widget *owner ) : q( owner ), mLastSelectedMessage(-1), mXmlGuiClient( 0 ), mMonitor( 0 ) { } Akonadi::Item::List selectionAsItems() const; Akonadi::Item itemForRow( int row ) const; KMime::Message::Ptr messageForRow( int row ) const; Widget * const q; int mLastSelectedMessage; KXMLGUIClient *mXmlGuiClient; QModelIndex mGroupHeaderItemIndex; Akonadi::Monitor *mMonitor; }; } // namespace MessageList using namespace MessageList; using namespace Akonadi; Widget::Widget( QWidget *parent ) : Core::Widget( parent ), d( new Private( this ) ) { populateStatusFilterCombo(); d->mMonitor = new Akonadi::Monitor( this ); d->mMonitor->setTypeMonitored( Akonadi::Monitor::Tags ); connect(d->mMonitor, SIGNAL(tagAdded(Akonadi::Tag)), this, SLOT(populateStatusFilterCombo())); connect(d->mMonitor, SIGNAL(tagRemoved(Akonadi::Tag)), this, SLOT(populateStatusFilterCombo())); connect(d->mMonitor, SIGNAL(tagChanged(Akonadi::Tag)), this, SLOT(populateStatusFilterCombo())); } Widget::~Widget() { delete d; } void Widget::setXmlGuiClient( KXMLGUIClient *xmlGuiClient ) { d->mXmlGuiClient = xmlGuiClient; } bool Widget::canAcceptDrag( const QDropEvent * e ) { if ( e->source() == view()->viewport() ) return false; Collection::List collections = static_cast( storageModel() )->displayedCollections(); if ( collections.size()!=1 ) return false; // no folder here or too many (in case we can't decide where the drop will end) const Collection target = collections.first(); if ( ( target.rights() & Collection::CanCreateItem ) == 0 ) return false; // no way to drag into const KUrl::List urls = KUrl::List::fromMimeData( e->mimeData() ); foreach ( const KUrl &url, urls ) { const Collection collection = Collection::fromUrl( url ); if ( collection.isValid() ) { // You're not supposed to drop collections here return false; } else { // Yay, this is an item! const QString type = url.queryItems()[QLatin1String( "type" )]; // But does it have the right type? if ( !target.contentMimeTypes().contains( type ) ) { return false; } } } return true; } bool Widget::selectNextMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop ) { return view()->selectNextMessageItem( messageTypeFilter, existingSelectionBehaviour, centerItem, loop ); } bool Widget::selectPreviousMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter, MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop ) { return view()->selectPreviousMessageItem( messageTypeFilter, existingSelectionBehaviour, centerItem, loop ); } bool Widget::focusNextMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop ) { return view()->focusNextMessageItem( messageTypeFilter, centerItem, loop ); } bool Widget::focusPreviousMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem, bool loop ) { return view()->focusPreviousMessageItem( messageTypeFilter, centerItem, loop ); } void Widget::selectFocusedMessageItem( bool centerItem ) { view()->selectFocusedMessageItem( centerItem ); } bool Widget::selectFirstMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter, bool centerItem ) { return view()->selectFirstMessageItem( messageTypeFilter, centerItem ); } bool Widget::selectLastMessageItem( Core::MessageTypeFilter messageTypeFilter, bool centerItem ) { return view()->selectLastMessageItem( messageTypeFilter, centerItem ); } void Widget::selectAll() { view()->setAllGroupsExpanded( true ); view()->selectAll(); } void Widget::setCurrentThreadExpanded( bool expand ) { view()->setCurrentThreadExpanded(expand ); } void Widget::setAllThreadsExpanded( bool expand ) { view()->setAllThreadsExpanded( expand ); } void Widget::setAllGroupsExpanded( bool expand ) { view()->setAllGroupsExpanded(expand); } void Widget::focusQuickSearch(const QString &selectedText) { view()->focusQuickSearch(selectedText); } void Widget::setQuickSearchClickMessage(const QString &msg) { view()->setQuickSearchClickMessage(msg); } void Widget::fillMessageTagCombo() { Akonadi::TagFetchJob *fetchJob = new Akonadi::TagFetchJob(this); fetchJob->fetchScope().fetchAttribute(); connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(slotTagsFetched(KJob*))); } void Widget::slotTagsFetched(KJob *job) { if (job->error()) { kWarning() << "Failed to load tags " << job->errorString(); return; } Akonadi::TagFetchJob *fetchJob = static_cast(job); KConfigGroup conf( MessageList::Core::Settings::self()->config(),"MessageListView"); const QString tagSelected= conf.readEntry(QLatin1String("TagSelected")); if(tagSelected.isEmpty()) { setCurrentStatusFilterItem(); return; } const QStringList tagSelectedLst = tagSelected.split(QLatin1Char(',')); addMessageTagItem( SmallIcon( QLatin1String( "mail-flag" ) ), i18n( "All" ), QString() ); QStringList tagFound; foreach( const Akonadi::Tag &akonadiTag, fetchJob->tags() ) { if(tagSelectedLst.contains(akonadiTag.url().url())) { tagFound.append(akonadiTag.url().url()); QString iconName = QLatin1String( "mail-tagged" ); const QString label = akonadiTag.name(); const QString id = akonadiTag.url().url(); Akonadi::TagAttribute *attr = akonadiTag.attribute(); if (attr) { iconName = attr->iconName(); } addMessageTagItem( SmallIcon( iconName ), label, QVariant( id ) ); } } conf.writeEntry(QLatin1String("TagSelected"), tagFound); conf.sync(); setCurrentStatusFilterItem(); } void Widget::viewMessageSelected( MessageList::Core::MessageItem *msg ) { int row = -1; if ( msg ) { row = msg->currentModelIndexRow(); } if ( !msg || !msg->isValid() || !storageModel() ) { d->mLastSelectedMessage = -1; emit messageSelected( Item() ); return; } Q_ASSERT( row >= 0 ); d->mLastSelectedMessage = row; emit messageSelected( d->itemForRow( row ) ); // this MAY be null } void Widget::viewMessageActivated( MessageList::Core::MessageItem *msg ) { Q_ASSERT( msg ); // must not be null Q_ASSERT( storageModel() ); if ( !msg->isValid() ) { return; } int row = msg->currentModelIndexRow(); Q_ASSERT( row >= 0 ); // The assert below may fail when quickly opening and closing a non-selected thread. // This will actually activate the item without selecting it... //Q_ASSERT( d->mLastSelectedMessage == row ); if ( d->mLastSelectedMessage != row ) { // Very ugly. We are activating a non selected message. // This is very likely a double click on the plus sign near a thread leader. // Dealing with mLastSelectedMessage here would be expensive: it would involve releasing the last selected, // emitting signals, handling recursion... ugly. // We choose a very simple solution: double clicking on the plus sign near a thread leader does // NOT activate the message (i.e open it in a toplevel window) if it isn't previously selected. return; } emit messageActivated( d->itemForRow( row ) ); // this MAY be null } void Widget::viewSelectionChanged() { emit selectionChanged(); if ( !currentMessageItem() ) { emit messageSelected( Item() ); } } void Widget::viewMessageListContextPopupRequest( const QList< MessageList::Core::MessageItem * > &selectedItems, const QPoint &globalPos ) { Q_UNUSED( selectedItems ); if ( !d->mXmlGuiClient ) return; QMenu *popup = static_cast( d->mXmlGuiClient->factory()->container( QLatin1String( "akonadi_messagelist_contextmenu" ), d->mXmlGuiClient ) ); if ( popup ) { popup->exec( globalPos ); } } void Widget::viewMessageStatusChangeRequest( MessageList::Core::MessageItem *msg, const Akonadi::MessageStatus &set, const Akonadi::MessageStatus &clear ) { Q_ASSERT( msg ); // must not be null Q_ASSERT( storageModel() ); if ( !msg->isValid() ) { return; } int row = msg->currentModelIndexRow(); Q_ASSERT( row >= 0 ); Item item = d->itemForRow( row ); Q_ASSERT( item.isValid() ); emit messageStatusChangeRequest( item, set, clear ); } void Widget::viewGroupHeaderContextPopupRequest( MessageList::Core::GroupHeaderItem *ghi, const QPoint &globalPos ) { Q_UNUSED( ghi ); KMenu menu( this ); QAction *act; QModelIndex index = view()->model()->index( ghi, 0 ); d->mGroupHeaderItemIndex = index; if ( view()->isExpanded( index ) ) { act = menu.addAction( i18n ( "Collapse Group" ) ); connect( act, SIGNAL(triggered(bool)), this, SLOT(slotCollapseItem()) ); } else { act = menu.addAction( i18n ( "Expand Group" ) ); connect( act, SIGNAL(triggered(bool)), this, SLOT(slotExpandItem()) ); } menu.addSeparator(); act = menu.addAction( i18n( "Expand All Groups" ) ); connect( act, SIGNAL(triggered(bool)), view(), SLOT(slotExpandAllGroups()) ); act = menu.addAction( i18n( "Collapse All Groups" ) ); connect( act, SIGNAL(triggered(bool)), view(), SLOT(slotCollapseAllGroups()) ); menu.exec( globalPos ); } void Widget::viewDragEnterEvent( QDragEnterEvent *e ) { if ( !canAcceptDrag( e ) ) { e->ignore(); return; } e->accept(); } void Widget::viewDragMoveEvent( QDragMoveEvent *e ) { if ( !canAcceptDrag( e ) ) { e->ignore(); return; } e->accept(); } enum DragMode { DragCopy, DragMove, DragCancel }; void Widget::viewDropEvent( QDropEvent *e ) { if ( !canAcceptDrag( e ) ) { e->ignore(); return; } KUrl::List urls = KUrl::List::fromMimeData( e->mimeData() ); if ( urls.isEmpty() ) { kWarning() << "Could not decode drag data!"; e->ignore(); return; } e->accept(); int action; if ( ( e->possibleActions() & Qt::MoveAction ) == 0 ) { // We can't move anyway action = DragCopy; } else { action = DragCancel; int keybstate = QApplication::keyboardModifiers(); if ( keybstate & Qt::CTRL ) { action = DragCopy; } else if ( keybstate & Qt::SHIFT ) { action = DragMove; } else { KMenu menu; QAction *moveAction = menu.addAction( KIcon( QLatin1String( "go-jump" )), i18n( "&Move Here" ) ); QAction *copyAction = menu.addAction( KIcon( QLatin1String( "edit-copy" ) ), i18n( "&Copy Here" ) ); menu.addSeparator(); menu.addAction( KIcon( QLatin1String( "dialog-cancel" ) ), i18n( "C&ancel" ) ); QAction *menuChoice = menu.exec( QCursor::pos() ); if ( menuChoice == moveAction ) { action = DragMove; } else if ( menuChoice == copyAction ) { action = DragCopy; } else { action = DragCancel; } } } if ( action == DragCancel ) return; Collection::List collections = static_cast( storageModel() )->displayedCollections(); Collection target = collections.first(); Item::List items; foreach ( const KUrl &url, urls ) { items << Item::fromUrl( url ); } if ( action == DragCopy ) { new ItemCopyJob( items, target, this ); } else if ( action == DragMove ) { new ItemMoveJob( items, target, this ); } } void Widget::viewStartDragRequest() { Collection::List collections = static_cast( storageModel() )->displayedCollections(); if ( collections.isEmpty() ) return; // no folder here QList selection = view()->selectionAsMessageItemList(); if ( selection.isEmpty() ) { return; } bool readOnly = false; foreach ( const Collection &c, collections ) { // We won't be able to remove items from this collection if ( ( c.rights() & Collection::CanDeleteItem ) == 0 ) { // So the drag will be read-only readOnly = true; break; } } KUrl::List urls; Q_FOREACH ( Core::MessageItem *mi, selection ) { const Item i = d->itemForRow( mi->currentModelIndexRow() ); KUrl url = i.url( Item::Item::Item::UrlWithMimeType ); url.addQueryItem(QLatin1String("parent"), QString::number( mi->parentCollectionId() ) ); urls << url; } QMimeData *mimeData = new QMimeData; urls.populateMimeData( mimeData ); QDrag *drag = new QDrag( view()->viewport() ); drag->setMimeData( mimeData ); // Set pixmap QPixmap pixmap; if( selection.size() == 1 ) { pixmap = QPixmap( DesktopIcon(QLatin1String( "mail-message" ), KIconLoader::SizeSmall) ); } else { pixmap = QPixmap( DesktopIcon(QLatin1String( "document-multiple" ), KIconLoader::SizeSmall) ); } // Calculate hotspot (as in Konqueror) if( !pixmap.isNull() ) { drag->setHotSpot( QPoint( pixmap.width() / 2, pixmap.height() / 2 ) ); drag->setPixmap( pixmap ); } if ( readOnly ) drag->exec( Qt::CopyAction ); else drag->exec( Qt::CopyAction | Qt::MoveAction ); } Item::List Widget::Private::selectionAsItems() const { Item::List res; QList selection = q->view()->selectionAsMessageItemList(); foreach ( Core::MessageItem *mi, selection ) { Item i = itemForRow( mi->currentModelIndexRow() ); Q_ASSERT( i.isValid() ); res << i; } return res; } Item Widget::Private::itemForRow( int row ) const { return static_cast( q->storageModel() )->itemForRow( row ); } KMime::Message::Ptr Widget::Private::messageForRow( int row ) const { return static_cast( q->storageModel() )->messageForRow( row ); } Item Widget::currentItem() const { Core::MessageItem *mi = view()->currentMessageItem(); if ( mi == 0 ) { return Item(); } return d->itemForRow( mi->currentModelIndexRow() ); } KMime::Message::Ptr Widget::currentMessage() const { Core::MessageItem *mi = view()->currentMessageItem(); if ( mi == 0 ) { return KMime::Message::Ptr(); } return d->messageForRow( mi->currentModelIndexRow() ); } QList Widget::selectionAsMessageList( bool includeCollapsedChildren ) const { QList lstMiPtr; QList lstMi = view()->selectionAsMessageItemList( includeCollapsedChildren ); if ( lstMi.isEmpty() ) { return lstMiPtr; } foreach( Core::MessageItem *it, lstMi ) { lstMiPtr.append( d->messageForRow( it->currentModelIndexRow() ) ); } return lstMiPtr; } QList Widget::selectionAsMessageItemList( bool includeCollapsedChildren ) const { QList lstMiPtr; QList lstMi = view()->selectionAsMessageItemList( includeCollapsedChildren ); if ( lstMi.isEmpty() ) { return lstMiPtr; } foreach( Core::MessageItem *it, lstMi ) { lstMiPtr.append( d->itemForRow( it->currentModelIndexRow() ) ); } return lstMiPtr; } QVector Widget::selectionAsMessageItemListId( bool includeCollapsedChildren ) const { QVector lstMiPtr; QList lstMi = view()->selectionAsMessageItemList( includeCollapsedChildren ); if ( lstMi.isEmpty() ) { return lstMiPtr; } foreach( Core::MessageItem *it, lstMi ) { lstMiPtr.append( d->itemForRow( it->currentModelIndexRow() ).id() ); } return lstMiPtr; } QList Widget::selectionAsListMessageId( bool includeCollapsedChildren ) const { QList lstMiPtr; QList lstMi = view()->selectionAsMessageItemList( includeCollapsedChildren ); if ( lstMi.isEmpty() ) { return lstMiPtr; } foreach( Core::MessageItem *it, lstMi ) { lstMiPtr.append( d->itemForRow( it->currentModelIndexRow() ).id() ); } return lstMiPtr; } QList Widget::currentThreadAsMessageList() const { QList lstMiPtr; QList lstMi = view()->currentThreadAsMessageItemList(); if ( lstMi.isEmpty() ) { return lstMiPtr; } foreach( Core::MessageItem *it, lstMi ) { lstMiPtr.append( d->itemForRow( it->currentModelIndexRow() ) ); } return lstMiPtr; } QList Widget::currentFilterStatus() const { return view()->currentFilterStatus(); } QString Widget::currentFilterSearchString() const { return view()->currentFilterSearchString(); } bool Widget::isThreaded() const { return view()->isThreaded(); } bool Widget::selectionEmpty() const { return view()->selectionEmpty(); } bool Widget::getSelectionStats( Akonadi::Item::List &selectedItems, Akonadi::Item::List &selectedVisibleItems, bool * allSelectedBelongToSameThread, bool includeCollapsedChildren ) const { if ( !storageModel() ) return false; selectedItems.clear(); selectedVisibleItems.clear(); QList< Core::MessageItem * > selected = view()->selectionAsMessageItemList( includeCollapsedChildren ); Core::MessageItem * topmost = 0; *allSelectedBelongToSameThread = true; foreach( Core::MessageItem *it, selected ) { const Item item = d->itemForRow( it->currentModelIndexRow() ); selectedItems.append( item ); if ( view()->isDisplayedWithParentsExpanded( it ) ) selectedVisibleItems.append( item ); if ( topmost == 0 ) topmost = ( *it ).topmostMessage(); else { if ( topmost != ( *it ).topmostMessage() ) *allSelectedBelongToSameThread = false; } } return true; } void Widget::deletePersistentSet( MessageList::Core::MessageItemSetReference ref ) { view()->deletePersistentSet( ref ); } void Widget::markMessageItemsAsAboutToBeRemoved( MessageList::Core::MessageItemSetReference ref, bool bMark ) { QList< Core::MessageItem * > lstPersistent = view()->persistentSetCurrentMessageItemList( ref ); if ( !lstPersistent.isEmpty() ) view()->markMessageItemsAsAboutToBeRemoved( lstPersistent, bMark ); } QList Widget::itemListFromPersistentSet( MessageList::Core::MessageItemSetReference ref ) { QList lstItem; QList< Core::MessageItem * > refList = view()->persistentSetCurrentMessageItemList( ref ); if ( !refList.isEmpty() ) { foreach( Core::MessageItem *it, refList ) { lstItem.append( d->itemForRow( it->currentModelIndexRow() ) ); } } return lstItem; } MessageList::Core::MessageItemSetReference Widget::selectionAsPersistentSet( bool includeCollapsedChildren ) const { QList lstMi = view()->selectionAsMessageItemList( includeCollapsedChildren ); if ( lstMi.isEmpty() ) { return -1; } return view()->createPersistentSet( lstMi ); } MessageList::Core::MessageItemSetReference Widget::currentThreadAsPersistentSet() const { QList lstMi = view()->currentThreadAsMessageItemList(); if ( lstMi.isEmpty() ) { return -1; } return view()->createPersistentSet( lstMi ); } Akonadi::Collection Widget::currentCollection() const { Collection::List collections = static_cast( storageModel() )->displayedCollections(); if ( collections.size()!=1 ) return Akonadi::Collection(); // no folder here or too many (in case we can't decide where the drop will end) return collections.first(); } void Widget::slotCollapseItem() { view()->setCollapseItem(d->mGroupHeaderItemIndex); } void Widget::slotExpandItem() { view()->setExpandItem(d->mGroupHeaderItemIndex); }