playermanager.cpp 19.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/**
 * Copyright (C) 2004 Scott Wheeler <wheeler@kde.org>
 * Copyright (C) 2007 Matthias Kretz <kretz@kde.org>
 * Copyright (C) 2008, 2009 Michael Pyne <mpyne@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/>.
 */
18

19 20
#include "playermanager.h"

21
#include <kdebug.h>
Matthias Kretz's avatar
Matthias Kretz committed
22
#include <kmessagebox.h>
23
#include <klocale.h>
24 25 26
#include <kactioncollection.h>
#include <kselectaction.h>
#include <ktoggleaction.h>
Andreas Pakulat's avatar
Andreas Pakulat committed
27
#include <kurl.h>
28

29 30 31
#include <Phonon/AudioOutput>
#include <Phonon/MediaObject>
#include <Phonon/VolumeFaderEffect>
32

Laurent Montel's avatar
Laurent Montel committed
33
#include <QPixmap>
34 35
#include <QTimer>

36 37
#include <math.h>

38
#include "playlistinterface.h"
39
#include "playeradaptor.h"
40
#include "slideraction.h"
41
#include "statuslabel.h"
42
#include "actioncollection.h"
43
#include "collectionlist.h"
Michael Pyne's avatar
Michael Pyne committed
44
#include "coverinfo.h"
45
#include "tag.h"
46
#include "scrobbler.h"
47
#include "juk.h"
48 49

using namespace ActionCollection;
50

Stephan Kulow's avatar
Stephan Kulow committed
51
enum PlayerManagerStatus { StatusStopped = -1, StatusPaused = 1, StatusPlaying = 2 };
52

53 54 55 56 57
////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

PlayerManager::PlayerManager() :
58
    QObject(),
59
    m_playlistInterface(0),
60
    m_statusLabel(0),
61
    m_setup(false),
62
    m_crossfadeTracks(true),
63
    m_curOutputPath(0)
64
{
Michael Pyne's avatar
Michael Pyne committed
65 66 67 68 69
// This class is the first thing constructed during program startup, and
// therefore has no access to the widgets needed by the setup() method.
// Since the setup() method will be called indirectly by the player() method
// later, just disable it here. -- mpyne
//    setup();
Laurent Montel's avatar
Laurent Montel committed
70
    new PlayerAdaptor( this );
71 72 73 74
}

PlayerManager::~PlayerManager()
{
75

76 77 78 79 80 81
}

////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

82 83
bool PlayerManager::playing() const
{
84
    if(!m_setup)
85 86
        return false;

87 88
    Phonon::State state = m_media[m_curOutputPath]->state();
    return (state == Phonon::PlayingState || state == Phonon::BufferingState);
89 90 91 92
}

bool PlayerManager::paused() const
{
93
    if(!m_setup)
94 95
        return false;

96
    return m_media[m_curOutputPath]->state() == Phonon::PausedState;
97 98
}

99 100 101 102 103 104 105 106
bool PlayerManager::muted() const
{
    if(!m_setup)
        return false;

    return m_output[m_curOutputPath]->isMuted();
}

107 108
float PlayerManager::volume() const
{
109
    if(!m_setup)
110
        return 1.0;
111

112
    return m_output[m_curOutputPath]->volume();
113 114
}

115 116
int PlayerManager::status() const
{
117
    if(!m_setup)
118
        return StatusStopped;
119

120
    if(paused())
121
        return StatusPaused;
122

123
    if(playing())
124
        return StatusPlaying;
125

126 127 128
    return 0;
}

Scott Wheeler's avatar
Scott Wheeler committed
129
int PlayerManager::totalTime() const
130 131 132 133 134 135 136 137 138 139
{
    return totalTimeMSecs() / 1000;
}

int PlayerManager::currentTime() const
{
    return currentTimeMSecs() / 1000;
}

int PlayerManager::totalTimeMSecs() const
140
{
141
    if(!m_setup)
142 143
        return 0;

144
    return m_media[m_curOutputPath]->totalTime();
145 146
}

