collectionlist.cpp 16.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/**
 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
 *
 * 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/>.
 */
16

17 18
#include "collectionlist.h"

19
#include <kmessagebox.h>
Michael Pyne's avatar
Michael Pyne committed
20 21
#include <KConfigGroup>
#include <KSharedConfig>
22
#include <kactioncollection.h>
Laurent Montel's avatar
Laurent Montel committed
23
#include <ktoolbarpopupaction.h>
24
#include <kdirwatch.h>
Michael Pyne's avatar
Michael Pyne committed
25
#include <KLocalizedString>
26

27
#include <QList>
28 29
#include <QDragMoveEvent>
#include <QDropEvent>
30
#include <QApplication>
31 32
#include <QTimer>
#include <QTime>
Michael Pyne's avatar
Michael Pyne committed
33
#include <QMenu>
34
#include <QClipboard>
Laurent Montel's avatar
Laurent Montel committed
35
#include <QFileInfo>
Kacper Kasper's avatar
Kacper Kasper committed
36
#include <QHeaderView>
Michael Pyne's avatar
Michael Pyne committed
37
#include <QSaveFile>
38

39
#include "playlistcollection.h"
40
#include "stringshare.h"
41
#include "cache.h"
42
#include "actioncollection.h"
43 44
#include "tag.h"
#include "viewmode.h"
Michael Pyne's avatar
Michael Pyne committed
45
#include "juk_debug.h"
46

47
using ActionCollection::action;
48

49 50 51 52
////////////////////////////////////////////////////////////////////////////////
// static methods
////////////////////////////////////////////////////////////////////////////////

53
CollectionList *CollectionList::m_list = 0;
54 55 56

CollectionList *CollectionList::instance()
{
57
    return m_list;
58 59
}

60 61 62
static QTime stopwatch;

void CollectionList::startLoadingCachedItems()
63
{
Michael Pyne's avatar
Michael Pyne committed
64
    if(!m_list)
65
        return;
66

Michael Pyne's avatar
Michael Pyne committed
67
    qCDebug(JUK_LOG) << "Starting to load cached items";
68 69 70
    stopwatch.start();

    if(!Cache::instance()->prepareToLoadCachedItems()) {
Michael Pyne's avatar
Michael Pyne committed
71
        qCCritical(JUK_LOG) << "Unable to setup to load cache... perhaps it doesn't exist?";
72 73 74 75 76

        completedLoadingCachedItems();
        return;
    }

Michael Pyne's avatar
Michael Pyne committed
77
    qCDebug(JUK_LOG) << "Kicked off first batch";
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
    QTimer::singleShot(0, this, SLOT(loadNextBatchCachedItems()));
}

void CollectionList::loadNextBatchCachedItems()
{
    Cache *cache = Cache::instance();
    bool done = false;

    for(int i = 0; i < 20; ++i) {
        FileHandle cachedItem(cache->loadNextCachedItem());

        if(cachedItem.isNull()) {
            done = true;
            break;
        }

Michael Pyne's avatar
Michael Pyne committed
94
        // This may have already been created via a loaded playlist.
95 96
        if(!m_itemsDict.contains(cachedItem.absFilePath())) {
            CollectionListItem *newItem = new CollectionListItem(this, cachedItem);
97 98
            setupItem(newItem);
        }
Michael Pyne's avatar
Michael Pyne committed
99
    }
100

101 102 103 104 105 106 107 108 109 110
    if(!done) {
        QTimer::singleShot(0, this, SLOT(loadNextBatchCachedItems()));
    }
    else {
        completedLoadingCachedItems();
    }
}

void CollectionList::completedLoadingCachedItems()
{
111 112
    // The CollectionList is created with sorting disabled for speed.  Re-enable
    // it here, and perform the sort.
113
    KConfigGroup config(KSharedConfig::openConfig(), "Playlists");
114

115 116
    Qt::SortOrder order = Qt::DescendingOrder;
    if(config.readEntry("CollectionListSortAscending", true))
117
        order = Qt::AscendingOrder;
118

Kacper Kasper's avatar
Kacper Kasper committed
119
    m_list->sortByColumn(config.readEntry("CollectionListSortColumn", 1), order);
120

Michael Pyne's avatar
Michael Pyne committed
121 122
    qCDebug(JUK_LOG) << "Finished loading cached items, took" << stopwatch.elapsed() << "ms";
    qCDebug(JUK_LOG) << m_itemsDict.size() << "items are in the CollectionList";
123 124

    emit cachedItemsLoaded();
Michael Pyne's avatar
Michael Pyne committed
125 126 127 128 129 130 131 132 133 134 135 136 137 138
}

