1
/***************************************************************************
2
                      app.cpp  -  description
3
                         -------------------
4
begin                : Mit Okt 23 14:35:18 CEST 2002
5
copyright            : (C) 2002 by Mark Kretschmann
6
email                : markey@web.de
7
***************************************************************************/
8
9
/***************************************************************************
10
 *                                                                         *
11
 *   This program is free software; you can redistribute it and/or modify  *
12
 *   it under the terms of the GNU General Public License as published by  *
13
 *   the Free Software Foundation; either version 2 of the License, or     *
14
 *   (at your option) any later version.                                   *
15
 *                                                                         *
16
 ***************************************************************************/
17
18
#include "App.h"
19
20
#include "Amarok.h"
21
#include "amarokconfig.h"
22
#include "amarokurls/AmarokUrl.h"
23
#include "CollectionManager.h"
24
#include "ConfigDialog.h"
25
#include "covermanager/CoverFetcher.h"
26
#include "dbus/CollectionDBusHandler.h"
27
#include "Debug.h"
28
#include "EngineController.h"
29
#include "firstruntutorial/FirstRunTutorial.h"
30
#include "MainWindow.h"
31
#include "Meta.h"
32
#include "meta/MetaConstants.h"
33
#include "MountPointManager.h"
34
#include "Osd.h"
35
#include "PlayerDBusHandler.h"
36
#include "Playlist.h"
37
#include "PlaylistFileSupport.h"
38
#include "playlist/PlaylistActions.h"
39
#include "playlist/PlaylistModel.h"
40
#include "playlist/PlaylistController.h"
41
#include "playlistmanager/PlaylistManager.h"
42
#include "PluginManager.h"
43
#include "RootDBusHandler.h"
44
#include "ScriptManager.h"
45
#include "statusbar/StatusBar.h"
46
#include "Systray.h"
47
#include "TracklistDBusHandler.h"
48
49
#include <iostream>
50
51
#include <KAboutData>
52
#include <KAction>
53
#include <KCalendarSystem>
54
#include <KCmdLineArgs>                  //initCliArgs()
55
#include <KEditToolBar>                  //slotConfigToolbars()
56
#include <KGlobalSettings>
57
#include <KIO/CopyJob>
58
#include <KJob>
59
#include <KJobUiDelegate>
60
#include <KLocale>
61
#include <KShortcutsDialog>              //slotConfigShortcuts()
62
#include <KSplashScreen>
63
#include <KStandardDirs>
64
65
#include <QByteArray>
66
#include <QFile>
67
#include <KPixmapCache>
68
#include <QStringList>
69
#include <QTimer>                       //showHyperThreadingWarning()
70
#include <QtDBus/QtDBus>
71
72
#ifdef TAGLIB_EXTRAS_FOUND
73
#include <audiblefiletyperesolver.h>
74
#include <asffiletyperesolver.h>
75
#include <wavfiletyperesolver.h>
76
#include <realmediafiletyperesolver.h>
77
#include <mp4filetyperesolver.h>
78
#endif
79
80
QMutex Debug::mutex;
81
QMutex Amarok::globalDirsMutex;
82
QPointer<KActionCollection> Amarok::actionCollectionObject;
83
84
int App::mainThreadId = 0;
85
86
#ifdef Q_WS_MAC
87
#include <CoreFoundation/CoreFoundation.h>
88
extern void setupEventHandler_mac(long);
89
#endif
90
91
92
AMAROK_EXPORT KAboutData aboutData( "amarok", 0,
93
    ki18n( "Amarok" ), APP_VERSION,
94
    ki18n( "The audio player for KDE" ), KAboutData::License_GPL,
95
    ki18n( "(C) 2002-2003, Mark Kretschmann\n(C) 2003-2009, The Amarok Development Squad" ),
96
    ki18n( "IRC:\nirc.freenode.net - #amarok, #amarok.de, #amarok.es, #amarok.fr\n\nFeedback:\namarok@kde.org\n\n(Build Date: %1)" ).subs( __DATE__ ),
97
             ( "http://amarok.kde.org" ) );
98
99
App::App()
100
        : KUniqueApplication()
101
        , m_splash( 0 )
102
{
103
    DEBUG_BLOCK
104
    PERF_LOG( "Begin Application ctor" )
105
106
    // required for last.fm plugin to grab app version
107
    setApplicationVersion( APP_VERSION );
108
109
    if( AmarokConfig::showSplashscreen() && !isSessionRestored() )
110
    {
111
        PERF_LOG( "Init KStandardDirs cache" )
112
        KStandardDirs *stdDirs = KGlobal::dirs();
113
        PERF_LOG( "Finding image" )
114
        QString img = stdDirs->findResource( "data", "amarok/images/splash_screen.jpg" );
115
        PERF_LOG( "Creating pixmap" )
116
        QPixmap splashpix( img );
117
        PERF_LOG( "Creating splashscreen" )
118
        m_splash = new KSplashScreen( splashpix, Qt::WindowStaysOnTopHint );
119
        PERF_LOG( "showing splashscreen" )
120
        m_splash->show();
121
    }
122
123
#ifdef TAGLIB_EXTRAS_FOUND
124
    PERF_LOG( "Registering taglib plugins" )
125
    TagLib::FileRef::addFileTypeResolver(new MP4FileTypeResolver);
126
    TagLib::FileRef::addFileTypeResolver(new ASFFileTypeResolver);
127
    TagLib::FileRef::addFileTypeResolver(new RealMediaFileTypeResolver);
128
    TagLib::FileRef::addFileTypeResolver(new AudibleFileTypeResolver);
129
    TagLib::FileRef::addFileTypeResolver(new WavFileTypeResolver);
130
    PERF_LOG( "Done Registering taglib plugins" )
131
#endif
132
133
    qRegisterMetaType<Meta::DataPtr>();
134
    qRegisterMetaType<Meta::DataList>();
135
    qRegisterMetaType<Meta::TrackPtr>();
136
    qRegisterMetaType<Meta::TrackList>();
137
    qRegisterMetaType<Meta::AlbumPtr>();
138
    qRegisterMetaType<Meta::AlbumList>();
139
    qRegisterMetaType<Meta::ArtistPtr>();
140
    qRegisterMetaType<Meta::ArtistList>();
141
    qRegisterMetaType<Meta::GenrePtr>();
142
    qRegisterMetaType<Meta::GenreList>();
143
    qRegisterMetaType<Meta::ComposerPtr>();
144
    qRegisterMetaType<Meta::ComposerList>();
145
    qRegisterMetaType<Meta::YearPtr>();
146
    qRegisterMetaType<Meta::YearList>();
147
148
149
    //make sure we have enough cache space for all our crazy svg stuff
150
    KPixmapCache cache( "Amarok-pixmaps" );
151
    cache.setCacheLimit ( 20 * 1024 );
152
153
#ifdef Q_WS_MAC
154
    // this is inspired by OpenSceneGraph: osgDB/FilePath.cpp
155
156
    // Start with the the Bundle PlugIns directory.
157
158
    // Get the main bundle first. No need to retain or release it since
159
    //  we are not keeping a reference
160
    CFBundleRef myBundle = CFBundleGetMainBundle();
161
    if( myBundle )
162
    {
163
        // CFBundleGetMainBundle will return a bundle ref even if
164
        //  the application isn't part of a bundle, so we need to
165
        //  check
166
        //  if the path to the bundle ends in ".app" to see if it is
167
        //  a
168
        //  proper application bundle. If it is, the plugins path is
169
        //  added
170
        CFURLRef urlRef = CFBundleCopyBundleURL(myBundle);
171
        if(urlRef)
172
        {
173
            char bundlePath[1024];
174
            if( CFURLGetFileSystemRepresentation( urlRef, true, (UInt8 *)bundlePath, sizeof(bundlePath) ) )
175
            {
176
                QByteArray bp( bundlePath );
177
                size_t len = bp.length();
178
                if( len > 4 && bp.right( 4 ) == ".app" )
179
                {
180
                    bp.append( "/Contents/MacOS" );
181
                    QByteArray path = qgetenv( "PATH" );
182
                    if( path.length() > 0 )
183
                    {
184
                        path.prepend( ":" );
185
                    }
186
                    path.prepend( bp );
187
                    debug() << "setting PATH=" << path;
188
                    setenv("PATH", path, 1);
189
                }
190
            }
191
            // docs say we are responsible for releasing CFURLRef
192
            CFRelease(urlRef);
193
        }
194
    }
195
196
    setupEventHandler_mac((long)this);
197
#endif
198
199
    PERF_LOG( "Done App ctor" )
200
201
    continueInit();
202
}
203
204
App::~App()
205
{
206
    DEBUG_BLOCK
207
208
    delete m_splash;
209
    m_splash = 0;
210
211
    CollectionManager::instance()->stopScan();
212
213
    // Hiding the OSD before exit prevents crash
214
    Amarok::OSD::instance()->hide();
215
216
    if ( AmarokConfig::resumePlayback() )
217
    {
218
        if ( The::engineController()->state() != Phonon::StoppedState )
219
        {
220
            Meta::TrackPtr track = The::engineController()->currentTrack();
221
            if( track )
222
            {
223
                AmarokConfig::setResumeTrack( track->playableUrl().prettyUrl() );
224
                AmarokConfig::setResumeTime( The::engineController()->trackPosition() * 1000 );
225
                AmarokConfig::setLastPlaying( Playlist::Model::instance()->activeRow() );
226
            }
227
        }
228
        else
229
        {
230
            AmarokConfig::setResumeTrack( QString() ); //otherwise it'll play previous resume next time!
231
            AmarokConfig::setLastPlaying( -1 );
232
        }
233
    }
234
235
    The::engineController()->endSession(); //records final statistics
236
237
#ifndef Q_WS_MAC
238
    // do even if trayicon is not shown, it is safe
239
    Amarok::config().writeEntry( "HiddenOnExit", mainWindow()->isHidden() );
240
    AmarokConfig::self()->writeConfig();
241
#else
242
    // for some reason on OS X the main window always reports being hidden
243
    // this means if you have the tray icon enabled, amarok will always open minimized
244
    Amarok::config().writeEntry( "HiddenOnExit", false );
245
    AmarokConfig::self()->writeConfig();
246
#endif
247
248
    ScriptManager::destroy();
249
250
    // this must be deleted before the connection to the Xserver is
251
    // severed, or we risk a crash when the QApplication is exited,
252
    // I asked Trolltech! *smug*
253
    Amarok::OSD::destroy();
254
255
    // I tried this in the destructor for the Model but the object is destroyed after the
256
    // Config is written. Go figure!
257
    AmarokConfig::setLastPlaying( Playlist::Model::instance()->rowForTrack( Playlist::Model::instance()->activeTrack() ) );
258
259
    AmarokConfig::self()->writeConfig();
260
261
    //mainWindow()->deleteBrowsers();
262
    delete mainWindow();
263
264
    CollectionManager::destroy();
265
    MountPointManager::destroy();
266
    Playlist::Actions::destroy();
267
    Playlist::Model::destroy();
268
    PlaylistManager::destroy();
269
    EngineController::destroy();
270
    CoverFetcher::destroy();
271
272
273
#ifdef Q_WS_WIN
274
    // work around for KUniqueApplication being not completely implemented on windows
275
    QDBusConnectionInterface* dbusService;
276
    if (QDBusConnection::sessionBus().isConnected() && (dbusService = QDBusConnection::sessionBus().interface()))
277
    {
278
        dbusService->unregisterService("org.mpris.amarok");
279
    }
280
#endif
281
}
282
283
void
284
App::handleCliArgs() //static
285
{
286
    DEBUG_BLOCK
287
288
    KCmdLineArgs* const args = KCmdLineArgs::parsedArgs();
289
290
    if( args->isSet( "cwd" ) )
291
        KCmdLineArgs::setCwd( args->getOption( "cwd" ).toLocal8Bit() );
292
293
    bool haveArgs = false;
294
    if( args->count() > 0 )
295
    {
296
        haveArgs = true;
297
298
        KUrl::List list;
299
        for( int i = 0; i < args->count(); i++ )
300
        {
301
            KUrl url = args->url( i );
302
            //TODO:PORTME
303
//             if( url.protocol() == "itpc" || url.protocol() == "pcast" )
304
//                 PlaylistBrowserNS::instance()->addPodcast( url );
305
//             else
306
307
            if ( url.protocol() == "amarok" ) {
308
309
                AmarokUrl aUrl( url.url() );
310
                aUrl.run();
311
312
            } else {
313
                list << url;
314
                DEBUG_LINE_INFO
315
            }
316
        }
317
318
        int options = Playlist::AppendAndPlay;
319
        if( args->isSet( "queue" ) )
320
           options = Playlist::Queue;
321
        else if( args->isSet( "append" ) )
322
           options = Playlist::Append;
323
        else if( args->isSet( "load" ) )
324
            options = Playlist::Replace;
325
326
        if( args->isSet( "play" ) )
327
            options |= Playlist::DirectPlay;
328
329
        The::playlistController()->insertOptioned( list, options );
330
    }
331
332
    //we shouldn't let the user specify two of these since it is pointless!
333
    //so we prioritise, pause > stop > play > next > prev
334
    //thus pause is the least destructive, followed by stop as brakes are the most important bit of a car(!)
335
    //then the others seemed sensible. Feel free to modify this order, but please leave justification in the cvs log
336
    //I considered doing some sanity checks (eg only stop if paused or playing), but decided it wasn't worth it
337
    else if ( args->isSet( "pause" ) )
338
    {
339
        haveArgs = true;
340
        The::engineController()->pause();
341
    }
342
    else if ( args->isSet( "stop" ) )
343
    {
344
        haveArgs = true;
345
        The::engineController()->stop();
346
    }
347
    else if ( args->isSet( "play-pause" ) )
348
    {
349
        haveArgs = true;
350
        The::engineController()->playPause();
351
    }
352
    else if ( args->isSet( "play" ) ) //will restart if we are playing
353
    {
354
        haveArgs = true;
355
        The::engineController()->play();
356
    }
357
    else if ( args->isSet( "next" ) )
358
    {
359
        haveArgs = true;
360
        The::playlistActions()->next();
361
    }
362
    else if ( args->isSet( "previous" ) )
363
    {
364
        haveArgs = true;
365
        The::playlistActions()->back();
366
    }
367
    /*
368
    else if (args->isSet("cdplay"))
369
    {
370
        haveArgs = true;
371
        QString device = args->getOption("cdplay");
372
        KUrl::List urls;
373
        if (The::engineController()->getAudioCDContents(device, urls))
374
        {
375
            Meta::TrackList tracks = CollectionManager::instance()->tracksForUrls( urls );
376
            The::playlistController()->insertOptioned( tracks, Playlist::Replace|Playlist::DirectPlay );
377
        }
378
        else
379
        { // Default behaviour
380
            debug() << "Sorry, the engine does not support direct play from AudioCD...";
381
        }
382
    }
383
    */
384
385
    static bool firstTime = true;
386
    const bool debugWasJustEnabled = !Amarok::config().readEntry( "Debug Enabled", false ) && args->isSet( "debug" );
387
    const bool debugIsDisabled = !args->isSet( "debug" );
388
    //allows debugging on OS X. Bundles have to be started with "open". Therefore it is not possible to pass an argument
389
    const bool forceDebug = Amarok::config().readEntry( "Force Debug", false );
390
    
391
392
    Amarok::config().writeEntry( "Debug Enabled", forceDebug ? true : args->isSet( "debug" ) );
393
394
    // Debug output will only work from this point on. If Amarok was run without debug output before,
395
    // then a part of the output (until this point) will be missing. Inform the user about this:
396
    if( debugWasJustEnabled || forceDebug )
397
    {
398
        debug() << "************************************************************************************************************";
399
        debug() << "** DEBUGGING OUTPUT IS NOW ENABLED. PLEASE NOTE THAT YOU WILL ONLY SEE THE FULL OUTPUT ON THE NEXT START. **";
400
        debug() << "************************************************************************************************************";
401
    }
402
    else if( firstTime && debugIsDisabled )
403
    {
404
        Amarok::config().writeEntry( "Debug Enabled", true );
405
        debug() << "**********************************************************************************************";
406
        debug() << "** AMAROK WAS STARTED IN NORMAL MODE. IF YOU WANT TO SEE DEBUGGING INFORMATION, PLEASE USE: **";
407
        debug() << "** amarok --debug                                                                           **";
408
        debug() << "**********************************************************************************************";
409
        Amarok::config().writeEntry( "Debug Enabled", false );
410
    }
411
412
    if( !firstTime && !haveArgs )
413
        pApp->mainWindow()->activate();
414
    firstTime = false;
415
416
    args->clear();    //free up memory
417
}
418
419
420
/////////////////////////////////////////////////////////////////////////////////////
421
// INIT
422
/////////////////////////////////////////////////////////////////////////////////////
423
424
void
425
App::initCliArgs( int argc, char *argv[] )
426
{
427
    KCmdLineArgs::reset();
428
    KCmdLineArgs::init( argc, argv, &::aboutData ); //calls KCmdLineArgs::addStdCmdLineOptions()
429
    initCliArgs();
430
}
431
432
void
433
App::initCliArgs() //static
434
{
435
    KCmdLineOptions options;
436
437
    options.add("+[URL(s)]", ki18n( "Files/URLs to open" ));
438
    options.add("r");
439
    options.add("previous", ki18n( "Skip backwards in playlist" ));
440
    options.add("p");
441
    options.add("play", ki18n( "Start playing current playlist" ));
442
    options.add("t");
443
    options.add("play-pause", ki18n( "Play if stopped, pause if playing" ));
444
    options.add("pause", ki18n( "Pause playback" ));
445
    options.add("s");
446
    options.add("stop", ki18n( "Stop playback" ));
447
    options.add("f");
448
    options.add("next", ki18n( "Skip forwards in playlist" ));
449
    options.add(":", ki18n("Additional options:"));
450
    options.add("a");
451
    options.add("append", ki18n( "Append files/URLs to playlist" ));
452
    options.add("queue", ki18n("Queue URLs after the currently playing track"));
453
    options.add("l");
454
    options.add("load", ki18n("Load URLs, replacing current playlist"));
455
    options.add("d");
456
    options.add("debug", ki18n("Print verbose debugging information"));
457
    options.add("m");
458
    options.add("multipleinstances", ki18n("Allow running multiple Amarok instances"));
459
    options.add("cwd <directory>", ki18n( "Base for relative filenames/URLs" ));
460
    //options.add("cdplay <device>", ki18n("Play an AudioCD from <device> or system:/media/<device>"));
461
462
    KCmdLineArgs::addCmdLineOptions( options );   //add our own options
463
}
464
465
466
/////////////////////////////////////////////////////////////////////////////////////
467
// METHODS
468
/////////////////////////////////////////////////////////////////////////////////////
469
470
#include <id3v1tag.h>
471
#include <tbytevector.h>
472
#include <QTextCodec>
473
#include <KGlobal>
474
475
//this class is only used in this module, so I figured I may as well define it
476
//here and save creating another header/source file combination
477
478
// Local version of taglib's QStringToTString macro. It is here, because taglib's one is
479
// not Qt3Support clean (uses QString::utf8()). Once taglib will be clean of qt3support
480
// it is safe to use QStringToTString again
481
#define Qt4QStringToTString(s) TagLib::String(s.toUtf8().data(), TagLib::String::UTF8)
482
483
class ID3v1StringHandler : public TagLib::ID3v1::StringHandler
484
{
485
    QTextCodec *m_codec;
486
487
    virtual TagLib::String parse( const TagLib::ByteVector &data ) const
488
    {
489
        return Qt4QStringToTString( m_codec->toUnicode( data.data(), data.size() ) );
490
    }
491
492
    virtual TagLib::ByteVector render( const TagLib::String &ts ) const
493
    {
494
        const QByteArray qcs = m_codec->fromUnicode( TStringToQString(ts) );
495
        return TagLib::ByteVector( qcs, (uint) qcs.length() );
496
    }
497
498
public:
499
    ID3v1StringHandler( int codecIndex )
500
            : m_codec( QTextCodec::codecForName( QTextCodec::availableCodecs().at( codecIndex ) ) )
501
    {
502
        debug() << "codec: " << m_codec;
503
        debug() << "codec-name: " << m_codec->name();
504
    }
505
506
    ID3v1StringHandler( QTextCodec *codec )
507
            : m_codec( codec )
508
    {
509
        debug() << "codec: " << m_codec;
510
        debug() << "codec-name: " << m_codec->name();
511
    }
512
513
    virtual ~ID3v1StringHandler()
514
    {}
515
};
516
517
#undef Qt4QStringToTString
518
519
//SLOT
520
void App::applySettings( bool firstTime )
521
{
522
    ///Called when the configDialog is closed with OK or Apply
523
524
    DEBUG_BLOCK
525
526
    Amarok::OSD::instance()->applySettings();
527
    m_tray->setVisible( AmarokConfig::showTrayIcon() );
528
529
    //on startup we need to show the window, but only if it wasn't hidden on exit
530
    //and always if the trayicon isn't showing
531
    QWidget* main_window = mainWindow();
532
533
    if( ( main_window && firstTime && !Amarok::config().readEntry( "HiddenOnExit", false ) ) || ( main_window && !AmarokConfig::showTrayIcon() ) )
534
    {
535
        PERF_LOG( "showing main window again" )
536
        main_window->show();
537
        PERF_LOG( "after showing mainWindow" )
538
    }
539
540
    { //<Engine>
541
        if( The::engineController()->volume() != AmarokConfig::masterVolume() )
542
        {
543
            // block signals to make sure that the OSD isn't shown
544
            const bool osdEnabled = Amarok::OSD::instance()->isEnabled();
545
            Amarok::OSD::instance()->setEnabled( false );
546
            The::engineController()->setVolume( AmarokConfig::masterVolume() );
547
            Amarok::OSD::instance()->setEnabled( osdEnabled );
548
        }
549
550
        if( The::engineController()->isMuted() != AmarokConfig::muteState() )
551
            The::engineController()->setMuted( AmarokConfig::muteState() );
552
553
#if 0
554
    // Audio CD is not currently supported
555
    Amarok::actionCollection()->action( "play_audiocd" )->setEnabled( false );
556
#endif
557
558
    } //</Engine>
559
560
    {   // delete unneeded cover images from cache
561
        PERF_LOG( "Begin cover handling" )
562
        const QString size = QString::number( 100 ) + '@';
563
        const QDir cacheDir = Amarok::saveLocation( "albumcovers/cache/" );
564
        const QStringList obsoleteCovers = cacheDir.entryList( QStringList("*") );
565
        foreach( const QString &it, obsoleteCovers )
566
            //34@ is for playlist album cover images
567
            if ( !it.startsWith( size  ) && !it.startsWith( "50@" ) && !it.startsWith( "32@" ) && !it.startsWith( "34@" ) )
568
                QFile( cacheDir.filePath( it ) ).remove();
569
570
        PERF_LOG( "done cover handling" )
571
    }
572
573
    // show or hide CV
574
    if( mainWindow() )
575
        mainWindow()->hideContextView( AmarokConfig::hideContextView() );
576
    //if ( !firstTime )
577
        // Bizarrely and ironically calling this causes crashes for
578
        // some people! FIXME
579
        //AmarokConfig::self()->writeConfig();
580
}
581
582
//SLOT
583
void
584
App::continueInit()
585
{
586
    DEBUG_BLOCK
587
588
    PERF_LOG( "Begin App::continueInit" )
589
    const KCmdLineArgs* const args = KCmdLineArgs::parsedArgs();
590
    const bool restoreSession = args->count() == 0 || args->isSet( "append" ) || args->isSet( "queue" )
591
                                || Amarok::config().readEntry( "AppendAsDefault", false );
592
593
    QTextCodec* utf8codec = QTextCodec::codecForName( "UTF-8" );
594
    QTextCodec::setCodecForCStrings( utf8codec ); //We need this to make CollectionViewItem showing the right charecters.
595
596
    PERF_LOG( "Creating MainWindow" )
597
    m_mainWindow = new MainWindow();
598
    PERF_LOG( "Done creating MainWindow" )
599
600
    m_tray = new Amarok::TrayIcon( mainWindow() );
601
602
    PERF_LOG( "Creating DBus handlers" )
603
    new Amarok::RootDBusHandler();
604
    new Amarok::PlayerDBusHandler();
605
    new Amarok::TracklistDBusHandler();
606
    new CollectionDBusHandler( this );
607
    QDBusConnection::sessionBus().registerService("org.mpris.amarok");
608
    PERF_LOG( "Done creating DBus handlers" )
609
    //DON'T DELETE THIS NEXT LINE or the app crashes when you click the X (unless we reimplement closeEvent)
610
    //Reason: in ~App we have to call the deleteBrowsers method or else we run afoul of refcount foobar in KHTMLPart
611
    //But if you click the X (not Action->Quit) it automatically kills MainWindow because KMainWindow sets this
612
    //for us as default (bad KMainWindow)
613
    mainWindow()->setAttribute( Qt::WA_DeleteOnClose, false );
614
    //init playlist window as soon as the database is guaranteed to be usable
615
616
    //create engine, show TrayIcon etc.
617
    applySettings( true );
618
    // Start ScriptManager. Must be created _after_ MainWindow.
619
    PERF_LOG( "Starting ScriptManager" )
620
    ScriptManager::instance();
621
    PERF_LOG( "ScriptManager started" )
622
623
    if ( AmarokConfig::resumePlayback() && restoreSession && !args->isSet( "stop" ) ) {
624
        //restore session as long as the user didn't specify media to play etc.
625
        //do this after applySettings() so OSD displays correctly
626
        The::engineController()->restoreSession();
627
    }
628
629
    if( AmarokConfig::monitorChanges() )
630
        CollectionManager::instance()->checkCollectionChanges();
631
632
    // Restore keyboard shortcuts etc from config
633
    Amarok::actionCollection()->readSettings();
634
635
    delete m_splash;
636
    m_splash = 0;
637
    PERF_LOG( "App init done" )
638
    KConfigGroup config = KGlobal::config()->group( "General" );
639
640
    // NOTE: First Run Tutorial disabled for 2.1-beta1 release (too buggy / unfinished)
641
#if 0
642
    const bool firstruntut = config.readEntry( "FirstRunTutorial", true );
643
    debug() << "Checking whether to run first run tutorial..." << firstruntut;
644
    if( firstruntut )
645
    {
646
        config.writeEntry( "FirstRunTutorial", false );
647
        debug() << "Starting first run tutorial";
648
        FirstRunTutorial *frt = new FirstRunTutorial( mainWindow() );
649
        QTimer::singleShot( 1000, frt, SLOT( initOverlay() ) );
650
    }
651
652
    if( config.readEntry( "First Run", true ) )
653
    {
654
        slotConfigAmarok( "CollectionConfig" );
655
        config.writeEntry( "First Run", false );
656
    }
657
#endif
658
}
659
660
void App::slotConfigEqualizer() //SLOT
661
{
662
//    PORT 2.0
663
//    EqualizerSetup::instance()->show();
664
//    EqualizerSetup::instance()->raise();
665
}
666
667
void App::slotConfigAmarok( const QString& page )
668
{
669
    Amarok2ConfigDialog* dialog = static_cast<Amarok2ConfigDialog*>( KConfigDialog::exists( "settings" ) );
670
671
    if( !dialog )
672
    {
673
        //KConfigDialog didn't find an instance of this dialog, so lets create it :
674
        dialog = new Amarok2ConfigDialog( mainWindow(), "settings", AmarokConfig::self() );
675
676
        connect( dialog, SIGNAL( settingsChanged( const QString& ) ), SLOT( applySettings() ) );
677
    }
678
679
    dialog->show( page );
680
}
681
682
void App::slotConfigShortcuts()
683
{
684
    KShortcutsDialog::configure( Amarok::actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, mainWindow() );
685
    AmarokConfig::self()->writeConfig();
686
}
687
688
KIO::Job *App::trashFiles( const KUrl::List &files )
689
{
690
    KIO::Job *job = KIO::trash( files );
691
    The::statusBar()->newProgressOperation( job, i18n("Moving files to trash") );
692
    connect( job, SIGNAL( result( KJob* ) ), this, SLOT( slotTrashResult( KJob* ) ) );
693
    return job;
694
}
695
696
void App::slotTrashResult( KJob *job )
697
{
698
    if( job->error() )
699
        job->uiDelegate()->showErrorMessage();
700
}
701
702
void App::quit()
703
{
704
    emit prepareToQuit();
705
    /*
706
    if( MediaBrowser::instance() && MediaBrowser::instance()->blockQuit() )
707
    {
708
        // don't quit yet, as some media devices still have to finish transferring data
709
        QTimer::singleShot( 100, this, SLOT( quit() ) );
710
        return;
711
    }
712
    */
713
    KApplication::quit();
714
}
715
716
bool App::event( QEvent *event )
717
{
718
719
    switch( event->type() )
720
    {
721
        //allows Amarok to open files from the finder on OS X
722
        case QEvent::FileOpen:
723
        {
724
            QString file = static_cast<QFileOpenEvent*>( event )->file();
725
            //we are only going to receive local files here
726
            KUrl url( file );
727
            if( PlaylistManager::instance()->isPlaylist( url ) )
728
            {
729
                Meta::PlaylistPtr playlist = Meta::loadPlaylist( url );
730
                The::playlistController()->insertOptioned( playlist, Playlist::AppendAndPlay );
731
            }
732
            else
733
            {
734
                Meta::TrackPtr track = CollectionManager::instance()->trackForUrl( url );
735
                The::playlistController()->insertOptioned( track, Playlist::AppendAndPlay );
736
            }
737
            return true;
738
        }
739
        default:
740
            return KUniqueApplication::event( event );
741
    }
742
}
743
744
namespace Amarok
745
{
746
    /// @see amarok.h
747
748
    /*
749
    * Transform to be usable within HTML/HTML attributes
750
    */
751
    QString escapeHTMLAttr( const QString &s )
752
    {
753
        return QString(s).replace( '%', "%25" ).replace( '\'', "%27" ).replace( '"', "%22" ).
754
                replace( '#', "%23" ).replace( '?', "%3F" );
755
    }
756
    QString unescapeHTMLAttr( const QString &s )
757
    {
758
        return QString(s).replace( "%3F", "?" ).replace( "%23", "#" ).replace( "%22", "\"" ).
759
                replace( "%27", "'" ).replace( "%25", "%" );
760
    }
761
762
    /**
763
     * Function that must be used when separating contextBrowser escaped urls
764
     * detail can contain track/discnumber
765
     */
766
    void albumArtistTrackFromUrl( QString url, QString &artist, QString &album, QString &detail )
767
    {
768
        if ( !url.contains("@@@") ) return;
769
        //KHTML removes the trailing space!
770
        if ( url.endsWith( " @@@" ) )
771
            url += ' ';
772
773
        const QStringList list = url.split( " @@@ ", QString::KeepEmptyParts );
774
775
        int size = list.count();
776
777
        if( size<=0 )
778
            error() << "size<=0";
779
780
        artist = size > 0 ? unescapeHTMLAttr( list[0] ) : "";
781
        album  = size > 1 ? unescapeHTMLAttr( list[1] ) : "";
782
        detail = size > 2 ? unescapeHTMLAttr( list[2] ) : "";
783
    }
784
785
    QString verboseTimeSince( const QDateTime &datetime )
786
    {
787
        const QDateTime now = QDateTime::currentDateTime();
788
        const int datediff = datetime.daysTo( now );
789
790
        if( datediff >= 6*7 /*six weeks*/ ) {  // return absolute month/year
791
            const KCalendarSystem *cal = KGlobal::locale()->calendar();
792
            const QDate date = datetime.date();
793
            return i18nc( "monthname year", "%1 %2", cal->monthName(date),
794
                          cal->yearString(date, KCalendarSystem::LongFormat) );
795
        }
796
797
        //TODO "last week" = maybe within 7 days, but prolly before last sunday
798
799
        if( datediff >= 7 )  // return difference in weeks
800
            return i18np( "One week ago", "%1 weeks ago", (datediff+3)/7 );
801
802
        if( datediff == -1 )
803
            return i18nc( "When this track was last played", "Tomorrow" );
804
805
        const int timediff = datetime.secsTo( now );
806
807
        if( timediff >= 24*60*60 /*24 hours*/ )  // return difference in days
808
            return datediff == 1 ?
809
                    i18n( "Yesterday" ) :
810
                    i18np( "One day ago", "%1 days ago", (timediff+12*60*60)/(24*60*60) );
811
812
        if( timediff >= 90*60 /*90 minutes*/ )  // return difference in hours
813
            return i18np( "One hour ago", "%1 hours ago", (timediff+30*60)/(60*60) );
814
815
        //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently"
816
817
        if( timediff >= 0 )  // return difference in minutes
818
            return timediff/60 ?
819
                    i18np( "One minute ago", "%1 minutes ago", (timediff+30)/60 ) :
820
                    i18n( "Within the last minute" );
821
822
        return i18n( "The future" );
823
    }
824
825
    QString verboseTimeSince( uint time_t )
826
    {
827
        if( !time_t )
828
            return i18nc( "The amount of time since last played", "Never" );
829
830
        QDateTime dt;
831
        dt.setTime_t( time_t );
832
        return verboseTimeSince( dt );
833
    }
834
835
    QString conciseTimeSince( uint time_t )
836
    {
837
        if( !time_t )
838
            return i18nc( "The amount of time since last played", "0" );
839
840
        QDateTime datetime;
841
        datetime.setTime_t( time_t );
842
843
        const QDateTime now = QDateTime::currentDateTime();
844
        const int datediff = datetime.daysTo( now );
845
846
        if( datediff >= 6*7 /*six weeks*/ ) {  // return difference in months
847
            return i18nc( "number of months ago", "%1M", datediff/7/4 );
848
        }
849
850
        if( datediff >= 7 )  // return difference in weeks
851
            return i18nc( "w for weeks", "%1w", (datediff+3)/7 );
852
853
        if( datediff == -1 )
854
            return i18nc( "When this track was last played", "Tomorrow" );
855
856
        const int timediff = datetime.secsTo( now );
857
858
        if( timediff >= 24*60*60 /*24 hours*/ )  // return difference in days
859
            // xgettext: no-c-format
860
            return i18nc( "d for days", "%1d", (timediff+12*60*60)/(24*60*60) );
861
862
        if( timediff >= 90*60 /*90 minutes*/ )  // return difference in hours
863
            return i18nc( "h for hours", "%1h", (timediff+30*60)/(60*60) );
864
865
        //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently"
866
867
        if( timediff >= 60 )  // return difference in minutes
868
            return QString("%1'").arg( ( timediff + 30 )/60 );
869
        if( timediff >= 0 )  // return difference in seconds
870
            return QString("%1\"").arg( ( timediff + 1 )/60 );
871
872
        return i18n( "0" );
873
874
    }
875
876
    QWidget *mainWindow()
877
    {
878
        return pApp->mainWindow();
879
    }
880
881
    KActionCollection* actionCollection()  // TODO: constify?
882
    {
883
        if( !actionCollectionObject )
884
        {
885
            actionCollectionObject = new KActionCollection( pApp );
886
            actionCollectionObject->setObjectName( "Amarok-KActionCollection" );
887
        }
888
889
        return actionCollectionObject;
890
    }
891
892
    KConfigGroup config( const QString &group )
893
    {
894
        //Slightly more useful config() that allows setting the group simultaneously
895
        return KGlobal::config()->group( group );
896
    }
897
898
    namespace ColorScheme
899
    {
900
        QColor Base;
901
        QColor Text;
902
        QColor Background;
903
        QColor Foreground;
904
        QColor AltBase;
905
    }
906
907
    OverrideCursor::OverrideCursor( Qt::CursorShape cursor )
908
    {
909
        QApplication::setOverrideCursor( cursor == Qt::WaitCursor ?
910
                                        Qt::WaitCursor :
911
                                        Qt::BusyCursor );
912
    }
913
914
    OverrideCursor::~OverrideCursor()
915
    {
916
        QApplication::restoreOverrideCursor();
917
    }
918
919
    QString saveLocation( const QString &directory )
920
    {
921
        globalDirsMutex.lock();
922
        QString result = KGlobal::dirs()->saveLocation( "data", QString("amarok/") + directory, true );
923
        globalDirsMutex.unlock();
924
        return result;
925
    }
926
927
    QString cleanPath( const QString &path )
928
    {
929
        QString result = path;
930
        // german umlauts
931
        result.replace( QChar(0x00e4), "ae" ).replace( QChar(0x00c4), "Ae" );
932
        result.replace( QChar(0x00f6), "oe" ).replace( QChar(0x00d6), "Oe" );
933
        result.replace( QChar(0x00fc), "ue" ).replace( QChar(0x00dc), "Ue" );
934
        result.replace( QChar(0x00df), "ss" );
935
936
        // some strange accents
937
        result.replace( QChar(0x00e7), "c" ).replace( QChar(0x00c7), "C" );
938
        result.replace( QChar(0x00fd), "y" ).replace( QChar(0x00dd), "Y" );
939
        result.replace( QChar(0x00f1), "n" ).replace( QChar(0x00d1), "N" );
940
941
        // czech letters with carons
942
        result.replace( QChar(0x0161), "s" ).replace( QChar(0x0160), "S" );
943
        result.replace( QChar(0x010d), "c" ).replace( QChar(0x010c), "C" );
944
        result.replace( QChar(0x0159), "r" ).replace( QChar(0x0158), "R" );
945
        result.replace( QChar(0x017e), "z" ).replace( QChar(0x017d), "Z" );
946
        result.replace( QChar(0x0165), "t" ).replace( QChar(0x0164), "T" );
947
        result.replace( QChar(0x0148), "n" ).replace( QChar(0x0147), "N" );
948
        result.replace( QChar(0x010f), "d" ).replace( QChar(0x010e), "D" );
949
950
        // accented vowels
951
        QChar a[] = { 'a', 0xe0,0xe1,0xe2,0xe3,0xe5, 0 };
952
        QChar A[] = { 'A', 0xc0,0xc1,0xc2,0xc3,0xc5, 0 };
953
        QChar E[] = { 'e', 0xe8,0xe9,0xea,0xeb,0x11a, 0 };
954
        QChar e[] = { 'E', 0xc8,0xc9,0xca,0xcb,0x11b, 0 };
955
        QChar i[] = { 'i', 0xec,0xed,0xee,0xef, 0 };
956
        QChar I[] = { 'I', 0xcc,0xcd,0xce,0xcf, 0 };
957
        QChar o[] = { 'o', 0xf2,0xf3,0xf4,0xf5,0xf8, 0 };
958
        QChar O[] = { 'O', 0xd2,0xd3,0xd4,0xd5,0xd8, 0 };
959
        QChar u[] = { 'u', 0xf9,0xfa,0xfb,0x16e, 0 };
960
        QChar U[] = { 'U', 0xd9,0xda,0xdb,0x16f, 0 };
961
        QChar nul[] = { 0 };
962
        QChar *replacements[] = { a, A, e, E, i, I, o, O, u, U, nul };
963
964
        for( int i = 0; i < result.length(); i++ )
965
        {
966
            QChar c = result[ i ];
967
            for( uint n = 0; replacements[n][0] != QChar(0); n++ )
968
            {
969
                for( uint k=0; replacements[n][k] != QChar(0); k++ )
970
                {
971
                    if( replacements[n][k] == c )
972
                    {
973
                        c = replacements[n][0];
974
                    }
975
                }
976
            }
977
            result[ i ] = c;
978
        }
979
        return result;
980
    }
981
982
    QString asciiPath( const QString &path )
983
    {
984
        QString result = path;
985
        for( int i = 0; i < result.length(); i++ )
986
        {
987
            QChar c = result[ i ];
988
            if( c > QChar(0x7f) || c == QChar(0) )
989
            {
990
                c = '_';
991
            }
992
            result[ i ] = c;
993
        }
994
        return result;
995
    }
996
997
    QString vfatPath( const QString &path )
998
    {
999
        QString s = path;
1000
1001
        for( int i = 0; i < s.length(); i++ )
1002
        {
1003
            QChar c = s[ i ];
1004
            if( c < QChar(0x20)
1005
                    || c=='*' || c=='?' || c=='<' || c=='>'
1006
                    || c=='|' || c=='"' || c==':' || c=='/'
1007
                    || c=='\\' )
1008
                c = '_';
1009
            s[ i ] = c;
1010
        }
1011
1012
        uint len = s.length();
1013
        if( len == 3 || (len > 3 && s[3] == '.') )
1014
        {
1015
            QString l = s.left(3).toLower();
1016
            if( l=="aux" || l=="con" || l=="nul" || l=="prn" )
1017
                s = '_' + s;
1018
        }
1019
        else if( len == 4 || (len > 4 && s[4] == '.') )
1020
        {
1021
            QString l = s.left(3).toLower();
1022
            QString d = s.mid(3,1);
1023
            if( (l=="com" || l=="lpt") &&
1024
                    (d=="0" || d=="1" || d=="2" || d=="3" || d=="4" ||
1025
                     d=="5" || d=="6" || d=="7" || d=="8" || d=="9") )
1026
                s = '_' + s;
1027
        }
1028
1029
        while( s.startsWith( '.' ) )
1030
            s = s.mid(1);
1031
1032
        while( s.endsWith( '.' ) )
1033
            s = s.left( s.length()-1 );
1034
1035
        s = s.left(255);
1036
        len = s.length();
1037
        if( s[len-1] == ' ' )
1038
            s[len-1] = '_';
1039
1040
        return s;
1041
    }
1042
1043
    /* Strip the common prefix of two strings from the first one and trim
1044
     * whitespaces from the beginning of the resultant string.
1045
     * Case-insensitive.
1046
     *
1047
     * @param input the string being processed
1048
     * @param ref the string used to determine prefix
1049
     */
1050
    QString decapitateString( const QString &input, const QString &ref )
1051
    {
1052
        int len;    //the length of common prefix calculated so far
1053
        for ( len = 0; len < input.length() && len < ref.length(); len++ )
1054
        {
1055
            if ( input.at( len ).toUpper() != ref.at( len ).toUpper() )
1056
                break;
1057
        }
1058
1059
        return input.right( input.length() - len ).trimmed();
1060
    }
1061
1062
    KIO::Job *trashFiles( const KUrl::List &files ) { return App::instance()->trashFiles( files ); }
1063
}
1064
1065
int App::newInstance()
1066
{
1067
    DEBUG_BLOCK
1068
1069
    static bool first = true;
1070
    if ( isSessionRestored() && first )
1071
    {
1072
        first = false;
1073
        return 0;
1074
    }
1075
1076
    first = false;
1077
1078
    handleCliArgs();
1079
    return 0;
1080
}
1081
1082
1083
#include "App.moc"