1 /*
2  *  Copyright (C) 2010 Parker Coates <coates@kde.org>
3  *
4  *  This program is free software; you can redistribute it and/or
5  *  modify it under the terms of the GNU General Public License as
6  *  published by the Free Software Foundation; either version 2 of
7  *  the License, or (at your option) any later version.
8  *
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.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  */
18 
19 #include "kcardthemewidget.h"
20 #include "kcardthemewidget_p.h"
21 
22 // own
23 #include "common.h"
24 // KF
25 #include <KImageCache>
26 #include <KLineEdit>
27 #include <KLocalizedString>
28 #include <KNS3/DownloadDialog>
29 // Qt
30 #include <QPushButton>
31 #include <QMutexLocker>
32 #include <QApplication>
33 #include <QListView>
34 #include <QPainter>
35 #include <QPixmap>
36 #include <QVBoxLayout>
37 #include <QSvgRenderer>
38 
39 
40 namespace
41 {
timestampKey(const KCardTheme & theme)42     inline QString timestampKey( const KCardTheme & theme )
43     {
44         return theme.dirName() + QLatin1String("_timestamp");
45     }
46 
previewKey(const KCardTheme & theme,const QString & previewString)47     inline QString previewKey( const KCardTheme & theme, const QString & previewString )
48     {
49         return theme.dirName() + QLatin1Char('_') + previewString;
50     }
51 }
52 
53 
PreviewThread(const KCardThemeWidgetPrivate * d,const QList<KCardTheme> & themes)54 PreviewThread::PreviewThread( const KCardThemeWidgetPrivate * d, const QList<KCardTheme> & themes )
55   : d( d ),
56     m_themes( themes ),
57     m_haltFlag( false ),
58     m_haltMutex()
59 {
60 }
61 
62 
halt()63 void PreviewThread::halt()
64 {
65     {
66         QMutexLocker l( &m_haltMutex );
67         m_haltFlag = true;
68     }
69     wait();
70 }
71 
72 
run()73 void PreviewThread::run()
74 {
75     for (const KCardTheme & theme : qAsConst(m_themes)) {
76         {
77             QMutexLocker l( &m_haltMutex );
78             if ( m_haltFlag )
79                 return;
80         }
81 
82         const auto dpr = qApp->devicePixelRatio();
83         QImage img( d->previewSize * dpr, QImage::Format_ARGB32 );
84         img.setDevicePixelRatio( dpr );
85         img.fill( Qt::transparent );
86         QPainter p( &img );
87 
88         QSvgRenderer renderer( theme.graphicsFilePath() );
89 
90         QSizeF size = renderer.boundsOnElement(QStringLiteral("back")).size();
91         size.scale( 1.5 * d->baseCardSize.width(), d->baseCardSize.height(), Qt::KeepAspectRatio );
92 
93         qreal yPos = ( d->previewSize.height() - size.height() ) / 2;
94         qreal spacingWidth = d->baseCardSize.width()
95                              * ( d->previewSize.width() - d->previewLayout.size() * size.width() )
96                              / ( d->previewSize.width() - d->previewLayout.size() * d->baseCardSize.width() );
97 
98         qreal xPos = 0;
99         for (const QList<QString> & pile : qAsConst(d->previewLayout)) {
100             for (const QString & card : pile) {
101                 renderer.render( &p, card, QRectF( QPointF( xPos, yPos ), size ) );
102                 xPos += 0.3 * spacingWidth;
103             }
104             xPos += 1 * size.width() + ( 0.1 - 0.3 ) * spacingWidth;
105         }
106 
107         Q_EMIT previewRendered( theme, img );
108     }
109 }
110 
111 
CardThemeModel(KCardThemeWidgetPrivate * d,QObject * parent)112 CardThemeModel::CardThemeModel( KCardThemeWidgetPrivate * d, QObject * parent )
113   : QAbstractListModel( parent ),
114     d( d ),
115     m_thread( nullptr )
116 {
117     qRegisterMetaType<KCardTheme>();
118 
119     reload();
120 }
121 
122 
~CardThemeModel()123 CardThemeModel::~CardThemeModel()
124 {
125     deleteThread();
126 
127     qDeleteAll( m_previews );
128 }
129 
130 
lessThanByDisplayName(const KCardTheme & a,const KCardTheme & b)131 bool lessThanByDisplayName( const KCardTheme & a, const KCardTheme & b )
132 {
133     return a.displayName() < b.displayName();
134 }
135 
136 
reload()137 void CardThemeModel::reload()
138 {
139     deleteThread();
140 
141     beginResetModel();
142 
143     m_themes.clear();
144     qDeleteAll( m_previews );
145     m_previews.clear();
146 
147     QList<KCardTheme> previewsNeeded;
148     const auto dpr = qApp->devicePixelRatio();
149 
150     const auto themes = KCardTheme::findAllWithFeatures(d->requiredFeatures);
151     for (const KCardTheme & theme : themes) {
152         if ( !theme.isValid() )
153             continue;
154 
155         QPixmap * pix = new QPixmap();
156         QDateTime timestamp;
157         if ( cacheFind( d->cache, timestampKey( theme ), &timestamp )
158              && timestamp >= theme.lastModified()
159              && d->cache->findPixmap( previewKey( theme, d->previewString ), pix )
160              && pix->size() == d->previewSize * dpr )
161         {
162             pix->setDevicePixelRatio( dpr );
163             m_previews.insert( theme.displayName(), pix );
164         }
165         else
166         {
167             delete pix;
168             m_previews.insert( theme.displayName(), nullptr );
169             previewsNeeded << theme;
170         }
171 
172         m_themes.insert( theme.displayName(), theme );
173     }
174 
175     endResetModel();
176 
177     if ( !previewsNeeded.isEmpty() )
178     {
179         std::sort( previewsNeeded.begin(), previewsNeeded.end(), lessThanByDisplayName ) ;
180 
181         m_thread = new PreviewThread( d, previewsNeeded );
182         connect(m_thread, &PreviewThread::previewRendered, this, &CardThemeModel::submitPreview, Qt::QueuedConnection );
183         m_thread->start();
184     }
185 }
186 
187 
deleteThread()188 void CardThemeModel::deleteThread()
189 {
190     if ( m_thread && m_thread->isRunning() )
191         m_thread->halt();
192     delete m_thread;
193     m_thread = nullptr;
194 }
195 
196 
submitPreview(const KCardTheme & theme,const QImage & image)197 void CardThemeModel::submitPreview( const KCardTheme & theme, const QImage & image )
198 {
199     d->cache->insertImage( previewKey( theme, d->previewString ), image );
200     cacheInsert( d->cache, timestampKey( theme ), theme.lastModified() );
201 
202     QPixmap * pix = new QPixmap( QPixmap::fromImage( image ) );
203     delete m_previews.value( theme.displayName(), nullptr );
204     m_previews.insert( theme.displayName(), pix );
205 
206     QModelIndex index = indexOf( theme.dirName() );
207     Q_EMIT dataChanged( index, index );
208 }
209 
210 
rowCount(const QModelIndex & parent) const211 int CardThemeModel::rowCount( const QModelIndex & parent ) const
212 {
213     Q_UNUSED( parent )
214     return m_themes.size();
215 }
216 
217 
data(const QModelIndex & index,int role) const218 QVariant CardThemeModel::data( const QModelIndex & index, int role ) const
219 {
220     if ( !index.isValid() || index.row() >= m_themes.size())
221         return QVariant();
222 
223     if ( role == Qt::UserRole )
224     {
225         QMap<QString,KCardTheme>::const_iterator it = m_themes.constBegin();
226         for ( int i = 0; i < index.row(); ++i )
227             ++it;
228         return it.value().dirName();
229     }
230 
231     if ( role == Qt::DisplayRole )
232     {
233         QMap<QString,KCardTheme>::const_iterator it = m_themes.constBegin();
234         for ( int i = 0; i < index.row(); ++i )
235             ++it;
236         return it.value().displayName();
237     }
238 
239     if ( role == Qt::DecorationRole )
240     {
241         QMap<QString,QPixmap*>::const_iterator it = m_previews.constBegin();
242         for ( int i = 0; i < index.row(); ++i )
243             ++it;
244         return QVariant::fromValue( (void*)(it.value()) );
245     }
246 
247     return QVariant();
248 }
249 
250 
indexOf(const QString & dirName) const251 QModelIndex CardThemeModel::indexOf( const QString & dirName ) const
252 {
253     QMap<QString,KCardTheme>::const_iterator it = m_themes.constBegin();
254     for ( int i = 0; i < m_themes.size(); ++i )
255     {
256         if ( it.value().dirName() == dirName )
257             return index( i, 0 );
258         ++it;
259     }
260 
261     return QModelIndex();
262 }
263 
264 
CardThemeDelegate(KCardThemeWidgetPrivate * d,QObject * parent)265 CardThemeDelegate::CardThemeDelegate( KCardThemeWidgetPrivate * d, QObject * parent )
266   : QAbstractItemDelegate( parent ),
267     d( d )
268 {
269 }
270 
271 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const272 void CardThemeDelegate::paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
273 {
274     QApplication::style()->drawControl( QStyle::CE_ItemViewItem, &option, painter );
275 
276     painter->save();
277     QFont font = painter->font();
278     font.setWeight( QFont::Bold );
279     painter->setFont( font );
280 
281     QRect previewRect( option.rect.left() + ( option.rect.width() - d->previewSize.width() ) / 2,
282                        option.rect.top() + d->itemMargin,
283                        d->previewSize.width(),
284                        d->previewSize.height() );
285 
286     QVariant var = index.model()->data( index, Qt::DecorationRole );
287     QPixmap * pix = static_cast<QPixmap*>( var.value<void*>() );
288     if ( pix )
289     {
290         painter->drawPixmap( previewRect.topLeft(), *pix );
291     }
292     else
293     {
294         painter->fillRect( previewRect, QColor( 0, 0, 0, 16 ) );
295         painter->drawText( previewRect, Qt::AlignCenter, i18n("Loading...") );
296     }
297 
298     QRect textRect = option.rect.adjusted( 0, 0, 0, -d->itemMargin );
299     QString name = index.model()->data( index, Qt::DisplayRole ).toString();
300     painter->drawText( textRect, Qt::AlignHCenter | Qt::AlignBottom, name );
301 
302     painter->restore();
303 }
304 
305 
sizeHint(const QStyleOptionViewItem & option,const QModelIndex & index) const306 QSize CardThemeDelegate::sizeHint( const QStyleOptionViewItem & option, const QModelIndex & index ) const
307 {
308     Q_UNUSED( option )
309     Q_UNUSED( index )
310     return d->itemSize;
311 }
312 
313 
KCardThemeWidgetPrivate(KCardThemeWidget * q)314 KCardThemeWidgetPrivate::KCardThemeWidgetPrivate( KCardThemeWidget * q )
315   : QObject( q ),
316     q( q )
317 {
318 }
319 
320 
updateLineEdit(const QModelIndex & index)321 void KCardThemeWidgetPrivate::updateLineEdit( const QModelIndex & index )
322 {
323     hiddenLineEdit->setText( model->data( index, Qt::UserRole ).toString() );
324 }
325 
326 
updateListView(const QString & dirName)327 void KCardThemeWidgetPrivate::updateListView( const QString & dirName )
328 {
329     QModelIndex index = model->indexOf( dirName );
330     if ( index.isValid() )
331         listView->setCurrentIndex( index );
332 }
333 
334 
getNewCardThemes()335 void KCardThemeWidgetPrivate::getNewCardThemes()
336 {
337     QPointer<KNS3::DownloadDialog> dialog = new KNS3::DownloadDialog( QStringLiteral("kcardtheme.knsrc"), q );
338     dialog->exec();
339     if ( dialog && !dialog->changedEntries().isEmpty() )
340         model->reload();
341     delete dialog;
342 }
343 
344 
KCardThemeWidget(const QSet<QString> & requiredFeatures,const QString & previewString,QWidget * parent)345 KCardThemeWidget::KCardThemeWidget( const QSet<QString> & requiredFeatures, const QString & previewString, QWidget * parent )
346   : QWidget( parent ),
347     d( new KCardThemeWidgetPrivate( this ) )
348 {
349     d->cache = new KImageCache( QStringLiteral("libkcardgame-themes/previews"), 1 * 1024 * 1024 );
350     d->cache->setPixmapCaching( false );
351     d->cache->setEvictionPolicy( KSharedDataCache::EvictOldest );
352 
353     d->requiredFeatures = requiredFeatures;
354     d->previewString = previewString;
355 
356     d->previewLayout.clear();
357     const auto piles = previewString.split(QLatin1Char(';'));
358     for (const QString & pile : piles)
359         d->previewLayout << pile.split(QLatin1Char(','));
360 
361     d->abstractPreviewWidth = 0;
362     for ( int i = 0; i < d->previewLayout.size(); ++i )
363     {
364         d->abstractPreviewWidth += 1.0;
365         d->abstractPreviewWidth += 0.3 * ( d->previewLayout.at( i ).size() - 1 );
366         if ( i + 1 < d->previewLayout.size() )
367             d->abstractPreviewWidth += 0.1;
368     }
369 
370     d->baseCardSize = QSize( 80, 100 );
371     d->previewSize = QSize( d->baseCardSize.width() * d->abstractPreviewWidth, d->baseCardSize.height() );
372     d->itemMargin = 5;
373     d->textHeight = fontMetrics().height();
374     d->itemSize = QSize( d->previewSize.width() + 2 * d->itemMargin, d->previewSize.height() + d->textHeight + 3 * d->itemMargin );
375 
376     d->model = new CardThemeModel( d, this );
377 
378     d->listView = new QListView( this );
379     d->listView->setModel( d->model );
380     d->listView->setItemDelegate( new CardThemeDelegate( d, d->model ) );
381     d->listView->setVerticalScrollMode( QAbstractItemView::ScrollPerPixel );
382     d->listView->setAlternatingRowColors( true );
383 
384     if (parent && parent->width() >= 650) {
385         // FIXME This is just a fudge factor. It should be possible to detemine
386         // the actual width necessary including frame and scrollbar somehow.
387         d->listView->setMinimumWidth( d->itemSize.width() * 1.1 );
388         d->listView->setMinimumHeight( d->itemSize.height() * 2.5 );
389     }
390 
391     d->hiddenLineEdit = new KLineEdit( this );
392     d->hiddenLineEdit->setObjectName( QStringLiteral( "kcfg_CardTheme" ) );
393     d->hiddenLineEdit->hide();
394     connect( d->listView->selectionModel(), &QItemSelectionModel::currentChanged, d, &KCardThemeWidgetPrivate::updateLineEdit );
395     connect( d->hiddenLineEdit, &QLineEdit::textChanged, d, &KCardThemeWidgetPrivate::updateListView );
396 
397     d->newDeckButton = new QPushButton( QIcon::fromTheme( QStringLiteral( "get-hot-new-stuff") ), i18n("Get New Card Decks..." ), this );
398     connect( d->newDeckButton, &QAbstractButton::clicked, d, &KCardThemeWidgetPrivate::getNewCardThemes );
399 
400     QHBoxLayout * hLayout = new QHBoxLayout();
401     hLayout->addStretch( 1 );
402     hLayout->addWidget( d->newDeckButton );
403 
404     QVBoxLayout * layout = new QVBoxLayout( this );
405     layout->setContentsMargins(0, 0, 0, 0);
406     layout->addWidget( d->listView );
407     layout->addWidget( d->hiddenLineEdit );
408     layout->addLayout( hLayout );
409 }
410 
411 
~KCardThemeWidget()412 KCardThemeWidget::~KCardThemeWidget()
413 {
414 }
415 
416 
setCurrentSelection(const QString & dirName)417 void KCardThemeWidget::setCurrentSelection( const QString & dirName )
418 {
419     QModelIndex index = d->model->indexOf( dirName );
420     if ( index.isValid() )
421         d->listView->setCurrentIndex( index );
422 }
423 
424 
currentSelection() const425 QString KCardThemeWidget::currentSelection() const
426 {
427     QModelIndex index = d->listView->currentIndex();
428     if ( index.isValid() )
429         return d->model->data( index, Qt::UserRole ).toString();
430     else
431         return QString();
432 }
433 
434 
KCardThemeDialog(QWidget * parent,KConfigSkeleton * config,const QSet<QString> & requiredFeatures,const QString & previewString)435 KCardThemeDialog::KCardThemeDialog( QWidget * parent, KConfigSkeleton * config, const QSet<QString> & requiredFeatures, const QString & previewString )
436   : KConfigDialog( parent, QStringLiteral("KCardThemeDialog"), config )
437 {
438     // Leaving the header text and icon empty prevents the header from being shown.
439     addPage( new KCardThemeWidget( requiredFeatures, previewString, this ), QString() );
440 
441     setFaceType( KPageDialog::Plain );
442     setStandardButtons( QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel);
443 }
444 
445 
~KCardThemeDialog()446 KCardThemeDialog::~KCardThemeDialog()
447 {
448 }
449 
450 
showDialog()451 bool KCardThemeDialog::showDialog()
452 {
453     return KConfigDialog::showDialog( QStringLiteral("KCardThemeDialog") );
454 }
455 
456