void CollectionList::initialize(PlaylistCollection *collection)
{
    if(m_list)
        return;

    // We have to delay initialization here because dynamic_cast or comparing to
    // the collection instance won't work in the PlaylistBox::Item initialization
    // won't work until the CollectionList is fully constructed.

    m_list = new CollectionList(collection);
    m_list->setName(i18n("Collection List"));

Pino Toscano's avatar
Pino Toscano committed
139
    collection->setupPlaylist(m_list, "folder-sound");
140 141 142 143 144 145
}

////////////////////////////////////////////////////////////////////////////////
// public methods
////////////////////////////////////////////////////////////////////////////////

146
CollectionListItem *CollectionList::createItem(const FileHandle &file, QTreeWidgetItem *)
147
{
148 149 150
    // It's probably possible to optimize the line below away, but, well, right
    // now it's more important to not load duplicate items.

151
    if(m_itemsDict.contains(file.absFilePath()))
152
        return nullptr;
153

154
    CollectionListItem *item = new CollectionListItem(this, file);
155

156
    if(!item->isValid()) {
Michael Pyne's avatar
Michael Pyne committed
157
        qCCritical(JUK_LOG) << "CollectionList::createItem() -- A valid tag was not created for \""
158
                 << file.absFilePath() << "\"";
159
        delete item;
160
        return nullptr;
161
    }
162 163 164

    setupItem(item);

165
    return item;
166 167
}

168 169
void CollectionList::clearItems(const PlaylistItemList &items)
{
170 171
    foreach(PlaylistItem *item, items) {
        delete item;
172 173
    }

174
    playlistItemsChanged();
175 176
}

177 178
void CollectionList::setupTreeViewEntries(ViewMode *viewMode) const
{
179
    TreeViewMode *treeViewMode = dynamic_cast<TreeViewMode *>(viewMode);
180
    if(!treeViewMode) {
Michael Pyne's avatar
Michael Pyne committed
181
        qCWarning(JUK_LOG) << "Can't setup entries on a non-tree-view mode!\n";
182
        return;
183 184
    }

185
    QList<int> columnList;
186 187 188 189
    columnList << PlaylistItem::ArtistColumn;
    columnList << PlaylistItem::GenreColumn;
    columnList << PlaylistItem::AlbumColumn;

190 191
    foreach(int column, columnList)
        treeViewMode->addItems(m_columnTags[column]->keys(), column);
192 193
}

194
void CollectionList::slotNewItems(const KFileItemList &items)
195 196 197
{
    QStringList files;

Laurent Montel's avatar
Laurent Montel committed
198
    for(KFileItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it)
Laurent Montel's avatar
Laurent Montel committed
199
        files.append((*it).url().path());
200

201
    addFiles(files);
202 203 204
    update();
}

205
void CollectionList::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> > &items)
206
{
207 208 209
    for(int i = 0; i < items.count(); ++i) {
        const KFileItem fileItem = items[i].second;
        CollectionListItem *item = lookup(fileItem.url().path());
210

211 212
        if(item) {
            item->refreshFromDisk();
213

214
            // If the item is no longer on disk, remove it from the collection.
215

216 217 218
            if(item->file().fileInfo().exists())
                item->repaint();
            else
219
                delete item;
220
        }
221 222 223 224 225
    }

    update();
}

226
void CollectionList::slotDeleteItems(const KFileItemList &items)
227
{
228 229 230
    for(const auto &item : items) {
        delete lookup(item.url().path());
    }
231 232
}

