2 Copyright (c) 2009 Kevin Ottens <ervin@kde.org>
4 This program is free software; you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include <akonadi/collection.h>
22 #include <akonadi/item.h>
23 #include <akonadi/itemcopyjob.h>
24 #include <akonadi/itemmovejob.h>
26 #include "storagemodel.h"
27 #include "core/messageitem.h"
28 #include "core/view.h"
30 #include <QtCore/QTimer>
31 #include <QtGui/QAction>
32 #include <QtGui/QApplication>
33 #include <QtGui/QDrag>
34 #include <QtGui/QDragMoveEvent>
35 #include <QtGui/QDropEvent>
39 #include <KDE/KIconLoader>
40 #include <KDE/KLocale>
42 #include <KDE/KXMLGUIClient>
43 #include <KDE/KXMLGUIFactory>
44 #include <KDE/KComboBox>
46 #include <Nepomuk/Tag>
54 Private( Widget
*owner
)
55 : q( owner
), mXmlGuiClient( 0 ) { }
57 Akonadi::Item::List
selectionAsItems() const;
58 Akonadi::Item
itemForRow( int row
) const;
59 KMime::Message::Ptr
messageForRow( int row
) const;
63 int mLastSelectedMessage
;
64 KXMLGUIClient
*mXmlGuiClient
;
67 } // namespace MessageList
69 using namespace MessageList
;
70 using namespace Akonadi
;
72 Widget::Widget( QWidget
*parent
)
73 : Core::Widget( parent
), d( new Private( this ) )
75 populateStatusFilterCombo();
83 void Widget::setXmlGuiClient( KXMLGUIClient
*xmlGuiClient
)
85 d
->mXmlGuiClient
= xmlGuiClient
;
88 bool Widget::canAcceptDrag( const QDropEvent
* e
)
90 Collection::List collections
= static_cast<const StorageModel
*>( storageModel() )->displayedCollections();
92 if ( collections
.size()!=1 )
93 return false; // no folder here or too many (in case we can't decide where the drop will end)
95 const Collection target
= collections
.first();
97 if ( ( target
.rights() & Collection::CanCreateItem
) == 0 )
98 return false; // no way to drag into
100 const KUrl::List urls
= KUrl::List::fromMimeData( e
->mimeData() );
101 foreach ( const KUrl
&url
, urls
) {
102 const Collection collection
= Collection::fromUrl( url
);
103 if ( collection
.isValid() ) { // You're not supposed to drop collections here
105 } else { // Yay, this is an item!
106 const QString type
= url
.queryItems()["type"]; // But does it have the right type?
107 if ( !target
.contentMimeTypes().contains( type
) ) {
116 bool Widget::selectNextMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter
,
117 MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour
,
121 return view()->selectNextMessageItem( messageTypeFilter
, existingSelectionBehaviour
, centerItem
, loop
);
124 bool Widget::selectPreviousMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter
,
125 MessageList::Core::ExistingSelectionBehaviour existingSelectionBehaviour
,
129 return view()->selectPreviousMessageItem( messageTypeFilter
, existingSelectionBehaviour
, centerItem
, loop
);
132 bool Widget::focusNextMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter
, bool centerItem
, bool loop
)
134 return view()->focusNextMessageItem( messageTypeFilter
, centerItem
, loop
);
137 bool Widget::focusPreviousMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter
, bool centerItem
, bool loop
)
139 return view()->focusNextMessageItem( messageTypeFilter
, centerItem
, loop
);
142 void Widget::selectFocusedMessageItem( bool centerItem
)
144 view()->selectFocusedMessageItem( centerItem
);
147 bool Widget::selectFirstMessageItem( MessageList::Core::MessageTypeFilter messageTypeFilter
, bool centerItem
)
149 return view()->selectFirstMessageItem( messageTypeFilter
, centerItem
);
152 void Widget::selectAll()
154 view()->setAllGroupsExpanded( true );
158 void Widget::setCurrentThreadExpanded( bool expand
)
160 view()->setCurrentThreadExpanded(expand
);
163 void Widget::setAllThreadsExpanded( bool expand
)
165 view()->setAllThreadsExpanded( expand
);
168 void Widget::setAllGroupsExpanded( bool expand
)
170 view()->setAllGroupsExpanded(expand
);
173 void Widget::focusQuickSearch()
175 view()->focusQuickSearch();
179 void Widget::fillMessageTagCombo( KComboBox
* combo
)
181 foreach( const Nepomuk::Tag
&nepomukTag
, Nepomuk::Tag::allTags() ) {
182 QString iconName
= "mail-tagged";
183 const QString label
= nepomukTag
.label();
184 if ( !nepomukTag
.symbols().isEmpty() )
185 iconName
= nepomukTag
.symbols().first();
186 const QString id
= nepomukTag
.resourceUri().toString();
187 combo
->addItem( SmallIcon( iconName
), label
, QVariant( id
) );
191 void Widget::viewMessageSelected( MessageList::Core::MessageItem
*msg
)
195 row
= msg
->currentModelIndexRow();
198 if ( !msg
|| !msg
->isValid() || !storageModel() ) {
199 d
->mLastSelectedMessage
= -1;
200 emit
messageSelected( Item() );
204 Q_ASSERT( row
>= 0 );
206 d
->mLastSelectedMessage
= row
;
208 emit
messageSelected( d
->itemForRow( row
) ); // this MAY be null
211 void Widget::viewMessageActivated( MessageList::Core::MessageItem
*msg
)
213 Q_ASSERT( msg
); // must not be null
214 Q_ASSERT( storageModel() );
216 if ( !msg
->isValid() ) {
220 int row
= msg
->currentModelIndexRow();
221 Q_ASSERT( row
>= 0 );
223 // The assert below may fail when quickly opening and closing a non-selected thread.
224 // This will actually activate the item without selecting it...
225 //Q_ASSERT( d->mLastSelectedMessage == row );
227 if ( d
->mLastSelectedMessage
!= row
) {
228 // Very ugly. We are activating a non selected message.
229 // This is very likely a double click on the plus sign near a thread leader.
230 // Dealing with mLastSelectedMessage here would be expensive: it would involve releasing the last selected,
231 // emitting signals, handling recursion... ugly.
232 // We choose a very simple solution: double clicking on the plus sign near a thread leader does
233 // NOT activate the message (i.e open it in a toplevel window) if it isn't previously selected.
237 emit
messageActivated( d
->itemForRow( row
) ); // this MAY be null
240 void Widget::viewSelectionChanged()
242 emit
selectionChanged();
243 if ( !currentMessageItem() ) {
244 emit
messageSelected( Item() );
248 void Widget::viewMessageListContextPopupRequest( const QList
< MessageList::Core::MessageItem
* > &selectedItems
,
249 const QPoint
&globalPos
)
251 if ( !d
->mXmlGuiClient
)
254 QMenu
*popup
= static_cast<QMenu
*>( d
->mXmlGuiClient
->factory()->container(
255 QLatin1String( "akonadi_messagelist_contextmenu" ),
256 d
->mXmlGuiClient
) );
258 popup
->exec( globalPos
);
262 void Widget::viewMessageStatusChangeRequest( MessageList::Core::MessageItem
*msg
, const KPIM::MessageStatus
&set
, const KPIM::MessageStatus
&clear
)
264 Q_ASSERT( msg
); // must not be null
265 Q_ASSERT( storageModel() );
267 if ( !msg
->isValid() ) {
271 int row
= msg
->currentModelIndexRow();
272 Q_ASSERT( row
>= 0 );
274 Item item
= d
->itemForRow( row
);
275 Q_ASSERT( item
.isValid() );
277 emit
messageStatusChangeRequest( item
, set
, clear
);
280 void Widget::viewGroupHeaderContextPopupRequest( MessageList::Core::GroupHeaderItem
*ghi
, const QPoint
&globalPos
)
290 act
= menu
.addAction( i18n( "Expand All Groups" ) );
291 connect( act
, SIGNAL( triggered( bool ) ),
292 view(), SLOT( slotExpandAllGroups() ) );
294 act
= menu
.addAction( i18n( "Collapse All Groups" ) );
295 connect( act
, SIGNAL( triggered( bool ) ),
296 view(), SLOT( slotCollapseAllGroups() ) );
298 menu
.exec( globalPos
);
301 void Widget::viewDragEnterEvent( QDragEnterEvent
*e
)
303 if ( !canAcceptDrag( e
) ) {
311 void Widget::viewDragMoveEvent( QDragMoveEvent
*e
)
313 if ( !canAcceptDrag( e
) ) {
328 void Widget::viewDropEvent( QDropEvent
*e
)
330 if ( !canAcceptDrag( e
) ) {
335 KUrl::List urls
= KUrl::List::fromMimeData( e
->mimeData() );
336 if ( urls
.isEmpty() ) {
337 kWarning() << "Could not decode drag data!";
345 if ( ( e
->possibleActions() & Qt::MoveAction
) == 0 ) { // We can't move anyway
349 int keybstate
= QApplication::keyboardModifiers();
350 if ( keybstate
& Qt::CTRL
) {
353 } else if ( keybstate
& Qt::SHIFT
) {
358 QAction
*moveAction
= menu
.addAction( KIcon( "go-jump"), i18n( "&Move Here" ) );
359 QAction
*copyAction
= menu
.addAction( KIcon( "edit-copy" ), i18n( "&Copy Here" ) );
361 menu
.addAction( KIcon( "dialog-cancel" ), i18n( "C&ancel" ) );
363 QAction
*menuChoice
= menu
.exec( QCursor::pos() );
364 if ( menuChoice
== moveAction
) {
366 } else if ( menuChoice
== copyAction
) {
374 Collection::List collections
= static_cast<const StorageModel
*>( storageModel() )->displayedCollections();
375 Collection target
= collections
.first();
377 foreach ( const KUrl
&url
, urls
) {
378 items
<< Item::fromUrl( url
);
381 if ( action
== DragCopy
) {
382 new ItemCopyJob( items
, target
, this );
383 } else if ( action
== DragMove
) {
384 new ItemMoveJob( items
, target
, this );
389 void Widget::viewStartDragRequest()
391 Collection::List collections
= static_cast<const StorageModel
*>( storageModel() )->displayedCollections();
393 if ( collections
.isEmpty() )
394 return; // no folder here
396 QList
<Item
> items
= d
->selectionAsItems();
397 if ( items
.isEmpty() )
400 bool readOnly
= false;
402 foreach ( const Collection c
, collections
) {
403 // We won't be able to remove items from this collection
404 if ( ( c
.rights() & Collection::CanDeleteItem
) == 0 ) {
405 // So the drag will be read-only
411 foreach ( const Item i
, items
) {
412 urls
<< i
.url( Item::UrlWithMimeType
);
415 QMimeData
*mimeData
= new QMimeData
;
416 urls
.populateMimeData( mimeData
);
418 QDrag
*drag
= new QDrag( view()->viewport() );
419 drag
->setMimeData( mimeData
);
423 if( items
.size() == 1 ) {
424 pixmap
= QPixmap( DesktopIcon("mail-message", KIconLoader::SizeSmall
) );
426 pixmap
= QPixmap( DesktopIcon("document-multiple", KIconLoader::SizeSmall
) );
429 // Calculate hotspot (as in Konqueror)
430 if( !pixmap
.isNull() ) {
431 drag
->setHotSpot( QPoint( pixmap
.width() / 2, pixmap
.height() / 2 ) );
432 drag
->setPixmap( pixmap
);
436 drag
->exec( Qt::CopyAction
);
438 drag
->exec( Qt::CopyAction
| Qt::MoveAction
);
441 Item::List
Widget::Private::selectionAsItems() const
444 QList
<Core::MessageItem
*> selection
= q
->view()->selectionAsMessageItemList();
446 foreach ( Core::MessageItem
*mi
, selection
) {
447 Item i
= itemForRow( mi
->currentModelIndexRow() );
448 Q_ASSERT( i
.isValid() );
455 Item
Widget::Private::itemForRow( int row
) const
457 return static_cast<const StorageModel
*>( q
->storageModel() )->itemForRow( row
);
460 KMime::Message::Ptr
Widget::Private::messageForRow( int row
) const
462 return static_cast<const StorageModel
*>( q
->storageModel() )->messageForRow( row
);
465 Item
Widget::currentItem() const
467 Core::MessageItem
*mi
= view()->currentMessageItem();
473 return d
->itemForRow( mi
->currentModelIndexRow() );
476 KMime::Message::Ptr
Widget::currentMessage() const
478 Core::MessageItem
*mi
= view()->currentMessageItem();
481 return KMime::Message::Ptr();
484 return d
->messageForRow( mi
->currentModelIndexRow() );
488 QList
<KMime::Message::Ptr
> Widget::selectionAsMessageList( bool includeCollapsedChildren
) const
490 QList
<KMime::Message::Ptr
> lstMiPtr
;
491 QList
<Core::MessageItem
*> lstMi
= view()->selectionAsMessageItemList( includeCollapsedChildren
);
492 if ( lstMi
.isEmpty() ) {
495 foreach( Core::MessageItem
*it
, lstMi
) {
496 lstMiPtr
.append( d
->messageForRow( it
->currentModelIndexRow() ) );
501 QList
<Akonadi::Item
> Widget::selectionAsMessageItemList( bool includeCollapsedChildren
) const
503 QList
<Item
> lstMiPtr
;
504 QList
<Core::MessageItem
*> lstMi
= view()->selectionAsMessageItemList( includeCollapsedChildren
);
505 if ( lstMi
.isEmpty() ) {
508 foreach( Core::MessageItem
*it
, lstMi
) {
509 lstMiPtr
.append( d
->itemForRow( it
->currentModelIndexRow() ) );
514 QList
<Akonadi::Item
> Widget::currentThreadAsMessageList() const
516 QList
<Item
> lstMiPtr
;
517 QList
<Core::MessageItem
*> lstMi
= view()->currentThreadAsMessageItemList();
518 if ( lstMi
.isEmpty() ) {
521 foreach( Core::MessageItem
*it
, lstMi
) {
522 lstMiPtr
.append( d
->itemForRow( it
->currentModelIndexRow() ) );
528 KPIM::MessageStatus
Widget::currentFilterStatus() const
530 return view()->currentFilterStatus();
533 QString
Widget::currentFilterSearchString() const
535 return view()->currentFilterSearchString();
539 bool Widget::isThreaded() const
541 return view()->isThreaded();
544 bool Widget::selectionEmpty() const
546 return view()->selectionEmpty();
549 bool Widget::getSelectionStats(
550 Akonadi::Item::List
&selectedItems
,
551 Akonadi::Item::List
&selectedVisibleItems
,
552 bool * allSelectedBelongToSameThread
,
553 bool includeCollapsedChildren
) const
555 if ( !storageModel() )
558 selectedItems
.clear();
559 selectedVisibleItems
.clear();
561 QList
< Core::MessageItem
* > selected
= view()->selectionAsMessageItemList( includeCollapsedChildren
);
563 Core::MessageItem
* topmost
= 0;
565 *allSelectedBelongToSameThread
= true;
567 foreach( Core::MessageItem
*it
, selected
) {
568 const Item item
= d
->itemForRow( it
->currentModelIndexRow() );
569 selectedItems
.append( item
);
570 if ( view()->isDisplayedWithParentsExpanded( it
) )
571 selectedVisibleItems
.append( item
);
573 topmost
= ( *it
).topmostMessage();
575 if ( topmost
!= ( *it
).topmostMessage() )
576 *allSelectedBelongToSameThread
= false;
582 void Widget::deletePersistentSet( MessageList::Core::MessageItemSetReference ref
)
584 view()->deletePersistentSet( ref
);
587 void Widget::markMessageItemsAsAboutToBeRemoved( MessageList::Core::MessageItemSetReference ref
, bool bMark
)
589 QList
< Core::MessageItem
* > lstPersistent
= view()->persistentSetCurrentMessageItemList( ref
);
590 if ( !lstPersistent
.isEmpty() )
591 view()->markMessageItemsAsAboutToBeRemoved( lstPersistent
, bMark
);
594 QList
<Akonadi::Item
> Widget::itemListFromPersistentSet( MessageList::Core::MessageItemSetReference ref
)
596 QList
<Akonadi::Item
> lstItem
;
597 QList
< Core::MessageItem
* > refList
= view()->persistentSetCurrentMessageItemList( ref
);
598 if ( !refList
.isEmpty() ) {
599 foreach( Core::MessageItem
*it
, refList
) {
600 lstItem
.append( d
->itemForRow( it
->currentModelIndexRow() ) );
607 MessageList::Core::MessageItemSetReference
Widget::selectionAsPersistentSet( bool includeCollapsedChildren
) const
609 QList
<Core::MessageItem
*> lstMi
= view()->selectionAsMessageItemList( includeCollapsedChildren
);
610 if ( lstMi
.isEmpty() ) {
613 return view()->createPersistentSet( lstMi
);
616 MessageList::Core::MessageItemSetReference
Widget::currentThreadAsPersistentSet() const
618 QList
<Item
> lstMiPtr
;
619 QList
<Core::MessageItem
*> lstMi
= view()->currentThreadAsMessageItemList();
620 if ( lstMi
.isEmpty() ) {
623 return view()->createPersistentSet( lstMi
);