147
int PlayerManager::currentTimeMSecs() const
148
{
149
    if(!m_setup)
150 151
        return 0;

152
    return m_media[m_curOutputPath]->currentTime();
153 154
}

155 156 157 158 159 160 161 162
bool PlayerManager::seekable() const
{
    if(!m_setup)
        return false;

    return m_media[m_curOutputPath]->isSeekable();
}

163 164 165 166 167 168 169
QStringList PlayerManager::trackProperties()
{
    return FileHandle::properties();
}

QString PlayerManager::trackProperty(const QString &property) const
{
Michael Pyne's avatar
Michael Pyne committed
170
    if(!playing() && !paused())
171
        return QString();
172 173 174 175

    return m_file.property(property);
}

Michael Pyne's avatar
Michael Pyne committed
176 177 178 179 180
QPixmap PlayerManager::trackCover(const QString &size) const
{
    if(!playing() && !paused())
        return QPixmap();

181
    if(size.toLower() == "small")
Michael Pyne's avatar
Michael Pyne committed
182
        return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail);
183
    if(size.toLower() == "large")
Michael Pyne's avatar
Michael Pyne committed
184 185 186 187 188
        return m_file.coverInfo()->pixmap(CoverInfo::FullSize);

    return QPixmap();
}

189 190 191 192 193
FileHandle PlayerManager::playingFile() const
{
    return m_file;
}

194 195
QString PlayerManager::playingString() const
{
196
    if(!playing() || m_file.isNull())
197
        return QString();
198

199
    return m_file.tag()->playingString();
200 201
}

202
void PlayerManager::setPlaylistInterface(PlaylistInterface *interface)
203 204 205 206
{
    m_playlistInterface = interface;
}

207 208 209 210 211
void PlayerManager::setStatusLabel(StatusLabel *label)
{
    m_statusLabel = label;
}

212 213 214 215
////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

216
void PlayerManager::play(const FileHandle &file)
217
{
218
    if(!m_setup)
219 220
        setup();

221
    if(!m_media[0] || !m_media[1] || !m_playlistInterface)
222 223
        return;

224 225 226 227
    stopCrossfade();

    // The "currently playing" media object.
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
228
    
229
    if(file.isNull()) {
230
        if(paused())
231
            mediaObject->play();
232
        else if(playing()) {
233
            mediaObject->seek(0);
234
            emit seeked(0);
235
        }
236
        else {
237
            m_playlistInterface->playNext();
238
            m_file = m_playlistInterface->currentFile();
239

240
            if(!m_file.isNull())
241
            {
242 243
                mediaObject->setCurrentSource(KUrl::fromPath(m_file.absFilePath()));
                mediaObject->play();
244 245

                emit signalItemChanged(m_file);
246
            }
247
        }
248
    }
249
    else {
250
        mediaObject->setCurrentSource(KUrl::fromPath(file.absFilePath()));
251
        mediaObject->play();
252 253 254 255 256

        if(m_file != file)
            emit signalItemChanged(file);

        m_file = file;
257
    }
258

259 260
    // Our state changed handler will perform the follow up actions necessary
    // once we actually start playing.
261 262
}

263 264 265 266 267 268 269 270 271
void PlayerManager::play(const QString &file)
{
    CollectionListItem *item = CollectionList::instance()->lookup(file);
    if(item) {
        Playlist::setPlaying(item);
        play(item->file());
    }
}

Scott Wheeler's avatar
Scott Wheeler committed
272 273 274 275 276
void PlayerManager::play()
{
    play(FileHandle::null());
}

277 278
void PlayerManager::pause()
{
279
    if(!m_setup)
280 281
        return;

282
    if(paused()) {
283 284 285 286
        play();
        return;
    }

287
    action("pause")->setEnabled(false);
288

289
    m_media[m_curOutputPath]->pause();
290 291 292 293
}

