kcardthemewidget.cpp 13.2 KB
Newer Older
1
/*
Parker Coates's avatar
Parker Coates committed
2
 *  Copyright (C) 2010 Parker Coates <coates@kde.org>
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public License as
 *  published by the Free Software Foundation; either version 2 of 
 *  the License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "kcardthemewidget.h"
#include "kcardthemewidget_p.h"

22
// own
23
#include "common.h"
24
// KF
Laurent Montel's avatar
Laurent Montel committed
25
#include <KImageCache>
26
#include <KLineEdit>
Laurent Montel's avatar
Laurent Montel committed
27
#include <KLocalizedString>
28
29
#include <KNS3/DownloadDialog>
// Qt
Laurent Montel's avatar
Laurent Montel committed
30
#include <QPushButton>
31
#include <QMutexLocker>
Laurent Montel's avatar
Laurent Montel committed
32
33
34
35
36
#include <QApplication>
#include <QListView>
#include <QPainter>
#include <QPixmap>
#include <QVBoxLayout>
37
#include <QSvgRenderer>
38
39


40
41
namespace
{
42
43
    inline QString timestampKey( const KCardTheme & theme )
    {
44
        return theme.dirName() + QLatin1String("_timestamp");
45
46
47
48
    }

    inline QString previewKey( const KCardTheme & theme, const QString & previewString )
    {
49
        return theme.dirName() + QLatin1Char('_') + previewString;
50
    }
51
52
53
}


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
PreviewThread::PreviewThread( const KCardThemeWidgetPrivate * d, const QList<KCardTheme> & themes )
  : d( d ),
    m_themes( themes ),
    m_haltFlag( false ),
    m_haltMutex()
{
}


void PreviewThread::halt()
{
    {
        QMutexLocker l( &m_haltMutex );
        m_haltFlag = true;
    }
    wait();
}


void PreviewThread::run()
{
75
    for (const KCardTheme & theme : qAsConst(m_themes)) {
76
77
78
79
80
81
        {
            QMutexLocker l( &m_haltMutex );
            if ( m_haltFlag )
                return;
        }

Alexander Volkov's avatar
Alexander Volkov committed
82
83
84
        const auto dpr = qApp->devicePixelRatio();
        QImage img( d->previewSize * dpr, QImage::Format_ARGB32 );
        img.setDevicePixelRatio( dpr );
85
86
87
88
89
        img.fill( Qt::transparent );
        QPainter p( &img );

        QSvgRenderer renderer( theme.graphicsFilePath() );

90
        QSizeF size = renderer.boundsOnElement(QStringLiteral("back")).size();
91
92
93
94
95
96
97
98
        size.scale( 1.5 * d->baseCardSize.width(), d->baseCardSize.height(), Qt::KeepAspectRatio );

        qreal yPos = ( d->previewSize.height() - size.height() ) / 2;
        qreal spacingWidth = d->baseCardSize.width()
                             * ( d->previewSize.width() - d->previewLayout.size() * size.width() )
                             / ( d->previewSize.width() - d->previewLayout.size() * d->baseCardSize.width() );

        qreal xPos = 0;
99
100
        for (const QList<QString> & pile : qAsConst(d->previewLayout)) {
            for (const QString & card : pile) {
101
102
103
104
105
106
                renderer.render( &p, card, QRectF( QPointF( xPos, yPos ), size ) );
                xPos += 0.3 * spacingWidth;
            }
            xPos += 1 * size.width() + ( 0.1 - 0.3 ) * spacingWidth;
        }

107
        Q_EMIT previewRendered( theme, img );
108
109
    }
}
110
111
112
113


CardThemeModel::CardThemeModel( KCardThemeWidgetPrivate * d, QObject * parent )
  : QAbstractListModel( parent ),
114
    d( d ),
Fabian Kosmale's avatar
Fabian Kosmale committed
115
    m_thread( nullptr )
116
{
117
118
    qRegisterMetaType<KCardTheme>();

119
120
121
122
123
124
    reload();
}


CardThemeModel::~CardThemeModel()
{
125
126
    deleteThread();

127
128
129
130
    qDeleteAll( m_previews );
}


131
132
133
134
135
136
bool lessThanByDisplayName( const KCardTheme & a, const KCardTheme & b )
{
    return a.displayName() < b.displayName();
}


137
138
void CardThemeModel::reload()
{
139
140
    deleteThread();

141
    beginResetModel();
142

143
    m_themes.clear();
144
145
    qDeleteAll( m_previews );
    m_previews.clear();
146
147

    QList<KCardTheme> previewsNeeded;
Alexander Volkov's avatar
Alexander Volkov committed
148
    const auto dpr = qApp->devicePixelRatio();
149

150
151
    const auto themes = KCardTheme::findAllWithFeatures(d->requiredFeatures);
    for (const KCardTheme & theme : themes) {
152
153
154
        if ( !theme.isValid() )
            continue;

155
        QPixmap * pix = new QPixmap();
156
157
158
        QDateTime timestamp;
        if ( cacheFind( d->cache, timestampKey( theme ), &timestamp )
             && timestamp >= theme.lastModified() 
Alexander Volkov's avatar
Alexander Volkov committed
159
160
             && d->cache->findPixmap( previewKey( theme, d->previewString ), pix )
             && pix->size() == d->previewSize * dpr )
161
        {
Alexander Volkov's avatar
Alexander Volkov committed
162
            pix->setDevicePixelRatio( dpr );
163
            m_previews.insert( theme.displayName(), pix );
164
165
166
167
        }
        else
        {
            delete pix;
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
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
178
    if ( !previewsNeeded.isEmpty() )
    {
179
        std::sort( previewsNeeded.begin(), previewsNeeded.end(), lessThanByDisplayName ) ;
180
181

        m_thread = new PreviewThread( d, previewsNeeded );
Laurent Montel's avatar
Laurent Montel committed
182
        connect(m_thread, &PreviewThread::previewRendered, this, &CardThemeModel::submitPreview, Qt::QueuedConnection );
183
184
        m_thread->start();
    }
185
186
187
}


188
void CardThemeModel::deleteThread()
189
{
190
191
192
    if ( m_thread && m_thread->isRunning() )
        m_thread->halt();
    delete m_thread;
Fabian Kosmale's avatar
Fabian Kosmale committed
193
    m_thread = nullptr;
194
}
195

196

197
void CardThemeModel::submitPreview( const KCardTheme & theme, const QImage & image )
198
{
199
200
    d->cache->insertImage( previewKey( theme, d->previewString ), image );
    cacheInsert( d->cache, timestampKey( theme ), theme.lastModified() );
201

202
    QPixmap * pix = new QPixmap( QPixmap::fromImage( image ) );
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
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
}


211
int CardThemeModel::rowCount( const QModelIndex & parent ) const
212
213
{
    Q_UNUSED( parent )
214
    return m_themes.size();
215
216
217
218
219
}


QVariant CardThemeModel::data( const QModelIndex & index, int role ) const
{
220
    if ( !index.isValid() || index.row() >= m_themes.size())
221
222
        return QVariant();

223
224
225
226
227
228
229
230
    if ( role == Qt::UserRole )
    {
        QMap<QString,KCardTheme>::const_iterator it = m_themes.constBegin();
        for ( int i = 0; i < index.row(); ++i )
            ++it;
        return it.value().dirName();
    }

231
232
233
234
235
236
237
    if ( role == Qt::DisplayRole )
    {
        QMap<QString,KCardTheme>::const_iterator it = m_themes.constBegin();
        for ( int i = 0; i < index.row(); ++i )
            ++it;
        return it.value().displayName();
    }
238

239
    if ( role == Qt::DecorationRole )
240
    {
241
242
243
        QMap<QString,QPixmap*>::const_iterator it = m_previews.constBegin();
        for ( int i = 0; i < index.row(); ++i )
            ++it;
Laurent Montel's avatar
Laurent Montel committed
244
        return QVariant::fromValue( (void*)(it.value()) );
245
    }
246
247

    return QVariant();
248
249
250
}


251
QModelIndex CardThemeModel::indexOf( const QString & dirName ) const
252
{
253
254
    QMap<QString,KCardTheme>::const_iterator it = m_themes.constBegin();
    for ( int i = 0; i < m_themes.size(); ++i )
255
    {
256
        if ( it.value().dirName() == dirName )
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
            return index( i, 0 );
        ++it;
    }

    return QModelIndex();
}


CardThemeDelegate::CardThemeDelegate( KCardThemeWidgetPrivate * d, QObject * parent )
  : QAbstractItemDelegate( parent ),
    d( d )
{
}


void CardThemeDelegate::paint( QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
    QApplication::style()->drawControl( QStyle::CE_ItemViewItem, &option, painter );

    painter->save();
    QFont font = painter->font();
    font.setWeight( QFont::Bold );
    painter->setFont( font );
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297

    QRect previewRect( option.rect.left() + ( option.rect.width() - d->previewSize.width() ) / 2,
                       option.rect.top() + d->itemMargin,
                       d->previewSize.width(),
                       d->previewSize.height() );

    QVariant var = index.model()->data( index, Qt::DecorationRole );
    QPixmap * pix = static_cast<QPixmap*>( var.value<void*>() );
    if ( pix )
    {
        painter->drawPixmap( previewRect.topLeft(), *pix );
    }
    else
    {
        painter->fillRect( previewRect, QColor( 0, 0, 0, 16 ) );
        painter->drawText( previewRect, Qt::AlignCenter, i18n("Loading...") );
    }

298
    QRect textRect = option.rect.adjusted( 0, 0, 0, -d->itemMargin );
299
300
301
    QString name = index.model()->data( index, Qt::DisplayRole ).toString();
    painter->drawText( textRect, Qt::AlignHCenter | Qt::AlignBottom, name );

302
303
304
305
306
307
308
309
310
311
312
313
    painter->restore();
}


QSize CardThemeDelegate::sizeHint( const QStyleOptionViewItem & option, const QModelIndex & index ) const
{
    Q_UNUSED( option )
    Q_UNUSED( index )
    return d->itemSize;
}


314
315
316
317
318
319
320
KCardThemeWidgetPrivate::KCardThemeWidgetPrivate( KCardThemeWidget * q )
  : QObject( q ),
    q( q )
{
}


321
322
323
324
325
326
327
328
329
330
331
332
333
334
void KCardThemeWidgetPrivate::updateLineEdit( const QModelIndex & index )
{
    hiddenLineEdit->setText( model->data( index, Qt::UserRole ).toString() );
}


void KCardThemeWidgetPrivate::updateListView( const QString & dirName )
{
    QModelIndex index = model->indexOf( dirName );
    if ( index.isValid() )
        listView->setCurrentIndex( index );
}


335
336
void KCardThemeWidgetPrivate::getNewCardThemes()
{
337
    QPointer<KNS3::DownloadDialog> dialog = new KNS3::DownloadDialog( QStringLiteral("kcardtheme.knsrc"), q );
338
339
    dialog->exec();
    if ( dialog && !dialog->changedEntries().isEmpty() )
340
        model->reload();
341
    delete dialog;
342
343
344
}


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
351
352
    d->cache->setPixmapCaching( false );
    d->cache->setEvictionPolicy( KSharedDataCache::EvictOldest );

353
    d->requiredFeatures = requiredFeatures;
354
355
356
    d->previewString = previewString;

    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
366
        d->abstractPreviewWidth += 0.3 * ( d->previewLayout.at( i ).size() - 1 );
        if ( i + 1 < d->previewLayout.size() )
367
368
369
370
371
372
373
374
375
376
            d->abstractPreviewWidth += 0.1;
    }

    d->baseCardSize = QSize( 80, 100 );
    d->previewSize = QSize( d->baseCardSize.width() * d->abstractPreviewWidth, d->baseCardSize.height() );
    d->itemMargin = 5;
    d->textHeight = fontMetrics().height();
    d->itemSize = QSize( d->previewSize.width() + 2 * d->itemMargin, d->previewSize.height() + d->textHeight + 3 * d->itemMargin );

    d->model = new CardThemeModel( d, this );
377

378
379
380
381
382
383
384
385
386
387
388
    d->listView = new QListView( this );
    d->listView->setModel( d->model );
    d->listView->setItemDelegate( new CardThemeDelegate( d, d->model ) );
    d->listView->setVerticalScrollMode( QAbstractItemView::ScrollPerPixel );
    d->listView->setAlternatingRowColors( true );

    // FIXME This is just a fudge factor. It should be possible to detemine
    // the actual width necessary including frame and scrollbar somehow.
    d->listView->setMinimumWidth( d->itemSize.width() * 1.1 ); 
    d->listView->setMinimumHeight( d->itemSize.height() * 2.5 );

389
    d->hiddenLineEdit = new KLineEdit( this );
390
    d->hiddenLineEdit->setObjectName( QStringLiteral( "kcfg_CardTheme" ) );
391
    d->hiddenLineEdit->hide();
392
393
    connect( d->listView->selectionModel(), &QItemSelectionModel::currentChanged, d, &KCardThemeWidgetPrivate::updateLineEdit );
    connect( d->hiddenLineEdit, &QLineEdit::textChanged, d, &KCardThemeWidgetPrivate::updateListView );
394

395
396
    d->newDeckButton = new QPushButton( QIcon::fromTheme( QStringLiteral( "get-hot-new-stuff") ), i18n("Get New Card Decks..." ), this );
    connect( d->newDeckButton, &QAbstractButton::clicked, d, &KCardThemeWidgetPrivate::getNewCardThemes );
397
398
399

    QHBoxLayout * hLayout = new QHBoxLayout();
    hLayout->addStretch( 1 );
400
    hLayout->addWidget( d->newDeckButton );
401

402
    QVBoxLayout * layout = new QVBoxLayout( this );
403
    layout->setContentsMargins(0, 0, 0, 0);
404
    layout->addWidget( d->listView );
405
    layout->addWidget( d->hiddenLineEdit );
406
    layout->addLayout( hLayout );
407
408
409
410
411
412
413
414
}


KCardThemeWidget::~KCardThemeWidget()
{
}


415
void KCardThemeWidget::setCurrentSelection( const QString & dirName )
416
{
417
    QModelIndex index = d->model->indexOf( dirName );
418
419
420
421
422
423
424
425
426
    if ( index.isValid() )
        d->listView->setCurrentIndex( index );
}


QString KCardThemeWidget::currentSelection() const
{
    QModelIndex index = d->listView->currentIndex();
    if ( index.isValid() )
427
        return d->model->data( index, Qt::UserRole ).toString();
428
429
430
431
    else
        return QString();
}

432

433
KCardThemeDialog::KCardThemeDialog( QWidget * parent, KConfigSkeleton * config, const QSet<QString> & requiredFeatures, const QString & previewString )
434
  : KConfigDialog( parent, QStringLiteral("KCardThemeDialog"), config )
435
436
{
    // Leaving the header text and icon empty prevents the header from being shown.
437
    addPage( new KCardThemeWidget( requiredFeatures, previewString, this ), QString() );
438
439

    setFaceType( KPageDialog::Plain );
Laurent Montel's avatar
Laurent Montel committed
440
    setStandardButtons( QDialogButtonBox::Ok | QDialogButtonBox::Apply | QDialogButtonBox::Cancel);
441
442
443
444
445
446
447
448
449
450
}


KCardThemeDialog::~KCardThemeDialog()
{
}


bool KCardThemeDialog::showDialog()
{
451
    return KConfigDialog::showDialog( QStringLiteral("KCardThemeDialog") );
452
453
}