233 234
void CollectionList::saveItemsToCache() const
{
Michael Pyne's avatar
Michael Pyne committed
235
    qCDebug(JUK_LOG) << "Saving collection list to cache";
236

237
    QSaveFile f(Cache::fileHandleCacheFileName());
238 239

    if(!f.open(QIODevice::WriteOnly)) {
Michael Pyne's avatar
Michael Pyne committed
240
        qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString();
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
        return;
    }

    QByteArray data;
    QDataStream s(&data, QIODevice::WriteOnly);
    s.setVersion(QDataStream::Qt_4_3);

    QHash<QString, CollectionListItem *>::const_iterator it;
    for(it = m_itemsDict.begin(); it != m_itemsDict.end(); ++it) {
        s << it.key();
        s << (*it)->file();
    }

    QDataStream fs(&f);

    qint32 checksum = qChecksum(data.data(), data.size());

    fs << qint32(Cache::playlistItemsCacheVersion)
       << checksum
       << data;

Michael Pyne's avatar
Michael Pyne committed
262
    if(!f.commit())
Michael Pyne's avatar
Michael Pyne committed
263
        qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString();
264 265
}

266 267 268 269
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

270 271 272 273 274
void CollectionList::paste()
{
    decode(QApplication::clipboard()->mimeData());
}

275 276
void CollectionList::clear()
{
277 278 279 280 281
    int result = KMessageBox::warningContinueCancel(this,
        i18n("Removing an item from the collection will also remove it from "
             "all of your playlists. Are you sure you want to continue?\n\n"
             "Note, however, that if the directory that these files are in is in "
             "your \"scan on startup\" list, they will be readded on startup."));
282 283

    if(result == KMessageBox::Continue) {
284 285
        Playlist::clear();
        emit signalCollectionChanged();
286
    }
287 288
}

289 290
void CollectionList::slotCheckCache()
{
291
    PlaylistItemList invalidItems;
Michael Pyne's avatar
Michael Pyne committed
292
    qCDebug(JUK_LOG) << "Starting to check cached items for consistency";
293
    stopwatch.start();
294

295
    int i = 0;
296 297 298
    foreach(CollectionListItem *item, m_itemsDict) {
        if(!item->checkCurrent())
            invalidItems.append(item);
299
        if(++i == (m_itemsDict.size() / 2))
Michael Pyne's avatar
Michael Pyne committed
300
            qCDebug(JUK_LOG) << "Checkpoint";
301
    }
302 303

    clearItems(invalidItems);
304

Michael Pyne's avatar
Michael Pyne committed
305
    qCDebug(JUK_LOG) << "Finished consistency check, took" << stopwatch.elapsed() << "ms";
306 307
}

308 309
void CollectionList::slotRemoveItem(const QString &file)
{
310
    delete m_itemsDict[file];
311 312 313 314
}

void CollectionList::slotRefreshItem(const QString &file)
{
315 316
    if(m_itemsDict[file])
        m_itemsDict[file]->refresh();
317 318
}

319 320 321 322
////////////////////////////////////////////////////////////////////////////////
// protected methods
////////////////////////////////////////////////////////////////////////////////

323 324
CollectionList::CollectionList(PlaylistCollection *collection) :
    Playlist(collection, true),
325
    m_columnTags(15, 0)
326
{
Laurent Montel's avatar
Laurent Montel committed
327 328
    QAction *spaction = ActionCollection::actions()->addAction("showPlaying");
    spaction->setText(i18n("Show Playing"));
Laurent Montel's avatar
Laurent Montel committed
329
    connect(spaction, SIGNAL(triggered(bool)), SLOT(slotShowPlaying()));
330

Laurent Montel's avatar
Laurent Montel committed
331
    connect(action<KToolBarPopupAction>("back")->menu(), SIGNAL(aboutToShow()),
332
            this, SLOT(slotPopulateBackMenu()));
Laurent Montel's avatar
Laurent Montel committed
333 334
    connect(action<KToolBarPopupAction>("back")->menu(), SIGNAL(triggered(QAction*)),
            this, SLOT(slotPlayFromBackMenu(QAction*)));
Kacper Kasper's avatar
Kacper Kasper committed
335
    setSortingEnabled(false); // Temporarily disable sorting to add items faster.
336

337 338 339
    m_columnTags[PlaylistItem::ArtistColumn] = new TagCountDict;
    m_columnTags[PlaylistItem::AlbumColumn] = new TagCountDict;
    m_columnTags[PlaylistItem::GenreColumn] = new TagCountDict;
340 341 342

    // Even set to true it wouldn't work with this class due to other checks
    setAllowDuplicates(false);
343 344
}