void PlayerManager::stop()
{
294
    if(!m_setup || !m_playlistInterface)
295 296
        return;

297 298 299 300
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
301
    action("forwardAlbum")->setEnabled(false);
302

303 304 305 306
    // Fading out playback is for chumps.
    stopCrossfade();
    m_media[0]->stop();
    m_media[1]->stop();
307 308 309 310 311

    if(!m_file.isNull()) {
        m_file = FileHandle::null();
        emit signalItemChanged(m_file);
    }
312 313
}

Laurent Montel's avatar
Laurent Montel committed
314
void PlayerManager::setVolume(float volume)
315
{
316
    if(!m_setup)
317
        setup();
318

319 320
    m_output[0]->setVolume(volume);
    m_output[1]->setVolume(volume);
321 322
}

323
void PlayerManager::seek(int seekTime)
324
{
325
    if(!m_setup || m_media[m_curOutputPath]->currentTime() == seekTime)
326
        return;
327

328 329
    kDebug() << "Stopping crossfade to seek from" << m_media[m_curOutputPath]->currentTime()
             << "to" << seekTime;
330 331
    stopCrossfade();
    m_media[m_curOutputPath]->seek(seekTime);
332
    emit seeked(seekTime);
333 334 335 336
}

void PlayerManager::seekForward()
{
337 338 339
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
    const qint64 total = mediaObject->totalTime();
    const qint64 newtime = mediaObject->currentTime() + total / 100;
340
    const qint64 seekTo = qMin(total, newtime);
341 342

    stopCrossfade();
343 344
    mediaObject->seek(seekTo);
    emit seeked(seekTo);
345 346 347 348
}

void PlayerManager::seekBack()
{
349 350 351
    Phonon::MediaObject *mediaObject = m_media[m_curOutputPath];
    const qint64 total = mediaObject->totalTime();
    const qint64 newtime = mediaObject->currentTime() - total / 100;
352
    const qint64 seekTo = qMax(qint64(0), newtime);
353 354

    stopCrossfade();
355 356
    mediaObject->seek(seekTo);
    emit seeked(seekTo);
357 358
}

359 360
void PlayerManager::playPause()
{
David Faure's avatar
David Faure committed
361
    playing() ? action("pause")->trigger() : action("play")->trigger();
362 363
}

364 365
void PlayerManager::forward()
{
366 367
    m_playlistInterface->playNext();
    FileHandle file = m_playlistInterface->currentFile();
368

369 370 371 372 373 374 375 376
    if(!file.isNull())
        play(file);
    else
        stop();
}

void PlayerManager::back()
{
377 378
    m_playlistInterface->playPrevious();
    FileHandle file = m_playlistInterface->currentFile();
379

380 381 382 383 384 385
    if(!file.isNull())
        play(file);
    else
        stop();
}

386 387
void PlayerManager::volumeUp()
{
388
    if(!m_setup)
389
        return;
390

391
    setVolume(volume() + 0.04); // 4% up
392 393 394 395
}

void PlayerManager::volumeDown()
{
396
    if(!m_output)
397
        return;
398

399
    setVolume(volume() - 0.04); // 4% down
400 401
}

402
void PlayerManager::setMuted(bool m)
403
{
404
    if(!m_setup)
405 406
        return;

407 408 409 410 411 412 413 414 415 416 417
    m_output[m_curOutputPath]->setMuted(m);
}

bool PlayerManager::mute()
{
    if(!m_setup)
        return false;

    bool newState = !muted();
    setMuted(newState);
    return newState;
418 419
}

420 421 422
////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////
423

424 425
void PlayerManager::slotNeedNextUrl()
{
426
    if(m_file.isNull() || !m_crossfadeTracks)
Matthias Kretz's avatar
Matthias Kretz committed
427
        return;
428

429 430
    m_playlistInterface->playNext();
    FileHandle nextFile = m_playlistInterface->currentFile();
431 432

    if(!nextFile.isNull()) {
433
        m_file = nextFile;
434
        crossfadeToFile(m_file);
435 436 437
    }
}

