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 ), ×tamp )
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