345
CollectionList::~CollectionList()
346
{
347
    KConfigGroup config(KSharedConfig::openConfig(), "Playlists");
Kacper Kasper's avatar
Kacper Kasper committed
348 349
    config.writeEntry("CollectionListSortColumn", header()->sortIndicatorSection());
    config.writeEntry("CollectionListSortAscending", header()->sortIndicatorOrder() == Qt::AscendingOrder);
350

351 352 353 354 355 356
    // In some situations the dataChanged signal from clearItems will cause observers to
    // subsequently try to access a deleted item.  Since we're going away just remove all
    // observers.

    clearObservers();

357 358 359 360 361
    // The CollectionListItems will try to remove themselves from the
    // m_columnTags member, so we must make sure they're gone before we
    // are.

    clearItems(items());
362 363 364

    qDeleteAll(m_columnTags);
    m_columnTags.clear();
365
}
366

367
void CollectionList::dropEvent(QDropEvent *e)
368 369
{
    if(e->source() == this)
370 371
        return; // Don't rearrange in the CollectionList.
    else
372
        Playlist::dropEvent(e);
373 374
}

375
void CollectionList::dragMoveEvent(QDragMoveEvent *e)
376
{
377 378 379
    if(e->source() != this)
        Playlist::dragMoveEvent(e);
    else
380
        e->setAccepted(false);
381 382
}

383
QString CollectionList::addStringToDict(const QString &value, int column)
384
{
Laurent Montel's avatar
Laurent Montel committed
385
    if(column > m_columnTags.count() || value.trimmed().isEmpty())
386
        return QString();
387

388 389
    if(m_columnTags[column]->contains(value))
        ++((*m_columnTags[column])[value]);
390
    else {
391
        m_columnTags[column]->insert(value, 1);
392
        emit signalNewTag(value, column);
393 394 395 396 397 398
    }

    return value;
}

QStringList CollectionList::uniqueSet(UniqueSetType t) const
399
{
400 401 402 403 404 405 406
    int column;

    switch(t)
    {
    case Artists:
        column = PlaylistItem::ArtistColumn;
    break;
407

408 409 410
    case Albums:
        column = PlaylistItem::AlbumColumn;
    break;
411

412 413 414 415 416
    case Genres:
        column = PlaylistItem::GenreColumn;
    break;

    default:
417
        return QStringList();
418 419
    }

420 421
    return m_columnTags[column]->keys();
}
422

423 424
CollectionListItem *CollectionList::lookup(const QString &file) const
{
425
    return m_itemsDict.value(file, nullptr);
426 427
}

428
void CollectionList::removeStringFromDict(const QString &value, int column)
429
{
430
    if(column > m_columnTags.count() || value.trimmed().isEmpty())
431
        return;
432

433 434 435 436 437
    if(m_columnTags[column]->contains(value) &&
       --((*m_columnTags[column])[value])) // If the decrement goes to 0...
    {
        emit signalRemovedTag(value, column);
        m_columnTags[column]->remove(value);
438
    }
439 440
}

441 442 443 444 445 446 447 448 449 450
void CollectionList::addWatched(const QString &file)
{
    m_dirWatch->addFile(file);
}

void CollectionList::removeWatched(const QString &file)
{
    m_dirWatch->removeFile(file);
}

451
////////////////////////////////////////////////////////////////////////////////
452
// CollectionListItem public methods
453 454
////////////////////////////////////////////////////////////////////////////////