438 439
void PlayerManager::slotFinished()
{
440 441 442 443
    // It is possible to end up in this function if a file simply fails to play or if the
    // user moves the slider all the way to the end, therefore see if we can keep playing
    // and if we can, do so.  Otherwise, stop.  Note that this slot should
    // only be called by the currently "main" output path (i.e. not from the
444 445 446 447 448 449
    // crossfading one).  However life isn't always so nice apparently, so do some
    // sanity-checking.

    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(mediaObject != m_media[m_curOutputPath])
        return;
450

451 452
    m_playlistInterface->playNext();
    m_file = m_playlistInterface->currentFile();
453

454 455 456 457
    if(m_file.isNull()) {
        stop();
    }
    else {
458
        emit signalItemChanged(m_file);
459
        m_media[m_curOutputPath]->setCurrentSource(QUrl::fromLocalFile(m_file.absFilePath()));
460 461
        m_media[m_curOutputPath]->play();
    }
462 463
}

464 465 466
void PlayerManager::slotLength(qint64 msec)
{
    m_statusLabel->setItemTotalTime(msec / 1000);
467
    emit totalTimeChanged(msec);
468 469
}

470
void PlayerManager::slotTick(qint64 msec)
471
{
472
    if(!m_setup || !m_playlistInterface)
473
        return;
474

475
    if(m_statusLabel)
476
        m_statusLabel->setItemCurrentTime(msec / 1000);
477 478

    emit tick(msec);
479 480
}

