4 * Copyright (c) 2000, 2001, 2007 Frerich Raabe <raabe@kde.org>
6 * This program is distributed in the hope that it will be useful, but WITHOUT
7 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
8 * FOR A PARTICULAR PURPOSE. For licensing and distribution details, check the
9 * accompanying file 'COPYING'.
11 #include "knewsticker.h"
12 #include "newsfeedmanager.h"
14 #include "ui_feedsettings.h"
15 #include "ui_visualsettings.h"
17 #include <kconfigdialog.h>
18 #include <kiconloader.h>
19 #include <kmimetype.h>
20 #include <syndication/item.h>
22 #include <QDesktopServices>
23 #include <QGraphicsSceneContextMenuEvent>
25 #include <QMessageBox>
26 #include <QSignalMapper>
29 using namespace Syndication
;
31 HyperlinkItem::HyperlinkItem( const QString
&text
, const QUrl
&url
,
32 QGraphicsItem
*parentItem
)
33 : QObject(), QGraphicsSimpleTextItem( text
, parentItem
),
36 setFont( Settings::font() );
37 setBrush( Settings::color() );
38 setCursor( Qt::PointingHandCursor
);
39 setAcceptedMouseButtons( Qt::LeftButton
);
40 setAcceptsHoverEvents( true );
43 void HyperlinkItem::hoverEnterEvent( QGraphicsSceneHoverEvent
* )
45 if ( m_url
.isEmpty() ) {
50 f
.setUnderline( true );
55 void HyperlinkItem::hoverLeaveEvent( QGraphicsSceneHoverEvent
* )
57 if ( m_url
.isEmpty() ) {
62 f
.setUnderline( false );
64 setBrush( Settings::color() );
67 void HyperlinkItem::mouseReleaseEvent( QGraphicsSceneMouseEvent
* )
69 if ( m_url
.isEmpty() ) {
73 emit
activated( m_url
.toString() );
76 SeparatorItem::SeparatorItem( QGraphicsItem
*parentItem
)
77 : QGraphicsSimpleTextItem( " +++ ", parentItem
)
79 setBrush( Settings::color() );
80 setFont( Settings::font() );
83 NewsTickerItem::NewsTickerItem( const QString
&text
, const QUrl
&url
,
84 QGraphicsItem
*parentItem
)
85 : QGraphicsItemGroup( parentItem
)
87 setHandlesChildEvents( false );
91 QGraphicsItem
*pi
= 0;
92 const QString favIcon
= KMimeType::favIconForUrl( url
);
93 if ( !favIcon
.isEmpty() ) {
94 pi
= new QGraphicsPixmapItem( SmallIcon( favIcon
), this );
96 xpos
+= boundingRect().width() + 8;
99 m_headlineItem
= new HyperlinkItem( text
, url
, this );
100 m_headlineItem
->setPos( xpos
, 0 );
101 addToGroup( m_headlineItem
);
103 m_separatorItem
= new SeparatorItem( this );
104 m_separatorItem
->setPos( boundingRect().width(), 0 );
105 addToGroup( m_separatorItem
);
108 pi
->setPos( 0, ( boundingRect().height() - pi
->boundingRect().height() ) / 2 );
112 KNewsTicker::KNewsTicker( QObject
*parent
, const QVariantList
&args
)
113 : Plasma::Applet( parent
, args
),
116 setDrawStandardBackground( true );
118 m_scrollerItem
= new QGraphicsRectItem( 0, 0, 512, QFontMetrics( Settings::font() ).height() * 2, this );
120 connect( NewsFeedManager::self(), SIGNAL( feedLoaded( const QUrl
& ) ),
121 this, SLOT( feedLoaded( const QUrl
& ) ) );
122 connect( NewsFeedManager::self(), SIGNAL( updateFinished() ),
123 this, SLOT( feedUpdateFinished() ) );
125 foreach ( const QString
&url
, Settings::feedUrls() ) {
126 NewsFeedManager::self()->subscribeTo( url
);
129 m_scrollTimer
= new QTimer( this );
130 connect( m_scrollTimer
, SIGNAL( timeout() ),
131 this, SLOT( advance() ) );
132 m_scrollTimer
->start( 1000 / 25 );
134 m_updateTimer
= new QTimer( this );
135 connect( m_updateTimer
, SIGNAL( timeout() ),
136 this, SLOT( updateFeeds() ) );
137 m_updateTimer
->start( Settings::updateInterval() * 60 * 1000 );
139 setHasConfigurationInterface( true );
144 void KNewsTicker::showConfigurationInterface()
146 KConfigDialog
dlg( 0, "settings", Settings::self() );
147 connect( &dlg
, SIGNAL( settingsChanged( const QString
& ) ),
148 this, SLOT( settingsChanged( const QString
& ) ) );
149 dlg
.setFaceType( KPageDialog::Tabbed
);
151 QWidget
*page
= new QWidget( 0 );
153 Ui::VisualSettings ui
;
156 dlg
.addPage( page
, i18n( "Appearance" ) );
158 page
= new QWidget( 0 );
163 dlg
.addPage( page
, i18n( "Feed Access" ) );
165 QFont origFont
= Settings::font();
167 if ( dlg
.exec() == QDialog::Accepted
) {
168 if ( Settings::font() != origFont
) {
175 void KNewsTicker::settingsChanged( const QString
& /* dialogName */ )
181 QSizeF
KNewsTicker::contentSizeHint() const
183 return m_scrollerItem
->boundingRect().size();
186 QList
<QAction
*> KNewsTicker::contextActions()
188 QList
<QAction
*> actions
;
190 delete m_signalMapper
;
191 m_signalMapper
= new QSignalMapper( this );
192 connect( m_signalMapper
, SIGNAL( mapped( const QString
& ) ),
193 this, SLOT( openFeedItem( const QString
& ) ) );
195 QList
<FeedPtr
> availableFeeds
= NewsFeedManager::self()->availableFeeds().values();
196 foreach ( FeedPtr feed
, availableFeeds
) {
197 QMenu
*feedMenu
= new QMenu
;
198 QList
<ItemPtr
> items
= feed
->items();
199 foreach ( ItemPtr item
, items
) {
200 QString title
= item
->title();
201 title
.replace( """, "\"" );
202 title
.replace( "&", "&" );
203 QAction
*itemAction
= feedMenu
->addAction( title
,
204 m_signalMapper
, SLOT( map() ) );
206 m_signalMapper
->setMapping( itemAction
, item
->link() );
209 QAction
*feedAction
= new QAction( feed
->title(), 0 );
210 const QString favIcon
= KMimeType::favIconForUrl( feed
->link() );
211 if ( !favIcon
.isEmpty() ) {
212 feedAction
->setIcon( SmallIcon( favIcon
) );
214 feedAction
->setMenu( feedMenu
);
215 actions
.append( feedAction
);
221 void KNewsTicker::relayoutItems()
223 foreach ( QGraphicsItem
*item
, m_graphicsItems
) {
226 m_graphicsItems
.clear();
228 if ( m_items
.empty() ) {
232 qreal ypos
= ( contentSize().height() - QFontMetrics( font() ).height() ) / 2;
234 QList
<Item
>::ConstIterator it
, end
= m_items
.end();
235 for ( it
= m_items
.begin(); it
!= end
; ++it
) {
236 addItemToLayout( *it
, &xpos
, &ypos
);
239 /* In case none of the available items were added to the scroll text (this
240 * can happen if all of them have been read), add a filler item which tells
241 * the user that no unread news are available.
243 if ( m_graphicsItems
.isEmpty() ) {
245 item
.text
= i18n( "No unread news available" );
247 addItemToLayout( item
, &xpos
, &ypos
);
248 const qreal firstItemWidth
= m_graphicsItems
.first()->boundingRect().width();
249 while ( xpos
< contentSize().width() + firstItemWidth
) {
250 addItemToLayout( item
, &xpos
, &ypos
);
255 const qreal firstItemWidth
= m_graphicsItems
.first()->boundingRect().width();
257 it
= m_items
.begin();
258 while ( xpos
< contentSize().width() + firstItemWidth
) {
259 addItemToLayout( *it
, &xpos
, &ypos
);
261 if ( ++it
== m_items
.end() ) {
262 it
= m_items
.begin();
267 void KNewsTicker::addItemToLayout( const Item
&item
, qreal
*xpos
, qreal
*ypos
)
269 if ( Settings::hideReadArticles() && m_readArticles
.contains( item
.url
) ) {
273 NewsTickerItem
*i
= new NewsTickerItem( item
.text
, item
.url
, m_scrollerItem
);
274 connect( i
->headlineItem(), SIGNAL( activated( const QString
& ) ),
275 this, SLOT( openFeedItem( const QString
& ) ) );
276 i
->setPos( *xpos
, *ypos
);
277 m_graphicsItems
.append( i
);
278 *xpos
+= i
->boundingRect().width();
281 void KNewsTicker::advance()
283 if ( m_graphicsItems
.empty() ) {
287 const qreal ypos
= ( contentSize().height() - QFontMetrics( font() ).height() ) / 2;
289 /* In case an item scrolled out to the left, take it out of the m_graphicsItems list
290 * and insert it at the end again. Also, move it's position to the very end (determine
291 * the end by adding up the widths of the remaining items).
293 NewsTickerItem
*firstItem
= m_graphicsItems
.first();
294 while ( firstItem
->x() + firstItem
->boundingRect().width() < 0 ) {
295 m_graphicsItems
.erase( m_graphicsItems
.begin() );
297 /* If we just scrolled an item out of view which we read already,
298 * don't show it again in case the user configured this.
300 if ( Settings::hideReadArticles() &&
301 m_readArticles
.contains( firstItem
->headlineItem()->url() ) ) {
305 foreach ( QGraphicsItem
*item
, m_graphicsItems
) {
306 xpos
+= item
->boundingRect().width();
308 firstItem
->setPos( xpos
, ypos
);
309 m_graphicsItems
.append( firstItem
);
311 firstItem
= m_graphicsItems
.first();
314 foreach ( NewsTickerItem
*item
, m_graphicsItems
) {
315 item
->moveBy( -1, 0 );
319 void KNewsTicker::feedLoaded( const QUrl
&url
)
321 FeedPtr feed
= NewsFeedManager::self()->availableFeeds()[ url
];
322 foreach ( ItemPtr item
, feed
->items() ) {
324 i
.text
= item
->title();
325 i
.text
.replace( """, "\"" );
326 i
.url
= item
->link();
331 void KNewsTicker::feedUpdateFinished()
333 /* Filter out unneded entries in our m_readArticles set; first find out
334 * which URLs are shown after the update. Then, create a new list of read
335 * articles in which only those articles get added which are available
339 foreach ( const Item
&item
, m_items
) {
340 allUrls
.insert( item
.url
);
343 QSet
<QUrl
> activeReadArticles
;
344 foreach ( const QUrl
&readUrl
, m_readArticles
) {
345 if ( allUrls
.contains( readUrl
) ) {
346 activeReadArticles
.insert( readUrl
);
349 m_readArticles
= activeReadArticles
;
354 void KNewsTicker::updateFeeds()
357 NewsFeedManager::self()->update();
360 void KNewsTicker::openFeedItem( const QString
&url
)
362 QDesktopServices::openUrl( url
);
363 if ( !m_readArticles
.contains( url
) ) {
364 m_readArticles
.insert( url
);
368 void KNewsTicker::reloadSettings()
370 foreach ( NewsTickerItem
*item
, m_graphicsItems
) {
371 item
->headlineItem()->setBrush( Settings::color() );
372 item
->separatorItem()->setBrush( Settings::color() );
374 m_scrollerItem
->setRect( 0, 0, 512, QFontMetrics( Settings::font() ).height() * 2 );
375 m_updateTimer
->setInterval( Settings::updateInterval() * 60 * 1000 );
378 #include "knewsticker.moc"