akregator
articlelistview.cpp
Go to the documentation of this file.00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025 #include "articlelistview.h"
00026 #include "actionmanager.h"
00027 #include "akregatorconfig.h"
00028 #include "article.h"
00029 #include "articlemodel.h"
00030 #include "kernel.h"
00031 #include "types.h"
00032
00033 #include <utils/filtercolumnsproxymodel.h>
00034
00035 #include <KIcon>
00036 #include <KLocale>
00037 #include <KUrl>
00038 #include <KMenu>
00039
00040 #include <QApplication>
00041 #include <QContextMenuEvent>
00042 #include <QHeaderView>
00043 #include <QKeyEvent>
00044 #include <QList>
00045 #include <QMenu>
00046 #include <QPaintEvent>
00047 #include <QPalette>
00048
00049 #include <cassert>
00050
00051 using namespace Akregator;
00052
00053
00054 FilterDeletedProxyModel::FilterDeletedProxyModel( QObject* parent ) : QSortFilterProxyModel( parent )
00055 {
00056 setDynamicSortFilter( true );
00057 }
00058
00059 bool FilterDeletedProxyModel::filterAcceptsRow( int source_row, const QModelIndex& source_parent ) const
00060 {
00061 return !sourceModel()->index( source_row, 0, source_parent ).data( ArticleModel::IsDeletedRole ).toBool();
00062 }
00063
00064 SortColorizeProxyModel::SortColorizeProxyModel( QObject* parent ) : QSortFilterProxyModel( parent ), m_keepFlagIcon( KIcon( "mail-mark-important" ) )
00065 {
00066 }
00067
00068 bool SortColorizeProxyModel::filterAcceptsRow ( int source_row, const QModelIndex& source_parent ) const
00069 {
00070 if ( source_parent.isValid() )
00071 return false;
00072
00073 for ( uint i = 0; i < m_matchers.size(); ++i )
00074 {
00075 if ( !static_cast<ArticleModel*>( sourceModel() )->rowMatches( source_row, m_matchers[i] ) )
00076 return false;
00077 }
00078
00079 return true;
00080 }
00081
00082 void SortColorizeProxyModel::setFilters( const std::vector<boost::shared_ptr<const Filters::AbstractMatcher> >& matchers )
00083 {
00084 if ( m_matchers == matchers )
00085 return;
00086 m_matchers = matchers;
00087 invalidateFilter();
00088 }
00089
00090 QVariant SortColorizeProxyModel::data( const QModelIndex& idx, int role ) const
00091 {
00092 if ( !idx.isValid() || !sourceModel() )
00093 return QVariant();
00094
00095 const QModelIndex sourceIdx = mapToSource( idx );
00096
00097 switch ( role )
00098 {
00099 case Qt::ForegroundRole:
00100 {
00101 switch ( static_cast<ArticleStatus>( sourceIdx.data( ArticleModel::StatusRole ).toInt() ) )
00102 {
00103 case Unread:
00104 {
00105 return Settings::useCustomColors() ?
00106 Settings::colorUnreadArticles() : Qt::blue;
00107 }
00108 case New:
00109 {
00110 return Settings::useCustomColors() ?
00111 Settings::colorNewArticles() : Qt::red;
00112 }
00113 case Read:
00114 {
00115 return QApplication::palette().color( QPalette::WindowText );
00116 }
00117 }
00118 }
00119 break;
00120 case Qt::DecorationRole:
00121 {
00122 if ( sourceIdx.column() == ArticleModel::ItemTitleColumn )
00123 {
00124 return sourceIdx.data( ArticleModel::IsImportantRole ).toBool() ? m_keepFlagIcon : QVariant();
00125 }
00126 }
00127 break;
00128 }
00129 return sourceIdx.data( role );
00130 }
00131
00132 namespace {
00133
00134 static bool isRead( const QModelIndex& idx )
00135 {
00136 if ( !idx.isValid() )
00137 return false;
00138
00139 return static_cast<ArticleStatus>( idx.data( ArticleModel::StatusRole ).toInt() ) == Read;
00140 }
00141 }
00142
00143 void ArticleListView::setArticleModel( ArticleModel* model )
00144 {
00145 slotClear();
00146 if ( !model )
00147 return;
00148 m_proxy = new SortColorizeProxyModel( model );
00149 m_proxy->setSourceModel( model );
00150 m_proxy->setSortRole( ArticleModel::SortRole );
00151 FilterDeletedProxyModel* const proxy2 = new FilterDeletedProxyModel( model );
00152 proxy2->setSortRole( ArticleModel::SortRole );
00153 proxy2->setSourceModel( m_proxy );
00154
00155 FilterColumnsProxyModel* const columnsProxy = new FilterColumnsProxyModel( model );
00156 columnsProxy->setSortRole( ArticleModel::SortRole );
00157 columnsProxy->setSourceModel( proxy2 );
00158 columnsProxy->setColumnEnabled( ArticleModel::ItemTitleColumn );
00159 columnsProxy->setColumnEnabled( ArticleModel::FeedTitleColumn );
00160 columnsProxy->setColumnEnabled( ArticleModel::DateColumn );
00161 columnsProxy->setColumnEnabled( ArticleModel::AuthorColumn );
00162
00163 setModel( columnsProxy );
00164
00165 if ( !m_headerSetUp )
00166 {
00167 loadHeaderSettings();
00168 m_headerSetUp = true;
00169 }
00170 header()->setContextMenuPolicy( Qt::CustomContextMenu );
00171 }
00172
00173 void ArticleListView::showHeaderMenu(const QPoint& pos)
00174 {
00175 if ( !model() )
00176 return;
00177
00178 QPointer<KMenu> menu = new KMenu( this );
00179 connect( menu, SIGNAL( triggered( QAction* ) ),
00180 this, SLOT( headerMenuItemTriggered( QAction* ) ) );
00181 menu->addTitle( i18n( "Columns" ) );
00182 menu->setAttribute( Qt::WA_DeleteOnClose );
00183
00184 const int colCount = model()->columnCount();
00185 for ( int i = 0; i < colCount; ++i )
00186 {
00187 QAction* act = menu->addAction( model()->headerData( i, Qt::Horizontal ).toString() );
00188 act->setCheckable( true );
00189 act->setData( i );
00190 act->setChecked( !header()->isSectionHidden( i ) );
00191 }
00192
00193 menu->popup( header()->mapToGlobal( pos ) );
00194 }
00195
00196 void ArticleListView::headerMenuItemTriggered( QAction* act )
00197 {
00198 assert( act );
00199 const int col = act->data().toInt();
00200 if ( act->isChecked() )
00201 header()->showSection( col );
00202 else
00203 header()->hideSection( col );
00204 }
00205
00206 void ArticleListView::saveHeaderSettings()
00207 {
00208
00209
00210
00211
00212 QByteArray s = header()->saveState();
00213 KConfigGroup conf( Settings::self()->config(), "General" );
00214 conf.writeEntry( "ArticleListHeaders", s.toBase64() );
00215 }
00216
00217 void ArticleListView::loadHeaderSettings()
00218 {
00219
00220
00221
00222 KConfigGroup conf( Settings::self()->config(), "General" );
00223 QByteArray s = QByteArray::fromBase64( conf.readEntry( "ArticleListHeaders" ).toAscii() );
00224 if( !s.isNull() )
00225 header()->restoreState( s );
00226 }
00227
00228 QItemSelectionModel* ArticleListView::articleSelectionModel() const
00229 {
00230 return selectionModel();
00231 }
00232
00233 const QAbstractItemView* ArticleListView::itemView() const
00234 {
00235 return this;
00236 }
00237
00238 QAbstractItemView* ArticleListView::itemView()
00239 {
00240 return this;
00241 }
00242
00243 void ArticleListView::setGroupMode()
00244 {
00245 if ( m_columnMode == GroupMode )
00246 return;
00247 setColumnHidden( ArticleListView::FeedTitleColumn, false );
00248 m_columnMode = GroupMode;
00249 }
00250
00251 void ArticleListView::setFeedMode()
00252 {
00253 if ( m_columnMode == FeedMode )
00254 return;
00255 setColumnHidden( ArticleListView::FeedTitleColumn, true );
00256 m_columnMode = FeedMode;
00257 }
00258
00259 ArticleListView::~ArticleListView()
00260 {
00261 saveHeaderSettings();
00262 }
00263
00264 void ArticleListView::setIsAggregation( bool aggregation )
00265 {
00266 if ( aggregation == m_isAggregation )
00267 return;
00268 m_isAggregation = aggregation;
00269 if ( aggregation )
00270 setGroupMode();
00271 else
00272 setFeedMode();
00273 }
00274
00275 ArticleListView::ArticleListView( QWidget* parent )
00276 : QTreeView(parent),
00277 m_columnMode( Unspecified ),
00278 m_isAggregation( false ),
00279 m_headerSetUp( false )
00280 {
00281 setSortingEnabled( true );
00282 setAlternatingRowColors( true );
00283 setSelectionMode( QAbstractItemView::ExtendedSelection );
00284 setUniformRowHeights( true );
00285 setRootIsDecorated( false );
00286 setAllColumnsShowFocus(true);
00287
00288 setMinimumSize( 250, 150 );
00289 setWhatsThis( i18n("<h2>Article list</h2>"
00290 "Here you can browse articles from the currently selected feed. "
00291 "You can also manage articles, as marking them as persistent (\"Keep Article\") or delete them, using the right mouse button menu."
00292 "To view the web page of the article, you can open the article internally in a tab or in an external browser window."));
00293
00294 connect( header(), SIGNAL( customContextMenuRequested( QPoint ) ), this, SLOT( showHeaderMenu( QPoint ) ) );
00295 }
00296
00297 void ArticleListView::mousePressEvent( QMouseEvent *ev )
00298 {
00299
00300 QTreeView::mousePressEvent( ev );
00301
00302 if( ev->button() == Qt::MidButton ) {
00303 QModelIndex idx( currentIndex() );
00304 const KUrl url = currentIndex().data( ArticleModel::LinkRole ).value<KUrl>();
00305
00306 emit signalMouseButtonPressed( ev->button(), url );
00307 }
00308 }
00309
00310
00311 #if 0 // unused
00312 namespace {
00313 static QString itemIdForIndex( const QModelIndex& index )
00314 {
00315 return index.isValid() ? index.data( ArticleModel::ItemIdRole ).toString() : QString();
00316 }
00317
00318 static QStringList itemIdsForIndexes( const QModelIndexList& indexes )
00319 {
00320 QStringList articles;
00321 Q_FOREACH ( const QModelIndex i, indexes )
00322 {
00323 articles.append( itemIdForIndex( i ) );
00324 }
00325
00326 return articles;
00327 }
00328 }
00329 #endif
00330
00331 void ArticleListView::contextMenuEvent( QContextMenuEvent* event )
00332 {
00333 QWidget* w = ActionManager::getInstance()->container( "article_popup" );
00334 QMenu* popup = qobject_cast<QMenu*>( w );
00335 if ( popup )
00336 popup->exec( event->globalPos() );
00337 }
00338
00339 void ArticleListView::paintEvent( QPaintEvent* e )
00340 {
00341 QTreeView::paintEvent( e );
00342
00343 #ifdef __GNUC__
00344 #warning The distinction between empty node and 0 items after filtering is hard to port to interview
00345 #endif
00346 #if 0
00347 QString message;
00348
00349 if ( !model() || model()->rowCount() > 0 )
00350 {
00351 if (visibleArticles() == 0)
00352 {
00353 message = i18n("<div align=center>"
00354 "<h3>No matches</h3>"
00355 "Filter does not match any articles, "
00356 "please change your criteria and try again."
00357 "</div>");
00358 }
00359 }
00360 else if ( !model() )
00361 {
00362 if (!d->node)
00363 {
00364 message = i18n("<div align=center>"
00365 "<h3>No feed selected</h3>"
00366 "This area is article list. "
00367 "Select a feed from the feed list "
00368 "and you will see its articles here."
00369 "</div>");
00370 }
00371 }
00372
00373 if (!message.isNull())
00374 paintInfoBox( message, viewport(), palette() );
00375 #endif
00376 }
00377
00378
00379 void ArticleListView::slotClear()
00380 {
00381 QAbstractItemModel* const oldModel = model();
00382 setModel( 0L );
00383 delete oldModel;
00384 }
00385
00386 void ArticleListView::slotPreviousArticle()
00387 {
00388 if ( !model() )
00389 return;
00390
00391 const QModelIndex idx = currentIndex();
00392 const int newRow = qMax( 0, ( idx.isValid() ? idx.row() : model()->rowCount() ) - 1 );
00393 const QModelIndex newIdx = idx.isValid() ? idx.sibling( newRow, 0 ) : model()->index( newRow, 0 );
00394 selectIndex( newIdx );
00395 }
00396
00397 void ArticleListView::slotNextArticle()
00398 {
00399 if ( !model() )
00400 return;
00401
00402 const QModelIndex idx = currentIndex();
00403 const int newRow = idx.isValid() ? ( idx.row() + 1 ) : 0;
00404 const QModelIndex newIdx = model()->index( qMin( newRow, model()->rowCount() - 1 ), 0 );
00405 selectIndex( newIdx );
00406 }
00407
00408 void ArticleListView::slotNextUnreadArticle()
00409 {
00410 if (!model())
00411 return;
00412
00413 const int rowCount = model()->rowCount();
00414 const int startRow = qMin( rowCount - 1, ( currentIndex().isValid() ? currentIndex().row() + 1 : 0 ) );
00415
00416 int i = startRow;
00417 bool foundUnread = false;
00418
00419 do
00420 {
00421 if ( !::isRead( model()->index( i, 0 ) ) )
00422 foundUnread = true;
00423 else
00424 i = (i + 1) % rowCount;
00425 }
00426 while ( !foundUnread && i != startRow );
00427
00428 if ( foundUnread )
00429 {
00430 selectIndex( model()->index( i, 0 ) );
00431 }
00432 }
00433
00434 void ArticleListView::selectIndex( const QModelIndex& idx )
00435 {
00436 if ( !idx.isValid() )
00437 return;
00438 setCurrentIndex( idx );
00439 clearSelection();
00440 Q_ASSERT( selectionModel() );
00441 selectionModel()->select( idx, QItemSelectionModel::Select | QItemSelectionModel::Rows );
00442 scrollTo( idx, PositionAtCenter );
00443 }
00444
00445 void ArticleListView::slotPreviousUnreadArticle()
00446 {
00447 if ( !model() )
00448 return;
00449
00450 const int rowCount = model()->rowCount();
00451 const int startRow = qMax( 0, ( currentIndex().isValid() ? currentIndex().row() : rowCount ) - 1 );
00452
00453 int i = startRow;
00454 bool foundUnread = false;
00455
00456 do
00457 {
00458 if ( !::isRead( model()->index( i, 0 ) ) )
00459 foundUnread = true;
00460 else
00461 i = i > 0 ? i - 1 : rowCount - 1;
00462 }
00463 while ( !foundUnread && i != startRow );
00464
00465 if ( foundUnread )
00466 {
00467 selectIndex( model()->index( i, 0 ) );
00468 }
00469 }
00470
00471
00472 void ArticleListView::setFilters( const std::vector<boost::shared_ptr<const Filters::AbstractMatcher> >& matchers )
00473 {
00474 if ( m_proxy )
00475 m_proxy->setFilters( matchers );
00476 }
00477
00478 #include "articlelistview.moc"