481
void PlayerManager::slotStateChanged(Phonon::State newstate, Phonon::State oldstate)
Matthias Kretz's avatar
Matthias Kretz committed
482
{
483 484 485 486 487 488
    // Use sender() since either media object may have sent the signal.
    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(!mediaObject)
        return;

    // Handle errors for either media object
489
    if(newstate == Phonon::ErrorState) {
490 491 492
        QString errorMessage =
            i18nc(
              "%1 will be the /path/to/file, %2 will be some string from Phonon describing the error",
493 494
              "JuK is unable to play the audio file<nl/><filename>%1</filename><nl/>"
                "for the following reason:<nl/><message>%2</message>",
495
              m_file.absFilePath(),
496
              mediaObject->errorString()
497 498
            );

499
        switch(mediaObject->errorType()) {
500 501 502
            case Phonon::NoError:
                kDebug() << "received a state change to ErrorState but errorType is NoError!?";
                break;
503

Matthias Kretz's avatar
Matthias Kretz committed
504 505
            case Phonon::NormalError:
                forward();
506
                KMessageBox::information(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
507
                break;
508

Matthias Kretz's avatar
Matthias Kretz committed
509 510
            case Phonon::FatalError:
                stop();
511
                KMessageBox::sorry(0, errorMessage);
Matthias Kretz's avatar
Matthias Kretz committed
512 513 514
                break;
        }
    }
515

516 517 518 519 520
    // Now bail out if we're not dealing with the currently playing media
    // object.

    if(mediaObject != m_media[m_curOutputPath])
        return;
521

522 523 524 525 526 527
    // Handle state changes for the playing media object.
    if(newstate == Phonon::StoppedState && oldstate != Phonon::LoadingState) {
        // If this occurs it should be due to a transitory shift (i.e. playing a different
        // song when one is playing now), since it didn't occur in the error handler.  Just
        // in case we really did abruptly stop, handle that case in a couple of seconds.
        QTimer::singleShot(2000, this, SLOT(slotUpdateGuiIfStopped()));
528 529

        JuK::JuKInstance()->setWindowTitle(i18n("JuK"));
530

Michael Pyne's avatar
Michael Pyne committed
531
        emit signalStop();
532
    }
533 534 535 536
    else if(newstate == Phonon::PausedState) {
        emit signalPause();
    }
    else { // PlayingState or BufferingState
537 538 539 540 541 542 543
        action("pause")->setEnabled(true);
        action("stop")->setEnabled(true);
        action("forward")->setEnabled(true);
        if(action<KToggleAction>("albumRandomPlay")->isChecked())
            action("forwardAlbum")->setEnabled(true);
        action("back")->setEnabled(true);

544 545
        JuK::JuKInstance()->setWindowTitle(i18nc(
            "%1 is the artist and %2 is the title of the currently playing track.", 
546 547
            "%1 - %2 :: JuK",
            m_file.tag()->artist(),
548
            m_file.tag()->title()));
549

550 551
        emit signalPlay();
    }
552 553
}

554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
void PlayerManager::slotSeekableChanged(bool isSeekable)
{
    // Use sender() since either media object may have sent the signal.
    Phonon::MediaObject *mediaObject = qobject_cast<Phonon::MediaObject *>(sender());
    if(!mediaObject)
        return;
    if(mediaObject != m_media[m_curOutputPath])
        return;

    emit seekableChanged(isSeekable);
}

void PlayerManager::slotMutedChanged(bool muted)
{
    // Use sender() since either output object may have sent the signal.
    Phonon::AudioOutput *output = qobject_cast<Phonon::AudioOutput *>(sender());
    if(!output)
        return;

573 574 575
    if(output != m_output[m_curOutputPath] ||
            m_output[m_curOutputPath]->isMuted() == muted)
    {
576
        return;
577
    }
578 579 580 581 582 583 584 585 586 587 588

    emit mutedChanged(muted);
}

void PlayerManager::slotVolumeChanged(qreal volume)
{
    // Use sender() since either output object may have sent the signal.
    Phonon::AudioOutput *output = qobject_cast<Phonon::AudioOutput *>(sender());
    if(!output)
        return;

589 590 591
    if(output != m_output[m_curOutputPath] ||
            qFuzzyCompare(m_output[m_curOutputPath]->volume(), volume))
    {
592
        return;
593
    }
594 595 596 597

    emit volumeChanged(volume);
}

598 599 600 601 602 603 604 605
////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

void PlayerManager::setup()
{
    // All of the actions required by this class should be listed here.

606 607 608
    if(!action("pause") ||
       !action("stop") ||
       !action("back") ||
609
       !action("forwardAlbum") ||
610 611
       !action("forward") ||
       !action("trackPositionAction"))
612
    {
613
        kWarning() << "Could not find all of the required actions.";
614 615 616
        return;
    }

617 618 619 620
    if(m_setup)
        return;
    m_setup = true;

621 622 623 624 625 626
    // We use two audio paths at all times to make cross fading easier (and to also easily
    // support not using cross fading with the same code).  The currently playing audio
    // path is controlled using m_curOutputPath.

    for(int i = 0; i < 2; ++i) {
        m_output[i] = new Phonon::AudioOutput(Phonon::MusicCategory, this);
627 628
        connect(m_output[i], SIGNAL(mutedChanged(bool)), SLOT(slotMutedChanged(bool)));
        connect(m_output[i], SIGNAL(volumeChanged(qreal)), SLOT(slotVolumeChanged(qreal)));
629

630 631 632
        m_media[i] = new Phonon::MediaObject(this);
        m_audioPath[i] = Phonon::createPath(m_media[i], m_output[i]);
        m_media[i]->setTickInterval(200);
633
        m_media[i]->setPrefinishMark(2000);
634

635 636 637 638 639
        // Pre-cache a volume fader object
        m_fader[i] = new Phonon::VolumeFaderEffect(m_media[i]);
        m_audioPath[i].insertEffect(m_fader[i]);
        m_fader[i]->setVolume(1.0f);

Laurent Montel's avatar
Laurent Montel committed
640
        connect(m_media[i], SIGNAL(stateChanged(Phonon::State,Phonon::State)), SLOT(slotStateChanged(Phonon::State,Phonon::State)));
641
        connect(m_media[i], SIGNAL(prefinishMarkReached(qint32)), SLOT(slotNeedNextUrl()));
642 643 644
        connect(m_media[i], SIGNAL(totalTimeChanged(qint64)), SLOT(slotLength(qint64)));
        connect(m_media[i], SIGNAL(tick(qint64)), SLOT(slotTick(qint64)));
        connect(m_media[i], SIGNAL(finished()), SLOT(slotFinished()));
645
        connect(m_media[i], SIGNAL(seekableChanged(bool)), SLOT(slotSeekableChanged(bool)));
646
    }
647

648 649
    // initialize action states

650 651 652 653
    action("pause")->setEnabled(false);
    action("stop")->setEnabled(false);
    action("back")->setEnabled(false);
    action("forward")->setEnabled(false);
654
    action("forwardAlbum")->setEnabled(false);
655

656
    QDBusConnection::sessionBus().registerObject("/Player", this);
657 658 659
}

void PlayerManager::slotUpdateGuiIfStopped()
660
{
661 662 663
    if(m_media[0]->state() == Phonon::StoppedState && m_media[1]->state() == Phonon::StoppedState)
        stop();
}
664

665 666 667
void PlayerManager::crossfadeToFile(const FileHandle &newFile)
{
    int nextOutputPath = 1 - m_curOutputPath;
668

669 670 671
    // Don't need this anymore
    disconnect(m_media[m_curOutputPath], SIGNAL(finished()), this, 0);
    connect(m_media[nextOutputPath], SIGNAL(finished()), SLOT(slotFinished()));
672

673
    m_fader[nextOutputPath]->setVolume(0.0f);
674

675
    emit signalItemChanged(newFile);
676
    m_media[nextOutputPath]->setCurrentSource(QUrl::fromLocalFile(newFile.absFilePath()));
677
    m_media[nextOutputPath]->play();
678

679 680
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[m_curOutputPath]->fadeTo(0.0f, 2000);
681

682
    m_fader[nextOutputPath]->fadeTo(1.0f, 2000);
683

684 685
    m_curOutputPath = nextOutputPath;
}
686

687 688 689 690
void PlayerManager::stopCrossfade()
{
    // According to the Phonon docs, setVolume immediately takes effect,
    // which is "good enough for government work" ;)
691

692 693 694
    // 1 - curOutputPath is the other output path...
    m_fader[m_curOutputPath]->setVolume(1.0f);
    m_fader[1 - m_curOutputPath]->setVolume(0.0f);
695

696 697 698 699 700 701 702 703
    // We don't actually need to physically stop crossfading as the playback
    // code will call ->play() when necessary anyways.  If we hit stop()
    // here instead of pause() then we will trick our stateChanged handler
    // into thinking Phonon had a spurious stop and we'll switch tracks
    // unnecessarily.  (This isn't a problem after crossfade completes due to
    // the signals being disconnected).

    m_media[1 - m_curOutputPath]->pause();
704 705
}

Michael Pyne's avatar
Michael Pyne committed
706 707 708 709 710 711 712 713 714
QString PlayerManager::randomPlayMode() const
{
    if(action<KToggleAction>("randomPlay")->isChecked())
        return "Random";
    if(action<KToggleAction>("albumRandomPlay")->isChecked())
        return "AlbumRandom";
    return "NoRandom";
}

715 716 717 718 719
void PlayerManager::setCrossfadeEnabled(bool crossfadeEnabled)
{
    m_crossfadeTracks = crossfadeEnabled;
}

Michael Pyne's avatar
Michael Pyne committed
720 721
void PlayerManager::setRandomPlayMode(const QString &randomMode)
{
722
    if(randomMode.toLower() == "random")
Michael Pyne's avatar
Michael Pyne committed
723
        action<KToggleAction>("randomPlay")->setChecked(true);
724
    if(randomMode.toLower() == "albumrandom")
Michael Pyne's avatar
Michael Pyne committed
725
        action<KToggleAction>("albumRandomPlay")->setChecked(true);
726
    if(randomMode.toLower() == "norandom")
Michael Pyne's avatar
Michael Pyne committed
727 728 729
        action<KToggleAction>("disableRandomPlay")->setChecked(true);
}

730
#include "playermanager.moc"
731

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