455
void CollectionListItem::refresh()
456
{
457
    int offset = CollectionList::instance()->columnOffset();
458
    int columns = lastColumn() + offset + 1;
459

460 461
    sharedData()->metadata.resize(columns);
    sharedData()->cachedWidths.resize(columns);
462 463

    for(int i = offset; i < columns; i++) {
464
        setText(i, text(i));
465 466 467
        int id = i - offset;
        if(id != TrackNumberColumn && id != LengthColumn) {
            // All columns other than track num and length need local-encoded data for sorting
468

469
            QString toLower = text(i).toLower();
470

471
            // For some columns, we may be able to share some strings
472

473 474 475 476
            if((id == ArtistColumn) || (id == AlbumColumn) ||
               (id == GenreColumn)  || (id == YearColumn)  ||
               (id == CommentColumn))
            {
Dirk Mueller's avatar
Dirk Mueller committed
477
                toLower = StringShare::tryShare(toLower);
478

479 480
                if(id != YearColumn && id != CommentColumn && sharedData()->metadata[id] != toLower) {
                    CollectionList::instance()->removeStringFromDict(sharedData()->metadata[id], id);
481 482 483
                    CollectionList::instance()->addStringToDict(text(i), id);
                }
            }
484

485
            sharedData()->metadata[id] = toLower;
486
        }
487

488
        int newWidth = treeWidget()->fontMetrics().width(text(i));
489
        if(newWidth != sharedData()->cachedWidths[i])
490
            playlist()->slotWeightDirty(i);
491

492
        sharedData()->cachedWidths[i] = newWidth;
493 494
    }

495
    for(PlaylistItemList::Iterator it = m_children.begin(); it != m_children.end(); ++it) {
496
        (*it)->playlist()->update();
497
        (*it)->playlist()->playlistItemsChanged();
498
    }
499 500
    if(treeWidget()->isVisible())
        treeWidget()->viewport()->update();
501

502
    CollectionList::instance()->playlistItemsChanged();
503
    emit CollectionList::instance()->signalCollectionChanged();
504
}
505

506
PlaylistItem *CollectionListItem::itemForPlaylist(const Playlist *playlist)
507
{
508
    if(playlist == CollectionList::instance())
509
        return this;
510

511
    PlaylistItemList::ConstIterator it;
Laurent Montel's avatar
Laurent Montel committed
512
    for(it = m_children.constBegin(); it != m_children.constEnd(); ++it)
513 514
        if((*it)->playlist() == playlist)
            return *it;
515 516 517
    return 0;
}

518 519 520 521 522
void CollectionListItem::updateCollectionDict(const QString &oldPath, const QString &newPath)
{
    CollectionList *collection = CollectionList::instance();

    if(!collection)
523
        return;
524 525 526 527 528

    collection->removeFromDict(oldPath);
    collection->addToDict(newPath, this);
}

529 530
void CollectionListItem::repaint() const
{
531
    // FIXME repaint
532
    /*QItemDelegate::repaint();
Laurent Montel's avatar
Laurent Montel committed
533
    for(PlaylistItemList::ConstIterator it = m_children.constBegin(); it != m_children.constEnd(); ++it)
534
        (*it)->repaint();*/
535 536
}

537
////////////////////////////////////////////////////////////////////////////////
538
// CollectionListItem protected methods
539 540
////////////////////////////////////////////////////////////////////////////////

541 542
CollectionListItem::CollectionListItem(CollectionList *parent, const FileHandle &file) :
    PlaylistItem(parent),
543
    m_shuttingDown(false)
544
{
545
    parent->addToDict(file.absFilePath(), this);
546

547
    sharedData()->fileHandle = file;
548

549 550
    if(file.tag()) {
        refresh();
551
        parent->playlistItemsChanged();
552 553
    }
    else {
554
        qCCritical(JUK_LOG) << "CollectionListItem::CollectionListItem() -- Tag() could not be created.";
555 556 557 558 559
    }
}

CollectionListItem::~CollectionListItem()
{
560 561
    m_shuttingDown = true;

562 563
    foreach(PlaylistItem *item, m_children)
        delete item;
564

565
    CollectionList *l = CollectionList::instance();
566
    if(l) {
567 568 569 570
        l->removeFromDict(file().absFilePath());
        l->removeStringFromDict(file().tag()->album(), AlbumColumn);
        l->removeStringFromDict(file().tag()->artist(), ArtistColumn);
        l->removeStringFromDict(file().tag()->genre(), GenreColumn);
571
    }
572 573 574 575
}

void CollectionListItem::addChildItem(PlaylistItem *child)
{
576 577 578 579 580 581
    m_children.append(child);
}

void CollectionListItem::removeChildItem(PlaylistItem *child)
{
    if(!m_shuttingDown)
582
        m_children.removeAll(child);
583 584
}

585
bool CollectionListItem::checkCurrent()
586
{
587
    if(!file().fileInfo().exists() || !file().fileInfo().isFile())
588
        return false;
589

590
    if(!file().current()) {
591 592
        file().refresh();
        refresh();
593
    }
594 595

    return true;
596 597
}

598
// vim: set et sw=4 tw=0 sta: