diff options
author | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
---|---|---|
committer | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
commit | e2de64d6f1beb9e492daf5b886e19933c1fa41dd (patch) | |
tree | 9047cf9e6b5c43878d5bf82660adae77ceee097a /juk | |
download | tdemultimedia-e2de64d6f1beb9e492daf5b886e19933c1fa41dd.tar.gz tdemultimedia-e2de64d6f1beb9e492daf5b886e19933c1fa41dd.zip |
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdemultimedia@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'juk')
152 files changed, 29928 insertions, 0 deletions
diff --git a/juk/HACKING b/juk/HACKING new file mode 100644 index 00000000..2fb4d392 --- /dev/null +++ b/juk/HACKING @@ -0,0 +1,147 @@ +Since I tend to be one of the more pedantic people on coding style I'll provide +a bit of a reference here. Please take a few minutes to read this as it will +save us both time when processing patches. + +================================================================================ +Indentation +================================================================================ + +The older files in JuK are indented using Qt-style. The first level was 4 +spaces, the second one tab, the third one tab followed by 4 spaces. I'm not +particularly fond of this style anymore, but it used to be the Emacs default +when using the KDE Emacs scripts. + +static void foo() +{ + if(bar()) // <-- 4 spaces + baz() // <-- 1 tab +} + +Newer files simply use 4 spaces at all levels. In most cases the style of the +file currently being worked in should be followed. So: + +static void foo() +{ + if(bar()) // <-- 4 spaces + baz() // <-- 8 spaces +} + +================================================================================ +Braces +================================================================================ + +Braces opening classes, structs, namespaces and functions should be on their own +line. Braces opening conditionals should be on the same line with one notable +exception: if the conditional is split up onto several lines then the opening +brace should be on its own line. i.e. + +class Foo +{ + // stuff +}; + +if(foo == bar) { + // stuff +} + +while(foo == bar && + baz == quux && + flop == pop) +{ + // stuff +} + +static void foo() +{ + // stuff +} + +Other exceptions include inline functions that are just returning or setting a +value. However multiple statements should not ever be combined onto one line: + +class Foo +{ +public: + String value() const { return m_value; } +}; + +Also conditionals / loops that only contiain one line in their body (but where +the conditional statement fits onto one line) should omit braces: + +if(foo == bar) + baz(); + +But: + +if(baz == quux && + ralf == spot) +{ + bar(); +} + +================================================================================ +Spaces +================================================================================ + +Spaces should not be used between the conditional / loop type and the +conditional statement. They should also not be used after parenthesis. However +the should be to mark of mathematical or comparative operators. + +if ( foo == bar ) + ^ ^ ^ + +The marked spaces should be ommitted to produce: + +if(foo == bar) + +================================================================================ +Header Organization +================================================================================ + +Member variables should always be private and prefixed with "m_". Accessors may +be inline in the headers. The organization of the members in a class should be +roughly as follows: + +public: +public slots: +protected: +protected slots: +signals: +private: // member funtions +private slots: +private: // member variables + +If there are no private slots there is no need for two private sections, however +private functions and private variables should be clearly separated. + +The implementations files -- .cpp files -- should follow (when possible) the +same order of function declarations as the header files. + +Virtual functions should always be marked as such even in derrived classes where +it is not strictly necessary. + +================================================================================ +Whitespace +================================================================================ + +Whitespace should be used liberally. When blocks of code are logically distinct +I tend to put a blank line between them. This is difficult to explain +systematically but after looking a bit at the current code organization this +ideally will be somewhat clear. + +Also I tend to separate comments by blank lines on both sides. + +================================================================================ +Pointer and Reference Operators +================================================================================ + +This one is pretty simple. I prefer "Foo *f" to "Foo* f" in function signatures +and declarations. The same goes for "Foo &f" over "Foo& f". + +================================================================================ + +There are likely things missing here and I'll try to add them over time as I +notice things that are often missed. Please let me know if specific points are +ambiguous. + +Scott Wheeler <wheeler@kde.org> diff --git a/juk/Makefile.am b/juk/Makefile.am new file mode 100644 index 00000000..4fba4b1e --- /dev/null +++ b/juk/Makefile.am @@ -0,0 +1,113 @@ +bin_PROGRAMS = juk +check_PROGRAMS = tagguessertest + +juk_SOURCES = \ + advancedsearchdialog.cpp \ + actioncollection.cpp \ + akodeplayer.cpp \ + artsplayer.cpp \ + cache.cpp \ + categoryreaderinterface.cpp \ + collectionlist.cpp \ + coverdialog.cpp \ + coverdialogbase.ui \ + covericonview.cpp \ + coverinfo.cpp \ + covermanager.cpp \ + deletedialog.cpp \ + deletedialogbase.ui \ + directorylist.cpp \ + directorylistbase.ui \ + dynamicplaylist.cpp \ + exampleoptions.cpp \ + exampleoptionsbase.ui \ + folderplaylist.cpp \ + filehandle.cpp \ + filerenamer.cpp \ + filerenamerbase.ui \ + filerenameroptions.cpp \ + filerenameroptionsbase.ui \ + filerenamerconfigdlg.cpp \ + gstreamerplayer.cpp \ + webimagefetcher.cpp \ + webimagefetcherdialog.cpp \ + historyplaylist.cpp \ + juk.cpp \ + jukIface.skel \ + k3bexporter.cpp \ + keydialog.cpp \ + main.cpp \ + mediafiles.cpp \ + musicbrainzquery.cpp \ + nowplaying.cpp \ + playermanager.cpp \ + playlist.cpp \ + playlistbox.cpp \ + playlistcollection.cpp \ + playlistinterface.cpp \ + playlistitem.cpp \ + playlistsearch.cpp \ + playlistsplitter.cpp \ + searchplaylist.cpp \ + searchwidget.cpp \ + slideraction.cpp \ + sortedstringlist.cpp \ + splashscreen.cpp \ + statuslabel.cpp \ + stringshare.cpp \ + systemtray.cpp \ + tag.cpp \ + tageditor.cpp \ + tagguesser.cpp \ + tagguesserconfigdlg.cpp \ + tagguesserconfigdlgwidget.ui \ + tagrenameroptions.cpp \ + tagtransactionmanager.cpp \ + trackpickerdialog.cpp \ + trackpickerdialogbase.ui \ + tracksequenceiterator.cpp \ + tracksequencemanager.cpp \ + treeviewitemplaylist.cpp \ + upcomingplaylist.cpp \ + ktrm.cpp \ + viewmode.cpp + +tagguessertest_SOURCES = tagguessertest.cpp tagguesser.cpp + +INCLUDES= $(all_includes) $(taglib_includes) $(akode_includes) $(GST_CFLAGS) $(ARTS_CFLAGS) + +################################################## +# check to see if MusicBrainz is available +################################################## +if link_lib_MB +mblibs = -ltunepimp +endif +################################################## + +juk_LDADD = -lm $(LDADD_GST) $(mblibs) $(LIB_KIO) $(taglib_libs) $(akode_libs) $(LIB_KHTML) $(LIB_ARTS) +juk_LDFLAGS = $(all_libraries) $(KDE_RPATH) $(LDFLAGS_GST) + +KDE_CXXFLAGS = $(USE_EXCEPTIONS) + +tagguessertest_LDADD = $(LIB_KDECORE) +tagguessertest_LDFLAGS = $(all_libraries) + +SUBDIRS = pics + +rcdir = $(kde_datadir)/juk +rc_DATA = jukui.rc jukui-rtl.rc + +servicemenudir = $(kde_datadir)/konqueror/servicemenus +servicemenu_DATA = jukservicemenu.desktop + +METASOURCES = AUTO +KDE_ICON = AUTO +POFILES = AUTO + +xdg_apps_DATA = juk.desktop + +messages: rc.cpp + $(EXTRACTRC) *.rc >> rc.cpp + $(XGETTEXT) *.rc *.cpp *.h -o $(podir)/juk.pot + -rm rc.cpp +KDE_OPTIONS=nofinal diff --git a/juk/TODO b/juk/TODO new file mode 100644 index 00000000..f515343d --- /dev/null +++ b/juk/TODO @@ -0,0 +1,17 @@ +Just a quick 3.4 checklist. I'll add things as they pop into my mind... + +( ) Make the upcoming playlist not suck, specifically I'd like to see it as a + real and usable replacement for the "Play Next" feature, which it + presently is not. + +( ) At least play with making the "Now Playing" bar resizable. Currently have + something sort of working putting it in a slider type of thing. Not sure + if I like it. + +( ) Fix the cover manager in lots of ways. I really don't like having the icon + in the listview and have played around with a few things to fix this. What + I'm currently thinking is making a "cover manager" as a view mode and when + the user clicks to set the cover switching to that mode. Or something like + that. Something's got to give. + +( ) Show a preview of the cover to be added before saving it. diff --git a/juk/actioncollection.cpp b/juk/actioncollection.cpp new file mode 100644 index 00000000..2ba64474 --- /dev/null +++ b/juk/actioncollection.cpp @@ -0,0 +1,41 @@ +/*************************************************************************** + begin : Fri Feb 27 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kactioncollection.h> +#include <kdebug.h> + +#include "actioncollection.h" + +namespace ActionCollection +{ + KActionCollection *actions() + { + static KActionCollection *a = + new KActionCollection(static_cast<QWidget *>(0), "JuK Action Collection"); + return a; + } + + KAction *action(const char *key) + { +#ifndef NO_DEBUG + KAction *a = actions()->action(key); + if(!a) + kdWarning(65432) << "KAction \"" << key << "\" is not defined yet." << endl; + return a; +#else + return actions()->action(key); +#endif + } +} diff --git a/juk/actioncollection.h b/juk/actioncollection.h new file mode 100644 index 00000000..22eddd54 --- /dev/null +++ b/juk/actioncollection.h @@ -0,0 +1,45 @@ +/*************************************************************************** + begin : Fri Feb 27 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef JUK_ACTIONCOLLECTION_H +#define JUK_ACTIONCOLLECTION_H + +class KActionCollection; +class KAction; + +namespace ActionCollection +{ + /** + * The global action collection for JuK. + */ + KActionCollection *actions(); + + /** + * Returns the action for the associated key from the global action + * collection. + */ + KAction *action(const char *key); + + /** + * Returns the action for the associated key but includes a cast to the + * type \a T. i.e. KSelectAction *a = action<KSelectAction>("chooser"); + */ + template <class T> T *action(const char *key) + { + return dynamic_cast<T *>(action(key)); + } +} + +#endif diff --git a/juk/advancedsearchdialog.cpp b/juk/advancedsearchdialog.cpp new file mode 100644 index 00000000..4b5f2dbb --- /dev/null +++ b/juk/advancedsearchdialog.cpp @@ -0,0 +1,175 @@ +/*************************************************************************** + begin : Thu Jul 31 00:31:51 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kcombobox.h> +#include <klineedit.h> +#include <kpushbutton.h> +#include <klocale.h> + +#include <qradiobutton.h> +#include <qvgroupbox.h> +#include <qlabel.h> +#include <qhbox.h> +#include <qvbox.h> +#include <qlayout.h> +#include <qhbuttongroup.h> + +#include "collectionlist.h" +#include "advancedsearchdialog.h" +#include "searchwidget.h" + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +AdvancedSearchDialog::AdvancedSearchDialog(const QString &defaultName, + const PlaylistSearch &defaultSearch, + QWidget *parent, + const char *name) : + KDialogBase(parent, name, true, i18n("Create Search Playlist"), Ok|Cancel) +{ + makeVBoxMainWidget(); + + QHBox *box = new QHBox(mainWidget()); + box->setSpacing(5); + + new QLabel(i18n("Playlist name:"), box); + m_playlistNameLineEdit = new KLineEdit(defaultName, box); + + QVGroupBox *criteriaGroupBox = new QVGroupBox(i18n("Search Criteria"), mainWidget()); + static_cast<QHBox *>(mainWidget())->setStretchFactor(criteriaGroupBox, 1); + + QHButtonGroup *group = new QHButtonGroup(criteriaGroupBox); + m_matchAnyButton = new QRadioButton(i18n("Match any of the following"), group); + m_matchAllButton = new QRadioButton(i18n("Match all of the following"), group); + + m_criteria = new QVBox(criteriaGroupBox); + + if(defaultSearch.isNull()) { + m_searchLines.append(new SearchLine(m_criteria)); + m_searchLines.append(new SearchLine(m_criteria)); + m_matchAnyButton->setChecked(true); + } + else { + PlaylistSearch::ComponentList components = defaultSearch.components(); + for(PlaylistSearch::ComponentList::ConstIterator it = components.begin(); + it != components.end(); + ++it) + { + SearchLine *s = new SearchLine(m_criteria); + s->setSearchComponent(*it); + m_searchLines.append(s); + } + if(defaultSearch.searchMode() == PlaylistSearch::MatchAny) + m_matchAnyButton->setChecked(true); + else + m_matchAllButton->setChecked(true); + } + + QWidget *buttons = new QWidget(criteriaGroupBox); + QBoxLayout *l = new QHBoxLayout(buttons, 0, 5); + + KPushButton *clearButton = new KPushButton(KStdGuiItem::clear(), buttons); + connect(clearButton, SIGNAL(clicked()), SLOT(clear())); + l->addWidget(clearButton); + + l->addStretch(1); + + m_moreButton = new KPushButton(i18n("More"), buttons); + connect(m_moreButton, SIGNAL(clicked()), SLOT(more())); + l->addWidget(m_moreButton); + + m_fewerButton = new KPushButton(i18n("Fewer"), buttons); + connect(m_fewerButton, SIGNAL(clicked()), SLOT(fewer())); + l->addWidget(m_fewerButton); + + m_playlistNameLineEdit->setFocus(); +} + +AdvancedSearchDialog::~AdvancedSearchDialog() +{ + +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +AdvancedSearchDialog::Result AdvancedSearchDialog::exec() +{ + Result r; + r.result = DialogCode(KDialogBase::exec()); + r.search = m_search; + r.playlistName = m_playlistName; + return r; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected slots +//////////////////////////////////////////////////////////////////////////////// + +void AdvancedSearchDialog::accept() +{ + m_search.clearPlaylists(); + m_search.clearComponents(); + + m_search.addPlaylist(CollectionList::instance()); + + QValueListConstIterator<SearchLine *> it = m_searchLines.begin(); + for(; it != m_searchLines.end(); ++it) + m_search.addComponent((*it)->searchComponent()); + + PlaylistSearch::SearchMode m = PlaylistSearch::SearchMode(!m_matchAnyButton->isChecked()); + m_search.setSearchMode(m); + + m_playlistName = m_playlistNameLineEdit->text(); + + KDialogBase::accept(); +} + +void AdvancedSearchDialog::clear() +{ + QValueListConstIterator<SearchLine *> it = m_searchLines.begin(); + for(; it != m_searchLines.end(); ++it) + (*it)->clear(); +} + +void AdvancedSearchDialog::more() +{ + SearchLine *searchLine = new SearchLine(m_criteria); + m_searchLines.append(searchLine); + searchLine->show(); + updateButtons(); +} + +void AdvancedSearchDialog::fewer() +{ + SearchLine *searchLine = m_searchLines.last(); + m_searchLines.remove(searchLine); + delete searchLine; + updateButtons(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void AdvancedSearchDialog::updateButtons() +{ + m_moreButton->setEnabled(m_searchLines.count() < 16); + m_fewerButton->setEnabled(m_searchLines.count() > 1); +} + +#include "advancedsearchdialog.moc" diff --git a/juk/advancedsearchdialog.h b/juk/advancedsearchdialog.h new file mode 100644 index 00000000..03280d19 --- /dev/null +++ b/juk/advancedsearchdialog.h @@ -0,0 +1,69 @@ +/*************************************************************************** + begin : Thu Jul 31 00:31:51 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef ADVANCEDSEARCHDIALOG_H +#define ADVANCEDSEARCHDIALOG_H + +#include <kdialogbase.h> + +class KLineEdit; +class KPushButton; +class QGroupBox; +class QRadioButton; +class SearchLine; + +class AdvancedSearchDialog : public KDialogBase +{ + Q_OBJECT + +public: + struct Result + { + DialogCode result; + PlaylistSearch search; + QString playlistName; + }; + + AdvancedSearchDialog(const QString &defaultName, + const PlaylistSearch &defaultSearch = PlaylistSearch(), + QWidget *parent = 0, + const char *name = 0); + + virtual ~AdvancedSearchDialog(); + +public slots: + Result exec(); + +protected slots: + virtual void accept(); + virtual void clear(); + virtual void more(); + virtual void fewer(); + +private: + void updateButtons(); + + QWidget *m_criteria; + PlaylistSearch m_search; + QString m_playlistName; + QValueList<SearchLine *> m_searchLines; + KLineEdit *m_playlistNameLineEdit; + QRadioButton *m_matchAnyButton; + QRadioButton *m_matchAllButton; + KPushButton *m_moreButton; + KPushButton *m_fewerButton; +}; + +#endif diff --git a/juk/akodeplayer.cpp b/juk/akodeplayer.cpp new file mode 100644 index 00000000..0d75d830 --- /dev/null +++ b/juk/akodeplayer.cpp @@ -0,0 +1,170 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.com +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <config.h> + +#ifdef HAVE_AKODE + +#include <kdebug.h> + +#include <qfile.h> + +#include <akode/player.h> +#include <akode/decoder.h> + +#include "akodeplayer.h" + +using namespace aKode; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +aKodePlayer::aKodePlayer() : Player(), + m_player(0), + m_volume(1.0) +{} + +aKodePlayer::~aKodePlayer() +{ + delete m_player; +} + +void aKodePlayer::play(const FileHandle &file) +{ + kdDebug( 65432 ) << k_funcinfo << endl; + + if (file.isNull()) { // null FileHandle file means unpause + if (paused()) + m_player->resume(); + else + stop(); + return; + } + + QString filename = file.absFilePath(); + + kdDebug( 65432 ) << "Opening: " << filename << endl; + + if (m_player) + m_player->stop(); + else { + m_player = new aKode::Player(); + m_player->open("auto"); + m_player->setVolume(m_volume); + } + + if (m_player->load(filename.local8Bit().data())) + m_player->play(); + +} + +void aKodePlayer::pause() +{ + if (m_player) + m_player->pause(); +} + +void aKodePlayer::stop() +{ + if (m_player) { + m_player->stop(); + m_player->unload(); + } +} + +void aKodePlayer::setVolume(float volume) +{ + m_volume = volume; + + if (m_player) + m_player->setVolume(m_volume); +} + +float aKodePlayer::volume() const +{ + return m_volume; +} + +///////////////////////////////////////////////////////////////////////////////// +// m_player status functions +///////////////////////////////////////////////////////////////////////////////// + +bool aKodePlayer::playing() const +{ + if (m_player && m_player->decoder() && m_player->state() == aKode::Player::Playing) + return !m_player->decoder()->eof(); + else + return false; +} + +bool aKodePlayer::paused() const +{ + return m_player && (m_player->state() == aKode::Player::Paused); +} + +int aKodePlayer::totalTime() const +{ + if (m_player) { + Decoder *d = m_player->decoder(); + if (d) + return d->length() / 1000; + } + return -1; +} + +int aKodePlayer::currentTime() const +{ + if (m_player) { + Decoder *d = m_player->decoder(); + if (d) + return d->position() / 1000; + } + return -1; +} + +int aKodePlayer::position() const +{ + if (m_player) { + Decoder *d = m_player->decoder(); + if (d && d->length()) + return (d->position()*1000)/(d->length()); + else + return -1; + } + else + return -1; +} + +///////////////////////////////////////////////////////////////////////////////// +// m_player seek functions +///////////////////////////////////////////////////////////////////////////////// + +void aKodePlayer::seek(int seekTime) +{ + // seek time in seconds? + if (m_player) + m_player->decoder()->seek(seekTime*1000); +} + +void aKodePlayer::seekPosition(int position) +{ + // position unit is 1/1000th + if (m_player) + m_player->decoder()->seek((position * m_player->decoder()->length())/1000); +} + +#include "akodeplayer.moc" + +#endif diff --git a/juk/akodeplayer.h b/juk/akodeplayer.h new file mode 100644 index 00000000..6b44f2fd --- /dev/null +++ b/juk/akodeplayer.h @@ -0,0 +1,66 @@ +/*************************************************************************** + copyright : (C) 2004 by Allan Sandfeld Jensen + email : kde@carewolf.com +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + + +#ifndef AKODEPLAYER_H +#define AKODEPLAYER_H + +#include <config.h> + +#ifdef HAVE_AKODE + +#include <qstring.h> + +#include "player.h" +#include <kdemacros.h> + +namespace aKode { + class File; + class Player; +} + +class KDE_EXPORT aKodePlayer : public Player +{ + Q_OBJECT + +public: + aKodePlayer(); + virtual ~aKodePlayer(); + + virtual void play(const FileHandle &file = FileHandle::null()); + + virtual void setVolume(float volume = 1.0); + virtual float volume() const; + + virtual bool playing() const; + virtual bool paused() const; + + virtual int totalTime() const; + virtual int currentTime() const; + virtual int position() const; + + virtual void seek(int seekTime); + virtual void seekPosition(int position); + +public slots: + void pause(); + void stop(); + +private: + aKode::Player *m_player; + float m_volume; +}; + +#endif +#endif diff --git a/juk/artsplayer.cpp b/juk/artsplayer.cpp new file mode 100644 index 00000000..410a6356 --- /dev/null +++ b/juk/artsplayer.cpp @@ -0,0 +1,295 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : wheeler@kde.org + + copyright : (C) 2003 by Matthias Kretz + email : kretz@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. * + * * + ***************************************************************************/ + +#include "artsplayer.h" + +#if HAVE_ARTS + +#include <kdebug.h> +#include <kconfig.h> +#include <kstandarddirs.h> + +#include <qdir.h> + +#include <kartsserver.h> +#include <kartsdispatcher.h> +#include <kplayobject.h> +#include <kplayobjectfactory.h> + +#include <cstdlib> +#include <sys/wait.h> + +#include <kmessagebox.h> +#include <kaudiomanagerplay.h> +#include <klocale.h> + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +ArtsPlayer::ArtsPlayer() : Player(), + m_dispatcher(0), + m_server(0), + m_factory(0), + m_playobject(0), + m_amanPlay(0), + m_volumeControl(Arts::StereoVolumeControl::null()), + m_currentVolume(1.0) +{ + setupPlayer(); +} + +ArtsPlayer::~ArtsPlayer() +{ + delete m_playobject; + delete m_factory; + delete m_amanPlay; + delete m_server; + delete m_dispatcher; +} + +void ArtsPlayer::play(const FileHandle &file) +{ + // kdDebug(65432) << k_funcinfo << endl; + // Make sure that the server still exists, if it doesn't a new one should + // be started automatically and the factory and amanPlay are created again. + + if(!file.isNull()) + m_currentURL.setPath(file.absFilePath()); + + if(m_server->server().isNull()) { + KMessageBox::error(0, i18n("Cannot find the aRts soundserver.")); + return; + } + + if(!m_playobject || !file.isNull()) { + stop(); + + delete m_playobject; + m_playobject = m_factory->createPlayObject(m_currentURL, false); + + if(m_playobject->object().isNull()) + connect(m_playobject, SIGNAL(playObjectCreated()), SLOT(playObjectCreated())); + else + playObjectCreated(); + } + + m_playobject->play(); +} + +void ArtsPlayer::pause() +{ + // kdDebug(65432) << k_funcinfo << endl; + if(m_playobject) + m_playobject->pause(); +} + +void ArtsPlayer::stop() +{ + // kdDebug(65432) << k_funcinfo << endl; + if(m_playobject) { + m_playobject->halt(); + delete m_playobject; + m_playobject = 0; + } + if(!m_volumeControl.isNull()) { + m_volumeControl.stop(); + m_volumeControl = Arts::StereoVolumeControl::null(); + } +} + +void ArtsPlayer::setVolume(float volume) +{ + // kdDebug( 65432 ) << k_funcinfo << endl; + + m_currentVolume = volume; + + if(serverRunning() && m_playobject && !m_playobject->isNull()) { + if(m_volumeControl.isNull()) + setupVolumeControl(); + if(!m_volumeControl.isNull()) { + m_volumeControl.scaleFactor(volume); + // kdDebug( 65432 ) << "set volume to " << volume << endl; + } + } +} + +float ArtsPlayer::volume() const +{ + return m_currentVolume; +} + +///////////////////////////////////////////////////////////////////////////////// +// player status functions +///////////////////////////////////////////////////////////////////////////////// + +bool ArtsPlayer::playing() const +{ + if(serverRunning() && m_playobject && m_playobject->state() == Arts::posPlaying) + return true; + else + return false; +} + +bool ArtsPlayer::paused() const +{ + if(serverRunning() && m_playobject && m_playobject->state() == Arts::posPaused) + return true; + else + return false; +} + +int ArtsPlayer::totalTime() const +{ + if(serverRunning() && m_playobject) + return m_playobject->overallTime().seconds; + else + return -1; +} + +int ArtsPlayer::currentTime() const +{ + if(serverRunning() && m_playobject && + (m_playobject->state() == Arts::posPlaying || + m_playobject->state() == Arts::posPaused)) + { + return m_playobject->currentTime().seconds; + } + else + return -1; +} + +int ArtsPlayer::position() const +{ + if(serverRunning() && m_playobject && m_playobject->state() == Arts::posPlaying) { + long total = m_playobject->overallTime().seconds * 1000 + m_playobject->overallTime().ms; + long current = m_playobject->currentTime().seconds * 1000 + m_playobject->currentTime().ms; + + // add .5 to make rounding happen properly + + return int(double(current) * 1000 / total + .5); + } + else + return -1; +} + +///////////////////////////////////////////////////////////////////////////////// +// player seek functions +///////////////////////////////////////////////////////////////////////////////// + +void ArtsPlayer::seek(int seekTime) +{ + if(serverRunning() && m_playobject) { + Arts::poTime poSeekTime; + poSeekTime.custom = 0; + poSeekTime.ms = 0; + poSeekTime.seconds = seekTime; + m_playobject->object().seek(poSeekTime); + } +} + +void ArtsPlayer::seekPosition(int position) +{ + if(serverRunning() && m_playobject) { + Arts::poTime poSeekTime; + long total = m_playobject->overallTime().seconds; + poSeekTime.custom = 0; + poSeekTime.ms = 0; + poSeekTime.seconds = long(double(total) * position / 1000 + .5); + m_playobject->object().seek(poSeekTime); + } +} + +///////////////////////////////////////////////////////////////////////////////// +// private +///////////////////////////////////////////////////////////////////////////////// + +void ArtsPlayer::setupArtsObjects() +{ + // kdDebug( 65432 ) << k_funcinfo << endl; + delete m_factory; + delete m_amanPlay; + m_volumeControl = Arts::StereoVolumeControl::null(); + m_factory = new KDE::PlayObjectFactory(m_server); + m_amanPlay = new KAudioManagerPlay(m_server); + + if(m_amanPlay->isNull() || !m_factory) { + KMessageBox::error(0, i18n("Connecting/starting aRts soundserver failed. " + "Make sure that artsd is configured properly.")); + exit(1); + } + + m_amanPlay->setTitle(i18n("JuK")); + m_amanPlay->setAutoRestoreID("JuKAmanPlay"); + + m_factory->setAudioManagerPlay(m_amanPlay); +} + +void ArtsPlayer::playObjectCreated() +{ + // kdDebug(65432) << k_funcinfo << endl; + setVolume(m_currentVolume); +} + +void ArtsPlayer::setupPlayer() +{ + m_dispatcher = new KArtsDispatcher; + m_server = new KArtsServer; + setupArtsObjects(); + connect(m_server, SIGNAL(restartedServer()), SLOT(setupArtsObjects())); +} + +void ArtsPlayer::setupVolumeControl() +{ + // kdDebug( 65432 ) << k_funcinfo << endl; + m_volumeControl = Arts::DynamicCast(m_server->server().createObject("Arts::StereoVolumeControl")); + if(!m_volumeControl.isNull() && !m_playobject->isNull() && !m_playobject->object().isNull()) { + Arts::Synth_AMAN_PLAY ap = m_amanPlay->amanPlay(); + Arts::PlayObject po = m_playobject->object(); + ap.stop(); + Arts::disconnect(po, "left" , ap, "left" ); + Arts::disconnect(po, "right", ap, "right"); + + m_volumeControl.start(); + ap.start(); + + Arts::connect(po, "left" , m_volumeControl, "inleft" ); + Arts::connect(po, "right", m_volumeControl, "inright"); + Arts::connect(m_volumeControl, "outleft" , ap, "left" ); + Arts::connect(m_volumeControl, "outright", ap, "right"); + // kdDebug( 65432 ) << "connected volume control" << endl; + } + else { + m_volumeControl = Arts::StereoVolumeControl::null(); + kdDebug(65432) << "Could not initialize volume control!" << endl; + } +} + +bool ArtsPlayer::serverRunning() const +{ + if(m_server) + return !(m_server->server().isNull()); + else + return false; +} + +#include "artsplayer.moc" + +#endif + +// vim: sw=4 ts=8 et diff --git a/juk/artsplayer.h b/juk/artsplayer.h new file mode 100644 index 00000000..2c1306e1 --- /dev/null +++ b/juk/artsplayer.h @@ -0,0 +1,95 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : wheeler@kde.org + + copyright : (C) 2003 by Matthias Kretz + email : kretz@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. * + * * + ***************************************************************************/ + +#ifndef ARTSPLAYER_H +#define ARTSPLAYER_H + +#include <config.h> + +#if HAVE_ARTS + +#include "player.h" + +#include <kurl.h> + +#include <qstring.h> +#include <qobject.h> +#include <artsflow.h> + +class KArtsDispatcher; +class KArtsServer; +class KAudioManagerPlay; + +namespace KDE { + class PlayObjectFactory; + class PlayObject; +} + +class ArtsPlayer : public Player +{ + Q_OBJECT + +public: + ArtsPlayer(); + virtual ~ArtsPlayer(); + + virtual void play(const FileHandle &file = FileHandle::null()); + virtual void pause(); + virtual void stop(); + + virtual void setVolume(float volume = 1.0); + virtual float volume() const; + + virtual bool playing() const; + virtual bool paused() const; + + virtual int totalTime() const; + virtual int currentTime() const; + virtual int position() const; // in this case not really the percent + + virtual void seek(int seekTime); + virtual void seekPosition(int position); + +private slots: + void setupArtsObjects(); + void playObjectCreated(); + +private: + void setupPlayer(); + void setupVolumeControl(); + bool serverRunning() const; + + KArtsDispatcher *m_dispatcher; + KArtsServer *m_server; + KDE::PlayObjectFactory *m_factory; + KDE::PlayObject *m_playobject; + KAudioManagerPlay *m_amanPlay; + + // This is a pretty heavy module for the needs that JuK has, it would probably + // be good to use two Synth_MUL instead or the one from Noatun. + + Arts::StereoVolumeControl m_volumeControl; + + KURL m_currentURL; + float m_currentVolume; +}; + +#endif +#endif + +// vim: sw=4 ts=8 et diff --git a/juk/cache.cpp b/juk/cache.cpp new file mode 100644 index 00000000..3f37ee4a --- /dev/null +++ b/juk/cache.cpp @@ -0,0 +1,323 @@ +/*************************************************************************** + begin : Sat Sep 7 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kstandarddirs.h> +#include <kmessagebox.h> +#include <kconfig.h> +#include <klocale.h> +#include <kactionclasses.h> +#include <kdebug.h> + +#include <qdir.h> +#include <qbuffer.h> + +#include "cache.h" +#include "tag.h" +#include "searchplaylist.h" +#include "historyplaylist.h" +#include "upcomingplaylist.h" +#include "folderplaylist.h" +#include "playlistcollection.h" +#include "actioncollection.h" + +using namespace ActionCollection; + +static const int playlistCacheVersion = 2; + +enum PlaylistType +{ + Normal = 0, + Search = 1, + History = 2, + Upcoming = 3, + Folder = 4 +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +Cache *Cache::instance() +{ + static Cache cache; + + // load() indirectly calls instance() so we have to protect against recursion. + static bool loaded = false; + + if(!loaded) { + loaded = true; + cache.load(); + } + + return &cache; +} + +void Cache::save() +{ + QString dirName = KGlobal::dirs()->saveLocation("appdata"); + QString cacheFileName = dirName + "cache.new"; + + QFile f(cacheFileName); + + if(!f.open(IO_WriteOnly)) + return; + + QByteArray data; + QDataStream s(data, IO_WriteOnly); + + for(Iterator it = begin(); it != end(); ++it) { + s << (*it).absFilePath(); + s << *it; + } + + QDataStream fs(&f); + + Q_INT32 checksum = qChecksum(data.data(), data.size()); + + fs << Q_INT32(m_currentVersion) + << checksum + << data; + + f.close(); + + QDir(dirName).rename("cache.new", "cache"); +} + +void Cache::loadPlaylists(PlaylistCollection *collection) // static +{ + QString playlistsFile = KGlobal::dirs()->saveLocation("appdata") + "playlists"; + + QFile f(playlistsFile); + + if(!f.open(IO_ReadOnly)) + return; + + QDataStream fs(&f); + + Q_INT32 version; + fs >> version; + + switch(version) { + case 1: + case 2: + { + // Our checksum is only for the values after the version and checksum so + // we want to get a byte array with just the checksummed data. + + QByteArray data; + Q_UINT16 checksum; + fs >> checksum >> data; + + if(checksum != qChecksum(data.data(), data.size())) + return; + + // Create a new stream just based on the data. + + QDataStream s(data, IO_ReadOnly); + + while(!s.atEnd()) { + + Q_INT32 playlistType; + s >> playlistType; + + Playlist *playlist = 0; + + switch(playlistType) { + case Search: + { + SearchPlaylist *p = new SearchPlaylist(collection); + s >> *p; + playlist = p; + break; + } + case History: + { + action<KToggleAction>("showHistory")->setChecked(true); + collection->setHistoryPlaylistEnabled(true); + s >> *collection->historyPlaylist(); + playlist = collection->historyPlaylist(); + break; + } + case Upcoming: + { + /* + collection->setUpcomingPlaylistEnabled(true); + Playlist *p = collection->upcomingPlaylist(); + action<KToggleAction>("saveUpcomingTracks")->setChecked(true); + s >> *p; + playlist = p; + */ + break; + } + case Folder: + { + FolderPlaylist *p = new FolderPlaylist(collection); + s >> *p; + playlist = p; + break; + } + default: + Playlist *p = new Playlist(collection, true); + s >> *p; + playlist = p; + break; + } + if(version == 2) { + Q_INT32 sortColumn; + s >> sortColumn; + if(playlist) + playlist->setSorting(sortColumn); + } + } + break; + } + default: + { + // Because the original version of the playlist cache did not contain a + // version number, we want to revert to the beginning of the file before + // reading the data. + + f.reset(); + + while(!fs.atEnd()) { + Playlist *p = new Playlist(collection); + fs >> *p; + } + break; + } + } + + f.close(); +} + +void Cache::savePlaylists(const PlaylistList &playlists) +{ + QString dirName = KGlobal::dirs()->saveLocation("appdata"); + QString playlistsFile = dirName + "playlists.new"; + QFile f(playlistsFile); + + if(!f.open(IO_WriteOnly)) + return; + + QByteArray data; + QDataStream s(data, IO_WriteOnly); + + for(PlaylistList::ConstIterator it = playlists.begin(); it != playlists.end(); ++it) { + if(*it) { + if(dynamic_cast<HistoryPlaylist *>(*it)) { + s << Q_INT32(History) + << *static_cast<HistoryPlaylist *>(*it); + } + else if(dynamic_cast<SearchPlaylist *>(*it)) { + s << Q_INT32(Search) + << *static_cast<SearchPlaylist *>(*it); + } + else if(dynamic_cast<UpcomingPlaylist *>(*it)) { + if(!action<KToggleAction>("saveUpcomingTracks")->isChecked()) + continue; + s << Q_INT32(Upcoming) + << *static_cast<UpcomingPlaylist *>(*it); + } + else if(dynamic_cast<FolderPlaylist *>(*it)) { + s << Q_INT32(Folder) + << *static_cast<FolderPlaylist *>(*it); + } + else { + s << Q_INT32(Normal) + << *(*it); + } + s << Q_INT32((*it)->sortColumn()); + } + } + + QDataStream fs(&f); + fs << Q_INT32(playlistCacheVersion); + fs << qChecksum(data.data(), data.size()); + + fs << data; + f.close(); + + QDir(dirName).rename("playlists.new", "playlists"); +} + +bool Cache::cacheFileExists() // static +{ + return QFile::exists(KGlobal::dirs()->saveLocation("appdata") + "cache"); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +Cache::Cache() : FileHandleHash() +{ + +} + +void Cache::load() +{ + QString cacheFileName = KGlobal::dirs()->saveLocation("appdata") + "cache"; + + QFile f(cacheFileName); + + if(!f.open(IO_ReadOnly)) + return; + + CacheDataStream s(&f); + + Q_INT32 version; + s >> version; + + QBuffer buffer; + + // Do the version specific stuff. + + switch(version) { + case 1: { + s.setCacheVersion(1); + + Q_INT32 checksum; + QByteArray data; + s >> checksum + >> data; + + buffer.setBuffer(data); + buffer.open(IO_ReadOnly); + s.setDevice(&buffer); + + if(checksum != qChecksum(data.data(), data.size())) { + KMessageBox::sorry(0, i18n("The music data cache has been corrupted. JuK " + "needs to rescan it now. This may take some time.")); + return; + } + break; + } + default: { + s.device()->reset(); + s.setCacheVersion(0); + break; + } + } + + // Read the cached tags. + + while(!s.atEnd()) { + QString fileName; + s >> fileName; + fileName.squeeze(); + + FileHandle f(fileName, s); + } +} diff --git a/juk/cache.h b/juk/cache.h new file mode 100644 index 00000000..09f6906d --- /dev/null +++ b/juk/cache.h @@ -0,0 +1,67 @@ +/*************************************************************************** + begin : Sat Sep 7 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef CACHE_H +#define CACHE_H + + +#include "stringhash.h" + +class Tag; +class Playlist; +class PlaylistCollection; + +typedef QValueList<Playlist *> PlaylistList; + +class Cache : public FileHandleHash +{ +public: + static Cache *instance(); + void save(); + + static void loadPlaylists(PlaylistCollection *collection); + static void savePlaylists(const PlaylistList &playlists); + + static bool cacheFileExists(); + +protected: + Cache(); + void load(); + +private: + static const int m_currentVersion = 1; +}; + +/** + * A simple QDataStream subclass that has an extra field to indicate the cache + * version. + */ + +class CacheDataStream : public QDataStream +{ +public: + CacheDataStream(QIODevice *d) : QDataStream(d), m_cacheVersion(0) {} + CacheDataStream(QByteArray a, int mode) : QDataStream(a, mode), m_cacheVersion(0) {} + + virtual ~CacheDataStream() {} + + int cacheVersion() const { return m_cacheVersion; } + void setCacheVersion(int v) { m_cacheVersion = v; } + +private: + int m_cacheVersion; +}; + +#endif diff --git a/juk/categoryreaderinterface.cpp b/juk/categoryreaderinterface.cpp new file mode 100644 index 00000000..f9f73103 --- /dev/null +++ b/juk/categoryreaderinterface.cpp @@ -0,0 +1,67 @@ +/*************************************************************************** + begin : Sun Oct 31 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <qstring.h> + +#include "filerenameroptions.h" +#include "categoryreaderinterface.h" + +QString CategoryReaderInterface::value(const CategoryID &category) const +{ + QString value = categoryValue(category.category).stripWhiteSpace(); + + if(category.category == Track) + value = fixupTrack(value, category.categoryNumber).stripWhiteSpace(); + + if(value.isEmpty() && emptyAction(category) == TagRenamerOptions::UseReplacementValue) + value = emptyText(category); + + return prefix(category) + value + suffix(category); +} + +bool CategoryReaderInterface::isRequired(const CategoryID &category) const +{ + return emptyAction(category) != TagRenamerOptions::IgnoreEmptyTag; +} + +bool CategoryReaderInterface::isEmpty(TagType category) const +{ + return categoryValue(category).isEmpty(); +} + +QString CategoryReaderInterface::fixupTrack(const QString &track, unsigned categoryNum) const +{ + QString str(track); + CategoryID trackId(Track, categoryNum); + + if(track == "0") { + if(emptyAction(trackId) == TagRenamerOptions::UseReplacementValue) + str = emptyText(trackId); + else + return QString::null; + } + + unsigned minimumWidth = trackWidth(categoryNum); + + if(str.length() < minimumWidth) { + QString prefix; + prefix.fill('0', minimumWidth - str.length()); + return prefix + str; + } + + return str; +} + +// vim: set et sw=4 ts=4: diff --git a/juk/categoryreaderinterface.h b/juk/categoryreaderinterface.h new file mode 100644 index 00000000..5d2dfd0f --- /dev/null +++ b/juk/categoryreaderinterface.h @@ -0,0 +1,122 @@ +/*************************************************************************** + begin : Sun Oct 31 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_CATEGORYREADERINTERFACE_H +#define JUK_CATEGORYREADERINTERFACE_H + +#include "tagrenameroptions.h" + +enum TagType; +class QString; + +template<class T> class QValueList; + +/** + * This class is used to map categories into values. You should implement the + * functionality in a subclass. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class CategoryReaderInterface +{ +public: + virtual ~CategoryReaderInterface() { } + + /** + * Returns the textual representation of \p type, without any processing done + * on it. For example, track values shouldn't be expanded out to the minimum + * width from this function. No CategoryID is needed since the value is constant + * for a category. + * + * @param type, The category to retrieve the value of. + * @return textual representation of that category's value. + */ + virtual QString categoryValue(TagType type) const = 0; + + /** + * Returns the user-specified prefix string for \p category. + * + * @param category the category to retrieve the value for. + * @return user-specified prefix string for \p category. + */ + virtual QString prefix(const CategoryID &category) const = 0; + + /** + * Returns the user-specified suffix string for \p category. + * + * @param category the category to retrieve the value for. + * @return user-specified suffix string for \p category. + */ + virtual QString suffix(const CategoryID &category) const = 0; + + /** + * Returns the user-specified empty action for \p category. + * + * @param category the category to retrieve the value for. + * @return user-specified empty action for \p category. + */ + virtual TagRenamerOptions::EmptyActions emptyAction(const CategoryID &category) const = 0; + + /** + * Returns the user-specified empty text for \p category. This text might + * be used to replace an empty value. + * + * @param category the category to retrieve the value for. + * @return the user-specified empty text for \p category. + */ + virtual QString emptyText(const CategoryID &category) const = 0; + + /** + * @return the categories in the order the user has chosen. Categories may + * be repeated (which is why CategoryID has the categoryNumber value to + * disambiguate duplicates). + */ + virtual QValueList<CategoryID> categoryOrder() const = 0; + + /** + * @return track width for the Track item identified by categoryNum. + */ + virtual int trackWidth(unsigned categoryNum) const = 0; + + // You probably shouldn't reimplement this + virtual QString value(const CategoryID &category) const; + + virtual QString separator() const = 0; + + virtual QString musicFolder() const = 0; + + /** + * @param index the zero-based index of the item just before the + * separator in question. + * @return true if a folder separator should be placed between the tags + * at index and index + 1. + */ + virtual bool hasFolderSeparator(unsigned index) const = 0; + + virtual bool isDisabled(const CategoryID &category) const = 0; + + // You probably shouldn't reimplement this + virtual bool isRequired(const CategoryID &category) const; + + // You probably shouldn't reimplement this + virtual bool isEmpty(TagType category) const; + + // You probably shouldn't reimplement this + virtual QString fixupTrack(const QString &track, unsigned categoryNum) const; +}; + +#endif /* JUK_CATEGORYREADERINTERFACE_H */ + +// vim: set et sw=4 ts=4: diff --git a/juk/collectionlist.cpp b/juk/collectionlist.cpp new file mode 100644 index 00000000..9540be85 --- /dev/null +++ b/juk/collectionlist.cpp @@ -0,0 +1,511 @@ +/*************************************************************************** + begin : Fri Sep 13 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kurldrag.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kdebug.h> +#include <kpopupmenu.h> +#include <kiconloader.h> +#include <kconfig.h> +#include <kaction.h> +#include <kurl.h> + +#include "collectionlist.h" +#include "playlistcollection.h" +#include "splashscreen.h" +#include "stringshare.h" +#include "cache.h" +#include "actioncollection.h" +#include "tag.h" +#include "viewmode.h" +#include "coverinfo.h" +#include "covermanager.h" + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// static methods +//////////////////////////////////////////////////////////////////////////////// + +CollectionList *CollectionList::m_list = 0; + +CollectionList *CollectionList::instance() +{ + return m_list; +} + +void CollectionList::initialize(PlaylistCollection *collection) +{ + if(m_list) + return; + + // We have to delay initilaization 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")); + + FileHandleHash::Iterator end = Cache::instance()->end(); + for(FileHandleHash::Iterator it = Cache::instance()->begin(); it != end; ++it) + new CollectionListItem(*it); + + SplashScreen::update(); + + // The CollectionList is created with sorting disabled for speed. Re-enable + // it here, and perform the sort. + KConfigGroup config(KGlobal::config(), "Playlists"); + + SortOrder order = Descending; + if(config.readBoolEntry("CollectionListSortAscending", true)) + order = Ascending; + + m_list->setSortOrder(order); + m_list->setSortColumn(config.readNumEntry("CollectionListSortColumn", 1)); + + m_list->sort(); + + collection->setupPlaylist(m_list, "folder_sound"); +} + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistItem *CollectionList::createItem(const FileHandle &file, QListViewItem *, bool) +{ + // It's probably possible to optimize the line below away, but, well, right + // now it's more important to not load duplicate items. + + if(m_itemsDict.find(file.absFilePath())) + return 0; + + PlaylistItem *item = new CollectionListItem(file); + + if(!item->isValid()) { + kdError() << "CollectionList::createItem() -- A valid tag was not created for \"" + << file.absFilePath() << "\"" << endl; + delete item; + return 0; + } + + setupItem(item); + + return item; +} + +void CollectionList::clearItems(const PlaylistItemList &items) +{ + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + Cache::instance()->remove((*it)->file()); + clearItem(*it, false); + } + + dataChanged(); +} + +void CollectionList::setupTreeViewEntries(ViewMode *viewMode) const +{ + TreeViewMode *treeViewMode = dynamic_cast<TreeViewMode *>(viewMode); + if(!treeViewMode) { + kdWarning(65432) << "Can't setup entries on a non-tree-view mode!\n"; + return; + } + + QValueList<int> columnList; + columnList << PlaylistItem::ArtistColumn; + columnList << PlaylistItem::GenreColumn; + columnList << PlaylistItem::AlbumColumn; + + QStringList items; + for(QValueList<int>::Iterator colIt = columnList.begin(); colIt != columnList.end(); ++colIt) { + items.clear(); + for(TagCountDictIterator it(*m_columnTags[*colIt]); it.current(); ++it) + items << it.currentKey(); + + treeViewMode->addItems(items, *colIt); + } +} + +void CollectionList::slotNewItems(const KFileItemList &items) +{ + QStringList files; + + for(KFileItemListIterator it(items); it.current(); ++it) + files.append((*it)->url().path()); + + addFiles(files); + update(); +} + +void CollectionList::slotRefreshItems(const KFileItemList &items) +{ + for(KFileItemListIterator it(items); it.current(); ++it) { + CollectionListItem *item = lookup((*it)->url().path()); + + if(item) { + item->refreshFromDisk(); + + // If the item is no longer on disk, remove it from the collection. + + if(item->file().fileInfo().exists()) + item->repaint(); + else + clearItem(item); + } + } + + update(); +} + +void CollectionList::slotDeleteItem(KFileItem *item) +{ + CollectionListItem *listItem = lookup(item->url().path()); + if(listItem) + clearItem(listItem); +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void CollectionList::clear() +{ + 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.")); + + if(result == KMessageBox::Continue) { + Playlist::clear(); + emit signalCollectionChanged(); + } +} + +void CollectionList::slotCheckCache() +{ + PlaylistItemList invalidItems; + + for(QDictIterator<CollectionListItem>it(m_itemsDict); it.current(); ++it) { + if(!it.current()->checkCurrent()) + invalidItems.append(*it); + processEvents(); + } + + clearItems(invalidItems); +} + +void CollectionList::slotRemoveItem(const QString &file) +{ + clearItem(m_itemsDict[file]); +} + +void CollectionList::slotRefreshItem(const QString &file) +{ + m_itemsDict[file]->refresh(); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +CollectionList::CollectionList(PlaylistCollection *collection) : + Playlist(collection, true), + m_itemsDict(5003), + m_columnTags(15, 0) +{ + new KAction(i18n("Show Playing"), KShortcut(), actions(), "showPlaying"); + + connect(action("showPlaying"), SIGNAL(activated()), this, SLOT(slotShowPlaying())); + + connect(action<KToolBarPopupAction>("back")->popupMenu(), SIGNAL(aboutToShow()), + this, SLOT(slotPopulateBackMenu())); + connect(action<KToolBarPopupAction>("back")->popupMenu(), SIGNAL(activated(int)), + this, SLOT(slotPlayFromBackMenu(int))); + setSorting(-1); // Temporarily disable sorting to add items faster. + + m_columnTags[PlaylistItem::ArtistColumn] = new TagCountDict(5001, false); + m_columnTags[PlaylistItem::ArtistColumn]->setAutoDelete(true); + + m_columnTags[PlaylistItem::AlbumColumn] = new TagCountDict(5001, false); + m_columnTags[PlaylistItem::AlbumColumn]->setAutoDelete(true); + + m_columnTags[PlaylistItem::GenreColumn] = new TagCountDict(5001, false); + m_columnTags[PlaylistItem::GenreColumn]->setAutoDelete(true); + + polish(); +} + +CollectionList::~CollectionList() +{ + KConfigGroup config(KGlobal::config(), "Playlists"); + config.writeEntry("CollectionListSortColumn", sortColumn()); + config.writeEntry("CollectionListSortAscending", sortOrder() == Ascending); + + // 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()); + for(TagCountDicts::Iterator it = m_columnTags.begin(); it != m_columnTags.end(); ++it) + delete *it; +} + +void CollectionList::contentsDropEvent(QDropEvent *e) +{ + if(e->source() == this) + return; // Don't rearrange in the CollectionList. + else + Playlist::contentsDropEvent(e); +} + +void CollectionList::contentsDragMoveEvent(QDragMoveEvent *e) +{ + if(canDecode(e) && e->source() != this) + e->accept(true); + else + e->accept(false); +} + +QString CollectionList::addStringToDict(const QString &value, unsigned column) +{ + if(column > m_columnTags.count() || value.stripWhiteSpace().isEmpty()) + return QString::null; + + int *refCountPtr = m_columnTags[column]->find(value); + if(refCountPtr) + ++(*refCountPtr); + else { + m_columnTags[column]->insert(value, new int(1)); + emit signalNewTag(value, column); + } + + return value; +} + +QStringList CollectionList::uniqueSet(UniqueSetType t) const +{ + int column; + + switch(t) + { + case Artists: + column = PlaylistItem::ArtistColumn; + break; + + case Albums: + column = PlaylistItem::AlbumColumn; + break; + + case Genres: + column = PlaylistItem::GenreColumn; + break; + + default: + return QStringList(); + } + + if((unsigned) column >= m_columnTags.count()) + return QStringList(); + + TagCountDictIterator it(*m_columnTags[column]); + QStringList list; + + for(; it.current(); ++it) + list += it.currentKey(); + + return list; +} + +void CollectionList::removeStringFromDict(const QString &value, unsigned column) +{ + if(column > m_columnTags.count() || value.isEmpty()) + return; + + int *refCountPtr = m_columnTags[column]->find(value); + if(refCountPtr) { + --(*refCountPtr); + if(*refCountPtr == 0) { + emit signalRemovedTag(value, column); + m_columnTags[column]->remove(value); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CollectionListItem public methods +//////////////////////////////////////////////////////////////////////////////// + +void CollectionListItem::refresh() +{ + int offset = static_cast<Playlist *>(listView())->columnOffset(); + int columns = lastColumn() + offset + 1; + + data()->local8Bit.resize(columns); + data()->cachedWidths.resize(columns); + + for(int i = offset; i < columns; i++) { + int id = i - offset; + if(id != TrackNumberColumn && id != LengthColumn) { + // All columns other than track num and length need local-encoded data for sorting + + QCString lower = text(i).lower().local8Bit(); + + // For some columns, we may be able to share some strings + + if((id == ArtistColumn) || (id == AlbumColumn) || + (id == GenreColumn) || (id == YearColumn) || + (id == CommentColumn)) + { + lower = StringShare::tryShare(lower); + + if(id != YearColumn && id != CommentColumn && data()->local8Bit[id] != lower) { + CollectionList::instance()->removeStringFromDict(data()->local8Bit[id], id); + CollectionList::instance()->addStringToDict(text(i), id); + } + } + + data()->local8Bit[id] = lower; + } + + int newWidth = width(listView()->fontMetrics(), listView(), i); + data()->cachedWidths[i] = newWidth; + + if(newWidth != data()->cachedWidths[i]) + playlist()->slotWeightDirty(i); + } + + file().coverInfo()->setCover(); + + if(listView()->isVisible()) + repaint(); + + for(PlaylistItemList::Iterator it = m_children.begin(); it != m_children.end(); ++it) { + (*it)->playlist()->update(); + (*it)->playlist()->dataChanged(); + if((*it)->listView()->isVisible()) + (*it)->repaint(); + } + + CollectionList::instance()->dataChanged(); + emit CollectionList::instance()->signalCollectionChanged(); +} + +PlaylistItem *CollectionListItem::itemForPlaylist(const Playlist *playlist) +{ + if(playlist == CollectionList::instance()) + return this; + + PlaylistItemList::ConstIterator it; + for(it = m_children.begin(); it != m_children.end(); ++it) + if((*it)->playlist() == playlist) + return *it; + return 0; +} + +void CollectionListItem::updateCollectionDict(const QString &oldPath, const QString &newPath) +{ + CollectionList *collection = CollectionList::instance(); + + if(!collection) + return; + + collection->removeFromDict(oldPath); + collection->addToDict(newPath, this); +} + +void CollectionListItem::repaint() const +{ + QListViewItem::repaint(); + for(PlaylistItemList::ConstIterator it = m_children.begin(); it != m_children.end(); ++it) + (*it)->repaint(); +} + +//////////////////////////////////////////////////////////////////////////////// +// CollectionListItem protected methods +//////////////////////////////////////////////////////////////////////////////// + +CollectionListItem::CollectionListItem(const FileHandle &file) : + PlaylistItem(CollectionList::instance()), + m_shuttingDown(false) +{ + CollectionList *l = CollectionList::instance(); + if(l) { + l->addToDict(file.absFilePath(), this); + + data()->fileHandle = file; + + if(file.tag()) { + refresh(); + l->dataChanged(); + // l->addWatched(m_path); + } + else + kdError() << "CollectionListItem::CollectionListItem() -- Tag() could not be created." << endl; + } + else + kdError(65432) << "CollectionListItems should not be created before " + << "CollectionList::initialize() has been called." << endl; + + SplashScreen::increment(); +} + +CollectionListItem::~CollectionListItem() +{ + m_shuttingDown = true; + + for(PlaylistItemList::ConstIterator it = m_children.begin(); + it != m_children.end(); + ++it) + { + (*it)->playlist()->clearItem(*it); + } + + CollectionList *l = CollectionList::instance(); + if(l) { + l->removeFromDict(file().absFilePath()); + l->removeStringFromDict(file().tag()->album(), AlbumColumn); + l->removeStringFromDict(file().tag()->artist(), ArtistColumn); + l->removeStringFromDict(file().tag()->genre(), GenreColumn); + } +} + +void CollectionListItem::addChildItem(PlaylistItem *child) +{ + m_children.append(child); +} + +void CollectionListItem::removeChildItem(PlaylistItem *child) +{ + if(!m_shuttingDown) + m_children.remove(child); +} + +bool CollectionListItem::checkCurrent() +{ + if(!file().fileInfo().exists() || !file().fileInfo().isFile()) + return false; + + if(!file().current()) { + file().refresh(); + refresh(); + } + + return true; +} + +#include "collectionlist.moc" diff --git a/juk/collectionlist.h b/juk/collectionlist.h new file mode 100644 index 00000000..c5cafca2 --- /dev/null +++ b/juk/collectionlist.h @@ -0,0 +1,197 @@ +/*************************************************************************** + begin : Fri Sep 13 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef COLLECTIONLIST_H +#define COLLECTIONLIST_H + +#include <kapplication.h> +#include <kdirwatch.h> +#include <kfileitem.h> + +#include <qdict.h> +#include <qclipboard.h> +#include <qvaluevector.h> + +#include "playlist.h" +#include "playlistitem.h" +#include "sortedstringlist.h" + +class CollectionListItem; +class ViewMode; + +/** + * This type is for mapping QString track attributes like the album, artist + * and track to an integer count representing the number of outstanding items + * that hold the string. + */ + +typedef QDict<int> TagCountDict; +typedef QDictIterator<int> TagCountDictIterator; + +/** + * We then have an array of dicts, one for each column in the list view. We + * use pointers to TagCountDicts because QDict has a broken copy ctor, which + * doesn't copy the case sensitivity setting. + */ + +typedef QValueVector<TagCountDict*> TagCountDicts; + +/** + * This is the "collection", or all of the music files that have been opened + * in any playlist and not explicitly removed from the collection. + * + * It is being implemented as a "semi-singleton" because I need universal access + * to just one instance. However, because the collection needs initialization + * parameters (that will not always be available when an instance is needed). + * Hence there will be the familiar singleton "instance()" method allong with an + * "initialize()" method. + */ + +class CollectionList : public Playlist +{ + friend class CollectionListItem; + + Q_OBJECT + +public: + /** + * A variety of unique value lists will be kept in the collection. This + * enum can be used as an index into those structures. + */ + enum UniqueSetType { Artists = 0, Albums = 1, Genres = 2 }; + + static CollectionList *instance(); + static void initialize(PlaylistCollection *collection); + + /** + * Returns a unique set of values associated with the type specified. + */ + QStringList uniqueSet(UniqueSetType t) const; + + CollectionListItem *lookup(const QString &file) { return m_itemsDict.find(file); } + + virtual PlaylistItem *createItem(const FileHandle &file, + QListViewItem * = 0, + bool = false); + + void emitVisibleColumnsChanged() { emit signalVisibleColumnsChanged(); } + + virtual void clearItems(const PlaylistItemList &items); + + void setupTreeViewEntries(ViewMode *viewMode) const; + + virtual bool canReload() const { return true; } + +public slots: + virtual void paste() { decode(kapp->clipboard()->data()); } + virtual void clear(); + void slotCheckCache(); + + void slotRemoveItem(const QString &file); + void slotRefreshItem(const QString &file); + + void slotNewItems(const KFileItemList &items); + void slotRefreshItems(const KFileItemList &items); + void slotDeleteItem(KFileItem *item); + +protected: + CollectionList(PlaylistCollection *collection); + virtual ~CollectionList(); + + virtual void contentsDropEvent(QDropEvent *e); + virtual void contentsDragMoveEvent(QDragMoveEvent *e); + + // These methods are used by CollectionListItem, which is a friend class. + + void addToDict(const QString &file, CollectionListItem *item) { m_itemsDict.replace(file, item); } + void removeFromDict(const QString &file) { m_itemsDict.remove(file); } + + // These methods are also used by CollectionListItem, to manage the + // strings used in generating the unique sets and tree view mode playlists. + + QString addStringToDict(const QString &value, unsigned column); + void removeStringFromDict(const QString &value, unsigned column); + + void addWatched(const QString &file) { m_dirWatch->addFile(file); } + void removeWatched(const QString &file) { m_dirWatch->removeFile(file); } + + virtual bool hasItem(const QString &file) const { return m_itemsDict.find(file); } + +signals: + void signalCollectionChanged(); + + /** + * This is emitted when the set of columns that is visible is changed. + * + * \see Playlist::hideColumn() + * \see Playlist::showColumn() + * \see Playlsit::isColumnVisible() + */ + void signalVisibleColumnsChanged(); + void signalNewTag(const QString &, unsigned); + void signalRemovedTag(const QString &, unsigned); + +private: + /** + * Just the size of the above enum to keep from hard coding it in several + * locations. + */ + static const int m_uniqueSetCount = 3; + + static CollectionList *m_list; + QDict<CollectionListItem> m_itemsDict; + KDirWatch *m_dirWatch; + TagCountDicts m_columnTags; +}; + +class CollectionListItem : public PlaylistItem +{ + friend class Playlist; + friend class CollectionList; + friend class PlaylistItem; + + /** + * Needs access to the destructor, even though the destructor isn't used by QDict. + */ + friend class QDict<CollectionListItem>; + +public: + virtual void refresh(); + PlaylistItem *itemForPlaylist(const Playlist *playlist); + void updateCollectionDict(const QString &oldPath, const QString &newPath); + void repaint() const; + PlaylistItemList children() const { return m_children; } + +protected: + CollectionListItem(const FileHandle &file); + virtual ~CollectionListItem(); + + void addChildItem(PlaylistItem *child); + void removeChildItem(PlaylistItem *child); + + /** + * Returns true if the item is now up to date (even if this required a refresh) or + * false if the item is invalid. + */ + bool checkCurrent(); + + virtual CollectionListItem *collectionItem() { return this; } + +private: + bool m_shuttingDown; + PlaylistItemList m_children; +}; + +#endif diff --git a/juk/configure.in.bot b/juk/configure.in.bot new file mode 100644 index 00000000..f611be1c --- /dev/null +++ b/juk/configure.in.bot @@ -0,0 +1,40 @@ +if test "x$with_taglib" = xcheck && test "x$have_taglib" != xyes; then + echo "**************************************************" + echo "*" + echo "* JuK will not be built without TagLib." + echo "* See the notice below for where to find TagLib." + echo "*" + echo "**************************************************" +fi + +if test "x$with_gstreamer" = xcheck && test "x$have_gst" = xno; then + echo "**************************************************" + echo "*" + echo "* You do not seem to have GStreamer 0.8.x installed." + echo "* Without this aRts and/or aKode output will be used exclusively." + echo "*" + echo "* If you actually have gstreamer installed make sure you also have" + echo "* the gst-plugins collection installed. Both gstreamer and" + echo "* gst-plugins need to be 0.8.x (0.9 is not supported)" + echo "* http://gstreamer.freedesktop.org/modules/" + echo "*" + echo "* JuK supports GStreamer output but will also" + echo "* work with aRts and aKode." + echo "*" + echo "**************************************************" +fi + +if test "x$with_musicbrainz" = xcheck && test "x$have_musicbrainz" = xno; then + echo "**************************************************" + echo "*" + echo "* You do not seem to have libmusicbrainz and" + echo "* libtunepimp. JuK will be compiled without" + echo "* MusicBrainz support and automatic song" + echo "* recognition will not be supported." + echo "* Please download libmusicbrainz 2.x and libtunepimp" + echo "* 0.3.x from:" + echo "* http://www.musicbrainz.org/products/client/download.html " + echo "* http://www.musicbrainz.org/products/tunepimp/download.html" + echo "*" + echo "**************************************************" +fi diff --git a/juk/configure.in.in b/juk/configure.in.in new file mode 100644 index 00000000..9d357c67 --- /dev/null +++ b/juk/configure.in.in @@ -0,0 +1,102 @@ +#MIN_CONFIG(3) + +AM_INIT_AUTOMAKE(juk,1.0) + +AC_ARG_WITH(musicbrainz, + [AC_HELP_STRING(--with-musicbrainz, + [enable support for MusicBrainz @<:@default=check@:>@])], + [], with_musicbrainz=check) + +have_musicbrainz=no + +if test "x$with_musicbrainz" != xno; then + KDE_CHECK_HEADER(tunepimp/tp_c.h, have_musicbrainz=yes) + + if test "x$with_musicbrainz" != xcheck && test "x$have_musicbrainz" != xyes; then + AC_MSG_ERROR([--with-musicbrainz was given, but test for MusicBrainz failed]) + fi +fi + +if test "x$have_musicbrainz" = xyes; then + v=1 + KDE_CHECK_LIB(tunepimp, tp_SetFileNameEncoding, + [v=4]) + case "$v" in + 4) KDE_CHECK_LIB(tunepimp, tp_SetTRMCollisionThreshold, + AC_DEFINE(HAVE_MUSICBRAINZ, 4, [have MusicBrainz 0.4.x]), + [AC_MSG_WARN([Tunepimp 0.5 detected - disabled.]) + AC_DEFINE(HAVE_MUSICBRAINZ, 0, [have MusicBrainz 0.5.x - disabled]) + ]) + ;; + *) AC_DEFINE(HAVE_MUSICBRAINZ, 1, [have MusicBrainz]) ;; + esac +else + AC_DEFINE(HAVE_MUSICBRAINZ, 0, [have MusicBrainz]) +fi + +AM_CONDITIONAL(link_lib_MB, test "x$have_musicbrainz" = xyes) + +AC_ARG_WITH(gstreamer, + [AC_HELP_STRING(--with-gstreamer, + [enable support for GStreamer @<:@default=check@:>@])], + [], with_gstreamer=check) + +have_gst=no + +if test "x$with_gstreamer" != xno; then + # pkg-config seems to have a bug where it masks needed -L entries when it + # shouldn't, so disable that. + + PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 + export PKG_CONFIG_ALLOW_SYSTEM_LIBS + + GST_MAJORMINOR=0.10 + GST_REQ=0.10.0 + GST_VERSION=10 + + PKG_CHECK_MODULES(GST, \ + gstreamer-$GST_MAJORMINOR >= $GST_REQ, \ + have_gst=yes, have_gst=no) + + if test "x$have_gst" != xyes; then + + GST_MAJORMINOR=0.8 + GST_REQ=0.8.0 + GST_VERSION=8 + + PKG_CHECK_MODULES(GST, \ + gstreamer-$GST_MAJORMINOR >= $GST_REQ, \ + have_gst=yes, have_gst=no) + fi + + if test "x$with_gstreamer" != xcheck && test "x$have_gst" != xyes; then + AC_MSG_ERROR([--with-gstreamer was given, but test for GStreamer failed]) + fi +fi + +if test "x$have_gst" = "xno"; then + GST_CFLAGS="" + LDADD_GST="" + LDFLAGS_GST="" + AC_DEFINE(HAVE_GSTREAMER, 0, [have GStreamer]) +else + LDADD_GST=`$PKG_CONFIG --libs-only-l gstreamer-$GST_MAJORMINOR` + LDFLAGS_GST=`$PKG_CONFIG --libs-only-other gstreamer-$GST_MAJORMINOR` + + # Append -L entries, since they are masked by --libs-only-l and + # --libs-only-other + LIBDIRS_GST=`$PKG_CONFIG --libs-only-L gstreamer-$GST_MAJORMINOR` + LDADD_GST="$LDADD_GST $LIBDIRS_GST" + + AC_MSG_NOTICE([GStreamer version >= $GST_REQ found.]) + AC_DEFINE(HAVE_GSTREAMER, 1, [have GStreamer]) + AC_DEFINE_UNQUOTED(GSTREAMER_VERSION, $GST_VERSION, [GStreamer Version]) +fi + +AC_SUBST(GST_CFLAGS) +AC_SUBST(LDADD_GST) +AC_SUBST(LDFLAGS_GST) + +if test "x$have_taglib" != xyes || ( test "x$build_arts" = "xno" && test "x$have_gst" = "xno" && test "x$have_akode" = "xno") ; then + DO_NOT_COMPILE="$DO_NOT_COMPILE juk" +fi diff --git a/juk/coverdialog.cpp b/juk/coverdialog.cpp new file mode 100644 index 00000000..cdb9853a --- /dev/null +++ b/juk/coverdialog.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + begin : Sun May 15 2005 + copyright : (C) 2005 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <klistview.h> +#include <kiconview.h> +#include <kiconviewsearchline.h> +#include <kiconloader.h> +#include <kapplication.h> +#include <kpopupmenu.h> +#include <klocale.h> + +#include <qtimer.h> +#include <qtoolbutton.h> + +#include "coverdialog.h" +#include "covericonview.h" +#include "covermanager.h" +#include "collectionlist.h" + +using CoverUtility::CoverIconViewItem; + +class AllArtistsListViewItem : public KListViewItem +{ +public: + AllArtistsListViewItem(QListView *parent) : + KListViewItem(parent, i18n("<All Artists>")) + { + } + + int compare(QListViewItem *, int, bool) const + { + return -1; // Always be at the top. + } +}; + +class CaseInsensitiveItem : public KListViewItem +{ +public: + CaseInsensitiveItem(QListView *parent, const QString &text) : + KListViewItem(parent, text) + { + } + + int compare(QListViewItem *item, int column, bool ascending) const + { + Q_UNUSED(ascending); + return text(column).lower().localeAwareCompare(item->text(column).lower()); + } +}; + +CoverDialog::CoverDialog(QWidget *parent) : + CoverDialogBase(parent, "juk_cover_dialog", WType_Dialog) +{ + m_covers->setResizeMode(QIconView::Adjust); + m_covers->setGridX(140); + m_covers->setGridY(150); + + m_searchLine->setIconView(m_covers); + m_clearSearch->setIconSet(SmallIconSet("locationbar_erase")); +} + +CoverDialog::~CoverDialog() +{ +} + +void CoverDialog::show() +{ + m_artists->clear(); + m_covers->clear(); + + QStringList artists = CollectionList::instance()->uniqueSet(CollectionList::Artists); + + m_artists->setSorting(-1); + new AllArtistsListViewItem(m_artists); + for(QStringList::ConstIterator it = artists.begin(); it != artists.end(); ++it) + new CaseInsensitiveItem(m_artists, *it); + + m_artists->setSorting(0); + + QTimer::singleShot(0, this, SLOT(loadCovers())); + CoverDialogBase::show(); +} + +// Here we try to keep the GUI from freezing for too long while we load the +// covers. +void CoverDialog::loadCovers() +{ + QValueList<coverKey> keys = CoverManager::keys(); + QValueList<coverKey>::ConstIterator it; + int i = 0; + + for(it = keys.begin(); it != keys.end(); ++it) { + new CoverIconViewItem(*it, m_covers); + + if(++i == 10) { + i = 0; + kapp->processEvents(); + } + } +} + +// TODO: Add a way to show cover art for tracks with no artist. +void CoverDialog::slotArtistClicked(QListViewItem *item) +{ + m_covers->clear(); + + if(dynamic_cast<AllArtistsListViewItem *>(item)) { + // All artists. + loadCovers(); + } + else { + QString artist = item->text(0).lower(); + QValueList<coverKey> keys = CoverManager::keys(); + QValueList<coverKey>::ConstIterator it; + + for(it = keys.begin(); it != keys.end(); ++it) { + CoverDataPtr data = CoverManager::coverInfo(*it); + if(data->artist == artist) + new CoverIconViewItem(*it, m_covers); + } + } +} + +void CoverDialog::slotContextRequested(QIconViewItem *item, const QPoint &pt) +{ + static KPopupMenu *menu = 0; + + if(!item) + return; + + if(!menu) { + menu = new KPopupMenu(this); + menu->insertItem(i18n("Remove Cover"), this, SLOT(removeSelectedCover())); + } + + menu->popup(pt); +} + +void CoverDialog::removeSelectedCover() +{ + CoverIconViewItem *coverItem = m_covers->currentItem(); + + if(!coverItem || !coverItem->isSelected()) { + kdWarning(65432) << "No item selected for removeSelectedCover.\n"; + return; + } + + if(!CoverManager::removeCover(coverItem->id())) + kdError(65432) << "Unable to remove selected cover: " << coverItem->id() << endl; + else + delete coverItem; +} + +#include "coverdialog.moc" + +// vim: set et ts=4 sw=4: diff --git a/juk/coverdialog.h b/juk/coverdialog.h new file mode 100644 index 00000000..3ced0d75 --- /dev/null +++ b/juk/coverdialog.h @@ -0,0 +1,41 @@ +/*************************************************************************** + begin : Sun May 15 2005 + copyright : (C) 2005 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_COVERDIALOG_H +#define JUK_COVERDIALOG_H + +#include "coverdialogbase.h" + +class CoverDialog : public CoverDialogBase +{ + Q_OBJECT +public: + CoverDialog(QWidget *parent); + ~CoverDialog(); + + virtual void show(); + +public slots: + void slotArtistClicked(QListViewItem *item); + void slotContextRequested(QIconViewItem *item, const QPoint &pt); + +private slots: + void loadCovers(); + void removeSelectedCover(); +}; + +#endif /* JUK_COVERDIALOG_H */ + +// vim: set et ts=4 sw=4: diff --git a/juk/coverdialogbase.ui b/juk/coverdialogbase.ui new file mode 100644 index 00000000..180dc8b0 --- /dev/null +++ b/juk/coverdialogbase.ui @@ -0,0 +1,210 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>CoverDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>CoverDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>685</width> + <height>554</height> + </rect> + </property> + <property name="caption"> + <string>Cover Manager</string> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KListView"> + <column> + <property name="text"> + <string>Artist</string> + </property> + <property name="clickable"> + <bool>false</bool> + </property> + <property name="resizable"> + <bool>false</bool> + </property> + </column> + <item> + <property name="text"> + <string><All></string> + </property> + <property name="pixmap"> + <pixmap></pixmap> + </property> + </item> + <property name="name"> + <cstring>m_artists</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>164</width> + <height>0</height> + </size> + </property> + <property name="resizeMode"> + <enum>LastColumn</enum> + </property> + <property name="shadeSortColumn"> + <bool>false</bool> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QToolButton"> + <property name="name"> + <cstring>m_clearSearch</cstring> + </property> + <property name="text"> + <string>Clear Search</string> + </property> + <property name="toolTip" stdset="0"> + <string>Clear Search</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Clear the current cover search.</string> + </property> + </widget> + <widget class="KIconViewSearchLine"> + <property name="name"> + <cstring>m_searchLine</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>0</vsizetype> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </hbox> + </widget> + <widget class="CoverIconView"> + <property name="name"> + <cstring>m_covers</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>7</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="gridX" stdset="0"> + <number>140</number> + </property> + <property name="gridY" stdset="0"> + <number>150</number> + </property> + </widget> + </vbox> + </widget> + </hbox> +</widget> +<customwidgets> + <customwidget> + <class>KIconViewSearchLine</class> + <header location="global">kiconviewsearchline.h</header> + <sizehint> + <width>100</width> + <height>-1</height> + </sizehint> + <container>0</container> + <sizepolicy> + <hordata>5</hordata> + <verdata>0</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + <slot access="public" specifier="">clear()</slot> + </customwidget> + <customwidget> + <class>CoverIconView</class> + <header location="local">covericonview.h</header> + <sizehint> + <width>100</width> + <height>100</height> + </sizehint> + <container>1</container> + <sizepolicy> + <hordata>7</hordata> + <verdata>7</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + <signal>contextMenuRequested(QIconViewItem *item, const QPoint &pos)</signal> + <property type="Int">gridX</property> + <property type="Int">gridY</property> + </customwidget> +</customwidgets> +<images> + <image name="image0"> + <data format="PNG" length="1002">89504e470d0a1a0a0000000d4948445200000016000000160806000000c4b46c3b000003b149444154388dad945f4c5b551cc73fe7dc4b7b4bcba0762d45c43114323599ee6192609c51d883892ce083f1718b3ebb185f8dc91e972cf39d2d2a2f1af664b6f1e0fe3863a0718969700eb0c52142da0242a1bd6d696f7bcff101585203ceb8fd9ece39f99dcff9fe7edf939f88c562ec465f5f9fe609442c161362173c3e3eae7b7a7ac8e7f36432196cdbfe4f907c3e4f2291201e8fe338cec3737357e9e8e828aded1e229d650e1f2d51754b082110124c13a4dc5ea341eb9dc284c0558a853f3ce8cb0677ef500fde7d39d2596679e326597b8e9abb85d7a770ab16ab6983ec5a05b487a70e36f0f4e10afe408d6a558310980108478dba4a1e8233990c5d474b64ed39aa3a8fe5f3317fbf81dbd70bccfeb205947632fd74f6589c1c6ea2f70d03a58ba0c1f2c9bdc1b66de3b8256a6e11cbe7e3ee1d181b590124fe2693aeee08d223c82c3a2c24b7b874bec8f26288774f7bd054504aef0dde6e99c0eb83f9fb266323cb80a27fb0958141836044605a2ee5523393371cc646fee2da37195aa35d0c0c5b4859ac03d7e91712dcaac5adab3650a3ff9d08ef7dd8404bb48869e5d958b5b87dadc4c9a1464e9f0d0326df7ebd86bd2e310cb1bf62d384d59441f2d70a070e1c60e09489929b988681bdd9cc97170bcc4c65595f71f8e0e3301337fc24a7732467831875a47f289652b0be5e4151e6d07316c1b0c0340d8ab92023e76d66a6b2840e36d2fb7a13fee632475e6edc367ea98a90fb98b7dd6310ca0328a44761582e1bab41befabcc0ec940d28bc5e93b68e064cab84e1d9beaeb48934eac1f53b01c1b000fca496aa54b61a99fcde61662a4b4b4b23d1680be9d426173e4df3602a48ea411989a4fd590f52a8fd156b05ed9d350e3defe3cfdf4b4c7ce770ea7d3fb9f520afbe1620daeee5c26735d20b9b9cfb6811a754a439e4e5c5639a4caa1e5caf586bfc0197b78702005cb9b4cae4cd3267ce8638fe964bd72b393e39d74928d242617303a756a37f284447770dcdbffc6384a05a85de1306e9a52057c7527c7131c3c42d3f475eb2303c82d4fc3276d6811db37efeb148723082d9b08f79f97c1e5729109a9a28307cc622d2d6cdf52b2b24efe548dedb00142009862cfa879ee1a71f6cec928353511472fbf4389148b0b0e0c108081412458dfe21c9f11351e67e7358595468246d1d1e5e38a6e9e851bc39d84ab502a669331dafec0d8ec7e3e8cb06e1a881d727d1ae40180a434a8c9db129a54126ad48a7358c2b4c5352c8c374bcccdab2bb37d8719cba79fab8211f9df218e0582c261e95f8bfc04f1a1e8bc5c4dfe0a190172af6a9690000000049454e44ae426082</data> + </image> +</images> +<connections> + <connection> + <sender>m_artists</sender> + <signal>clicked(QListViewItem*)</signal> + <receiver>CoverDialogBase</receiver> + <slot>slotArtistClicked(QListViewItem*)</slot> + </connection> + <connection> + <sender>m_clearSearch</sender> + <signal>clicked()</signal> + <receiver>m_searchLine</receiver> + <slot>clear()</slot> + </connection> + <connection> + <sender>m_covers</sender> + <signal>contextMenuRequested(QIconViewItem*,const QPoint&)</signal> + <receiver>CoverDialogBase</receiver> + <slot>slotContextRequested(QIconViewItem*,const QPoint&)</slot> + </connection> +</connections> +<slots> + <slot>slotArtistClicked(QListViewItem *item)</slot> + <slot>slotContextRequested(QIconViewItem *, const QPoint &pt)</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<forwards> + <forward>class QIconViewItem;</forward> +</forwards> +<includehints> + <includehint>klistview.h</includehint> + <includehint>kiconviewsearchline.h</includehint> + <includehint>covericonview.h</includehint> +</includehints> +</UI> diff --git a/juk/covericonview.cpp b/juk/covericonview.cpp new file mode 100644 index 00000000..88defcab --- /dev/null +++ b/juk/covericonview.cpp @@ -0,0 +1,48 @@ +/*************************************************************************** + begin : Sat Jul 9 2005 + copyright : (C) 2005 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "covericonview.h" +#include "covermanager.h" + +using CoverUtility::CoverIconViewItem; + +CoverIconViewItem::CoverIconViewItem(coverKey id, QIconView *parent) : + KIconViewItem(parent), m_id(id) +{ + CoverDataPtr data = CoverManager::coverInfo(id); + setText(QString("%1 - %2").arg(data->artist, data->album)); + setPixmap(data->thumbnail()); +} + +CoverIconView::CoverIconView(QWidget *parent, const char *name) : KIconView(parent, name) +{ + setResizeMode(Adjust); +} + +CoverIconViewItem *CoverIconView::currentItem() const +{ + return static_cast<CoverIconViewItem *>(KIconView::currentItem()); +} + +QDragObject *CoverIconView::dragObject() +{ + CoverIconViewItem *item = currentItem(); + if(item) + return new CoverDrag(item->id(), this); + + return 0; +} + +// vim: set et ts=4 sw=4: diff --git a/juk/covericonview.h b/juk/covericonview.h new file mode 100644 index 00000000..4126cb3f --- /dev/null +++ b/juk/covericonview.h @@ -0,0 +1,62 @@ +/*************************************************************************** + begin : Sat Jul 9 2005 + copyright : (C) 2005 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_COVERICONVIEW_H +#define JUK_COVERICONVIEW_H + +#include <kiconview.h> + +#include "covermanager.h" + +// The WebImageFetcher dialog also has a class named CoverIconViewItem and I +// don't like the idea of naming it "CoverIVI" or something, so just namespace +// it out. I would merge them except for webimagefetcher's dependence on KIO +// and such. +namespace CoverUtility +{ + class CoverIconViewItem : public KIconViewItem + { + public: + CoverIconViewItem(coverKey id, QIconView *parent); + + coverKey id() const { return m_id; } + + private: + coverKey m_id; + }; +} + +using CoverUtility::CoverIconViewItem; + +/** + * This class subclasses KIconView in order to provide cover drag-and-drop + * support. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class CoverIconView : public KIconView +{ +public: + CoverIconView(QWidget *parent, const char *name); + + CoverIconViewItem *currentItem() const; + +protected: + virtual QDragObject *dragObject(); +}; + +#endif /* JUK_COVERICONVIEW_H */ + +// vim: set et ts=4 sw=4: diff --git a/juk/coverinfo.cpp b/juk/coverinfo.cpp new file mode 100644 index 00000000..243bc429 --- /dev/null +++ b/juk/coverinfo.cpp @@ -0,0 +1,283 @@ +/*************************************************************************** + copyright : (C) 2004 Nathan Toone + : (C) 2005 Michael Pyne <michael.pyne@kdemail.net> + email : nathan@toonetown.com +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kglobal.h> +#include <kapplication.h> +#include <kstandarddirs.h> +#include <kdebug.h> + +#include <qregexp.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qcursor.h> + +#include "collectionlist.h" +#include "playlistsearch.h" +#include "playlistitem.h" +#include "coverinfo.h" +#include "tag.h" + +struct CoverPopup : public QWidget +{ + CoverPopup(const QPixmap &image, const QPoint &p) : + QWidget(0, 0, WDestructiveClose | WX11BypassWM) + { + QHBoxLayout *layout = new QHBoxLayout(this); + QLabel *label = new QLabel(this); + + layout->addWidget(label); + label->setFrameStyle(QFrame::Box | QFrame::Raised); + label->setLineWidth(1); + label->setPixmap(image); + + setGeometry(p.x(), p.y(), label->width(), label->height()); + show(); + } + virtual void leaveEvent(QEvent *) { close(); } + virtual void mouseReleaseEvent(QMouseEvent *) { close(); } +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + + +CoverInfo::CoverInfo(const FileHandle &file) : + m_file(file), + m_hasCover(false), + m_haveCheckedForCover(false), + m_coverKey(CoverManager::NoMatch), + m_needsConverting(false) +{ + +} + +bool CoverInfo::hasCover() +{ + if(m_haveCheckedForCover) + return m_hasCover; + + m_haveCheckedForCover = true; + + // Check for new-style covers. First let's determine what our coverKey is + // if it's not already set, as that's also tracked by the CoverManager. + if(m_coverKey == CoverManager::NoMatch) + m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); + + // We were assigned a key, let's see if we already have a cover. Notice + // that due to the way the CoverManager is structured, we should have a + // cover if we have a cover key. If we don't then either there's a logic + // error, or the user has been mucking around where they shouldn't. + if(m_coverKey != CoverManager::NoMatch) + m_hasCover = CoverManager::hasCover(m_coverKey); + + // We *still* don't have it? Check the old-style covers then. + if(!m_hasCover) { + m_hasCover = QFile(coverLocation(FullSize)).exists(); + + if(m_hasCover) + m_needsConverting = true; + } + + return m_hasCover; +} + +void CoverInfo::clearCover() +{ + m_hasCover = false; + + // Yes, we have checked, and we don't have it. ;) + m_haveCheckedForCover = true; + + m_needsConverting = false; + + // We don't need to call removeCover because the CoverManager will + // automatically unlink the cover if we were the last track to use it. + CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch); + m_coverKey = CoverManager::NoMatch; +} + +void CoverInfo::setCover(const QImage &image) +{ + if(image.isNull()) + return; + + m_haveCheckedForCover = true; + m_needsConverting = false; + m_hasCover = true; + + QPixmap cover; + cover.convertFromImage(image); + + // If we use replaceCover we'll change the cover for every other track + // with the same coverKey, which we don't want since that case will be + // handled by Playlist. Instead just replace this track's cover. + m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album()); + if(m_coverKey != CoverManager::NoMatch) + CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); +} + +void CoverInfo::setCoverId(coverKey id) +{ + m_coverKey = id; + m_haveCheckedForCover = true; + m_needsConverting = false; + m_hasCover = id != CoverManager::NoMatch; + + // Inform CoverManager of the change. + CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); +} + +void CoverInfo::applyCoverToWholeAlbum(bool overwriteExistingCovers) const +{ + QString artist = m_file.tag()->artist(); + QString album = m_file.tag()->album(); + PlaylistSearch::ComponentList components; + ColumnList columns; + + columns.append(PlaylistItem::ArtistColumn); + components.append(PlaylistSearch::Component(artist, false, columns, PlaylistSearch::Component::Exact)); + + columns.clear(); + columns.append(PlaylistItem::AlbumColumn); + components.append(PlaylistSearch::Component(album, false, columns, PlaylistSearch::Component::Exact)); + + PlaylistList playlists; + playlists.append(CollectionList::instance()); + + PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll); + + // Search done, iterate through results. + + PlaylistItemList results = search.matchedItems(); + PlaylistItemList::ConstIterator it = results.constBegin(); + for(; it != results.constEnd(); ++it) { + + // Don't worry about files that somehow already have a tag, + // unless the coversion is forced. + if(!overwriteExistingCovers && !(*it)->file().coverInfo()->m_needsConverting) + continue; + + kdDebug(65432) << "Setting cover for: " << *it << endl; + (*it)->file().coverInfo()->setCoverId(m_coverKey); + } +} + +QPixmap CoverInfo::pixmap(CoverSize size) const +{ + if(m_needsConverting) + convertOldStyleCover(); + + if(m_coverKey == CoverManager::NoMatch) + return QPixmap(); + + if(size == Thumbnail) + return CoverManager::coverFromId(m_coverKey, CoverManager::Thumbnail); + else + return CoverManager::coverFromId(m_coverKey, CoverManager::FullSize); +} + +void CoverInfo::popup() const +{ + QPixmap image = pixmap(FullSize); + QPoint mouse = QCursor::pos(); + QRect desktop = KApplication::desktop()->screenGeometry(mouse); + + int x = mouse.x(); + int y = mouse.y(); + int height = image.size().height() + 4; + int width = image.size().width() + 4; + + // Detect the right direction to pop up (always towards the center of the + // screen), try to pop up with the mouse pointer 10 pixels into the image in + // both directions. If we're too close to the screen border for this margin, + // show it at the screen edge, accounting for the four pixels (two on each + // side) for the window border. + + if(x - desktop.x() < desktop.width() / 2) + x = (x - desktop.x() < 10) ? desktop.x() : (x - 10); + else + x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10); + + if(y - desktop.y() < desktop.height() / 2) + y = (y - desktop.y() < 10) ? desktop.y() : (y - 10); + else + y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10); + + new CoverPopup(image, QPoint(x, y)); +} + +/** + * DEPRECATED + */ +QString CoverInfo::coverLocation(CoverSize size) const +{ + QString fileName(QFile::encodeName(m_file.tag()->artist() + " - " + m_file.tag()->album())); + QRegExp maskedFileNameChars("[ /?:\"]"); + + fileName.replace(maskedFileNameChars, "_"); + fileName.append(".png"); + + QString dataDir = KGlobal::dirs()->saveLocation("appdata"); + QString subDir; + + switch (size) { + case FullSize: + subDir = "large/"; + break; + default: + break; + } + QString fileLocation = dataDir + "covers/" + subDir + fileName.lower(); + + return fileLocation; +} + +bool CoverInfo::convertOldStyleCover() const +{ + // Ah, old-style cover. Let's transfer it to the new system. + kdDebug() << "Found old style cover for " << m_file.absFilePath() << endl; + + QString artist = m_file.tag()->artist(); + QString album = m_file.tag()->album(); + QString oldLocation = coverLocation(FullSize); + m_coverKey = CoverManager::addCover(oldLocation, artist, album); + + m_needsConverting = false; + + if(m_coverKey != CoverManager::NoMatch) { + CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); + + // Now let's also set the ID for the tracks matching the track and + // artist at this point so that the conversion is complete, otherwise + // we can't tell apart the "No cover on purpose" and "Has no cover yet" + // possibilities. + + applyCoverToWholeAlbum(); + + // If we convert we need to remove the old cover otherwise we'll find + // it later if the user un-sets the new cover. + if(!QFile::remove(oldLocation)) + kdError(65432) << "Unable to remove converted cover at " << oldLocation << endl; + + return true; + } + else { + kdDebug() << "We were unable to replace the old style cover.\n"; + return false; + } +} + +// vim: set et sw=4 ts=8: diff --git a/juk/coverinfo.h b/juk/coverinfo.h new file mode 100644 index 00000000..1caae8d0 --- /dev/null +++ b/juk/coverinfo.h @@ -0,0 +1,67 @@ +/*************************************************************************** + copyright : (C) 2004 Nathan Toone + email : nathan@toonetown.com +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef COVERINFO_H +#define COVERINFO_H + +#include <qimage.h> + +#include "filehandle.h" +#include "covermanager.h" + +class CoverInfo +{ + friend class FileHandle; + +public: + enum CoverSize { FullSize, Thumbnail }; + + CoverInfo(const FileHandle &file); + + bool hasCover(); + + void clearCover(); + void setCover(const QImage &image = QImage()); + + // Use this to assign to a specific cover id. + void setCoverId(coverKey id); + + /** + * This function sets the cover identifier for all tracks that have the + * same Artist and Album as this track, to the cover identifier of this + * track. + * + * @param overwriteExistingCovers If set to true, this function will always + * apply the new cover to a track even if the track already had + * a different cover set. + */ + void applyCoverToWholeAlbum(bool overwriteExistingCovers = false) const; + + coverKey coverId() const { return m_coverKey; } + + QPixmap pixmap(CoverSize size) const; + void popup() const; + +private: + QString coverLocation(CoverSize size) const; + bool convertOldStyleCover() const; + + FileHandle m_file; + bool m_hasCover; + bool m_haveCheckedForCover; + mutable coverKey m_coverKey; + mutable bool m_needsConverting; +}; +#endif + diff --git a/juk/covermanager.cpp b/juk/covermanager.cpp new file mode 100644 index 00000000..1fe36cc4 --- /dev/null +++ b/juk/covermanager.cpp @@ -0,0 +1,577 @@ +/*************************************************************************** + begin : Sun May 15 2005 + copyright : (C) 2005 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <qpixmap.h> +#include <qmap.h> +#include <qstring.h> +#include <qfile.h> +#include <qimage.h> +#include <qdir.h> +#include <qdatastream.h> +#include <qdict.h> +#include <qcache.h> +#include <qmime.h> +#include <qbuffer.h> + +#include <kdebug.h> +#include <kstaticdeleter.h> +#include <kstandarddirs.h> +#include <kglobal.h> + +#include "covermanager.h" + +// This is a dictionary to map the track path to their ID. Otherwise we'd have +// to store this info with each CollectionListItem, which would break the cache +// of users who upgrade, and would just generally be a big mess. +typedef QDict<coverKey> TrackLookupMap; + +// This is responsible for making sure that the CoverManagerPrivate class +// gets properly destructed on shutdown. +static KStaticDeleter<CoverManagerPrivate> sd; + +const char *CoverDrag::mimetype = "application/x-juk-coverid"; +// Caches the QPixmaps for the covers so that the covers are not all kept in +// memory for no reason. +typedef QCache<QPixmap> CoverPixmapCache; + +CoverManagerPrivate *CoverManager::m_data = 0; + +// Used to save and load CoverData from a QDataStream +QDataStream &operator<<(QDataStream &out, const CoverData &data); +QDataStream &operator>>(QDataStream &in, CoverData &data); + +// +// Implementation of CoverData struct +// + +QPixmap CoverData::pixmap() const +{ + return CoverManager::coverFromData(*this, CoverManager::FullSize); +} + +QPixmap CoverData::thumbnail() const +{ + return CoverManager::coverFromData(*this, CoverManager::Thumbnail); +} + +/** + * This class is responsible for actually keeping track of the storage for the + * different covers and such. It holds the covers, and the map of path names + * to cover ids, and has a few utility methods to load and save the data. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + * @see CoverManager + */ +class CoverManagerPrivate +{ +public: + + /// Maps coverKey id's to CoverDataPtrs + CoverDataMap covers; + + /// Maps file names to coverKey id's. + TrackLookupMap tracks; + + /// A cache of the cover representations. The key format is: + /// 'f' followed by the pathname for FullSize covers, and + /// 't' followed by the pathname for Thumbnail covers. + CoverPixmapCache pixmapCache; + + CoverManagerPrivate() : tracks(1301), pixmapCache(2 * 1024 * 768) + { + loadCovers(); + pixmapCache.setAutoDelete(true); + } + + ~CoverManagerPrivate() + { + saveCovers(); + } + + /** + * Creates the data directory for the covers if it doesn't already exist. + * Must be in this class for loadCovers() and saveCovers(). + */ + void createDataDir() const; + + /** + * Returns the next available unused coverKey that can be used for + * inserting new items. + * + * @return unused id that can be used for new CoverData + */ + coverKey nextId() const; + + void saveCovers() const; + + private: + void loadCovers(); + + /** + * @return the full path and filename of the file storing the cover + * lookup map and the translations between pathnames and ids. + */ + QString coverLocation() const; +}; + +// +// Implementation of CoverManagerPrivate methods. +// +void CoverManagerPrivate::createDataDir() const +{ + QDir dir; + QString dirPath(QDir::cleanDirPath(coverLocation() + "/..")); + if(!dir.exists(dirPath)) + KStandardDirs::makeDir(dirPath); +} + +void CoverManagerPrivate::saveCovers() const +{ + kdDebug() << k_funcinfo << endl; + + // Make sure the directory exists first. + createDataDir(); + + QFile file(coverLocation()); + + kdDebug() << "Opening covers db: " << coverLocation() << endl; + + if(!file.open(IO_WriteOnly)) { + kdError() << "Unable to save covers to disk!\n"; + return; + } + + QDataStream out(&file); + + // Write out the version and count + out << Q_UINT32(0) << Q_UINT32(covers.count()); + + // Write out the data + for(CoverDataMap::ConstIterator it = covers.begin(); it != covers.end(); ++it) { + out << Q_UINT32(it.key()); + out << *it.data(); + } + + // Now write out the track mapping. + out << Q_UINT32(tracks.count()); + + QDictIterator<coverKey> trackMapIt(tracks); + while(trackMapIt.current()) { + out << trackMapIt.currentKey() << Q_UINT32(*trackMapIt.current()); + ++trackMapIt; + } +} + +void CoverManagerPrivate::loadCovers() +{ + kdDebug() << k_funcinfo << endl; + + QFile file(coverLocation()); + + if(!file.open(IO_ReadOnly)) { + // Guess we don't have any covers yet. + return; + } + + QDataStream in(&file); + Q_UINT32 count, version; + + // First thing we'll read in will be the version. + // Only version 0 is defined for now. + in >> version; + if(version > 0) { + kdError() << "Cover database was created by a higher version of JuK,\n"; + kdError() << "I don't know what to do with it.\n"; + + return; + } + + // Read in the count next, then the data. + in >> count; + for(Q_UINT32 i = 0; i < count; ++i) { + // Read the id, and 3 QStrings for every 1 of the count. + Q_UINT32 id; + CoverDataPtr data(new CoverData); + + in >> id; + in >> *data; + data->refCount = 0; + + covers[(coverKey) id] = data; + } + + in >> count; + for(Q_UINT32 i = 0; i < count; ++i) { + QString path; + Q_UINT32 id; + + in >> path >> id; + + // If we somehow already managed to load a cover id with this path, + // don't do so again. Possible due to a coding error during 3.5 + // development. + + if(!tracks.find(path)) { + ++covers[(coverKey) id]->refCount; // Another track using this. + tracks.insert(path, new coverKey(id)); + } + } +} + +QString CoverManagerPrivate::coverLocation() const +{ + return KGlobal::dirs()->saveLocation("appdata") + "coverdb/covers"; +} + +// XXX: This could probably use some improvement, I don't like the linear +// search for ID idea. +coverKey CoverManagerPrivate::nextId() const +{ + // Start from 1... + coverKey key = 1; + + while(covers.contains(key)) + ++key; + + return key; +} + +// +// Implementation of CoverDrag +// +CoverDrag::CoverDrag(coverKey id, QWidget *src) : QDragObject(src, "coverDrag"), + m_id(id) +{ + QPixmap cover = CoverManager::coverFromId(id); + if(!cover.isNull()) + setPixmap(cover); +} + +const char *CoverDrag::format(int i) const +{ + if(i == 0) + return mimetype; + if(i == 1) + return "image/png"; + + return 0; +} + +QByteArray CoverDrag::encodedData(const char *mimetype) const +{ + if(qstrcmp(CoverDrag::mimetype, mimetype) == 0) { + QByteArray data; + QDataStream ds(data, IO_WriteOnly); + + ds << Q_UINT32(m_id); + return data; + } + else if(qstrcmp(mimetype, "image/png") == 0) { + QPixmap large = CoverManager::coverFromId(m_id, CoverManager::FullSize); + QImage img = large.convertToImage(); + QByteArray data; + QBuffer buffer(data); + + buffer.open(IO_WriteOnly); + img.save(&buffer, "PNG"); // Write in PNG format. + + return data; + } + + return QByteArray(); +} + +bool CoverDrag::canDecode(const QMimeSource *e) +{ + return e->provides(mimetype); +} + +bool CoverDrag::decode(const QMimeSource *e, coverKey &id) +{ + if(!canDecode(e)) + return false; + + QByteArray data = e->encodedData(mimetype); + QDataStream ds(data, IO_ReadOnly); + Q_UINT32 i; + + ds >> i; + id = (coverKey) i; + + return true; +} + +// +// Implementation of CoverManager methods. +// +coverKey CoverManager::idFromMetadata(const QString &artist, const QString &album) +{ + // Search for the string, yay! It might make sense to use a cache here, + // if so it's not hard to add a QCache. + CoverDataMap::ConstIterator it = begin(); + CoverDataMap::ConstIterator endIt = end(); + + for(; it != endIt; ++it) { + if(it.data()->album == album.lower() && it.data()->artist == artist.lower()) + return it.key(); + } + + return NoMatch; +} + +QPixmap CoverManager::coverFromId(coverKey id, Size size) +{ + CoverDataPtr info = coverInfo(id); + + if(!info) + return QPixmap(); + + if(size == Thumbnail) + return info->thumbnail(); + + return info->pixmap(); +} + +QPixmap CoverManager::coverFromData(const CoverData &coverData, Size size) +{ + QString path = coverData.path; + + // Prepend a tag to the path to separate in the cache between full size + // and thumbnail pixmaps. If we add a different kind of pixmap in the + // future we also need to add a tag letter for it. + if(size == FullSize) + path.prepend('f'); + else + path.prepend('t'); + + // Check in cache for the pixmap. + QPixmap *pix = data()->pixmapCache[path]; + if(pix) { + kdDebug(65432) << "Found pixmap in cover cache.\n"; + return *pix; + } + + // Not in cache, load it and add it. + pix = new QPixmap(coverData.path); + if(pix->isNull()) + return QPixmap(); + + if(size == Thumbnail) { + // Convert to image for smoothScale() + QImage image = pix->convertToImage(); + pix->convertFromImage(image.smoothScale(80, 80, QImage::ScaleMin)); + } + + QPixmap returnValue = *pix; // Save it early. + if(!data()->pixmapCache.insert(path, pix, pix->height() * pix->width())) + delete pix; + + return returnValue; +} + +coverKey CoverManager::addCover(const QPixmap &large, const QString &artist, const QString &album) +{ + kdDebug() << k_funcinfo << endl; + + coverKey id = data()->nextId(); + CoverDataPtr coverData(new CoverData); + + if(large.isNull()) { + kdDebug() << "The pixmap you're trying to add is NULL!\n"; + return NoMatch; + } + + // Save it to file first! + + QString ext = QString("/coverdb/coverID-%1.png").arg(id); + coverData->path = KGlobal::dirs()->saveLocation("appdata") + ext; + + kdDebug() << "Saving pixmap to " << coverData->path << endl; + data()->createDataDir(); + + if(!large.save(coverData->path, "PNG")) { + kdError() << "Unable to save pixmap to " << coverData->path << endl; + return NoMatch; + } + + coverData->artist = artist.lower(); + coverData->album = album.lower(); + coverData->refCount = 0; + + data()->covers[id] = coverData; + + // Make sure the new cover isn't inadvertently cached. + data()->pixmapCache.remove(QString("f%1").arg(coverData->path)); + data()->pixmapCache.remove(QString("t%1").arg(coverData->path)); + + return id; +} + +coverKey CoverManager::addCover(const QString &path, const QString &artist, const QString &album) +{ + return addCover(QPixmap(path), artist, album); +} + +bool CoverManager::hasCover(coverKey id) +{ + return data()->covers.contains(id); +} + +bool CoverManager::removeCover(coverKey id) +{ + if(!hasCover(id)) + return false; + + // Remove cover from cache. + CoverDataPtr coverData = coverInfo(id); + data()->pixmapCache.remove(QString("f%1").arg(coverData->path)); + data()->pixmapCache.remove(QString("t%1").arg(coverData->path)); + + // Remove references to files that had that track ID. + QDictIterator<coverKey> it(data()->tracks); + for(; it.current(); ++it) + if(*it.current() == id) + data()->tracks.remove(it.currentKey()); + + // Remove covers from disk. + QFile::remove(coverData->path); + + // Finally, forget that we ever knew about this cover. + data()->covers.remove(id); + + return true; +} + +bool CoverManager::replaceCover(coverKey id, const QPixmap &large) +{ + if(!hasCover(id)) + return false; + + CoverDataPtr coverData = coverInfo(id); + + // Empty old pixmaps from cache. + data()->pixmapCache.remove(QString("%1%2").arg("t", coverData->path)); + data()->pixmapCache.remove(QString("%1%2").arg("f", coverData->path)); + + large.save(coverData->path, "PNG"); + return true; +} + +CoverManagerPrivate *CoverManager::data() +{ + if(!m_data) + sd.setObject(m_data, new CoverManagerPrivate); + + return m_data; +} + +void CoverManager::saveCovers() +{ + data()->saveCovers(); +} + +void CoverManager::shutdown() +{ + sd.destructObject(); +} + +CoverDataMap::ConstIterator CoverManager::begin() +{ + return data()->covers.constBegin(); +} + +CoverDataMap::ConstIterator CoverManager::end() +{ + return data()->covers.constEnd(); +} + +QValueList<coverKey> CoverManager::keys() +{ + return data()->covers.keys(); +} + +void CoverManager::setIdForTrack(const QString &path, coverKey id) +{ + coverKey *oldId = data()->tracks.find(path); + if(oldId && (id == *oldId)) + return; // We're already done. + + if(oldId && *oldId != NoMatch) { + data()->covers[*oldId]->refCount--; + data()->tracks.remove(path); + + if(data()->covers[*oldId]->refCount == 0) { + kdDebug(65432) << "Cover " << *oldId << " is unused, removing.\n"; + removeCover(*oldId); + } + } + + if(id != NoMatch) { + data()->covers[id]->refCount++; + data()->tracks.insert(path, new coverKey(id)); + } +} + +coverKey CoverManager::idForTrack(const QString &path) +{ + coverKey *coverPtr = data()->tracks.find(path); + + if(!coverPtr) + return NoMatch; + + return *coverPtr; +} + +CoverDataPtr CoverManager::coverInfo(coverKey id) +{ + if(data()->covers.contains(id)) + return data()->covers[id]; + + return CoverDataPtr(0); +} + +/** + * Write @p data out to @p out. + * + * @param out the data stream to write @p data out to. + * @param data the CoverData to write out. + * @return the data stream that the data was written to. + */ +QDataStream &operator<<(QDataStream &out, const CoverData &data) +{ + out << data.artist; + out << data.album; + out << data.path; + + return out; +} + +/** + * Read @p data from @p in. + * + * @param in the data stream to read from. + * @param data the CoverData to read into. + * @return the data stream read from. + */ +QDataStream &operator>>(QDataStream &in, CoverData &data) +{ + in >> data.artist; + in >> data.album; + in >> data.path; + + return in; +} + +// vim: set et sw=4 ts=4: diff --git a/juk/covermanager.h b/juk/covermanager.h new file mode 100644 index 00000000..9a95d991 --- /dev/null +++ b/juk/covermanager.h @@ -0,0 +1,262 @@ +/*************************************************************************** + begin : Sun May 15 2005 + copyright : (C) 2005 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_COVERMANAGER_H +#define JUK_COVERMANAGER_H + +#include <ksharedptr.h> + +#include <qmap.h> +#include <qdragobject.h> + +class CoverManagerPrivate; +class QString; +class QPixmap; +class QDataStream; + +/** + * This class holds the data on a cover. This includes the path to the cover + * representation on-disk, and the artist and album associated with the cover. + * Don't assume that the artist or album information is filled out, it is + * there to allow the CoverManager to try to automatically assign covers to + * new tracks. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + * @see CoverManager + */ +class CoverData : public KShared +{ +public: + QPixmap pixmap() const; + QPixmap thumbnail() const; + + QString artist; + QString album; + QString path; + + unsigned refCount; // Refers to number of tracks using this. +}; + +typedef KSharedPtr<CoverData> CoverDataPtr; +typedef unsigned long coverKey; ///< Type of the id for a cover. +typedef QMap<coverKey, CoverDataPtr> CoverDataMap; + +/** + * This class is used to drag covers in JuK. It adds a special mimetype that + * contains the cover ID used for this cover, and also supports an image/png + * mimetype for dragging to other applications. + * + * As of this writing the mimetype is application/x-juk-coverid + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class CoverDrag : public QDragObject +{ +public: + CoverDrag(coverKey id, QWidget *src); + + virtual const char *format(int i) const; + virtual QByteArray encodedData(const char *mimetype) const; + + void setId(coverKey id) { m_id = id; } + + /** + * Returns true if CoverDrag can decode the given mime source. Note that + * true is returned only if \p e contains a cover id, even though + * CoverDrag can convert it to an image. + */ + static bool canDecode(const QMimeSource *e); + static bool decode(const QMimeSource *e, coverKey &id); + + static const char* mimetype; + +private: + coverKey m_id; +}; + +/** + * This class holds all of the cover art, and manages looking it up by artist + * and/or album. This class is similar to a singleton class, but instead all + * of the methods are static. This way you can invoke methods like this: + * \code + * CoverManager::method() + * \endcode + * instead of using: + * \code + * CoverManager::instance()->method() + * \endcode + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class CoverManager +{ +public: + /// The set of different sizes you can request a pixmap as. + typedef enum { Thumbnail, FullSize } Size; + + /** + * Tries to match @p artist and @p album to a cover in the database. + * + * @param artist The artist to look for matching covers on. + * @param album The album to look for matching covers on. + * @return NoMatch if no match could be found, otherwise the id of the + * cover art that matches the given metadata. + */ + static coverKey idFromMetadata(const QString &artist, const QString &album); + + /** + * Returns the cover art for @p id. + * + * @param id The id of the cover. + * @param size The size to return it as. Note that FullSize doesn't + * necessarily mean the pixmap is large, so you may need to + * scale it up. + * @return QPixmap::null if there is no cover art for @p id, otherwise the + * cover art. + */ + static QPixmap coverFromId(coverKey id, Size size = Thumbnail); + + /** + * Returns the cover art for @p ptr. This function is intended for use + * by CoverData. + * + * @param ptr The CoverData to get the cover of. Note that it is a + * CoverData, not CoverDataPtr. + * @param size The size to return it as. + * @see CoverData + */ + static QPixmap coverFromData(const CoverData &coverData, Size size = Thumbnail); + + /** + * Returns the full suite of information known about the cover given by + * @p id. + * + * @param id the id of the cover to retrieve info on. + * @return 0 if there is no info on @p id, otherwise its information. + */ + static CoverDataPtr coverInfo(coverKey id); + + /** + * Adds @p large to the cover database, associating with it @p artist and + * @p album. + * + * @param large The full size cover (the thumbnail is automatically + * generated). + * @param artist The artist of the new cover. + * @param album The album of the new cover. + */ + static coverKey addCover(const QPixmap &large, const QString &artist = "", const QString &album = ""); + + /** + * Adds the file pointed to by the local path @p path to the database, + * associating it with @p artist and @p album. + * + * @param path The absolute path to the fullsize cover art. + * @param artist The artist of the new cover. + * @param album The album of the new cover. + */ + static coverKey addCover(const QString &path, const QString &artist = "", const QString &album = ""); + + /** + * Function to determine if @p id matches any covers in the database. + * + * @param id The id of the cover to search for. + * @return true if the database has a cover identified by @p id, false + * otherwise. + */ + static bool hasCover(coverKey id); + + /** + * Removes the cover identified by @p id. + * + * @param id the id of the cover to remove. + * @return true if the removal was successful, false if unsuccessful or if + * the cover didn't exist. + */ + static bool removeCover(coverKey id); + + /** + * Replaces the cover art for the cover identified by @p id with @p large. + * Any other metadata such as artist and album is unchanged. + * + * @param id The id of the cover to replace. + * @param large The full size cover art for the new cover. + */ + static bool replaceCover(coverKey id, const QPixmap &large); + + /** + * Saves the current CoverManager information to disk. Changes are not + * automatically written to disk due to speed issues, so you can + * periodically call this function while running to reduce the chance of + * lost data in the event of a crash. + */ + static void saveCovers(); + + /** + * This is a hack, as we should be shut down automatically by + * KStaticDeleter, but JuK is crashing for me on shutdown before + * KStaticDeleter gets a chance to run, which is cramping my testing. + */ + static void shutdown(); + + /** + * @return Iterator pointing to the first element in the cover database. + */ + static CoverDataMap::ConstIterator begin(); + + /** + * @return Iterator pointing after the last element in the cover database. + */ + static CoverDataMap::ConstIterator end(); + + /** + * @return A list of all of the id's listed in the database. + */ + static QValueList<coverKey> keys(); + + /** + * Associates @p path with the cover identified by @id. No comparison of + * metadata is performed to enforce this matching. + * + * @param path The absolute file path to the track. + * @param id The identifier of the cover to use with @p path. + */ + static void setIdForTrack(const QString &path, coverKey id); + + /** + * Returns the identifier of the cover for the track at @p path. + * + * @param path The absolute file path to the track. + * @return NoMatch if @p path doesn't have a cover, otherwise the id of + * its cover. + */ + static coverKey idForTrack(const QString &path); + + /** + * This identifier is used to indicate that no cover was found in the + * database. + */ + static const coverKey NoMatch = 0; + + private: + static CoverManagerPrivate *m_data; + + static CoverManagerPrivate *data(); + static QPixmap createThumbnail(const QPixmap &base); +}; + +#endif /* JUK_COVERMANAGER_H */ + +// vim: set et sw=4 ts=4: diff --git a/juk/cr22-action-juk_dock.png b/juk/cr22-action-juk_dock.png Binary files differnew file mode 100644 index 00000000..8d023aa6 --- /dev/null +++ b/juk/cr22-action-juk_dock.png diff --git a/juk/deletedialog.cpp b/juk/deletedialog.cpp new file mode 100644 index 00000000..cafd7849 --- /dev/null +++ b/juk/deletedialog.cpp @@ -0,0 +1,121 @@ +/*************************************************************************** + begin : Tue Aug 31 21:59:58 EST 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kdialogbase.h> +#include <kglobal.h> +#include <kstdguiitem.h> +#include <klocale.h> +#include <kiconloader.h> +#include <kconfig.h> + +#include <qstringlist.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qvbox.h> +#include <qhbox.h> + +#include "deletedialog.h" + +////////////////////////////////////////////////////////////////////////////// +// DeleteWidget implementation +////////////////////////////////////////////////////////////////////////////// + +DeleteWidget::DeleteWidget(QWidget *parent, const char *name) + : DeleteDialogBase(parent, name) +{ + KConfigGroup messageGroup(KGlobal::config(), "FileRemover"); + + bool deleteInstead = messageGroup.readBoolEntry("deleteInsteadOfTrash", false); + slotShouldDelete(deleteInstead); + ddShouldDelete->setChecked(deleteInstead); +} + +void DeleteWidget::setFiles(const QStringList &files) +{ + ddFileList->clear(); + ddFileList->insertStringList(files); + ddNumFiles->setText(i18n("<b>1</b> file selected.", "<b>%n</b> files selected.", files.count())); +} + +void DeleteWidget::slotShouldDelete(bool shouldDelete) +{ + if(shouldDelete) { + ddDeleteText->setText(i18n("<qt>These items will be <b>permanently " + "deleted</b> from your hard disk.</qt>")); + ddWarningIcon->setPixmap(KGlobal::iconLoader()->loadIcon("messagebox_warning", + KIcon::Desktop, KIcon::SizeLarge)); + } + else { + ddDeleteText->setText(i18n("<qt>These items will be moved to the Trash Bin.</qt>")); + ddWarningIcon->setPixmap(KGlobal::iconLoader()->loadIcon("trashcan_full", + KIcon::Desktop, KIcon::SizeLarge)); + } +} + +////////////////////////////////////////////////////////////////////////////// +// DeleteDialog implementation +////////////////////////////////////////////////////////////////////////////// + +DeleteDialog::DeleteDialog(QWidget *parent, const char *name) : + KDialogBase(Swallow, WStyle_DialogBorder, parent, name, + true /* modal */, i18n("About to delete selected files"), + Ok | Cancel, Cancel /* Default */, true /* separator */), + m_trashGuiItem(i18n("&Send to Trash"), "trashcan_full") +{ + m_widget = new DeleteWidget(this, "delete_dialog_widget"); + setMainWidget(m_widget); + + m_widget->setMinimumSize(400, 300); + setMinimumSize(410, 326); + adjustSize(); + + slotShouldDelete(shouldDelete()); + + connect(m_widget->ddShouldDelete, SIGNAL(toggled(bool)), SLOT(slotShouldDelete(bool))); +} + +bool DeleteDialog::confirmDeleteList(const QStringList &condemnedFiles) +{ + m_widget->setFiles(condemnedFiles); + + return exec() == QDialog::Accepted; +} + +void DeleteDialog::setFiles(const QStringList &files) +{ + m_widget->setFiles(files); +} + +void DeleteDialog::accept() +{ + KConfigGroup messageGroup(KGlobal::config(), "FileRemover"); + + // Save user's preference + + messageGroup.writeEntry("deleteInsteadOfTrash", shouldDelete()); + messageGroup.sync(); + + KDialogBase::accept(); +} + +void DeleteDialog::slotShouldDelete(bool shouldDelete) +{ + setButtonGuiItem(Ok, shouldDelete ? KStdGuiItem::del() : m_trashGuiItem); +} + +#include "deletedialog.moc" + +// vim: set et ts=4 sw=4: diff --git a/juk/deletedialog.h b/juk/deletedialog.h new file mode 100644 index 00000000..9fd45838 --- /dev/null +++ b/juk/deletedialog.h @@ -0,0 +1,64 @@ +/*************************************************************************** + begin : Tue Aug 31 21:54:20 EST 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef _DELETEDIALOG_H +#define _DELETEDIALOG_H + + +#include <qcheckbox.h> + +#include "deletedialogbase.h" + +class QStringList; +class KListBox; +class QLabel; +class QWidgetStack; + +class DeleteWidget : public DeleteDialogBase +{ + Q_OBJECT + +public: + DeleteWidget(QWidget *parent = 0, const char *name = 0); + + void setFiles(const QStringList &files); + +protected slots: + virtual void slotShouldDelete(bool shouldDelete); +}; + +class DeleteDialog : public KDialogBase +{ + Q_OBJECT + +public: + DeleteDialog(QWidget *parent, const char *name = "delete_dialog"); + + bool confirmDeleteList(const QStringList &condemnedFiles); + void setFiles(const QStringList &files); + bool shouldDelete() const { return m_widget->ddShouldDelete->isChecked(); } + +protected slots: + virtual void accept(); + void slotShouldDelete(bool shouldDelete); + +private: + DeleteWidget *m_widget; + KGuiItem m_trashGuiItem; +}; + +#endif + +// vim: set et ts=4 sw=4: diff --git a/juk/deletedialogbase.ui b/juk/deletedialogbase.ui new file mode 100644 index 00000000..f1054d79 --- /dev/null +++ b/juk/deletedialogbase.ui @@ -0,0 +1,143 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>DeleteDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>DeleteDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>542</width> + <height>374</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>420</width> + <height>320</height> + </size> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout4</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>ddWarningIcon</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>4</hsizetype> + <vsizetype>4</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Icon Placeholder, not in GUI</string> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>Are you sure that you want to remove these items?</string> + </property> + <property name="alignment"> + <set>AlignCenter</set> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>ddDeleteText</cstring> + </property> + <property name="text"> + <string>Deletion method placeholder, never shown to user.</string> + </property> + <property name="alignment"> + <set>WordBreak|AlignCenter</set> + </property> + </widget> + </vbox> + </widget> + </hbox> + </widget> + <widget class="KListBox"> + <property name="name"> + <cstring>ddFileList</cstring> + </property> + <property name="selectionMode"> + <enum>NoSelection</enum> + </property> + <property name="toolTip" stdset="0"> + <string>List of files that are about to be deleted.</string> + </property> + <property name="whatsThis" stdset="0"> + <string>This is the list of items that are about to be deleted.</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>ddNumFiles</cstring> + </property> + <property name="text"> + <string>Placeholder for number of files, not in GUI</string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + </widget> + <widget class="QCheckBox"> + <property name="name"> + <cstring>ddShouldDelete</cstring> + </property> + <property name="text"> + <string>&Delete files instead of moving them to the trash</string> + </property> + <property name="toolTip" stdset="0"> + <string>If checked, files will be permanently removed instead of being placed in the Trash Bin</string> + </property> + <property name="whatsThis" stdset="0"> + <string><qt><p>If this box is checked, files will be <b>permanently removed</b> instead of being placed in the Trash Bin.</p> + +<p><em>Use this option with caution</em>: Most filesystems are unable to reliably undelete deleted files.</p></qt></string> + </property> + </widget> + </vbox> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>ddShouldDelete</sender> + <signal>toggled(bool)</signal> + <receiver>DeleteDialogBase</receiver> + <slot>slotShouldDelete(bool)</slot> + </connection> +</connections> +<slots> + <slot access="protected">slotShouldDelete(bool)</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>klistbox.h</includehint> +</includehints> +</UI> diff --git a/juk/directorylist.cpp b/juk/directorylist.cpp new file mode 100644 index 00000000..1e53cac8 --- /dev/null +++ b/juk/directorylist.cpp @@ -0,0 +1,101 @@ +/*************************************************************************** + begin : Tue Feb 4 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kfiledialog.h> +#include <klocale.h> +#include <klistview.h> +#include <kpushbutton.h> + +#include <qcheckbox.h> + +#include "directorylistbase.h" +#include "directorylist.h" + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +DirectoryList::DirectoryList(const QStringList &directories, bool importPlaylists, + QWidget *parent, const char *name) : + KDialogBase(parent, name, true, i18n("Folder List"), Ok | Cancel, Ok, true), + m_dirList(directories), + m_importPlaylists(importPlaylists) +{ + m_base = new DirectoryListBase(this); + + setMainWidget(m_base); + + m_base->directoryListView->setFullWidth(true); + + connect(m_base->addDirectoryButton, SIGNAL(clicked()), + SLOT(slotAddDirectory())); + connect(m_base->removeDirectoryButton, SIGNAL(clicked()), + SLOT(slotRemoveDirectory())); + + QStringList::ConstIterator it = directories.begin(); + for(; it != directories.end(); ++it) + new KListViewItem(m_base->directoryListView, *it); + + m_base->importPlaylistsCheckBox->setChecked(importPlaylists); + + QSize sz = sizeHint(); + setMinimumSize(kMax(350, sz.width()), kMax(250, sz.height())); + resize(sizeHint()); +} + +DirectoryList::~DirectoryList() +{ + +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +DirectoryList::Result DirectoryList::exec() +{ + m_result.status = static_cast<DialogCode>(KDialogBase::exec()); + m_result.addPlaylists = m_base->importPlaylistsCheckBox->isChecked(); + return m_result; +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void DirectoryList::slotAddDirectory() +{ + QString dir = KFileDialog::getExistingDirectory(); + if(!dir.isEmpty() && m_dirList.find(dir) == m_dirList.end()) { + m_dirList.append(dir); + new KListViewItem(m_base->directoryListView, dir); + m_result.addedDirs.append(dir); + } +} + +void DirectoryList::slotRemoveDirectory() +{ + if(!m_base->directoryListView->selectedItem()) + return; + + QString dir = m_base->directoryListView->selectedItem()->text(0); + m_dirList.remove(dir); + m_result.removedDirs.append(dir); + delete m_base->directoryListView->selectedItem(); +} + +#include "directorylist.moc" + +// vim: ts=8 diff --git a/juk/directorylist.h b/juk/directorylist.h new file mode 100644 index 00000000..fef730a2 --- /dev/null +++ b/juk/directorylist.h @@ -0,0 +1,59 @@ +/*************************************************************************** + begin : Tue Feb 4 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef DIRECTORYLIST_H +#define DIRECTORYLIST_H + + +class DirectoryListBase; + +class DirectoryList : public KDialogBase +{ + Q_OBJECT + +public: + struct Result + { + QStringList addedDirs; + QStringList removedDirs; + DialogCode status; + bool addPlaylists; + }; + + DirectoryList(const QStringList &directories, bool importPlaylists, + QWidget *parent = 0, const char *name = 0); + virtual ~DirectoryList(); + +public slots: + Result exec(); + +signals: + void signalDirectoryAdded(const QString &directory); + void signalDirectoryRemoved(const QString &directory); + +private slots: + void slotAddDirectory(); + void slotRemoveDirectory(); + +private: + QStringList m_dirList; + bool m_importPlaylists; + DirectoryListBase *m_base; + Result m_result; +}; + +#endif + +// vim:ts=8 diff --git a/juk/directorylistbase.ui b/juk/directorylistbase.ui new file mode 100644 index 00000000..643417d3 --- /dev/null +++ b/juk/directorylistbase.ui @@ -0,0 +1,112 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>DirectoryListBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>DirectoryListBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>458</width> + <height>229</height> + </rect> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KListView"> + <column> + <property name="text"> + <string>Folders</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <property name="name"> + <cstring>directoryListView</cstring> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>rightColumnLayout</cstring> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KPushButton"> + <property name="name"> + <cstring>addDirectoryButton</cstring> + </property> + <property name="text"> + <string>Add Folder...</string> + </property> + </widget> + <widget class="KPushButton"> + <property name="name"> + <cstring>removeDirectoryButton</cstring> + </property> + <property name="text"> + <string>Remove Folder</string> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>informationLabel</cstring> + </property> + <property name="text"> + <string>These folders will be scanned on startup for new files.</string> + </property> + <property name="alignment"> + <set>WordBreak|AlignVCenter</set> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>rightColumnSpacer</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + <widget class="QCheckBox"> + <property name="name"> + <cstring>importPlaylistsCheckBox</cstring> + </property> + <property name="text"> + <string>Import playlists</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </vbox> + </widget> + </hbox> +</widget> +<includes> + <include location="global" impldecl="in implementation">kdialog.h</include> +</includes> +<layoutdefaults spacing="6" margin="0"/> +<layoutfunctions spacing="KDialog::spacingHint"/> +<includehints> + <includehint>klistview.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/juk/dynamicplaylist.cpp b/juk/dynamicplaylist.cpp new file mode 100644 index 00000000..69d0a4ae --- /dev/null +++ b/juk/dynamicplaylist.cpp @@ -0,0 +1,194 @@ +/*************************************************************************** + begin : Mon May 5 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kdebug.h> + +#include "dynamicplaylist.h" +#include "collectionlist.h" +#include "playlistcollection.h" +#include "tracksequencemanager.h" + +class PlaylistDirtyObserver : public PlaylistObserver +{ +public: + PlaylistDirtyObserver(DynamicPlaylist *parent, Playlist *playlist) : + PlaylistObserver(playlist), + m_parent(parent) + { + + } + virtual void updateData() { m_parent->slotSetDirty(); } + virtual void updateCurrent() {} + +private: + DynamicPlaylist *m_parent; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +DynamicPlaylist::DynamicPlaylist(const PlaylistList &playlists, + PlaylistCollection *collection, + const QString &name, + const QString &iconName, + bool setupPlaylist, + bool synchronizePlaying) : + Playlist(collection, true), + m_playlists(playlists), + m_dirty(true), + m_synchronizePlaying(synchronizePlaying) +{ + if(setupPlaylist) + collection->setupPlaylist(this, iconName); + setName(name); + + setSorting(columns() + 1); + + for(PlaylistList::ConstIterator it = playlists.begin(); it != playlists.end(); ++it) + m_observers.append(new PlaylistDirtyObserver(this, *it)); + + connect(CollectionList::instance(), SIGNAL(signalCollectionChanged()), this, SLOT(slotSetDirty())); +} + +DynamicPlaylist::~DynamicPlaylist() +{ + lower(); + + for(QValueList<PlaylistObserver *>::ConstIterator it = m_observers.begin(); + it != m_observers.end(); + ++it) + { + delete *it; + } +} + +void DynamicPlaylist::setPlaylists(const PlaylistList &playlists) +{ + m_playlists = playlists; + updateItems(); +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void DynamicPlaylist::slotReload() +{ + for(PlaylistList::Iterator it = m_playlists.begin(); it != m_playlists.end(); ++it) + (*it)->slotReload(); + + checkUpdateItems(); +} + +void DynamicPlaylist::lower(QWidget *top) +{ + if(top == this) + return; + + if(playing()) { + PlaylistList l; + l.append(this); + for(PlaylistList::Iterator it = m_playlists.begin(); + it != m_playlists.end(); ++it) + { + (*it)->synchronizePlayingItems(l, true); + } + } + + PlaylistItemList list = PlaylistItem::playingItems(); + for(PlaylistItemList::Iterator it = list.begin(); it != list.end(); ++it) { + if((*it)->playlist() == this) { + list.remove(it); + break; + } + } + + if(!list.isEmpty()) + TrackSequenceManager::instance()->setCurrentPlaylist(list.front()->playlist()); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistItemList DynamicPlaylist::items() +{ + checkUpdateItems(); + return Playlist::items(); +} + +void DynamicPlaylist::showEvent(QShowEvent *e) +{ + checkUpdateItems(); + Playlist::showEvent(e); +} + +void DynamicPlaylist::paintEvent(QPaintEvent *e) +{ + checkUpdateItems(); + Playlist::paintEvent(e); +} + +void DynamicPlaylist::updateItems() +{ + PlaylistItemList siblings; + + for(PlaylistList::ConstIterator it = m_playlists.begin(); it != m_playlists.end(); ++it) + siblings += (*it)->items(); + + + PlaylistItemList newSiblings = siblings; + if(m_siblings != newSiblings) { + m_siblings = newSiblings; + QTimer::singleShot(0, this, SLOT(slotUpdateItems())); + } +} + +bool DynamicPlaylist::synchronizePlaying() const +{ + return m_synchronizePlaying; +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void DynamicPlaylist::checkUpdateItems() +{ + if(!m_dirty) + return; + + updateItems(); + + m_dirty = false; +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void DynamicPlaylist::slotUpdateItems() +{ + // This should be optimized to check to see which items are already in the + // list and just adding those and removing the ones that aren't. + + clear(); + createItems(m_siblings); + if(m_synchronizePlaying) + synchronizePlayingItems(m_playlists, true); +} + +#include "dynamicplaylist.moc" diff --git a/juk/dynamicplaylist.h b/juk/dynamicplaylist.h new file mode 100644 index 00000000..3e6e2c4b --- /dev/null +++ b/juk/dynamicplaylist.h @@ -0,0 +1,110 @@ +/*************************************************************************** + begin : Mon May 5 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef DYNAMICPLAYLIST_H +#define DYNAMICPLAYLIST_H + +#include "playlist.h" + +/** + * A Playlist that is a union of other playlists that is created dynamically. + */ + +class DynamicPlaylist : public Playlist +{ + Q_OBJECT +public: + /** + * Creates a dynamic playlist based on lists. + */ + DynamicPlaylist(const PlaylistList &lists, + PlaylistCollection *collection, + const QString &name = QString::null, + const QString &iconName = "midi", + bool setupPlaylist = true, + bool synchronizePlaying = false); + + virtual ~DynamicPlaylist(); + + virtual bool canReload() const { return false; } + + void setPlaylists(const PlaylistList &playlists); + +public slots: + /** + * Reimplemented so that it will reload all of the playlists that are + * associated with the dynamic list. + */ + virtual void slotReload(); + void slotSetDirty() { m_dirty = true; } + + /** + * This is called when lowering the widget from the widget stack so that + * it can synchronize the playing item with the one that playlist it was + * create from. + */ + void lower(QWidget *top = 0); + +protected: + /** + * Returns true if this list's items need to be updated the next time it's + * shown. + */ + bool dirty() const { return m_dirty; } + + /** + * Return a list of the items in this playlist. For example in a search + * list this should return only the matched items. By default it returns + * all of the items in the playlists associated with this dynamic list. + */ + virtual PlaylistItemList items(); + + /** + * Reimplemented from QWidget. Here it updates the list of items (when + * appropriate) as the widget is shown. + */ + virtual void showEvent(QShowEvent *e); + + virtual void paintEvent(QPaintEvent *e); + + /** + * Updates the items (unconditionally). This should be reimplemented in + * subclasses to refresh the items in the dynamic list (i.e. running a + * search). + */ + virtual void updateItems(); + + bool synchronizePlaying() const; + +private: + /** + * Checks to see if the current list of items is "dirty" and if so updates + * this dynamic playlist's items to be in sync with the lists that it is a + * wrapper around. + */ + void checkUpdateItems(); + +private slots: + void slotUpdateItems(); + +private: + QValueList<PlaylistObserver *> m_observers; + PlaylistItemList m_siblings; + PlaylistList m_playlists; + bool m_dirty; + bool m_synchronizePlaying; +}; + +#endif diff --git a/juk/exampleoptions.cpp b/juk/exampleoptions.cpp new file mode 100644 index 00000000..68e2bc0f --- /dev/null +++ b/juk/exampleoptions.cpp @@ -0,0 +1,83 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kurlrequester.h> +#include <klocale.h> + +#include <qradiobutton.h> +#include <qlayout.h> + +#include "exampleoptions.h" + +ExampleOptions::ExampleOptions(QWidget *parent) : + ExampleOptionsBase(parent, "example options widget") +{ +} + +void ExampleOptions::exampleSelectionChanged() +{ + if(m_fileTagsButton->isChecked()) + emit fileChanged(); + else + emit dataChanged(); +} + +void ExampleOptions::exampleDataChanged() +{ + emit dataChanged(); +} + +void ExampleOptions::exampleFileChanged() +{ + emit fileChanged(); +} + +ExampleOptionsDialog::ExampleOptionsDialog(QWidget *parent) : + QDialog(parent, "example options dialog") +{ + setCaption(i18n("JuK")); + QVBoxLayout *l = new QVBoxLayout(this); + + m_options = new ExampleOptions(this); + l->addWidget(m_options); + + // Forward signals + + connect(m_options, SIGNAL(fileChanged()), SLOT(fileModeSelected())); + connect(m_options, SIGNAL(dataChanged()), SIGNAL(dataChanged())); + connect(m_options->m_exampleFile, SIGNAL(urlSelected(const QString &)), + this, SIGNAL(fileChanged(const QString &))); + connect(m_options->m_exampleFile, SIGNAL(returnPressed(const QString &)), + this, SIGNAL(fileChanged(const QString &))); +} + +void ExampleOptionsDialog::hideEvent(QHideEvent *) +{ + emit signalHidden(); +} + +void ExampleOptionsDialog::showEvent(QShowEvent *) +{ + emit signalShown(); +} + +void ExampleOptionsDialog::fileModeSelected() +{ + emit fileChanged(m_options->m_exampleFile->url()); +} + +#include "exampleoptions.moc" + +// vim: set et sw=4 ts=4: diff --git a/juk/exampleoptions.h b/juk/exampleoptions.h new file mode 100644 index 00000000..26ee23c1 --- /dev/null +++ b/juk/exampleoptions.h @@ -0,0 +1,63 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_EXAMPLEOPTIONS_H +#define JUK_EXAMPLEOPTIONS_H + +#include <qdialog.h> +#include "exampleoptionsbase.h" + +class ExampleOptions : public ExampleOptionsBase +{ + Q_OBJECT + public: + ExampleOptions(QWidget *parent); + + protected slots: + virtual void exampleSelectionChanged(); + virtual void exampleDataChanged(); + virtual void exampleFileChanged(); +}; + +// We're not using KDialog(Base) because this dialog won't have any push +// buttons to close it. It's just a little floating dialog. +class ExampleOptionsDialog : public QDialog +{ + Q_OBJECT + public: + ExampleOptionsDialog(QWidget *parent); + + const ExampleOptions *widget() const { return m_options; } + + protected: + virtual void hideEvent(QHideEvent *); + virtual void showEvent(QShowEvent *); + + protected slots: + void fileModeSelected(); + + signals: + void fileChanged(const QString &); + void dataChanged(); + void signalHidden(); + void signalShown(); + + private: + ExampleOptions *m_options; +}; + +#endif /* JUK_EXAMPLEOPTIONS_H */ + +// vim: set et sw=4 ts=4: diff --git a/juk/exampleoptionsbase.ui b/juk/exampleoptionsbase.ui new file mode 100644 index 00000000..a2280ef4 --- /dev/null +++ b/juk/exampleoptionsbase.ui @@ -0,0 +1,285 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>ExampleOptionsBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>ExampleOptionsBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>308</width> + <height>334</height> + </rect> + </property> + <property name="caption"> + <string>Example</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QButtonGroup"> + <property name="name"> + <cstring>buttonGroup3</cstring> + </property> + <property name="title"> + <string>Example Tag Selection</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QRadioButton"> + <property name="name"> + <cstring>m_fileTagsButton</cstring> + </property> + <property name="text"> + <string>Get example tags from this file:</string> + </property> + <property name="checked"> + <bool>false</bool> + </property> + </widget> + <widget class="KURLRequester"> + <property name="name"> + <cstring>m_exampleFile</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="mode"> + <number>24</number> + </property> + </widget> + <widget class="QRadioButton"> + <property name="name"> + <cstring>m_manualTagsButton</cstring> + </property> + <property name="text"> + <string>Enter example tags manually:</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + <widget class="QGroupBox"> + <property name="name"> + <cstring>m_manualGroup</cstring> + </property> + <property name="title"> + <string>Example Tags</string> + </property> + <property name="flat"> + <bool>true</bool> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLineEdit" row="1" column="1"> + <property name="name"> + <cstring>m_exampleArtist</cstring> + </property> + <property name="text"> + <string>Artist</string> + </property> + </widget> + <widget class="QLineEdit" row="0" column="1"> + <property name="name"> + <cstring>m_exampleTitle</cstring> + </property> + <property name="text"> + <string>Title</string> + </property> + </widget> + <widget class="QLineEdit" row="2" column="1"> + <property name="name"> + <cstring>m_exampleAlbum</cstring> + </property> + <property name="text"> + <string>Album</string> + </property> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>Title:</string> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>textLabel2</cstring> + </property> + <property name="text"> + <string>Artist:</string> + </property> + </widget> + <widget class="QSpinBox" row="5" column="1"> + <property name="name"> + <cstring>m_exampleYear</cstring> + </property> + <property name="maxValue"> + <number>2006</number> + </property> + <property name="minValue"> + <number>1901</number> + </property> + <property name="value"> + <number>2004</number> + </property> + </widget> + <widget class="QLabel" row="2" column="0"> + <property name="name"> + <cstring>textLabel3</cstring> + </property> + <property name="text"> + <string>Album:</string> + </property> + </widget> + <widget class="QLabel" row="3" column="0"> + <property name="name"> + <cstring>textLabel4</cstring> + </property> + <property name="text"> + <string>Genre:</string> + </property> + </widget> + <widget class="QLabel" row="4" column="0"> + <property name="name"> + <cstring>textLabel5</cstring> + </property> + <property name="text"> + <string>Track number:</string> + </property> + </widget> + <widget class="QSpinBox" row="4" column="1"> + <property name="name"> + <cstring>m_exampleTrack</cstring> + </property> + <property name="value"> + <number>1</number> + </property> + </widget> + <widget class="QLineEdit" row="3" column="1"> + <property name="name"> + <cstring>m_exampleGenre</cstring> + </property> + <property name="text"> + <string>Genre</string> + </property> + </widget> + <widget class="QLabel" row="5" column="0"> + <property name="name"> + <cstring>textLabel6</cstring> + </property> + <property name="text"> + <string>Year:</string> + </property> + </widget> + </grid> + </widget> + </vbox> + </widget> + </vbox> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>m_manualTagsButton</sender> + <signal>toggled(bool)</signal> + <receiver>m_manualGroup</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>m_fileTagsButton</sender> + <signal>toggled(bool)</signal> + <receiver>m_exampleFile</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>m_fileTagsButton</sender> + <signal>stateChanged(int)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleSelectionChanged()</slot> + </connection> + <connection> + <sender>m_manualTagsButton</sender> + <signal>stateChanged(int)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleSelectionChanged()</slot> + </connection> + <connection> + <sender>m_exampleTitle</sender> + <signal>textChanged(const QString&)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleDataChanged()</slot> + </connection> + <connection> + <sender>m_exampleArtist</sender> + <signal>textChanged(const QString&)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleDataChanged()</slot> + </connection> + <connection> + <sender>m_exampleAlbum</sender> + <signal>textChanged(const QString&)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleDataChanged()</slot> + </connection> + <connection> + <sender>m_exampleGenre</sender> + <signal>textChanged(const QString&)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleDataChanged()</slot> + </connection> + <connection> + <sender>m_exampleTrack</sender> + <signal>valueChanged(int)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleDataChanged()</slot> + </connection> + <connection> + <sender>m_exampleYear</sender> + <signal>valueChanged(int)</signal> + <receiver>ExampleOptionsBase</receiver> + <slot>exampleDataChanged()</slot> + </connection> +</connections> +<tabstops> + <tabstop>m_exampleFile</tabstop> + <tabstop>m_manualTagsButton</tabstop> + <tabstop>m_exampleTitle</tabstop> + <tabstop>m_exampleArtist</tabstop> + <tabstop>m_exampleAlbum</tabstop> + <tabstop>m_exampleGenre</tabstop> + <tabstop>m_exampleTrack</tabstop> + <tabstop>m_exampleYear</tabstop> +</tabstops> +<signals> + <signal>dataChanged()</signal> + <signal>fileChanged()</signal> +</signals> +<slots> + <slot access="protected">exampleSelectionChanged()</slot> + <slot access="protected">exampleDataChanged()</slot> + <slot access="protected">exampleFileChanged()</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/juk/filehandle.cpp b/juk/filehandle.cpp new file mode 100644 index 00000000..583cfc0d --- /dev/null +++ b/juk/filehandle.cpp @@ -0,0 +1,298 @@ +/*************************************************************************** + begin : Sun Feb 29 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <limits.h> +#include <stdlib.h> + +#include <kdebug.h> + +#include <qfileinfo.h> + +#include "filehandle.h" +#include "filehandleproperties.h" +#include "tag.h" +#include "cache.h" +#include "coverinfo.h" + +AddProperty(Title, tag()->title()) +AddProperty(Artist, tag()->artist()) +AddProperty(Album, tag()->album()) +AddProperty(Genre, tag()->genre()) +AddNumberProperty(Track, tag()->track()) +AddNumberProperty(Year, tag()->year()) +AddProperty(Comment, tag()->comment()) +AddNumberProperty(Seconds, tag()->seconds()) +AddNumberProperty(Bitrate, tag()->bitrate()) +AddProperty(Path, absFilePath()) +AddNumberProperty(Size, fileInfo().size()) +AddProperty(Extension, fileInfo().extension(false)) + +static QString resolveSymLinks(const QFileInfo &file) // static +{ + char real[PATH_MAX]; + + if(file.exists() && realpath(QFile::encodeName(file.absFilePath()).data(), real)) + return QFile::decodeName(real); + else + return file.filePath(); +} + +/** + * A simple reference counter -- pasted from TagLib. + */ + +class RefCounter +{ +public: + RefCounter() : refCount(1) {} + void ref() { refCount++; } + bool deref() { return ! --refCount ; } + int count() const { return refCount; } +private: + uint refCount; +}; + +class FileHandle::FileHandlePrivate : public RefCounter +{ +public: + FileHandlePrivate() : + tag(0), + coverInfo(0) {} + + ~FileHandlePrivate() + { + delete tag; + delete coverInfo; + } + + mutable Tag *tag; + mutable CoverInfo *coverInfo; + mutable QString absFilePath; + QFileInfo fileInfo; + QDateTime modificationTime; + QDateTime lastModified; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +FileHandle::FileHandle() +{ + static FileHandlePrivate nullPrivate; + d = &nullPrivate; + d->ref(); +} + +FileHandle::FileHandle(const FileHandle &f) : + d(f.d) +{ + if(!d) { + kdDebug(65432) << "The source FileHandle was not initialized." << endl; + d = null().d; + } + d->ref(); +} + +FileHandle::FileHandle(const QFileInfo &info, const QString &path) : + d(0) +{ + setup(info, path); +} + +FileHandle::FileHandle(const QString &path) : + d(0) +{ + setup(QFileInfo(path), path); +} + +FileHandle::FileHandle(const QString &path, CacheDataStream &s) +{ + d = new FileHandlePrivate; + d->fileInfo = QFileInfo(path); + d->absFilePath = path; + read(s); + Cache::instance()->insert(*this); +} + +FileHandle::~FileHandle() +{ + if(d->deref()) + delete d; +} + +void FileHandle::refresh() +{ + d->fileInfo.refresh(); + delete d->tag; + d->tag = new Tag(d->absFilePath); +} + +void FileHandle::setFile(const QString &path) +{ + if(!d || isNull()) + setup(QFileInfo(path), path); + else { + d->absFilePath = resolveSymLinks(path); + d->fileInfo.setFile(path); + d->tag->setFileName(d->absFilePath); + } +} + +Tag *FileHandle::tag() const +{ + if(!d->tag) + d->tag = new Tag(d->absFilePath); + + return d->tag; +} + +CoverInfo *FileHandle::coverInfo() const +{ + if(!d->coverInfo) + d->coverInfo = new CoverInfo(*this); + + return d->coverInfo; +} + +QString FileHandle::absFilePath() const +{ + if(d->absFilePath.isNull()) + d->absFilePath = resolveSymLinks(d->fileInfo.absFilePath()); + return d->absFilePath; +} + +const QFileInfo &FileHandle::fileInfo() const +{ + return d->fileInfo; +} + +bool FileHandle::isNull() const +{ + return *this == null(); +} + +bool FileHandle::current() const +{ + return (d->modificationTime.isValid() && + lastModified().isValid() && + d->modificationTime >= lastModified()); +} + +const QDateTime &FileHandle::lastModified() const +{ + if(d->lastModified.isNull()) + d->lastModified = d->fileInfo.lastModified(); + + return d->lastModified; +} + +void FileHandle::read(CacheDataStream &s) +{ + switch(s.cacheVersion()) { + case 1: + default: + if(!d->tag) + d->tag = new Tag(d->absFilePath, true); + + s >> *(d->tag); + s >> d->modificationTime; + break; + } +} + +FileHandle &FileHandle::operator=(const FileHandle &f) +{ + if(&f == this) + return *this; + + if(d->deref()) + delete d; + + d = f.d; + d->ref(); + + return *this; +} + +bool FileHandle::operator==(const FileHandle &f) const +{ + return d == f.d; +} + +bool FileHandle::operator!=(const FileHandle &f) const +{ + return d != f.d; +} + +QStringList FileHandle::properties() // static +{ + return FileHandleProperties::properties(); +} + +QString FileHandle::property(const QString &name) const +{ + return FileHandleProperties::property(*this, name.latin1()); +} + +const FileHandle &FileHandle::null() // static +{ + static FileHandle f; + return f; +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void FileHandle::setup(const QFileInfo &info, const QString &path) +{ + if(d && !isNull()) + return; + + QString fileName = path.isNull() ? info.absFilePath() : path; + + FileHandle cached = Cache::instance()->value(resolveSymLinks(fileName)); + + if(cached != null()) { + d = cached.d; + d->ref(); + } + else { + d = new FileHandlePrivate; + d->fileInfo = info; + d->absFilePath = resolveSymLinks(fileName); + d->modificationTime = info.lastModified(); + Cache::instance()->insert(*this); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// related functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const FileHandle &f) +{ + s << *(f.tag()) + << f.lastModified(); + + return s; +} + +CacheDataStream &operator>>(CacheDataStream &s, FileHandle &f) +{ + f.read(s); + return s; +} diff --git a/juk/filehandle.h b/juk/filehandle.h new file mode 100644 index 00000000..6c12a344 --- /dev/null +++ b/juk/filehandle.h @@ -0,0 +1,82 @@ +/*************************************************************************** + begin : Sun Feb 29 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef JUK_FILEHANDLE_H +#define JUK_FILEHANDLE_H + +#include <qstringlist.h> + +class QFileInfo; +class QDateTime; +class QDataStream; +class CoverInfo; + +class Tag; +class CacheDataStream; + +/** + * An value based, explicitly shared wrapper around file related information + * used in JuK's playlists. + */ + +class FileHandle +{ +public: + FileHandle(); + FileHandle(const FileHandle &f); + explicit FileHandle(const QFileInfo &info, const QString &path = QString::null); + explicit FileHandle(const QString &path); + FileHandle(const QString &path, CacheDataStream &s); + ~FileHandle(); + + /** + * Forces the FileHandle to reread its information from the disk. + */ + void refresh(); + void setFile(const QString &path); + + Tag *tag() const; + CoverInfo *coverInfo() const; + QString absFilePath() const; + const QFileInfo &fileInfo() const; + + bool isNull() const; + bool current() const; + const QDateTime &lastModified() const; + + void read(CacheDataStream &s); + + FileHandle &operator=(const FileHandle &f); + bool operator==(const FileHandle &f) const; + bool operator!=(const FileHandle &f) const; + + static QStringList properties(); + QString property(const QString &name) const; + + static const FileHandle &null(); + +private: + class FileHandlePrivate; + FileHandlePrivate *d; + + void setup(const QFileInfo &info, const QString &path); +}; + +typedef QValueList<FileHandle> FileHandleList; + +QDataStream &operator<<(QDataStream &s, const FileHandle &f); +CacheDataStream &operator>>(CacheDataStream &s, FileHandle &f); + +#endif diff --git a/juk/filehandleproperties.h b/juk/filehandleproperties.h new file mode 100644 index 00000000..b0b708bc --- /dev/null +++ b/juk/filehandleproperties.h @@ -0,0 +1,94 @@ +/*************************************************************************** + Copyright (C) 2004 by 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. * + * * + ***************************************************************************/ + +#ifndef FILEHANDLEPROPERTIES_H +#define FILEHANDLEPROPERTIES_H + +#include <qmap.h> + +/* + * These ugly macros make possible a property registration system that makes it + * easy to add properties to the FileHandle that can be accessed via the DCOP + * interface. + * + * Properties should actually be added to the filehandle.cpp file. This file + * is just here to separate out some of the macro-related ugliness. + */ + +#define AddProperty(name, method) \ + namespace FileHandleProperties \ + { \ + struct name##Property : public Property \ + { \ + virtual QString value(const FileHandle &f) const \ + { \ + return f.method; \ + } \ + static const int dummy; \ + }; \ + static name##Property name##Instance; \ + const int name##Property::dummy = addToPropertyMap(#name, &name##Instance); \ + } + +#define AddNumberProperty(name, method) \ + namespace FileHandleProperties \ + { \ + struct name##Property : public Property \ + { \ + virtual QString value(const FileHandle &f) const \ + { \ + return QString::number(f.method); \ + } \ + static const int dummy; \ + }; \ + static name##Property name##Instance; \ + const int name##Property::dummy = addToPropertyMap(#name, &name##Instance); \ + } + +namespace FileHandleProperties +{ + struct Property + { + virtual QString value(const FileHandle &) const + { + return QString::null; + } + }; + + static QMap<QCString, const Property *> propertyMap; + + static int addToPropertyMap(const QCString &name, Property *property) + { + propertyMap[name] = property; + return 0; + } + + static QString property(const FileHandle &file, const QCString &key) + { + return propertyMap.contains(key) ? propertyMap[key]->value(file) : QString::null; + } + + static QStringList properties() + { + static QStringList l; + + if(l.isEmpty()) { + QMap<QCString, const Property *>::ConstIterator it = propertyMap.begin(); + for(; it != propertyMap.end(); ++it) + l.append(QString(it.key())); + } + return l; + } +} + +#endif diff --git a/juk/filerenamer.cpp b/juk/filerenamer.cpp new file mode 100644 index 00000000..ec45a268 --- /dev/null +++ b/juk/filerenamer.cpp @@ -0,0 +1,1047 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + : (c) 2003 Frerich Raabe <raabe@kde.org> + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <algorithm> + +#include <kdebug.h> +#include <kcombobox.h> +#include <kurl.h> +#include <kurlrequester.h> +#include <kiconloader.h> +#include <knuminput.h> +#include <kstandarddirs.h> +#include <kio/netaccess.h> +#include <kconfigbase.h> +#include <kconfig.h> +#include <kglobal.h> +#include <klineedit.h> +#include <klocale.h> +#include <kpushbutton.h> +#include <kapplication.h> +#include <kmessagebox.h> +#include <ksimpleconfig.h> + +#include <qfile.h> +#include <qhbox.h> +#include <qvbox.h> +#include <qscrollview.h> +#include <qobjectlist.h> +#include <qtimer.h> +#include <qregexp.h> +#include <qcheckbox.h> +#include <qdir.h> +#include <qlabel.h> +#include <qlayout.h> +#include <qsignalmapper.h> +#include <qheader.h> + +#include "tag.h" +#include "filehandle.h" +#include "filerenamer.h" +#include "exampleoptions.h" +#include "playlistitem.h" +#include "playlist.h" +#include "coverinfo.h" + +class ConfirmationDialog : public KDialogBase +{ +public: + ConfirmationDialog(const QMap<QString, QString> &files, + QWidget *parent = 0, const char *name = 0) + : KDialogBase(parent, name, true, i18n("Warning"), Ok | Cancel) + { + QVBox *vbox = makeVBoxMainWidget(); + QHBox *hbox = new QHBox(vbox); + + QLabel *l = new QLabel(hbox); + l->setPixmap(SmallIcon("messagebox_warning", 32)); + + l = new QLabel(i18n("You are about to rename the following files. " + "Are you sure you want to continue?"), hbox); + hbox->setStretchFactor(l, 1); + + KListView *lv = new KListView(vbox); + + lv->addColumn(i18n("Original Name")); + lv->addColumn(i18n("New Name")); + + int lvHeight = 0; + + QMap<QString, QString>::ConstIterator it = files.begin(); + for(; it != files.end(); ++it) { + KListViewItem *i = it.key() != it.data() + ? new KListViewItem(lv, it.key(), it.data()) + : new KListViewItem(lv, it.key(), i18n("No Change")); + lvHeight += i->height(); + } + + lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height(); + lv->setMinimumHeight(QMIN(lvHeight, 400)); + resize(QMIN(width(), 500), QMIN(minimumHeight(), 400)); + } +}; + +// +// Implementation of ConfigCategoryReader +// + +ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(), + m_currentItem(0) +{ + KConfigGroup config(KGlobal::config(), "FileRenamer"); + + QValueList<int> categoryOrder = config.readIntListEntry("CategoryOrder"); + unsigned categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered. + + // Set a default: + + if(categoryOrder.isEmpty()) + categoryOrder << Artist << Album << Title << Track; + + QValueList<int>::ConstIterator catIt = categoryOrder.constBegin(); + for(; catIt != categoryOrder.constEnd(); ++catIt) + { + unsigned catCount = categoryCount[*catIt]++; + TagType category = static_cast<TagType>(*catIt); + CategoryID catId(category, catCount); + + m_options[catId] = TagRenamerOptions(catId); + m_categoryOrder << catId; + } + + m_folderSeparators.resize(m_categoryOrder.count() - 1, false); + + QValueList<int> checkedSeparators = config.readIntListEntry("CheckedDirSeparators"); + + QValueList<int>::ConstIterator it = checkedSeparators.constBegin(); + for(; it != checkedSeparators.constEnd(); ++it) { + unsigned index = static_cast<unsigned>(*it); + if(index < m_folderSeparators.count()) + m_folderSeparators[index] = true; + } + + m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music"); + m_separator = config.readEntry("Separator", " - "); +} + +QString ConfigCategoryReader::categoryValue(TagType type) const +{ + if(!m_currentItem) + return QString::null; + + Tag *tag = m_currentItem->file().tag(); + + switch(type) { + case Track: + return QString::number(tag->track()); + + case Year: + return QString::number(tag->year()); + + case Title: + return tag->title(); + + case Artist: + return tag->artist(); + + case Album: + return tag->album(); + + case Genre: + return tag->genre(); + + default: + return QString::null; + } +} + +QString ConfigCategoryReader::prefix(const CategoryID &category) const +{ + return m_options[category].prefix(); +} + +QString ConfigCategoryReader::suffix(const CategoryID &category) const +{ + return m_options[category].suffix(); +} + +TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const +{ + return m_options[category].emptyAction(); +} + +QString ConfigCategoryReader::emptyText(const CategoryID &category) const +{ + return m_options[category].emptyText(); +} + +QValueList<CategoryID> ConfigCategoryReader::categoryOrder() const +{ + return m_categoryOrder; +} + +QString ConfigCategoryReader::separator() const +{ + return m_separator; +} + +QString ConfigCategoryReader::musicFolder() const +{ + return m_musicFolder; +} + +int ConfigCategoryReader::trackWidth(unsigned categoryNum) const +{ + return m_options[CategoryID(Track, categoryNum)].trackWidth(); +} + +bool ConfigCategoryReader::hasFolderSeparator(unsigned index) const +{ + if(index >= m_folderSeparators.count()) + return false; + return m_folderSeparators[index]; +} + +bool ConfigCategoryReader::isDisabled(const CategoryID &category) const +{ + return m_options[category].disabled(); +} + +// +// Implementation of FileRenamerWidget +// + +FileRenamerWidget::FileRenamerWidget(QWidget *parent) : + FileRenamerBase(parent), CategoryReaderInterface(), + m_exampleFromFile(false) +{ + QLabel *temp = new QLabel(0); + m_exampleText->setPaletteBackgroundColor(temp->paletteBackgroundColor()); + delete temp; + + layout()->setMargin(0); // We'll be wrapped by KDialogBase + + // This must be created before createTagRows() is called. + + m_exampleDialog = new ExampleOptionsDialog(this); + + createTagRows(); + loadConfig(); + + // Add correct text to combo box. + m_category->clear(); + for(unsigned i = StartTag; i < NumTypes; ++i) { + QString category = TagRenamerOptions::tagTypeText(static_cast<TagType>(i)); + m_category->insertItem(category); + } + + connect(m_exampleDialog, SIGNAL(signalShown()), SLOT(exampleDialogShown())); + connect(m_exampleDialog, SIGNAL(signalHidden()), SLOT(exampleDialogHidden())); + connect(m_exampleDialog, SIGNAL(dataChanged()), SLOT(dataSelected())); + connect(m_exampleDialog, SIGNAL(fileChanged(const QString &)), + this, SLOT(fileSelected(const QString &))); + + exampleTextChanged(); +} + +void FileRenamerWidget::loadConfig() +{ + QValueList<int> checkedSeparators; + KConfigGroup config(KGlobal::config(), "FileRenamer"); + + for(unsigned i = 0; i < m_rows.count(); ++i) + m_rows[i].options = TagRenamerOptions(m_rows[i].category); + + checkedSeparators = config.readIntListEntry("CheckedDirSeparators"); + + QValueList<int>::ConstIterator it = checkedSeparators.begin(); + for(; it != checkedSeparators.end(); ++it) { + unsigned separator = static_cast<unsigned>(*it); + if(separator < m_folderSwitches.count()) + m_folderSwitches[separator]->setChecked(true); + } + + QString url = config.readPathEntry("MusicFolder", "${HOME}/music"); + m_musicFolder->setURL(url); + + m_separator->setCurrentText(config.readEntry("Separator", " - ")); +} + +void FileRenamerWidget::saveConfig() +{ + KConfigGroup config(KGlobal::config(), "FileRenamer"); + QValueList<int> checkedSeparators; + QValueList<int> categoryOrder; + + for(unsigned i = 0; i < m_rows.count(); ++i) { + unsigned rowId = idOfPosition(i); // Write out in GUI order, not m_rows order + m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber); + categoryOrder += m_rows[rowId].category.category; + } + + for(unsigned i = 0; i < m_folderSwitches.count(); ++i) + if(m_folderSwitches[i]->isChecked() == true) + checkedSeparators += i; + + config.writeEntry("CheckedDirSeparators", checkedSeparators); + config.writeEntry("CategoryOrder", categoryOrder); + config.writePathEntry("MusicFolder", m_musicFolder->url()); + config.writeEntry("Separator", m_separator->currentText()); + + config.sync(); +} + +FileRenamerWidget::~FileRenamerWidget() +{ +} + +unsigned FileRenamerWidget::addRowCategory(TagType category) +{ + static QPixmap up = SmallIcon("up"); + static QPixmap down = SmallIcon("down"); + + // Find number of categories already of this type. + unsigned categoryCount = 0; + for(unsigned i = 0; i < m_rows.count(); ++i) + if(m_rows[i].category.category == category) + ++categoryCount; + + Row row; + + row.category = CategoryID(category, categoryCount); + row.position = m_rows.count(); + unsigned id = row.position; + + QHBox *frame = new QHBox(m_mainFrame); + frame->setPaletteBackgroundColor(frame->paletteBackgroundColor().dark(110)); + + row.widget = frame; + frame->setFrameShape(QFrame::Box); + frame->setLineWidth(1); + frame->setMargin(3); + + m_mainFrame->setStretchFactor(frame, 1); + + QVBox *buttons = new QVBox(frame); + buttons->setFrameStyle(QFrame::Plain | QFrame::Box); + buttons->setLineWidth(1); + + row.upButton = new KPushButton(buttons); + row.downButton = new KPushButton(buttons); + + row.upButton->setPixmap(up); + row.downButton->setPixmap(down); + row.upButton->setFlat(true); + row.downButton->setFlat(true); + + upMapper->connect(row.upButton, SIGNAL(clicked()), SLOT(map())); + upMapper->setMapping(row.upButton, id); + downMapper->connect(row.downButton, SIGNAL(clicked()), SLOT(map())); + downMapper->setMapping(row.downButton, id); + + QString labelText = QString("<b>%1</b>").arg(TagRenamerOptions::tagTypeText(category)); + QLabel *label = new QLabel(labelText, frame); + frame->setStretchFactor(label, 1); + label->setAlignment(AlignCenter); + + QVBox *options = new QVBox(frame); + row.enableButton = new KPushButton(i18n("Remove"), options); + toggleMapper->connect(row.enableButton, SIGNAL(clicked()), SLOT(map())); + toggleMapper->setMapping(row.enableButton, id); + + row.optionsButton = new KPushButton(i18n("Options"), options); + mapper->connect(row.optionsButton, SIGNAL(clicked()), SLOT(map())); + mapper->setMapping(row.optionsButton, id); + + row.widget->show(); + m_rows.append(row); + + // Disable add button if there's too many rows. + if(m_rows.count() == MAX_CATEGORIES) + m_insertCategory->setEnabled(false); + + return id; +} + +void FileRenamerWidget::moveSignalMappings(unsigned oldId, unsigned newId) +{ + mapper->setMapping(m_rows[oldId].optionsButton, newId); + downMapper->setMapping(m_rows[oldId].downButton, newId); + upMapper->setMapping(m_rows[oldId].upButton, newId); + toggleMapper->setMapping(m_rows[oldId].enableButton, newId); +} + +bool FileRenamerWidget::removeRow(unsigned id) +{ + if(id >= m_rows.count()) { + kdWarning(65432) << "Trying to remove row, but " << id << " is out-of-range.\n"; + return false; + } + + if(m_rows.count() == 1) { + kdError(65432) << "Can't remove last row of File Renamer.\n"; + return false; + } + + // Remove widget. Don't delete it since it appears QSignalMapper may still need it. + m_rows[id].widget->deleteLater(); + m_rows[id].widget = 0; + m_rows[id].enableButton = 0; + m_rows[id].upButton = 0; + m_rows[id].optionsButton = 0; + m_rows[id].downButton = 0; + + unsigned checkboxPosition = 0; // Remove first checkbox. + + // If not the first row, remove the checkbox before it. + if(m_rows[id].position > 0) + checkboxPosition = m_rows[id].position - 1; + + // The checkbox is contained within a layout widget, so the layout + // widget is the one the needs to die. + delete m_folderSwitches[checkboxPosition]->parent(); + m_folderSwitches.erase(&m_folderSwitches[checkboxPosition]); + + // Go through all the rows and if they have the same category and a + // higher categoryNumber, decrement the number. Also update the + // position identifier. + for(unsigned i = 0; i < m_rows.count(); ++i) { + if(i == id) + continue; // Don't mess with ourself. + + if((m_rows[id].category.category == m_rows[i].category.category) && + (m_rows[id].category.categoryNumber < m_rows[i].category.categoryNumber)) + { + --m_rows[i].category.categoryNumber; + } + + // Items are moving up. + if(m_rows[id].position < m_rows[i].position) + --m_rows[i].position; + } + + // Every row after the one we delete will have a different identifier, since + // the identifier is simply its index into m_rows. So we need to re-do the + // signal mappings for the affected rows. + for(unsigned i = id + 1; i < m_rows.count(); ++i) + moveSignalMappings(i, i - 1); + + m_rows.erase(&m_rows[id]); + + // Make sure we update the buttons of affected rows. + m_rows[idOfPosition(0)].upButton->setEnabled(false); + m_rows[idOfPosition(m_rows.count() - 1)].downButton->setEnabled(false); + + // We can insert another row now, make sure GUI is updated to match. + m_insertCategory->setEnabled(true); + + QTimer::singleShot(0, this, SLOT(exampleTextChanged())); + return true; +} + +void FileRenamerWidget::addFolderSeparatorCheckbox() +{ + QWidget *temp = new QWidget(m_mainFrame); + QHBoxLayout *l = new QHBoxLayout(temp); + + QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp); + m_folderSwitches.append(cb); + l->addWidget(cb, 0, AlignCenter); + cb->setChecked(false); + + connect(cb, SIGNAL(toggled(bool)), + SLOT(exampleTextChanged())); + + temp->show(); +} + +void FileRenamerWidget::createTagRows() +{ + KConfigGroup config(KGlobal::config(), "FileRenamer"); + QValueList<int> categoryOrder = config.readIntListEntry("CategoryOrder"); + + if(categoryOrder.isEmpty()) + categoryOrder << Artist << Album << Artist << Title << Track; + + // Setup arrays. + m_rows.reserve(categoryOrder.count()); + m_folderSwitches.reserve(categoryOrder.count() - 1); + + mapper = new QSignalMapper(this, "signal mapper"); + toggleMapper = new QSignalMapper(this, "toggle mapper"); + upMapper = new QSignalMapper(this, "up button mapper"); + downMapper = new QSignalMapper(this, "down button mapper"); + + connect(mapper, SIGNAL(mapped(int)), SLOT(showCategoryOption(int))); + connect(toggleMapper, SIGNAL(mapped(int)), SLOT(slotRemoveRow(int))); + connect(upMapper, SIGNAL(mapped(int)), SLOT(moveItemUp(int))); + connect(downMapper, SIGNAL(mapped(int)), SLOT(moveItemDown(int))); + + m_mainFrame = new QVBox(m_mainView->viewport()); + m_mainFrame->setMargin(10); + m_mainFrame->setSpacing(5); + + m_mainView->addChild(m_mainFrame); + m_mainView->setResizePolicy(QScrollView::AutoOneFit); + + // OK, the deal with the categoryOrder variable is that we need to create + // the rows in the order that they were saved in (the order given by categoryOrder). + // The signal mappers operate according to the row identifier. To find the position of + // a row given the identifier, use m_rows[id].position. To find the id of a given + // position, use idOfPosition(position). + + QValueList<int>::ConstIterator it = categoryOrder.constBegin(); + + for(; it != categoryOrder.constEnd(); ++it) { + if(*it < StartTag || *it >= NumTypes) { + kdError(65432) << "Invalid category encountered in file renamer configuration.\n"; + continue; + } + + if(m_rows.count() == MAX_CATEGORIES) { + kdError(65432) << "Maximum number of File Renamer tags reached, bailing.\n"; + break; + } + + TagType i = static_cast<TagType>(*it); + + addRowCategory(i); + + // Insert the directory separator checkbox if this isn't the last + // item. + + QValueList<int>::ConstIterator dup(it); + + // Check for last item + if(++dup != categoryOrder.constEnd()) + addFolderSeparatorCheckbox(); + } + + m_rows.first().upButton->setEnabled(false); + m_rows.last().downButton->setEnabled(false); + + // If we have maximum number of categories already, don't let the user + // add more. + if(m_rows.count() >= MAX_CATEGORIES) + m_insertCategory->setEnabled(false); +} + +void FileRenamerWidget::exampleTextChanged() +{ + // Just use .mp3 as an example + + if(m_exampleFromFile && (m_exampleFile.isEmpty() || + !FileHandle(m_exampleFile).tag()->isValid())) + { + m_exampleText->setText(i18n("No file selected, or selected file has no tags.")); + return; + } + + m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3"); +} + +QString FileRenamerWidget::fileCategoryValue(TagType category) const +{ + FileHandle file(m_exampleFile); + Tag *tag = file.tag(); + + switch(category) { + case Track: + return QString::number(tag->track()); + + case Year: + return QString::number(tag->year()); + + case Title: + return tag->title(); + + case Artist: + return tag->artist(); + + case Album: + return tag->album(); + + case Genre: + return tag->genre(); + + default: + return QString::null; + } +} + +QString FileRenamerWidget::categoryValue(TagType category) const +{ + if(m_exampleFromFile) + return fileCategoryValue(category); + + const ExampleOptions *example = m_exampleDialog->widget(); + + switch (category) { + case Track: + return example->m_exampleTrack->text(); + + case Year: + return example->m_exampleYear->text(); + + case Title: + return example->m_exampleTitle->text(); + + case Artist: + return example->m_exampleArtist->text(); + + case Album: + return example->m_exampleAlbum->text(); + + case Genre: + return example->m_exampleGenre->text(); + + default: + return QString::null; + } +} + +QValueList<CategoryID> FileRenamerWidget::categoryOrder() const +{ + QValueList<CategoryID> list; + + // Iterate in GUI row order. + for(unsigned i = 0; i < m_rows.count(); ++i) { + unsigned rowId = idOfPosition(i); + + list += m_rows[rowId].category; + } + + return list; +} + +bool FileRenamerWidget::hasFolderSeparator(unsigned index) const +{ + if(index >= m_folderSwitches.count()) + return false; + return m_folderSwitches[index]->isChecked(); +} + +void FileRenamerWidget::moveItem(unsigned id, MovementDirection direction) +{ + QWidget *l = m_rows[id].widget; + unsigned bottom = m_rows.count() - 1; + unsigned pos = m_rows[id].position; + unsigned newPos = (direction == MoveUp) ? pos - 1 : pos + 1; + + // Item we're moving can't go further down after this. + + if((pos == (bottom - 1) && direction == MoveDown) || + (pos == bottom && direction == MoveUp)) + { + unsigned idBottomRow = idOfPosition(bottom); + unsigned idAboveBottomRow = idOfPosition(bottom - 1); + + m_rows[idBottomRow].downButton->setEnabled(true); + m_rows[idAboveBottomRow].downButton->setEnabled(false); + } + + // We're moving the top item, do some button switching. + + if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) { + unsigned idTopItem = idOfPosition(0); + unsigned idBelowTopItem = idOfPosition(1); + + m_rows[idTopItem].upButton->setEnabled(true); + m_rows[idBelowTopItem].upButton->setEnabled(false); + } + + // This is the item we're swapping with. + + unsigned idSwitchWith = idOfPosition(newPos); + QWidget *w = m_rows[idSwitchWith].widget; + + // Update the table of widget rows. + + std::swap(m_rows[id].position, m_rows[idSwitchWith].position); + + // Move the item two spaces above/below its previous position. It has to + // be 2 spaces because of the checkbox. + + QBoxLayout *layout = dynamic_cast<QBoxLayout *>(m_mainFrame->layout()); + + layout->remove(l); + layout->insertWidget(2 * newPos, l); + + // Move the top item two spaces in the opposite direction, for a similar + // reason. + + layout->remove(w); + layout->insertWidget(2 * pos, w); + layout->invalidate(); + + QTimer::singleShot(0, this, SLOT(exampleTextChanged())); +} + +unsigned FileRenamerWidget::idOfPosition(unsigned position) const +{ + if(position >= m_rows.count()) { + kdError(65432) << "Search for position " << position << " out-of-range.\n"; + return static_cast<unsigned>(-1); + } + + for(unsigned i = 0; i < m_rows.count(); ++i) + if(m_rows[i].position == position) + return i; + + kdError(65432) << "Unable to find identifier for position " << position << endl; + return static_cast<unsigned>(-1); +} + +unsigned FileRenamerWidget::findIdentifier(const CategoryID &category) const +{ + for(unsigned index = 0; index < m_rows.count(); ++index) + if(m_rows[index].category == category) + return index; + + kdError(65432) << "Unable to find match for category " << + TagRenamerOptions::tagTypeText(category.category) << + ", number " << category.categoryNumber << endl; + + return MAX_CATEGORIES; +} + +void FileRenamerWidget::enableAllUpButtons() +{ + for(unsigned i = 0; i < m_rows.count(); ++i) + m_rows[i].upButton->setEnabled(true); +} + +void FileRenamerWidget::enableAllDownButtons() +{ + for(unsigned i = 0; i < m_rows.count(); ++i) + m_rows[i].downButton->setEnabled(true); +} + +void FileRenamerWidget::showCategoryOption(int id) +{ + TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber); + + if(dialog->exec() == QDialog::Accepted) { + m_rows[id].options = dialog->options(); + exampleTextChanged(); + } + + delete dialog; +} + +void FileRenamerWidget::moveItemUp(int id) +{ + moveItem(static_cast<unsigned>(id), MoveUp); +} + +void FileRenamerWidget::moveItemDown(int id) +{ + moveItem(static_cast<unsigned>(id), MoveDown); +} + +void FileRenamerWidget::toggleExampleDialog() +{ + m_exampleDialog->setShown(!m_exampleDialog->isShown()); +} + +void FileRenamerWidget::insertCategory() +{ + TagType category = TagRenamerOptions::tagFromCategoryText(m_category->currentText()); + if(category == Unknown) { + kdError(65432) << "Trying to add unknown category somehow.\n"; + return; + } + + // We need to enable the down button of the current bottom row since it + // can now move down. + unsigned idBottom = idOfPosition(m_rows.count() - 1); + m_rows[idBottom].downButton->setEnabled(true); + + addFolderSeparatorCheckbox(); + + // Identifier of new row. + unsigned id = addRowCategory(category); + + // Set its down button to be disabled. + m_rows[id].downButton->setEnabled(false); + + m_mainFrame->layout()->invalidate(); + m_mainView->update(); + + // Now update according to the code in loadConfig(). + m_rows[id].options = TagRenamerOptions(m_rows[id].category); + exampleTextChanged(); +} + +void FileRenamerWidget::exampleDialogShown() +{ + m_showExample->setText(i18n("Hide Renamer Test Dialog")); +} + +void FileRenamerWidget::exampleDialogHidden() +{ + m_showExample->setText(i18n("Show Renamer Test Dialog")); +} + +void FileRenamerWidget::fileSelected(const QString &file) +{ + m_exampleFromFile = true; + m_exampleFile = file; + exampleTextChanged(); +} + +void FileRenamerWidget::dataSelected() +{ + m_exampleFromFile = false; + exampleTextChanged(); +} + +QString FileRenamerWidget::separator() const +{ + return m_separator->currentText(); +} + +QString FileRenamerWidget::musicFolder() const +{ + return m_musicFolder->url(); +} + +void FileRenamerWidget::slotRemoveRow(int id) +{ + // Remove the given identified row. + if(!removeRow(id)) + kdError(65432) << "Unable to remove row " << id << endl; +} + +// +// Implementation of FileRenamer +// + +FileRenamer::FileRenamer() +{ +} + +void FileRenamer::rename(PlaylistItem *item) +{ + PlaylistItemList list; + list.append(item); + + rename(list); +} + +void FileRenamer::rename(const PlaylistItemList &items) +{ + ConfigCategoryReader reader; + QStringList errorFiles; + QMap<QString, QString> map; + QMap<QString, PlaylistItem *> itemMap; + + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + reader.setPlaylistItem(*it); + QString oldFile = (*it)->file().absFilePath(); + QString extension = (*it)->file().fileInfo().extension(false); + QString newFile = fileName(reader) + "." + extension; + + if(oldFile != newFile) { + map[oldFile] = newFile; + itemMap[oldFile] = *it; + } + } + + if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted) + return; + + KApplication::setOverrideCursor(Qt::waitCursor); + for(QMap<QString, QString>::ConstIterator it = map.begin(); + it != map.end(); ++it) + { + if(moveFile(it.key(), it.data())) { + itemMap[it.key()]->setFile(it.data()); + itemMap[it.key()]->refresh(); + + setFolderIcon(it.data(), itemMap[it.key()]); + } + else + errorFiles << i18n("%1 to %2").arg(it.key()).arg(it.data()); + + processEvents(); + } + KApplication::restoreOverrideCursor(); + + if(!errorFiles.isEmpty()) + KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles); +} + +bool FileRenamer::moveFile(const QString &src, const QString &dest) +{ + kdDebug(65432) << "Moving file " << src << " to " << dest << endl; + + if(src == dest) + return false; + + // Escape URL. + KURL srcURL = KURL::fromPathOrURL(src); + KURL dstURL = KURL::fromPathOrURL(dest); + + // Clean it. + srcURL.cleanPath(); + dstURL.cleanPath(); + + // Make sure it is valid. + if(!srcURL.isValid() || !dstURL.isValid()) + return false; + + // Get just the directory. + KURL dir = dstURL; + dir.setFileName(QString::null); + + // Create the directory. + if(!KStandardDirs::exists(dir.path())) + if(!KStandardDirs::makeDir(dir.path())) { + kdError() << "Unable to create directory " << dir.path() << endl; + return false; + } + + // Move the file. + return KIO::NetAccess::file_move(srcURL, dstURL); +} + +void FileRenamer::setFolderIcon(const KURL &dst, const PlaylistItem *item) +{ + if(item->file().tag()->album().isEmpty() || + !item->file().coverInfo()->hasCover()) + { + return; + } + + KURL dstURL = dst; + dstURL.cleanPath(); + + // Split path, and go through each path element. If a path element has + // the album information, set its folder icon. + QStringList elements = QStringList::split("/", dstURL.directory()); + QString path; + + for(QStringList::ConstIterator it = elements.begin(); it != elements.end(); ++it) { + path.append("/" + (*it)); + + kdDebug() << "Checking path: " << path << endl; + if((*it).find(item->file().tag()->album()) != -1 && + !QFile::exists(path + "/.directory")) + { + // Seems to be a match, let's set the folder icon for the current + // path. First we should write out the file. + + QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail); + thumb.save(path + "/.juk-thumbnail.png", "PNG"); + + KSimpleConfig config(path + "/.directory"); + config.setGroup("Desktop Entry"); + + if(!config.hasKey("Icon")) { + config.writeEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path)); + config.sync(); + } + + return; + } + } +} + +/** + * Returns iterator pointing to the last item enabled in the given list with + * a non-empty value (or is required to be included). + */ +QValueList<CategoryID>::ConstIterator lastEnabledItem(const QValueList<CategoryID> &list, + const CategoryReaderInterface &interface) +{ + QValueList<CategoryID>::ConstIterator it = list.constBegin(); + QValueList<CategoryID>::ConstIterator last = list.constEnd(); + + for(; it != list.constEnd(); ++it) { + if(interface.isRequired(*it) || (!interface.isDisabled(*it) && + !interface.categoryValue((*it).category).isEmpty())) + { + last = it; + } + } + + return last; +} + +QString FileRenamer::fileName(const CategoryReaderInterface &interface) +{ + const QValueList<CategoryID> categoryOrder = interface.categoryOrder(); + const QString separator = interface.separator(); + const QString folder = interface.musicFolder(); + QValueList<CategoryID>::ConstIterator lastEnabled; + unsigned i = 0; + QStringList list; + QChar dirSeparator = QChar(QDir::separator()); + + // Use lastEnabled to properly handle folder separators. + lastEnabled = lastEnabledItem(categoryOrder, interface); + bool pastLast = false; // Toggles to true once we've passed lastEnabled. + + for(QValueList<CategoryID>::ConstIterator it = categoryOrder.begin(); + it != categoryOrder.end(); + ++it, ++i) + { + if(it == lastEnabled) + pastLast = true; + + if(interface.isDisabled(*it)) + continue; + + QString value = interface.value(*it); + + // The user can use the folder separator checkbox to add folders, so don't allow + // slashes that slip in to accidentally create new folders. Should we filter this + // back out when showing it in the GUI? + value.replace('/', "%2f"); + + if(!pastLast && interface.hasFolderSeparator(i)) + value.append(dirSeparator); + + if(interface.isRequired(*it) || !value.isEmpty()) + list.append(value); + } + + // Construct a single string representation, handling strings ending in + // '/' specially + + QString result; + + for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) { + result += *it; + + ++it; // Manually advance iterator to check for end-of-list. + + // Add separator unless at a directory boundary + if(it != list.constEnd() && + !(*it).startsWith(dirSeparator) && // Check beginning of next item. + !result.endsWith(dirSeparator)) + { + result += separator; + } + } + + return QString(folder + dirSeparator + result); +} + +#include "filerenamer.moc" + +// vim: set et sw=4 ts=8: diff --git a/juk/filerenamer.h b/juk/filerenamer.h new file mode 100644 index 00000000..0097ed86 --- /dev/null +++ b/juk/filerenamer.h @@ -0,0 +1,543 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + : (C) 2003 Frerich Raabe <raabe@kde.org> + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_FILERENAMER_H +#define JUK_FILERENAMER_H + +#include <qstring.h> +#include <qvaluevector.h> +#include <qmap.h> + +#include "filerenamerbase.h" +#include "filerenameroptions.h" +#include "categoryreaderinterface.h" +#include "tagrenameroptions.h" +#include "playlistitem.h" + +class ExampleOptionsDialog; +class QCheckBox; +class QLayout; +class QLayoutItem; +class QPushButton; +class QVBox; +class PlaylistItem; +class QSignalMapper; + +// Used to decide what direction the FileRenamerWidget will move rows in. +enum MovementDirection { MoveUp, MoveDown }; + +/** + * This is used by FileRenamerWidget to store information about a particular + * tag type, including its position, the QFrame holding the information, + * the up, down, and enable buttons, and the user-selected renaming options. + */ +struct Row +{ + Row() : widget(0), upButton(0), downButton(0), enableButton(0) {} + + QWidget *widget; + + QPushButton *upButton, *downButton, *optionsButton, *enableButton; + + TagRenamerOptions options; + CategoryID category; // Includes category and a disambiguation id. + unsigned position; ///< Position in the GUI (0 == top) + QString name; +}; + +/** + * A list of rows, each of which may have its own category options and other + * associated data. There is no relation between the order of rows in the vector and their + * GUI layout. Instead, each Row has a position member which indicates what GUI position it + * takes up. The index into the vector is known as the row identifier (which is unique but + * not necessarily constant). + */ +typedef QValueVector<Row> Rows; + +/** + * Holds a list directory separator checkboxes which separate a row. There + * should always be 1 less than the number of rows in the GUI. + * + * Used for ConfigCategoryReader. + */ +typedef QValueVector<QCheckBox *> DirSeparatorCheckBoxes; + +/** + * Associates a CategoryID combination with a set of options. + * + * Used for ConfigCategoryReader + */ +typedef QMap<CategoryID, TagRenamerOptions> CategoryOptionsMap; + +/** + * An implementation of CategoryReaderInterface that reads the user's settings + * from the global KConfig configuration object, and reads track information + * from whatever the given PlaylistItem is. You can assign different + * PlaylistItems in order to change the returned tag category information. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class ConfigCategoryReader : public CategoryReaderInterface +{ +public: + // ConfigCategoryReader specific members + + ConfigCategoryReader(); + + const PlaylistItem *playlistItem() const { return m_currentItem; } + void setPlaylistItem(const PlaylistItem *item) { m_currentItem = item; } + + // CategoryReaderInterface reimplementations + + virtual QString categoryValue(TagType type) const; + virtual QString prefix(const CategoryID &category) const; + virtual QString suffix(const CategoryID &category) const; + virtual TagRenamerOptions::EmptyActions emptyAction(const CategoryID &category) const; + virtual QString emptyText(const CategoryID &category) const; + virtual QValueList<CategoryID> categoryOrder() const; + virtual QString separator() const; + virtual QString musicFolder() const; + virtual int trackWidth(unsigned categoryNum) const; + virtual bool hasFolderSeparator(unsigned index) const; + virtual bool isDisabled(const CategoryID &category) const; + +private: + const PlaylistItem *m_currentItem; + CategoryOptionsMap m_options; + QValueList<CategoryID> m_categoryOrder; + QString m_separator; + QString m_musicFolder; + QValueVector<bool> m_folderSeparators; +}; + +/** + * This class implements a dialog that allows the user to alter the behavior + * of the file renamer. It supports 6 different genre types at this point, + * and it shouldn't be too difficult to extend that in the future if needed. + * It allows the user to open an external dialog, which will let the user see + * an example of what their current options will look like, by either allowing + * the user to type in some sample information, or by loading a file and + * reading tags from there. + * + * It also implements the CategoryReaderInterface in order to implement the + * example filename functionality. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class FileRenamerWidget : public FileRenamerBase, public CategoryReaderInterface +{ + Q_OBJECT + +public: + FileRenamerWidget(QWidget *parent); + ~FileRenamerWidget(); + + /// Maximum number of total categories the widget will allow. + static unsigned const MAX_CATEGORIES = 16; + + /** + * This function saves all of the category options to the global KConfig + * object. You must call this manually, FileRenamerWidget doesn't call it + * automatically so that situations where the user hits "Cancel" work + * correctly. + */ + void saveConfig(); + +protected slots: + /** + * This function should be called whenever the example text may need to be + * changed. For example, when the user selects a different separator or + * changes the example text, this slot should be called. + */ + virtual void exampleTextChanged(); + + /** + * This function shows the example dialog if it is hidden, and hides the + * example dialog if it is shown. + */ + virtual void toggleExampleDialog(); + + /** + * This function inserts the currently selected category, so that the + * user can use duplicate tags in the file renamer. + */ + virtual void insertCategory(); + +private: + /** + * This function initializes the category options by loading the data from + * the global KConfig object. This is called automatically in the constructor. + */ + void loadConfig(); + + /** + * This function adds a "Insert Folder separator" checkbox to the end of + * the current layout. The setting defaults to being unchecked. + */ + void addFolderSeparatorCheckbox(); + + /** + * This function creates a row in the main view for category, appending it + * to the end. It handles connecting signals to the mapper and such as + * well. + * + * @param category Type of row to append. + * @return identifier of newly added row. + */ + unsigned addRowCategory(TagType category); + + /** + * Removes the given row, updating the other rows to have the correct + * number of categoryNumber. + * + * @param id The identifier of the row to remove. + * @return true if the delete succeeded, false otherwise. + */ + bool removeRow(unsigned id); + + /** + * Updates the mappings currently set for the row identified by oldId so + * that they emit newId instead. Does not actually delete the row given + * by oldId. + * + * @param oldId The identifier of the row to change mappings for. + * @param newId The identifier to use instead. + */ + void moveSignalMappings(unsigned oldId, unsigned newId); + + /** + * This function sets up the internal view by creating the checkboxes and + * the rows for each category. + */ + void createTagRows(); + + /** + * Returns the value for \p category by retrieving the tag from m_exampleFile. + * If \p category is Track, then an appropriate fixup will be applied if needed + * to match the user's desired minimum width. + * + * @param category the category to retrieve the value for. + * @return the string representation of the value for \p category. + */ + QString fileCategoryValue(TagType category) const; + + /** + * Returns the value for \p category by reading the user entry for that + * category. If \p category is Track, then an appropriate fixup will be applied + * if needed to match the user's desired minimum width. + * + * @param category the category to retrieve the value for. + * @return the string representation of the value for \p category. + */ + virtual QString categoryValue(TagType category) const; + + /** + * Returns the user-specified prefix string for \p category. + * + * @param category the category to retrieve the value for. + * @return user-specified prefix string for \p category. + */ + virtual QString prefix(const CategoryID &category) const + { + return m_rows[findIdentifier(category)].options.prefix(); + } + + /** + * Returns the user-specified suffix string for \p category. + * + * @param category the category to retrieve the value for. + * @return user-specified suffix string for \p category. + */ + virtual QString suffix(const CategoryID &category) const + { + return m_rows[findIdentifier(category)].options.suffix(); + } + + /** + * Returns the user-specified empty action for \p category. + * + * @param category the category to retrieve the value for. + * @return user-specified empty action for \p category. + */ + virtual TagRenamerOptions::EmptyActions emptyAction(const CategoryID &category) const + { + return m_rows[findIdentifier(category)].options.emptyAction(); + } + + /** + * Returns the user-specified empty text for \p category. This text might + * be used to replace an empty value. + * + * @param category the category to retrieve the value for. + * @return the user-specified empty text for \p category. + */ + virtual QString emptyText(const CategoryID &category) const + { + return m_rows[findIdentifier(category)].options.emptyText(); + } + + /** + * @return list of CategoryIDs corresponding to the user-specified category order. + */ + virtual QValueList<CategoryID> categoryOrder() const; + + /** + * @return string that separates the tag values in the file name. + */ + virtual QString separator() const; + + /** + * @return local path to the music folder used to store renamed files. + */ + virtual QString musicFolder() const; + + /** + * @param categoryNum Zero-based number of category to get results for (if more than one). + * @return the minimum width of the track category. + */ + virtual int trackWidth(unsigned categoryNum) const + { + CategoryID id(Track, categoryNum); + return m_rows[findIdentifier(id)].options.trackWidth(); + } + + /** + * @param index, the 0-based index for the folder boundary. + * @return true if there should be a folder separator between category + * index and index + 1, and false otherwise. Note that for purposes + * of this function, only categories that are required or non-empty + * should count. + */ + virtual bool hasFolderSeparator(unsigned index) const; + + /** + * @param category The category to get the status of. + * @return true if \p category is disabled by the user, and false otherwise. + */ + virtual bool isDisabled(const CategoryID &category) const + { + return m_rows[findIdentifier(category)].options.disabled(); + } + + /** + * This moves the widget \p l in the direction given by \p direction, taking + * care to make sure that the checkboxes are not moved, and that they are + * enabled or disabled as appropriate for the new layout, and that the up and + * down buttons are also adjusted as necessary. + * + * @param id the identifier of the row to move + * @param direction the direction to move + */ + void moveItem(unsigned id, MovementDirection direction); + + /** + * This function actually performs the work of showing the options dialog for + * \p category. + * + * @param category the category to show the options dialog for. + */ + void showCategoryOptions(TagType category); + + /** + * This function enables or disables the widget in the row identified by \p id, + * controlled by \p enable. This function also makes sure that checkboxes are + * enabled or disabled as appropriate if they no longer make sense due to the + * adjacent category being enabled or disabled. + * + * @param id the identifier of the row to change. This is *not* the category to + * change. + * @param enable enables the category if true, disables if false. + */ + void setCategoryEnabled(int id, bool enable); + + /** + * This function enables all of the up buttons. + */ + void enableAllUpButtons(); + + /** + * This function enables all of the down buttons. + */ + void enableAllDownButtons(); + + /** + * This function returns the identifier of the row at \p position. + * + * @param position The position to find the identifier of. + * @return The unique id of the row at \p position. + */ + unsigned idOfPosition(unsigned position) const; + + /** + * This function returns the identifier of the row in the m_rows index that + * contains \p category and matches \p categoryNum. + * + * @param category the category to find. + * @return the identifier of the category, or MAX_CATEGORIES if it couldn't + * be found. + */ + unsigned findIdentifier(const CategoryID &category) const; + +private slots: + /** + * This function reads the tags from \p file and ensures that the dialog will + * use those tags until a different file is selected or dataSelected() is + * called. + * + * @param file the path to the local file to read. + */ + virtual void fileSelected(const QString &file); + + /** + * This function reads the tags from the user-supplied examples and ensures + * that the dialog will use those tags until a file is selected using + * fileSelected(). + */ + virtual void dataSelected(); + + /** + * This function brings up a dialog that allows the user to edit the options + * for \p id. + * + * @param id the unique id to bring up the options for. + */ + virtual void showCategoryOption(int id); + + /** + * This function removes the row identified by id and updates the internal data to be + * consistent again, by forwarding the call to removeRow(). + * This roundabout way is done due to QSignalMapper. + * + * @param id The unique id to update + */ + virtual void slotRemoveRow(int id); + + /** + * This function moves \p category up in the layout. + * + * @param id the unique id of the widget to move up. + */ + virtual void moveItemUp(int id); + + /** + * This function moves \p category down in the layout. + * + * @param id the unique id of the widget to move down. + */ + virtual void moveItemDown(int id); + + /** + * This slot should be called whenever the example input dialog is shown. + */ + virtual void exampleDialogShown(); + + /** + * This slot should be called whever the example input dialog is hidden. + */ + virtual void exampleDialogHidden(); + +private: + /// This is the frame that holds all of the category widgets and checkboxes. + QVBox *m_mainFrame; + + /** + * This is the meat of the widget, it holds the rows for the user configuration. It is + * initially created such that m_rows[0] is the top and row + 1 is the row just below. + * However, this is NOT NECESSARILY true, so don't rely on this. As soon as the user + * clicks an arrow to move a row then the order will be messed up. Use row.position to + * determine where the row is in the GUI. + * + * @see idOfPosition + * @see findIdentifier + */ + Rows m_rows; + + /** + * This holds an array of checkboxes that allow the user to insert folder + * separators in between categories. + */ + DirSeparatorCheckBoxes m_folderSwitches; + + ExampleOptionsDialog *m_exampleDialog; + + /// This is true if we're reading example tags from m_exampleFile. + bool m_exampleFromFile; + QString m_exampleFile; + + // Used to map signals from rows to the correct widget. + QSignalMapper *mapper; + QSignalMapper *toggleMapper; + QSignalMapper *upMapper; + QSignalMapper *downMapper; +}; + +/** + * This class contains the backend code to actually implement the file renaming. It performs + * the function of moving the files from one location to another, constructing the file name + * based off of the user's options (see ConfigCategoryReader) and of setting folder icons + * if appropriate. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class FileRenamer +{ +public: + FileRenamer(); + + /** + * Renames the filename on disk of the file represented by item according + * to the user configuration stored in KConfig. + * + * @param item The item to rename. + */ + void rename(PlaylistItem *item); + + /** + * Renames the filenames on disk of the files given in items according to + * the user configuration stored in KConfig. + * + * @param items The items to rename. + */ + void rename(const PlaylistItemList &items); + + /** + * Returns the file name that would be generated based on the options read from + * interface, which must implement CategoryReaderInterface. (A whole interface is used + * so that we can re-use the code to generate filenames from a in-memory GUI and from + * KConfig). + * + * @param interface object to read options/data from. + */ + static QString fileName(const CategoryReaderInterface &interface); + +private: + /** + * Sets the folder icon for elements of the destination path for item (if + * there is not already a folder icon set, and if the folder's name has + * the album name. + */ + void setFolderIcon(const KURL &dst, const PlaylistItem *item); + + /** + * Attempts to rename the file from \a src to \a dest. Returns true if the + * operation succeeded. + */ + bool moveFile(const QString &src, const QString &dest); +}; + +#endif /* JUK_FILERENAMER_H */ + +// vim: set et sw=4 ts=8: diff --git a/juk/filerenamerbase.ui b/juk/filerenamerbase.ui new file mode 100644 index 00000000..28370971 --- /dev/null +++ b/juk/filerenamerbase.ui @@ -0,0 +1,379 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>FileRenamerBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>FileRenamerBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>489</width> + <height>579</height> + </rect> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="caption"> + <string>File Renamer Configuration</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout4</cstring> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KComboBox" row="1" column="1"> + <item> + <property name="text"> + <string> - </string> + </property> + </item> + <item> + <property name="text"> + <string>_</string> + </property> + </item> + <item> + <property name="text"> + <string>-</string> + </property> + </item> + <property name="name"> + <cstring>m_separator</cstring> + </property> + <property name="editable"> + <bool>true</bool> + </property> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>textLabel1_6</cstring> + </property> + <property name="text"> + <string>Music folder:</string> + </property> + </widget> + <widget class="QLayoutWidget" row="2" column="1"> + <property name="name"> + <cstring>layout3</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QComboBox"> + <item> + <property name="text"> + <string>Album Tag</string> + </property> + </item> + <item> + <property name="text"> + <string>Artist Tag</string> + </property> + </item> + <item> + <property name="text"> + <string>Genre Tag</string> + </property> + </item> + <item> + <property name="text"> + <string>Title Tag</string> + </property> + </item> + <item> + <property name="text"> + <string>Track Tag</string> + </property> + </item> + <item> + <property name="text"> + <string>Year Tag</string> + </property> + </item> + <property name="name"> + <cstring>m_category</cstring> + </property> + </widget> + <widget class="KPushButton"> + <property name="name"> + <cstring>m_insertCategory</cstring> + </property> + <property name="text"> + <string>Insert Category</string> + </property> + </widget> + </hbox> + </widget> + <widget class="KURLRequester" row="0" column="1"> + <property name="name"> + <cstring>m_musicFolder</cstring> + </property> + <property name="url" stdset="0"> + <string>/home/kde-cvs/music</string> + </property> + <property name="showLocalProtocol"> + <bool>true</bool> + </property> + <property name="mode"> + <number>26</number> + </property> + </widget> + <widget class="QLabel" row="2" column="0"> + <property name="name"> + <cstring>textLabel1</cstring> + </property> + <property name="text"> + <string>Add category:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>m_category</cstring> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>textLabel1_7</cstring> + </property> + <property name="text"> + <string>Separator:</string> + </property> + </widget> + </grid> + </widget> + <widget class="Line"> + <property name="name"> + <cstring>line1_2</cstring> + </property> + <property name="frameShape"> + <enum>HLine</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QScrollView"> + <property name="name"> + <cstring>m_mainView</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + </widget> + <widget class="Line"> + <property name="name"> + <cstring>line1</cstring> + </property> + <property name="frameShape"> + <enum>HLine</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + </widget> + <widget class="QGroupBox"> + <property name="name"> + <cstring>groupBox1</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>5</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Example</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>layout2</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <spacer> + <property name="name"> + <cstring>spacer2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="KPushButton"> + <property name="name"> + <cstring>m_showExample</cstring> + </property> + <property name="text"> + <string>Show Renamer Test Dialog</string> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer2_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </hbox> + </widget> + <widget class="QLineEdit"> + <property name="name"> + <cstring>m_exampleText</cstring> + </property> + <property name="focusPolicy"> + <enum>NoFocus</enum> + </property> + <property name="acceptDrops"> + <bool>false</bool> + </property> + <property name="lineWidth"> + <number>2</number> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </vbox> + </widget> + <spacer> + <property name="name"> + <cstring>spacer7</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + </spacer> + </vbox> +</widget> +<customwidgets> + <customwidget> + <class>QScrollView</class> + <header location="global">qscrollview.h</header> + <sizehint> + <width>100</width> + <height>30</height> + </sizehint> + <container>1</container> + <sizepolicy> + <hordata>5</hordata> + <verdata>5</verdata> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + <pixmap>image0</pixmap> + </customwidget> +</customwidgets> +<images> + <image name="image0"> + <data format="PNG" length="1002">89504e470d0a1a0a0000000d4948445200000016000000160806000000c4b46c3b000003b149444154388dad945f4c5b551cc73fe7dc4b7b4bcba0762d45c43114323599ee6192609c51d883892ce083f1718b3ebb185f8dc91e972cf39d2d2a2f1af664b6f1e0fe3863a0718969700eb0c52142da0242a1bd6d696f7bcff101585203ceb8fd9ece39f99dcff9fe7edf939f88c562ec465f5f9fe609442c161362173c3e3eae7b7a7ac8e7f36432196cdbfe4f907c3e4f2291201e8fe338cec3737357e9e8e828aded1e229d650e1f2d51754b082110124c13a4dc5ea341eb9dc284c0558a853f3ce8cb0677ef500fde7d39d2596679e326597b8e9abb85d7a770ab16ab6983ec5a05b487a70e36f0f4e10afe408d6a558310980108478dba4a1e8233990c5d474b64ed39aa3a8fe5f3317fbf81dbd70bccfeb205947632fd74f6589c1c6ea2f70d03a58ba0c1f2c9bdc1b66de3b8256a6e11cbe7e3ee1d181b590124fe2693aeee08d223c82c3a2c24b7b874bec8f26288774f7bd054504aef0dde6e99c0eb83f9fb266323cb80a27fb0958141836044605a2ee5523393371cc646fee2da37195aa35d0c0c5b4859ac03d7e91712dcaac5adab3650a3ff9d08ef7dd8404bb48869e5d958b5b87dadc4c9a1464e9f0d0326df7ebd86bd2e310cb1bf62d384d59441f2d70a070e1c60e09489929b988681bdd9cc97170bcc4c65595f71f8e0e3301337fc24a7732467831875a47f289652b0be5e4151e6d07316c1b0c0340d8ab92023e76d66a6b2840e36d2fb7a13fee632475e6edc367ea98a90fb98b7dd6310ca0328a44761582e1bab41befabcc0ec940d28bc5e93b68e064cab84e1d9beaeb48934eac1f53b01c1b000fca496aa54b61a99fcde61662a4b4b4b23d1680be9d426173e4df3602a48ea411989a4fd590f52a8fd156b05ed9d350e3defe3cfdf4b4c7ce770ea7d3fb9f520afbe1620daeee5c26735d20b9b9cfb6811a754a439e4e5c5639a4caa1e5caf586bfc0197b78702005cb9b4cae4cd3267ce8638fe964bd72b393e39d74928d242617303a756a37f284447770dcdbffc6384a05a85de1306e9a52057c7527c7131c3c42d3f475eb2303c82d4fc3276d6811db37efeb148723082d9b08f79f97c1e5729109a9a28307cc622d2d6cdf52b2b24efe548dedb00142009862cfa879ee1a71f6cec928353511472fbf4389148b0b0e0c108081412458dfe21c9f11351e67e7358595468246d1d1e5e38a6e9e851bc39d84ab502a669331dafec0d8ec7e3e8cb06e1a881d727d1ae40180a434a8c9db129a54126ad48a7358c2b4c5352c8c374bcccdab2bb37d8719cba79fab8211f9df218e0582c261e95f8bfc04f1a1e8bc5c4dfe0a190172af6a9690000000049454e44ae426082</data> + </image> +</images> +<connections> + <connection> + <sender>m_separator</sender> + <signal>textChanged(const QString&)</signal> + <receiver>FileRenamerBase</receiver> + <slot>exampleTextChanged()</slot> + </connection> + <connection> + <sender>m_musicFolder</sender> + <signal>textChanged(const QString&)</signal> + <receiver>FileRenamerBase</receiver> + <slot>exampleTextChanged()</slot> + </connection> + <connection> + <sender>m_showExample</sender> + <signal>clicked()</signal> + <receiver>FileRenamerBase</receiver> + <slot>toggleExampleDialog()</slot> + </connection> + <connection> + <sender>m_insertCategory</sender> + <signal>clicked()</signal> + <receiver>FileRenamerBase</receiver> + <slot>insertCategory()</slot> + </connection> +</connections> +<tabstops> + <tabstop>m_musicFolder</tabstop> + <tabstop>m_separator</tabstop> + <tabstop>m_category</tabstop> + <tabstop>m_insertCategory</tabstop> + <tabstop>m_showExample</tabstop> +</tabstops> +<slots> + <slot access="protected">exampleTextChanged()</slot> + <slot access="protected">toggleExampleDialog()</slot> + <slot access="protected">insertCategory()</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>kcombobox.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kurlrequester.h</includehint> + <includehint>klineedit.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>qscrollview.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/juk/filerenamerconfigdlg.cpp b/juk/filerenamerconfigdlg.cpp new file mode 100644 index 00000000..74038dc6 --- /dev/null +++ b/juk/filerenamerconfigdlg.cpp @@ -0,0 +1,43 @@ +/*************************************************************************** + begin : Mon Nov 01 2004 + copyright : (C) 2004 by Michael Pyne + : (c) 2003 Frerich Raabe <raabe@kde.org> + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <klocale.h> + +#include "filerenamer.h" +#include "filerenamerconfigdlg.h" + +FileRenamerConfigDlg::FileRenamerConfigDlg(QWidget *parent) : + KDialogBase(parent, "file renamer dialog", true, + i18n("File Renamer Options"), Ok | Cancel), + m_renamerWidget(new FileRenamerWidget(this)) +{ + m_renamerWidget->setMinimumSize(400, 300); + + setMainWidget(m_renamerWidget); +} + +void FileRenamerConfigDlg::accept() +{ + // Make sure the config gets saved. + + m_renamerWidget->saveConfig(); + + KDialogBase::accept(); +} + +#include "filerenamerconfigdlg.moc" + +// vim: set et sw=4 ts=4: diff --git a/juk/filerenamerconfigdlg.h b/juk/filerenamerconfigdlg.h new file mode 100644 index 00000000..0678f52b --- /dev/null +++ b/juk/filerenamerconfigdlg.h @@ -0,0 +1,38 @@ +/*************************************************************************** + begin : Mon Nov 01 2004 + copyright : (C) 2004 by Michael Pyne + : (c) 2003 Frerich Raabe <raabe@kde.org> + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_FILERENAMERCONFIGDLG_H +#define JUK_FILERENAMERCONFIGDLG_H + + +class FileRenamerWidget; + +class FileRenamerConfigDlg : public KDialogBase +{ + Q_OBJECT + public: + FileRenamerConfigDlg(QWidget *parent); + + protected slots: + virtual void accept(); + + private: + FileRenamerWidget *m_renamerWidget; +}; + +#endif // FILERENAMERCONFIGDLG_H + +// vim: set et ts=4 sw=4: diff --git a/juk/filerenameroptions.cpp b/juk/filerenameroptions.cpp new file mode 100644 index 00000000..2813be4b --- /dev/null +++ b/juk/filerenameroptions.cpp @@ -0,0 +1,157 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <klocale.h> +#include <kdebug.h> +#include <knuminput.h> + +#include <qlayout.h> +#include <qlabel.h> +#include <qradiobutton.h> +#include <qlineedit.h> +#include <qbuttongroup.h> + +#include "filerenameroptions.h" + +FileRenamerTagOptions::FileRenamerTagOptions(QWidget *parent, + const TagRenamerOptions &options) : + FileRenamerTagOptionsBase(parent), m_options(options) +{ + layout()->setSpacing(KDialog::spacingHint()); + layout()->setMargin(0); + + m_emptyTagGroup->layout()->setSpacing(KDialog::spacingHint()); + m_trackGroup->layout()->setSpacing(KDialog::spacingHint()); + m_emptyValueLayout->setSpacing(KDialog::spacingHint()); + m_exampleLayout->setSpacing(KDialog::spacingHint()); + m_spinLayout->setSpacing(KDialog::spacingHint()); + m_widthLayout->setSpacing(KDialog::spacingHint()); + m_tagLayout->setSpacing(KDialog::spacingHint()); + m_tagFormatGroup->layout()->setSpacing(KDialog::spacingHint()); + + if(m_options.category() != Track) + m_trackGroup->hide(); + + QString tagText = m_options.tagTypeText(); + + setCaption(caption().arg(tagText)); + m_tagFormatGroup->setTitle(m_tagFormatGroup->title().arg(tagText)); + m_emptyTagGroup->setTitle(m_emptyTagGroup->title().arg(tagText)); + m_description->setText(m_description->text().arg(tagText)); + m_tagLabel->setText(m_tagLabel->text().arg(tagText)); + + m_prefixText->setText(options.prefix()); + m_suffixText->setText(options.suffix()); + if(options.emptyAction() == TagRenamerOptions::ForceEmptyInclude) + m_includeEmptyButton->setChecked(true); + else if(options.emptyAction() == TagRenamerOptions::UseReplacementValue) + m_useValueButton->setChecked(true); + m_emptyTagValue->setText(options.emptyText()); + m_trackWidth->setValue(options.trackWidth()); + + slotBracketsChanged(); + slotEmptyActionChanged(); + slotTrackWidthChanged(); +} + +void FileRenamerTagOptions::slotBracketsChanged() +{ + QString tag = m_options.tagTypeText(); + + m_options.setPrefix(m_prefixText->text()); + m_options.setSuffix(m_suffixText->text()); + + m_substitution->setText(m_options.prefix() + tag + m_options.suffix()); +} + +void FileRenamerTagOptions::slotTrackWidthChanged() +{ + unsigned width = m_trackWidth->value(); + + m_options.setTrackWidth(width); + + QString singleDigitText = m_singleDigit->text(); + singleDigitText.remove(" ->"); + QString doubleDigitText = m_doubleDigit->text(); + doubleDigitText.remove(" ->"); + + if(singleDigitText.length() < width) { + QString p; + p.fill('0', width - singleDigitText.length()); + singleDigitText.prepend(p); + } + + if(doubleDigitText.length() < width) { + QString p; + p.fill('0', width - doubleDigitText.length()); + doubleDigitText.prepend(p); + } + + m_singleDigitExample->setText(singleDigitText); + m_doubleDigitExample->setText(doubleDigitText); +} + +void FileRenamerTagOptions::slotEmptyActionChanged() +{ + m_options.setEmptyText(m_emptyTagValue->text()); + + m_options.setEmptyAction(TagRenamerOptions::IgnoreEmptyTag); + + if(m_useValueButton->isChecked()) + m_options.setEmptyAction(TagRenamerOptions::UseReplacementValue); + else if(m_includeEmptyButton->isChecked()) + m_options.setEmptyAction(TagRenamerOptions::ForceEmptyInclude); +} + +TagOptionsDialog::TagOptionsDialog(QWidget *parent, + const TagRenamerOptions &options, + unsigned categoryNumber) : + KDialogBase(parent, 0, true, i18n("File Renamer"), Ok | Cancel), + m_options(options), + m_categoryNumber(categoryNumber) +{ + loadConfig(); + + m_widget = new FileRenamerTagOptions(this, m_options); + m_widget->setMinimumSize(400, 200); + + setMainWidget(m_widget); +} + +void TagOptionsDialog::accept() +{ + m_options = m_widget->options(); + + saveConfig(); + KDialogBase::accept(); +} + +void TagOptionsDialog::loadConfig() +{ + // Our m_options may not have been loaded from KConfig, force that to + // happen. + + CategoryID category(m_options.category(), m_categoryNumber); + m_options = TagRenamerOptions(category); +} + +void TagOptionsDialog::saveConfig() +{ + m_options.saveConfig(m_categoryNumber); +} + +#include "filerenameroptions.moc" + +// vim: set et ts=4 sw=4: diff --git a/juk/filerenameroptions.h b/juk/filerenameroptions.h new file mode 100644 index 00000000..0711fbf7 --- /dev/null +++ b/juk/filerenameroptions.h @@ -0,0 +1,79 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_FILERENAMEROPTIONS_H +#define JUK_FILERENAMEROPTIONS_H + +#include <kdialogbase.h> +#include "filerenameroptionsbase.h" +#include "tagrenameroptions.h" + +/** + * Base widget implementing the options for a particular tag type. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class FileRenamerTagOptions : public FileRenamerTagOptionsBase +{ + Q_OBJECT + + public: + FileRenamerTagOptions(QWidget *parent, const TagRenamerOptions &options); + + const TagRenamerOptions &options() const { return m_options; } + + protected slots: + virtual void slotBracketsChanged(); + virtual void slotTrackWidthChanged(); + virtual void slotEmptyActionChanged(); + + private: + TagRenamerOptions m_options; +}; + +/** + * This defines the dialog that actually gets the options from the user. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class TagOptionsDialog : public KDialogBase +{ + Q_OBJECT + + public: + TagOptionsDialog(QWidget *parent, const TagRenamerOptions &options, unsigned categoryNumber); + + const TagRenamerOptions &options() const { return m_options; } + + protected slots: + virtual void accept(); + + private: + + // Private methods + + void loadConfig(); // Loads m_options from KConfig + void saveConfig(); // Saves m_options to KConfig + + // Private members + + FileRenamerTagOptions *m_widget; + TagRenamerOptions m_options; + unsigned m_categoryNumber; +}; + +#endif /* JUK_FILERENAMEROPTIONS_H */ + +// vim: set et ts=4 sw=4: diff --git a/juk/filerenameroptionsbase.ui b/juk/filerenameroptionsbase.ui new file mode 100644 index 00000000..1e40ce60 --- /dev/null +++ b/juk/filerenameroptionsbase.ui @@ -0,0 +1,425 @@ +<!DOCTYPE UI><UI version="3.3" stdsetdef="1"> +<class>FileRenamerTagOptionsBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>FileRenamerTagOptionsBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>568</width> + <height>377</height> + </rect> + </property> + <property name="caption"> + <string>%1 Options</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox"> + <property name="name"> + <cstring>m_tagFormatGroup</cstring> + </property> + <property name="title"> + <string>%1 Format</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>m_description</cstring> + </property> + <property name="text"> + <string>When using the file renamer your files will be renamed to the values that you have in your track's %1 tag, plus any additional text that you specify below.</string> + </property> + <property name="textFormat"> + <enum>RichText</enum> + </property> + <property name="alignment"> + <set>WordBreak|AlignCenter</set> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>m_tagLayout</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <spacer> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + <widget class="QLineEdit"> + <property name="name"> + <cstring>m_prefixText</cstring> + </property> + <property name="alignment"> + <set>AlignRight</set> + </property> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>m_tagLabel</cstring> + </property> + <property name="text"> + <string>%1</string> + </property> + </widget> + <widget class="QLineEdit"> + <property name="name"> + <cstring>m_suffixText</cstring> + </property> + <property name="alignment"> + <set>AlignLeft</set> + </property> + </widget> + <spacer> + <property name="name"> + <cstring>spacer1_2</cstring> + </property> + <property name="orientation"> + <enum>Horizontal</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </hbox> + </widget> + <widget class="QLabel"> + <property name="name"> + <cstring>m_substitution</cstring> + </property> + <property name="font"> + <font> + <italic>1</italic> + </font> + </property> + <property name="text"> + <string>Substitution Example</string> + </property> + <property name="alignment"> + <set>AlignCenter</set> + </property> + </widget> + </vbox> + </widget> + <widget class="QButtonGroup"> + <property name="name"> + <cstring>m_emptyTagGroup</cstring> + </property> + <property name="title"> + <string>When the Track's %1 is Empty</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QRadioButton"> + <property name="name"> + <cstring>m_includeEmptyButton</cstring> + </property> + <property name="text"> + <string>Include in the &filename anyways</string> + </property> + </widget> + <widget class="QRadioButton"> + <property name="name"> + <cstring>m_ignoreTagButton</cstring> + </property> + <property name="text"> + <string>&Ignore this tag when renaming the file</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>m_emptyValueLayout</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QRadioButton"> + <property name="name"> + <cstring>m_useValueButton</cstring> + </property> + <property name="text"> + <string>Use &this value:</string> + </property> + </widget> + <widget class="QLineEdit"> + <property name="name"> + <cstring>m_emptyTagValue</cstring> + </property> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Empty</string> + </property> + </widget> + </hbox> + </widget> + </vbox> + </widget> + <widget class="QGroupBox"> + <property name="name"> + <cstring>m_trackGroup</cstring> + </property> + <property name="title"> + <string>Track Width Options</string> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel10</cstring> + </property> + <property name="text"> + <string>JuK can force the track used in a file name to have a minimum number of digits. You may want to do this for better sorting in file managers.</string> + </property> + <property name="textFormat"> + <enum>RichText</enum> + </property> + <property name="alignment"> + <set>WordBreak|AlignCenter</set> + </property> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>m_widthLayout</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>m_spinLayout</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>textLabel5</cstring> + </property> + <property name="text"> + <string>Minimum track &width:</string> + </property> + <property name="buddy" stdset="0"> + <cstring>m_trackWidth</cstring> + </property> + </widget> + <widget class="KIntSpinBox"> + <property name="name"> + <cstring>m_trackWidth</cstring> + </property> + <property name="specialValueText"> + <string>None</string> + </property> + <property name="maxValue"> + <number>6</number> + </property> + <property name="minValue"> + <number>1</number> + </property> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>m_exampleLayout</cstring> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <property name="margin"> + <number>0</number> + </property> + <widget class="QLabel" row="1" column="1"> + <property name="name"> + <cstring>m_doubleDigitExample</cstring> + </property> + <property name="font"> + <font> + <italic>1</italic> + </font> + </property> + <property name="text"> + <string>014</string> + </property> + </widget> + <widget class="QLabel" row="0" column="1"> + <property name="name"> + <cstring>m_singleDigitExample</cstring> + </property> + <property name="font"> + <font> + <italic>1</italic> + </font> + </property> + <property name="text"> + <string>003</string> + </property> + </widget> + <widget class="QLabel" row="0" column="0"> + <property name="name"> + <cstring>m_singleDigit</cstring> + </property> + <property name="text"> + <string>3 -></string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + </widget> + <widget class="QLabel" row="1" column="0"> + <property name="name"> + <cstring>m_doubleDigit</cstring> + </property> + <property name="text"> + <string>14 -></string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignRight</set> + </property> + </widget> + </grid> + </widget> + </hbox> + </widget> + </vbox> + </widget> + <spacer> + <property name="name"> + <cstring>spacer6</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + </spacer> + </vbox> +</widget> +<customwidgets> +</customwidgets> +<connections> + <connection> + <sender>m_useValueButton</sender> + <signal>toggled(bool)</signal> + <receiver>m_emptyTagValue</receiver> + <slot>setEnabled(bool)</slot> + </connection> + <connection> + <sender>m_useValueButton</sender> + <signal>clicked()</signal> + <receiver>m_emptyTagValue</receiver> + <slot>setFocus()</slot> + </connection> + <connection> + <sender>m_trackWidth</sender> + <signal>valueChanged(int)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotTrackWidthChanged()</slot> + </connection> + <connection> + <sender>m_prefixText</sender> + <signal>textChanged(const QString&)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotBracketsChanged()</slot> + </connection> + <connection> + <sender>m_suffixText</sender> + <signal>textChanged(const QString&)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotBracketsChanged()</slot> + </connection> + <connection> + <sender>m_includeEmptyButton</sender> + <signal>toggled(bool)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotEmptyActionChanged()</slot> + </connection> + <connection> + <sender>m_ignoreTagButton</sender> + <signal>toggled(bool)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotEmptyActionChanged()</slot> + </connection> + <connection> + <sender>m_useValueButton</sender> + <signal>toggled(bool)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotEmptyActionChanged()</slot> + </connection> + <connection> + <sender>m_emptyTagValue</sender> + <signal>textChanged(const QString&)</signal> + <receiver>FileRenamerTagOptionsBase</receiver> + <slot>slotEmptyActionChanged()</slot> + </connection> +</connections> +<slots> + <slot access="protected">slotBracketsChanged()</slot> + <slot access="protected">slotTrackWidthChanged()</slot> + <slot access="protected">slotEmptyActionChanged()</slot> +</slots> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>knuminput.h</includehint> +</includehints> +</UI> diff --git a/juk/folderplaylist.cpp b/juk/folderplaylist.cpp new file mode 100644 index 00000000..ecde8f77 --- /dev/null +++ b/juk/folderplaylist.cpp @@ -0,0 +1,81 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include "folderplaylist.h" +#include "playlistcollection.h" + +#include <qtimer.h> + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +FolderPlaylist::FolderPlaylist(PlaylistCollection *collection, const QString &folder, + const QString &name) : + Playlist(collection, name, "folder"), + m_folder(folder) +{ + QTimer::singleShot(0, this, SLOT(slotReload())); +} + +FolderPlaylist::~FolderPlaylist() +{ + +} + +QString FolderPlaylist::folder() const +{ + return m_folder; +} + +void FolderPlaylist::setFolder(const QString &s) +{ + m_folder = s; + QTimer::singleShot(0, this, SLOT(slotReload())); +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void FolderPlaylist::slotReload() +{ + if(!m_folder.isNull()) + addFiles(m_folder); +} + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const FolderPlaylist &p) +{ + s << p.name() + << p.folder(); + return s; +} + +QDataStream &operator>>(QDataStream &s, FolderPlaylist &p) +{ + QString name; + QString folder; + s >> name + >> folder; + + p.setFolder(folder); + p.setName(name); + return s; +} + +#include "folderplaylist.moc" diff --git a/juk/folderplaylist.h b/juk/folderplaylist.h new file mode 100644 index 00000000..0635a131 --- /dev/null +++ b/juk/folderplaylist.h @@ -0,0 +1,44 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef FOLDERPLAYLIST_H +#define FOLDERPLAYLIST_H + +#include "playlist.h" + +class FolderPlaylist : public Playlist +{ + Q_OBJECT + +public: + FolderPlaylist(PlaylistCollection *collection, const QString &folder = QString::null, + const QString &name = QString::null); + virtual ~FolderPlaylist(); + + QString folder() const; + void setFolder(const QString &s); + + virtual bool canReload() const { return true; } + +public slots: + virtual void slotReload(); + +private: + QString m_folder; +}; + +QDataStream &operator<<(QDataStream &s, const FolderPlaylist &p); +QDataStream &operator>>(QDataStream &s, FolderPlaylist &p); + +#endif diff --git a/juk/gstreamerplayer.cpp b/juk/gstreamerplayer.cpp new file mode 100644 index 00000000..c538d9ff --- /dev/null +++ b/juk/gstreamerplayer.cpp @@ -0,0 +1,346 @@ +/*************************************************************************** + copyright : (C) 2004 Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include "gstreamerplayer.h" + +#if HAVE_GSTREAMER + +#include <kapplication.h> +#include <kconfig.h> +#include <kglobal.h> +#include <kdebug.h> + +#include <qfile.h> +#include <qtimer.h> + +// Defined because recent versions of glib add support for having gcc check +// whether the sentinel used on g_object_{set,get} is correct. Although 0 +// is a valid NULL pointer in C++, when used in a C function call g++ doesn't +// know to turn it into a pointer so it leaves it as an int instead (which is +// wrong for 64-bit arch). So, use the handy define below instead. + +#define JUK_GLIB_NULL static_cast<gpointer>(0) + +#if GSTREAMER_VERSION == 8 + +/******************************************************************************/ +/******************************************************************************/ +/****************************** GSTREAMER 0.8 *******************************/ +/******************************************************************************/ +/******************************************************************************/ + + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +GStreamerPlayer::GStreamerPlayer() : + Player(), + m_pipeline(0), + m_source(0), + m_decoder(0), + m_volume(0), + m_sink(0) +{ + readConfig(); + setupPipeline(); +} + +GStreamerPlayer::~GStreamerPlayer() +{ + stop(); + gst_object_unref(GST_OBJECT(m_pipeline)); +} + +void GStreamerPlayer::play(const FileHandle &file) +{ + if(!file.isNull()) { + stop(); + g_object_set(G_OBJECT(m_source), "location", file.absFilePath().local8Bit().data(), JUK_GLIB_NULL); + } + + gst_element_set_state(m_pipeline, GST_STATE_PLAYING); +} + +void GStreamerPlayer::pause() +{ + gst_element_set_state(m_pipeline, GST_STATE_PAUSED); +} + +void GStreamerPlayer::stop() +{ + gst_element_set_state(m_pipeline, GST_STATE_NULL); +} + +void GStreamerPlayer::setVolume(float volume) +{ + g_object_set(G_OBJECT(m_volume), "volume", volume, JUK_GLIB_NULL); +} + +float GStreamerPlayer::volume() const +{ + gdouble value; + g_object_get(G_OBJECT(m_volume), "volume", &value, JUK_GLIB_NULL); + return (float) value; +} + +bool GStreamerPlayer::playing() const +{ + return gst_element_get_state(m_pipeline) == GST_STATE_PLAYING; +} + +bool GStreamerPlayer::paused() const +{ + return gst_element_get_state(m_pipeline) == GST_STATE_PAUSED; +} + +int GStreamerPlayer::totalTime() const +{ + return time(GST_QUERY_TOTAL) / GST_SECOND; +} + +int GStreamerPlayer::currentTime() const +{ + return time(GST_QUERY_POSITION) / GST_SECOND; +} + +int GStreamerPlayer::position() const +{ + long long total = time(GST_QUERY_TOTAL); + long long current = time(GST_QUERY_POSITION); + return total > 0 ? int((double(current) / double(total)) * double(1000) + 0.5) : 0; +} + +void GStreamerPlayer::seek(int seekTime) +{ + int type = (GST_FORMAT_TIME | GST_SEEK_METHOD_SET | GST_SEEK_FLAG_FLUSH); + gst_element_seek(m_sink, GstSeekType(type), seekTime * GST_SECOND); +} + +void GStreamerPlayer::seekPosition(int position) +{ + long long total = time(GST_QUERY_TOTAL); + if(total > 0) + seek(int(double(position) / double(1000) * double(totalTime()) + 0.5)); +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void GStreamerPlayer::readConfig() +{ + KConfigGroup config(KGlobal::config(), "GStreamerPlayer"); + m_sinkName = config.readEntry("SinkName", QString::null); +} + +void GStreamerPlayer::setupPipeline() +{ + static bool initialized = false; + if(!initialized) { + int argc = kapp->argc(); + char **argv = kapp->argv(); + gst_init(&argc, &argv); + initialized = true; + } + + m_pipeline = gst_thread_new("pipeline"); + m_source = gst_element_factory_make("filesrc", "source"); + m_decoder = gst_element_factory_make("spider", "decoder"); + m_volume = gst_element_factory_make("volume", "volume"); + + if(!m_sinkName.isNull()) + m_sink = gst_element_factory_make(m_sinkName.utf8().data(), "sink"); + else { + m_sink = gst_element_factory_make("alsasink", "sink"); + if(!m_sink) + m_sink = gst_element_factory_make("osssink", "sink"); + } + + + gst_bin_add_many(GST_BIN(m_pipeline), m_source, m_decoder, m_volume, m_sink, 0); + gst_element_link_many(m_source, m_decoder, m_volume, m_sink, 0); +} + +long long GStreamerPlayer::time(GstQueryType type) const +{ + gint64 ns = 0; + GstFormat format = GST_FORMAT_TIME; + gst_element_query(m_sink, type, &format, &ns); + return ns; +} + +#else + +/******************************************************************************/ +/******************************************************************************/ +/****************************** GSTREAMER 0.10 ******************************/ +/******************************************************************************/ +/******************************************************************************/ + +static GstBusSyncReply messageHandler(GstBus *, GstMessage *message, gpointer data) +{ + if(GST_MESSAGE_TYPE(message) == GST_MESSAGE_EOS) { + GStreamerPlayer *player = static_cast<GStreamerPlayer *>(data); + QTimer::singleShot(0, player, SLOT(stop())); + } + + gst_message_unref(message); + return GST_BUS_DROP; +} + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +GStreamerPlayer::GStreamerPlayer() : + Player(), + m_playbin(0) +{ + setupPipeline(); +} + +GStreamerPlayer::~GStreamerPlayer() +{ + stop(); + gst_object_unref(GST_OBJECT(m_playbin)); +} + +void GStreamerPlayer::play(const FileHandle &file) +{ + if(!file.isNull()) { + stop(); + gchar *uri = g_filename_to_uri(file.absFilePath().local8Bit().data(), NULL, NULL); + g_object_set(G_OBJECT(m_playbin), "uri", uri, JUK_GLIB_NULL); + } + + gst_element_set_state(m_playbin, GST_STATE_PLAYING); +} + +void GStreamerPlayer::pause() +{ + gst_element_set_state(m_playbin, GST_STATE_PAUSED); +} + +void GStreamerPlayer::stop() +{ + gst_element_set_state(m_playbin, GST_STATE_NULL); +} + +void GStreamerPlayer::setVolume(float volume) +{ + g_object_set(G_OBJECT(m_playbin), "volume", volume, JUK_GLIB_NULL); +} + +float GStreamerPlayer::volume() const +{ + gdouble value; + g_object_get(G_OBJECT(m_playbin), "volume", &value, JUK_GLIB_NULL); + return (float) value; +} + +bool GStreamerPlayer::playing() const +{ + return state() == GST_STATE_PLAYING; +} + +bool GStreamerPlayer::paused() const +{ + return state() == GST_STATE_PAUSED; +} + +int GStreamerPlayer::totalTime() const +{ + return time(TotalLength) / GST_SECOND; +} + +int GStreamerPlayer::currentTime() const +{ + return time(CurrentPosition) / GST_SECOND; +} + +int GStreamerPlayer::position() const +{ + long long total = time(TotalLength); + long long current = time(CurrentPosition); + return total > 0 ? int((double(current) / double(total)) * double(1000) + 0.5) : 0; +} + +void GStreamerPlayer::seek(int seekTime) +{ + gst_element_seek(m_playbin, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + GST_SEEK_TYPE_SET, seekTime * GST_SECOND, GST_SEEK_TYPE_END, 0); +} + +void GStreamerPlayer::seekPosition(int position) +{ + gint64 time = gint64((double(position) / double(1000) * double(totalTime()) + + 0.5) * double(GST_SECOND)); + gst_element_seek(m_playbin, 1.0, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + GST_SEEK_TYPE_SET, time, GST_SEEK_TYPE_END, 0); +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void GStreamerPlayer::setupPipeline() +{ + static bool initialized = false; + if(!initialized) { + int argc = kapp->argc(); + char **argv = kapp->argv(); + gst_init(&argc, &argv); + initialized = true; + } + + m_playbin = gst_element_factory_make("playbin", "playbin"); + gst_bus_set_sync_handler(gst_pipeline_get_bus(GST_PIPELINE(m_playbin)), messageHandler, this); +} + +long long GStreamerPlayer::time(TimeQuery type) const +{ + GstQuery *query = (type == CurrentPosition) + ? gst_query_new_position(GST_FORMAT_TIME) + : gst_query_new_duration(GST_FORMAT_TIME); + + gint64 ns = 0; + GstFormat format; + + if(gst_element_query(m_playbin, query)) + { + if(type == CurrentPosition) + gst_query_parse_position(query, &format, &ns); + else + gst_query_parse_duration(query, &format, &ns); + } + + gst_query_unref(query); + + return ns; +} + +GstState GStreamerPlayer::state() const +{ + GstState state; + gst_element_get_state(m_playbin, &state, NULL, GST_CLOCK_TIME_NONE); + return state; +} + +#endif + +#include "gstreamerplayer.moc" +#endif + +// vim: set et sw=4: diff --git a/juk/gstreamerplayer.h b/juk/gstreamerplayer.h new file mode 100644 index 00000000..1d15562b --- /dev/null +++ b/juk/gstreamerplayer.h @@ -0,0 +1,81 @@ +/*************************************************************************** + copyright : (C) 2004 Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + + +#ifndef GSTREAMERPLAYER_H +#define GSTREAMERPLAYER_H + +#include "config.h" + +#if HAVE_GSTREAMER + +#include <gst/gst.h> +#include <qstring.h> +#include "player.h" + +class GStreamerPlayer : public Player +{ + Q_OBJECT + +public: + GStreamerPlayer(); + virtual ~GStreamerPlayer(); + + virtual void play(const FileHandle &file = FileHandle::null()); + + virtual void setVolume(float volume = 1.0); + virtual float volume() const; + + virtual bool playing() const; + virtual bool paused() const; + + virtual int totalTime() const; + virtual int currentTime() const; + virtual int position() const; // in this case not really the percent + + virtual void seek(int seekTime); + virtual void seekPosition(int position); + + virtual void pause(); +public slots: + virtual void stop(); + +private: + void setupPipeline(); + +#if GSTREAMER_VERSION == 8 + + void readConfig(); + long long time(GstQueryType type) const; + + QString m_sinkName; + GstElement *m_pipeline; + GstElement *m_source; + GstElement *m_decoder; + GstElement *m_volume; + GstElement *m_sink; + +#else + + enum TimeQuery { CurrentPosition, TotalLength }; + long long time(TimeQuery type) const; + + GstState state() const; + GstElement *m_playbin; + +#endif +}; + +#endif +#endif diff --git a/juk/hi128-app-juk.png b/juk/hi128-app-juk.png Binary files differnew file mode 100644 index 00000000..5c41868e --- /dev/null +++ b/juk/hi128-app-juk.png diff --git a/juk/hi16-app-juk.png b/juk/hi16-app-juk.png Binary files differnew file mode 100644 index 00000000..57933cde --- /dev/null +++ b/juk/hi16-app-juk.png diff --git a/juk/hi32-app-juk.png b/juk/hi32-app-juk.png Binary files differnew file mode 100644 index 00000000..2b58a33d --- /dev/null +++ b/juk/hi32-app-juk.png diff --git a/juk/hi48-app-juk.png b/juk/hi48-app-juk.png Binary files differnew file mode 100644 index 00000000..73a07612 --- /dev/null +++ b/juk/hi48-app-juk.png diff --git a/juk/hi64-app-juk.png b/juk/hi64-app-juk.png Binary files differnew file mode 100644 index 00000000..e258cd80 --- /dev/null +++ b/juk/hi64-app-juk.png diff --git a/juk/historyplaylist.cpp b/juk/historyplaylist.cpp new file mode 100644 index 00000000..6ebd4643 --- /dev/null +++ b/juk/historyplaylist.cpp @@ -0,0 +1,160 @@ + /*************************************************************************** + begin : Fri Aug 8 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <klocale.h> +#include <kglobal.h> +#include <kdebug.h> + +#include "historyplaylist.h" +#include "collectionlist.h" +#include "playermanager.h" + +//////////////////////////////////////////////////////////////////////////////// +// HistoryPlayList public members +//////////////////////////////////////////////////////////////////////////////// + +HistoryPlaylist::HistoryPlaylist(PlaylistCollection *collection) : + Playlist(collection, true), m_timer(0) +{ + setAllowDuplicates(true); + m_timer = new QTimer(this); + + connect(PlayerManager::instance(), SIGNAL(signalPlay()), this, SLOT(slotAddPlaying())); + connect(m_timer, SIGNAL(timeout()), this, SLOT(slotCreateNewItem())); +} + +HistoryPlaylist::~HistoryPlaylist() +{ + +} + +HistoryPlaylistItem *HistoryPlaylist::createItem(const FileHandle &file, + QListViewItem *after, bool emitChanged) +{ + if(!after) + after = lastItem(); + return Playlist::createItem<HistoryPlaylistItem, CollectionListItem, + CollectionList>(file, after, emitChanged); +} + +void HistoryPlaylist::createItems(const PlaylistItemList &siblings) +{ + Playlist::createItems<CollectionListItem, HistoryPlaylistItem, PlaylistItem>(siblings); +} + +//////////////////////////////////////////////////////////////////////////////// +// HistoryPlaylist protected members +//////////////////////////////////////////////////////////////////////////////// + +void HistoryPlaylist::polish() +{ + addColumn(i18n("Time")); + Playlist::polish(); + setSorting(-1); +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void HistoryPlaylist::slotAddPlaying() +{ + m_file = PlayerManager::instance()->playingFile(); + m_timer->stop(); + m_timer->start(delay(), true); +} + +void HistoryPlaylist::slotCreateNewItem() +{ + PlayerManager *player = PlayerManager::instance(); + + if(player->playing() && m_file == player->playingFile()) { + createItem(m_file); + m_file = FileHandle::null(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// HistoryPlaylistItem public members +//////////////////////////////////////////////////////////////////////////////// + +HistoryPlaylistItem::HistoryPlaylistItem(CollectionListItem *item, Playlist *parent, QListViewItem *after) : + PlaylistItem(item, parent, after), + m_dateTime(QDateTime::currentDateTime()) +{ + setText(0, KGlobal::locale()->formatDateTime(m_dateTime)); +} + +HistoryPlaylistItem::HistoryPlaylistItem(CollectionListItem *item, Playlist *parent) : + PlaylistItem(item, parent), + m_dateTime(QDateTime::currentDateTime()) +{ + setText(0, KGlobal::locale()->formatDateTime(m_dateTime)); +} + +HistoryPlaylistItem::~HistoryPlaylistItem() +{ + +} + +void HistoryPlaylistItem::setDateTime(const QDateTime &dt) +{ + m_dateTime = dt; + setText(0, KGlobal::locale()->formatDateTime(m_dateTime)); +} + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const HistoryPlaylist &p) +{ + PlaylistItemList l = const_cast<HistoryPlaylist *>(&p)->items(); + + s << Q_INT32(l.count()); + + for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) { + const HistoryPlaylistItem *i = static_cast<HistoryPlaylistItem *>(*it); + s << i->file().absFilePath(); + s << i->dateTime(); + } + + return s; +} + +QDataStream &operator>>(QDataStream &s, HistoryPlaylist &p) +{ + Q_INT32 count; + s >> count; + + HistoryPlaylistItem *after = 0; + + QString fileName; + QDateTime dateTime; + + for(int i = 0; i < count; i++) { + s >> fileName; + s >> dateTime; + + after = p.createItem(FileHandle(fileName), after, false); + after->setDateTime(dateTime); + } + + p.dataChanged(); + + return s; +} + +#include "historyplaylist.moc" diff --git a/juk/historyplaylist.h b/juk/historyplaylist.h new file mode 100644 index 00000000..144ec11c --- /dev/null +++ b/juk/historyplaylist.h @@ -0,0 +1,72 @@ +/*************************************************************************** + begin : Fri Aug 8 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef HISTORYPLAYLIST_H +#define HISTORYPLAYLIST_H + + +#include "playlist.h" +#include "playlistitem.h" + +class HistoryPlaylistItem : public PlaylistItem +{ +public: + HistoryPlaylistItem(CollectionListItem *item, Playlist *parent, QListViewItem *after); + HistoryPlaylistItem(CollectionListItem *item, Playlist *parent); + virtual ~HistoryPlaylistItem(); + + QDateTime dateTime() const { return m_dateTime; } + void setDateTime(const QDateTime &dt); + +private: + QDateTime m_dateTime; +}; + +class HistoryPlaylist : public Playlist +{ + Q_OBJECT + +public: + HistoryPlaylist(PlaylistCollection *collection); + virtual ~HistoryPlaylist(); + + virtual HistoryPlaylistItem *createItem(const FileHandle &file, QListViewItem *after = 0, + bool emitChanged = true); + virtual void createItems(const PlaylistItemList &siblings); + virtual int columnOffset() const { return 1; } + virtual bool readOnly() const { return true; } + + static int delay() { return 5000; } + +public slots: + void cut() {} + void clear() {} + +protected: + virtual void polish(); + +private slots: + void slotAddPlaying(); + void slotCreateNewItem(); + +private: + FileHandle m_file; + QTimer *m_timer; +}; + +QDataStream &operator<<(QDataStream &s, const HistoryPlaylist &p); +QDataStream &operator>>(QDataStream &s, HistoryPlaylist &p); + +#endif diff --git a/juk/juk.cpp b/juk/juk.cpp new file mode 100644 index 00000000..a3e0988c --- /dev/null +++ b/juk/juk.cpp @@ -0,0 +1,478 @@ +/*************************************************************************** + begin : Mon Feb 4 23:40:41 EST 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <config.h> + +#include <kcmdlineargs.h> +#include <kstatusbar.h> +#include <kdebug.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> + + +#include "juk.h" +#include "slideraction.h" +#include "statuslabel.h" +#include "splashscreen.h" +#include "systemtray.h" +#include "keydialog.h" +#include "tagguesserconfigdlg.h" +#include "filerenamerconfigdlg.h" +#include "actioncollection.h" +#include "cache.h" +#include "playlistsplitter.h" +#include "collectionlist.h" +#include "covermanager.h" +#include "tagtransactionmanager.h" + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +JuK::JuK(QWidget *parent, const char *name) : + KMainWindow(parent, name, WDestructiveClose), + m_player(PlayerManager::instance()), + m_shuttingDown(false) +{ + // Expect segfaults if you change this order. + + readSettings(); + + if(m_showSplash && !m_startDocked && Cache::cacheFileExists()) { + SplashScreen::instance()->show(); + kapp->processEvents(); + } + + setupActions(); + setupLayout(); + + if(QApplication::reverseLayout()) + setupGUI(ToolBar | Save | Create, "jukui-rtl.rc"); + else + setupGUI(ToolBar | Save | Create); + + readConfig(); + setupSystemTray(); + setupGlobalAccels(); + createDirs(); + + SplashScreen::finishedLoading(); + QTimer::singleShot(0, CollectionList::instance(), SLOT(slotCheckCache())); + QTimer::singleShot(0, this, SLOT(slotProcessArgs())); + + m_sliderAction->slotUpdateOrientation(); +} + +JuK::~JuK() +{ + kdDebug(65432) << k_funcinfo << endl; +} + +KActionCollection *JuK::actionCollection() const +{ + return ActionCollection::actions(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void JuK::setupLayout() +{ + new TagTransactionManager(this); + + m_splitter = new PlaylistSplitter(this, "playlistSplitter"); + setCentralWidget(m_splitter); + + m_statusLabel = new StatusLabel(m_splitter->playlist(), statusBar()); + connect(CollectionList::instance(), SIGNAL(signalCollectionChanged()), + m_statusLabel, SLOT(updateData())); + statusBar()->addWidget(m_statusLabel, 1); + PlayerManager::instance()->setStatusLabel(m_statusLabel); + + m_splitter->setFocus(); + resize(750, 500); +} + +void JuK::setupActions() +{ + ActionCollection::actions()->setWidget(this); + + KStdAction::quit(this, SLOT(slotQuit()), actions()); + KStdAction::undo(this, SLOT(slotUndo()), actions()); + KStdAction::cut(kapp, SLOT(cut()), actions()); + KStdAction::copy(kapp, SLOT(copy()), actions()); + KStdAction::paste(kapp, SLOT(paste()), actions()); + KStdAction::clear(kapp, SLOT(clear()), actions()); + KStdAction::selectAll(kapp, SLOT(selectAll()), actions()); + + new KAction(i18n("Remove From Playlist"), "edit_remove", 0, kapp, SLOT(clear()), actions(), "removeFromPlaylist"); + + KActionMenu *actionMenu = new KActionMenu(i18n("&Random Play"), "roll", actions(), "actionMenu"); + actionMenu->setDelayed(false); + + KRadioAction *ka = new KRadioAction(i18n("&Disable Random Play"), "player_playlist", 0, actions(), "disableRandomPlay"); + ka->setExclusiveGroup("randomPlayGroup"); + actionMenu->insert(ka); + + m_randomPlayAction = new KRadioAction(i18n("Use &Random Play"), "roll", 0, actions(), "randomPlay"); + m_randomPlayAction->setExclusiveGroup("randomPlayGroup"); + actionMenu->insert(m_randomPlayAction); + + ka = new KRadioAction(i18n("Use &Album Random Play"), "roll", 0, actions(), "albumRandomPlay"); + ka->setExclusiveGroup("randomPlayGroup"); + connect(ka, SIGNAL(toggled(bool)), SLOT(slotCheckAlbumNextAction(bool))); + actionMenu->insert(ka); + + new KAction(i18n("&Play"), "player_play", 0, m_player, SLOT(play()), actions(), "play"); + new KAction(i18n("P&ause"), "player_pause", 0, m_player, SLOT(pause()), actions(), "pause"); + new KAction(i18n("&Stop"), "player_stop", 0, m_player, SLOT(stop()), actions(), "stop"); + + new KToolBarPopupAction(i18n("previous track", "Previous"), "player_start", KShortcut(), m_player, SLOT(back()), actions(), "back"); + new KAction(i18n("next track", "&Next"), "player_end", KShortcut(), m_player, SLOT(forward()), actions(), "forward"); + new KToggleAction(i18n("&Loop Playlist"), 0, KShortcut(), actions(), "loopPlaylist"); + KToggleAction *resizeColumnAction = + new KToggleAction(i18n("&Resize Playlist Columns Manually"), + KShortcut(), actions(), "resizeColumnsManually"); + resizeColumnAction->setCheckedState(i18n("&Resize Column Headers Automatically")); + + // the following are not visible by default + + new KAction(i18n("Mute"), "mute", 0, m_player, SLOT(mute()), actions(), "mute"); + new KAction(i18n("Volume Up"), "volumeUp", 0, m_player, SLOT(volumeUp()), actions(), "volumeUp"); + new KAction(i18n("Volume Down"), "volumeDown", 0, m_player, SLOT(volumeDown()), actions(), "volumeDown"); + new KAction(i18n("Play / Pause"), "playPause", 0, m_player, SLOT(playPause()), actions(), "playPause"); + new KAction(i18n("Seek Forward"), "seekForward", 0, m_player, SLOT(seekForward()), actions(), "seekForward"); + new KAction(i18n("Seek Back"), "seekBack", 0, m_player, SLOT(seekBack()), actions(), "seekBack"); + + ////////////////////////////////////////////////// + // settings menu + ////////////////////////////////////////////////// + + m_toggleSplashAction = + new KToggleAction(i18n("Show Splash Screen on Startup"), + KShortcut(), actions(), "showSplashScreen"); + m_toggleSplashAction->setCheckedState(i18n("Hide Splash Screen on Startup")); + m_toggleSystemTrayAction = + new KToggleAction(i18n("&Dock in System Tray"), + KShortcut(), actions(), "toggleSystemTray"); + m_toggleDockOnCloseAction = + new KToggleAction(i18n("&Stay in System Tray on Close"), + KShortcut(), actions(), "dockOnClose"); + m_togglePopupsAction = + new KToggleAction(i18n("Popup &Track Announcement"), + KShortcut(), this, 0, actions(), "togglePopups"); + new KToggleAction(i18n("Save &Play Queue on Exit"), + KShortcut(), this, 0, actions(), "saveUpcomingTracks"); + + connect(m_toggleSystemTrayAction, SIGNAL(toggled(bool)), + this, SLOT(slotToggleSystemTray(bool))); + + + m_outputSelectAction = PlayerManager::playerSelectAction(actions()); + + if(m_outputSelectAction) + m_outputSelectAction->setCurrentItem(0); + + new KAction(i18n("&Tag Guesser..."), 0, 0, this, SLOT(slotConfigureTagGuesser()), + actions(), "tagGuesserConfig"); + + new KAction(i18n("&File Renamer..."), 0, 0, this, SLOT(slotConfigureFileRenamer()), + actions(), "fileRenamerConfig"); + + KStdAction::keyBindings(this, SLOT(slotEditKeys()), actions()); + + ////////////////////////////////////////////////// + // just in the toolbar + ////////////////////////////////////////////////// + + m_sliderAction = new SliderAction(i18n("Track Position"), actions(), + "trackPositionAction"); +} + +void JuK::setupSystemTray() +{ + if(m_toggleSystemTrayAction && m_toggleSystemTrayAction->isChecked()) { + m_systemTray = new SystemTray(this, "systemTray"); + m_systemTray->show(); + + m_toggleDockOnCloseAction->setEnabled(true); + m_togglePopupsAction->setEnabled(true); + + connect(m_systemTray, SIGNAL(quitSelected()), this, SLOT(slotAboutToQuit())); + } + else { + m_systemTray = 0; + m_toggleDockOnCloseAction->setEnabled(false); + m_togglePopupsAction->setEnabled(false); + } +} + +void JuK::setupGlobalAccels() +{ + m_accel = new KGlobalAccel(this); + + KeyDialog::insert(m_accel, "Play", i18n("Play"), action("play"), SLOT(activate())); + KeyDialog::insert(m_accel, "PlayPause", i18n("Play / Pause"), action("playPause"), SLOT(activate())); + KeyDialog::insert(m_accel, "Stop", i18n("Stop Playing"), action("stop"), SLOT(activate())); + KeyDialog::insert(m_accel, "Back", i18n("Back"), action("back"), SLOT(activate())); + KeyDialog::insert(m_accel, "Forward", i18n("Forward"), action("forward"), SLOT(activate())); + KeyDialog::insert(m_accel, "SeekBack", i18n("Seek Back"), action("seekBack"), SLOT(activate())); + KeyDialog::insert(m_accel, "SeekForward", i18n("Seek Forward"), action("seekForward"), SLOT(activate())); + KeyDialog::insert(m_accel, "VolumeUp", i18n("Volume Up"), action("volumeUp"), SLOT(activate())); + KeyDialog::insert(m_accel, "VolumeDown", i18n("Volume Down"), action("volumeDown"), SLOT(activate())); + KeyDialog::insert(m_accel, "Mute", i18n("Mute"), action("mute"), SLOT(activate())); + KeyDialog::insert(m_accel, "ShowHide", i18n("Show / Hide"), this, SLOT(slotShowHide())); + KeyDialog::insert(m_accel, "ForwardAlbum", i18n("Play Next Album"), action("forwardAlbum"), SLOT(activate())); + + m_accel->setConfigGroup("Shortcuts"); + m_accel->readSettings(); + m_accel->updateConnections(); +} + +void JuK::slotProcessArgs() +{ + KCmdLineArgs *args = KCmdLineArgs::parsedArgs(); + QStringList files; + + for(int i = 0; i < args->count(); i++) + files.append(args->arg(i)); + + CollectionList::instance()->addFiles(files); +} + +void JuK::createDirs() +{ + QDir dir(KGlobal::dirs()->saveLocation("data", kapp->instanceName() + '/')); + if (!dir.exists("covers", false)) + dir.mkdir("covers", false); + dir.cd("covers"); + if (!dir.exists("large", false)) + dir.mkdir("large", false); +} + +void JuK::keyPressEvent(QKeyEvent *e) +{ + if (e->key() >= Qt::Key_Back && e->key() <= Qt::Key_MediaLast) + e->accept(); + KMainWindow::keyPressEvent(e); +} + +/** + * These are settings that need to be know before setting up the GUI. + */ + +void JuK::readSettings() +{ + KConfigGroup config(KGlobal::config(), "Settings"); + m_showSplash = config.readBoolEntry("ShowSplashScreen", true); + m_startDocked = config.readBoolEntry("StartDocked", false); +} + +void JuK::readConfig() +{ + // player settings + + KConfigGroup playerConfig(KGlobal::config(), "Player"); + + if(m_sliderAction->volumeSlider()) { + int maxVolume = m_sliderAction->volumeSlider()->maxValue(); + int volume = playerConfig.readNumEntry("Volume", maxVolume); + m_sliderAction->volumeSlider()->setVolume(volume); + } + + // Default to no random play + + ActionCollection::action<KToggleAction>("disableRandomPlay")->setChecked(true); + + QString randomPlayMode = playerConfig.readEntry("RandomPlay", "Disabled"); + if(randomPlayMode == "true" || randomPlayMode == "Normal") + m_randomPlayAction->setChecked(true); + else if(randomPlayMode == "AlbumRandomPlay") + ActionCollection::action<KToggleAction>("albumRandomPlay")->setChecked(true); + + bool loopPlaylist = playerConfig.readBoolEntry("LoopPlaylist", false); + ActionCollection::action<KToggleAction>("loopPlaylist")->setChecked(loopPlaylist); + + // general settings + + KConfigGroup settingsConfig(KGlobal::config(), "Settings"); + + bool dockInSystemTray = settingsConfig.readBoolEntry("DockInSystemTray", true); + m_toggleSystemTrayAction->setChecked(dockInSystemTray); + + bool dockOnClose = settingsConfig.readBoolEntry("DockOnClose", true); + m_toggleDockOnCloseAction->setChecked(dockOnClose); + + bool showPopups = settingsConfig.readBoolEntry("TrackPopup", false); + m_togglePopupsAction->setChecked(showPopups); + + if(m_outputSelectAction) + m_outputSelectAction->setCurrentItem(settingsConfig.readNumEntry("MediaSystem", 0)); + + m_toggleSplashAction->setChecked(m_showSplash); +} + +void JuK::saveConfig() +{ + // player settings + + KConfigGroup playerConfig(KGlobal::config(), "Player"); + + if (m_sliderAction->volumeSlider()) + { + playerConfig.writeEntry("Volume", m_sliderAction->volumeSlider()->volume()); + } + + playerConfig.writeEntry("RandomPlay", m_randomPlayAction->isChecked()); + + KToggleAction *a = ActionCollection::action<KToggleAction>("loopPlaylist"); + playerConfig.writeEntry("LoopPlaylist", a->isChecked()); + + a = ActionCollection::action<KToggleAction>("albumRandomPlay"); + if(a->isChecked()) + playerConfig.writeEntry("RandomPlay", "AlbumRandomPlay"); + else if(m_randomPlayAction->isChecked()) + playerConfig.writeEntry("RandomPlay", "Normal"); + else + playerConfig.writeEntry("RandomPlay", "Disabled"); + + // general settings + + KConfigGroup settingsConfig(KGlobal::config(), "Settings"); + settingsConfig.writeEntry("ShowSplashScreen", m_toggleSplashAction->isChecked()); + settingsConfig.writeEntry("StartDocked", m_startDocked); + settingsConfig.writeEntry("DockInSystemTray", m_toggleSystemTrayAction->isChecked()); + settingsConfig.writeEntry("DockOnClose", m_toggleDockOnCloseAction->isChecked()); + settingsConfig.writeEntry("TrackPopup", m_togglePopupsAction->isChecked()); + if(m_outputSelectAction) + settingsConfig.writeEntry("MediaSystem", m_outputSelectAction->currentItem()); + + KGlobal::config()->sync(); +} + +bool JuK::queryExit() +{ + m_startDocked = !isVisible(); + + kdDebug(65432) << k_funcinfo << endl; + + hide(); + + action("stop")->activate(); + delete m_systemTray; + m_systemTray = 0; + + CoverManager::shutdown(); + Cache::instance()->save(); + saveConfig(); + + delete m_splitter; + m_splitter = 0; + return true; +} + +bool JuK::queryClose() +{ + kdDebug(65432) << k_funcinfo << endl; + + if(!m_shuttingDown && + !kapp->sessionSaving() && + m_systemTray && + m_toggleDockOnCloseAction->isChecked()) + { + KMessageBox::information(this, + i18n("<qt>Closing the main window will keep JuK running in the system tray. " + "Use Quit from the File menu to quit the application.</qt>"), + i18n("Docking in System Tray"), "hideOnCloseInfo"); + hide(); + return false; + } + else + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// private slot definitions +//////////////////////////////////////////////////////////////////////////////// + +void JuK::slotShowHide() +{ + setShown(!isShown()); +} + +void JuK::slotAboutToQuit() +{ + m_shuttingDown = true; +} + +void JuK::slotQuit() +{ + kdDebug(65432) << k_funcinfo << endl; + m_shuttingDown = true; + + kapp->quit(); +} + +//////////////////////////////////////////////////////////////////////////////// +// settings menu +//////////////////////////////////////////////////////////////////////////////// + +void JuK::slotToggleSystemTray(bool enabled) +{ + if(enabled && !m_systemTray) + setupSystemTray(); + else if(!enabled && m_systemTray) { + delete m_systemTray; + m_systemTray = 0; + m_toggleDockOnCloseAction->setEnabled(false); + m_togglePopupsAction->setEnabled(false); + } +} + +void JuK::slotEditKeys() +{ + KeyDialog::configure(m_accel, actions(), this); +} + +void JuK::slotConfigureTagGuesser() +{ + TagGuesserConfigDlg(this).exec(); +} + +void JuK::slotConfigureFileRenamer() +{ + FileRenamerConfigDlg(this).exec(); +} + +void JuK::slotUndo() +{ + TagTransactionManager::instance()->undo(); +} + +void JuK::slotCheckAlbumNextAction(bool albumRandomEnabled) +{ + // If album random play is enabled, then enable the Play Next Album action + // unless we're not playing right now. + + if(albumRandomEnabled && !m_player->playing()) + albumRandomEnabled = false; + + action("forwardAlbum")->setEnabled(albumRandomEnabled); +} + +#include "juk.moc" diff --git a/juk/juk.desktop b/juk/juk.desktop new file mode 100644 index 00000000..b7734c31 --- /dev/null +++ b/juk/juk.desktop @@ -0,0 +1,72 @@ +# KDE Config File +[Desktop Entry] +Type=Application +Exec=juk -caption "%c" %i %m +Icon=juk +DocPath=juk/index.html +Comment= +Terminal=false +Name=JuK +Name[bn]=জà§à¦• +Name[hi]=जà¥à¤¯à¥‚क +Name[sv]=Juk +Name[tr]=Juk +GenericName=Music Player +GenericName[ar]=مشغّل موسيقى +GenericName[bg]=Плеър за музикални файлове +GenericName[br]=C'hoarier ar sonerezh +GenericName[bs]=Sviranje muzike +GenericName[ca]=Reproductor musical +GenericName[cs]=PÅ™ehrávaÄ hudby +GenericName[cy]=Chwaraewr Cerdd +GenericName[da]=Musikafspiller +GenericName[de]=Audio-Wiedergabe +GenericName[el]=ΑναπαÏαγωγÎας μουσικής +GenericName[eo]=Muzikludilo +GenericName[es]=Reproductor de audio +GenericName[et]=Muusika mängija +GenericName[eu]=Musika erreproduzigailua +GenericName[fa]=پخش‌کنندۀ موسیقی +GenericName[fi]=Musiikkisoitin +GenericName[fr]=Lecteur multimédia +GenericName[ga]=Seinnteoir Ceoil +GenericName[gl]=Reproductor de Música +GenericName[he]=× ×’×Ÿ מוזיקה +GenericName[hi]=मà¥à¤¯à¥‚ज़िक पà¥à¤²à¥‡à¤¯à¤° +GenericName[hu]=Zenelejátszó +GenericName[is]=Tónlistarforrit +GenericName[it]=Lettore musicale +GenericName[ja]=ミュージックプレーヤ +GenericName[kk]=Музыка ойнатқышы +GenericName[km]=កម្មវិធី​ចាក់​ážáž“្ážáŸ’រី +GenericName[ko]=ìŒì•… 재ìƒê¸° +GenericName[lt]=Muzikos grotuvas +GenericName[mk]=Изведувач на музика +GenericName[nb]=Musikkavspiller +GenericName[nds]=Musikafspeler +GenericName[ne]=सङà¥à¤—ित पà¥à¤²à¥‡à¤¯à¤° +GenericName[nl]=Muziekspeler +GenericName[nn]=Musikkspelar +GenericName[pa]=ਸੰਗੀਤ ਵਾਜਾ +GenericName[pl]=Odtwarzacz muzyki +GenericName[pt]=Leitor de Música +GenericName[pt_BR]=Músicas +GenericName[ro]=Program de redare muzică +GenericName[ru]=Проигрыватель +GenericName[sk]=PrehrávaÄ hudby +GenericName[sl]=Glasbeni predvajalnik +GenericName[sr]=Музички плејер +GenericName[sr@Latn]=MuziÄki plejer +GenericName[sv]=Musikspelare +GenericName[ta]=இசை இயகà¯à®•à®¿ +GenericName[tg]=Бозингари МуÑиқӣ +GenericName[th]=โปรà¹à¸à¸£à¸¡à¹€à¸¥à¹ˆà¸™à¸”นตรี +GenericName[tr]=Müzik Çalar +GenericName[uk]=Програвач музики +GenericName[uz]=Musiqa pleyer +GenericName[uz@cyrillic]=МуÑиқа плейер +GenericName[zh_CN]=音ä¹æ’放器 +GenericName[zh_HK]=音樂æ’放器 +GenericName[zh_TW]=音樂æ’放器 +MimeType=application/x-ogg;audio/mpeg;audio/mpegurl;audio/vorbis;audio/x-adpcm;audio/x-flac;audio/x-matroska;audio/x-mp2;audio/x-mp3;audio/x-mpegurl;audio/x-musepack;audio/x-oggflac;audio/x-speex;audio/x-vorbis;audio/x-wav; +Categories=Qt;KDE;AudioVideo; diff --git a/juk/juk.h b/juk/juk.h new file mode 100644 index 00000000..41f3150a --- /dev/null +++ b/juk/juk.h @@ -0,0 +1,100 @@ +/*************************************************************************** + begin : Mon Feb 4 23:40:41 EST 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef JUK_H +#define JUK_H + +#include <kmainwindow.h> + +#include "playermanager.h" + +class QTimer; +class QListViewItem; + +class KToggleAction; +class KSelectAction; +class KToolBarPopupAction; +class KGlobalAccel; + +class SliderAction; +class StatusLabel; +class SystemTray; +class PlayerManager; +class PlaylistSplitter; + +class JuK : public KMainWindow +{ + Q_OBJECT + +public: + JuK(QWidget* parent = 0, const char *name = 0); + virtual ~JuK(); + virtual KActionCollection *actionCollection() const; + +private: + void setupLayout(); + void setupActions(); + void setupSystemTray(); + void setupGlobalAccels(); + void createDirs(); + + void keyPressEvent(QKeyEvent *); + + /** + * readSettings() is separate from readConfig() in that it contains settings + * that need to be read before the GUI is setup. + */ + void readSettings(); + void readConfig(); + void saveConfig(); + + virtual bool queryExit(); + virtual bool queryClose(); + +private slots: + void slotShowHide(); + void slotAboutToQuit(); + void slotQuit(); + void slotToggleSystemTray(bool enabled); + void slotEditKeys(); + void slotConfigureTagGuesser(); + void slotConfigureFileRenamer(); + void slotUndo(); + void slotCheckAlbumNextAction(bool albumRandomEnabled); + void slotProcessArgs(); + +private: + PlaylistSplitter *m_splitter; + StatusLabel *m_statusLabel; + SystemTray *m_systemTray; + + SliderAction *m_sliderAction; + KToggleAction *m_randomPlayAction; + KToggleAction *m_toggleSystemTrayAction; + KToggleAction *m_toggleDockOnCloseAction; + KToggleAction *m_togglePopupsAction; + KToggleAction *m_toggleSplashAction; + KToggleAction *m_loopPlaylistAction; + KSelectAction *m_outputSelectAction; + + PlayerManager *m_player; + KGlobalAccel *m_accel; + + bool m_startDocked; + bool m_showSplash; + bool m_shuttingDown; +}; + +#endif diff --git a/juk/jukIface.h b/juk/jukIface.h new file mode 100644 index 00000000..e119b25e --- /dev/null +++ b/juk/jukIface.h @@ -0,0 +1,108 @@ +#ifndef JUKIFACE_H +#define JUKIFACE_H + +#include <dcopobject.h> +#include <qstringlist.h> +#include <qpixmap.h> + +class CollectionIface : public DCOPObject +{ + K_DCOP +k_dcop: + void openFile(const QString &file) { open(file); } + void openFile(const QStringList &files) { open(files); } + void openFile(const QString &playlist, const QString &file) { open(playlist, file); } + void openFile(const QString &playlist, const QStringList &files) { open(playlist, files); } + + virtual QStringList playlists() const = 0; + virtual void createPlaylist(const QString &) = 0; + virtual void remove() = 0; + + virtual void removeTrack(const QString &playlist, const QString &file) { removeTrack(playlist, QStringList(file)); } + virtual void removeTrack(const QString &playlist, const QStringList &files) = 0; + + virtual QString playlist() const = 0; + virtual QString playingPlaylist() const = 0; + virtual void setPlaylist(const QString &playlist) = 0; + + virtual QStringList playlistTracks(const QString &playlist) const = 0; + virtual QString trackProperty(const QString &file, const QString &property) const = 0; + + virtual QPixmap trackCover(const QString &file, const QString &size = "Small") const = 0; + +protected: + CollectionIface() : DCOPObject("Collection") {} + virtual void open(const QStringList &files) = 0; + virtual void open(const QString &playlist, const QStringList &files) = 0; +}; + +class PlayerIface : public DCOPObject +{ + K_DCOP +k_dcop: + virtual bool playing() const = 0; + virtual bool paused() const = 0; + virtual float volume() const = 0; + virtual int status() const = 0; + + virtual QStringList trackProperties() = 0; + virtual QString trackProperty(const QString &property) const = 0; + virtual QPixmap trackCover(const QString &size = "Small") const = 0; + + virtual QString currentFile() const + { + return trackProperty("Path"); + } + + virtual void play() = 0; + virtual void play(const QString &file) = 0; + virtual void pause() = 0; + virtual void stop() = 0; + virtual void playPause() = 0; + + virtual void back() = 0; + virtual void forward() = 0; + virtual void seekBack() = 0; + virtual void seekForward() = 0; + + virtual void volumeUp() = 0; + virtual void volumeDown() = 0; + virtual void mute() = 0; + virtual void setVolume(float volume) = 0; + virtual void seek(int time) = 0; + + virtual QString playingString() const = 0; + virtual int currentTime() const = 0; + virtual int totalTime() const = 0; + + /** + * @return The current player mode. + * @see setRandomPlayMode() + */ + virtual QString randomPlayMode() const = 0; + + /** + * Sets the player mode to one of normal, random play, or album + * random play, depending on @p randomMode. + * "NoRandom" -> Normal + * "Random" -> Random + * "AlbumRandom" -> Album Random + */ + virtual void setRandomPlayMode(const QString &randomMode) = 0; + +protected: + PlayerIface() : DCOPObject("Player") {} +}; + +class SearchIface : public DCOPObject +{ + K_DCOP +k_dcop: + virtual QString searchText() const = 0; + virtual void setSearchText(const QString &text) = 0; + +protected: + SearchIface() : DCOPObject("Search") {} +}; + +#endif diff --git a/juk/jukservicemenu.desktop b/juk/jukservicemenu.desktop new file mode 100644 index 00000000..942a6a45 --- /dev/null +++ b/juk/jukservicemenu.desktop @@ -0,0 +1,63 @@ +[Desktop Entry] +ServiceTypes=application/ogg,audio/vorbis,audio/x-mp3,audio/x-flac,audio/x-oggflac,audio/x-musepack +Actions=addToCollection + +[Desktop Action addToCollection] +Name=Add to JuK Collection +Name[bn]=জà§à¦• সংকলনে যোগ করো +Name[br]=Ouzhpennañ d'an dastumad JuK +Name[bs]=Dodaj u JuK kolekciju +Name[ca]=Afegeix a la col·lecció JuK +Name[cs]=PÅ™idat do JuK kolekce +Name[cy]=Ychwanegu i Gasgliad JuK +Name[da]=Tilføj til JuK-samling +Name[de]=Zur JuK-Kollektion hinzufügen +Name[el]=Î Ïοσθήκη στη συλλογή του JuK +Name[eo]=Aldoni al JuK-kolekto +Name[es]=Añadir a colección de JuK +Name[et]=Lisa JuKi kollektsiooni +Name[eu]=Gehitu Juk-en bildumara +Name[fa]=اÙزودن به مجموعۀ JuK +Name[fi]=Lisää JuKin kokoelmalistaan +Name[fr]=Ajouter à la collection de JuK +Name[ga]=Cuir le bailiúchán JuK +Name[gl]=Engadir á colección de JuK +Name[he]=הוסף ל×וסף של Juk +Name[hi]=जà¥à¤¯à¥‚क संगà¥à¤°à¤¹ में जोड़ें +Name[hu]=Hozzáadás egy JuK-válogatáshoz +Name[is]=Bæta við JuK safnið +Name[it]=Aggiungi alla collezione di JuK +Name[ja]=JuK コレクションã«è¿½åŠ +Name[kk]=JuK жинағына қоÑу +Name[km]=បន្ážáŸ‚ម​ទៅ​ការ​ប្រមូលផ្ដុំ JuK +Name[ko]=JuK 모ìŒì§‘ì— ì¶”ê°€í•˜ê¸° +Name[lt]=PridÄ—ti prie JuK kolekcijos +Name[mk]=Додај во колекција на JuK +Name[ms]=Tambah ke koleksi JuK +Name[nb]=Legg til JuK-samling +Name[nds]=Na de JuK-Sammeln tofögen +Name[ne]=JuK सङà¥à¤•à¤²à¤¨à¤®à¤¾ थपà¥à¤¨à¥à¤¹à¥‹à¤¸à¥ +Name[nl]=Toevoegen aan JuK-collectie +Name[nn]=Legg til JuK-samlinga +Name[pa]=JuK à¨à©°à¨¡à¨¾à¨° 'ਚ ਸ਼ਾਮਿਲ +Name[pl]=Dodaj do kolekcji JuK +Name[pt]=Adicionar à Colecção do JuK +Name[pt_BR]=Adicionar à Coleção do Juk +Name[ro]=Adaugă în colecÅ£ia JuK +Name[ru]=Добавить в коллекцию JuK +Name[sk]=PridaÅ¥ do kolekcie JuK +Name[sl]=Dodaj v zbirko JuK +Name[sr]=Додај у JuK колекцију +Name[sr@Latn]=Dodaj u JuK kolekciju +Name[sv]=Lägg till i Juk-samlingslista +Name[ta]=JuK திரடà¯à®Ÿà®¿à®•à¯à®•à¯ சேர௠+Name[tg]=Иловакунӣ ба Маҷмӯаи JuK +Name[th]=เพิ่มลงชุดสะสมขà¸à¸‡ JuK +Name[tr]=JuK Koleksiyonuna Ekle +Name[uk]=Додати до збірки JuK +Name[uz]=JuK toÊ»plamiga qoÊ»shish +Name[uz@cyrillic]=JuK тўпламига қўшиш +Name[zh_CN]=æ·»åŠ åˆ° JuK æ”¶è— +Name[zh_TW]=新增到 JuK 收è—清單 +Icon=juk +Exec=dcop juk Collection "openFile(QStringList)" [ %U ] diff --git a/juk/jukui-rtl.rc b/juk/jukui-rtl.rc new file mode 100644 index 00000000..d8eb65ae --- /dev/null +++ b/juk/jukui-rtl.rc @@ -0,0 +1,109 @@ +<!DOCTYPE kpartgui> +<kpartgui name="juk" version="8"> +<MenuBar> + <Menu name="file" noMerge="1"><text>&File</text> + <Action name="file_new"/> + <Action name="file_open"/> + <Action name="openDirectory"/> + + <Separator/> + + <Action name="renamePlaylist"/> + <Action name="editSearch"/> + <Action name="duplicatePlaylist"/> + <Action name="reloadPlaylist"/> + <Action name="deleteItemPlaylist"/> + + <Separator/> + + <Action name="file_save"/> + <Action name="file_save_as"/> + + <Separator/> + + <Action name="file_quit"/> + </Menu> + <Menu name="view" noMerge="1"><text>&View</text> + <Action name="showSearch"/> + <Action name="showEditor"/> + <Action name="showHistory"/> + <Action name="showUpcoming"/> + <Action name="showColumns"/> + <Action name="resizeColumnsManually"/> + + <Separator/> + + <Action name="viewModeMenu"/> + </Menu> + <Menu name="player"><text>&Player</text> + <Action name="actionMenu"/> + + <Action name="loopPlaylist"/> + + <Separator/> + + <Action name="play"/> + <Action name="pause"/> + <Action name="stop"/> + <Action name="forward"/> + <Action name="back"/> + + <Separator/> + + <Action name="forwardAlbum"/> + </Menu> + <Menu name="playlist"><text>&Tagger</text> + <Action name="saveItem"/> + <Action name="removeItem"/> + <Action name="refresh"/> + + <Separator/> + + <Action name="guessTag"/> + <Action name="coverManager"/> + <Action name="renameFile"/> + </Menu> + <Menu name="settings"><text>&Settings</text> + <Action name="showSplashScreen" append="show_merge"/> + <Action name="toggleSystemTray" append="show_merge"/> + <Action name="dockOnClose" append="show_merge"/> + <Action name="togglePopups" append="show_merge"/> + <!-- <Action name="saveUpcomingTracks" append="show_merge"/> --> + <Action name="tagGuesserConfig" append="save_merge"/> + <Action name="fileRenamerConfig" append="save_merge"/> + <Action name="outputSelect" append="save_merge"/> + </Menu> +</MenuBar> + +<ToolBar name="mainToolBar" hidden="true" noMerge="1"><text>Main Toolbar</text> + <Action name="file_new"/> + <Action name="file_open"/> + <Action name="file_save"/> + + <Separator lineSeparator="true"/> + + <Action name="edit_cut"/> + <Action name="edit_copy"/> + <Action name="edit_paste"/> + + <Separator lineSeparator="true"/> + + <Action name="showSearch"/> + <Action name="showEditor"/> + <Action name="showNowPlaying"/> +</ToolBar> + +<ToolBar name="playToolBar" noMerge="1"><text>Play Toolbar</text> + + <Action name="trackPositionAction"/> + <Action name="forward"/> + <Action name="back"/> + <Separator lineSeparator="false"/> + + + <Action name="stop"/> + <Action name="pause"/> + <Action name="play"/> + +</ToolBar> +</kpartgui> diff --git a/juk/jukui.rc b/juk/jukui.rc new file mode 100644 index 00000000..453739da --- /dev/null +++ b/juk/jukui.rc @@ -0,0 +1,109 @@ +<!-- PLEASE UPDATE jukui-rtl.rc WHEN UPDATING THIS FILE --> + +<!DOCTYPE kpartgui> +<kpartgui name="juk" version="8"> +<MenuBar> + <Menu name="file" noMerge="1"><text>&File</text> + <Action name="file_new"/> + <Action name="file_open"/> + <Action name="openDirectory"/> + + <Separator/> + + <Action name="renamePlaylist"/> + <Action name="editSearch"/> + <Action name="duplicatePlaylist"/> + <Action name="reloadPlaylist"/> + <Action name="deleteItemPlaylist"/> + + <Separator/> + + <Action name="file_save"/> + <Action name="file_save_as"/> + + <Separator/> + + <Action name="file_quit"/> + </Menu> + <Menu name="view" noMerge="1"><text>&View</text> + <Action name="showSearch"/> + <Action name="showEditor"/> + <Action name="showHistory"/> + <Action name="showUpcoming"/> + <Action name="showColumns"/> + <Action name="resizeColumnsManually"/> + + <Separator/> + + <Action name="viewModeMenu"/> + </Menu> + <Menu name="player"><text>&Player</text> + <Action name="actionMenu"/> + + <Action name="loopPlaylist"/> + + <Separator/> + + <Action name="play"/> + <Action name="pause"/> + <Action name="stop"/> + <Action name="forward"/> + <Action name="back"/> + + <Separator/> + + <Action name="forwardAlbum"/> + </Menu> + <Menu name="playlist"><text>&Tagger</text> + <Action name="saveItem"/> + <Action name="removeItem"/> + <Action name="refresh"/> + + <Separator/> + + <Action name="guessTag"/> + <Action name="coverManager"/> + <Action name="renameFile"/> + </Menu> + <Menu name="settings"><text>&Settings</text> + <Action name="showSplashScreen" append="show_merge"/> + <Action name="toggleSystemTray" append="show_merge"/> + <Action name="dockOnClose" append="show_merge"/> + <Action name="togglePopups" append="show_merge"/> + <!-- <Action name="saveUpcomingTracks" append="show_merge"/> --> + <Action name="tagGuesserConfig" append="save_merge"/> + <Action name="fileRenamerConfig" append="save_merge"/> + <Action name="outputSelect" append="save_merge"/> + </Menu> +</MenuBar> + +<ToolBar name="mainToolBar" hidden="true" noMerge="1"><text>Main Toolbar</text> + <Action name="file_new"/> + <Action name="file_open"/> + <Action name="file_save"/> + + <Separator lineSeparator="true"/> + + <Action name="edit_cut"/> + <Action name="edit_copy"/> + <Action name="edit_paste"/> + + <Separator lineSeparator="true"/> + + <Action name="showSearch"/> + <Action name="showEditor"/> + <Action name="showNowPlaying"/> +</ToolBar> + +<ToolBar name="playToolBar" noMerge="1"><text>Play Toolbar</text> + <Action name="play"/> + <Action name="pause"/> + <Action name="stop"/> + + <Separator lineSeparator="false"/> + + <Action name="back"/> + <Action name="forward"/> + <Action name="trackPositionAction"/> +</ToolBar> +</kpartgui> diff --git a/juk/k3bexporter.cpp b/juk/k3bexporter.cpp new file mode 100644 index 00000000..a08ddcfe --- /dev/null +++ b/juk/k3bexporter.cpp @@ -0,0 +1,298 @@ +/*************************************************************************** + begin : Mon May 31 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kprocess.h> +#include <kmessagebox.h> +#include <kurl.h> +#include <klocale.h> +#include <kaction.h> +#include <kactioncollection.h> +#include <kstandarddirs.h> +#include <kiconloader.h> +#include <kapplication.h> + +#include <qcstring.h> +#include <qmap.h> + +#include <dcopref.h> +#include <dcopclient.h> + +#include "k3bexporter.h" +#include "playlistitem.h" +#include "playlist.h" +#include "playlistbox.h" +#include "actioncollection.h" + +using ActionCollection::actions; + +// static member variable definition + +PlaylistAction *K3bExporter::m_action = 0; + +// Special KAction subclass used to automatically call a slot when activated, +// depending on the visible playlist at the time. In other words, use *one* +// instance of this action for many playlists. +// +// This is used to handle some actions in the Playlist context menu. +class PlaylistAction : public KAction +{ + public: + PlaylistAction(const char *name, + const QString &userText, + const QIconSet &pix, + const char *slot, + const KShortcut &cut = 0) : + KAction(userText, pix, cut, 0 /* receiver */, 0 /* slot */, actions(), name), + m_slot(slot) + { + } + + typedef QMap<const Playlist *, QObject *> PlaylistRecipientMap; + + /** + * Defines a QObject to call (using the m_slot SLOT) when an action is + * emitted from a Playlist. + */ + void addCallMapping(const Playlist *p, QObject *obj) + { + m_playlistRecipient[p] = obj; + } + + protected slots: + void slotActivated() + { + kdDebug(65432) << k_funcinfo << endl; + + // Determine current playlist, and call its slot. + Playlist *p = PlaylistCollection::instance()->visiblePlaylist(); + if(!p) + return; + + // Make sure we're supposed to notify someone about this playlist. + QObject *recipient = m_playlistRecipient[p]; + if(!recipient) + return; + + // Invoke the slot using some trickery. + // XXX: Use the QMetaObject to do this in Qt 4. + connect(this, SIGNAL(activated()), recipient, m_slot); + emit(activated()); + disconnect(this, SIGNAL(activated()), recipient, m_slot); + } + + private: + QCString m_slot; + PlaylistRecipientMap m_playlistRecipient; +}; + +K3bExporter::K3bExporter(Playlist *parent) : PlaylistExporter(parent), m_parent(parent) +{ +} + +KAction *K3bExporter::action() +{ + if(!m_action && !KStandardDirs::findExe("k3b").isNull()) { + m_action = new PlaylistAction( + "export_to_k3b", + i18n("Add Selected Items to Audio or Data CD"), + SmallIconSet("k3b"), + SLOT(slotExport()) + ); + + m_action->setShortcutConfigurable(false); + } + + // Tell the action to let us know when it is activated when + // m_parent is the visible playlist. This allows us to reuse the + // action to avoid duplicate entries in KActionCollection. + if(m_action) + m_action->addCallMapping(m_parent, this); + + return m_action; +} + +void K3bExporter::exportPlaylistItems(const PlaylistItemList &items) +{ + if(items.empty()) + return; + + DCOPClient *client = DCOPClient::mainClient(); + QCString appId, appObj; + QByteArray data; + + if(!client->findObject("k3b-*", "K3bInterface", "", data, appId, appObj)) + exportViaCmdLine(items); + else { + DCOPRef ref(appId, appObj); + exportViaDCOP(items, ref); + } +} + +void K3bExporter::slotExport() +{ + if(m_parent) + exportPlaylistItems(m_parent->selectedItems()); +} + +void K3bExporter::exportViaCmdLine(const PlaylistItemList &items) +{ + K3bOpenMode mode = openMode(); + QCString cmdOption; + + switch(mode) { + case AudioCD: + cmdOption = "--audiocd"; + break; + + case DataCD: + cmdOption = "--datacd"; + break; + + case Abort: + return; + } + + KProcess *process = new KProcess; + + *process << "k3b"; + *process << cmdOption; + + PlaylistItemList::ConstIterator it; + for(it = items.begin(); it != items.end(); ++it) + *process << (*it)->file().absFilePath(); + + if(!process->start(KProcess::DontCare)) + KMessageBox::error(m_parent, i18n("Unable to start K3b.")); +} + +void K3bExporter::exportViaDCOP(const PlaylistItemList &items, DCOPRef &ref) +{ + QValueList<DCOPRef> projectList; + DCOPReply projectListReply = ref.call("projects()"); + + if(!projectListReply.get<QValueList<DCOPRef> >(projectList, "QValueList<DCOPRef>")) { + DCOPErrorMessage(); + return; + } + + if(projectList.count() == 0 && !startNewK3bProject(ref)) + return; + + KURL::List urlList; + PlaylistItemList::ConstIterator it; + + for(it = items.begin(); it != items.end(); ++it) { + KURL item; + + item.setPath((*it)->file().absFilePath()); + urlList.append(item); + } + + if(!ref.send("addUrls(KURL::List)", DCOPArg(urlList, "KURL::List"))) { + DCOPErrorMessage(); + return; + } +} + +void K3bExporter::DCOPErrorMessage() +{ + KMessageBox::error(m_parent, i18n("There was a DCOP communication error with K3b.")); +} + +bool K3bExporter::startNewK3bProject(DCOPRef &ref) +{ + QCString request; + K3bOpenMode mode = openMode(); + + switch(mode) { + case AudioCD: + request = "createAudioCDProject()"; + break; + + case DataCD: + request = "createDataCDProject()"; + break; + + case Abort: + return false; + } + + if(!ref.send(request)) { + DCOPErrorMessage(); + return false; + } + + return true; +} + +K3bExporter::K3bOpenMode K3bExporter::openMode() +{ + int reply = KMessageBox::questionYesNoCancel( + m_parent, + i18n("Create an audio mode CD suitable for CD players, or a data " + "mode CD suitable for computers and other digital music " + "players?"), + i18n("Create K3b Project"), + i18n("Audio Mode"), + i18n("Data Mode") + ); + + switch(reply) { + case KMessageBox::Cancel: + return Abort; + + case KMessageBox::No: + return DataCD; + + case KMessageBox::Yes: + return AudioCD; + } + + return Abort; +} + +K3bPlaylistExporter::K3bPlaylistExporter(PlaylistBox *parent) : K3bExporter(0), + m_playlistBox(parent) +{ +} + +KAction *K3bPlaylistExporter::action() +{ + if(!KStandardDirs::findExe("k3b").isNull()) { + return new KAction( + i18n("Add Playlist to Audio or Data CD"), + SmallIconSet("k3b"), + 0, + this, + SLOT(slotExport()), + actions(), + "export_playlist_to_k3b" + ); + } + + return 0; +} + +void K3bPlaylistExporter::slotExport() +{ + if(m_playlistBox) { + setPlaylist(m_playlistBox->visiblePlaylist()); + exportPlaylistItems(m_playlistBox->visiblePlaylist()->items()); + } +} + +#include "k3bexporter.moc" + +// vim: set et sw=4 ts=4: diff --git a/juk/k3bexporter.h b/juk/k3bexporter.h new file mode 100644 index 00000000..7f23fb0e --- /dev/null +++ b/juk/k3bexporter.h @@ -0,0 +1,94 @@ +/*************************************************************************** + begin : Mon May 31 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef K3BEXPORTER_H +#define K3BEXPORTER_H + +#include "playlistexporter.h" +#include "playlistitem.h" + +class QWidget; +class DCOPRef; +class PlaylistBox; +class PlaylistAction; + +/** + * Class that will export the selected items of a playlist to K3b. + */ +class K3bExporter : public PlaylistExporter +{ + Q_OBJECT + +public: + K3bExporter(Playlist *parent = 0); + + /** + * Returns a KAction that can be used to invoke the export. + * + * @return action used to start the export. + */ + virtual KAction *action(); + + Playlist *playlist() const { return m_parent; } + void setPlaylist(Playlist *playlist) { m_parent = playlist; } + +protected: + void exportPlaylistItems(const PlaylistItemList &items); + +private slots: + void slotExport(); + +private: + enum K3bOpenMode { AudioCD, DataCD, Abort }; + + // Private method declarations + void exportViaCmdLine(const PlaylistItemList &items); + void exportViaDCOP(const PlaylistItemList &items, DCOPRef &ref); + void DCOPErrorMessage(); + bool startNewK3bProject(DCOPRef &ref); + K3bOpenMode openMode(); + + // Private member variable declarations + Playlist *m_parent; + + // Static member used to avoid adding more than one action to KDE's + // action list. + static PlaylistAction *m_action; +}; + +/** + * Class to export EVERY item in a playlist to K3b. Used with the PlaylistBox + * class to implement context-menus there. + * + * @see PlaylistBox + */ +class K3bPlaylistExporter : public K3bExporter +{ + Q_OBJECT +public: + K3bPlaylistExporter(PlaylistBox *parent = 0); + + virtual KAction *action(); + +private slots: + void slotExport(); + +private: + PlaylistBox *m_playlistBox; +}; + +#endif /* K3BEXPORTER_H */ + +// vim: set et ts=4 sw=4: diff --git a/juk/keydialog.cpp b/juk/keydialog.cpp new file mode 100644 index 00000000..2fe429c1 --- /dev/null +++ b/juk/keydialog.cpp @@ -0,0 +1,204 @@ +/*************************************************************************** + begin : Tue Mar 11 19:00:00 CET 2003 + copyright : (C) 2003 by Stefan Asserhall + email : stefan.asserhall@telia.com +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "keydialog.h" + +#include <kconfig.h> +#include <klocale.h> + +#include <qradiobutton.h> +#include <qvbox.h> +#include <qwhatsthis.h> + + +// Table of shortcut keys for each action, key group and three or four button modifier + +const KeyDialog::KeyInfo KeyDialog::keyInfo[] = { + { "PlayPause", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::Key_P, KKey::QtWIN+Qt::ALT+Qt::Key_P }, + { Qt::Key_MediaPlay, Qt::Key_MediaPlay } } }, + { "Stop", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::Key_S, KKey::QtWIN+Qt::ALT+Qt::Key_S }, + { Qt::Key_MediaStop, Qt::Key_MediaStop } } }, + { "Back", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::Key_Left, KKey::QtWIN+Qt::ALT+Qt::Key_Left }, + { Qt::Key_MediaPrev, Qt::Key_MediaPrev } } }, + { "Forward", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::Key_Right, KKey::QtWIN+Qt::ALT+Qt::Key_Right }, + { Qt::Key_MediaNext, Qt::Key_MediaNext } } }, + { "ForwardAlbum", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::Key_Up, KKey::QtWIN+Qt::ALT+Qt::Key_Up }, + { Qt::CTRL+Qt::Key_MediaNext, Qt::CTRL+Qt::Key_MediaNext } } }, + { "SeekBack", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::SHIFT+Qt::ALT+Qt::Key_Left, KKey::QtWIN+Qt::SHIFT+Qt::ALT+Qt::Key_Left }, + { Qt::SHIFT+Qt::Key_MediaPrev, Qt::SHIFT+Qt::Key_MediaPrev } } }, + { "SeekForward", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::SHIFT+Qt::ALT+Qt::Key_Right, KKey::QtWIN+Qt::SHIFT+Qt::ALT+Qt::Key_Right }, + { Qt::SHIFT+Qt::Key_MediaNext, Qt::SHIFT+Qt::Key_MediaNext } } }, + { "VolumeUp", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::SHIFT+Qt::Key_Up, KKey::QtWIN+Qt::ALT+Qt::SHIFT+Qt::Key_Up }, + { Qt::Key_VolumeUp, Qt::Key_VolumeUp } } }, + { "VolumeDown", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::SHIFT+Qt::Key_Down, KKey::QtWIN+Qt::ALT+Qt::SHIFT+Qt::Key_Down }, + { Qt::Key_VolumeDown, Qt::Key_VolumeDown } } }, + { "Mute", + { { KShortcut::null(), KShortcut::null() }, + { Qt::CTRL+Qt::ALT+Qt::Key_M, KKey::QtWIN+Qt::ALT+Qt::Key_M }, + { Qt::Key_VolumeMute, Qt::Key_VolumeMute } } }, + { "ShowHide", + { { KShortcut::null(), KShortcut::null() }, + { KShortcut::null(), KShortcut::null() }, + { KShortcut::null(), KShortcut::null() } } } +}; + +const uint KeyDialog::keyInfoCount = sizeof(KeyDialog::keyInfo) / sizeof(KeyDialog::keyInfo[0]); + +KeyDialog::KeyDialog(KGlobalAccel *keys, KActionCollection *actionCollection, + QWidget *parent, const char *name) + : KDialogBase(parent, name, true, i18n("Configure Shortcuts"), Default | Ok | Cancel, Ok) +{ + // Read key group from configuration + + int selectedButton; + + KConfigGroup config(KGlobal::config(), "Shortcuts"); + selectedButton = config.readNumEntry("GlobalKeys", StandardKeys); + + // Create widgets for key chooser - widget stack used to replace key chooser + + QVBox *vbox = new QVBox(this); + vbox->setSpacing(KDialog::spacingHint()); + m_widgetStack = new QWidgetStack(vbox); + + vbox->setStretchFactor(m_widgetStack, 1); + + // Create buttons to select key group + + m_group = new QHButtonGroup(i18n("Global Shortcuts"), vbox); + new QRadioButton(i18n("&No keys"), m_group); + new QRadioButton(i18n("&Standard keys"), m_group); + new QRadioButton(i18n("&Multimedia keys"), m_group); + connect(m_group, SIGNAL(clicked(int)), this, SLOT(slotKeys(int))); + QWhatsThis::add(m_group, + i18n("Here you can select the keys used as global shortcuts to control the player")); + + // Create the key chooser + + setMainWidget(vbox); + newDialog(keys, actionCollection, selectedButton); +} + +KeyDialog::~KeyDialog() +{ + +} + +void KeyDialog::newDialog(KGlobalAccel *keys, KActionCollection *actionCollection, + int selectedButton) +{ + m_keys = keys; + m_actionCollection = actionCollection; + + // Create key chooser and show it in the widget stack + m_pKeyChooser = new KKeyChooser(keys, this); + m_pKeyChooser->insert(actionCollection); + m_widgetStack->addWidget(m_pKeyChooser); + m_widgetStack->raiseWidget(m_pKeyChooser); + m_group->setButton(selectedButton); + + connect(this, SIGNAL(defaultClicked()), this, SLOT(slotDefault())); +} + +int KeyDialog::configure() +{ + // Show the dialog and save configuration if accepted + + int retcode = exec(); + if(retcode == Accepted) { + + KConfigGroup config(KGlobal::config(), "Shortcuts"); + config.writeEntry("GlobalKeys", m_group->id(m_group->selected())); + KGlobal::config()->sync(); + + m_pKeyChooser->save(); + } + return retcode; +} + +void KeyDialog::slotKeys(int group) +{ + bool fourModKeys = KGlobalAccel::useFourModifierKeys(); + + // Set modifier keys according to key group and modifier keys + + for(uint i = 0; i < keyInfoCount; i++) + m_keys->setShortcut(keyInfo[i].action, keyInfo[i].shortcut[group][fourModKeys]); + + // Create a new key chooser to show the keys, and delete the old one + + QWidget *w = m_widgetStack->visibleWidget(); + newDialog(m_keys, m_actionCollection, group); + m_widgetStack->removeWidget(w); + delete w; +} + +void KeyDialog::slotDefault() +{ + // Select default keys - standard key group + + m_group->setButton(StandardKeys); + m_pKeyChooser->allDefault(); +} + +int KeyDialog::configure(KGlobalAccel *keys, KActionCollection *actionCollection, + QWidget *parent) +{ + // Create and show dialog - update connections if accepted + + int retcode = KeyDialog(keys, actionCollection, parent).configure(); + + if(retcode == Accepted) + keys->updateConnections(); + return retcode; +} + +void KeyDialog::insert(KGlobalAccel *keys, const QString &action, const QString &label, + const QObject *objSlot, const char *methodSlot) +{ + KShortcut def3 = KShortcut::null(); + KShortcut def4 = KShortcut::null(); + + // Find and insert a standard key + + for(uint i = 0; i < keyInfoCount; i++) { + if(keyInfo[i].action == action) { + def3 = keyInfo[i].shortcut[StandardKeys][0]; + def4 = keyInfo[i].shortcut[StandardKeys][1]; + break; + } + } + keys->insert(action, label, QString::null, def3, def4, objSlot, methodSlot); +} + +#include "keydialog.moc" diff --git a/juk/keydialog.h b/juk/keydialog.h new file mode 100644 index 00000000..13d76d4f --- /dev/null +++ b/juk/keydialog.h @@ -0,0 +1,85 @@ +/*************************************************************************** + begin : Tue Mar 11 19:00:00 CET 2003 + copyright : (C) 2003 by Stefan Asserhall + email : stefan.asserhall@telia.com +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef KEYDIALOG_H +#define KEYDIALOG_H + +#include <kdeversion.h> +#include <kglobalaccel.h> +#include <kkeydialog.h> + +#include <qhbuttongroup.h> +#include <qwidgetstack.h> + +class KeyDialog : public KDialogBase +{ + Q_OBJECT + +public: + /** + * Constructs a KeyDialog called @p name as a child of @p parent. + */ + KeyDialog(KGlobalAccel *keys, KActionCollection *actionCollection, QWidget *parent = 0, const char* name = 0); + + /** + * Destructor. Deletes all resources used by a KeyDialog object. + */ + virtual ~KeyDialog(); + + /** + * This is a member function, provided to allow inserting both global + * accelerators and actions. It behaves essentially like the functions + * in KKeyDialog. + */ + static int configure(KGlobalAccel *keys, KActionCollection *actionCollection, QWidget *parent = 0); + + /** + * This is a member function, provided to create a global accelerator with + * standard keys. It behaves like the function in KGlobalAccel. + */ + static void insert(KGlobalAccel *keys, const QString &action, const QString &label, + const QObject *objSlot, const char *methodSlot); + +private: + + /** + * Groups of keys that can be selected in the dialog. + */ + enum KeyGroup { NoKeys = 0, StandardKeys = 1, MultimediaKeys = 2 }; + + struct KeyInfo { + QString action; + KShortcut shortcut[3][2]; + }; + + void newDialog(KGlobalAccel *keys, KActionCollection *actionCollection, int selectedButton = 0); + int configure(); + +private slots: + void slotKeys(int group); + void slotDefault(); + +private: + KActionCollection *m_actionCollection; + KGlobalAccel *m_keys; + KKeyChooser *m_pKeyChooser; + QHButtonGroup *m_group; + QWidgetStack *m_widgetStack; + + static const KeyInfo keyInfo[]; + static const uint keyInfoCount; +}; + +#endif // KEYDIALOG_H diff --git a/juk/ktrm.cpp b/juk/ktrm.cpp new file mode 100644 index 00000000..ae4a2e61 --- /dev/null +++ b/juk/ktrm.cpp @@ -0,0 +1,606 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + ***************************************************************************/ + +#include "ktrm.h" + +#if HAVE_MUSICBRAINZ + +#include <kapplication.h> +#include <kresolver.h> +#include <kprotocolmanager.h> +#include <kurl.h> +#include <kdebug.h> + +#include <qmutex.h> +#include <qregexp.h> +#include <qevent.h> +#include <qobject.h> +#include <qfile.h> + +#include <tunepimp/tp_c.h> +#include <fixx11h.h> + +class KTRMLookup; + +extern "C" +{ +#if HAVE_MUSICBRAINZ >= 4 + static void TRMNotifyCallback(tunepimp_t pimp, void *data, TPCallbackEnum type, int fileId, TPFileStatus status); +#else + static void TRMNotifyCallback(tunepimp_t pimp, void *data, TPCallbackEnum type, int fileId); +#endif +} + +/** + * This represents the main TunePimp instance and handles incoming requests. + */ + +class KTRMRequestHandler +{ +public: + static KTRMRequestHandler *instance() + { + static QMutex mutex; + mutex.lock(); + static KTRMRequestHandler handler; + mutex.unlock(); + return &handler; + } + + int startLookup(KTRMLookup *lookup) + { + int id; + + if(!m_fileMap.contains(lookup->file())) { +#if HAVE_MUSICBRAINZ >= 4 + id = tp_AddFile(m_pimp, QFile::encodeName(lookup->file()), 0); +#else + id = tp_AddFile(m_pimp, QFile::encodeName(lookup->file())); +#endif + m_fileMap.insert(lookup->file(), id); + } + else { + id = m_fileMap[lookup->file()]; + tp_IdentifyAgain(m_pimp, id); + } + m_lookupMap[id] = lookup; + return id; + } + + void endLookup(KTRMLookup *lookup) + { + tp_ReleaseTrack(m_pimp, tp_GetTrack(m_pimp, lookup->fileId())); + tp_Remove(m_pimp, lookup->fileId()); + m_lookupMap.remove(lookup->fileId()); + } + + bool lookupMapContains(int fileId) const + { + m_lookupMapMutex.lock(); + bool contains = m_lookupMap.contains(fileId); + m_lookupMapMutex.unlock(); + return contains; + } + + KTRMLookup *lookup(int fileId) const + { + m_lookupMapMutex.lock(); + KTRMLookup *l = m_lookupMap[fileId]; + m_lookupMapMutex.unlock(); + return l; + } + + void removeFromLookupMap(int fileId) + { + m_lookupMapMutex.lock(); + m_lookupMap.remove(fileId); + m_lookupMapMutex.unlock(); + } + + const tunepimp_t &tunePimp() const + { + return m_pimp; + } + +protected: + KTRMRequestHandler() + { + m_pimp = tp_New("KTRM", "0.1"); + //tp_SetDebug(m_pimp, true); + tp_SetTRMCollisionThreshold(m_pimp, 100); + tp_SetAutoSaveThreshold(m_pimp, -1); + tp_SetMoveFiles(m_pimp, false); + tp_SetRenameFiles(m_pimp, false); +#if HAVE_MUSICBRAINZ >= 4 + tp_SetFileNameEncoding(m_pimp, "UTF-8"); +#else + tp_SetUseUTF8(m_pimp, true); +#endif + tp_SetNotifyCallback(m_pimp, TRMNotifyCallback, 0); + + // Re-read proxy config. + KProtocolManager::reparseConfiguration(); + + if(KProtocolManager::useProxy()) { + // split code copied from kcm_kio. + QString noProxiesFor = KProtocolManager::noProxyFor(); + QStringList noProxies = QStringList::split(QRegExp("[',''\t'' ']"), noProxiesFor); + bool useProxy = true; + + // Host that libtunepimp will contact. + QString tunepimpHost = "www.musicbrainz.org"; + QString tunepimpHostWithPort = "www.musicbrainz.org:80"; + + // Check what hosts are allowed to proceed without being proxied, + // or is using reversed proxy, what hosts must be proxied. + for(QStringList::ConstIterator it = noProxies.constBegin(); it != noProxies.constEnd(); ++it) { + QString normalizedHost = KNetwork::KResolver::normalizeDomain(*it); + + if(normalizedHost == tunepimpHost || + tunepimpHost.endsWith("." + normalizedHost)) + { + useProxy = false; + break; + } + + // KDE's proxy mechanism also supports exempting a specific + // host/port combo, check that also. + if(normalizedHost == tunepimpHostWithPort || + tunepimpHostWithPort.endsWith("." + normalizedHost)) + { + useProxy = false; + break; + } + } + + // KDE supports a reverse proxy mechanism. Uh, yay. + if(KProtocolManager::useReverseProxy()) + useProxy = !useProxy; + + if(useProxy) { + KURL proxy = KProtocolManager::proxyFor("http"); + QString proxyHost = proxy.host(); + + kdDebug(65432) << "Using proxy server " << proxyHost << " for www.musicbrainz.org.\n"; + tp_SetProxy(m_pimp, proxyHost.latin1(), short(proxy.port())); + } + } + } + + ~KTRMRequestHandler() + { + tp_Delete(m_pimp); + } + +private: + tunepimp_t m_pimp; + QMap<int, KTRMLookup *> m_lookupMap; + QMap<QString, int> m_fileMap; + mutable QMutex m_lookupMapMutex; +}; + + +/** + * A custom event type used for signalling that a TRM lookup is finished. + */ + +class KTRMEvent : public QCustomEvent +{ +public: + enum Status { + Recognized, + Unrecognized, + Collision, + Error + }; + + KTRMEvent(int fileId, Status status) : + QCustomEvent(id), + m_fileId(fileId), + m_status(status) {} + + int fileId() const + { + return m_fileId; + } + + Status status() const + { + return m_status; + } + + static const int id = User + 1984; // random, unique, event id + +private: + int m_fileId; + Status m_status; +}; + +/** + * A helper class to intercept KTRMQueryEvents and call recognized() (from the GUI + * thread) for the lookup. + */ + +class KTRMEventHandler : public QObject +{ +public: + static void send(int fileId, KTRMEvent::Status status) + { + KApplication::postEvent(instance(), new KTRMEvent(fileId, status)); + } + +protected: + KTRMEventHandler() : QObject() {} + + static KTRMEventHandler *instance() + { + static QMutex mutex; + mutex.lock(); + static KTRMEventHandler handler; + mutex.unlock(); + return &handler; + } + + virtual void customEvent(QCustomEvent *event) + { + if(!event->type() == KTRMEvent::id) + return; + + KTRMEvent *e = static_cast<KTRMEvent *>(event); + + static QMutex mutex; + mutex.lock(); + + if(!KTRMRequestHandler::instance()->lookupMapContains(e->fileId())) { + mutex.unlock(); + return; + } + + KTRMLookup *lookup = KTRMRequestHandler::instance()->lookup(e->fileId()); +#if HAVE_MUSICBRAINZ >= 4 + if ( e->status() != KTRMEvent::Unrecognized) +#endif + KTRMRequestHandler::instance()->removeFromLookupMap(e->fileId()); + + mutex.unlock(); + + switch(e->status()) { + case KTRMEvent::Recognized: + lookup->recognized(); + break; + case KTRMEvent::Unrecognized: + lookup->unrecognized(); + break; + case KTRMEvent::Collision: + lookup->collision(); + break; + case KTRMEvent::Error: + lookup->error(); + break; + } + } +}; + +/** + * Callback fuction for TunePimp lookup events. + */ +#if HAVE_MUSICBRAINZ >= 4 +static void TRMNotifyCallback(tunepimp_t /*pimp*/, void* /*data*/, TPCallbackEnum type, int fileId, TPFileStatus status) +#else +static void TRMNotifyCallback(tunepimp_t pimp, void *data, TPCallbackEnum type, int fileId) +#endif +{ + if(type != tpFileChanged) + return; + +#if HAVE_MUSICBRAINZ < 4 + track_t track = tp_GetTrack(pimp, fileId); + TPFileStatus status = tr_GetStatus(track); +#endif + + switch(status) { + case eRecognized: + KTRMEventHandler::send(fileId, KTRMEvent::Recognized); + break; + case eUnrecognized: + KTRMEventHandler::send(fileId, KTRMEvent::Unrecognized); + break; + case eTRMCollision: +#if HAVE_MUSICBRAINZ >= 4 + case eUserSelection: +#endif + KTRMEventHandler::send(fileId, KTRMEvent::Collision); + break; + case eError: + KTRMEventHandler::send(fileId, KTRMEvent::Error); + break; + default: + break; + } +#if HAVE_MUSICBRAINZ < 4 + tp_ReleaseTrack(pimp, track); +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// KTRMResult implementation +//////////////////////////////////////////////////////////////////////////////// + +class KTRMResult::KTRMResultPrivate +{ +public: + KTRMResultPrivate() : track(0), year(0), relevance(0) {} + QString title; + QString artist; + QString album; + int track; + int year; + int relevance; +}; + +//////////////////////////////////////////////////////////////////////////////// +// KTRMResult public methods +//////////////////////////////////////////////////////////////////////////////// + +KTRMResult::KTRMResult() +{ + d = new KTRMResultPrivate; +} + +KTRMResult::KTRMResult(const KTRMResult &result) +{ + d = new KTRMResultPrivate(*result.d); +} + +KTRMResult::~KTRMResult() +{ + delete d; +} + +QString KTRMResult::title() const +{ + return d->title; +} + +QString KTRMResult::artist() const +{ + return d->artist; +} + +QString KTRMResult::album() const +{ + return d->album; +} + +int KTRMResult::track() const +{ + return d->track; +} + +int KTRMResult::year() const +{ + return d->year; +} + +bool KTRMResult::operator<(const KTRMResult &r) const +{ + return r.d->relevance < d->relevance; +} + +bool KTRMResult::operator>(const KTRMResult &r) const +{ + return r.d->relevance > d->relevance; +} + +bool KTRMResult::isEmpty() const +{ + return d->title.isEmpty() && d->artist.isEmpty() && d->album.isEmpty() && + d->track == 0 && d->year == 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// KTRMLookup implementation +//////////////////////////////////////////////////////////////////////////////// + +class KTRMLookup::KTRMLookupPrivate +{ +public: + KTRMLookupPrivate() : + fileId(-1) {} + QString file; + KTRMResultList results; + int fileId; + bool autoDelete; +}; + +//////////////////////////////////////////////////////////////////////////////// +// KTRMLookup public methods +//////////////////////////////////////////////////////////////////////////////// + +KTRMLookup::KTRMLookup(const QString &file, bool autoDelete) +{ + d = new KTRMLookupPrivate; + d->file = file; + d->autoDelete = autoDelete; + d->fileId = KTRMRequestHandler::instance()->startLookup(this); +} + +KTRMLookup::~KTRMLookup() +{ + KTRMRequestHandler::instance()->endLookup(this); + delete d; +} + +QString KTRMLookup::file() const +{ + return d->file; +} + +int KTRMLookup::fileId() const +{ + return d->fileId; +} + +void KTRMLookup::recognized() +{ + kdDebug() << k_funcinfo << d->file << endl; + + d->results.clear(); + + metadata_t *metaData = md_New(); + track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId); + tr_Lock(track); + tr_GetServerMetadata(track, metaData); + + KTRMResult result; + + result.d->title = QString::fromUtf8(metaData->track); + result.d->artist = QString::fromUtf8(metaData->artist); + result.d->album = QString::fromUtf8(metaData->album); + result.d->track = metaData->trackNum; + result.d->year = metaData->releaseYear; + + d->results.append(result); + + md_Delete(metaData); + tr_Unlock(track); + finished(); +} + +void KTRMLookup::unrecognized() +{ + kdDebug() << k_funcinfo << d->file << endl; +#if HAVE_MUSICBRAINZ >= 4 + char trm[255]; + bool finish = false; + trm[0] = 0; + track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId); + tr_Lock(track); + tr_GetTRM(track, trm, 255); + if ( !trm[0] ) { + tr_SetStatus(track, ePending); + tp_Wake(KTRMRequestHandler::instance()->tunePimp(), track); + } + else + finish = true; + tr_Unlock(track); + tp_ReleaseTrack(KTRMRequestHandler::instance()->tunePimp(), track); + if ( !finish ) + return; +#endif + d->results.clear(); + finished(); +} + +void KTRMLookup::collision() +{ + kdDebug() << k_funcinfo << d->file << endl; + + track_t track = tp_GetTrack(KTRMRequestHandler::instance()->tunePimp(), d->fileId); + + if(track <= 0) { + kdDebug() << "invalid track number" << endl; + return; + } + + tr_Lock(track); + int resultCount = tr_GetNumResults(track); + + if(resultCount > 0) { + TPResultType type; + result_t *results = new result_t[resultCount]; + tr_GetResults(track, &type, results, &resultCount); + + switch(type) { + case eNone: + kdDebug() << k_funcinfo << "eNone" << endl; + break; + case eArtistList: + kdDebug() << "eArtistList" << endl; + break; + case eAlbumList: + kdDebug() << "eAlbumList" << endl; + break; + case eTrackList: + { + kdDebug() << "eTrackList" << endl; + albumtrackresult_t **tracks = (albumtrackresult_t **) results; + d->results.clear(); + + for(int i = 0; i < resultCount; i++) { + KTRMResult result; + + result.d->title = QString::fromUtf8(tracks[i]->name); +#if HAVE_MUSICBRAINZ >= 4 + result.d->artist = QString::fromUtf8(tracks[i]->artist.name); + result.d->album = QString::fromUtf8(tracks[i]->album.name); + result.d->year = tracks[i]->album.releaseYear; +#else + result.d->artist = QString::fromUtf8(tracks[i]->artist->name); + result.d->album = QString::fromUtf8(tracks[i]->album->name); + result.d->year = tracks[i]->album->releaseYear; +#endif + result.d->track = tracks[i]->trackNum; + result.d->relevance = tracks[i]->relevance; + + d->results.append(result); + } + break; + } + case eMatchedTrack: + kdDebug() << k_funcinfo << "eMatchedTrack" << endl; + break; + } + + delete [] results; + } + + tr_Unlock(track); + + finished(); +} + +void KTRMLookup::error() +{ + kdDebug() << k_funcinfo << d->file << endl; + + d->results.clear(); + finished(); +} + +KTRMResultList KTRMLookup::results() const +{ + return d->results; +} + +//////////////////////////////////////////////////////////////////////////////// +// KTRMLookup protected methods +//////////////////////////////////////////////////////////////////////////////// + +void KTRMLookup::finished() +{ + if(d->autoDelete) + delete this; +} + +#endif + +// vim: set et ts=8 sw=4: diff --git a/juk/ktrm.h b/juk/ktrm.h new file mode 100644 index 00000000..028a436b --- /dev/null +++ b/juk/ktrm.h @@ -0,0 +1,197 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : wheeler@kde.org + ***************************************************************************/ + +/*************************************************************************** + * This library is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Lesser General Public License version * + * 2.1 as published by the Free Software Foundation. * + * * + * This library 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with this library; if not, write to the Free Software * + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * + * USA * + ***************************************************************************/ + +/* + * At some point this will likely be a library class, as such it's been written + * as such and is LGPL'ed. + */ + +#ifndef KTRM_H +#define KTRM_H + +#include <config.h> + +#if HAVE_MUSICBRAINZ + +#include <qstring.h> +#include <qvaluelist.h> +#include <qmap.h> + +/** + * This represents a potential match for a TRM lookup. KTRMResultList is + * returned from KTRMLookup and will be sorted by relevance (better matches + * at the beginning of the list). + */ + +class KTRMResult +{ + friend class KTRMLookup; + +public: + KTRMResult(); + KTRMResult(const KTRMResult &result); + ~KTRMResult(); + + /** + * Returns the title of the track for the potential match. + */ + QString title() const; + + /** + * Returns the artist name of the track for the potential match. + */ + QString artist() const; + + /** + * Returns the album name of the track for the potential match. + */ + QString album() const; + + /** + * Returns the track number of the track for the potential match. + */ + int track() const; + + /** + * Returns the original release year of the track for the potential match. + */ + int year() const; + + /** + * Returns true if all of the values for the result are empty. + */ + bool isEmpty() const; + + /** + * Compares to \a r based on the relevance of the match. Better matches + * will be greater than less accurate matches. + */ + bool operator<(const KTRMResult &r) const; + + /** + * Compares to \a r based on the relevance of the match. Better matches + * will be greater than less accurate matches. + */ + bool operator>(const KTRMResult &r) const; + +private: + class KTRMResultPrivate; + KTRMResultPrivate *d; +}; + +typedef QValueList<KTRMResult> KTRMResultList; + +/** + * An abstraction for libtunepimp's TRM based lookup and file recognition. + * + * A lookup is started when the object is created. One of the virtual methods + * -- recognized(), unrecognized(), collision() or error(). Those methods + * should be reimplemented in subclasses to specify what behavior should happen + * for each result. + * + * The lookups themselves happen in a background thread, but the return calls + * are guaranteed to run in the GUI thread. + */ +class KTRMLookup +{ +public: + /** + * Creates and starts a lookup for \a file. If \a autoDelete is set to + * true the lookup will delete itself when it has finished. + */ + KTRMLookup(const QString &file, bool autoDelete = false); + + virtual ~KTRMLookup(); + + /** + * Returns the file name that was specified in the constructor. + */ + QString file() const; + + /** + * Returns the TunePimp file ID for the file. This is of no use to the + * public API. + * + * @internal + */ + int fileId() const; + + /** + * This method is called if the track was recognized by the TRM server. + * results() will return just one value. This may be reimplemented to + * provide specific behavion in the case of the track being recognized. + * + * \note The base class call should happen at the beginning of the subclass + * reimplementation; it populates the values of results(). + */ + virtual void recognized(); + + /** + * This method is called if the track was not recognized by the TRM server. + * results() will return an empty set. This may be reimplemented to provide + * specific behavion in the case of the track not being recognized. + */ + virtual void unrecognized(); + + /** + * This method is called if there are multiple potential matches for the TRM + * value. results() will return a list of the potential matches, sorted by + * liklihood. This may be reimplemented to provide + * specific behavion in the case of the track not being recognized. + * + * \note The base class call should happen at the beginning of the subclass + * reimplementation; it populates the values of results(). + */ + virtual void collision(); + + /** + * This method is called if the track was not recognized by the TRM server. + * results() will return an empty set. This may be reimplemented to provide + * specific behavion in the case of the track not being recognized. + */ + virtual void error(); + + /** + * Returns the list of matches found by the lookup. In the case that there + * was a TRM collision this list will contain multiple entries. In the case + * that it was recognized this will only contain one entry. Otherwise it + * will remain empty. + */ + KTRMResultList results() const; + +protected: + /** + * This method is called when any of terminal states (recognized, + * unrecognized, collision or error) has been reached after the specifc + * method for the result has been called. + * + * This should be reimplemented in the case that there is some general + * processing to be done for all terminal states. + */ + virtual void finished(); + +private: + class KTRMLookupPrivate; + KTRMLookupPrivate *d; +}; + +#endif +#endif diff --git a/juk/main.cpp b/juk/main.cpp new file mode 100644 index 00000000..7a0c53e3 --- /dev/null +++ b/juk/main.cpp @@ -0,0 +1,98 @@ +/*************************************************************************** + begin : Mon Feb 4 23:40:41 EST 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + : (C) 2006 - 2007 by Michael Pyne + email : wheeler@kde.org + : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kuniqueapplication.h> +#include <kcmdlineargs.h> +#include <kaboutdata.h> +#include <dcopclient.h> +#include <kconfigbase.h> +#include <kconfig.h> + +#include "juk.h" + +static const char description[] = I18N_NOOP("Jukebox and music manager for KDE"); +static const char scott[] = I18N_NOOP("Author, chief dork and keeper of the funk"); +static const char michael[] = I18N_NOOP("Assistant super-hero, fixer of many things"); +static const char daniel[] = I18N_NOOP("System tray docking, \"inline\" tag editing,\nbug fixes, evangelism, moral support"); +static const char tim[] = I18N_NOOP("GStreamer port"); +static const char stefan[] = I18N_NOOP("Global keybindings support"); +static const char stephen[] = I18N_NOOP("Track announcement popups"); +static const char frerich[] = I18N_NOOP("Automagic track data guessing, bugfixes"); +static const char zack[] = I18N_NOOP("More automagical things, now using MusicBrainz"); +static const char adam[] = I18N_NOOP("Co-conspirator in MusicBrainz wizardry"); +static const char matthias[] = I18N_NOOP("Friendly, neighborhood aRts guru"); +static const char maks[] = I18N_NOOP("Making JuK friendlier to people with\nterabytes of music"); +static const char antonio[] = I18N_NOOP("DCOP interface"); +static const char allan[] = I18N_NOOP("FLAC and MPC support"); +static const char nathan[] = I18N_NOOP("Album cover manager"); +static const char pascal[] = I18N_NOOP("Gimper of splash screen"); + +static KCmdLineOptions options[] = +{ + { "+[file(s)]", I18N_NOOP("File(s) to open"), 0 }, + KCmdLineLastOption +}; + +int main(int argc, char *argv[]) +{ + KAboutData aboutData("juk", I18N_NOOP("JuK"), + "2.3.5", description, KAboutData::License_GPL, + "© 2002 - 2007, Scott Wheeler", 0, + "http://developer.kde.org/~wheeler/juk.html"); + + aboutData.addAuthor("Scott Wheeler", scott, "wheeler@kde.org"); + aboutData.addAuthor("Michael Pyne", michael, "michael.pyne@kdemail.net"); + aboutData.addCredit("Daniel Molkentin", daniel, "molkentin@kde.org"); + aboutData.addCredit("Tim Jansen", tim, "tim@tjansen.de"); + aboutData.addCredit("Stefan Asserhäll", stefan, "stefan.asserhall@telia.com"); + aboutData.addCredit("Stephen Douglas", stephen, "stephen_douglas@yahoo.com"); + aboutData.addCredit("Frerich Raabe", frerich, "raabe@kde.org"); + aboutData.addCredit("Zack Rusin", zack, "zack@kde.org"); + aboutData.addCredit("Adam Treat", adam, "manyoso@yahoo.com"); + aboutData.addCredit("Matthias Kretz", matthias, "kretz@kde.org"); + aboutData.addCredit("Maks Orlovich", maks, "maksim@kde.org"); + aboutData.addCredit("Antonio Larrosa Jimenez", antonio, "larrosa@kde.org"); + aboutData.addCredit("Allan Sandfeld Jensen", allan, "kde@carewolf.com"); + aboutData.addCredit("Nathan Toone", nathan, "nathan@toonetown.com"); + aboutData.addCredit("Pascal Klein", pascal, "4pascal@tpg.com.au"); + + KCmdLineArgs::init(argc, argv, &aboutData); + KCmdLineArgs::addCmdLineOptions(options); + + KUniqueApplication::addCmdLineOptions(); + + KUniqueApplication a; + + // Here we do some DCOP locking of sorts to prevent incoming DCOP calls + // before JuK has finished its initialization. + + a.dcopClient()->suspend(); + JuK *juk = new JuK; + a.dcopClient()->resume(); + + a.setMainWidget(juk); + + bool startDocked; + + KConfigGroup config(KGlobal::config(), "Settings"); + startDocked = config.readBoolEntry("StartDocked", false); + + if(!startDocked) + juk->show(); + + return a.exec(); +} diff --git a/juk/mediafiles.cpp b/juk/mediafiles.cpp new file mode 100644 index 00000000..b52c4be3 --- /dev/null +++ b/juk/mediafiles.cpp @@ -0,0 +1,158 @@ +/*************************************************************************** + begin : Fri Sep 13 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kfiledialog.h> +#include <kdebug.h> +#include <klocale.h> +#include <kio/netaccess.h> + +#include "mediafiles.h" + +#include <taglib/tag.h> +#if (TAGLIB_MAJOR_VERSION>1) || \ + ((TAGLIB_MAJOR_VERSION==1) && (TAGLIB_MINOR_VERSION>=2)) +#define TAGLIB_1_2 +#endif +#if (TAGLIB_MAJOR_VERSION>1) || \ + ((TAGLIB_MAJOR_VERSION==1) && (TAGLIB_MINOR_VERSION>=3)) +#define TAGLIB_1_3 +#endif + +namespace MediaFiles { + QStringList mimeTypes(); + + static const char mp3Type[] = "audio/mpeg"; + static const char oggType[] = "application/ogg"; + static const char flacType[] = "audio/x-flac"; + static const char mpcType[] = "audio/x-musepack"; + static const char m3uType[] = "audio/mpegurl"; + + static const char vorbisType[] = "audio/x-vorbis"; + static const char oggflacType[] = "audio/x-oggflac"; + + static const char playlistExtension[] = ".m3u"; +} + +QStringList MediaFiles::openDialog(QWidget *parent) +{ + KFileDialog dialog(QString::null, QString::null, parent, "filedialog", true); + dialog.setOperationMode(KFileDialog::Opening); + + dialog.setCaption(i18n("Open")); + dialog.setMode(KFile::Files | KFile::LocalOnly); + // dialog.ops->clearHistory(); + dialog.setMimeFilter(mimeTypes()); + + dialog.exec(); + + return convertURLsToLocal(dialog.selectedFiles()); +} + +QString MediaFiles::savePlaylistDialog(const QString &playlistName, QWidget *parent) +{ + QString fileName = KFileDialog::getSaveFileName(playlistName + playlistExtension, + QString("*").append(playlistExtension), + parent, + i18n("Playlists")); + if(!fileName.isEmpty() && !fileName.endsWith(playlistExtension)) + fileName.append(playlistExtension); + + return fileName; +} + +bool MediaFiles::isMediaFile(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, true); + + return result->is(mp3Type) || result->is(oggType) || result->is(flacType) +#ifdef TAGLIB_1_3 + || result->is(mpcType) +#endif + ; +} + +bool MediaFiles::isPlaylistFile(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, true); + return result->is(m3uType); +} + +bool MediaFiles::isMP3(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, true); + return result->is(mp3Type); +} + +bool MediaFiles::isOgg(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, true); + return result->is(oggType); +} + +bool MediaFiles::isFLAC(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, true); + return result->is(flacType); +} + +bool MediaFiles::isMPC(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, true); + return result->is(mpcType); +} + +bool MediaFiles::isVorbis(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, false); + return result->is(vorbisType); +} + +bool MediaFiles::isOggFLAC(const QString &fileName) +{ + KMimeType::Ptr result = KMimeType::findByPath(fileName, 0, false); + return result->is(oggflacType); +} + +QStringList MediaFiles::mimeTypes() +{ + QStringList l; + + l << mp3Type << oggType << flacType << m3uType << vorbisType +#ifdef TAGLIB_1_2 + << oggflacType +#endif +#ifdef TAGLIB_1_3 + << mpcType +#endif + ; + return l; +} + +QStringList MediaFiles::convertURLsToLocal(const QStringList &urlList, QWidget *w) +{ + QStringList result; + KURL localUrl; + + for(QStringList::ConstIterator it = urlList.constBegin(); it != urlList.constEnd(); ++it) { + localUrl = KIO::NetAccess::mostLocalURL(KURL::fromPathOrURL(*it), w); + + if(!localUrl.isLocalFile()) + kdDebug(65432) << localUrl << " is not a local file, skipping.\n"; + else + result.append(localUrl.path()); + } + + return result; +} diff --git a/juk/mediafiles.h b/juk/mediafiles.h new file mode 100644 index 00000000..e0cc8458 --- /dev/null +++ b/juk/mediafiles.h @@ -0,0 +1,89 @@ +/*************************************************************************** + begin : Fri Sep 13 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef MEDIAFILES_H +#define MEDIAFILES_H + + +/** + * A namespace for file JuK's file related functions. The goal is to hide + * all specific knowledge of mimetypes and file extensions here. + */ +namespace MediaFiles +{ + /** + * Creates a JuK specific KFileDialog with the specified parent. + */ + QStringList openDialog(QWidget *parent = 0); + + /** + * Creates a JuK specific KFileDialog for saving a playlist with the name + * playlistName and the specified parent and returns the file name. + */ + QString savePlaylistDialog(const QString &playlistName, QWidget *parent = 0); + + /** + * Returns true if fileName is a supported media file. + */ + bool isMediaFile(const QString &fileName); + + /** + * Returns true if fileName is a supported playlist file. + */ + bool isPlaylistFile(const QString &fileName); + + /** + * Returns true if fileName is a mp3 file. + */ + bool isMP3(const QString &fileName); + + /** + * Returns true if fileName is a mpc (aka musepack) file. + */ + bool isMPC(const QString &fileName); + + /** + * Returns true if fileName is an Ogg file. + */ + bool isOgg(const QString &fileName); + + /** + * Returns true if fileName is a FLAC file. + */ + bool isFLAC(const QString &fileName); + + /** + * Returns true if fileName is an Ogg/Vorbis file. + */ + bool isVorbis(const QString &fileName); + + /** + * Returns true if fileName is an Ogg/FLAC file. + */ + bool isOggFLAC(const QString &fileName); + + /** + * Returns a list of absolute local filenames, mapped from \p urlList. + * Any URLs in urlList that aren't really local files will be stripped + * from the result (so result.size() may be < urlList.size()). + * + * @param urlList list of file names or URLs to convert. + * @param w KIO may need the widget to handle user interaction. + * @return list of all local files in urlList, converted to absolute paths. + */ + QStringList convertURLsToLocal(const QStringList &urlList, QWidget *w = 0); +} + +#endif diff --git a/juk/musicbrainzquery.cpp b/juk/musicbrainzquery.cpp new file mode 100644 index 00000000..84bb38d7 --- /dev/null +++ b/juk/musicbrainzquery.cpp @@ -0,0 +1,120 @@ +/*************************************************************************** + begin : Tue Aug 3 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include "musicbrainzquery.h" + +#if HAVE_MUSICBRAINZ + +#include "trackpickerdialog.h" +#include "tag.h" +#include "collectionlist.h" +#include "tagtransactionmanager.h" + +#include <kmainwindow.h> +#include <kapplication.h> +#include <kstatusbar.h> +#include <klocale.h> +#include <kdebug.h> + +#include <qfileinfo.h> + +MusicBrainzLookup::MusicBrainzLookup(const FileHandle &file) : + KTRMLookup(file.absFilePath()), + m_file(file) +{ + message(i18n("Querying MusicBrainz server...")); +} + +void MusicBrainzLookup::recognized() +{ + KTRMLookup::recognized(); + confirmation(); + delete this; +} + +void MusicBrainzLookup::unrecognized() +{ + KTRMLookup::unrecognized(); + message(i18n("No matches found.")); + delete this; +} + +void MusicBrainzLookup::collision() +{ + KTRMLookup::collision(); + confirmation(); + delete this; +} + +void MusicBrainzLookup::error() +{ + KTRMLookup::error(); + message(i18n("Error connecting to MusicBrainz server.")); + delete this; +} + +void MusicBrainzLookup::message(const QString &s) const +{ + KMainWindow *w = static_cast<KMainWindow *>(kapp->mainWidget()); + w->statusBar()->message(QString("%1 (%2)").arg(s).arg(m_file.fileInfo().fileName()), 3000); +} + +void MusicBrainzLookup::confirmation() +{ + // Here we do a bit of queuing to make sure that we don't pop up multiple + // instances of the confirmation dialog at once. + + static QValueList< QPair<FileHandle, KTRMResultList> > queue; + + if(results().isEmpty()) + return; + + if(!queue.isEmpty()) { + queue.append(qMakePair(m_file, results())); + return; + } + + queue.append(qMakePair(m_file, results())); + + while(!queue.isEmpty()) { + QPair<FileHandle, KTRMResultList> item = queue.first(); + FileHandle file = item.first; + KTRMResultList results = item.second; + TrackPickerDialog dialog(file.fileInfo().fileName(), results); + + if(dialog.exec() == QDialog::Accepted && !dialog.result().isEmpty()) { + KTRMResult result = dialog.result(); + Tag *tag = TagTransactionManager::duplicateTag(file.tag()); + + if(!result.title().isEmpty()) + tag->setTitle(result.title()); + if(!result.artist().isEmpty()) + tag->setArtist(result.artist()); + if(!result.album().isEmpty()) + tag->setAlbum(result.album()); + if(result.track() != 0) + tag->setTrack(result.track()); + if(result.year() != 0) + tag->setYear(result.year()); + + PlaylistItem *item = CollectionList::instance()->lookup(file.absFilePath()); + TagTransactionManager::instance()->changeTagOnItem(item, tag); + TagTransactionManager::instance()->commit(); + } + queue.pop_front(); + } +} + +#endif diff --git a/juk/musicbrainzquery.h b/juk/musicbrainzquery.h new file mode 100644 index 00000000..6bf51278 --- /dev/null +++ b/juk/musicbrainzquery.h @@ -0,0 +1,43 @@ +/*************************************************************************** + begin : Tue Aug 3 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef MUSICBRAINZQUERY_H +#define MUSICBRAINZQUERY_H + +#include <config.h> + +#if HAVE_MUSICBRAINZ + +#include "ktrm.h" +#include "filehandle.h" + +class MusicBrainzLookup : public KTRMLookup +{ +public: + MusicBrainzLookup(const FileHandle &file); + virtual void recognized(); + virtual void unrecognized(); + virtual void collision(); + virtual void error(); + +private: + void message(const QString &s) const; + void confirmation(); + + FileHandle m_file; +}; + +#endif +#endif diff --git a/juk/nowplaying.cpp b/juk/nowplaying.cpp new file mode 100644 index 00000000..2336faac --- /dev/null +++ b/juk/nowplaying.cpp @@ -0,0 +1,368 @@ +/*************************************************************************** + begin : Tue Nov 9 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kiconloader.h> +#include <klocale.h> +#include <kdebug.h> +#include <kurldrag.h> +#include <kio/netaccess.h> + +#include <qimage.h> +#include <qlayout.h> +#include <qevent.h> +#include <qdragobject.h> +#include <qimage.h> +#include <qtimer.h> +#include <qpoint.h> + +#include "nowplaying.h" +#include "playlistcollection.h" +#include "playermanager.h" +#include "coverinfo.h" +#include "covermanager.h" +#include "tag.h" +#include "playlistitem.h" +#include "collectionlist.h" +#include "historyplaylist.h" + +static const int imageSize = 64; + +struct Line : public QFrame +{ + Line(QWidget *parent) : QFrame(parent) { setFrameShape(VLine); } +}; + +//////////////////////////////////////////////////////////////////////////////// +// NowPlaying +//////////////////////////////////////////////////////////////////////////////// + +NowPlaying::NowPlaying(QWidget *parent, PlaylistCollection *collection, const char *name) : + QHBox(parent, name), + m_observer(this, collection), + m_collection(collection) +{ + // m_observer is set to watch the PlaylistCollection, also watch for + // changes that come from CollectionList. + + CollectionList::instance()->addObserver(&m_observer); + + layout()->setMargin(5); + layout()->setSpacing(3); + setFixedHeight(imageSize + 2 + layout()->margin() * 2); + + setStretchFactor(new CoverItem(this), 0); + setStretchFactor(new TrackItem(this), 2); + setStretchFactor(new Line(this), 0); + setStretchFactor(new HistoryItem(this), 1); + + connect(PlayerManager::instance(), SIGNAL(signalPlay()), this, SLOT(slotUpdate())); + connect(PlayerManager::instance(), SIGNAL(signalStop()), this, SLOT(slotUpdate())); + + hide(); +} + +void NowPlaying::addItem(NowPlayingItem *item) +{ + m_items.append(item); +} + +PlaylistCollection *NowPlaying::collection() const +{ + return m_collection; +} + +void NowPlaying::slotUpdate() +{ + FileHandle file = PlayerManager::instance()->playingFile(); + + if(file.isNull()) { + hide(); + return; + } + else + show(); + + for(QValueList<NowPlayingItem *>::Iterator it = m_items.begin(); + it != m_items.end(); ++it) + { + (*it)->update(file); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CoverItem +//////////////////////////////////////////////////////////////////////////////// + +CoverItem::CoverItem(NowPlaying *parent) : + QLabel(parent, "CoverItem"), + NowPlayingItem(parent) +{ + setFixedHeight(parent->height() - parent->layout()->margin() * 2); + setFrameStyle(Box | Plain); + setLineWidth(1); + setMargin(1); + setAcceptDrops(true); +} + +void CoverItem::update(const FileHandle &file) +{ + m_file = file; + + if(file.coverInfo()->hasCover()) { + show(); + QImage image = file.coverInfo()->pixmap(CoverInfo::Thumbnail).convertToImage(); + setPixmap(image.smoothScale(imageSize, imageSize, QImage::ScaleMin)); + } + else + hide(); +} + +void CoverItem::mouseReleaseEvent(QMouseEvent *event) +{ + if(m_dragging) { + m_dragging = false; + return; + } + + if(event->x() >= 0 && event->y() >= 0 && + event->x() < width() && event->y() < height() && + event->button() == LeftButton && + m_file.coverInfo()->hasCover()) + { + m_file.coverInfo()->popup(); + } + + QLabel::mousePressEvent(event); +} + +void CoverItem::mousePressEvent(QMouseEvent *e) +{ + m_dragging = false; + m_dragStart = e->globalPos(); +} + +void CoverItem::mouseMoveEvent(QMouseEvent *e) +{ + if(m_dragging) + return; + + QPoint diff = m_dragStart - e->globalPos(); + if(QABS(diff.x()) > 1 || QABS(diff.y()) > 1) { + + // Start a drag. + + m_dragging = true; + + CoverDrag *drag = new CoverDrag(m_file.coverInfo()->coverId(), this); + drag->drag(); + } +} + +void CoverItem::dragEnterEvent(QDragEnterEvent *e) +{ + e->accept(QImageDrag::canDecode(e) || KURLDrag::canDecode(e) || CoverDrag::canDecode(e)); +} + +void CoverItem::dropEvent(QDropEvent *e) +{ + QImage image; + KURL::List urls; + coverKey key; + + if(e->source() == this) + return; + + if(QImageDrag::decode(e, image)) { + m_file.coverInfo()->setCover(image); + update(m_file); + } + else if(CoverDrag::decode(e, key)) { + m_file.coverInfo()->setCoverId(key); + update(m_file); + } + else if(KURLDrag::decode(e, urls)) { + QString fileName; + + if(KIO::NetAccess::download(urls.front(), fileName, this)) { + if(image.load(fileName)) { + m_file.coverInfo()->setCover(image); + update(m_file); + } + else + kdError(65432) << "Unable to load image from " << urls.front() << endl; + + KIO::NetAccess::removeTempFile(fileName); + } + else + kdError(65432) << "Unable to download " << urls.front() << endl; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// TrackItem +//////////////////////////////////////////////////////////////////////////////// + +TrackItem::TrackItem(NowPlaying *parent) : + QWidget(parent, "TrackItem"), + NowPlayingItem(parent) +{ + setFixedHeight(parent->height() - parent->layout()->margin() * 2); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + QVBoxLayout *layout = new QVBoxLayout(this); + + m_label = new LinkLabel(this); + m_label->setLinkUnderline(false); + + layout->addStretch(); + layout->addWidget(m_label); + layout->addStretch(); + + connect(m_label, SIGNAL(linkClicked(const QString &)), this, + SLOT(slotOpenLink(const QString &))); +} + +void TrackItem::update(const FileHandle &file) +{ + m_file = file; + QTimer::singleShot(0, this, SLOT(slotUpdate())); +} + +void TrackItem::slotOpenLink(const QString &link) +{ + PlaylistCollection *collection = NowPlayingItem::parent()->collection(); + + if(link == "artist") + collection->showMore(m_file.tag()->artist()); + else if(link == "album") + collection->showMore(m_file.tag()->artist(), m_file.tag()->album()); + else if(link == "clear") + collection->clearShowMore(); + + update(m_file); +} + +void TrackItem::slotUpdate() +{ + QString title = QStyleSheet::escape(m_file.tag()->title()); + QString artist = QStyleSheet::escape(m_file.tag()->artist()); + QString album = QStyleSheet::escape(m_file.tag()->album()); + QString separator = (artist.isNull() || album.isNull()) ? QString::null : QString(" - "); + + // This block-o-nastiness makes the font smaller and smaller until it actually fits. + + int size = 4; + QString format = + "<font size=\"+%1\"><b>%2</b></font>" + "<br />" + "<font size=\"+%3\"><b><a href=\"artist\">%4</a>%5<a href=\"album\">%6</a></b>"; + + if(NowPlayingItem::parent()->collection()->showMoreActive()) + format.append(QString(" (<a href=\"clear\">%1</a>)").arg(i18n("back to playlist"))); + + format.append("</font>"); + + do { + m_label->setText(format.arg(size).arg(title).arg(size - 2) + .arg(artist).arg(separator).arg(album)); + --size; + } while(m_label->heightForWidth(m_label->width()) > imageSize && size >= 0); + + m_label->setFixedHeight(QMIN(imageSize, m_label->heightForWidth(m_label->width()))); +} + +//////////////////////////////////////////////////////////////////////////////// +// HistoryItem +//////////////////////////////////////////////////////////////////////////////// + +HistoryItem::HistoryItem(NowPlaying *parent) : + LinkLabel(parent, "HistoryItem"), + NowPlayingItem(parent) +{ + setFixedHeight(parent->height() - parent->layout()->margin() * 2); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + setLinkUnderline(false); + setText(QString("<b>%1</b>").arg(i18n("History"))); + + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), this, SLOT(slotAddPlaying())); +} + +void HistoryItem::update(const FileHandle &file) +{ + if(file.isNull() || (!m_history.isEmpty() && m_history.front().file == file)) + return; + + if(m_history.count() >= 10) + m_history.remove(m_history.fromLast()); + + QString format = "<br /><a href=\"%1\"><font size=\"-1\">%2</font></a>"; + QString current = QString("<b>%1</b>").arg(i18n("History")); + QString previous; + + for(QValueList<Item>::ConstIterator it = m_history.begin(); + it != m_history.end(); ++it) + { + previous = current; + current.append(format.arg((*it).anchor).arg(QStyleSheet::escape((*it).file.tag()->title()))); + setText(current); + if(heightForWidth(width()) > imageSize) { + setText(previous); + break; + } + } + + m_file = file; + m_timer->stop(); + m_timer->start(HistoryPlaylist::delay(), true); +} + +void HistoryItem::openLink(const QString &link) +{ + for(QValueList<Item>::ConstIterator it = m_history.begin(); + it != m_history.end(); ++it) + { + if((*it).anchor == link) { + if((*it).playlist) { + CollectionListItem *collectionItem = + CollectionList::instance()->lookup((*it).file.absFilePath()); + PlaylistItem *item = collectionItem->itemForPlaylist((*it).playlist); + (*it).playlist->clearSelection(); + (*it).playlist->setSelected(item, true); + (*it).playlist->ensureItemVisible(item); + NowPlayingItem::parent()->collection()->raise((*it).playlist); + } + break; + } + } +} + +void HistoryItem::slotAddPlaying() +{ + // More or less copied from the HistoryPlaylist + + PlayerManager *manager = PlayerManager::instance(); + + if(manager->playing() && manager->playingFile() == m_file) { + m_history.prepend(Item(KApplication::randomString(20), + m_file, Playlist::playingItem()->playlist())); + } + + m_file = FileHandle::null(); +} + +#include "nowplaying.moc" + +// vim: set et sw=4 ts=8: diff --git a/juk/nowplaying.h b/juk/nowplaying.h new file mode 100644 index 00000000..208eabc4 --- /dev/null +++ b/juk/nowplaying.h @@ -0,0 +1,177 @@ +/*************************************************************************** + begin : Tue Nov 9 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef NOWPLAYING_H +#define NOWPLAYING_H + +#include <kactivelabel.h> + +#include <qhbox.h> +#include <qlabel.h> +#include <qguardedptr.h> + +#include "filehandle.h" +#include "playlist.h" + +class QTimer; +class QPoint; + +class NowPlayingItem; +class PlaylistCollection; +class Playlist; + +/** + * This is the widget that holds all of the other items and handles updating them + * when the playing item changes. + */ + +class NowPlaying : public QHBox +{ + Q_OBJECT + +public: + NowPlaying(QWidget *parent, PlaylistCollection *collection, + const char *name = 0); + void addItem(NowPlayingItem *item); + PlaylistCollection *collection() const; + +private slots: + void slotUpdate(); + +private: + struct Observer : public PlaylistObserver + { + Observer(NowPlaying *parent, PlaylistInterface *playlist) : + PlaylistObserver(playlist), + m_parent(parent) {} + virtual void updateCurrent() {} + virtual void updateData() { m_parent->slotUpdate(); } + NowPlaying *m_parent; + }; + friend struct Observer; + + Observer m_observer; + PlaylistCollection *m_collection; + QValueList<NowPlayingItem *> m_items; +}; + +/** + * Abstract base for the other NowPlaying items. + */ + +class NowPlayingItem +{ +public: + virtual void update(const FileHandle &file) = 0; + NowPlaying *parent() const { return m_parent; } +protected: + NowPlayingItem(NowPlaying *parent) : m_parent(parent) { parent->addItem(this); } +private: + NowPlaying *m_parent; +}; + +/** + * Displays the cover of the currently playing file if available, or hides + * itself if not. + */ + +class CoverItem : public QLabel, public NowPlayingItem +{ +public: + CoverItem(NowPlaying *parent); + virtual void update(const FileHandle &file); + virtual void mouseReleaseEvent(QMouseEvent *event); + +protected: + virtual void dragEnterEvent(QDragEnterEvent *e); + virtual void dropEvent(QDropEvent *e); + + virtual void mousePressEvent(QMouseEvent *e); + virtual void mouseMoveEvent(QMouseEvent *e); + +private: + FileHandle m_file; + bool m_dragging; + QPoint m_dragStart; +}; + +/** + * A link label that doesn't automatically open Konqueror. + */ + +class LinkLabel : public KActiveLabel +{ +public: + LinkLabel(QWidget *parent, const char *name = 0) : KActiveLabel(parent, name) {} + virtual void openLink(const QString &) {} +}; + +/** + * Show the text information on the current track and provides links to the + * album and artist of the currently playing item. + */ + +class TrackItem : public QWidget, public NowPlayingItem +{ + Q_OBJECT + +public: + TrackItem(NowPlaying *parent); + virtual void update(const FileHandle &file); + +private slots: + void slotOpenLink(const QString &link); + void slotUpdate(); + +private: + FileHandle m_file; + LinkLabel *m_label; +}; + +/** + * Shows up to 10 items of history and links to those items. + */ + +class HistoryItem : public LinkLabel, public NowPlayingItem +{ + Q_OBJECT + +public: + HistoryItem(NowPlaying *parent); + virtual void update(const FileHandle &file); + virtual void openLink(const QString &link); + +private slots: + void slotAddPlaying(); + +private: + struct Item + { + Item() {} + Item(const QString &a, const FileHandle &f, Playlist *p) + : anchor(a), file(f), playlist(p) {} + + QString anchor; + FileHandle file; + QGuardedPtr<Playlist> playlist; + }; + + QValueList<Item> m_history; + LinkLabel *m_label; + QTimer *m_timer; + FileHandle m_file; +}; + +#endif diff --git a/juk/pics/Makefile.am b/juk/pics/Makefile.am new file mode 100644 index 00000000..6dff1bce --- /dev/null +++ b/juk/pics/Makefile.am @@ -0,0 +1,2 @@ +pics_DATA = playing.png splash.png yahoo_credit.png +picsdir = $(kde_datadir)/juk/pics diff --git a/juk/pics/playing.png b/juk/pics/playing.png Binary files differnew file mode 100644 index 00000000..ba75b4d5 --- /dev/null +++ b/juk/pics/playing.png diff --git a/juk/pics/splash.png b/juk/pics/splash.png Binary files differnew file mode 100644 index 00000000..b61d7b59 --- /dev/null +++ b/juk/pics/splash.png diff --git a/juk/pics/yahoo_credit.png b/juk/pics/yahoo_credit.png Binary files differnew file mode 100644 index 00000000..cb64dbd7 --- /dev/null +++ b/juk/pics/yahoo_credit.png diff --git a/juk/player.h b/juk/player.h new file mode 100644 index 00000000..a3bd471a --- /dev/null +++ b/juk/player.h @@ -0,0 +1,50 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYER_H +#define PLAYER_H + +#include <qobject.h> + +#include "filehandle.h" + +class Player : public QObject +{ +public: + virtual ~Player() {} + + virtual void play(const FileHandle &file = FileHandle::null()) = 0; + virtual void pause() = 0; + virtual void stop() = 0; + + virtual void setVolume(float volume = 1.0) = 0; + virtual float volume() const = 0; + + virtual bool playing() const = 0; + virtual bool paused() const = 0; + + virtual int totalTime() const = 0; + virtual int currentTime() const = 0; + virtual int position() const = 0; // in this case not really the percent + + virtual void seek(int seekTime) = 0; + virtual void seekPosition(int position) = 0; + +protected: + Player() : QObject() {} + +}; + +#endif diff --git a/juk/playermanager.cpp b/juk/playermanager.cpp new file mode 100644 index 00000000..b60c7af5 --- /dev/null +++ b/juk/playermanager.cpp @@ -0,0 +1,689 @@ +/*************************************************************************** + begin : Sat Feb 14 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +/** + * Note to those who work here. The preprocessor variables HAVE_ARTS and HAVE_GSTREAMER + * are ::ALWAYS DEFINED::. You can't use #ifdef to see if they're present, you should just + * use #if. + * + * However, HAVE_AKODE is #define'd if present, and undefined if not present. + * - mpyne + */ + +#include <kdebug.h> +#include <klocale.h> + +#include <qslider.h> +#include <qtimer.h> + +#include <math.h> + +#include "artsplayer.h" +#include "akodeplayer.h" +#include "gstreamerplayer.h" +#include "playermanager.h" +#include "playlistinterface.h" +#include "slideraction.h" +#include "statuslabel.h" +#include "actioncollection.h" +#include "collectionlist.h" +#include "coverinfo.h" +#include "tag.h" + +#include "config.h" + +using namespace ActionCollection; + +enum PlayerManagerStatus { StatusStopped = -1, StatusPaused = 1, StatusPlaying = 2 }; + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +enum SoundSystem { ArtsBackend = 0, GStreamerBackend = 1, AkodeBackend = 2 }; + +static Player *createPlayer(int system = ArtsBackend) +{ + + Player *p = 0; + switch(system) { +#ifdef HAVE_AKODE + case AkodeBackend: + p = new aKodePlayer; + break; +#endif +#if HAVE_ARTS + case ArtsBackend: + p = new ArtsPlayer; + break; +#endif +#if HAVE_GSTREAMER + case GStreamerBackend: + p = new GStreamerPlayer; + break; +#endif + default: +#if HAVE_ARTS + p = new ArtsPlayer; +#elif HAVE_GSTREAMER + p = new GStreamerPlayer; +#else + p = new aKodePlayer; +#endif + break; + } + return p; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +PlayerManager::PlayerManager() : + Player(), + m_sliderAction(0), + m_playlistInterface(0), + m_statusLabel(0), + m_player(0), + m_timer(0), + m_noSeek(false), + m_muted(false), + m_setup(false) +{ +// 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(); +} + +PlayerManager::~PlayerManager() +{ + delete m_player; +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +PlayerManager *PlayerManager::instance() // static +{ + static PlayerManager manager; + return &manager; +} + +bool PlayerManager::playing() const +{ + if(!player()) + return false; + + return player()->playing(); +} + +bool PlayerManager::paused() const +{ + if(!player()) + return false; + + return player()->paused(); +} + +float PlayerManager::volume() const +{ + if(!player()) + return 0; + + return player()->volume(); +} + +int PlayerManager::status() const +{ + if(!player()) + return StatusStopped; + + if(player()->paused()) + return StatusPaused; + + if(player()->playing()) + return StatusPlaying; + + return 0; +} + +int PlayerManager::totalTime() const +{ + if(!player()) + return 0; + + return player()->totalTime(); +} + +int PlayerManager::currentTime() const +{ + if(!player()) + return 0; + + return player()->currentTime(); +} + +int PlayerManager::position() const +{ + if(!player()) + return 0; + + return player()->position(); +} + +QStringList PlayerManager::trackProperties() +{ + return FileHandle::properties(); +} + +QString PlayerManager::trackProperty(const QString &property) const +{ + if(!playing() && !paused()) + return QString::null; + + return m_file.property(property); +} + +QPixmap PlayerManager::trackCover(const QString &size) const +{ + if(!playing() && !paused()) + return QPixmap(); + + if(size.lower() == "small") + return m_file.coverInfo()->pixmap(CoverInfo::Thumbnail); + if(size.lower() == "large") + return m_file.coverInfo()->pixmap(CoverInfo::FullSize); + + return QPixmap(); +} + +FileHandle PlayerManager::playingFile() const +{ + return m_file; +} + +QString PlayerManager::playingString() const +{ + if(!playing()) + return QString::null; + + QString str = m_file.tag()->artist() + " - " + m_file.tag()->title(); + if(m_file.tag()->artist().isEmpty()) + str = m_file.tag()->title(); + + return str; +} + +void PlayerManager::setPlaylistInterface(PlaylistInterface *interface) +{ + m_playlistInterface = interface; +} + +void PlayerManager::setStatusLabel(StatusLabel *label) +{ + m_statusLabel = label; +} + +KSelectAction *PlayerManager::playerSelectAction(QObject *parent) // static +{ + KSelectAction *action = 0; + action = new KSelectAction(i18n("&Output To"), 0, parent, "outputSelect"); + QStringList l; + +#if HAVE_ARTS + l << i18n("aRts"); +#endif +#if HAVE_GSTREAMER + l << i18n("GStreamer"); +#endif +#ifdef HAVE_AKODE + l << i18n("aKode"); +#endif + + if(l.isEmpty()) { + kdError(65432) << "Your JuK seems to have no output backend possibilities.\n"; + l << i18n("aKode"); // Looks like akode is the default backend. + } + + action->setItems(l); + return action; +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void PlayerManager::play(const FileHandle &file) +{ + if(!player() || !m_playlistInterface) + return; + + if(file.isNull()) { + if(player()->paused()) + player()->play(); + else if(player()->playing()) { + if(m_sliderAction->trackPositionSlider()) + m_sliderAction->trackPositionSlider()->setValue(0); + player()->seekPosition(0); + } + else { + m_playlistInterface->playNext(); + m_file = m_playlistInterface->currentFile(); + + if(!m_file.isNull()) + player()->play(m_file); + } + } + else { + m_file = file; + player()->play(file); + } + + // Make sure that the player() actually starts before doing anything. + + if(!player()->playing()) { + kdWarning(65432) << "Unable to play " << file.absFilePath() << endl; + stop(); + return; + } + + 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); + + if(m_sliderAction->trackPositionSlider()) + m_sliderAction->trackPositionSlider()->setEnabled(true); + + m_timer->start(m_pollInterval); + + emit signalPlay(); +} + +void PlayerManager::play(const QString &file) +{ + CollectionListItem *item = CollectionList::instance()->lookup(file); + if(item) { + Playlist::setPlaying(item); + play(item->file()); + } +} + +void PlayerManager::play() +{ + play(FileHandle::null()); +} + +void PlayerManager::pause() +{ + if(!player()) + return; + + if(player()->paused()) { + play(); + return; + } + + m_timer->stop(); + action("pause")->setEnabled(false); + + player()->pause(); + + emit signalPause(); +} + +void PlayerManager::stop() +{ + if(!player() || !m_playlistInterface) + return; + + m_timer->stop(); + + action("pause")->setEnabled(false); + action("stop")->setEnabled(false); + action("back")->setEnabled(false); + action("forward")->setEnabled(false); + action("forwardAlbum")->setEnabled(false); + + if(m_sliderAction->trackPositionSlider()) { + m_sliderAction->trackPositionSlider()->setValue(0); + m_sliderAction->trackPositionSlider()->setEnabled(false); + } + + player()->stop(); + m_playlistInterface->stop(); + + m_file = FileHandle::null(); + + emit signalStop(); +} + +void PlayerManager::setVolume(float volume) +{ + if(!player()) + return; + + player()->setVolume(volume); +} + +void PlayerManager::seek(int seekTime) +{ + if(!player()) + return; + + player()->seek(seekTime); +} + +void PlayerManager::seekPosition(int position) +{ + if(!player()) + return; + + if(!player()->playing() || m_noSeek) + return; + + slotUpdateTime(position); + player()->seekPosition(position); + + if(m_sliderAction->trackPositionSlider()) + m_sliderAction->trackPositionSlider()->setValue(position); +} + +void PlayerManager::seekForward() +{ + seekPosition(kMin(SliderAction::maxPosition, position() + 10)); +} + +void PlayerManager::seekBack() +{ + seekPosition(kMax(SliderAction::minPosition, position() - 10)); +} + +void PlayerManager::playPause() +{ + playing() ? action("pause")->activate() : action("play")->activate(); +} + +void PlayerManager::forward() +{ + m_playlistInterface->playNext(); + FileHandle file = m_playlistInterface->currentFile(); + + if(!file.isNull()) + play(file); + else + stop(); +} + +void PlayerManager::back() +{ + m_playlistInterface->playPrevious(); + FileHandle file = m_playlistInterface->currentFile(); + + if(!file.isNull()) + play(file); + else + stop(); +} + +void PlayerManager::volumeUp() +{ + if(!player() || !m_sliderAction || !m_sliderAction->volumeSlider()) + return; + + int volume = m_sliderAction->volumeSlider()->volume() + + m_sliderAction->volumeSlider()->maxValue() / 25; // 4% up + + slotSetVolume(volume); + m_sliderAction->volumeSlider()->setVolume(volume); +} + +void PlayerManager::volumeDown() +{ + if(!player() || !m_sliderAction || !m_sliderAction->volumeSlider()) + return; + + int volume = m_sliderAction->volumeSlider()->value() - + m_sliderAction->volumeSlider()->maxValue() / 25; // 4% down + + slotSetVolume(volume); + m_sliderAction->volumeSlider()->setVolume(volume); +} + +void PlayerManager::mute() +{ + if(!player() || !m_sliderAction || !m_sliderAction->volumeSlider()) + return; + + slotSetVolume(m_muted ? m_sliderAction->volumeSlider()->value() : 0); + m_muted = !m_muted; +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void PlayerManager::slotPollPlay() +{ + if(!player() || !m_playlistInterface) + return; + + m_noSeek = true; + + if(!player()->playing()) { + m_timer->stop(); + + m_playlistInterface->playNext(); + FileHandle nextFile = m_playlistInterface->currentFile(); + if(!nextFile.isNull()) + play(nextFile); + else + stop(); + } + else if(!m_sliderAction->dragging()) { + if(m_sliderAction->trackPositionSlider()) + m_sliderAction->trackPositionSlider()->setValue(player()->position()); + + if(m_statusLabel) { + m_statusLabel->setItemTotalTime(player()->totalTime()); + m_statusLabel->setItemCurrentTime(player()->currentTime()); + } + } + + // This call is done because when the user adds the slider to the toolbar + // while playback is occuring the volume slider generally defaults to 0, + // and doesn't get updated to the correct volume. It might be better to + // have the SliderAction class fill in the correct volume, but I'm trying + // to avoid having it depend on PlayerManager since it may not be + // constructed in time during startup. -mpyne + + if(!m_sliderAction->volumeDragging() && m_sliderAction->volumeSlider()) + { + int maxV = m_sliderAction->volumeSlider()->maxValue(); + float v = sqrt(sqrt(volume())); // Cancel out exponential scaling + + m_sliderAction->volumeSlider()->blockSignals(true); + m_sliderAction->volumeSlider()->setVolume((int)((v) * maxV)); + m_sliderAction->volumeSlider()->blockSignals(false); + } + + // Ok, this is weird stuff, but it works pretty well. Ordinarily we don't + // need to check up on our playing time very often, but in the span of the + // last interval, we want to check a lot -- to figure out that we've hit the + // end of the song as soon as possible. + + if(player()->playing() && + player()->totalTime() > 0 && + float(player()->totalTime() - player()->currentTime()) < m_pollInterval * 2) + { + m_timer->changeInterval(50); + } + + m_noSeek = false; +} + +void PlayerManager::slotSetOutput(const QString &system) +{ + stop(); + setOutput(system); + setup(); +} + +void PlayerManager::setOutput(const QString &system) +{ + delete m_player; + if(system == i18n("aRts")) + m_player = createPlayer(ArtsBackend); + else if(system == i18n("GStreamer")) + m_player = createPlayer(GStreamerBackend); + else if(system == i18n("aKode")) + m_player = createPlayer(AkodeBackend); +} + +void PlayerManager::slotSetVolume(int volume) +{ + float scaledVolume; + + if(m_sliderAction->volumeSlider()) + scaledVolume = float(volume) / m_sliderAction->volumeSlider()->maxValue(); + else { + scaledVolume = float(volume) / 100.0; // Hopefully this is accurate + scaledVolume = kMin(1.0f, scaledVolume); + } + + // Perform exponential scaling to counteract the fact that humans perceive + // volume changes logarithmically. + + scaledVolume *= scaledVolume; + scaledVolume *= scaledVolume; + setVolume(scaledVolume); // scaledVolume ^ 4 +} + +void PlayerManager::slotUpdateTime(int position) +{ + if(!m_statusLabel) + return; + + float positionFraction = float(position) / SliderAction::maxPosition; + float totalTime = float(player()->totalTime()); + int seekTime = int(positionFraction * totalTime + 0.5); // "+0.5" for rounding + + m_statusLabel->setItemCurrentTime(seekTime); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +Player *PlayerManager::player() const +{ + if(!m_player) + instance()->setup(); + + return m_player; +} + +void PlayerManager::setup() +{ + // All of the actions required by this class should be listed here. + + if(!action("pause") || + !action("stop") || + !action("back") || + !action("forwardAlbum") || + !action("forward") || + !action("trackPositionAction")) + { + kdWarning(65432) << k_funcinfo << "Could not find all of the required actions." << endl; + return; + } + + if(m_setup) + return; + m_setup = true; + + // initialize action states + + action("pause")->setEnabled(false); + action("stop")->setEnabled(false); + action("back")->setEnabled(false); + action("forward")->setEnabled(false); + action("forwardAlbum")->setEnabled(false); + + // setup sliders + + m_sliderAction = action<SliderAction>("trackPositionAction"); + + connect(m_sliderAction, SIGNAL(signalPositionChanged(int)), + this, SLOT(seekPosition(int))); + connect(m_sliderAction->trackPositionSlider(), SIGNAL(valueChanged(int)), + this, SLOT(slotUpdateTime(int))); + connect(m_sliderAction, SIGNAL(signalVolumeChanged(int)), + this, SLOT(slotSetVolume(int))); + + // Call this method manually to avoid warnings. + + KAction *outputAction = actions()->action("outputSelect"); + + if(outputAction) { + setOutput(static_cast<KSelectAction *>(outputAction)->currentText()); + connect(outputAction, SIGNAL(activated(const QString &)), this, SLOT(slotSetOutput(const QString &))); + } + else + m_player = createPlayer(); + + float volume; + + if(m_sliderAction->volumeSlider()) { + volume = + float(m_sliderAction->volumeSlider()->volume()) / + float(m_sliderAction->volumeSlider()->maxValue()); + } + else + volume = 1; // Assume user wants full volume + + m_player->setVolume(volume); + + m_timer = new QTimer(this, "play timer"); + connect(m_timer, SIGNAL(timeout()), this, SLOT(slotPollPlay())); +} + +QString PlayerManager::randomPlayMode() const +{ + if(action<KToggleAction>("randomPlay")->isChecked()) + return "Random"; + if(action<KToggleAction>("albumRandomPlay")->isChecked()) + return "AlbumRandom"; + return "NoRandom"; +} + +void PlayerManager::setRandomPlayMode(const QString &randomMode) +{ + if(randomMode.lower() == "random") + action<KToggleAction>("randomPlay")->setChecked(true); + if(randomMode.lower() == "albumrandom") + action<KToggleAction>("albumRandomPlay")->setChecked(true); + if(randomMode.lower() == "norandom") + action<KToggleAction>("disableRandomPlay")->setChecked(true); +} + +#include "playermanager.moc" + +// vim: set et ts=4 sw=4: diff --git a/juk/playermanager.h b/juk/playermanager.h new file mode 100644 index 00000000..8f1920b1 --- /dev/null +++ b/juk/playermanager.h @@ -0,0 +1,117 @@ +/*************************************************************************** + begin : Sat Feb 14 2004 + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYERMANAGER_H +#define PLAYERMANAGER_H + +#include "player.h" +#include "jukIface.h" + +class QTimer; +class KSelectAction; +class SliderAction; +class StatusLabel; +class PlaylistInterface; + +/** + * This class serves as a proxy to the Player interface and handles managing + * the actions from the top-level mainwindow. + */ + +class PlayerManager : public Player, public PlayerIface +{ + Q_OBJECT + +protected: + PlayerManager(); + virtual ~PlayerManager(); + +public: + static PlayerManager *instance(); + + bool playing() const; + bool paused() const; + float volume() const; + int status() const; + int totalTime() const; + int currentTime() const; + int position() const; + + QStringList trackProperties(); + QString trackProperty(const QString &property) const; + QPixmap trackCover(const QString &size) const; + + FileHandle playingFile() const; + QString playingString() const; + + void setPlaylistInterface(PlaylistInterface *interface); + void setStatusLabel(StatusLabel *label); + + QString randomPlayMode() const; + + static KSelectAction *playerSelectAction(QObject *parent); + +public slots: + + void play(const FileHandle &file); + void play(const QString &file); + void play(); + void pause(); + void stop(); + void setVolume(float volume = 1.0); + void seek(int seekTime); + void seekPosition(int position); + void seekForward(); + void seekBack(); + void playPause(); + void forward(); + void back(); + void volumeUp(); + void volumeDown(); + void mute(); + + void setRandomPlayMode(const QString &randomMode); + +signals: + void signalPlay(); + void signalPause(); + void signalStop(); + +private: + Player *player() const; + void setup(); + void setOutput(const QString &); + +private slots: + void slotPollPlay(); + void slotUpdateTime(int position); + void slotSetOutput(const QString &); + void slotSetVolume(int volume); + +private: + FileHandle m_file; + SliderAction *m_sliderAction; + PlaylistInterface *m_playlistInterface; + StatusLabel *m_statusLabel; + Player *m_player; + QTimer *m_timer; + bool m_noSeek; + bool m_muted; + bool m_setup; + + static const int m_pollInterval = 800; +}; + +#endif diff --git a/juk/playlist.cpp b/juk/playlist.cpp new file mode 100644 index 00000000..b090dca2 --- /dev/null +++ b/juk/playlist.cpp @@ -0,0 +1,2361 @@ +/*************************************************************************** + begin : Sat Feb 16 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kconfig.h> +#include <kmessagebox.h> +#include <kurldrag.h> +#include <kiconloader.h> +#include <klineedit.h> +#include <kaction.h> +#include <kpopupmenu.h> +#include <klocale.h> +#include <kdebug.h> +#include <kinputdialog.h> +#include <kfiledialog.h> +#include <kglobalsettings.h> +#include <kurl.h> +#include <kio/netaccess.h> +#include <kio/job.h> +#include <dcopclient.h> + +#include <qheader.h> +#include <qcursor.h> +#include <qdir.h> +#include <qeventloop.h> +#include <qtooltip.h> +#include <qwidgetstack.h> +#include <qfile.h> +#include <qhbox.h> + +#include <id3v1genres.h> + +#include <time.h> +#include <math.h> +#include <dirent.h> + +#include "playlist.h" +#include "playlistitem.h" +#include "playlistcollection.h" +#include "playlistsearch.h" +#include "mediafiles.h" +#include "collectionlist.h" +#include "filerenamer.h" +#include "actioncollection.h" +#include "tracksequencemanager.h" +#include "juk.h" +#include "tag.h" +#include "k3bexporter.h" +#include "upcomingplaylist.h" +#include "deletedialog.h" +#include "webimagefetcher.h" +#include "coverinfo.h" +#include "coverdialog.h" +#include "tagtransactionmanager.h" +#include "cache.h" + +using namespace ActionCollection; + +/** + * Just a shortcut of sorts. + */ + +static bool manualResize() +{ + return action<KToggleAction>("resizeColumnsManually")->isChecked(); +} + +/** + * A tooltip specialized to show full filenames over the file name column. + */ + +class PlaylistToolTip : public QToolTip +{ +public: + PlaylistToolTip(QWidget *parent, Playlist *playlist) : + QToolTip(parent), m_playlist(playlist) {} + + virtual void maybeTip(const QPoint &p) + { + PlaylistItem *item = static_cast<PlaylistItem *>(m_playlist->itemAt(p)); + + if(!item) + return; + + QPoint contentsPosition = m_playlist->viewportToContents(p); + + int column = m_playlist->header()->sectionAt(contentsPosition.x()); + + if(column == m_playlist->columnOffset() + PlaylistItem::FileNameColumn || + item->cachedWidths()[column] > m_playlist->columnWidth(column) || + (column == m_playlist->columnOffset() + PlaylistItem::CoverColumn && + item->file().coverInfo()->hasCover())) + { + QRect r = m_playlist->itemRect(item); + int headerPosition = m_playlist->header()->sectionPos(column); + r.setLeft(headerPosition); + r.setRight(headerPosition + m_playlist->header()->sectionSize(column)); + + if(column == m_playlist->columnOffset() + PlaylistItem::FileNameColumn) + tip(r, item->file().absFilePath()); + else if(column == m_playlist->columnOffset() + PlaylistItem::CoverColumn) { + QMimeSourceFactory *f = QMimeSourceFactory::defaultFactory(); + f->setImage("coverThumb", + QImage(item->file().coverInfo()->pixmap(CoverInfo::Thumbnail).convertToImage())); + tip(r, "<center><img source=\"coverThumb\"/></center>"); + } + else + tip(r, item->text(column)); + } + } + +private: + Playlist *m_playlist; +}; + +//////////////////////////////////////////////////////////////////////////////// +// Playlist::SharedSettings definition +//////////////////////////////////////////////////////////////////////////////// + +bool Playlist::m_visibleChanged = false; +bool Playlist::m_shuttingDown = false; + +/** + * Shared settings between the playlists. + */ + +class Playlist::SharedSettings +{ +public: + static SharedSettings *instance(); + /** + * Sets the default column order to that of Playlist @param p. + */ + void setColumnOrder(const Playlist *l); + void toggleColumnVisible(int column); + void setInlineCompletionMode(KGlobalSettings::Completion mode); + + /** + * Apply the settings. + */ + void apply(Playlist *l) const; + void sync() { writeConfig(); } + +protected: + SharedSettings(); + ~SharedSettings() {} + +private: + void writeConfig(); + + static SharedSettings *m_instance; + QValueList<int> m_columnOrder; + QValueVector<bool> m_columnsVisible; + KGlobalSettings::Completion m_inlineCompletion; +}; + +Playlist::SharedSettings *Playlist::SharedSettings::m_instance = 0; + +//////////////////////////////////////////////////////////////////////////////// +// Playlist::SharedSettings public members +//////////////////////////////////////////////////////////////////////////////// + +Playlist::SharedSettings *Playlist::SharedSettings::instance() +{ + static SharedSettings settings; + return &settings; +} + +void Playlist::SharedSettings::setColumnOrder(const Playlist *l) +{ + if(!l) + return; + + m_columnOrder.clear(); + + for(int i = l->columnOffset(); i < l->columns(); ++i) + m_columnOrder.append(l->header()->mapToIndex(i)); + + writeConfig(); +} + +void Playlist::SharedSettings::toggleColumnVisible(int column) +{ + if(column >= int(m_columnsVisible.size())) + m_columnsVisible.resize(column + 1, true); + + m_columnsVisible[column] = !m_columnsVisible[column]; + + writeConfig(); +} + +void Playlist::SharedSettings::setInlineCompletionMode(KGlobalSettings::Completion mode) +{ + m_inlineCompletion = mode; + writeConfig(); +} + + +void Playlist::SharedSettings::apply(Playlist *l) const +{ + if(!l) + return; + + int offset = l->columnOffset(); + int i = 0; + for(QValueListConstIterator<int> it = m_columnOrder.begin(); it != m_columnOrder.end(); ++it) + l->header()->moveSection(i++ + offset, *it + offset); + + for(uint i = 0; i < m_columnsVisible.size(); i++) { + if(m_columnsVisible[i] && !l->isColumnVisible(i + offset)) + l->showColumn(i + offset, false); + else if(!m_columnsVisible[i] && l->isColumnVisible(i + offset)) + l->hideColumn(i + offset, false); + } + + l->updateLeftColumn(); + l->renameLineEdit()->setCompletionMode(m_inlineCompletion); + l->slotColumnResizeModeChanged(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Playlist::ShareSettings protected members +//////////////////////////////////////////////////////////////////////////////// + +Playlist::SharedSettings::SharedSettings() +{ + KConfigGroup config(KGlobal::config(), "PlaylistShared"); + + bool resizeColumnsManually = config.readBoolEntry("ResizeColumnsManually", false); + action<KToggleAction>("resizeColumnsManually")->setChecked(resizeColumnsManually); + + // save column order + m_columnOrder = config.readIntListEntry("ColumnOrder"); + + QValueList<int> l = config.readIntListEntry("VisibleColumns"); + + if(l.isEmpty()) { + + // Provide some default values for column visibility if none were + // read from the configuration file. + + for(int i = 0; i <= PlaylistItem::lastColumn(); i++) { + switch(i) { + case PlaylistItem::BitrateColumn: + case PlaylistItem::CommentColumn: + case PlaylistItem::FileNameColumn: + case PlaylistItem::FullPathColumn: + m_columnsVisible.append(false); + break; + default: + m_columnsVisible.append(true); + } + } + } + else { + // Convert the int list into a bool list. + + m_columnsVisible.resize(l.size(), true); + uint i = 0; + for(QValueList<int>::Iterator it = l.begin(); it != l.end(); ++it) { + if(! bool(*it)) + m_columnsVisible[i] = bool(*it); + i++; + } + } + + m_inlineCompletion = KGlobalSettings::Completion( + config.readNumEntry("InlineCompletionMode", KGlobalSettings::CompletionAuto)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Playlist::SharedSettings private members +//////////////////////////////////////////////////////////////////////////////// + +void Playlist::SharedSettings::writeConfig() +{ + KConfigGroup config(KGlobal::config(), "PlaylistShared"); + config.writeEntry("ColumnOrder", m_columnOrder); + + QValueList<int> l; + for(uint i = 0; i < m_columnsVisible.size(); i++) + l.append(int(m_columnsVisible[i])); + + config.writeEntry("VisibleColumns", l); + config.writeEntry("InlineCompletionMode", m_inlineCompletion); + + config.writeEntry("ResizeColumnsManually", manualResize()); + + KGlobal::config()->sync(); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +PlaylistItemList Playlist::m_history; +QMap<int, PlaylistItem *> Playlist::m_backMenuItems; +int Playlist::m_leftColumn = 0; + +Playlist::Playlist(PlaylistCollection *collection, const QString &name, + const QString &iconName) : + KListView(collection->playlistStack(), name.latin1()), + m_collection(collection), + m_fetcher(new WebImageFetcher(this)), + m_selectedCount(0), + m_allowDuplicates(false), + m_polished(false), + m_applySharedSettings(true), + m_columnWidthModeChanged(false), + m_disableColumnWidthUpdates(true), + m_time(0), + m_widthsDirty(true), + m_searchEnabled(true), + m_lastSelected(0), + m_playlistName(name), + m_rmbMenu(0), + m_toolTip(0), + m_blockDataChanged(false) +{ + setup(); + collection->setupPlaylist(this, iconName); +} + +Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, + const QString &name, const QString &iconName) : + KListView(collection->playlistStack(), name.latin1()), + m_collection(collection), + m_fetcher(new WebImageFetcher(this)), + m_selectedCount(0), + m_allowDuplicates(false), + m_polished(false), + m_applySharedSettings(true), + m_columnWidthModeChanged(false), + m_disableColumnWidthUpdates(true), + m_time(0), + m_widthsDirty(true), + m_searchEnabled(true), + m_lastSelected(0), + m_playlistName(name), + m_rmbMenu(0), + m_toolTip(0), + m_blockDataChanged(false) +{ + setup(); + collection->setupPlaylist(this, iconName); + createItems(items); +} + +Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, + const QString &iconName) : + KListView(collection->playlistStack()), + m_collection(collection), + m_fetcher(new WebImageFetcher(this)), + m_selectedCount(0), + m_allowDuplicates(false), + m_polished(false), + m_applySharedSettings(true), + m_columnWidthModeChanged(false), + m_disableColumnWidthUpdates(true), + m_time(0), + m_widthsDirty(true), + m_searchEnabled(true), + m_lastSelected(0), + m_fileName(playlistFile.absFilePath()), + m_rmbMenu(0), + m_toolTip(0), + m_blockDataChanged(false) +{ + setup(); + loadFile(m_fileName, playlistFile); + collection->setupPlaylist(this, iconName); +} + +Playlist::Playlist(PlaylistCollection *collection, bool delaySetup) : + KListView(collection->playlistStack()), + m_collection(collection), + m_fetcher(new WebImageFetcher(this)), + m_selectedCount(0), + m_allowDuplicates(false), + m_polished(false), + m_applySharedSettings(true), + m_columnWidthModeChanged(false), + m_disableColumnWidthUpdates(true), + m_time(0), + m_widthsDirty(true), + m_searchEnabled(true), + m_lastSelected(0), + m_rmbMenu(0), + m_toolTip(0), + m_blockDataChanged(false) +{ + setup(); + + if(!delaySetup) + collection->setupPlaylist(this, "midi"); +} + +Playlist::~Playlist() +{ + // clearItem() will take care of removing the items from the history, + // so call clearItems() to make sure it happens. + + clearItems(items()); + + delete m_toolTip; + + // Select a different playlist if we're the selected one + + if(isVisible() && this != CollectionList::instance()) + m_collection->raise(CollectionList::instance()); + + if(!m_shuttingDown) + m_collection->removePlaylist(this); +} + +QString Playlist::name() const +{ + if(m_playlistName.isNull()) + return m_fileName.section(QDir::separator(), -1).section('.', 0, -2); + else + return m_playlistName; +} + +FileHandle Playlist::currentFile() const +{ + return playingItem() ? playingItem()->file() : FileHandle::null(); +} + +int Playlist::time() const +{ + // Since this method gets a lot of traffic, let's optimize for such. + + if(!m_addTime.isEmpty()) { + for(PlaylistItemList::ConstIterator it = m_addTime.begin(); + it != m_addTime.end(); ++it) + { + if(*it) + m_time += (*it)->file().tag()->seconds(); + } + + m_addTime.clear(); + } + + if(!m_subtractTime.isEmpty()) { + for(PlaylistItemList::ConstIterator it = m_subtractTime.begin(); + it != m_subtractTime.end(); ++it) + { + if(*it) + m_time -= (*it)->file().tag()->seconds(); + } + + m_subtractTime.clear(); + } + + return m_time; +} + +void Playlist::playFirst() +{ + TrackSequenceManager::instance()->setNextItem(static_cast<PlaylistItem *>( + QListViewItemIterator(const_cast<Playlist *>(this), QListViewItemIterator::Visible).current())); + action("forward")->activate(); +} + +void Playlist::playNextAlbum() +{ + PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); + if(!current) + return; // No next album if we're not already playing. + + QString currentAlbum = current->file().tag()->album(); + current = TrackSequenceManager::instance()->nextItem(); + + while(current && current->file().tag()->album() == currentAlbum) + current = TrackSequenceManager::instance()->nextItem(); + + TrackSequenceManager::instance()->setNextItem(current); + action("forward")->activate(); +} + +void Playlist::playNext() +{ + TrackSequenceManager::instance()->setCurrentPlaylist(this); + setPlaying(TrackSequenceManager::instance()->nextItem()); +} + +void Playlist::stop() +{ + m_history.clear(); + setPlaying(0); +} + +void Playlist::playPrevious() +{ + if(!playingItem()) + return; + + bool random = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked(); + + PlaylistItem *previous = 0; + + if(random && !m_history.isEmpty()) { + PlaylistItemList::Iterator last = m_history.fromLast(); + previous = *last; + m_history.remove(last); + } + else { + m_history.clear(); + previous = TrackSequenceManager::instance()->previousItem(); + } + + if(!previous) + previous = static_cast<PlaylistItem *>(playingItem()->itemAbove()); + + setPlaying(previous, false); +} + +void Playlist::setName(const QString &n) +{ + m_collection->addNameToDict(n); + m_collection->removeNameFromDict(m_playlistName); + + m_playlistName = n; + emit signalNameChanged(m_playlistName); +} + +void Playlist::save() +{ + if(m_fileName.isEmpty()) + return saveAs(); + + QFile file(m_fileName); + + if(!file.open(IO_WriteOnly)) + return KMessageBox::error(this, i18n("Could not save to file %1.").arg(m_fileName)); + + QTextStream stream(&file); + + QStringList fileList = files(); + + for(QStringList::Iterator it = fileList.begin(); it != fileList.end(); ++it) + stream << *it << endl; + + file.close(); +} + +void Playlist::saveAs() +{ + m_collection->removeFileFromDict(m_fileName); + + m_fileName = MediaFiles::savePlaylistDialog(name(), this); + + if(!m_fileName.isEmpty()) { + m_collection->addFileToDict(m_fileName); + + // If there's no playlist name set, use the file name. + if(m_playlistName.isEmpty()) + emit signalNameChanged(name()); + save(); + } +} + +void Playlist::clearItem(PlaylistItem *item, bool emitChanged) +{ + emit signalAboutToRemove(item); + m_members.remove(item->file().absFilePath()); + m_search.clearItem(item); + + m_history.remove(item); + m_addTime.remove(item); + m_subtractTime.remove(item); + + delete item; + if(emitChanged) + dataChanged(); +} + +void Playlist::clearItems(const PlaylistItemList &items) +{ + m_blockDataChanged = true; + + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) + clearItem(*it, false); + + m_blockDataChanged = false; + + dataChanged(); +} + +PlaylistItem *Playlist::playingItem() // static +{ + return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); +} + +QStringList Playlist::files() const +{ + QStringList list; + + for(QListViewItemIterator it(const_cast<Playlist *>(this)); it.current(); ++it) + list.append(static_cast<PlaylistItem *>(*it)->file().absFilePath()); + + return list; +} + +PlaylistItemList Playlist::items() +{ + return items(QListViewItemIterator::IteratorFlag(0)); +} + +PlaylistItemList Playlist::visibleItems() +{ + return items(QListViewItemIterator::Visible); +} + +PlaylistItemList Playlist::selectedItems() +{ + PlaylistItemList list; + + switch(m_selectedCount) { + case 0: + break; + // case 1: + // list.append(m_lastSelected); + // break; + default: + list = items(QListViewItemIterator::IteratorFlag(QListViewItemIterator::Selected | + QListViewItemIterator::Visible)); + break; + } + + return list; +} + +PlaylistItem *Playlist::firstChild() const +{ + return static_cast<PlaylistItem *>(KListView::firstChild()); +} + +void Playlist::updateLeftColumn() +{ + int newLeftColumn = leftMostVisibleColumn(); + + if(m_leftColumn != newLeftColumn) { + updatePlaying(); + m_leftColumn = newLeftColumn; + } +} + +void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static +{ + m_visibleChanged = true; + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) + (*it)->setVisible(visible); +} + +void Playlist::setSearch(const PlaylistSearch &s) +{ + m_search = s; + + if(!m_searchEnabled) + return; + + setItemsVisible(s.matchedItems(), true); + setItemsVisible(s.unmatchedItems(), false); + + TrackSequenceManager::instance()->iterator()->playlistChanged(); +} + +void Playlist::setSearchEnabled(bool enabled) +{ + if(m_searchEnabled == enabled) + return; + + m_searchEnabled = enabled; + + if(enabled) { + setItemsVisible(m_search.matchedItems(), true); + setItemsVisible(m_search.unmatchedItems(), false); + } + else + setItemsVisible(items(), true); +} + +void Playlist::markItemSelected(PlaylistItem *item, bool selected) +{ + if(selected && !item->isSelected()) { + m_selectedCount++; + m_lastSelected = item; + } + else if(!selected && item->isSelected()) + m_selectedCount--; +} + +void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster) +{ + for(PlaylistList::ConstIterator it = sources.begin(); it != sources.end(); ++it) { + if((*it)->playing()) { + CollectionListItem *base = playingItem()->collectionItem(); + for(QListViewItemIterator itemIt(this); itemIt.current(); ++itemIt) { + PlaylistItem *item = static_cast<PlaylistItem *>(itemIt.current()); + if(base == item->collectionItem()) { + item->setPlaying(true, setMaster); + PlaylistItemList playing = PlaylistItem::playingItems(); + TrackSequenceManager::instance()->setCurrent(item); + return; + } + } + return; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void Playlist::copy() +{ + kapp->clipboard()->setData(dragObject(0), QClipboard::Clipboard); +} + +void Playlist::paste() +{ + decode(kapp->clipboard()->data(), static_cast<PlaylistItem *>(currentItem())); +} + +void Playlist::clear() +{ + PlaylistItemList l = selectedItems(); + if(l.isEmpty()) + l = items(); + + clearItems(l); +} + +void Playlist::slotRefresh() +{ + PlaylistItemList l = selectedItems(); + if(l.isEmpty()) + l = visibleItems(); + + KApplication::setOverrideCursor(Qt::waitCursor); + for(PlaylistItemList::Iterator it = l.begin(); it != l.end(); ++it) { + (*it)->refreshFromDisk(); + + if(!(*it)->file().tag() || !(*it)->file().fileInfo().exists()) { + kdDebug(65432) << "Error while trying to refresh the tag. " + << "This file has probably been removed." + << endl; + clearItem((*it)->collectionItem()); + } + + processEvents(); + } + KApplication::restoreOverrideCursor(); +} + +void Playlist::slotRenameFile() +{ + FileRenamer renamer; + PlaylistItemList items = selectedItems(); + + if(items.isEmpty()) + return; + + emit signalEnableDirWatch(false); + + m_blockDataChanged = true; + renamer.rename(items); + m_blockDataChanged = false; + dataChanged(); + + emit signalEnableDirWatch(true); +} + +void Playlist::slotViewCover() +{ + PlaylistItemList items = selectedItems(); + if (items.isEmpty()) + return; + for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) + (*it)->file().coverInfo()->popup(); +} + +void Playlist::slotRemoveCover() +{ + PlaylistItemList items = selectedItems(); + if(items.isEmpty()) + return; + int button = KMessageBox::warningContinueCancel(this, + i18n("Are you sure you want to delete these covers?"), + QString::null, + i18n("&Delete Covers")); + if(button == KMessageBox::Continue) + refreshAlbums(items); +} + +void Playlist::slotShowCoverManager() +{ + static CoverDialog *managerDialog = 0; + + if(!managerDialog) + managerDialog = new CoverDialog(this); + + managerDialog->show(); +} + +unsigned int Playlist::eligibleCoverItems(const PlaylistItemList &items) +{ + // This used to count the number of tracks with an artist and album, that + // is not strictly required anymore. This may prove useful in the future + // so I'm leaving it in for now, right now we just mark every item as + // eligible. + + return items.count(); +} + +void Playlist::slotAddCover(bool retrieveLocal) +{ + PlaylistItemList items = selectedItems(); + + if(items.isEmpty()) + return; + + if(eligibleCoverItems(items) == 0) { + // No items in the list can be assigned a cover, inform the user and + // bail. + + // KDE 4.0 Fix this string. + KMessageBox::sorry(this, i18n("None of the items you have selected can " + "be assigned a cover. A track must have both the Artist " + "and Album tags set to be assigned a cover.")); + + return; + } + + QPixmap newCover; + + if(retrieveLocal) { + KURL file = KFileDialog::getImageOpenURL( + ":homedir", this, i18n("Select Cover Image File")); + newCover = QPixmap(file.directory() + "/" + file.fileName()); + } + else { + m_fetcher->setFile((*items.begin())->file()); + m_fetcher->chooseCover(); + return; + } + + if(newCover.isNull()) + return; + + QString artist = items.front()->file().tag()->artist(); + QString album = items.front()->file().tag()->album(); + + coverKey newId = CoverManager::addCover(newCover, artist, album); + refreshAlbums(items, newId); +} + +// Called when image fetcher has added a new cover. +void Playlist::slotCoverChanged(int coverId) +{ + kdDebug(65432) << "Refreshing information for newly changed covers.\n"; + refreshAlbums(selectedItems(), coverId); +} + +void Playlist::slotGuessTagInfo(TagGuesser::Type type) +{ + KApplication::setOverrideCursor(Qt::waitCursor); + PlaylistItemList items = selectedItems(); + setDynamicListsFrozen(true); + + m_blockDataChanged = true; + + for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) { + (*it)->guessTagInfo(type); + processEvents(); + } + + // MusicBrainz queries automatically commit at this point. What would + // be nice is having a signal emitted when the last query is completed. + + if(type == TagGuesser::FileName) + TagTransactionManager::instance()->commit(); + + m_blockDataChanged = false; + + dataChanged(); + setDynamicListsFrozen(false); + KApplication::restoreOverrideCursor(); +} + +void Playlist::slotReload() +{ + QFileInfo fileInfo(m_fileName); + if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) + return; + + clearItems(items()); + loadFile(m_fileName, fileInfo); +} + +void Playlist::slotWeightDirty(int column) +{ + if(column < 0) { + m_weightDirty.clear(); + for(int i = 0; i < columns(); i++) { + if(isColumnVisible(i)) + m_weightDirty.append(i); + } + return; + } + + if(m_weightDirty.find(column) == m_weightDirty.end()) + m_weightDirty.append(column); +} + +void Playlist::slotShowPlaying() +{ + if(!playingItem()) + return; + + Playlist *l = playingItem()->playlist(); + + l->clearSelection(); + + // Raise the playlist before selecting the items otherwise the tag editor + // will not update when it gets the selectionChanged() notification + // because it will think the user is choosing a different playlist but not + // selecting a different item. + + m_collection->raise(l); + + l->setSelected(playingItem(), true); + l->setCurrentItem(playingItem()); + l->ensureItemVisible(playingItem()); +} + +void Playlist::slotColumnResizeModeChanged() +{ + if(manualResize()) + setHScrollBarMode(Auto); + else + setHScrollBarMode(AlwaysOff); + + if(!manualResize()) + slotUpdateColumnWidths(); + + SharedSettings::instance()->sync(); +} + +void Playlist::dataChanged() +{ + if(m_blockDataChanged) + return; + PlaylistInterface::dataChanged(); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +void Playlist::removeFromDisk(const PlaylistItemList &items) +{ + if(isVisible() && !items.isEmpty()) { + + QStringList files; + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) + files.append((*it)->file().absFilePath()); + + DeleteDialog dialog(this); + + m_blockDataChanged = true; + + if(dialog.confirmDeleteList(files)) { + bool shouldDelete = dialog.shouldDelete(); + QStringList errorFiles; + + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + if(playingItem() == *it) + action("forward")->activate(); + + QString removePath = (*it)->file().absFilePath(); + if((!shouldDelete && KIO::NetAccess::synchronousRun(KIO::trash(removePath), this)) || + (shouldDelete && QFile::remove(removePath))) + { + CollectionList::instance()->clearItem((*it)->collectionItem()); + } + else + errorFiles.append((*it)->file().absFilePath()); + } + + if(!errorFiles.isEmpty()) { + QString errorMsg = shouldDelete ? + i18n("Could not delete these files") : + i18n("Could not move these files to the Trash"); + KMessageBox::errorList(this, errorMsg, errorFiles); + } + } + + m_blockDataChanged = false; + + dataChanged(); + } +} + +QDragObject *Playlist::dragObject(QWidget *parent) +{ + PlaylistItemList items = selectedItems(); + KURL::List urls; + for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) { + KURL url; + url.setPath((*it)->file().absFilePath()); + urls.append(url); + } + + KURLDrag *drag = new KURLDrag(urls, parent, "Playlist Items"); + drag->setPixmap(BarIcon("sound")); + + return drag; +} + +void Playlist::contentsDragEnterEvent(QDragEnterEvent *e) +{ + KListView::contentsDragEnterEvent(e); + + if(CoverDrag::canDecode(e)) { + setDropHighlighter(true); + setDropVisualizer(false); + + e->accept(); + return; + } + + setDropHighlighter(false); + setDropVisualizer(true); + + KURL::List urls; + if(!KURLDrag::decode(e, urls) || urls.isEmpty()) { + e->ignore(); + return; + } + + e->accept(); + return; +} + +bool Playlist::acceptDrag(QDropEvent *e) const +{ + return CoverDrag::canDecode(e) || KURLDrag::canDecode(e); +} + +bool Playlist::canDecode(QMimeSource *s) +{ + KURL::List urls; + + if(CoverDrag::canDecode(s)) + return true; + + return KURLDrag::decode(s, urls) && !urls.isEmpty(); +} + +void Playlist::decode(QMimeSource *s, PlaylistItem *item) +{ + KURL::List urls; + + if(!KURLDrag::decode(s, urls) || urls.isEmpty()) + return; + + // handle dropped images + + if(!MediaFiles::isMediaFile(urls.front().path())) { + + QString file; + + if(urls.front().isLocalFile()) + file = urls.front().path(); + else + KIO::NetAccess::download(urls.front(), file, 0); + + KMimeType::Ptr mimeType = KMimeType::findByPath(file); + + if(item && mimeType->name().startsWith("image/")) { + item->file().coverInfo()->setCover(QImage(file)); + refreshAlbum(item->file().tag()->artist(), + item->file().tag()->album()); + } + + KIO::NetAccess::removeTempFile(file); + } + + QStringList fileList; + + for(KURL::List::Iterator it = urls.begin(); it != urls.end(); ++it) + fileList += MediaFiles::convertURLsToLocal((*it).path(), this); + + addFiles(fileList, item); +} + +bool Playlist::eventFilter(QObject *watched, QEvent *e) +{ + if(watched == header()) { + switch(e->type()) { + case QEvent::MouseMove: + { + if((static_cast<QMouseEvent *>(e)->state() & LeftButton) == LeftButton && + !action<KToggleAction>("resizeColumnsManually")->isChecked()) + { + m_columnWidthModeChanged = true; + + action<KToggleAction>("resizeColumnsManually")->setChecked(true); + slotColumnResizeModeChanged(); + } + + break; + } + case QEvent::MouseButtonPress: + { + if(static_cast<QMouseEvent *>(e)->button() == RightButton) + m_headerMenu->popup(QCursor::pos()); + + break; + } + case QEvent::MouseButtonRelease: + { + if(m_columnWidthModeChanged) { + m_columnWidthModeChanged = false; + notifyUserColumnWidthModeChanged(); + } + + if(!manualResize() && m_widthsDirty) + QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths())); + break; + } + default: + break; + } + } + + return KListView::eventFilter(watched, e); +} + +void Playlist::keyPressEvent(QKeyEvent *event) +{ + if(event->key() == Key_Up) { + QListViewItemIterator selected(this, QListViewItemIterator::IteratorFlag( + QListViewItemIterator::Selected | + QListViewItemIterator::Visible)); + if(selected.current()) { + QListViewItemIterator visible(this, QListViewItemIterator::IteratorFlag( + QListViewItemIterator::Visible)); + if(selected.current() == visible.current()) + KApplication::postEvent(parent(), new FocusUpEvent); + } + + } + + KListView::keyPressEvent(event); +} + +void Playlist::contentsDropEvent(QDropEvent *e) +{ + QPoint vp = contentsToViewport(e->pos()); + PlaylistItem *item = static_cast<PlaylistItem *>(itemAt(vp)); + + // First see if we're dropping a cover, if so we can get it out of the + // way early. + if(item && CoverDrag::canDecode(e)) { + coverKey id; + CoverDrag::decode(e, id); + + // If the item we dropped on is selected, apply cover to all selected + // items, otherwise just apply to the dropped item. + + if(item->isSelected()) { + PlaylistItemList selItems = selectedItems(); + for(PlaylistItemList::Iterator it = selItems.begin(); it != selItems.end(); ++it) { + (*it)->file().coverInfo()->setCoverId(id); + (*it)->refresh(); + } + } + else { + item->file().coverInfo()->setCoverId(id); + item->refresh(); + } + + return; + } + + // When dropping on the upper half of an item, insert before this item. + // This is what the user expects, and also allows the insertion at + // top of the list + + if(!item) + item = static_cast<PlaylistItem *>(lastItem()); + else if(vp.y() < item->itemPos() + item->height() / 2) + item = static_cast<PlaylistItem *>(item->itemAbove()); + + m_blockDataChanged = true; + + if(e->source() == this) { + + // Since we're trying to arrange things manually, turn off sorting. + + setSorting(columns() + 1); + + QPtrList<QListViewItem> items = KListView::selectedItems(); + + for(QPtrListIterator<QListViewItem> it(items); it.current(); ++it) { + if(!item) { + + // Insert the item at the top of the list. This is a bit ugly, + // but I don't see another way. + + takeItem(it.current()); + insertItem(it.current()); + } + else + it.current()->moveItem(item); + + item = static_cast<PlaylistItem *>(it.current()); + } + } + else + decode(e, item); + + m_blockDataChanged = false; + + dataChanged(); + emit signalPlaylistItemsDropped(this); + KListView::contentsDropEvent(e); +} + +void Playlist::contentsMouseDoubleClickEvent(QMouseEvent *e) +{ + // Filter out non left button double clicks, that way users don't have the + // weird experience of switching songs from a double right-click. + + if(e->button() == LeftButton) + KListView::contentsMouseDoubleClickEvent(e); +} + +void Playlist::showEvent(QShowEvent *e) +{ + if(m_applySharedSettings) { + SharedSettings::instance()->apply(this); + m_applySharedSettings = false; + } + KListView::showEvent(e); +} + +void Playlist::applySharedSettings() +{ + m_applySharedSettings = true; +} + +void Playlist::read(QDataStream &s) +{ + QString buffer; + + s >> m_playlistName + >> m_fileName; + + QStringList files; + s >> files; + + QListViewItem *after = 0; + + m_blockDataChanged = true; + + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) + after = createItem(FileHandle(*it), after, false); + + m_blockDataChanged = false; + + dataChanged(); + m_collection->setupPlaylist(this, "midi"); +} + +void Playlist::viewportPaintEvent(QPaintEvent *pe) +{ + // If there are columns that need to be updated, well, update them. + + if(!m_weightDirty.isEmpty() && !manualResize()) + { + calculateColumnWeights(); + slotUpdateColumnWidths(); + } + + KListView::viewportPaintEvent(pe); +} + +void Playlist::viewportResizeEvent(QResizeEvent *re) +{ + // If the width of the view has changed, manually update the column + // widths. + + if(re->size().width() != re->oldSize().width() && !manualResize()) + slotUpdateColumnWidths(); + + KListView::viewportResizeEvent(re); +} + +void Playlist::insertItem(QListViewItem *item) +{ + // Because we're called from the PlaylistItem ctor, item may not be a + // PlaylistItem yet (it would be QListViewItem when being inserted. But, + // it will be a PlaylistItem by the time it matters, but be careful if + // you need to use the PlaylistItem from here. + + m_addTime.append(static_cast<PlaylistItem *>(item)); + KListView::insertItem(item); +} + +void Playlist::takeItem(QListViewItem *item) +{ + // See the warning in Playlist::insertItem. + + m_subtractTime.append(static_cast<PlaylistItem *>(item)); + KListView::takeItem(item); +} + +void Playlist::addColumn(const QString &label) +{ + slotWeightDirty(columns()); + KListView::addColumn(label, 30); +} + +PlaylistItem *Playlist::createItem(const FileHandle &file, + QListViewItem *after, bool emitChanged) +{ + return createItem<PlaylistItem, CollectionListItem, CollectionList>(file, after, emitChanged); +} + +void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) +{ + createItems<CollectionListItem, PlaylistItem, PlaylistItem>(siblings, after); +} + +void Playlist::addFiles(const QStringList &files, PlaylistItem *after) +{ + if(!after) + after = static_cast<PlaylistItem *>(lastItem()); + + KApplication::setOverrideCursor(Qt::waitCursor); + + m_blockDataChanged = true; + + FileHandleList queue; + + const QStringList::ConstIterator filesEnd = files.end(); + for(QStringList::ConstIterator it = files.begin(); it != filesEnd; ++it) + addFile(*it, queue, true, &after); + + addFileHelper(queue, &after, true); + + m_blockDataChanged = false; + + slotWeightDirty(); + dataChanged(); + + KApplication::restoreOverrideCursor(); +} + +void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) +{ + QValueList< QPair<QString, QString> > albums; + bool setAlbumCovers = items.count() == 1; + + for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + QString artist = (*it)->file().tag()->artist(); + QString album = (*it)->file().tag()->album(); + + if(albums.find(qMakePair(artist, album)) == albums.end()) + albums.append(qMakePair(artist, album)); + + (*it)->file().coverInfo()->setCoverId(id); + if(setAlbumCovers) + (*it)->file().coverInfo()->applyCoverToWholeAlbum(true); + } + + for(QValueList< QPair<QString, QString> >::ConstIterator it = albums.begin(); + it != albums.end(); ++it) + { + refreshAlbum((*it).first, (*it).second); + } +} + +void Playlist::updatePlaying() const +{ + for(PlaylistItemList::ConstIterator it = PlaylistItem::playingItems().begin(); + it != PlaylistItem::playingItems().end(); ++it) + { + (*it)->listView()->triggerUpdate(); + } +} + +void Playlist::refreshAlbum(const QString &artist, const QString &album) +{ + ColumnList columns; + columns.append(PlaylistItem::ArtistColumn); + PlaylistSearch::Component artistComponent(artist, false, columns, + PlaylistSearch::Component::Exact); + + columns.clear(); + columns.append(PlaylistItem::AlbumColumn); + PlaylistSearch::Component albumComponent(album, false, columns, + PlaylistSearch::Component::Exact); + + PlaylistSearch::ComponentList components; + components.append(artist); + components.append(album); + + PlaylistList playlists; + playlists.append(CollectionList::instance()); + + PlaylistSearch search(playlists, components); + PlaylistItemList matches = search.matchedItems(); + + for(PlaylistItemList::Iterator it = matches.begin(); it != matches.end(); ++it) + (*it)->refresh(); +} + +void Playlist::hideColumn(int c, bool updateSearch) +{ + m_headerMenu->setItemChecked(c, false); + + if(!isColumnVisible(c)) + return; + + setColumnWidthMode(c, Manual); + setColumnWidth(c, 0); + + // Moving the column to the end seems to prevent it from randomly + // popping up. + + header()->moveSection(c, header()->count()); + header()->setResizeEnabled(false, c); + + if(c == m_leftColumn) { + updatePlaying(); + m_leftColumn = leftMostVisibleColumn(); + } + + if(!manualResize()) { + slotUpdateColumnWidths(); + triggerUpdate(); + } + + if(this != CollectionList::instance()) + CollectionList::instance()->hideColumn(c, false); + + if(updateSearch) + redisplaySearch(); +} + +void Playlist::showColumn(int c, bool updateSearch) +{ + m_headerMenu->setItemChecked(c, true); + + if(isColumnVisible(c)) + return; + + // Just set the width to one to mark the column as visible -- we'll update + // the real size in the next call. + + if(manualResize()) + setColumnWidth(c, 35); // Make column at least slightly visible. + else + setColumnWidth(c, 1); + + header()->setResizeEnabled(true, c); + header()->moveSection(c, c); // Approximate old position + + if(c == leftMostVisibleColumn()) { + updatePlaying(); + m_leftColumn = leftMostVisibleColumn(); + } + + if(!manualResize()) { + slotUpdateColumnWidths(); + triggerUpdate(); + } + + if(this != CollectionList::instance()) + CollectionList::instance()->showColumn(c, false); + + if(updateSearch) + redisplaySearch(); +} + +bool Playlist::isColumnVisible(int c) const +{ + return columnWidth(c) != 0; +} + +void Playlist::polish() +{ + KListView::polish(); + + if(m_polished) + return; + + m_polished = true; + + addColumn(i18n("Track Name")); + addColumn(i18n("Artist")); + addColumn(i18n("Album")); + addColumn(i18n("Cover")); + addColumn(i18n("Track")); + addColumn(i18n("Genre")); + addColumn(i18n("Year")); + addColumn(i18n("Length")); + addColumn(i18n("Bitrate")); + addColumn(i18n("Comment")); + addColumn(i18n("File Name")); + addColumn(i18n("File Name (full path)")); + + setRenameable(PlaylistItem::TrackColumn, true); + setRenameable(PlaylistItem::ArtistColumn, true); + setRenameable(PlaylistItem::AlbumColumn, true); + setRenameable(PlaylistItem::TrackNumberColumn, true); + setRenameable(PlaylistItem::GenreColumn, true); + setRenameable(PlaylistItem::YearColumn, true); + + setAllColumnsShowFocus(true); + setSelectionMode(QListView::Extended); + setShowSortIndicator(true); + setDropVisualizer(true); + + m_columnFixedWidths.resize(columns(), 0); + + ////////////////////////////////////////////////// + // setup header RMB menu + ////////////////////////////////////////////////// + + m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this, "showColumns"); + + m_headerMenu = m_columnVisibleAction->popupMenu(); + m_headerMenu->insertTitle(i18n("Show")); + m_headerMenu->setCheckable(true); + + for(int i = 0; i < header()->count(); ++i) { + if(i == PlaylistItem::FileNameColumn) + m_headerMenu->insertSeparator(); + m_headerMenu->insertItem(header()->label(i), i); + m_headerMenu->setItemChecked(i, true); + adjustColumn(i); + } + + connect(m_headerMenu, SIGNAL(activated(int)), this, SLOT(slotToggleColumnVisible(int))); + + connect(this, SIGNAL(contextMenuRequested(QListViewItem *, const QPoint &, int)), + this, SLOT(slotShowRMBMenu(QListViewItem *, const QPoint &, int))); + connect(this, SIGNAL(itemRenamed(QListViewItem *, const QString &, int)), + this, SLOT(slotInlineEditDone(QListViewItem *, const QString &, int))); + connect(this, SIGNAL(doubleClicked(QListViewItem *)), + this, SLOT(slotPlayCurrent())); + connect(this, SIGNAL(returnPressed(QListViewItem *)), + this, SLOT(slotPlayCurrent())); + + connect(header(), SIGNAL(sizeChange(int, int, int)), + this, SLOT(slotColumnSizeChanged(int, int, int))); + + connect(renameLineEdit(), SIGNAL(completionModeChanged(KGlobalSettings::Completion)), + this, SLOT(slotInlineCompletionModeChanged(KGlobalSettings::Completion))); + + connect(action("resizeColumnsManually"), SIGNAL(activated()), + this, SLOT(slotColumnResizeModeChanged())); + + if(action<KToggleAction>("resizeColumnsManually")->isChecked()) + setHScrollBarMode(Auto); + else + setHScrollBarMode(AlwaysOff); + + setAcceptDrops(true); + setDropVisualizer(true); + + m_disableColumnWidthUpdates = false; + + setShowToolTips(false); + m_toolTip = new PlaylistToolTip(viewport(), this); +} + +void Playlist::setupItem(PlaylistItem *item) +{ + if(!m_search.isEmpty()) + item->setVisible(m_search.checkItem(item)); + + if(childCount() <= 2 && !manualResize()) { + slotWeightDirty(); + slotUpdateColumnWidths(); + triggerUpdate(); + } +} + +void Playlist::setDynamicListsFrozen(bool frozen) +{ + m_collection->setDynamicListsFrozen(frozen); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected slots +//////////////////////////////////////////////////////////////////////////////// + +void Playlist::slotPopulateBackMenu() const +{ + if(!playingItem()) + return; + + KPopupMenu *menu = action<KToolBarPopupAction>("back")->popupMenu(); + menu->clear(); + m_backMenuItems.clear(); + + int count = 0; + PlaylistItemList::ConstIterator it = m_history.end(); + + while(it != m_history.begin() && count < 10) { + ++count; + --it; + int index = menu->insertItem((*it)->file().tag()->title()); + m_backMenuItems[index] = *it; + } +} + +void Playlist::slotPlayFromBackMenu(int number) const +{ + if(!m_backMenuItems.contains(number)) + return; + + TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]); + action("forward")->activate(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void Playlist::setup() +{ + setItemMargin(3); + + connect(header(), SIGNAL(indexChange(int, int, int)), this, SLOT(slotColumnOrderChanged(int, int, int))); + + connect(m_fetcher, SIGNAL(signalCoverChanged(int)), this, SLOT(slotCoverChanged(int))); + + // Prevent list of selected items from changing while internet search is in + // progress. + connect(this, SIGNAL(selectionChanged()), m_fetcher, SLOT(abortSearch())); + + setSorting(1); +} + +void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo) +{ + QFile file(fileName); + if(!file.open(IO_ReadOnly)) + return; + + QTextStream stream(&file); + + // Turn off non-explicit sorting. + + setSorting(PlaylistItem::lastColumn() + columnOffset() + 1); + + PlaylistItem *after = 0; + + m_disableColumnWidthUpdates = true; + + m_blockDataChanged = true; + + while(!stream.atEnd()) { + QString itemName = stream.readLine().stripWhiteSpace(); + + QFileInfo item(itemName); + + if(item.isRelative()) + item.setFile(QDir::cleanDirPath(fileInfo.dirPath(true) + "/" + itemName)); + + if(item.exists() && item.isFile() && item.isReadable() && + MediaFiles::isMediaFile(item.fileName())) + { + if(after) + after = createItem(FileHandle(item, item.absFilePath()), after, false); + else + after = createItem(FileHandle(item, item.absFilePath()), 0, false); + } + } + + m_blockDataChanged = false; + + file.close(); + + dataChanged(); + + m_disableColumnWidthUpdates = false; +} + +void Playlist::setPlaying(PlaylistItem *item, bool addToHistory) +{ + if(playingItem() == item) + return; + + if(playingItem()) { + if(addToHistory) { + if(playingItem()->playlist() == + playingItem()->playlist()->m_collection->upcomingPlaylist()) + m_history.append(playingItem()->collectionItem()); + else + m_history.append(playingItem()); + } + playingItem()->setPlaying(false); + } + + TrackSequenceManager::instance()->setCurrent(item); + QByteArray data; + kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); + + if(!item) + return; + + item->setPlaying(true); + + bool enableBack = !m_history.isEmpty(); + action<KToolBarPopupAction>("back")->popupMenu()->setEnabled(enableBack); +} + +bool Playlist::playing() const +{ + return playingItem() && this == playingItem()->playlist(); +} + +int Playlist::leftMostVisibleColumn() const +{ + int i = 0; + while(!isColumnVisible(header()->mapToSection(i)) && i < PlaylistItem::lastColumn()) + i++; + + return header()->mapToSection(i); +} + +PlaylistItemList Playlist::items(QListViewItemIterator::IteratorFlag flags) +{ + PlaylistItemList list; + + for(QListViewItemIterator it(this, flags); it.current(); ++it) + list.append(static_cast<PlaylistItem *>(it.current())); + + return list; +} + +void Playlist::calculateColumnWeights() +{ + if(m_disableColumnWidthUpdates) + return; + + PlaylistItemList l = items(); + QValueListConstIterator<int> columnIt; + + QValueVector<double> averageWidth(columns(), 0); + double itemCount = l.size(); + + QValueVector<int> cachedWidth; + + // Here we're not using a real average, but averaging the squares of the + // column widths and then using the square root of that value. This gives + // a nice weighting to the longer columns without doing something arbitrary + // like adding a fixed amount of padding. + + for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) { + cachedWidth = (*it)->cachedWidths(); + for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt) + averageWidth[*columnIt] += pow(double(cachedWidth[*columnIt]), 2.0) / itemCount; + } + + m_columnWeights.resize(columns(), -1); + + for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt) { + m_columnWeights[*columnIt] = int(sqrt(averageWidth[*columnIt]) + 0.5); + + // kdDebug(65432) << k_funcinfo << "m_columnWeights[" << *columnIt << "] == " + // << m_columnWeights[*columnIt] << endl; + } + + m_weightDirty.clear(); +} + +void Playlist::addFile(const QString &file, FileHandleList &files, bool importPlaylists, + PlaylistItem **after) +{ + if(hasItem(file) && !m_allowDuplicates) + return; + + processEvents(); + addFileHelper(files, after); + + // Our biggest thing that we're fighting during startup is too many stats + // of files. Make sure that we don't do one here if it's not needed. + + FileHandle cached = Cache::instance()->value(file); + + if(!cached.isNull()) { + cached.tag(); + files.append(cached); + return; + } + + + const QFileInfo fileInfo = QDir::cleanDirPath(file); + if(!fileInfo.exists()) + return; + + if(fileInfo.isFile() && fileInfo.isReadable()) { + if(MediaFiles::isMediaFile(file)) { + FileHandle f(fileInfo, fileInfo.absFilePath()); + f.tag(); + files.append(f); + } + } + + if(importPlaylists && MediaFiles::isPlaylistFile(file) && + !m_collection->containsPlaylistFile(fileInfo.absFilePath())) + { + new Playlist(m_collection, fileInfo); + return; + } + + if(fileInfo.isDir()) { + + // Resorting to the POSIX API because QDir::listEntries() stats every + // file and blocks while it's doing so. + + DIR *dir = ::opendir(QFile::encodeName(fileInfo.filePath())); + + if(dir) { + struct dirent *dirEntry; + + for(dirEntry = ::readdir(dir); dirEntry; dirEntry = ::readdir(dir)) { + if(strcmp(dirEntry->d_name, ".") != 0 && strcmp(dirEntry->d_name, "..") != 0) { + + // We set importPlaylists to the value from the add directories + // dialog as we want to load all of the ones that the user has + // explicitly asked for, but not those that we find in lower + // directories. + + addFile(fileInfo.filePath() + QDir::separator() + QFile::decodeName(dirEntry->d_name), + files, m_collection->importPlaylists(), after); + } + } + ::closedir(dir); + } + else { + kdWarning(65432) << "Unable to open directory " + << fileInfo.filePath() + << ", make sure it is readable.\n"; + } + } +} + +void Playlist::addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer) +{ + static QTime time = QTime::currentTime(); + + // Process new items every 10 seconds, when we've loaded 1000 items, or when + // it's been requested in the API. + + if(ignoreTimer || time.elapsed() > 10000 || + (files.count() >= 1000 && time.elapsed() > 1000)) + { + time.restart(); + + const bool focus = hasFocus(); + const bool visible = isVisible() && files.count() > 20; + + if(visible) + m_collection->raiseDistraction(); + const FileHandleList::ConstIterator filesEnd = files.end(); + for(FileHandleList::ConstIterator it = files.begin(); it != filesEnd; ++it) + *after = createItem(*it, *after, false); + files.clear(); + + if(visible) + m_collection->lowerDistraction(); + + if(focus) + setFocus(); + + processEvents(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void Playlist::slotUpdateColumnWidths() +{ + if(m_disableColumnWidthUpdates || manualResize()) + return; + + // Make sure that the column weights have been initialized before trying to + // update the columns. + + QValueList<int> visibleColumns; + for(int i = 0; i < columns(); i++) { + if(isColumnVisible(i)) + visibleColumns.append(i); + } + + QValueListConstIterator<int> it; + + if(count() == 0) { + for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) + setColumnWidth(*it, header()->fontMetrics().width(header()->label(*it)) + 10); + + return; + } + + if(m_columnWeights.isEmpty()) + return; + + // First build a list of minimum widths based on the strings in the listview + // header. We won't let the width of the column go below this width. + + QValueVector<int> minimumWidth(columns(), 0); + int minimumWidthTotal = 0; + + // Also build a list of either the minimum *or* the fixed width -- whichever is + // greater. + + QValueVector<int> minimumFixedWidth(columns(), 0); + int minimumFixedWidthTotal = 0; + + for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) { + int column = *it; + minimumWidth[column] = header()->fontMetrics().width(header()->label(column)) + 10; + minimumWidthTotal += minimumWidth[column]; + + minimumFixedWidth[column] = QMAX(minimumWidth[column], m_columnFixedWidths[column]); + minimumFixedWidthTotal += minimumFixedWidth[column]; + } + + // Make sure that the width won't get any smaller than this. We have to + // account for the scrollbar as well. Since this method is called from the + // resize event this will set a pretty hard lower bound on the size. + + setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); + + // If we've got enough room for the fixed widths (larger than the minimum + // widths) then instead use those for our "minimum widths". + + if(minimumFixedWidthTotal < visibleWidth()) { + minimumWidth = minimumFixedWidth; + minimumWidthTotal = minimumFixedWidthTotal; + } + + // We've got a list of columns "weights" based on some statistics gathered + // about the widths of the items in that column. We need to find the total + // useful weight to use as a divisor for each column's weight. + + double totalWeight = 0; + for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) + totalWeight += m_columnWeights[*it]; + + // Computed a "weighted width" for each visible column. This would be the + // width if we didn't have to handle the cases of minimum and maximum widths. + + QValueVector<int> weightedWidth(columns(), 0); + for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) + weightedWidth[*it] = int(double(m_columnWeights[*it]) / totalWeight * visibleWidth() + 0.5); + + // The "extra" width for each column. This is the weighted width less the + // minimum width or zero if the minimum width is greater than the weighted + // width. + + QValueVector<int> extraWidth(columns(), 0); + + // This is used as an indicator if we have any columns where the weighted + // width is less than the minimum width. If this is false then we can + // just use the weighted width with no problems, otherwise we have to + // "readjust" the widths. + + bool readjust = false; + + // If we have columns where the weighted width is less than the minimum width + // we need to steal that space from somewhere. The amount that we need to + // steal is the "neededWidth". + + int neededWidth = 0; + + // While we're on the topic of stealing -- we have to have somewhere to steal + // from. availableWidth is the sum of the amount of space beyond the minimum + // width that each column has been allocated -- the sum of the values of + // extraWidth[]. + + int availableWidth = 0; + + // Fill in the values discussed above. + + for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) { + if(weightedWidth[*it] < minimumWidth[*it]) { + readjust = true; + extraWidth[*it] = 0; + neededWidth += minimumWidth[*it] - weightedWidth[*it]; + } + else { + extraWidth[*it] = weightedWidth[*it] - minimumWidth[*it]; + availableWidth += extraWidth[*it]; + } + } + + // The adjustmentRatio is the amount of the "extraWidth[]" that columns will + // actually be given. + + double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); + + // This will be the sum of the total space that we actually use. Because of + // rounding error this won't be the exact available width. + + int usedWidth = 0; + + // Now set the actual column widths. If the weighted widths are all greater + // than the minimum widths, just use those, otherwise use the "reajusted + // weighted width". + + for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) { + int width; + if(readjust) { + int adjustedExtraWidth = int(double(extraWidth[*it]) * adjustmentRatio + 0.5); + width = minimumWidth[*it] + adjustedExtraWidth; + } + else + width = weightedWidth[*it]; + + setColumnWidth(*it, width); + usedWidth += width; + } + + // Fill the remaining gap for a clean fit into the available space. + + int remainingWidth = visibleWidth() - usedWidth; + setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); + + m_widthsDirty = false; +} + +void Playlist::slotAddToUpcoming() +{ + m_collection->setUpcomingPlaylistEnabled(true); + m_collection->upcomingPlaylist()->appendItems(selectedItems()); +} + +void Playlist::slotShowRMBMenu(QListViewItem *item, const QPoint &point, int column) +{ + if(!item) + return; + + // Create the RMB menu on demand. + + if(!m_rmbMenu) { + + // A bit of a hack to get a pointer to the action collection. + // Probably more of these actions should be ported over to using KActions. + + m_rmbMenu = new KPopupMenu(this); + + m_rmbUpcomingID = m_rmbMenu->insertItem(SmallIcon("today"), + i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming())); + m_rmbMenu->insertSeparator(); + + if(!readOnly()) { + action("edit_cut")->plug(m_rmbMenu); + action("edit_copy")->plug(m_rmbMenu); + action("edit_paste")->plug(m_rmbMenu); + m_rmbMenu->insertSeparator(); + action("removeFromPlaylist")->plug(m_rmbMenu); + } + else + action("edit_copy")->plug(m_rmbMenu); + + m_rmbEditID = m_rmbMenu->insertItem( + i18n("Edit"), this, SLOT(slotRenameTag())); + + action("refresh")->plug(m_rmbMenu); + action("removeItem")->plug(m_rmbMenu); + + m_rmbMenu->insertSeparator(); + + action("guessTag")->plug(m_rmbMenu); + action("renameFile")->plug(m_rmbMenu); + + action("coverManager")->plug(m_rmbMenu); + + m_rmbMenu->insertSeparator(); + + m_rmbMenu->insertItem( + SmallIcon("folder_new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup())); + + K3bExporter *exporter = new K3bExporter(this); + KAction *k3bAction = exporter->action(); + if(k3bAction) + k3bAction->plug(m_rmbMenu); + } + + // Ignore any columns added by subclasses. + + column -= columnOffset(); + + bool showEdit = + (column == PlaylistItem::TrackColumn) || + (column == PlaylistItem::ArtistColumn) || + (column == PlaylistItem::AlbumColumn) || + (column == PlaylistItem::TrackNumberColumn) || + (column == PlaylistItem::GenreColumn) || + (column == PlaylistItem::YearColumn); + + if(showEdit) + m_rmbMenu->changeItem(m_rmbEditID, + i18n("Edit '%1'").arg(columnText(column + columnOffset()))); + + m_rmbMenu->setItemVisible(m_rmbEditID, showEdit); + + // Disable edit menu if only one file is selected, and it's read-only + + FileHandle file = static_cast<PlaylistItem*>(item)->file(); + + m_rmbMenu->setItemEnabled(m_rmbEditID, file.fileInfo().isWritable() || + selectedItems().count() > 1); + + action("viewCover")->setEnabled(file.coverInfo()->hasCover()); + action("removeCover")->setEnabled(file.coverInfo()->hasCover()); + + m_rmbMenu->popup(point); + m_currentColumn = column + columnOffset(); +} + +void Playlist::slotRenameTag() +{ + // kdDebug(65432) << "Playlist::slotRenameTag()" << endl; + + // setup completions and validators + + CollectionList *list = CollectionList::instance(); + + KLineEdit *edit = renameLineEdit(); + + switch(m_currentColumn - columnOffset()) + { + case PlaylistItem::ArtistColumn: + edit->completionObject()->setItems(list->uniqueSet(CollectionList::Artists)); + break; + case PlaylistItem::AlbumColumn: + edit->completionObject()->setItems(list->uniqueSet(CollectionList::Albums)); + break; + case PlaylistItem::GenreColumn: + { + QStringList genreList; + TagLib::StringList genres = TagLib::ID3v1::genreList(); + for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) + genreList.append(TStringToQString((*it))); + edit->completionObject()->setItems(genreList); + break; + } + default: + edit->completionObject()->clear(); + break; + } + + m_editText = currentItem()->text(m_currentColumn); + + rename(currentItem(), m_currentColumn); +} + +bool Playlist::editTag(PlaylistItem *item, const QString &text, int column) +{ + Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag()); + + switch(column - columnOffset()) + { + case PlaylistItem::TrackColumn: + newTag->setTitle(text); + break; + case PlaylistItem::ArtistColumn: + newTag->setArtist(text); + break; + case PlaylistItem::AlbumColumn: + newTag->setAlbum(text); + break; + case PlaylistItem::TrackNumberColumn: + { + bool ok; + int value = text.toInt(&ok); + if(ok) + newTag->setTrack(value); + break; + } + case PlaylistItem::GenreColumn: + newTag->setGenre(text); + break; + case PlaylistItem::YearColumn: + { + bool ok; + int value = text.toInt(&ok); + if(ok) + newTag->setYear(value); + break; + } + } + + TagTransactionManager::instance()->changeTagOnItem(item, newTag); + return true; +} + +void Playlist::slotInlineEditDone(QListViewItem *, const QString &, int column) +{ + QString text = renameLineEdit()->text(); + bool changed = false; + + PlaylistItemList l = selectedItems(); + + // See if any of the files have a tag different from the input. + + for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end() && !changed; ++it) + if((*it)->text(column - columnOffset()) != text) + changed = true; + + if(!changed || + (l.count() > 1 && KMessageBox::warningContinueCancel( + 0, + i18n("This will edit multiple files. Are you sure?"), + QString::null, + i18n("Edit"), + "DontWarnMultipleTags") == KMessageBox::Cancel)) + { + return; + } + + for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) + editTag(*it, text, column); + + TagTransactionManager::instance()->commit(); + + CollectionList::instance()->dataChanged(); + dataChanged(); + update(); +} + +void Playlist::slotColumnOrderChanged(int, int from, int to) +{ + if(from == 0 || to == 0) { + updatePlaying(); + m_leftColumn = header()->mapToSection(0); + } + + SharedSettings::instance()->setColumnOrder(this); +} + +void Playlist::slotToggleColumnVisible(int column) +{ + if(!isColumnVisible(column)) { + int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); + int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); + + if(column == fileNameColumn && isColumnVisible(fullPathColumn)) { + hideColumn(fullPathColumn, false); + SharedSettings::instance()->toggleColumnVisible(fullPathColumn); + } + if(column == fullPathColumn && isColumnVisible(fileNameColumn)) { + hideColumn(fileNameColumn, false); + SharedSettings::instance()->toggleColumnVisible(fileNameColumn); + } + } + + if(isColumnVisible(column)) + hideColumn(column); + else + showColumn(column); + + SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); +} + +void Playlist::slotCreateGroup() +{ + QString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); + + if(!name.isEmpty()) + new Playlist(m_collection, selectedItems(), name); +} + +void Playlist::notifyUserColumnWidthModeChanged() +{ + KMessageBox::information(this, + i18n("Manual column widths have been enabled. You can " + "switch back to automatic column sizes in the view " + "menu."), + i18n("Manual Column Widths Enabled"), + "ShowManualColumnWidthInformation"); +} + +void Playlist::slotColumnSizeChanged(int column, int, int newSize) +{ + m_widthsDirty = true; + m_columnFixedWidths[column] = newSize; +} + +void Playlist::slotInlineCompletionModeChanged(KGlobalSettings::Completion mode) +{ + SharedSettings::instance()->setInlineCompletionMode(mode); +} + +void Playlist::slotPlayCurrent() +{ + QListViewItemIterator it(this, QListViewItemIterator::Selected); + PlaylistItem *next = static_cast<PlaylistItem *>(it.current()); + TrackSequenceManager::instance()->setNextItem(next); + action("forward")->activate(); +} + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const Playlist &p) +{ + s << p.name(); + s << p.fileName(); + s << p.files(); + + return s; +} + +QDataStream &operator>>(QDataStream &s, Playlist &p) +{ + p.read(s); + return s; +} + +bool processEvents() +{ + static QTime time = QTime::currentTime(); + + if(time.elapsed() > 100) { + time.restart(); + kapp->processEvents(); + return true; + } + return false; +} + +#include "playlist.moc" diff --git a/juk/playlist.h b/juk/playlist.h new file mode 100644 index 00000000..26c11ff9 --- /dev/null +++ b/juk/playlist.h @@ -0,0 +1,789 @@ +/*************************************************************************** + begin : Sat Feb 16 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLIST_H +#define PLAYLIST_H + +#include <klistview.h> +#include <kurldrag.h> +#include <kdebug.h> +#include <kglobalsettings.h> + +#include <qvaluevector.h> +#include <qfileinfo.h> + +#include "covermanager.h" +#include "stringhash.h" +#include "playlistsearch.h" +#include "tagguesser.h" +#include "playlistinterface.h" +#include "playlistitem.h" + +class KPopupMenu; +class KActionMenu; + +class QEvent; + +class PlaylistCollection; +class WebImageFetcher; +class PlaylistToolTip; +class UpcomingPlaylist; + +typedef QValueList<PlaylistItem *> PlaylistItemList; + +class Playlist : public KListView, public PlaylistInterface +{ + Q_OBJECT + +public: + + Playlist(PlaylistCollection *collection, const QString &name = QString::null, + const QString &iconName = "midi"); + Playlist(PlaylistCollection *collection, const PlaylistItemList &items, + const QString &name = QString::null, const QString &iconName = "midi"); + Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, + const QString &iconName = "midi"); + + /** + * This constructor should generally only be used either by the cache + * restoration methods or by subclasses that want to handle calls to + * PlaylistCollection::setupPlaylist() differently. + */ + Playlist(PlaylistCollection *collection, bool delaySetup); + + virtual ~Playlist(); + + + // The following group of functions implement the PlaylistInterface API. + + virtual QString name() const; + virtual FileHandle currentFile() const; + virtual int count() const { return childCount(); } + virtual int time() const; + virtual void playNext(); + virtual void playPrevious(); + virtual void stop(); + + /** + * Plays the top item of the playlist. + */ + void playFirst(); + + /** + * Plays the next album in the playlist. Only useful when in album random + * play mode. + */ + void playNextAlbum(); + + /** + * Saves the file to the currently set file name. If there is no filename + * currently set, the default behavior is to prompt the user for a file + * name. + */ + virtual void save(); + + /** + * Standard "save as". Prompts the user for a location where to save the + * playlist to. + */ + virtual void saveAs(); + + /** + * Removes \a item from the Playlist, but not from the disk. If + * \a emitChanged is true this will also notify relevant classes + * that the content of the list has changed. + * + * In some situations, for instance when removing items in a loop, it is + * preferable to delay this notification until after other operations have + * completed. In those cases set \a emitChanged to false and call the + * signal directly. + */ + virtual void clearItem(PlaylistItem *item, bool emitChanged = true); + + /** + * Remove \a items from the playlist and emit a signal indicating + * that the number of items in the list has changed. + */ + virtual void clearItems(const PlaylistItemList &items); + + /** + * Accessor function to return a pointer to the currently playing file. + * + * @return 0 if no file is playing, otherwise a pointer to the PlaylistItem + * of the track that is currently playing. + */ + static PlaylistItem *playingItem(); + + /** + * All of the (media) files in the list. + */ + QStringList files() const; + + /** + * Returns a list of all of the items in the playlist. + */ + virtual PlaylistItemList items(); + + /** + * Returns a list of all of the \e visible items in the playlist. + */ + PlaylistItemList visibleItems(); + + /** + * Returns a list of the currently selected items. + */ + PlaylistItemList selectedItems(); + + /** + * Returns properly casted first child item in list. + */ + PlaylistItem *firstChild() const; + + /** + * Allow duplicate files in the playlist. + */ + void setAllowDuplicates(bool allow) { m_allowDuplicates = allow; } + + /** + * This is being used as a mini-factory of sorts to make the construction + * of PlaylistItems virtual. In this case it allows for the creation of + * both PlaylistItems and CollectionListItems. + */ + virtual PlaylistItem *createItem(const FileHandle &file, + QListViewItem *after = 0, + bool emitChanged = true); + + /** + * This is implemented as a template method to allow subclasses to + * instantiate their PlaylistItem subclasses using the same method. Some + * of the types here are artificially templatized (i.e. CollectionListType and + * CollectionItemType) to avoid recursive includes, but in fact will always + * be the same. + */ + template <class ItemType, class CollectionItemType, class CollectionListType> + ItemType *createItem(const FileHandle &file, + QListViewItem *after = 0, + bool emitChanged = true); + + virtual void createItems(const PlaylistItemList &siblings, PlaylistItem *after = 0); + + /** + * This handles adding files of various types -- music, playlist or directory + * files. Music files that are found will be added to this playlist. New + * playlist files that are found will result in new playlists being created. + * + * Note that this should not be used in the case of adding *only* playlist + * items since it has the overhead of checking to see if the file is a playlist + * or directory first. + */ + virtual void addFiles(const QStringList &files, PlaylistItem *after = 0); + + /** + * Returns the file name associated with this playlist (an m3u file) or + * QString::null if no such file exists. + */ + QString fileName() const { return m_fileName; } + + /** + * Sets the file name to be associated with this playlist; this file should + * have the "m3u" extension. + */ + void setFileName(const QString &n) { m_fileName = n; } + + /** + * Hides column \a c. If \a updateSearch is true then a signal that the + * visible columns have changed will be emitted and things like the search + * will be udated. + */ + void hideColumn(int c, bool updateSearch = true); + + /** + * Shows column \a c. If \a updateSearch is true then a signal that the + * visible columns have changed will be emitted and things like the search + * will be udated. + */ + void showColumn(int c, bool updateSearch = true); + bool isColumnVisible(int c) const; + + /** + * This sets a name for the playlist that is \e different from the file name. + */ + void setName(const QString &n); + + /** + * Returns the KActionMenu that allows this to be embedded in menus outside + * of the playlist. + */ + KActionMenu *columnVisibleAction() const { return m_columnVisibleAction; } + + /** + * Set item to be the playing item. If \a item is null then this will clear + * the playing indicator. + */ + static void setPlaying(PlaylistItem *item, bool addToHistory = true); + + /** + * Returns true if this playlist is currently playing. + */ + bool playing() const; + + /** + * This forces an update of the left most visible column, but does not save + * the settings for this. + */ + void updateLeftColumn(); + + /** + * Returns the leftmost visible column of the listview. + */ + int leftColumn() const { return m_leftColumn; } + + /** + * Sets the items in the list to be either visible based on the value of + * visible. This is useful for search operations and such. + */ + static void setItemsVisible(const PlaylistItemList &items, bool visible = true); + + /** + * Returns the search associated with this list, or an empty search if one + * has not yet been set. + */ + PlaylistSearch search() const { return m_search; } + + /** + * Set the search associtated with this playlist. + */ + void setSearch(const PlaylistSearch &s); + + /** + * If the search is disabled then all items will be shown, not just those that + * match the current search. + */ + void setSearchEnabled(bool searchEnabled); + + /** + * Marks \a item as either selected or deselected based. + */ + void markItemSelected(PlaylistItem *item, bool selected); + + /** + * Subclasses of Playlist which add new columns will set this value to + * specify how many of those colums exist. This allows the Playlist + * class to do some internal calculations on the number and positions + * of columns. + */ + virtual int columnOffset() const { return 0; } + + /** + * Some subclasses of Playlist will be "read only" lists (i.e. the history + * playlist). This is a way for those subclasses to indicate that to the + * Playlist internals. + */ + virtual bool readOnly() const { return false; } + + /** + * Returns true if it's possible to reload this playlist. + */ + virtual bool canReload() const { return !m_fileName.isNull(); } + + /** + * Returns true if the playlist is a search playlist and the search should be + * editable. + */ + virtual bool searchIsEditable() const { return false; } + + /** + * Synchronizes the the playing item in this playlist with the playing item + * in \a sources. If \a setMaster is true, this list will become the source + * for determining the next item. + */ + void synchronizePlayingItems(const PlaylistList &sources, bool setMaster); + + /** + * Playlists have a common set of shared settings such as visible columns + * that should be applied just before the playlist is shown. Calling this + * method applies those. + */ + void applySharedSettings(); + + void read(QDataStream &s); + + static void setShuttingDown() { m_shuttingDown = true; } + +public slots: + /** + * Remove the currently selected items from the playlist and disk. + */ + void slotRemoveSelectedItems() { removeFromDisk(selectedItems()); }; + + /* + * The edit slots are required to use the canonical names so that they are + * detected by the application wide framework. + */ + virtual void cut() { copy(); clear(); } + + /** + * Puts a list of URLs pointing to the files in the current selection on the + * clipboard. + */ + virtual void copy(); + + /** + * Checks the clipboard for local URLs to be inserted into this playlist. + */ + virtual void paste(); + + /** + * Removes the selected items from the list, but not the disk. + * + * @see clearItem() + * @see clearItems() + */ + virtual void clear(); + virtual void selectAll() { KListView::selectAll(true); } + + /** + * Refreshes the tags of the selection from disk, or all of the files in the + * list if there is no selection. + */ + virtual void slotRefresh(); + + void slotGuessTagInfo(TagGuesser::Type type); + + /** + * Renames the selected items' files based on their tags contents. + * + * @see PlaylistItem::renameFile() + */ + void slotRenameFile(); + + /** + * Sets the cover of the selected items, pass in true if you want to load from the local system, + * false if you want to load from the internet. + */ + void slotAddCover(bool fromLocal); + + /** + * Shows a large image of the cover + */ + void slotViewCover(); + + /** + * Removes covers from the selected items + */ + void slotRemoveCover(); + + /** + * Shows the cover manager GUI dialog + */ + void slotShowCoverManager(); + + /** + * Reload the playlist contents from the m3u file. + */ + virtual void slotReload(); + + /** + * Tells the listview that the next time that it paints that the weighted + * column widths must be recalculated. If this is called without a column + * all visible columns are marked as dirty. + */ + void slotWeightDirty(int column = -1); + + void slotShowPlaying(); + + void slotColumnResizeModeChanged(); + + virtual void dataChanged(); + +protected: + /** + * Remove \a items from the playlist and disk. This will ignore items that + * are not actually in the list. + */ + void removeFromDisk(const PlaylistItemList &items); + + // the following are all reimplemented from base classes + + virtual bool eventFilter(QObject *watched, QEvent *e); + virtual void keyPressEvent(QKeyEvent *e); + virtual QDragObject *dragObject(QWidget *parent); + virtual QDragObject *dragObject() { return dragObject(this); } + virtual bool canDecode(QMimeSource *s); + virtual void decode(QMimeSource *s, PlaylistItem *item = 0); + virtual void contentsDropEvent(QDropEvent *e); + virtual void contentsMouseDoubleClickEvent(QMouseEvent *e); + virtual void contentsDragEnterEvent(QDragEnterEvent *e); + virtual void showEvent(QShowEvent *e); + virtual bool acceptDrag(QDropEvent *e) const; + virtual void viewportPaintEvent(QPaintEvent *pe); + virtual void viewportResizeEvent(QResizeEvent *re); + + virtual void insertItem(QListViewItem *item); + virtual void takeItem(QListViewItem *item); + + virtual bool hasItem(const QString &file) const { return m_members.contains(file); } + + void addColumn(const QString &label); + + /** + * Here I'm using delayed setup of some things that aren't quite intuitive. + * Creating columns and setting up connections are both time consuming if + * there are a lot of playlists to initialize. This moves that cost from the + * startup time to the time when the widget is "polished" -- i.e. just before + * it's painted the first time. + */ + virtual void polish(); + + /** + * Do some finial initialization of created items. Notably ensure that they + * are shown or hidden based on the contents of the current PlaylistSearch. + * + * This is called by the PlaylistItem constructor. + */ + void setupItem(PlaylistItem *item); + + /** + * Forwards the call to the parent to enable or disable automatic deletion + * of tree view playlists. Used by CollectionListItem. + */ + void setDynamicListsFrozen(bool frozen); + + template <class ItemType, class SiblingType> + ItemType *createItem(SiblingType *sibling, ItemType *after = 0); + + /** + * As a template this allows us to use the same code to initialize the items + * in subclasses. CollectionItemType should always be CollectionListItem and + * ItemType should be a PlaylistItem subclass. + */ + template <class CollectionItemType, class ItemType, class SiblingType> + void createItems(const QValueList<SiblingType *> &siblings, ItemType *after = 0); + +protected slots: + void slotPopulateBackMenu() const; + void slotPlayFromBackMenu(int number) const; + +signals: + + /** + * This is connected to the PlaylistBox::Item to let it know when the + * playlist's name has changed. + */ + void signalNameChanged(const QString &name); + + /** + * This signal is emitted just before a playlist item is removed from the + * list allowing for any cleanup that needs to happen. Typically this + * is used to remove the item from the history and safeguard against + * dangling pointers. + */ + void signalAboutToRemove(PlaylistItem *item); + + void signalEnableDirWatch(bool enable); + + void coverChanged(); + + void signalPlaylistItemsDropped(Playlist *p); + +private: + void setup(); + + /** + * This function is called to let the user know that JuK has automatically enabled + * manual column width adjust mode. + */ + void notifyUserColumnWidthModeChanged(); + + /** + * Load the playlist from a file. \a fileName should be the absolute path. + * \a fileInfo should point to the same file as \a fileName. This is a + * little awkward API-wise, but keeps us from throwing away useful + * information. + */ + void loadFile(const QString &fileName, const QFileInfo &fileInfo); + + /** + * Writes \a text to \a item in \a column. This is used by the inline tag + * editor. Returns false if the tag update failed. + */ + bool editTag(PlaylistItem *item, const QString &text, int column); + + /** + * Returns the index of the left most visible column in the playlist. + * + * \see isColumnVisible() + */ + int leftMostVisibleColumn() const; + + /** + * This method is used internally to provide the backend to the other item + * lists. + * + * \see items() + * \see visibleItems() + * \see selectedItems() + */ + PlaylistItemList items(QListViewItemIterator::IteratorFlag flags); + + /** + * Build the column "weights" for the weighted width mode. + */ + void calculateColumnWeights(); + + void addFile(const QString &file, FileHandleList &files, bool importPlaylists, + PlaylistItem **after); + void addFileHelper(FileHandleList &files, PlaylistItem **after, + bool ignoreTimer = false); + + void redisplaySearch() { setSearch(m_search); } + + /** + * Sets the cover for items to the cover identified by id. + */ + void refreshAlbums(const PlaylistItemList &items, coverKey id = CoverManager::NoMatch); + + void refreshAlbum(const QString &artist, const QString &album); + + /** + * Returns the number of PlaylistItems in @p items that can be assigned a + * cover. Used to avoid wasting the users' time setting the cover for 20 + * items when none are eligible. + */ + unsigned int eligibleCoverItems(const PlaylistItemList &items); + + void updatePlaying() const; + + /** + * This class is used internally to store settings that are shared by all + * of the playlists, such as column order. It is implemented as a singleton. + */ + class SharedSettings; + +private slots: + + void slotUpdateColumnWidths(); + + void slotAddToUpcoming(); + + /** + * Show the RMB menu. Matches the signature for the signal + * QListView::contextMenuRequested(). + */ + void slotShowRMBMenu(QListViewItem *item, const QPoint &point, int column); + + /** + * This slot is called when the inline tag editor has completed its editing + * and starts the process of renaming the values. + * + * \see editTag() + */ + void slotInlineEditDone(QListViewItem *, const QString &, int column); + + /** + * This starts the renaming process by displaying a line edit if the mouse is in + * an appropriate position. + */ + void slotRenameTag(); + + /** + * The image fetcher will update the cover asynchronously, this internal + * slot is called when it happens. + */ + void slotCoverChanged(int coverId); + + /** + * Moves the column \a from to the position \a to. This matches the signature + * for the signal QHeader::indexChange(). + */ + void slotColumnOrderChanged(int, int from, int to); + + /** + * Toggles a columns visible status. Useful for KActions. + * + * \see hideColumn() + * \see showColumn() + */ + void slotToggleColumnVisible(int column); + + /** + * Prompts the user to create a new playlist with from the selected items. + */ + void slotCreateGroup(); + + /** + * This slot is called when the user drags the slider in the listview header + * to manually set the size of the column. + */ + void slotColumnSizeChanged(int column, int oldSize, int newSize); + + /** + * The slot is called when the completion mode for the line edit in the + * inline tag editor is changed. It saves the settings and through the + * magic of the SharedSettings class will apply it to the other playlists as + * well. + */ + void slotInlineCompletionModeChanged(KGlobalSettings::Completion mode); + + void slotPlayCurrent(); + +private: + PlaylistCollection *m_collection; + + StringHash m_members; + + WebImageFetcher *m_fetcher; + + int m_currentColumn; + int m_processed; + int m_rmbEditID; + int m_rmbUpcomingID; + int m_selectedCount; + + bool m_allowDuplicates; + bool m_polished; + bool m_applySharedSettings; + bool m_columnWidthModeChanged; + + QValueList<int> m_weightDirty; + bool m_disableColumnWidthUpdates; + + mutable int m_time; + mutable PlaylistItemList m_addTime; + mutable PlaylistItemList m_subtractTime; + + /** + * The average minimum widths of columns to be used in balancing calculations. + */ + QValueVector<int> m_columnWeights; + QValueVector<int> m_columnFixedWidths; + bool m_widthsDirty; + + static PlaylistItemList m_history; + PlaylistSearch m_search; + + bool m_searchEnabled; + + PlaylistItem *m_lastSelected; + + /** + * Used to store the text for inline editing before it is changed so that + * we can know if something actually changed and as such if we need to save + * the tag. + */ + QString m_editText; + + /** + * This is only defined if the playlist name is something other than the + * file name. + */ + QString m_playlistName; + QString m_fileName; + + KPopupMenu *m_rmbMenu; + KPopupMenu *m_headerMenu; + KActionMenu *m_columnVisibleAction; + PlaylistToolTip *m_toolTip; + + /** + * This is used to indicate if the list of visible items has changed (via a + * call to setVisibleItems()) while random play is playing. + */ + static bool m_visibleChanged; + static bool m_shuttingDown; + static int m_leftColumn; + static QMap<int, PlaylistItem *> m_backMenuItems; + + bool m_blockDataChanged; +}; + +bool processEvents(); + +class FocusUpEvent : public QCustomEvent +{ +public: + FocusUpEvent() : QCustomEvent(id) {} + static const int id = 999; +}; + +QDataStream &operator<<(QDataStream &s, const Playlist &p); +QDataStream &operator>>(QDataStream &s, Playlist &p); + +// template method implementations + +template <class ItemType, class CollectionItemType, class CollectionListType> +ItemType *Playlist::createItem(const FileHandle &file, QListViewItem *after, + bool emitChanged) +{ + CollectionItemType *item = CollectionListType::instance()->lookup(file.absFilePath()); + + if(!item) { + item = new CollectionItemType(file); + setupItem(item); + + // If a valid tag was not created, destroy the CollectionListItem. + + if(!item->isValid()) { + kdError(65432) << "Playlist::createItem() -- A valid tag was not created for \"" + << file.absFilePath() << "\"" << endl; + delete item; + return 0; + } + } + + if(item && !m_members.insert(file.absFilePath()) || m_allowDuplicates) { + + ItemType *i = after ? new ItemType(item, this, after) : new ItemType(item, this); + setupItem(i); + + if(emitChanged) + dataChanged(); + + return i; + } + else + return 0; +} + +template <class ItemType, class SiblingType> +ItemType *Playlist::createItem(SiblingType *sibling, ItemType *after) +{ + m_disableColumnWidthUpdates = true; + + if(!m_members.insert(sibling->file().absFilePath()) || m_allowDuplicates) { + after = new ItemType(sibling->collectionItem(), this, after); + setupItem(after); + } + + m_disableColumnWidthUpdates = false; + + return after; +} + +template <class CollectionItemType, class ItemType, class SiblingType> +void Playlist::createItems(const QValueList<SiblingType *> &siblings, ItemType *after) +{ + if(siblings.isEmpty()) + return; + + QValueListConstIterator<SiblingType *> it = siblings.begin(); + for(; it != siblings.end(); ++it) + after = createItem(*it, after); + + dataChanged(); + slotWeightDirty(); +} + +#endif diff --git a/juk/playlistbox.cpp b/juk/playlistbox.cpp new file mode 100644 index 00000000..092342a3 --- /dev/null +++ b/juk/playlistbox.cpp @@ -0,0 +1,790 @@ +/*************************************************************************** + begin : Thu Sep 12 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler, + email : 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. * + * * + ***************************************************************************/ + +#include <kiconloader.h> +#include <kurldrag.h> +#include <kmessagebox.h> +#include <kpopupmenu.h> +#include <kaction.h> +#include <kdebug.h> + +#include <qheader.h> +#include <qpainter.h> +#include <qtimer.h> + +#include "playlistbox.h" +#include "playlist.h" +#include "collectionlist.h" +#include "covermanager.h" +#include "dynamicplaylist.h" +#include "historyplaylist.h" +#include "upcomingplaylist.h" +#include "viewmode.h" +#include "searchplaylist.h" +#include "treeviewitemplaylist.h" +#include "actioncollection.h" +#include "cache.h" +#include "k3bexporter.h" +#include "tracksequencemanager.h" +#include "tagtransactionmanager.h" + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistBox::PlaylistBox(QWidget *parent, QWidgetStack *playlistStack, + const char *name) : + KListView(parent, name), + PlaylistCollection(playlistStack), + m_viewModeIndex(0), + m_hasSelection(false), + m_doingMultiSelect(false), + m_dropItem(0), + m_showTimer(0) +{ + readConfig(); + addColumn("Playlists", width()); + + header()->blockSignals(true); + header()->hide(); + header()->blockSignals(false); + + setSorting(0); + setFullWidth(true); + setItemMargin(3); + + setAcceptDrops(true); + setSelectionModeExt(Extended); + + m_contextMenu = new KPopupMenu(this); + + K3bPlaylistExporter *exporter = new K3bPlaylistExporter(this); + m_k3bAction = exporter->action(); + + action("file_new")->plug(m_contextMenu); + action("renamePlaylist")->plug(m_contextMenu); + action("editSearch")->plug(m_contextMenu); + action("duplicatePlaylist")->plug(m_contextMenu); + action("reloadPlaylist")->plug(m_contextMenu); + action("deleteItemPlaylist")->plug(m_contextMenu); + action("file_save")->plug(m_contextMenu); + action("file_save_as")->plug(m_contextMenu); + if(m_k3bAction) + m_k3bAction->plug(m_contextMenu); + + m_contextMenu->insertSeparator(); + + // add the view modes stuff + + KSelectAction *viewModeAction = + new KSelectAction(i18n("View Modes"), "view_choose", KShortcut(), actions(), "viewModeMenu"); + + m_viewModes.append(new ViewMode(this)); + m_viewModes.append(new CompactViewMode(this)); + m_viewModes.append(new TreeViewMode(this)); + // m_viewModes.append(new CoverManagerMode(this)); + + QStringList modeNames; + + for(QValueListIterator<ViewMode *> it = m_viewModes.begin(); it != m_viewModes.end(); ++it) + modeNames.append((*it)->name()); + + viewModeAction->setItems(modeNames); + + QPopupMenu *p = viewModeAction->popupMenu(); + p->changeItem(0, SmallIconSet("view_detailed"), modeNames[0]); + p->changeItem(1, SmallIconSet("view_text"), modeNames[1]); + p->changeItem(2, SmallIconSet("view_tree"), modeNames[2]); + + CollectionList::initialize(this); + Cache::loadPlaylists(this); + + viewModeAction->setCurrentItem(m_viewModeIndex); + m_viewModes[m_viewModeIndex]->setShown(true); + + TrackSequenceManager::instance()->setCurrentPlaylist(CollectionList::instance()); + raise(CollectionList::instance()); + + viewModeAction->plug(m_contextMenu); + connect(viewModeAction, SIGNAL(activated(int)), this, SLOT(slotSetViewMode(int))); + + connect(this, SIGNAL(selectionChanged()), + this, SLOT(slotPlaylistChanged())); + + connect(this, SIGNAL(doubleClicked(QListViewItem *)), + this, SLOT(slotDoubleClicked())); + + connect(this, SIGNAL(contextMenuRequested(QListViewItem *, const QPoint &, int)), + this, SLOT(slotShowContextMenu(QListViewItem *, const QPoint &, int))); + + TagTransactionManager *tagManager = TagTransactionManager::instance(); + connect(tagManager, SIGNAL(signalAboutToModifyTags()), SLOT(slotFreezePlaylists())); + connect(tagManager, SIGNAL(signalDoneModifyingTags()), SLOT(slotUnfreezePlaylists())); + + setupUpcomingPlaylist(); + + connect(CollectionList::instance(), SIGNAL(signalNewTag(const QString &, unsigned)), + this, SLOT(slotAddItem(const QString &, unsigned))); + connect(CollectionList::instance(), SIGNAL(signalRemovedTag(const QString &, unsigned)), + this, SLOT(slotRemoveItem(const QString &, unsigned))); + + QTimer::singleShot(0, object(), SLOT(slotScanFolders())); + enableDirWatch(true); + + // Auto-save playlists after 10 minutes + QTimer::singleShot(600000, this, SLOT(slotSavePlaylists())); + + m_showTimer = new QTimer(this); + connect(m_showTimer, SIGNAL(timeout()), SLOT(slotShowDropTarget())); +} + +PlaylistBox::~PlaylistBox() +{ + PlaylistList l; + CollectionList *collection = CollectionList::instance(); + for(QListViewItem *i = firstChild(); i; i = i->nextSibling()) { + Item *item = static_cast<Item *>(i); + if(item->playlist() && item->playlist() != collection) + l.append(item->playlist()); + } + + Cache::savePlaylists(l); + saveConfig(); +} + +void PlaylistBox::raise(Playlist *playlist) +{ + if(!playlist) + return; + + Item *i = m_playlistDict.find(playlist); + + if(i) { + clearSelection(); + setSelected(i, true); + + setSingleItem(i); + ensureItemVisible(currentItem()); + } + else + PlaylistCollection::raise(playlist); + + slotPlaylistChanged(); +} + +void PlaylistBox::duplicate() +{ + Item *item = static_cast<Item *>(currentItem()); + if(!item || !item->playlist()) + return; + + QString name = playlistNameDialog(i18n("Duplicate"), item->text(0)); + + if(name.isNull()) + return; + + Playlist *p = new Playlist(this, name); + p->createItems(item->playlist()->items()); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox public slots +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistBox::paste() +{ + Item *i = static_cast<Item *>(currentItem()); + decode(kapp->clipboard()->data(), i); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox protected methods +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistBox::slotFreezePlaylists() +{ + setDynamicListsFrozen(true); +} + +void PlaylistBox::slotUnfreezePlaylists() +{ + setDynamicListsFrozen(false); +} + +void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName) +{ + setupPlaylist(playlist, iconName, 0); +} + +void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName, Item *parentItem) +{ + connect(playlist, SIGNAL(signalPlaylistItemsDropped(Playlist *)), + SLOT(slotPlaylistItemsDropped(Playlist *))); + + PlaylistCollection::setupPlaylist(playlist, iconName); + + if(parentItem) + new Item(parentItem, iconName, playlist->name(), playlist); + else + new Item(this, iconName, playlist->name(), playlist); +} + +void PlaylistBox::removePlaylist(Playlist *playlist) +{ + removeNameFromDict(m_playlistDict[playlist]->text(0)); + removeFileFromDict(playlist->fileName()); + m_playlistDict.remove(playlist); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox private methods +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistBox::readConfig() +{ + KConfigGroup config(KGlobal::config(), "PlaylistBox"); + m_viewModeIndex = config.readNumEntry("ViewMode", 0); +} + +void PlaylistBox::saveConfig() +{ + KConfigGroup config(KGlobal::config(), "PlaylistBox"); + config.writeEntry("ViewMode", action<KSelectAction>("viewModeMenu")->currentItem()); + KGlobal::config()->sync(); +} + +void PlaylistBox::remove() +{ + ItemList items = selectedItems(); + + if(items.isEmpty()) + return; + + QStringList files; + QStringList names; + + for(ItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + if(*it && (*it)->playlist() && + !(*it)->playlist()->fileName().isEmpty() && + QFileInfo((*it)->playlist()->fileName()).exists()) + { + files.append((*it)->playlist()->fileName()); + } + names.append((*it)->playlist()->name()); + } + + if(!files.isEmpty()) { + int remove = KMessageBox::warningYesNoCancelList( + this, i18n("Do you want to delete these files from the disk as well?"), files, QString::null, KStdGuiItem::del(), i18n("Keep")); + + if(remove == KMessageBox::Yes) { + QStringList couldNotDelete; + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + if(!QFile::remove(*it)) + couldNotDelete.append(*it); + } + + if(!couldNotDelete.isEmpty()) + KMessageBox::errorList(this, i18n("Could not delete these files."), couldNotDelete); + } + else if(remove == KMessageBox::Cancel) + return; + } + else if(items.count() > 1 || items.front()->playlist() != upcomingPlaylist()) { + if(KMessageBox::warningContinueCancelList(this, + i18n("Are you sure you want to remove these " + "playlists from your collection?"), + names, + i18n("Remove Items?"), + KGuiItem(i18n("&Remove"), "edittrash")) == KMessageBox::Cancel) + { + return; + } + } + + PlaylistList removeQueue; + + for(ItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + if(*it != Item::collectionItem() && + (*it)->playlist() && + (!(*it)->playlist()->readOnly())) + { + removeQueue.append((*it)->playlist()); + } + } + + if(items.back()->nextSibling() && static_cast<Item *>(items.back()->nextSibling())->playlist()) + setSingleItem(items.back()->nextSibling()); + else { + Item *i = static_cast<Item *>(items.front()->itemAbove()); + while(i && !i->playlist()) + i = static_cast<Item *>(i->itemAbove()); + + if(!i) + i = Item::collectionItem(); + + setSingleItem(i); + } + + for(PlaylistList::ConstIterator it = removeQueue.begin(); it != removeQueue.end(); ++it) { + if(*it != upcomingPlaylist()) + delete *it; + else { + action<KToggleAction>("showUpcoming")->setChecked(false); + setUpcomingPlaylistEnabled(false); + } + } +} + +void PlaylistBox::setDynamicListsFrozen(bool frozen) +{ + for(QValueList<ViewMode *>::Iterator it = m_viewModes.begin(); + it != m_viewModes.end(); + ++it) + { + (*it)->setDynamicListsFrozen(frozen); + } +} + +void PlaylistBox::slotSavePlaylists() +{ + kdDebug(65432) << "Auto-saving playlists and covers.\n"; + + PlaylistList l; + CollectionList *collection = CollectionList::instance(); + for(QListViewItem *i = firstChild(); i; i = i->nextSibling()) { + Item *item = static_cast<Item *>(i); + if(item->playlist() && item->playlist() != collection) + l.append(item->playlist()); + } + + Cache::savePlaylists(l); + CoverManager::saveCovers(); + + QTimer::singleShot(600000, this, SLOT(slotSavePlaylists())); +} + +void PlaylistBox::slotShowDropTarget() +{ + if(!m_dropItem) { + kdError(65432) << "Trying to show the playlist of a null item!\n"; + return; + } + + raise(m_dropItem->playlist()); +} + +void PlaylistBox::slotAddItem(const QString &tag, unsigned column) +{ + for(QValueListIterator<ViewMode *> it = m_viewModes.begin(); it != m_viewModes.end(); ++it) + (*it)->addItems(tag, column); +} + +void PlaylistBox::slotRemoveItem(const QString &tag, unsigned column) +{ + for(QValueListIterator<ViewMode *> it = m_viewModes.begin(); it != m_viewModes.end(); ++it) + (*it)->removeItem(tag, column); +} + +void PlaylistBox::decode(QMimeSource *s, Item *item) +{ + if(!s || (item && item->playlist() && item->playlist()->readOnly())) + return; + + KURL::List urls; + + if(KURLDrag::decode(s, urls) && !urls.isEmpty()) { + QStringList files; + for(KURL::List::Iterator it = urls.begin(); it != urls.end(); ++it) + files.append((*it).path()); + + if(item) { + TreeViewItemPlaylist *playlistItem; + playlistItem = dynamic_cast<TreeViewItemPlaylist *>(item->playlist()); + if(playlistItem) { + playlistItem->retag(files, currentPlaylist()); + TagTransactionManager::instance()->commit(); + currentPlaylist()->update(); + return; + } + } + + if(item && item->playlist()) + item->playlist()->addFiles(files); + else { + QString name = playlistNameDialog(); + if(!name.isNull()) { + Playlist *p = new Playlist(this, name); + p->addFiles(files); + } + } + } +} + +void PlaylistBox::contentsDropEvent(QDropEvent *e) +{ + m_showTimer->stop(); + + Item *i = static_cast<Item *>(itemAt(contentsToViewport(e->pos()))); + decode(e, i); + + if(m_dropItem) { + Item *old = m_dropItem; + m_dropItem = 0; + old->repaint(); + } +} + +void PlaylistBox::contentsDragMoveEvent(QDragMoveEvent *e) +{ + // If we can decode the input source, there is a non-null item at the "move" + // position, the playlist for that Item is non-null, is not the + // selected playlist and is not the CollectionList, then accept the event. + // + // Otherwise, do not accept the event. + + if(!KURLDrag::canDecode(e)) { + e->accept(false); + return; + } + + Item *target = static_cast<Item *>(itemAt(contentsToViewport(e->pos()))); + + if(target) { + + if(target->playlist() && target->playlist()->readOnly()) + return; + + // This is a semi-dirty hack to check if the items are coming from within + // JuK. If they are not coming from a Playlist (or subclass) then the + // dynamic_cast will fail and we can safely assume that the item is + // coming from outside of JuK. + + if(dynamic_cast<Playlist *>(e->source())) { + if(target->playlist() && + target->playlist() != CollectionList::instance() && + !target->isSelected()) + { + e->accept(true); + } + else + e->accept(false); + } + else // the dropped items are coming from outside of JuK + e->accept(true); + + if(m_dropItem != target) { + Item *old = m_dropItem; + m_showTimer->stop(); + + if(e->isAccepted()) { + m_dropItem = target; + target->repaint(); + m_showTimer->start(1500, true); + } + else + m_dropItem = 0; + + if(old) + old->repaint(); + } + } + else { + + // We're dragging over the whitespace. We'll use this case to make it + // possible to create new lists. + + e->accept(true); + } +} + +void PlaylistBox::contentsDragLeaveEvent(QDragLeaveEvent *e) +{ + if(m_dropItem) { + Item *old = m_dropItem; + m_dropItem = 0; + old->repaint(); + } + KListView::contentsDragLeaveEvent(e); +} + +void PlaylistBox::contentsMousePressEvent(QMouseEvent *e) +{ + if(e->button() == LeftButton) + m_doingMultiSelect = true; + KListView::contentsMousePressEvent(e); +} + +void PlaylistBox::contentsMouseReleaseEvent(QMouseEvent *e) +{ + if(e->button() == LeftButton) { + m_doingMultiSelect = false; + slotPlaylistChanged(); + } + KListView::contentsMouseReleaseEvent(e); +} + +void PlaylistBox::keyPressEvent(QKeyEvent *e) +{ + if((e->key() == Key_Up || e->key() == Key_Down) && e->state() == ShiftButton) + m_doingMultiSelect = true; + KListView::keyPressEvent(e); +} + +void PlaylistBox::keyReleaseEvent(QKeyEvent *e) +{ + if(m_doingMultiSelect && e->key() == Key_Shift) { + m_doingMultiSelect = false; + slotPlaylistChanged(); + } + KListView::keyReleaseEvent(e); +} + +PlaylistBox::ItemList PlaylistBox::selectedItems() const +{ + ItemList l; + + for(QListViewItemIterator it(const_cast<PlaylistBox *>(this), + QListViewItemIterator::Selected); it.current(); ++it) + l.append(static_cast<Item *>(*it)); + + return l; +} + +void PlaylistBox::setSingleItem(QListViewItem *item) +{ + setSelectionModeExt(Single); + KListView::setCurrentItem(item); + setSelectionModeExt(Extended); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox private slots +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistBox::slotPlaylistChanged() +{ + // Don't update while the mouse is pressed down. + + if(m_doingMultiSelect) + return; + + ItemList items = selectedItems(); + m_hasSelection = !items.isEmpty(); + + bool allowReload = false; + + PlaylistList playlists; + for(ItemList::ConstIterator it = items.begin(); it != items.end(); ++it) { + + Playlist *p = (*it)->playlist(); + if(p) { + if(p->canReload()) + allowReload = true; + playlists.append(p); + } + } + + bool singlePlaylist = playlists.count() == 1; + + if(playlists.isEmpty() || + (singlePlaylist && + (playlists.front() == CollectionList::instance() || + playlists.front()->readOnly()))) + { + action("file_save")->setEnabled(false); + action("file_save_as")->setEnabled(false); + action("renamePlaylist")->setEnabled(false); + action("deleteItemPlaylist")->setEnabled(false); + } + else { + action("file_save")->setEnabled(true); + action("file_save_as")->setEnabled(true); + action("renamePlaylist")->setEnabled(playlists.count() == 1); + action("deleteItemPlaylist")->setEnabled(true); + } + action("reloadPlaylist")->setEnabled(allowReload); + action("duplicatePlaylist")->setEnabled(!playlists.isEmpty()); + + if(m_k3bAction) + m_k3bAction->setEnabled(!playlists.isEmpty()); + + action("editSearch")->setEnabled(singlePlaylist && + playlists.front()->searchIsEditable()); + + if(singlePlaylist) { + PlaylistCollection::raise(playlists.front()); + + if(playlists.front() == upcomingPlaylist()) + action("deleteItemPlaylist")->setText(i18n("Hid&e")); + else + action("deleteItemPlaylist")->setText(i18n("R&emove")); + } + else if(!playlists.isEmpty()) + createDynamicPlaylist(playlists); +} + +void PlaylistBox::slotDoubleClicked() +{ + action("stop")->activate(); + action("play")->activate(); +} + +void PlaylistBox::slotShowContextMenu(QListViewItem *, const QPoint &point, int) +{ + m_contextMenu->popup(point); +} + +void PlaylistBox::slotPlaylistItemsDropped(Playlist *p) +{ + raise(p); +} + +void PlaylistBox::slotSetViewMode(int index) +{ + if(index == m_viewModeIndex) + return; + + viewMode()->setShown(false); + m_viewModeIndex = index; + viewMode()->setShown(true); +} + +void PlaylistBox::setupItem(Item *item) +{ + m_playlistDict.insert(item->playlist(), item); + viewMode()->queueRefresh(); +} + +void PlaylistBox::setupUpcomingPlaylist() +{ + KConfigGroup config(KGlobal::config(), "Playlists"); + bool enable = config.readBoolEntry("showUpcoming", false); + + setUpcomingPlaylistEnabled(enable); + action<KToggleAction>("showUpcoming")->setChecked(enable); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox::Item protected methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistBox::Item *PlaylistBox::Item::m_collectionItem = 0; + +PlaylistBox::Item::Item(PlaylistBox *listBox, const QString &icon, const QString &text, Playlist *l) + : QObject(listBox), KListViewItem(listBox, 0, text), + m_playlist(l), m_text(text), m_iconName(icon), m_sortedFirst(false) +{ + init(); +} + +PlaylistBox::Item::Item(Item *parent, const QString &icon, const QString &text, Playlist *l) + : QObject(parent->listView()), KListViewItem(parent, text), + m_playlist(l), m_text(text), m_iconName(icon), m_sortedFirst(false) +{ + init(); +} + +PlaylistBox::Item::~Item() +{ + +} + +int PlaylistBox::Item::compare(QListViewItem *i, int col, bool) const +{ + Item *otherItem = static_cast<Item *>(i); + PlaylistBox *playlistBox = static_cast<PlaylistBox *>(listView()); + + if(m_playlist == playlistBox->upcomingPlaylist() && otherItem->m_playlist != CollectionList::instance()) + return -1; + if(otherItem->m_playlist == playlistBox->upcomingPlaylist() && m_playlist != CollectionList::instance()) + return 1; + + if(m_sortedFirst && !otherItem->m_sortedFirst) + return -1; + else if(otherItem->m_sortedFirst && !m_sortedFirst) + return 1; + + return text(col).lower().localeAwareCompare(i->text(col).lower()); +} + +void PlaylistBox::Item::paintCell(QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align) +{ + PlaylistBox *playlistBox = static_cast<PlaylistBox *>(listView()); + playlistBox->viewMode()->paintCell(this, painter, colorGroup, column, width, align); +} + +void PlaylistBox::Item::setText(int column, const QString &text) +{ + m_text = text; + KListViewItem::setText(column, text); +} + +void PlaylistBox::Item::setup() +{ + listView()->viewMode()->setupItem(this); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox::Item protected slots +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistBox::Item::slotSetName(const QString &name) +{ + if(listView()) { + setText(0, name); + setSelected(true); + + listView()->sort(); + listView()->ensureItemVisible(listView()->currentItem()); + listView()->viewMode()->queueRefresh(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistBox::Item private methods +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistBox::Item::init() +{ + PlaylistBox *list = listView(); + + list->setupItem(this); + + int iconSize = list->viewModeIndex() == 0 ? 32 : 16; + setPixmap(0, SmallIcon(m_iconName, iconSize)); + list->addNameToDict(m_text); + + if(m_playlist) { + connect(m_playlist, SIGNAL(signalNameChanged(const QString &)), + this, SLOT(slotSetName(const QString &))); + connect(m_playlist, SIGNAL(destroyed()), this, SLOT(deleteLater())); + connect(m_playlist, SIGNAL(signalEnableDirWatch(bool)), + list->object(), SLOT(slotEnableDirWatch(bool))); + } + + if(m_playlist == CollectionList::instance()) { + m_sortedFirst = true; + m_collectionItem = this; + list->viewMode()->setupDynamicPlaylists(); + } + + if(m_playlist == list->historyPlaylist() || m_playlist == list->upcomingPlaylist()) + m_sortedFirst = true; +} + +#include "playlistbox.moc" diff --git a/juk/playlistbox.h b/juk/playlistbox.h new file mode 100644 index 00000000..412ebb86 --- /dev/null +++ b/juk/playlistbox.h @@ -0,0 +1,189 @@ +/*************************************************************************** + begin : Thu Sep 12 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLISTBOX_H +#define PLAYLISTBOX_H + +#include "playlistcollection.h" + +#include <klistview.h> + +#include <qptrdict.h> + +class Playlist; +class PlaylistItem; +class ViewMode; +class PlaylistSearch; +class SearchPlaylist; + +class KPopupMenu; +class KSelectAction; + +typedef QValueList<Playlist *> PlaylistList; + +/** + * This is the play list selection box that is by default on the right side of + * JuK's main widget (PlaylistSplitter). + */ + +class PlaylistBox : public KListView, public PlaylistCollection +{ + Q_OBJECT + +public: + class Item; + typedef QValueList<Item *> ItemList; + + friend class Item; + + PlaylistBox(QWidget *parent, QWidgetStack *playlistStack, + const char *name = 0); + + virtual ~PlaylistBox(); + + virtual void raise(Playlist *playlist); + virtual void duplicate(); + virtual void remove(); + + /** + * For view modes that have dynamic playlists, this freezes them from + * removing playlists. + */ + virtual void setDynamicListsFrozen(bool frozen); + + Item *dropItem() const { return m_dropItem; } + + void setupPlaylist(Playlist *playlist, const QString &iconName, Item *parentItem = 0); + +public slots: + void paste(); + void clear() {} + + void slotFreezePlaylists(); + void slotUnfreezePlaylists(); + +protected: + virtual void setupPlaylist(Playlist *playlist, const QString &iconName); + virtual void removePlaylist(Playlist *playlist); + +signals: + void signalPlaylistDestroyed(Playlist *); + +private: + void readConfig(); + void saveConfig(); + + virtual void decode(QMimeSource *s, Item *item); + virtual void contentsDropEvent(QDropEvent *e); + virtual void contentsDragMoveEvent(QDragMoveEvent *e); + virtual void contentsDragLeaveEvent(QDragLeaveEvent *e); + virtual void contentsMousePressEvent(QMouseEvent *e); + virtual void contentsMouseReleaseEvent(QMouseEvent *e); + virtual void keyPressEvent(QKeyEvent *e); + virtual void keyReleaseEvent(QKeyEvent *e); + + QValueList<Item *> selectedItems() const; + void setSingleItem(QListViewItem *item); + + void setupItem(Item *item); + void setupUpcomingPlaylist(); + int viewModeIndex() const { return m_viewModeIndex; } + ViewMode *viewMode() const { return m_viewModes[m_viewModeIndex]; } + +private slots: + /** + * Catches QListBox::currentChanged(QListBoxItem *), does a cast and then re-emits + * the signal as currentChanged(Item *). + */ + void slotPlaylistChanged(); + void slotDoubleClicked(); + void slotShowContextMenu(QListViewItem *, const QPoint &point, int); + void slotSetViewMode(int index); + void slotSavePlaylists(); + void slotShowDropTarget(); + + void slotPlaylistItemsDropped(Playlist *p); + + void slotAddItem(const QString &tag, unsigned column); + void slotRemoveItem(const QString &tag, unsigned column); + +private: + KPopupMenu *m_contextMenu; + QPtrDict<Item> m_playlistDict; + int m_viewModeIndex; + QValueList<ViewMode *> m_viewModes; + KAction *m_k3bAction; + bool m_hasSelection; + bool m_doingMultiSelect; + Item *m_dropItem; + QTimer *m_showTimer; +}; + + + +class PlaylistBox::Item : public QObject, public KListViewItem +{ + friend class PlaylistBox; + friend class ViewMode; + friend class CompactViewMode; + friend class TreeViewMode; + + Q_OBJECT + + // moc won't let me create private QObject subclasses and Qt won't let me + // make the destructor protected, so here's the closest hack that will + // compile. + +public: + virtual ~Item(); + +protected: + Item(PlaylistBox *listBox, const QString &icon, const QString &text, Playlist *l = 0); + Item(Item *parent, const QString &icon, const QString &text, Playlist *l = 0); + + Playlist *playlist() const { return m_playlist; } + PlaylistBox *listView() const { return static_cast<PlaylistBox *>(KListViewItem::listView()); } + QString iconName() const { return m_iconName; } + QString text() const { return m_text; } + void setSortedFirst(bool first = true) { m_sortedFirst = first; } + + virtual int compare(QListViewItem *i, int col, bool) const; + virtual void paintCell(QPainter *p, const QColorGroup &colorGroup, int column, int width, int align); + virtual void paintFocus(QPainter *, const QColorGroup &, const QRect &) {} + virtual void setText(int column, const QString &text); + + virtual QString text(int column) const { return KListViewItem::text(column); } + + virtual void setup(); + + static Item *collectionItem() { return m_collectionItem; } + static void setCollectionItem(Item *item) { m_collectionItem = item; } + + +protected slots: + void slotSetName(const QString &name); + +private: + // setup() was already taken. + void init(); + + Playlist *m_playlist; + QString m_text; + QString m_iconName; + bool m_sortedFirst; + static Item *m_collectionItem; +}; + +#endif diff --git a/juk/playlistcollection.cpp b/juk/playlistcollection.cpp new file mode 100644 index 00000000..548734c8 --- /dev/null +++ b/juk/playlistcollection.cpp @@ -0,0 +1,929 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kurl.h> + +#include <config.h> +#include <qobjectlist.h> + +#include <sys/types.h> +#include <dirent.h> + +#include "collectionlist.h" +#include "playlistcollection.h" +#include "actioncollection.h" +#include "advancedsearchdialog.h" +#include "coverinfo.h" +#include "searchplaylist.h" +#include "folderplaylist.h" +#include "historyplaylist.h" +#include "upcomingplaylist.h" +#include "directorylist.h" +#include "mediafiles.h" +#include "playermanager.h" +#include "tracksequencemanager.h" + +#include <kiconloader.h> +#include <kactionclasses.h> +#include <kapplication.h> +#include <kinputdialog.h> +#include <kmessagebox.h> +#include <kfiledialog.h> + +#include <qwidgetstack.h> +#include <qhbox.h> + +#define widget (kapp->mainWidget()) + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// static methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistCollection *PlaylistCollection::m_instance = 0; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistCollection::PlaylistCollection(QWidgetStack *playlistStack) : + m_playlistStack(playlistStack), + m_historyPlaylist(0), + m_upcomingPlaylist(0), + m_importPlaylists(true), + m_searchEnabled(true), + m_playing(false), + m_showMorePlaylist(0), + m_belowShowMorePlaylist(0), + m_dynamicPlaylist(0), + m_belowDistraction(0), + m_distraction(0) +{ + m_instance = this; + + m_actionHandler = new ActionHandler(this); + PlayerManager::instance()->setPlaylistInterface(this); + + // KDirLister's auto error handling seems to crash JuK during startup in + // readConfig(). + + m_dirLister.setAutoErrorHandlingEnabled(false, playlistStack); + readConfig(); +} + +PlaylistCollection::~PlaylistCollection() +{ + saveConfig(); + delete m_actionHandler; + PlayerManager::instance()->setPlaylistInterface(0); + Playlist::setShuttingDown(); +} + +QString PlaylistCollection::name() const +{ + return currentPlaylist()->name(); +} + +FileHandle PlaylistCollection::currentFile() const +{ + return currentPlaylist()->currentFile(); +} + +int PlaylistCollection::count() const +{ + return currentPlaylist()->count(); +} + +int PlaylistCollection::time() const +{ + return currentPlaylist()->time(); +} + +void PlaylistCollection::playFirst() +{ + m_playing = true; + currentPlaylist()->playFirst(); + currentChanged(); +} + +void PlaylistCollection::playNextAlbum() +{ + m_playing = true; + currentPlaylist()->playNextAlbum(); + currentChanged(); +} + +void PlaylistCollection::playPrevious() +{ + m_playing = true; + currentPlaylist()->playPrevious(); + currentChanged(); +} + +void PlaylistCollection::playNext() +{ + m_playing = true; + currentPlaylist()->playNext(); + currentChanged(); +} + +void PlaylistCollection::stop() +{ + m_playing = false; + currentPlaylist()->stop(); + dataChanged(); +} + +bool PlaylistCollection::playing() const +{ + return m_playing; +} + +QStringList PlaylistCollection::playlists() const +{ + QStringList l; + + QObjectList *childList = m_playlistStack->queryList("Playlist"); + QObject *obj; + for(obj = childList->first(); obj; obj = childList->next()) { + Playlist *p = static_cast<Playlist *>(obj); + l.append(p->name()); + } + + delete childList; + return l; +} + +void PlaylistCollection::createPlaylist(const QString &name) +{ + raise(new Playlist(this, name)); +} + +void PlaylistCollection::createDynamicPlaylist(const PlaylistList &playlists) +{ + if(m_dynamicPlaylist) + m_dynamicPlaylist->setPlaylists(playlists); + else + m_dynamicPlaylist = + new DynamicPlaylist(playlists, this, i18n("Dynamic List"), "midi", false, true); + + PlaylistCollection::raise(m_dynamicPlaylist); +} + +void PlaylistCollection::showMore(const QString &artist, const QString &album) +{ + + PlaylistList playlists; + PlaylistSearch::ComponentList components; + + if(currentPlaylist() != CollectionList::instance() && + currentPlaylist() != m_showMorePlaylist) + { + playlists.append(currentPlaylist()); + } + + playlists.append(CollectionList::instance()); + + { // Just setting off the artist stuff in its own block. + ColumnList columns; + columns.append(PlaylistItem::ArtistColumn); + PlaylistSearch::Component c(artist, false, columns, + PlaylistSearch::Component::Exact); + components.append(c); + } + + if(!album.isNull()) { + ColumnList columns; + columns.append(PlaylistItem::AlbumColumn); + PlaylistSearch::Component c(album, false, columns, + PlaylistSearch::Component::Exact); + components.append(c); + } + + PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll); + + if(m_showMorePlaylist) + m_showMorePlaylist->setPlaylistSearch(search); + else + m_showMorePlaylist = new SearchPlaylist(this, search, i18n("Now Playing"), false, true); + + // The call to raise() below will end up clearing m_belowShowMorePlaylist, + // so cache the value we want it to have now. + Playlist *belowShowMore = visiblePlaylist(); + + PlaylistCollection::setupPlaylist(m_showMorePlaylist, QString::null); + PlaylistCollection::raise(m_showMorePlaylist); + + m_belowShowMorePlaylist = belowShowMore; +} + +void PlaylistCollection::removeTrack(const QString &playlist, const QStringList &files) +{ + Playlist *p = playlistByName(playlist); + PlaylistItemList itemList; + if(!p) + return; + + QStringList::ConstIterator it; + for(it = files.begin(); it != files.end(); ++it) { + CollectionListItem *item = CollectionList::instance()->lookup(*it); + + if(item) { + PlaylistItem *playlistItem = item->itemForPlaylist(p); + if(playlistItem) + itemList.append(playlistItem); + } + } + + p->clearItems(itemList); +} + +QString PlaylistCollection::playlist() const +{ + return visiblePlaylist() ? visiblePlaylist()->name() : QString::null; +} + +QString PlaylistCollection::playingPlaylist() const +{ + return currentPlaylist() && m_playing ? currentPlaylist()->name() : QString::null; +} + +void PlaylistCollection::setPlaylist(const QString &playlist) +{ + Playlist *p = playlistByName(playlist); + if(p) + raise(p); +} + +QStringList PlaylistCollection::playlistTracks(const QString &playlist) const +{ + Playlist *p = playlistByName(playlist); + + if(p) + return p->files(); + return QStringList(); +} + +QString PlaylistCollection::trackProperty(const QString &file, const QString &property) const +{ + CollectionList *l = CollectionList::instance(); + CollectionListItem *item = l->lookup(file); + + return item ? item->file().property(property) : QString::null; +} + +QPixmap PlaylistCollection::trackCover(const QString &file, const QString &size) const +{ + if(size.lower() != "small" && size.lower() != "large") + return QPixmap(); + + CollectionList *l = CollectionList::instance(); + CollectionListItem *item = l->lookup(file); + + if(!item) + return QPixmap(); + + if(size.lower() == "small") + return item->file().coverInfo()->pixmap(CoverInfo::Thumbnail); + else + return item->file().coverInfo()->pixmap(CoverInfo::FullSize); +} + +void PlaylistCollection::open(const QStringList &l) +{ + QStringList files = l; + + if(files.isEmpty()) + files = MediaFiles::openDialog(widget); + + if(files.isEmpty()) + return; + + bool justPlaylists = true; + + for(QStringList::ConstIterator it = l.begin(); it != l.end() && justPlaylists; ++it) + justPlaylists = !MediaFiles::isPlaylistFile(*it); + + if(visiblePlaylist() == CollectionList::instance() || justPlaylists || + KMessageBox::questionYesNo( + widget, + i18n("Do you want to add these items to the current list or to the collection list?"), + QString::null, + KGuiItem(i18n("Current")), + KGuiItem(i18n("Collection"))) == KMessageBox::No) + { + CollectionList::instance()->addFiles(files); + } + else + visiblePlaylist()->addFiles(files); + + dataChanged(); +} + +void PlaylistCollection::open(const QString &playlist, const QStringList &files) +{ + Playlist *p = playlistByName(playlist); + + if(p) + p->addFiles(files); +} + +void PlaylistCollection::addFolder() +{ + kdDebug(65432) << k_funcinfo << endl; + DirectoryList l(m_folderList, m_importPlaylists, widget, "directoryList"); + DirectoryList::Result result = l.exec(); + + if(result.status == QDialog::Accepted) { + + m_dirLister.blockSignals(true); + + const bool reload = m_importPlaylists != result.addPlaylists; + m_importPlaylists = result.addPlaylists; + + for(QStringList::Iterator it = result.addedDirs.begin(); + it != result.addedDirs.end(); it++) + { + m_dirLister.openURL(*it, true); + m_folderList.append(*it); + } + + for(QStringList::Iterator it = result.removedDirs.begin(); + it != result.removedDirs.end(); it++) + { + m_dirLister.stop(*it); + m_folderList.remove(*it); + } + + if(reload) + open(m_folderList); + else if(!result.addedDirs.isEmpty()) + open(result.addedDirs); + + saveConfig(); + + m_dirLister.blockSignals(false); + } +} + +void PlaylistCollection::rename() +{ + QString old = visiblePlaylist()->name(); + QString name = playlistNameDialog(i18n("Rename"), old, false); + + m_playlistNames.remove(old); + + if(name.isNull()) + return; + + visiblePlaylist()->setName(name); +} + +void PlaylistCollection::duplicate() +{ + QString name = playlistNameDialog(i18n("Duplicate"), visiblePlaylist()->name()); + if(name.isNull()) + return; + raise(new Playlist(this, visiblePlaylist()->items(), name)); +} + +void PlaylistCollection::save() +{ + visiblePlaylist()->save(); +} + +void PlaylistCollection::saveAs() +{ + visiblePlaylist()->saveAs(); +} + +void PlaylistCollection::reload() +{ + if(visiblePlaylist() == CollectionList::instance()) + CollectionList::instance()->addFiles(m_folderList); + else + visiblePlaylist()->slotReload(); + +} + +void PlaylistCollection::editSearch() +{ + SearchPlaylist *p = dynamic_cast<SearchPlaylist *>(visiblePlaylist()); + + if(!p) + return; + + AdvancedSearchDialog::Result r = + AdvancedSearchDialog(p->name(), p->playlistSearch(), widget).exec(); + + if(r.result == AdvancedSearchDialog::Accepted) { + p->setPlaylistSearch(r.search); + p->setName(r.playlistName); + } +} + +void PlaylistCollection::removeItems() +{ + visiblePlaylist()->slotRemoveSelectedItems(); +} + +void PlaylistCollection::refreshItems() +{ + visiblePlaylist()->slotRefresh(); +} + +void PlaylistCollection::renameItems() +{ + visiblePlaylist()->slotRenameFile(); +} + +void PlaylistCollection::addCovers(bool fromFile) +{ + visiblePlaylist()->slotAddCover(fromFile); + dataChanged(); +} + +void PlaylistCollection::removeCovers() +{ + visiblePlaylist()->slotRemoveCover(); + dataChanged(); +} + +void PlaylistCollection::viewCovers() +{ + visiblePlaylist()->slotViewCover(); +} + +void PlaylistCollection::showCoverManager() +{ + visiblePlaylist()->slotShowCoverManager(); +} + +PlaylistItemList PlaylistCollection::selectedItems() +{ + return visiblePlaylist()->selectedItems(); +} + +void PlaylistCollection::scanFolders() +{ + CollectionList::instance()->addFiles(m_folderList); + + if(CollectionList::instance()->count() == 0) + addFolder(); +} + +void PlaylistCollection::createPlaylist() +{ + QString name = playlistNameDialog(); + if(!name.isNull()) + raise(new Playlist(this, name)); +} + +void PlaylistCollection::createSearchPlaylist() +{ + QString name = uniquePlaylistName(i18n("Search Playlist")); + + AdvancedSearchDialog::Result r = + AdvancedSearchDialog(name, PlaylistSearch(), widget).exec(); + + if(r.result == AdvancedSearchDialog::Accepted) + raise(new SearchPlaylist(this, r.search, r.playlistName)); +} + +void PlaylistCollection::createFolderPlaylist() +{ + QString folder = KFileDialog::getExistingDirectory(); + + if(folder.isEmpty()) + return; + + QString name = uniquePlaylistName(folder.mid(folder.findRev('/') + 1)); + name = playlistNameDialog(i18n("Create Folder Playlist"), name); + + if(!name.isNull()) + raise(new FolderPlaylist(this, folder, name)); +} + +void PlaylistCollection::guessTagFromFile() +{ + visiblePlaylist()->slotGuessTagInfo(TagGuesser::FileName); +} + +void PlaylistCollection::guessTagFromInternet() +{ + visiblePlaylist()->slotGuessTagInfo(TagGuesser::MusicBrainz); +} + +void PlaylistCollection::setSearchEnabled(bool enable) +{ + if(enable == m_searchEnabled) + return; + + m_searchEnabled = enable; + + visiblePlaylist()->setSearchEnabled(enable); +} + +HistoryPlaylist *PlaylistCollection::historyPlaylist() const +{ + return m_historyPlaylist; +} + +void PlaylistCollection::setHistoryPlaylistEnabled(bool enable) +{ + if((enable && m_historyPlaylist) || (!enable && !m_historyPlaylist)) + return; + + if(enable) { + action<KToggleAction>("showHistory")->setChecked(true); + m_historyPlaylist = new HistoryPlaylist(this); + m_historyPlaylist->setName(i18n("History")); + setupPlaylist(m_historyPlaylist, "history"); + } + else { + delete m_historyPlaylist; + m_historyPlaylist = 0; + } +} + +UpcomingPlaylist *PlaylistCollection::upcomingPlaylist() const +{ + return m_upcomingPlaylist; +} + +void PlaylistCollection::setUpcomingPlaylistEnabled(bool enable) +{ + if((enable && m_upcomingPlaylist) || (!enable && !m_upcomingPlaylist)) + return; + + if(enable) { + action<KToggleAction>("showUpcoming")->setChecked(true); + if(!m_upcomingPlaylist) + m_upcomingPlaylist = new UpcomingPlaylist(this); + + setupPlaylist(m_upcomingPlaylist, "today"); + } + else { + action<KToggleAction>("showUpcoming")->setChecked(false); + bool raiseCollection = m_playlistStack->visibleWidget() == m_upcomingPlaylist; + delete m_upcomingPlaylist; + m_upcomingPlaylist = 0; + + if(raiseCollection) { + kapp->processEvents(); // Seems to stop a crash, weird. + raise(CollectionList::instance()); + } + } +} + +QObject *PlaylistCollection::object() const +{ + return m_actionHandler; +} + +Playlist *PlaylistCollection::currentPlaylist() const +{ + if(m_belowDistraction) + return m_belowDistraction; + + if(m_upcomingPlaylist && m_upcomingPlaylist->active()) + return m_upcomingPlaylist; + + if(Playlist::playingItem()) + return Playlist::playingItem()->playlist(); + else + return visiblePlaylist(); +} + +Playlist *PlaylistCollection::visiblePlaylist() const +{ + return static_cast<Playlist *>(m_playlistStack->visibleWidget()); +} + +void PlaylistCollection::raise(Playlist *playlist) +{ + if(m_showMorePlaylist && currentPlaylist() == m_showMorePlaylist) + m_showMorePlaylist->lower(playlist); + if(m_dynamicPlaylist && currentPlaylist() == m_dynamicPlaylist) + m_dynamicPlaylist->lower(playlist); + + TrackSequenceManager::instance()->setCurrentPlaylist(playlist); + playlist->applySharedSettings(); + playlist->setSearchEnabled(m_searchEnabled); + m_playlistStack->raiseWidget(playlist); + clearShowMore(false); + dataChanged(); +} + +void PlaylistCollection::raiseDistraction() +{ + if(m_belowDistraction) + return; + + m_belowDistraction = currentPlaylist(); + + if(!m_distraction) { + m_distraction = new QHBox(m_playlistStack); + m_playlistStack->addWidget(m_distraction); + } + + m_playlistStack->raiseWidget(m_distraction); +} + +void PlaylistCollection::lowerDistraction() +{ + if(!m_distraction) + return; + + if(m_belowDistraction) + m_playlistStack->raiseWidget(m_belowDistraction); + + m_belowDistraction = 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +QWidgetStack *PlaylistCollection::playlistStack() const +{ + return m_playlistStack; +} + +void PlaylistCollection::setupPlaylist(Playlist *playlist, const QString &) +{ + if(!playlist->fileName().isNull()) + m_playlistFiles.insert(playlist->fileName()); + + if(!playlist->name().isNull()) + m_playlistNames.insert(playlist->name()); + + QObject::connect(playlist, SIGNAL(selectionChanged()), + object(), SIGNAL(signalSelectedItemsChanged())); +} + +bool PlaylistCollection::importPlaylists() const +{ + return m_importPlaylists; +} + +bool PlaylistCollection::containsPlaylistFile(const QString &file) const +{ + return m_playlistFiles.contains(file); +} + +bool PlaylistCollection::showMoreActive() const +{ + return visiblePlaylist() == m_showMorePlaylist; +} + +void PlaylistCollection::clearShowMore(bool raisePlaylist) +{ + if(!m_showMorePlaylist) + return; + + if(raisePlaylist) { + if(m_belowShowMorePlaylist) + raise(m_belowShowMorePlaylist); + else + raise(CollectionList::instance()); + } + + m_belowShowMorePlaylist = 0; +} + +void PlaylistCollection::enableDirWatch(bool enable) +{ + QObject *collection = CollectionList::instance(); + + m_dirLister.disconnect(object()); + if(enable) { + QObject::connect(&m_dirLister, SIGNAL(newItems(const KFileItemList &)), + object(), SLOT(slotNewItems(const KFileItemList &))); + QObject::connect(&m_dirLister, SIGNAL(refreshItems(const KFileItemList &)), + collection, SLOT(slotRefreshItems(const KFileItemList &))); + QObject::connect(&m_dirLister, SIGNAL(deleteItem(KFileItem *)), + collection, SLOT(slotDeleteItem(KFileItem *))); + } +} + +QString PlaylistCollection::playlistNameDialog(const QString &caption, + const QString &suggest, + bool forceUnique) const +{ + bool ok; + + QString name = KInputDialog::getText( + caption, + i18n("Please enter a name for this playlist:"), + forceUnique ? uniquePlaylistName(suggest) : suggest, + &ok); + + return ok ? uniquePlaylistName(name) : QString::null; +} + + +QString PlaylistCollection::uniquePlaylistName(const QString &suggest) const +{ + if(suggest.isEmpty()) + return uniquePlaylistName(); + + if(!m_playlistNames.contains(suggest)) + return suggest; + + QString base = suggest; + base.remove(QRegExp("\\s\\([0-9]+\\)$")); + + int count = 1; + QString s = QString("%1 (%2)").arg(base).arg(count); + + while(m_playlistNames.contains(s)) { + count++; + s = QString("%1 (%2)").arg(base).arg(count); + } + + return s; +} + +void PlaylistCollection::addNameToDict(const QString &name) +{ + m_playlistNames.insert(name); +} + +void PlaylistCollection::addFileToDict(const QString &file) +{ + m_playlistFiles.insert(file); +} + +void PlaylistCollection::removeNameFromDict(const QString &name) +{ + m_playlistNames.remove(name); +} + +void PlaylistCollection::removeFileFromDict(const QString &file) +{ + m_playlistFiles.remove(file); +} + +void PlaylistCollection::dirChanged(const QString &path) +{ + CollectionList::instance()->addFiles(path); +} + +Playlist *PlaylistCollection::playlistByName(const QString &name) const +{ + QObjectList *l = m_playlistStack->queryList("Playlist"); + Playlist *list = 0; + QObject *obj; + + for(obj = l->first(); obj; obj = l->next()) { + Playlist *p = static_cast<Playlist*>(obj); + if(p->name() == name) { + list = p; + break; + } + } + + delete l; + return list; +} + +void PlaylistCollection::newItems(const KFileItemList &list) const +{ + CollectionList::instance()->slotNewItems(list); +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistCollection::readConfig() +{ + KConfigGroup config(KGlobal::config(), "Playlists"); + + m_importPlaylists = config.readBoolEntry("ImportPlaylists", true); + m_folderList = config.readPathListEntry("DirectoryList"); + + for(QStringList::ConstIterator it = m_folderList.begin(); it != m_folderList.end(); ++it) + m_dirLister.openURL(*it, true); +} + +void PlaylistCollection::saveConfig() +{ + KConfigGroup config(KGlobal::config(), "Playlists"); + config.writeEntry("ImportPlaylists", m_importPlaylists); + config.writeEntry("showUpcoming", action<KToggleAction>("showUpcoming")->isChecked()); + config.writePathEntry("DirectoryList", m_folderList); +} + +//////////////////////////////////////////////////////////////////////////////// +// ActionHanlder implementation +//////////////////////////////////////////////////////////////////////////////// + +PlaylistCollection::ActionHandler::ActionHandler(PlaylistCollection *collection) : + QObject(0, "ActionHandler"), + m_collection(collection) +{ + KActionMenu *menu; + + // "New" menu + + menu = new KActionMenu(i18n("&New"), "filenew", actions(), "file_new"); + + menu->insert(createAction(i18n("&Empty Playlist..."), SLOT(slotCreatePlaylist()), + "newPlaylist", "window_new", "CTRL+n")); + menu->insert(createAction(i18n("&Search Playlist..."), SLOT(slotCreateSearchPlaylist()), + "newSearchPlaylist", "find", "CTRL+f")); + menu->insert(createAction(i18n("Playlist From &Folder..."), SLOT(slotCreateFolderPlaylist()), + "newDirectoryPlaylist", "fileopen", "CTRL+d")); + + // Guess tag info menu + +#if HAVE_MUSICBRAINZ + menu = new KActionMenu(i18n("&Guess Tag Information"), QString::null, actions(), "guessTag"); + menu->setIconSet(SmallIconSet("wizard")); + + menu->insert(createAction(i18n("From &File Name"), SLOT(slotGuessTagFromFile()), + "guessTagFile", "fileimport", "CTRL+g")); + menu->insert(createAction(i18n("From &Internet"), SLOT(slotGuessTagFromInternet()), + "guessTagInternet", "connect_established", "CTRL+i")); +#else + createAction(i18n("Guess Tag Information From &File Name"), SLOT(slotGuessTagFromFile()), + "guessTag", "fileimport", "CTRL+f"); +#endif + + + createAction(i18n("Play First Track"),SLOT(slotPlayFirst()), "playFirst"); + createAction(i18n("Play Next Album"), SLOT(slotPlayNextAlbum()), "forwardAlbum", "next"); + + createAction(i18n("Open..."), SLOT(slotOpen()), "file_open", "fileopen", "CTRL+o"); + createAction(i18n("Add &Folder..."), SLOT(slotAddFolder()), "openDirectory", "fileopen"); + createAction(i18n("&Rename..."), SLOT(slotRename()), "renamePlaylist", "lineedit"); + createAction(i18n("D&uplicate..."), SLOT(slotDuplicate()), "duplicatePlaylist", "editcopy"); + createAction(i18n("Save"), SLOT(slotSave()), "file_save", "filesave", "CTRL+s"); + createAction(i18n("Save As..."), SLOT(slotSaveAs()), "file_save_as", "filesaveas"); + createAction(i18n("R&emove"), SLOT(slotRemove()), "deleteItemPlaylist", "edittrash"); + createAction(i18n("Reload"), SLOT(slotReload()), "reloadPlaylist", "reload"); + createAction(i18n("Edit Search..."), SLOT(slotEditSearch()), "editSearch", "editclear"); + + createAction(i18n("&Delete"), SLOT(slotRemoveItems()), "removeItem", "editdelete"); + createAction(i18n("Refresh"), SLOT(slotRefreshItems()), "refresh", "reload"); + createAction(i18n("&Rename File"), SLOT(slotRenameItems()), "renameFile", "filesaveas", "CTRL+r"); + + menu = new KActionMenu(i18n("Cover Manager"), QString::null, actions(), "coverManager"); + menu->setIconSet(SmallIconSet("image")); + menu->insert(createAction(i18n("&View Cover"), + SLOT(slotViewCovers()), "viewCover", "viewmag")); + menu->insert(createAction(i18n("Get Cover From &File..."), + SLOT(slotAddLocalCover()), "addCover", "fileimport", "CTRL+SHIFT+f")); + + // Do not rename googleCover for backward compatibility + menu->insert(createAction(i18n("Get Cover From &Internet..."), + SLOT(slotAddInternetCover()), "googleCover", "connect_established", "CTRL+SHIFT+g")); + menu->insert(createAction(i18n("&Delete Cover"), + SLOT(slotRemoveCovers()), "removeCover", "editdelete")); + menu->insert(createAction(i18n("Show Cover &Manager"), + SLOT(slotShowCoverManager()), "showCoverManager")); + + KToggleAction *historyAction = + new KToggleAction(i18n("Show &History"), "history", 0, actions(), "showHistory"); + historyAction->setCheckedState(i18n("Hide &History")); + + KToggleAction *upcomingAction = + new KToggleAction(i18n("Show &Play Queue"), "today", 0, actions(), "showUpcoming"); + upcomingAction->setCheckedState(i18n("Hide &Play Queue")); + + connect(action<KToggleAction>("showHistory"), SIGNAL(toggled(bool)), + this, SLOT(slotSetHistoryPlaylistEnabled(bool))); + connect(action<KToggleAction>("showUpcoming"), SIGNAL(toggled(bool)), + this, SLOT(slotSetUpcomingPlaylistEnabled(bool))); +} + +KAction *PlaylistCollection::ActionHandler::createAction(const QString &text, + const char *slot, + const char *name, + const QString &icon, + const KShortcut &shortcut) +{ + if(icon.isNull()) + return new KAction(text, shortcut, this, slot, actions(), name); + else + return new KAction(text, icon, shortcut, this, slot, actions(), name); +} + +#undef widget +#include "playlistcollection.moc" + +// vim: set et sw=4: diff --git a/juk/playlistcollection.h b/juk/playlistcollection.h new file mode 100644 index 00000000..1ee4dea2 --- /dev/null +++ b/juk/playlistcollection.h @@ -0,0 +1,270 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLIST_COLLECTION_H +#define PLAYLIST_COLLECTION_H + +#include "playlistinterface.h" +#include "stringhash.h" +#include "jukIface.h" + +#include <kshortcut.h> +#include <klocale.h> +#include <kdirlister.h> + +#include <qguardedptr.h> + +class QWidgetStack; +class KAction; +class Playlist; +class PlaylistItem; +class HistoryPlaylist; +class UpcomingPlaylist; +class SearchPlaylist; +class DynamicPlaylist; + +typedef QValueList<Playlist *> PlaylistList; +typedef QValueList<PlaylistItem *> PlaylistItemList; + +class PlaylistCollection : public PlaylistInterface, CollectionIface +{ + friend class Playlist; + friend class CollectionList; + friend class DynamicPlaylist; + + static PlaylistCollection *m_instance; + +public: + PlaylistCollection(QWidgetStack *playlistStack); + virtual ~PlaylistCollection(); + + static PlaylistCollection *instance() { return m_instance; } + + virtual QString name() const; + virtual FileHandle currentFile() const; + virtual int count() const; + virtual int time() const; + virtual void playNext(); + virtual void playPrevious(); + virtual void stop(); + virtual bool playing() const; + + void playFirst(); + void playNextAlbum(); + + virtual QStringList playlists() const; + virtual void createPlaylist(const QString &name); + virtual void createDynamicPlaylist(const PlaylistList &playlists); + virtual void showMore(const QString &artist, const QString &album = QString::null); + virtual void removeTrack(const QString &playlist, const QStringList &files); + + virtual QString playlist() const; + virtual QString playingPlaylist() const; + virtual void setPlaylist(const QString &playlist); + + virtual QStringList playlistTracks(const QString &playlist) const; + virtual QString trackProperty(const QString &file, const QString &property) const; + virtual QPixmap trackCover(const QString &file, const QString &size = "Small") const; + + virtual void open(const QStringList &files = QStringList()); + virtual void open(const QString &playlist, const QStringList &files); + virtual void addFolder(); + virtual void rename(); + virtual void duplicate(); + virtual void save(); + virtual void saveAs(); + virtual void remove() = 0; + virtual void reload(); + virtual void editSearch(); + virtual void setDynamicListsFrozen(bool) = 0; + + bool showMoreActive() const; + void clearShowMore(bool raise = true); + void enableDirWatch(bool enable); + + void removeItems(); + void refreshItems(); + void renameItems(); + void addCovers(bool fromFile); + void removeCovers(); + void viewCovers(); + void showCoverManager(); + + virtual PlaylistItemList selectedItems(); + + void scanFolders(); + + void createPlaylist(); + void createSearchPlaylist(); + void createFolderPlaylist(); + + void guessTagFromFile(); + void guessTagFromInternet(); + + void setSearchEnabled(bool enable); + + HistoryPlaylist *historyPlaylist() const; + void setHistoryPlaylistEnabled(bool enable); + + UpcomingPlaylist *upcomingPlaylist() const; + void setUpcomingPlaylistEnabled(bool enable); + + void dirChanged(const QString &path); + + /** + * Returns a pointer to the action handler. + */ + QObject *object() const; + + void newItems(const KFileItemList &list) const; + + /** + * This is the current playlist in all things relating to the player. It + * represents the playlist that either should be played from or is currently + * playing. + */ + virtual Playlist *currentPlaylist() const; + + /** + * This is the currently visible playlist and should be used for all user + * interaction elements. + */ + virtual Playlist *visiblePlaylist() const; + + /** + * Makes \a playlist the currently visible playlist. + */ + virtual void raise(Playlist *playlist); + + /** + * This is used to put up a temporary widget over the top of the playlist + * stack. This is part of a trick to significantly speed up painting by + * hiding the playlist to which items are being added. + */ + void raiseDistraction(); + void lowerDistraction(); + + class ActionHandler; + +protected: + virtual QWidgetStack *playlistStack() const; + virtual void setupPlaylist(Playlist *playlist, const QString &iconName); + virtual void removePlaylist(Playlist *playlist) = 0; + + bool importPlaylists() const; + bool containsPlaylistFile(const QString &file) const; + + QString playlistNameDialog(const QString &caption = i18n("Create New Playlist"), + const QString &suggest = QString::null, + bool forceUnique = true) const; + QString uniquePlaylistName(const QString &suggest = i18n("Playlist")) const; + + void addNameToDict(const QString &name); + void addFileToDict(const QString &file); + void removeNameFromDict(const QString &name); + void removeFileFromDict(const QString &file); + + Playlist *playlistByName(const QString &name) const; + +private: + void readConfig(); + void saveConfig(); + + QWidgetStack *m_playlistStack; + HistoryPlaylist *m_historyPlaylist; + UpcomingPlaylist *m_upcomingPlaylist; + ActionHandler *m_actionHandler; + + KDirLister m_dirLister; + StringHash m_playlistNames; + StringHash m_playlistFiles; + QStringList m_folderList; + bool m_importPlaylists; + bool m_searchEnabled; + bool m_playing; + + QGuardedPtr<SearchPlaylist> m_showMorePlaylist; + QGuardedPtr<Playlist> m_belowShowMorePlaylist; + QGuardedPtr<DynamicPlaylist> m_dynamicPlaylist; + QGuardedPtr<Playlist> m_belowDistraction; + + QWidget *m_distraction; +}; + +/** + * This class is just used as a proxy to handle the signals coming from action + * activations without requiring PlaylistCollection to be a QObject. + */ + +class PlaylistCollection::ActionHandler : public QObject +{ + Q_OBJECT +public: + ActionHandler(PlaylistCollection *collection); + +private: + KAction *createAction(const QString &text, + const char *slot, + const char *name, + const QString &icon = QString::null, + const KShortcut &shortcut = KShortcut()); +private slots: + void slotPlayFirst() { m_collection->playFirst(); } + void slotPlayNextAlbum() { m_collection->playNextAlbum(); } + + void slotOpen() { m_collection->open(); } + void slotAddFolder() { m_collection->addFolder(); } + void slotRename() { m_collection->rename(); } + void slotDuplicate() { m_collection->duplicate(); } + void slotSave() { m_collection->save(); } + void slotSaveAs() { m_collection->saveAs(); } + void slotReload() { m_collection->reload(); } + void slotRemove() { m_collection->remove(); } + void slotEditSearch() { m_collection->editSearch(); } + + void slotRemoveItems() { m_collection->removeItems(); } + void slotRefreshItems() { m_collection->refreshItems(); } + void slotRenameItems() { m_collection->renameItems(); } + void slotScanFolders() { m_collection->scanFolders(); } + + void slotViewCovers() { m_collection->viewCovers(); } + void slotRemoveCovers() { m_collection->removeCovers(); } + void slotAddLocalCover() { m_collection->addCovers(true); } + void slotAddInternetCover() { m_collection->addCovers(false); } + + void slotCreatePlaylist() { m_collection->createPlaylist(); } + void slotCreateSearchPlaylist() { m_collection->createSearchPlaylist(); } + void slotCreateFolderPlaylist() { m_collection->createFolderPlaylist(); } + + void slotGuessTagFromFile() { m_collection->guessTagFromFile(); } + void slotGuessTagFromInternet() { m_collection->guessTagFromInternet(); } + + void slotSetSearchEnabled(bool enable) { m_collection->setSearchEnabled(enable); } + void slotSetHistoryPlaylistEnabled(bool enable) { m_collection->setHistoryPlaylistEnabled(enable); } + void slotSetUpcomingPlaylistEnabled(bool enable) { m_collection->setUpcomingPlaylistEnabled(enable); } + void slotShowCoverManager() { m_collection->showCoverManager(); } + void slotEnableDirWatch(bool enable) { m_collection->enableDirWatch(enable); } + void slotDirChanged(const QString &path) { m_collection->dirChanged(path); } + + void slotNewItems(const KFileItemList &list) { m_collection->newItems(list); } + +signals: + void signalSelectedItemsChanged(); + void signalCountChanged(); + +private: + PlaylistCollection *m_collection; +}; + +#endif diff --git a/juk/playlistexporter.h b/juk/playlistexporter.h new file mode 100644 index 00000000..291648f5 --- /dev/null +++ b/juk/playlistexporter.h @@ -0,0 +1,49 @@ +/*************************************************************************** + begin : Tue Jun 1 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLISTEXPORTER_H +#define PLAYLISTEXPORTER_H + + + +class KAction; +class KActionCollection; + +/** + * Abstract base class to define an interface for classes that export + * PlaylistItem data. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + * @see K3bExporter + */ +class PlaylistExporter : public QObject +{ +public: + PlaylistExporter(QWidget *parent = 0) : QObject(parent) { } + virtual ~PlaylistExporter() { } + + /** + * Returns a KAction that can be used to invoke the export. + * Returns 0 if it is not possible. + * + * @return pointer to a KAction that can invoke the export, or 0 on + * failure. + */ + virtual KAction *action() = 0; +}; + +#endif /* PLAYLISTEXPORTER_H */ + +// vim: set et ts=4 sw=4: diff --git a/juk/playlistinterface.cpp b/juk/playlistinterface.cpp new file mode 100644 index 00000000..9ec550a2 --- /dev/null +++ b/juk/playlistinterface.cpp @@ -0,0 +1,80 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include "playlistinterface.h" + +//////////////////////////////////////////////////////////////////////////////// +// Watched implementation +//////////////////////////////////////////////////////////////////////////////// + +void Watched::currentChanged() +{ + for(QValueList<PlaylistObserver *>::ConstIterator it = m_observers.begin(); + it != m_observers.end(); + ++it) + { + (*it)->updateCurrent(); + } +} + +void Watched::dataChanged() +{ + for(QValueList<PlaylistObserver *>::ConstIterator it = m_observers.begin(); + it != m_observers.end(); + ++it) + { + (*it)->updateData(); + } +} + +void Watched::addObserver(PlaylistObserver *observer) +{ + m_observers.append(observer); +} + +void Watched::removeObserver(PlaylistObserver *observer) +{ + m_observers.remove(observer); +} + +Watched::~Watched() +{ + for(QValueList<PlaylistObserver *>::Iterator it = m_observers.begin(); + it != m_observers.end(); + ++it) + { + (*it)->clearWatched(); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistObserver implementation +//////////////////////////////////////////////////////////////////////////////// + +PlaylistObserver::~PlaylistObserver() +{ + if(m_playlist) + m_playlist->removeObserver(this); +} + +PlaylistObserver::PlaylistObserver(PlaylistInterface *playlist) : + m_playlist(playlist) +{ + playlist->addObserver(this); +} + +const PlaylistInterface *PlaylistObserver::playlist() const +{ + return m_playlist; +} diff --git a/juk/playlistinterface.h b/juk/playlistinterface.h new file mode 100644 index 00000000..8251d4f2 --- /dev/null +++ b/juk/playlistinterface.h @@ -0,0 +1,102 @@ +/*************************************************************************** + copyright : (C) 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLISTINTERFACE_H +#define PLAYLISTINTERFACE_H + +#include "filehandle.h" + + +class PlaylistObserver; + +/** + * An interface implemented by PlaylistInterface to make it possible to watch + * for changes in the PlaylistInterface. This is a semi-standard observer + * pattern from i.e. Design Patterns. + */ + +class Watched +{ +public: + void addObserver(PlaylistObserver *observer); + void removeObserver(PlaylistObserver *observer); + + /** + * This is triggered when the currently playing item has been changed. + */ + virtual void currentChanged(); + + /** + * This is triggered when the data in the playlist -- i.e. the tag content + * changes. + */ + virtual void dataChanged(); + +protected: + virtual ~Watched(); + +private: + QValueList<PlaylistObserver *> m_observers; +}; + +/** + * This is a simple interface that should be used by things that implement a + * playlist-like API. + */ + +class PlaylistInterface : public Watched +{ +public: + virtual QString name() const = 0; + virtual FileHandle currentFile() const = 0; + virtual int time() const = 0; + virtual int count() const = 0; + + virtual void playNext() = 0; + virtual void playPrevious() = 0; + virtual void stop() = 0; + + virtual bool playing() const = 0; +}; + +class PlaylistObserver +{ +public: + virtual ~PlaylistObserver(); + + /** + * This method must be implemented in concrete implementations; it should + * define what action should be taken in the observer when the currently + * playing item changes. + */ + virtual void updateCurrent() = 0; + + /** + * This method must be implemented in concrete implementations; it should + * define what action should be taken when the data of the PlaylistItems in + * the playlist changes. + */ + virtual void updateData() = 0; + + void clearWatched() { m_playlist = 0; } + +protected: + PlaylistObserver(PlaylistInterface *playlist); + const PlaylistInterface *playlist() const; + +private: + PlaylistInterface *m_playlist; +}; + +#endif diff --git a/juk/playlistitem.cpp b/juk/playlistitem.cpp new file mode 100644 index 00000000..a86737c0 --- /dev/null +++ b/juk/playlistitem.cpp @@ -0,0 +1,464 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <config.h> + +#include <kdebug.h> +#include <kaction.h> +#include <kiconloader.h> + +#include "playlistitem.h" +#include "collectionlist.h" +#include "musicbrainzquery.h" +#include "tag.h" +#include "actioncollection.h" +#include "ktrm.h" +#include "coverinfo.h" +#include "tagtransactionmanager.h" + +PlaylistItemList PlaylistItem::m_playingItems; // static + +static void startMusicBrainzQuery(const FileHandle &file) +{ +#if HAVE_MUSICBRAINZ + // This deletes itself when finished. + new MusicBrainzLookup(file); +#else + Q_UNUSED(file) +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistItem public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistItem::~PlaylistItem() +{ + // Although this isn't the most efficient way to accomplish the task of + // stopping playback when deleting the item being played, it has the + // stark advantage of working reliably. I'll tell anyone who tries to + // optimize this, the timing issues can be *hard*. -- mpyne + + m_collectionItem->removeChildItem(this); + + if(m_playingItems.find(this) != m_playingItems.end()) { + m_playingItems.remove(this); + if(m_playingItems.isEmpty()) + playlist()->setPlaying(0); + } + + if(m_watched) + Pointer::clear(this); +} + +void PlaylistItem::setFile(const FileHandle &file) +{ + m_collectionItem->updateCollectionDict(d->fileHandle.absFilePath(), file.absFilePath()); + d->fileHandle = file; + refresh(); +} + +void PlaylistItem::setFile(const QString &file) +{ + QString oldPath = d->fileHandle.absFilePath(); + d->fileHandle.setFile(file); + m_collectionItem->updateCollectionDict(oldPath, d->fileHandle.absFilePath()); + refresh(); +} + +FileHandle PlaylistItem::file() const +{ + return d->fileHandle; +} + +const QPixmap *PlaylistItem::pixmap(int column) const +{ + static QPixmap image(SmallIcon("image")); + static QPixmap playing(UserIcon("playing")); + + int offset = playlist()->columnOffset(); + + if((column - offset) == CoverColumn && d->fileHandle.coverInfo()->hasCover()) + return ℑ + + if(column == playlist()->leftColumn() && + m_playingItems.contains(const_cast<PlaylistItem *>(this))) + return &playing; + + return KListViewItem::pixmap(column); +} + +QString PlaylistItem::text(int column) const +{ + if(!d->fileHandle.tag()) + return QString::null; + + int offset = playlist()->columnOffset(); + + switch(column - offset) { + case TrackColumn: + return d->fileHandle.tag()->title(); + case ArtistColumn: + return d->fileHandle.tag()->artist(); + case AlbumColumn: + return d->fileHandle.tag()->album(); + case CoverColumn: + return QString::null; + case TrackNumberColumn: + return d->fileHandle.tag()->track() > 0 + ? QString::number(d->fileHandle.tag()->track()) + : QString::null; + case GenreColumn: + return d->fileHandle.tag()->genre(); + case YearColumn: + return d->fileHandle.tag()->year() > 0 + ? QString::number(d->fileHandle.tag()->year()) + : QString::null; + case LengthColumn: + return d->fileHandle.tag()->lengthString(); + case BitrateColumn: + return QString::number(d->fileHandle.tag()->bitrate()); + case CommentColumn: + return d->fileHandle.tag()->comment(); + case FileNameColumn: + return d->fileHandle.fileInfo().fileName(); + case FullPathColumn: + return d->fileHandle.fileInfo().absFilePath(); + default: + return KListViewItem::text(column); + } +} + +void PlaylistItem::setText(int column, const QString &text) +{ + int offset = playlist()->columnOffset(); + if(column - offset >= 0 && column + offset <= lastColumn()) { + KListViewItem::setText(column, QString::null); + return; + } + + KListViewItem::setText(column, text); + playlist()->slotWeightDirty(column); +} + +void PlaylistItem::setPlaying(bool playing, bool master) +{ + m_playingItems.remove(this); + + if(playing) { + if(master) + m_playingItems.prepend(this); + else + m_playingItems.append(this); + } + else { + + // This is a tricky little recursion, but it + // in fact does clear the list. + + if(!m_playingItems.isEmpty()) + m_playingItems.front()->setPlaying(false); + } + + listView()->triggerUpdate(); +} + +void PlaylistItem::setSelected(bool selected) +{ + playlist()->markItemSelected(this, selected); + KListViewItem::setSelected(selected); +} + +void PlaylistItem::guessTagInfo(TagGuesser::Type type) +{ + switch(type) { + case TagGuesser::FileName: + { + TagGuesser guesser(d->fileHandle.absFilePath()); + Tag *tag = TagTransactionManager::duplicateTag(d->fileHandle.tag()); + + if(!guesser.title().isNull()) + tag->setTitle(guesser.title()); + if(!guesser.artist().isNull()) + tag->setArtist(guesser.artist()); + if(!guesser.album().isNull()) + tag->setAlbum(guesser.album()); + if(!guesser.track().isNull()) + tag->setTrack(guesser.track().toInt()); + if(!guesser.comment().isNull()) + tag->setComment(guesser.comment()); + + TagTransactionManager::instance()->changeTagOnItem(this, tag); + break; + } + case TagGuesser::MusicBrainz: + startMusicBrainzQuery(d->fileHandle); + break; + } +} + +Playlist *PlaylistItem::playlist() const +{ + return static_cast<Playlist *>(listView()); +} + +QValueVector<int> PlaylistItem::cachedWidths() const +{ + return d->cachedWidths; +} + +void PlaylistItem::refresh() +{ + m_collectionItem->refresh(); +} + +void PlaylistItem::refreshFromDisk() +{ + d->fileHandle.refresh(); + refresh(); +} + +void PlaylistItem::clear() +{ + playlist()->clearItem(this); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistItem protected methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent) : + KListViewItem(parent), + d(0), + m_watched(0) +{ + setup(item); +} + +PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent, QListViewItem *after) : + KListViewItem(parent, after), + d(0), + m_watched(0) +{ + setup(item); +} + + +// This constructor should only be used by the CollectionList subclass. + +PlaylistItem::PlaylistItem(CollectionList *parent) : + KListViewItem(parent), + m_watched(0) +{ + d = new Data; + m_collectionItem = static_cast<CollectionListItem *>(this); + setDragEnabled(true); +} + +void PlaylistItem::paintCell(QPainter *p, const QColorGroup &cg, int column, int width, int align) +{ + if(!m_playingItems.contains(this)) + return KListViewItem::paintCell(p, cg, column, width, align); + + QColorGroup colorGroup = cg; + + QColor base = colorGroup.base(); + QColor selection = colorGroup.highlight(); + + int r = (base.red() + selection.red()) / 2; + int b = (base.blue() + selection.blue()) / 2; + int g = (base.green() + selection.green()) / 2; + + QColor c(r, g, b); + + colorGroup.setColor(QColorGroup::Base, c); + QListViewItem::paintCell(p, colorGroup, column, width, align); +} + +int PlaylistItem::compare(QListViewItem *item, int column, bool ascending) const +{ + // reimplemented from QListViewItem + + int offset = playlist()->columnOffset(); + + if(!item) + return 0; + + PlaylistItem *playlistItem = static_cast<PlaylistItem *>(item); + + // The following statments first check to see if you can sort based on the + // specified column. If the values for the two PlaylistItems are the same + // in that column it then trys to sort based on columns 1, 2, 3 and 0, + // (artist, album, track number, track name) in that order. + + int c = compare(this, playlistItem, column, ascending); + + if(c != 0) + return c; + else { + // Loop through the columns doing comparisons until something is differnt. + // If all else is the same, compare the track name. + + int last = playlist()->isColumnVisible(AlbumColumn + offset) ? TrackNumberColumn : ArtistColumn; + + for(int i = ArtistColumn; i <= last; i++) { + if(playlist()->isColumnVisible(i + offset)) { + c = compare(this, playlistItem, i, ascending); + if(c != 0) + return c; + } + } + return compare(this, playlistItem, TrackColumn + offset, ascending); + } +} + +int PlaylistItem::compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool) const +{ + int offset = playlist()->columnOffset(); + + if(column < 0 || column > lastColumn() + offset) + return 0; + + if(column < offset) { + QString first = firstItem->text(column).lower(); + QString second = secondItem->text(column).lower(); + return first.localeAwareCompare(second); + } + + switch(column - offset) { + case TrackNumberColumn: + if(firstItem->d->fileHandle.tag()->track() > secondItem->d->fileHandle.tag()->track()) + return 1; + else if(firstItem->d->fileHandle.tag()->track() < secondItem->d->fileHandle.tag()->track()) + return -1; + else + return 0; + break; + case LengthColumn: + if(firstItem->d->fileHandle.tag()->seconds() > secondItem->d->fileHandle.tag()->seconds()) + return 1; + else if(firstItem->d->fileHandle.tag()->seconds() < secondItem->d->fileHandle.tag()->seconds()) + return -1; + else + return 0; + break; + case BitrateColumn: + if(firstItem->d->fileHandle.tag()->bitrate() > secondItem->d->fileHandle.tag()->bitrate()) + return 1; + else if(firstItem->d->fileHandle.tag()->bitrate() < secondItem->d->fileHandle.tag()->bitrate()) + return -1; + else + return 0; + break; + case CoverColumn: + if(firstItem->d->fileHandle.coverInfo()->hasCover() == secondItem->d->fileHandle.coverInfo()->hasCover()) + return 0; + else if (firstItem->d->fileHandle.coverInfo()->hasCover()) + return -1; + else + return 1; + break; + default: + return strcoll(firstItem->d->local8Bit[column - offset], + secondItem->d->local8Bit[column - offset]); + } +} + +bool PlaylistItem::isValid() const +{ + return bool(d->fileHandle.tag()); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistItem private methods +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistItem::setup(CollectionListItem *item) +{ + m_collectionItem = item; + + d = item->d; + item->addChildItem(this); + setDragEnabled(true); +} + +//////////////////////////////////////////////////////////////////////////////// +// PlaylistItem::Pointer implementation +//////////////////////////////////////////////////////////////////////////////// + +QMap<PlaylistItem *, QValueList<PlaylistItem::Pointer *> > PlaylistItem::Pointer::m_map; // static + +PlaylistItem::Pointer::Pointer(PlaylistItem *item) : + m_item(item) +{ + if(!m_item) + return; + + m_item->m_watched = true; + m_map[m_item].append(this); +} + +PlaylistItem::Pointer::Pointer(const Pointer &p) : + m_item(p.m_item) +{ + m_map[m_item].append(this); +} + +PlaylistItem::Pointer::~Pointer() +{ + if(!m_item) + return; + + m_map[m_item].remove(this); + if(m_map[m_item].isEmpty()) { + m_map.remove(m_item); + m_item->m_watched = false; + } +} + +PlaylistItem::Pointer &PlaylistItem::Pointer::operator=(PlaylistItem *item) +{ + if(item == m_item) + return *this; + + if(m_item) { + m_map[m_item].remove(this); + if(m_map[m_item].isEmpty()) { + m_map.remove(m_item); + m_item->m_watched = false; + } + } + + if(item) { + m_map[item].append(this); + item->m_watched = true; + } + + m_item = item; + + return *this; +} + +void PlaylistItem::Pointer::clear(PlaylistItem *item) // static +{ + if(!item) + return; + + QValueList<Pointer *> l = m_map[item]; + for(QValueList<Pointer *>::Iterator it = l.begin(); it != l.end(); ++it) + (*it)->m_item = 0; + m_map.remove(item); + item->m_watched = false; +} diff --git a/juk/playlistitem.h b/juk/playlistitem.h new file mode 100644 index 00000000..c969d736 --- /dev/null +++ b/juk/playlistitem.h @@ -0,0 +1,213 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLISTITEM_H +#define PLAYLISTITEM_H + +#include <klistview.h> +#include <ksharedptr.h> +#include <kdebug.h> + +#include <qvaluevector.h> +#include <qptrdict.h> + +#include "tagguesser.h" +#include "filehandle.h" + +class Playlist; +class PlaylistItem; +class CollectionListItem; +class CollectionList; + +typedef QValueList<PlaylistItem *> PlaylistItemList; + +/** + * Items for the Playlist and the baseclass for CollectionListItem. + * The constructors and destructor are protected and new items should be + * created via Playlist::createItem(). Items should be removed by + * Playlist::clear(), Playlist::deleteFromDisk(), Playlist::clearItem() or + * Playlist::clearItem(). + */ + +class PlaylistItem : public KListViewItem +{ + friend class Playlist; + friend class SearchPlaylist; + friend class UpcomingPlaylist; + friend class CollectionList; + friend class CollectionListItem; + friend class QPtrDict<PlaylistItem>; + friend class Pointer; + +public: + enum ColumnType { TrackColumn = 0, + ArtistColumn = 1, + AlbumColumn = 2, + CoverColumn = 3, + TrackNumberColumn = 4, + GenreColumn = 5, + YearColumn = 6, + LengthColumn = 7, + BitrateColumn = 8, + CommentColumn = 9, + FileNameColumn = 10, + FullPathColumn = 11 }; + + /** + * A helper class to implement guarded pointer semantics. + */ + + class Pointer + { + public: + Pointer() : m_item(0) {} + Pointer(PlaylistItem *item); + Pointer(const Pointer &p); + ~Pointer(); + Pointer &operator=(PlaylistItem *item); + bool operator==(const Pointer &p) const { return m_item == p.m_item; } + bool operator!=(const Pointer &p) const { return m_item != p.m_item; } + PlaylistItem *operator->() const { return m_item; } + PlaylistItem &operator*() const { return *m_item; } + operator PlaylistItem*() const { return m_item; } + static void clear(PlaylistItem *item); + + private: + PlaylistItem *m_item; + static QMap<PlaylistItem *, QValueList<Pointer *> > m_map; + }; + friend class Pointer; + + static int lastColumn() { return FullPathColumn; } + + void setFile(const FileHandle &file); + void setFile(const QString &file); + FileHandle file() const; + + virtual const QPixmap *pixmap(int column) const; + virtual QString text(int column) const; + virtual void setText(int column, const QString &text); + + void setPlaying(bool playing = true, bool master = true); + + virtual void setSelected(bool selected); + void guessTagInfo(TagGuesser::Type type); + + Playlist *playlist() const; + + virtual CollectionListItem *collectionItem() const { return m_collectionItem; } + + /** + * The widths of items are cached when they're updated for us in computations + * in the "weighted" listview column width mode. + */ + QValueVector<int> cachedWidths() const; + + /** + * This just refreshes from the in memory data. This may seem pointless at + * first, but this data is shared between all of the list view items that are + * based on the same file, so if another one of those items changes its data + * it is important to refresh the others. + */ + virtual void refresh(); + + /** + * This rereads the tag from disk. This affects all PlaylistItems based on + * the same file. + */ + virtual void refreshFromDisk(); + + /** + * Asks the item's playlist to remove the item (which uses deleteLater()). + */ + virtual void clear(); + + /** + * Returns properly casted item below this one. + */ + PlaylistItem *itemBelow() { return static_cast<PlaylistItem *>(KListViewItem::itemBelow()); } + + /** + * Returns properly casted item above this one. + */ + PlaylistItem *itemAbove() { return static_cast<PlaylistItem *>(KListViewItem::itemAbove()); } + + /** + * Returns a reference to the list of the currnetly playing items, with the + * first being the "master" item (i.e. the item from which the next track is + * chosen). + */ + static const PlaylistItemList &playingItems() { return m_playingItems; } + +protected: + /** + * Items should always be created using Playlist::createItem() or through a + * subclss or friend class. + */ + PlaylistItem(CollectionListItem *item, Playlist *parent); + PlaylistItem(CollectionListItem *item, Playlist *parent, QListViewItem *after); + + /** + * This is the constructor that shold be used by subclasses. + */ + PlaylistItem(CollectionList *parent); + + /** + * See the class documentation for an explanation of construction and deletion + * of PlaylistItems. + */ + virtual ~PlaylistItem(); + + virtual void paintCell(QPainter *p, const QColorGroup &cg, int column, int width, int align); + virtual void paintFocus(QPainter *, const QColorGroup &, const QRect &) {} + + virtual int compare(QListViewItem *item, int column, bool ascending) const; + int compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool ascending) const; + + bool isValid() const; + + struct Data : public KShared + { + Data() {} + Data(const QFileInfo &info, const QString &path) : fileHandle(info, path) {} + Data(const QString &path) : fileHandle(path) {} + + FileHandle fileHandle; + QValueVector<QCString> local8Bit; + QValueVector<int> cachedWidths; + }; + + KSharedPtr<Data> data() const { return d; } + +private: + KSharedPtr<Data> d; + + void setup(CollectionListItem *item); + CollectionListItem *m_collectionItem; + bool m_watched; + static PlaylistItemList m_playingItems; +}; + +inline kdbgstream &operator<<(kdbgstream &s, const PlaylistItem &item) +{ + if(&item == 0) + s << "(nil)"; + else + s << item.text(PlaylistItem::TrackColumn); + + return s; +} + +#endif diff --git a/juk/playlistsearch.cpp b/juk/playlistsearch.cpp new file mode 100644 index 00000000..a2109b26 --- /dev/null +++ b/juk/playlistsearch.cpp @@ -0,0 +1,328 @@ +/*************************************************************************** + begin : Sun Mar 6 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kdatastream.h> +#include <kdebug.h> + +#include "playlistsearch.h" +#include "playlist.h" +#include "playlistitem.h" +#include "collectionlist.h" + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistSearch::PlaylistSearch() : + m_mode(MatchAny) +{ + +} + +PlaylistSearch::PlaylistSearch(const PlaylistList &playlists, + const ComponentList &components, + SearchMode mode, + bool searchNow) : + m_playlists(playlists), + m_components(components), + m_mode(mode) +{ + if(searchNow) + search(); +} + +void PlaylistSearch::search() +{ + m_items.clear(); + m_matchedItems.clear(); + m_unmatchedItems.clear(); + + // This really isn't as bad as it looks. Despite the four nexted loops + // most of the time this will be searching one playlist for one search + // component -- possibly for one column. + + // Also there should be some caching of previous searches in here and + // allowance for appending and removing chars. If one is added it + // should only search the current list. If one is removed it should + // pop the previous search results off of a stack. + + PlaylistList::Iterator playlistIt = m_playlists.begin(); + for(; playlistIt != m_playlists.end(); ++playlistIt) { + if(!isEmpty()) { + for(QListViewItemIterator it(*playlistIt); it.current(); ++it) + checkItem(static_cast<PlaylistItem *>(*it)); + } + else { + m_items += (*playlistIt)->items(); + m_matchedItems += (*playlistIt)->items(); + } + } +} + +bool PlaylistSearch::checkItem(PlaylistItem *item) +{ + m_items.append(item); + + // set our default + bool match = bool(m_mode); + + ComponentList::Iterator componentIt = m_components.begin(); + for(; componentIt != m_components.end(); ++componentIt) { + + bool componentMatches = (*componentIt).matches(item); + + if(componentMatches && m_mode == MatchAny) { + match = true; + break; + } + + if(!componentMatches && m_mode == MatchAll) { + match = false; + break; + } + } + + if(match) + m_matchedItems.append(item); + else + m_unmatchedItems.append(item); + + return match; +} + +void PlaylistSearch::addComponent(const Component &c) +{ + m_components.append(c); +} + +void PlaylistSearch::clearComponents() +{ + m_components.clear(); +} + +PlaylistSearch::ComponentList PlaylistSearch::components() const +{ + return m_components; +} + +bool PlaylistSearch::isNull() const +{ + return m_components.isEmpty(); +} + +bool PlaylistSearch::isEmpty() const +{ + if(isNull()) + return true; + + ComponentList::ConstIterator it = m_components.begin(); + for(; it != m_components.end(); ++it) { + if(!(*it).query().isEmpty() || !(*it).pattern().isEmpty()) + return false; + } + + return true; +} + +void PlaylistSearch::clearItem(PlaylistItem *item) +{ + m_items.remove(item); + m_matchedItems.remove(item); + m_unmatchedItems.remove(item); +} + +//////////////////////////////////////////////////////////////////////////////// +// Component public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistSearch::Component::Component() : + m_mode(Contains), + m_searchAllVisible(true), + m_caseSensitive(false) +{ + +} + +PlaylistSearch::Component::Component(const QString &query, + bool caseSensitive, + const ColumnList &columns, + MatchMode mode) : + m_query(query), + m_columns(columns), + m_mode(mode), + m_searchAllVisible(columns.isEmpty()), + m_caseSensitive(caseSensitive), + m_re(false) +{ + +} + +PlaylistSearch::Component::Component(const QRegExp &query, const ColumnList& columns) : + m_queryRe(query), + m_columns(columns), + m_mode(Exact), + m_searchAllVisible(columns.isEmpty()), + m_caseSensitive(false), + m_re(true) +{ + +} + +bool PlaylistSearch::Component::matches(PlaylistItem *item) const +{ + if((m_re && m_queryRe.isEmpty()) || (!m_re && m_query.isEmpty())) + return false; + + if(m_columns.isEmpty()) { + Playlist *p = static_cast<Playlist *>(item->listView()); + for(int i = 0; i < p->columns(); i++) { + if(p->isColumnVisible(i)) + m_columns.append(i); + } + } + + + for(ColumnList::Iterator it = m_columns.begin(); it != m_columns.end(); ++it) { + + if(m_re) { + if(item->text(*it).find(m_queryRe) > -1) + return true; + else + break; + } + + switch(m_mode) { + case Contains: + if(item->text(*it).find(m_query, 0, m_caseSensitive) > -1) + return true; + break; + case Exact: + if(item->text(*it).length() == m_query.length()) { + if(m_caseSensitive) { + if(item->text(*it) == m_query) + return true; + } + else if(item->text(*it).lower() == m_query.lower()) + return true; + } + break; + case ContainsWord: + { + QString s = item->text(*it); + int i = s.find(m_query, 0, m_caseSensitive); + + if(i >= 0) { + + // If we found the pattern and the lengths are the same, then + // this is a match. + + if(s.length() == m_query.length()) + return true; + + // First: If the match starts at the beginning of the text or the + // character before the match is not a word character + + // AND + + // Second: Either the pattern was found at the end of the text, + // or the text following the match is a non-word character + + // ...then we have a match + + if((i == 0 || !s.at(i - 1).isLetterOrNumber()) && + (i + m_query.length() == s.length() || !s.at(i + m_query.length()).isLetterOrNumber())) + return true; + break; + } + } + } + } + return false; +} + +bool PlaylistSearch::Component::operator==(const Component &v) const +{ + return m_query == v.m_query && + m_queryRe == v.m_queryRe && + m_columns == v.m_columns && + m_mode == v.m_mode && + m_searchAllVisible == v.m_searchAllVisible && + m_caseSensitive == v.m_caseSensitive && + m_re == v.m_re; +} + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const PlaylistSearch &search) +{ + s << search.components() + << Q_INT32(search.searchMode()); + + return s; +} + +QDataStream &operator>>(QDataStream &s, PlaylistSearch &search) +{ + search.clearPlaylists(); + search.addPlaylist(CollectionList::instance()); + + search.clearComponents(); + PlaylistSearch::ComponentList components; + s >> components; + PlaylistSearch::ComponentList::ConstIterator it = components.begin(); + for(; it != components.end(); ++it) + search.addComponent(*it); + + Q_INT32 mode; + s >> mode; + search.setSearchMode(PlaylistSearch::SearchMode(mode)); + + return s; +} + +QDataStream &operator<<(QDataStream &s, const PlaylistSearch::Component &c) +{ + s << c.isPatternSearch() + << (c.isPatternSearch() ? c.pattern().pattern() : c.query()) + << c.isCaseSensitive() + << c.columns() + << Q_INT32(c.matchMode()); + + return s; +} + +QDataStream &operator>>(QDataStream &s, PlaylistSearch::Component &c) +{ + bool patternSearch; + QString pattern; + bool caseSensitive; + ColumnList columns; + Q_INT32 mode; + + s >> patternSearch + >> pattern + >> caseSensitive + >> columns + >> mode; + + if(patternSearch) + c = PlaylistSearch::Component(QRegExp(pattern), columns); + else + c = PlaylistSearch::Component(pattern, caseSensitive, columns, PlaylistSearch::Component::MatchMode(mode)); + + return s; +} diff --git a/juk/playlistsearch.h b/juk/playlistsearch.h new file mode 100644 index 00000000..9b46a92c --- /dev/null +++ b/juk/playlistsearch.h @@ -0,0 +1,150 @@ +/*************************************************************************** + begin : Sun Mar 6 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLISTSEARCH_H +#define PLAYLISTSEARCH_H + +#include <qregexp.h> + +class Playlist; +typedef QValueList<Playlist *> PlaylistList; + +class PlaylistItem; +typedef QValueList<PlaylistItem *> PlaylistItemList; + +typedef QValueList<int> ColumnList; + +class PlaylistSearch +{ +public: + class Component; + typedef QValueList<Component> ComponentList; + + enum SearchMode { MatchAny = 0, MatchAll = 1 }; + + PlaylistSearch(); + PlaylistSearch(const PlaylistList &playlists, + const ComponentList &components, + SearchMode mode = MatchAny, + bool searchNow = true); + + void search(); + bool checkItem(PlaylistItem *item); + + PlaylistItemList searchedItems() const { return m_items; } + PlaylistItemList matchedItems() const { return m_matchedItems; } + PlaylistItemList unmatchedItems() const { return m_unmatchedItems; } + + void addPlaylist(Playlist *p) { m_playlists.append(p); } + void clearPlaylists() { m_playlists.clear(); } + PlaylistList playlists() const { return m_playlists; } + + void addComponent(const Component &c); + void clearComponents(); + ComponentList components() const; + + void setSearchMode(SearchMode m) { m_mode = m; } + SearchMode searchMode() const { return m_mode; } + + bool isNull() const; + bool isEmpty() const; + + /** + * This is used to clear an item from the matched and unmatched lists. This + * is useful because it can prevent keeping a dangling pointer around without + * requiring invalidating the search. + */ + void clearItem(PlaylistItem *item); + +private: + PlaylistList m_playlists; + ComponentList m_components; + SearchMode m_mode; + + PlaylistItemList m_items; + PlaylistItemList m_matchedItems; + PlaylistItemList m_unmatchedItems; +}; + +/** + * A search is built from several search components. These corespond to to lines + * in the search bar. + */ + +class PlaylistSearch::Component +{ +public: + enum MatchMode { Contains = 0, Exact = 1, ContainsWord = 2 }; + + /** + * Create an empty search component. This is only provided for use by + * QValueList and should not be used in any other context. + */ + Component(); + + /** + * Create a query component. This defaults to searching all visible coulumns. + */ + Component(const QString &query, + bool caseSensitive = false, + const ColumnList &columns = ColumnList(), + MatchMode mode = Contains); + + /** + * Create a query component. This defaults to searching all visible coulumns. + */ + Component(const QRegExp &query, const ColumnList &columns = ColumnList()); + + QString query() const { return m_query; } + QRegExp pattern() const { return m_queryRe; } + ColumnList columns() const { return m_columns; } + + bool matches(PlaylistItem *item) const; + bool isPatternSearch() const { return m_re; } + bool isCaseSensitive() const { return m_caseSensitive; } + MatchMode matchMode() const { return m_mode; } + + bool operator==(const Component &v) const; + +private: + QString m_query; + QRegExp m_queryRe; + mutable ColumnList m_columns; + MatchMode m_mode; + bool m_searchAllVisible; + bool m_caseSensitive; + bool m_re; +}; + +/** + * Streams \a search to the stream \a s. + * \note This does not save the playlist list, but instead will assume that the + * search is just relevant to the collection list. This is all that is presently + * needed by JuK. + */ +QDataStream &operator<<(QDataStream &s, const PlaylistSearch &search); + +/** + * Streams \a search from the stream \a s. + * \note This does not save the playlist list, but instead will assume that the + * search is just relevant to the collection list. This is all that is presently + * needed by JuK. + */ +QDataStream &operator>>(QDataStream &s, PlaylistSearch &search); + +QDataStream &operator<<(QDataStream &s, const PlaylistSearch::Component &c); +QDataStream &operator>>(QDataStream &s, PlaylistSearch::Component &c); + +#endif diff --git a/juk/playlistsplitter.cpp b/juk/playlistsplitter.cpp new file mode 100644 index 00000000..5bc33a69 --- /dev/null +++ b/juk/playlistsplitter.cpp @@ -0,0 +1,237 @@ +/*************************************************************************** + begin : Fri Sep 13 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kaction.h> +#include <kdebug.h> + +#include <qlayout.h> +#include <qevent.h> + +#include "playlistsplitter.h" +#include "searchwidget.h" +#include "playlistsearch.h" +#include "actioncollection.h" +#include "tageditor.h" +#include "collectionlist.h" +#include "playermanager.h" +#include "nowplaying.h" + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +PlaylistSplitter::PlaylistSplitter(QWidget *parent, const char *name) : + QSplitter(Qt::Horizontal, parent, name), + m_newVisible(0), + m_playlistBox(0), + m_searchWidget(0), + m_playlistStack(0), + m_editor(0) +{ + setupActions(); + setupLayout(); + readConfig(); + + m_editor->slotUpdateCollection(); + m_editor->setupObservers(); +} + +PlaylistSplitter::~PlaylistSplitter() +{ + saveConfig(); + + // Since we want to ensure that the shutdown process for the PlaylistCollection + // (a base class for PlaylistBox) has a chance to write the playlists to disk + // before they are deleted we're explicitly deleting the PlaylistBox here. + + delete m_playlistBox; +} + +bool PlaylistSplitter::eventFilter(QObject *, QEvent *event) +{ + if(event->type() == FocusUpEvent::id) { + m_searchWidget->setFocus(); + return true; + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void PlaylistSplitter::setFocus() +{ + m_searchWidget->setFocus(); +} + +void PlaylistSplitter::slotFocusCurrentPlaylist() +{ + Playlist *playlist = m_playlistBox->visiblePlaylist(); + + if(playlist) { + playlist->setFocus(); + playlist->KListView::selectAll(false); + + // Select the top visible (and matching) item. + + PlaylistItem *item = static_cast<PlaylistItem *>(playlist->itemAt(QPoint(0, 0))); + + if(!item) + return; + + // A little bit of a hack to make QListView repaint things properly. Switch + // to single selection mode, set the selection and then switch back. + + playlist->setSelectionMode(QListView::Single); + + playlist->markItemSelected(item, true); + playlist->setCurrentItem(item); + + playlist->setSelectionMode(QListView::Extended); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +Playlist *PlaylistSplitter::visiblePlaylist() const +{ + return m_newVisible ? m_newVisible : m_playlistBox->visiblePlaylist(); +} + +void PlaylistSplitter::setupActions() +{ + KToggleAction *showSearch = + new KToggleAction(i18n("Show &Search Bar"), "filefind", 0, actions(), "showSearch"); + showSearch->setCheckedState(i18n("Hide &Search Bar")); + + new KAction(i18n("Edit Track Search"), "edit_clear", "F6", this, SLOT(setFocus()), actions(), "editTrackSearch"); +} + +void PlaylistSplitter::setupLayout() +{ + setOpaqueResize(false); + + // Create a splitter to go between the playlists and the editor. + + QSplitter *editorSplitter = new QSplitter(Qt::Vertical, this, "editorSplitter"); + + // Create the playlist and the editor. + + QWidget *top = new QWidget(editorSplitter); + QVBoxLayout *topLayout = new QVBoxLayout(top); + + m_playlistStack = new QWidgetStack(top, "playlistStack"); + m_playlistStack->installEventFilter(this); + + connect(m_playlistStack, SIGNAL(aboutToShow(QWidget *)), this, SLOT(slotPlaylistChanged(QWidget *))); + + m_editor = new TagEditor(editorSplitter, "tagEditor"); + + // Make the editor as small as possible (or at least as small as recommended) + + editorSplitter->setResizeMode(m_editor, QSplitter::FollowSizeHint); + + // Create the PlaylistBox + + m_playlistBox = new PlaylistBox(this, m_playlistStack, "playlistBox"); + + connect(m_playlistBox->object(), SIGNAL(signalSelectedItemsChanged()), + this, SLOT(slotPlaylistSelectionChanged())); + connect(m_playlistBox, SIGNAL(signalPlaylistDestroyed(Playlist *)), + m_editor, SLOT(slotPlaylistDestroyed(Playlist *))); + + moveToFirst(m_playlistBox); + + connect(CollectionList::instance(), SIGNAL(signalCollectionChanged()), + m_editor, SLOT(slotUpdateCollection())); + + NowPlaying *nowPlaying = new NowPlaying(top, m_playlistBox); + + // Create the search widget -- this must be done after the CollectionList is created. + + m_searchWidget = new SearchWidget(top, "searchWidget"); + connect(m_searchWidget, SIGNAL(signalQueryChanged()), + this, SLOT(slotShowSearchResults())); + connect(m_searchWidget, SIGNAL(signalDownPressed()), + this, SLOT(slotFocusCurrentPlaylist())); + connect(m_searchWidget, SIGNAL(signalAdvancedSearchClicked()), + m_playlistBox->object(), SLOT(slotCreateSearchPlaylist())); + connect(m_searchWidget, SIGNAL(signalShown(bool)), + m_playlistBox->object(), SLOT(slotSetSearchEnabled(bool))); + connect(action<KToggleAction>("showSearch"), SIGNAL(toggled(bool)), + m_searchWidget, SLOT(setEnabled(bool))); + + topLayout->addWidget(nowPlaying); + topLayout->addWidget(m_searchWidget); + topLayout->addWidget(m_playlistStack); + + // Show the collection on startup. + m_playlistBox->setSelected(0, true); +} + +void PlaylistSplitter::readConfig() +{ + KConfigGroup config(KGlobal::config(), "Splitter"); + + QValueList<int> splitterSizes = config.readIntListEntry("PlaylistSplitterSizes"); + if(splitterSizes.isEmpty()) { + splitterSizes.append(100); + splitterSizes.append(640); + } + setSizes(splitterSizes); + + bool showSearch = config.readBoolEntry("ShowSearch", true); + action<KToggleAction>("showSearch")->setChecked(showSearch); + m_searchWidget->setShown(showSearch); +} + +void PlaylistSplitter::saveConfig() +{ + KConfigGroup config(KGlobal::config(), "Splitter"); + config.writeEntry("PlaylistSplitterSizes", sizes()); + config.writeEntry("ShowSearch", action<KToggleAction>("showSearch")->isChecked()); +} + +void PlaylistSplitter::slotShowSearchResults() +{ + PlaylistList playlists; + playlists.append(visiblePlaylist()); + PlaylistSearch search = m_searchWidget->search(playlists); + visiblePlaylist()->setSearch(search); +} + +void PlaylistSplitter::slotPlaylistSelectionChanged() +{ + m_editor->slotSetItems(visiblePlaylist()->selectedItems()); +} + +void PlaylistSplitter::slotPlaylistChanged(QWidget *w) +{ + Playlist *p = dynamic_cast<Playlist *>(w); + + if(!p) + return; + + m_newVisible = p; + m_searchWidget->setSearch(p->search()); + m_newVisible = 0; +} + +#include "playlistsplitter.moc" diff --git a/juk/playlistsplitter.h b/juk/playlistsplitter.h new file mode 100644 index 00000000..0734ee05 --- /dev/null +++ b/juk/playlistsplitter.h @@ -0,0 +1,88 @@ +/*************************************************************************** + begin : Fri Sep 13 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef PLAYLISTSPLITTER_H +#define PLAYLISTSPLITTER_H + +#include <kfiledialog.h> + +#include <qwidgetstack.h> + +#include "playlistbox.h" + +class KActionMenu; +class PlaylistItem; +class SearchWidget; +class HistoryPlaylist; +class PlaylistInterface; +class TagEditor; + +/** + * This is the main layout class of JuK. It should contain a PlaylistBox and + * a QWidgetStack of the Playlists. + * + * This class serves as a "mediator" (see "Design Patterns") between the JuK + * class and the playlist classes. Thus all access to the playlist classes from + * non-Playlist related classes should be through the public API of this class. + */ + +class PlaylistSplitter : public QSplitter +{ + Q_OBJECT + +public: + PlaylistSplitter(QWidget *parent, const char *name = 0); + virtual ~PlaylistSplitter(); + + PlaylistInterface *playlist() const { return m_playlistBox; } + + virtual bool eventFilter(QObject *watched, QEvent *event); + +public slots: + virtual void setFocus(); + virtual void slotFocusCurrentPlaylist(); + +private: + + /** + * This returns a pointer to the first item in the playlist on the top + * of the QWidgetStack of playlists. + */ + Playlist *visiblePlaylist() const; + + void setupActions(); + void setupLayout(); + void readConfig(); + void saveConfig(); + +private slots: + + /** + * Updates the visible search results based on the result of the search + * associated with the currently visible playlist. + */ + void slotShowSearchResults(); + void slotPlaylistSelectionChanged(); + void slotPlaylistChanged(QWidget *w); + +private: + Playlist *m_newVisible; + PlaylistBox *m_playlistBox; + SearchWidget *m_searchWidget; + QWidgetStack *m_playlistStack; + TagEditor *m_editor; +}; + +#endif diff --git a/juk/searchplaylist.cpp b/juk/searchplaylist.cpp new file mode 100644 index 00000000..2afd8549 --- /dev/null +++ b/juk/searchplaylist.cpp @@ -0,0 +1,115 @@ +/*************************************************************************** + begin : Mon May 5 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kdebug.h> + +#include <qptrdict.h> + +#include "searchplaylist.h" +#include "playlistitem.h" +#include "collectionlist.h" + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +SearchPlaylist::SearchPlaylist(PlaylistCollection *collection, + const PlaylistSearch &search, + const QString &name, + bool setupPlaylist, + bool synchronizePlaying) : + DynamicPlaylist(search.playlists(), collection, name, "find", + setupPlaylist, synchronizePlaying), + m_search(search) +{ + +} + +void SearchPlaylist::setPlaylistSearch(const PlaylistSearch &s, bool update) +{ + m_search = s; + if(update) + setPlaylists(s.playlists()); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected methods +//////////////////////////////////////////////////////////////////////////////// + +void SearchPlaylist::updateItems() +{ + // Here we don't simply use "clear" since that would involve a call to + // items() which would in turn call this method... + + PlaylistItemList l = Playlist::items(); + + QPtrDict<PlaylistItem> oldItems(503); + + for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) + oldItems.insert((*it)->collectionItem(), *it); + + m_search.search(); + PlaylistItemList matched = m_search.matchedItems(); + PlaylistItemList newItems; + + for(PlaylistItemList::ConstIterator it = matched.begin(); it != matched.end(); ++it) { + if(!oldItems.remove((*it)->collectionItem())) + newItems.append((*it)->collectionItem()); + } + + // kdDebug(65432) << k_funcinfo << "newItems.size() == " << newItems.size() << endl; + + for(QPtrDictIterator<PlaylistItem> it(oldItems); it.current(); ++it) + clearItem(it.current(), false); + + if(!oldItems.isEmpty() && newItems.isEmpty()) + dataChanged(); + + createItems(newItems); + + if(synchronizePlaying()) { + kdDebug(65432) << k_funcinfo << "synchronizing playing" << endl; + synchronizePlayingItems(m_search.playlists(), true); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const SearchPlaylist &p) +{ + s << p.name() + << p.playlistSearch(); + + return s; +} + +QDataStream &operator>>(QDataStream &s, SearchPlaylist &p) +{ + QString name; + PlaylistSearch search; + + s >> name + >> search; + + p.setName(name); + p.setPlaylistSearch(search, false); + + return s; +} + +#include "searchplaylist.moc" diff --git a/juk/searchplaylist.h b/juk/searchplaylist.h new file mode 100644 index 00000000..2acd1e5c --- /dev/null +++ b/juk/searchplaylist.h @@ -0,0 +1,48 @@ +/*************************************************************************** + begin : Mon May 5 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef SEARCHPLAYLIST_H +#define SEARCHPLAYLIST_H + +#include "dynamicplaylist.h" + +class SearchPlaylist : public DynamicPlaylist +{ + Q_OBJECT +public: + SearchPlaylist(PlaylistCollection *collection, + const PlaylistSearch &search = PlaylistSearch(), + const QString &name = QString::null, + bool setupPlaylist = true, + bool synchronizePlaying = false); + + PlaylistSearch playlistSearch() const { return m_search; } + void setPlaylistSearch(const PlaylistSearch &s, bool update = true); + virtual bool searchIsEditable() const { return true; } + +protected: + /** + * Runs the search to update the current items. + */ + virtual void updateItems(); + +private: + PlaylistSearch m_search; +}; + +QDataStream &operator<<(QDataStream &s, const SearchPlaylist &p); +QDataStream &operator>>(QDataStream &s, SearchPlaylist &p); + +#endif diff --git a/juk/searchwidget.cpp b/juk/searchwidget.cpp new file mode 100644 index 00000000..83b7808e --- /dev/null +++ b/juk/searchwidget.cpp @@ -0,0 +1,292 @@ +/*************************************************************************** + begin : Sun Mar 6 2003 + copyright : (C) 2003 by Richard Lärkäng + email : nouseforaname@home.se + + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <klocale.h> +#include <klineedit.h> +#include <kiconloader.h> +#include <kcombobox.h> +#include <kdebug.h> +#include <kaction.h> + +#include <qlayout.h> +#include <qlabel.h> +#include <qcheckbox.h> +#include <qpushbutton.h> +#include <qtoolbutton.h> + +#include "searchwidget.h" +#include "collectionlist.h" +#include "actioncollection.h" + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// SearchLine public methods +//////////////////////////////////////////////////////////////////////////////// + +SearchLine::SearchLine(QWidget *parent, bool simple, const char *name) : + QHBox(parent, name), + m_simple(simple), + m_searchFieldsBox(0) +{ + setSpacing(5); + + if(!m_simple) { + m_searchFieldsBox = new KComboBox(this, "searchFields"); + connect(m_searchFieldsBox, SIGNAL(activated(int)), + this, SIGNAL(signalQueryChanged())); + } + + m_lineEdit = new KLineEdit(this, "searchLineEdit"); + m_lineEdit->installEventFilter(this); + connect(m_lineEdit, SIGNAL(textChanged(const QString &)), + this, SIGNAL(signalQueryChanged())); + connect(m_lineEdit, SIGNAL(returnPressed()), + this, SLOT(slotActivate())); + + if(!m_simple) { + m_caseSensitive = new KComboBox(this); + m_caseSensitive->insertItem(i18n("Normal Matching"), 0); + m_caseSensitive->insertItem(i18n("Case Sensitive"), 1); + m_caseSensitive->insertItem(i18n("Pattern Matching"), 2); + connect(m_caseSensitive, SIGNAL(activated(int)), + this, SIGNAL(signalQueryChanged())); + } + else + m_caseSensitive = 0; + + updateColumns(); +} + +PlaylistSearch::Component SearchLine::searchComponent() const +{ + QString query = m_lineEdit->text(); + bool caseSensitive = m_caseSensitive && m_caseSensitive->currentItem() == CaseSensitive; + + Playlist *playlist = CollectionList::instance(); + + QValueList<int> searchedColumns; + + if(!m_searchFieldsBox || m_searchFieldsBox->currentItem() == 0) { + QValueListConstIterator<int> it = m_columnList.begin(); + for(; it != m_columnList.end(); ++it) { + if(playlist->isColumnVisible(*it)) + searchedColumns.append(*it); + } + } + else + searchedColumns.append(m_columnList[m_searchFieldsBox->currentItem() - 1]); + + if(m_caseSensitive && m_caseSensitive->currentItem() == Pattern) + return PlaylistSearch::Component(QRegExp(query), searchedColumns); + else + return PlaylistSearch::Component(query, caseSensitive, searchedColumns); +} + +void SearchLine::setSearchComponent(const PlaylistSearch::Component &component) +{ + if(component == searchComponent()) + return; + + if(m_simple || !component.isPatternSearch()) { + m_lineEdit->setText(component.query()); + if(m_caseSensitive) + m_caseSensitive->setCurrentItem(component.isCaseSensitive() ? CaseSensitive : Default); + } + else { + m_lineEdit->setText(component.pattern().pattern()); + if(m_caseSensitive) + m_caseSensitive->setCurrentItem(Pattern); + } + + if(!m_simple) { + if(component.columns().isEmpty() || component.columns().size() > 1) + m_searchFieldsBox->setCurrentItem(0); + else + m_searchFieldsBox->setCurrentItem(component.columns().front() + 1); + } +} + +void SearchLine::clear() +{ + // We don't want to emit the signal if it's already empty. + if(!m_lineEdit->text().isEmpty()) + m_lineEdit->clear(); +} + +void SearchLine::setFocus() +{ + m_lineEdit->setFocus(); +} + +bool SearchLine::eventFilter(QObject *watched, QEvent *e) +{ + if(watched != m_lineEdit || e->type() != QEvent::KeyPress) + return QHBox::eventFilter(watched, e); + + QKeyEvent *key = static_cast<QKeyEvent *>(e); + if(key->key() == Qt::Key_Down) + emit signalDownPressed(); + + return QHBox::eventFilter(watched, e); +} + +void SearchLine::slotActivate() +{ + action("stop")->activate(); + action("playFirst")->activate(); +} + +void SearchLine::updateColumns() +{ + QString currentText; + + if(m_searchFieldsBox) { + currentText = m_searchFieldsBox->currentText(); + m_searchFieldsBox->clear(); + } + + QStringList columnHeaders; + + columnHeaders.append(QString("<%1>").arg(i18n("All Visible"))); + + Playlist *playlist = CollectionList::instance(); + + int selection = -1; + m_columnList.clear(); + + for(int i = 0; i < playlist->columns(); i++) { + m_columnList.append(i); + QString text = playlist->columnText(i); + columnHeaders.append(text); + if(currentText == text) + selection = m_columnList.size() - 1; + } + + if(m_searchFieldsBox) { + m_searchFieldsBox->insertStringList(columnHeaders); + m_searchFieldsBox->setCurrentItem(selection + 1); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// SearchWidget public methods +//////////////////////////////////////////////////////////////////////////////// + +SearchWidget::SearchWidget(QWidget *parent, const char *name) : KToolBar(parent, name) +{ + setupLayout(); + updateColumns(); +} + +SearchWidget::~SearchWidget() +{ + +} + +void SearchWidget::setSearch(const PlaylistSearch &search) +{ + PlaylistSearch::ComponentList components = search.components(); + + if(components.isEmpty()) { + clear(); + return; + } + + m_searchLine->setSearchComponent(*components.begin()); +} + +QString SearchWidget::searchText() const +{ + return m_searchLine->searchComponent().query(); +} + +void SearchWidget::setSearchText(const QString &text) +{ + m_searchLine->setSearchComponent(PlaylistSearch::Component(text)); +} + +PlaylistSearch SearchWidget::search(const PlaylistList &playlists) const +{ + PlaylistSearch::ComponentList components; + components.append(m_searchLine->searchComponent()); + return PlaylistSearch(playlists, components); +} + + + +//////////////////////////////////////////////////////////////////////////////// +// SearchWidget public slots +//////////////////////////////////////////////////////////////////////////////// + +void SearchWidget::clear() +{ + m_searchLine->clear(); +} + +void SearchWidget::setEnabled(bool enable) +{ + emit signalShown(enable); + setShown(enable); +} + +void SearchWidget::setFocus() +{ + m_searchLine->setFocus(); +} + +//////////////////////////////////////////////////////////////////////////////// +// SearchWidget private methods +//////////////////////////////////////////////////////////////////////////////// + +void SearchWidget::updateColumns() +{ + m_searchLine->updateColumns(); +} + +void SearchWidget::setupLayout() +{ + boxLayout()->setSpacing(5); + + QToolButton *clearSearchButton = new QToolButton(this); + clearSearchButton->setTextLabel(i18n("Clear Search"), true); + clearSearchButton->setIconSet(SmallIconSet("locationbar_erase")); + + QLabel *label = new QLabel(i18n("Search:"), this, "kde toolbar widget"); + + m_searchLine = new SearchLine(this, true, "kde toolbar widget"); + + label->setBuddy(m_searchLine); + + connect(m_searchLine, SIGNAL(signalQueryChanged()), this, SIGNAL(signalQueryChanged())); + connect(m_searchLine, SIGNAL(signalDownPressed()), this, SIGNAL(signalDownPressed())); + connect(clearSearchButton, SIGNAL(pressed()), m_searchLine, SLOT(clear())); + setStretchableWidget(m_searchLine); + + // I've decided that I think this is ugly, for now. + + /* + QToolButton *b = new QToolButton(this); + b->setTextLabel(i18n("Advanced Search"), true); + b->setIconSet(SmallIconSet("wizard")); + + connect(b, SIGNAL(clicked()), this, SIGNAL(signalAdvancedSearchClicked())); + */ +} + +#include "searchwidget.moc" diff --git a/juk/searchwidget.h b/juk/searchwidget.h new file mode 100644 index 00000000..37a44cb7 --- /dev/null +++ b/juk/searchwidget.h @@ -0,0 +1,111 @@ +/*************************************************************************** + begin : Sun Mar 6 2003 + copyright : (C) 2003 by Richard Lärkäng + email : nouseforaname@home.se + + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef SEARCHWIDGET_H +#define SEARCHWIDGET_H + +#include <ktoolbar.h> + +#include <qhbox.h> + +#include "playlistsearch.h" +#include "jukIface.h" + +class QCheckBox; + +class KComboBox; + +class Playlist; + +class SearchLine : public QHBox +{ + Q_OBJECT + +public: + enum Mode { Default = 0, CaseSensitive = 1, Pattern = 2 }; + + SearchLine(QWidget *parent, bool simple = false, const char *name = 0); + virtual ~SearchLine() {} + + PlaylistSearch::Component searchComponent() const; + void setSearchComponent(const PlaylistSearch::Component &component); + + void updateColumns(); + +public slots: + void clear(); + virtual void setFocus(); + +protected: + virtual bool eventFilter(QObject *watched, QEvent *e); + +signals: + void signalQueryChanged(); + void signalDownPressed(); + +private slots: + void slotActivate(); + +private: + bool m_simple; + KLineEdit *m_lineEdit; + KComboBox *m_searchFieldsBox; + KComboBox *m_caseSensitive; + QValueList<int> m_columnList; +}; + +class SearchWidget : public KToolBar, public SearchIface +{ + Q_OBJECT + +public: + SearchWidget(QWidget *parent, const char *name = 0); + virtual ~SearchWidget(); + + PlaylistSearch search(const PlaylistList &playlists) const; + void setSearch(const PlaylistSearch &search); + + virtual QString searchText() const; + virtual void setSearchText(const QString &text); + +public slots: + void clear(); + void setEnabled(bool enable); + virtual void setFocus(); + +signals: + void signalQueryChanged(); + void signalAdvancedSearchClicked(); + + // This signal is only emitted when the Show/Hide action is triggered. + // Minimizing/closing the JuK window will not trigger this signal. + + void signalShown(bool shown); + + void signalDownPressed(); + +private: + void updateColumns(); + void setupLayout(); + +private: + SearchLine *m_searchLine; + QStringList m_columnHeaders; +}; + +#endif diff --git a/juk/slideraction.cpp b/juk/slideraction.cpp new file mode 100644 index 00000000..eacb4242 --- /dev/null +++ b/juk/slideraction.cpp @@ -0,0 +1,357 @@ +/*************************************************************************** + begin : Wed Feb 6 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <ktoolbar.h> +#include <klocale.h> +#include <kiconloader.h> +#include <kdebug.h> + +#include <qtooltip.h> +#include <qlayout.h> +#include <qlabel.h> +#include <qtimer.h> + +#include "slideraction.h" + +//////////////////////////////////////////////////////////////////////////////// +// convenience class +//////////////////////////////////////////////////////////////////////////////// + +/** + * This "custom" slider reverses the left and middle buttons. Typically the + * middle button "instantly" seeks rather than moving the slider towards the + * click position in fixed intervals. This behavior has now been mapped on + * to the left mouse button. + */ + +class TrackPositionSlider : public QSlider +{ +public: + TrackPositionSlider(QWidget *parent, const char *name) : QSlider(parent, name) + { + setFocusPolicy(NoFocus); + } + +protected: + virtual void mousePressEvent(QMouseEvent *e) + { + if(e->button() == LeftButton) { + QMouseEvent reverse(QEvent::MouseButtonPress, e->pos(), MidButton, e->state()); + QSlider::mousePressEvent(&reverse); + emit sliderPressed(); + } + else if(e->button() == MidButton) { + QMouseEvent reverse(QEvent::MouseButtonPress, e->pos(), LeftButton, e->state()); + QSlider::mousePressEvent(&reverse); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// VolumeSlider implementation +//////////////////////////////////////////////////////////////////////////////// + +VolumeSlider::VolumeSlider(Orientation o, QWidget *parent, const char *name) : + QSlider(o, parent, name) +{ + connect(this, SIGNAL(valueChanged(int)), this, SLOT(slotValueChanged(int))); +} + +void VolumeSlider::wheelEvent(QWheelEvent *e) +{ + if(orientation() == Horizontal) { + QWheelEvent transposed(e->pos(), -(e->delta()), e->state(), e->orientation()); + QSlider::wheelEvent(&transposed); + } + else + QSlider::wheelEvent(e); +} + +void VolumeSlider::focusInEvent(QFocusEvent *) +{ + clearFocus(); +} + +int VolumeSlider::volume() const +{ + if(orientation() == Horizontal) + return value(); + else + return maxValue() - value(); +} + +void VolumeSlider::setVolume(int value) +{ + if(orientation() == Horizontal) + setValue(value); + else + setValue(maxValue() - value); +} + +void VolumeSlider::setOrientation(Orientation o) +{ + if(o == orientation()) + return; + + blockSignals(true); + setValue(maxValue() - value()); + blockSignals(false); + QSlider::setOrientation(o); +} + +void VolumeSlider::slotValueChanged(int value) +{ + if(orientation() == Horizontal) + emit signalVolumeChanged(value); + else + emit signalVolumeChanged(maxValue() - value); +} + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +const int SliderAction::minPosition = 0; +const int SliderAction::maxPosition = 1000; + +SliderAction::SliderAction(const QString &text, QObject *parent, const char *name) + : KAction(text, 0, parent, name), + m_toolBar(0), + m_layout(0), + m_trackPositionSlider(0), + m_volumeSlider(0), + m_dragging(false), + m_volumeDragging(false) +{ + +} + +SliderAction::~SliderAction() +{ + +} + +int SliderAction::plug(QWidget *parent, int index) +{ + QWidget *w = createWidget(parent); + + if(!w) + return -1; + + // the check for null makes sure that there is only one toolbar that this is + // "plugged" in to + + if(parent->inherits("KToolBar") && !m_toolBar) { + m_toolBar = static_cast<KToolBar *>(parent); + + int id = KAction::getToolButtonID(); + + m_toolBar->insertWidget(id, w->width(), w, index); + + addContainer(m_toolBar, id); + + connect(m_toolBar, SIGNAL(destroyed()), this, SLOT(slotToolbarDestroyed())); + connect(m_toolBar, SIGNAL(orientationChanged(Orientation)), + this, SLOT(slotUpdateOrientation())); + connect(m_toolBar, SIGNAL(placeChanged(QDockWindow::Place)), + this, SLOT(slotUpdateOrientation())); + + slotUpdateOrientation(); + return (containerCount() - 1); + } + else + slotUpdateOrientation(); + + return -1; +} + + +void SliderAction::unplug(QWidget *parent) +{ + if (parent->inherits("KToolBar")) { + m_toolBar = static_cast<KToolBar *>(parent); + + int index = findContainer(m_toolBar); + if (index != -1) { + m_toolBar->removeItem(itemId(index)); + removeContainer(index); + + m_toolBar = 0; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void SliderAction::slotUpdateOrientation() +{ + // if the toolbar is not null and either the dockWindow not defined or is the toolbar + + if(!m_toolBar) + return; + + if(m_toolBar->barPos() == KToolBar::Right || m_toolBar->barPos() == KToolBar::Left) { + m_trackPositionSlider->setOrientation(Vertical); + m_volumeSlider->setOrientation(Vertical); + m_layout->setDirection(QBoxLayout::TopToBottom); + } + else { + m_trackPositionSlider->setOrientation(Horizontal); + m_volumeSlider->setOrientation(Horizontal); + m_layout->setDirection(QBoxLayout::LeftToRight); + } + slotUpdateSize(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +QWidget *SliderAction::createWidget(QWidget *parent) // virtual -- used by base class +{ + if(parent) { + QWidget *base = new QWidget(parent); + base->setBackgroundMode(parent->backgroundMode()); + base->setName("kde toolbar widget"); + + KToolBar *toolBar = dynamic_cast<KToolBar *>(parent); + + if(toolBar) + toolBar->setStretchableWidget(base); + + Orientation orientation; + + if(toolBar && toolBar->barPos() == KToolBar::Right || toolBar->barPos() == KToolBar::Left) + orientation = Vertical; + else + orientation = Horizontal; + + m_layout = new QBoxLayout(base, QBoxLayout::TopToBottom, 5, 5); + + m_layout->addItem(new QSpacerItem(20, 1)); + + QLabel *trackPositionLabel = new QLabel(base); + trackPositionLabel->setName("kde toolbar widget"); + trackPositionLabel->setPixmap(SmallIcon("player_time")); + QToolTip::add(trackPositionLabel, i18n("Track position")); + m_layout->addWidget(trackPositionLabel); + + m_trackPositionSlider = new TrackPositionSlider(base, "trackPositionSlider"); + m_trackPositionSlider->setMaxValue(maxPosition); + QToolTip::add(m_trackPositionSlider, i18n("Track position")); + m_layout->addWidget(m_trackPositionSlider); + connect(m_trackPositionSlider, SIGNAL(sliderPressed()), this, SLOT(slotSliderPressed())); + connect(m_trackPositionSlider, SIGNAL(sliderReleased()), this, SLOT(slotSliderReleased())); + + m_layout->addItem(new QSpacerItem(10, 1)); + + QLabel *volumeLabel = new QLabel(base); + volumeLabel->setName("kde toolbar widget"); + volumeLabel->setPixmap(SmallIcon("player_volume")); + QToolTip::add(volumeLabel, i18n("Volume")); + m_layout->addWidget(volumeLabel); + + m_volumeSlider = new VolumeSlider(orientation, base, "volumeSlider"); + m_volumeSlider->setMaxValue(100); + QToolTip::add(m_volumeSlider, i18n("Volume")); + m_layout->addWidget(m_volumeSlider); + connect(m_volumeSlider, SIGNAL(signalVolumeChanged(int)), SIGNAL(signalVolumeChanged(int))); + connect(m_volumeSlider, SIGNAL(sliderPressed()), this, SLOT(slotVolumeSliderPressed())); + connect(m_volumeSlider, SIGNAL(sliderReleased()), this, SLOT(slotVolumeSliderReleased())); + + m_volumeSlider->setName("kde toolbar widget"); + m_trackPositionSlider->setName("kde toolbar widget"); + + m_layout->setStretchFactor(m_trackPositionSlider, 4); + m_layout->setStretchFactor(m_volumeSlider, 1); + + connect(parent, SIGNAL(modechange()), this, SLOT(slotUpdateSize())); + + return base; + } + else + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void SliderAction::slotUpdateSize() +{ + static const int offset = 3; + static const int absoluteMax = 10000; + + if(!m_toolBar) + return; + + if(m_toolBar->barPos() == KToolBar::Right || m_toolBar->barPos() == KToolBar::Left) { + m_volumeSlider->setMaximumWidth(m_toolBar->iconSize() - offset); + m_volumeSlider->setMaximumHeight(volumeMax); + + m_trackPositionSlider->setMaximumWidth(m_toolBar->iconSize() - offset); + m_trackPositionSlider->setMaximumHeight(absoluteMax); + } + else { + m_volumeSlider->setMaximumHeight(m_toolBar->iconSize() - offset); + m_volumeSlider->setMaximumWidth(volumeMax); + + m_trackPositionSlider->setMaximumHeight(m_toolBar->iconSize() - offset); + m_trackPositionSlider->setMaximumWidth(absoluteMax); + } +} + +void SliderAction::slotSliderPressed() +{ + m_dragging = true; +} + +void SliderAction::slotSliderReleased() +{ + m_dragging = false; + emit signalPositionChanged(m_trackPositionSlider->value()); +} + +void SliderAction::slotVolumeSliderPressed() +{ + m_volumeDragging = true; +} + +void SliderAction::slotVolumeSliderReleased() +{ + m_volumeDragging = false; + emit signalVolumeChanged(m_volumeSlider->value()); +} + +void SliderAction::slotToolbarDestroyed() +{ + int index = findContainer(m_toolBar); + if(index != -1) + removeContainer(index); + + m_toolBar = 0; + + // This is probably a leak, but this code path hardly ever occurs, and it's + // too hard to debug correctly. + + m_trackPositionSlider = 0; + m_volumeSlider = 0; +} + +#include "slideraction.moc" + +// vim: set et sw=4 ts=4: diff --git a/juk/slideraction.h b/juk/slideraction.h new file mode 100644 index 00000000..f312196c --- /dev/null +++ b/juk/slideraction.h @@ -0,0 +1,97 @@ +/*************************************************************************** + begin : Wed Feb 6 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef SLIDERACTION_H +#define SLIDERACTION_H + +#include <kaction.h> +#include <qslider.h> + +class QBoxLayout; +class QDockWindow; + +class VolumeSlider : public QSlider +{ + Q_OBJECT + +public: + VolumeSlider(Orientation o, QWidget *parent, const char *name); + + int volume() const; + void setVolume(int value); + + void setOrientation(Orientation o); + +signals: + void signalVolumeChanged(int value); + +protected: + virtual void wheelEvent(QWheelEvent *e); + virtual void focusInEvent(QFocusEvent *); + +private slots: + void slotValueChanged(int value); +}; + +class SliderAction : public KAction +{ + Q_OBJECT + +public: + SliderAction(const QString &text, QObject *parent, const char *name); + virtual ~SliderAction(); + + VolumeSlider *volumeSlider() const { return m_volumeSlider; } + QSlider *trackPositionSlider() const { return m_trackPositionSlider; } + + bool dragging() const { return m_dragging; } + bool volumeDragging() const { return m_volumeDragging; } + + virtual int plug(QWidget *parent, int index = -1); + virtual void unplug(QWidget *widget); + + static const int minPosition; + static const int maxPosition; + +public slots: + void slotUpdateOrientation(); + +signals: + void signalPositionChanged(int position); + void signalVolumeChanged(int volume); + +private: + QWidget *createWidget(QWidget *parent); + +private slots: + void slotUpdateSize(); + void slotVolumeSliderPressed(); + void slotVolumeSliderReleased(); + void slotSliderPressed(); + void slotSliderReleased(); + void slotToolbarDestroyed(); + +private: + KToolBar *m_toolBar; + QBoxLayout *m_layout; + QSlider *m_trackPositionSlider; + VolumeSlider *m_volumeSlider; + bool m_dragging; + bool m_volumeDragging; + + static const int volumeMax = 50; +}; + +#endif diff --git a/juk/sortedstringlist.cpp b/juk/sortedstringlist.cpp new file mode 100644 index 00000000..87f2938d --- /dev/null +++ b/juk/sortedstringlist.cpp @@ -0,0 +1,180 @@ +/*************************************************************************** + begin : Wed Jan 29 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kdebug.h> + +#include "sortedstringlist.h" + +class SortedStringList::Node +{ +public: + Node(const QString &value) : key(value), parent(0), left(0), right(0) {} + ~Node() {} + + QString key; + Node *parent; + Node *left; + Node *right; +}; + +SortedStringList::SortedStringList() : m_root(0) +{ + +} + +SortedStringList::~SortedStringList() +{ + +} + +bool SortedStringList::insert(const QString &value) +{ + return BSTInsert(value); +} + +bool SortedStringList::contains(const QString &value) const +{ + return find(value); +} + +SortedStringList::Node *SortedStringList::treeMinimum(Node *n) const +{ + while(n->left) + n = n->left; + return n; +} + +SortedStringList::Node *SortedStringList::treeSuccessor(Node *n) const +{ + if(n->right) + return treeMinimum(n->right); + + Node *p = n->parent; + + while(p && n == p->right) { + n = p; + p = p->parent; + } + + return p; +} + +bool SortedStringList::remove(const QString &value) +{ + Node *n = find(value); + + if(!n) + return false; + + Node *y; + Node *x; + + if(!n->left || !n->right) + y = n; + else + y = treeSuccessor(n); + + if(y->left) + x = y->left; + else + x = y->right; + + if(x) + x->parent = y->parent; + + if(!y->parent) + m_root = x; + else { + if(y == y->parent->left) + y->parent->left = x; + else + y->parent->right = x; + } + + if(y != x) + n->key = y->key; + + delete y; + + return true; +} + +QStringList SortedStringList::values() const +{ + QStringList l; + traverse(m_root, l); + return l; +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +SortedStringList::Node *SortedStringList::find(const QString &value) const +{ + Node *n = m_root; + while(n && value != n->key) { + if(value < n->key) + n = n->left; + else + n = n->right; + } + + return n; +} + +bool SortedStringList::BSTInsert(const QString &value) +{ + Node *previousNode = 0; + Node *node = m_root; + + while(node) { + previousNode = node; + if(value == node->key) + return true; + else if(value < node->key) + node = node->left; + else + node = node->right; + } + + if(previousNode && value == previousNode->key) + return true; + + Node *n = new Node(value); + + n->parent = previousNode; + + if(!m_root) + m_root = n; + else { + if(value < previousNode->key) + previousNode->left = n; + else + previousNode->right = n; + } + + return false; +} + +void SortedStringList::traverse(const Node *n, QStringList &list) const +{ + if(!n) + return; + + traverse(n->left, list); + list.append(n->key); + traverse(n->right, list); +} diff --git a/juk/sortedstringlist.h b/juk/sortedstringlist.h new file mode 100644 index 00000000..108a16c6 --- /dev/null +++ b/juk/sortedstringlist.h @@ -0,0 +1,59 @@ +/*************************************************************************** + begin : Wed Jan 29 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef SORTEDSTRINGLIST_H +#define SORTEDSTRINGLIST_H + +#include <qstringlist.h> + +class SortedStringList +{ +public: + SortedStringList(); + ~SortedStringList(); + + /** + * Insert the value. Returns true if the item was already in the list + * or false otherwise. + */ + bool insert(const QString &value); + bool contains(const QString &value) const; + bool remove(const QString &value); + + /** + * Returns a sorted list of the values. + * Warning, this method is expensive and shouldn't be used except when + * necessary. + */ + QStringList values() const; + +private: + class Node; + + Node *find(const QString &value) const; + /** + * The insertion implementation. Returns true if the item was already + * present in the list. + */ + bool BSTInsert(const QString &value); + void traverse(const Node *n, QStringList &list) const; + + Node *treeMinimum(Node *n) const; + Node *treeSuccessor(Node *n) const; + + Node *m_root; +}; + +#endif diff --git a/juk/splashscreen.cpp b/juk/splashscreen.cpp new file mode 100644 index 00000000..5f91a070 --- /dev/null +++ b/juk/splashscreen.cpp @@ -0,0 +1,104 @@ +/*************************************************************************** + begin : Sun Dec 8 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kapplication.h> +#include <kiconloader.h> +#include <klocale.h> +#include <kstandarddirs.h> +#include <kdebug.h> + +#include <qpainter.h> + +#include "splashscreen.h" + +SplashScreen *SplashScreen::splash = 0; +bool SplashScreen::done = false; +int SplashScreen::count = 0; + +static QString loadedText(int i) +{ + static QString loading = i18n("Loading").upper(); + return loading + ": " + QString::number(i);; +} + +//////////////////////////////////////////////////////////////////////////////// +// pubic members +//////////////////////////////////////////////////////////////////////////////// + +SplashScreen *SplashScreen::instance() +{ + if(!splash && !done) + splash = new SplashScreen(); + return splash; +} + +void SplashScreen::finishedLoading() +{ + done = true; + delete splash; + splash = 0; +} + +void SplashScreen::increment() +{ + if(splash) { + count++; + if(( count & 63 ) == 0) + splash->processEvents(); + } +} + +void SplashScreen::update() +{ + if(splash) + splash->processEvents(); +} + +//////////////////////////////////////////////////////////////////////////////// +// protected members +//////////////////////////////////////////////////////////////////////////////// + +SplashScreen::SplashScreen() : QLabel(0 , "splashScreen", Qt::WStyle_Splash) +{ + QPixmap background = UserIcon("splash"); + resize(background.size()); + setPaletteBackgroundPixmap(background); + + setMargin(7); + setAlignment(AlignLeft | AlignBottom); + + setPaletteForegroundColor(QColor(107, 158, 194)); + + QFont f = font(); + f.setPixelSize(10); + setFont(f); + + setText(loadedText(0)); +} + +SplashScreen::~SplashScreen() +{ + +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void SplashScreen::processEvents() +{ + setText(loadedText(count)); + kapp->processEvents(); +} diff --git a/juk/splashscreen.h b/juk/splashscreen.h new file mode 100644 index 00000000..407ed509 --- /dev/null +++ b/juk/splashscreen.h @@ -0,0 +1,52 @@ +/*************************************************************************** + begin : Sun Dec 8 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef SPLASHSCREEN_H +#define SPLASHSCREEN_H + +#include <qlabel.h> + +/** + * Well, all of this session restoration sure is fun, but it's starting to take + * a while, especially say, if you're building KDE and indexing your file system + * in the background. ;-) So, despite my general hate of splashscreens I + * thought on appropriate here. + * + * As in other places, this is a singleton. That makes it relatively easy to + * handle the updates from whichever class seems appropriate through static + * methods. + */ + +class SplashScreen : public QLabel +{ +public: + static SplashScreen *instance(); + static void finishedLoading(); + static void increment(); + static void update(); + +protected: + SplashScreen(); + virtual ~SplashScreen(); + +private: + void processEvents(); + + static SplashScreen *splash; + static bool done; + static int count; +}; + +#endif diff --git a/juk/statuslabel.cpp b/juk/statuslabel.cpp new file mode 100644 index 00000000..fa8dfb38 --- /dev/null +++ b/juk/statuslabel.cpp @@ -0,0 +1,205 @@ +/*************************************************************************** + begin : Fri Oct 18 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kaction.h> +#include <kpushbutton.h> +#include <kiconloader.h> +#include <ksqueezedtextlabel.h> +#include <klocale.h> +#include <kdebug.h> + +#include <qtooltip.h> +#include <qlayout.h> + +#include "statuslabel.h" +#include "filehandle.h" +#include "playlistinterface.h" +#include "actioncollection.h" +#include "tag.h" + +using namespace ActionCollection; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +StatusLabel::StatusLabel(PlaylistInterface *playlist, QWidget *parent, const char *name) : + QHBox(parent, name), + PlaylistObserver(playlist), + m_showTimeRemaining(false) +{ + QFrame *trackAndPlaylist = new QFrame(this); + trackAndPlaylist->setFrameStyle(Box | Sunken); + trackAndPlaylist->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // Make sure that we have enough of a margin to suffice for the borders, + // hence the "lineWidth() * 2" + QHBoxLayout *trackAndPlaylistLayout = new QHBoxLayout(trackAndPlaylist, + trackAndPlaylist->lineWidth() * 2, + 5, "trackAndPlaylistLayout"); + trackAndPlaylistLayout->addSpacing(5); + + m_playlistLabel = new KSqueezedTextLabel(trackAndPlaylist, "playlistLabel"); + trackAndPlaylistLayout->addWidget(m_playlistLabel); + m_playlistLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_playlistLabel->setTextFormat(PlainText); + m_playlistLabel->setAlignment(AlignLeft | AlignVCenter); + + m_trackLabel = new KSqueezedTextLabel(trackAndPlaylist, "trackLabel"); + trackAndPlaylistLayout->addWidget(m_trackLabel); + m_trackLabel->setAlignment(AlignRight | AlignVCenter); + m_trackLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_trackLabel->setTextFormat(PlainText); + + trackAndPlaylistLayout->addSpacing(5); + + m_itemTimeLabel = new QLabel(this); + QFontMetrics fontMetrics(font()); + m_itemTimeLabel->setAlignment(AlignCenter); + m_itemTimeLabel->setMinimumWidth(fontMetrics.boundingRect("000:00 / 000:00").width()); + m_itemTimeLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + m_itemTimeLabel->setFrameStyle(Box | Sunken); + m_itemTimeLabel->installEventFilter(this); + + setItemTotalTime(0); + setItemCurrentTime(0); + + QHBox *jumpBox = new QHBox(this); + jumpBox->setFrameStyle(Box | Sunken); + jumpBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Minimum); + + QPushButton *jumpButton = new QPushButton(jumpBox); + jumpButton->setPixmap(SmallIcon("up")); + jumpButton->setFlat(true); + + QToolTip::add(jumpButton, i18n("Jump to the currently playing item")); + connect(jumpButton, SIGNAL(clicked()), action("showPlaying"), SLOT(activate())); + + installEventFilter(this); + + updateData(); +} + +StatusLabel::~StatusLabel() +{ + +} + +void StatusLabel::updateCurrent() +{ + if(playlist()->playing()) { + FileHandle file = playlist()->currentFile(); + + QString mid = file.tag()->artist().isEmpty() || file.tag()->title().isEmpty() + ? QString::null : QString(" - "); + + QString text = file.tag()->artist() + mid + file.tag()->title(); + + m_trackLabel->setText(text); + m_playlistLabel->setText(playlist()->name().simplifyWhiteSpace()); + } +} + +void StatusLabel::updateData() +{ + updateCurrent(); + + if(!playlist()->playing()) { + setItemTotalTime(0); + setItemCurrentTime(0); + + int time = playlist()->time(); + + int days = time / (60 * 60 * 24); + int hours = time / (60 * 60) % 24; + int minutes = time / 60 % 60; + int seconds = time % 60; + + QString timeString; + + if(days > 0) { + timeString = i18n("1 day", "%n days", days); + timeString.append(" "); + } + + if(days > 0 || hours > 0) + timeString.append(QString().sprintf("%1d:%02d:%02d", hours, minutes, seconds)); + else + timeString.append(QString().sprintf("%1d:%02d", minutes, seconds)); + + m_playlistLabel->setText(playlist()->name()); + m_trackLabel->setText(i18n("1 item", "%n items", playlist()->count()) + " - " + timeString); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +void StatusLabel::updateTime() +{ + int minutes; + int seconds; + + if(m_showTimeRemaining) { + minutes = int((m_itemTotalTime - m_itemCurrentTime) / 60); + seconds = (m_itemTotalTime - m_itemCurrentTime) % 60; + } + else { + minutes = int(m_itemCurrentTime / 60); + seconds = m_itemCurrentTime % 60; + } + + int totalMinutes = int(m_itemTotalTime / 60); + int totalSeconds = m_itemTotalTime % 60; + + QString timeString = formatTime(minutes, seconds) + " / " + + formatTime(totalMinutes, totalSeconds); + m_itemTimeLabel->setText(timeString); +} + +bool StatusLabel::eventFilter(QObject *o, QEvent *e) +{ + if(!o || !e) + return false; + + QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(e); + if(e->type() == QEvent::MouseButtonRelease && + mouseEvent->button() == LeftButton) + { + if(o == m_itemTimeLabel) { + m_showTimeRemaining = !m_showTimeRemaining; + updateTime(); + } + else + action("showPlaying")->activate(); + + return true; + } + return false; +} + +QString StatusLabel::formatTime(int minutes, int seconds) // static +{ + QString m = QString::number(minutes); + if(m.length() == 1) + m = "0" + m; + QString s = QString::number(seconds); + if(s.length() == 1) + s = "0" + s; + return m + ":" + s; +} + +#include "statuslabel.moc" diff --git a/juk/statuslabel.h b/juk/statuslabel.h new file mode 100644 index 00000000..6c733f21 --- /dev/null +++ b/juk/statuslabel.h @@ -0,0 +1,64 @@ +/*************************************************************************** + begin : Fri Oct 18 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef STATUSLABEL_H +#define STATUSLABEL_H + +#include "playlistinterface.h" + +#include <qhbox.h> + +class QLabel; +class KSqueezedTextLabel; + +class FileHandle; + +class StatusLabel : public QHBox, public PlaylistObserver +{ + Q_OBJECT + +public: + StatusLabel(PlaylistInterface *playlist, QWidget *parent = 0, const char *name = 0); + virtual ~StatusLabel(); + virtual void updateCurrent(); + +public slots: + /** + * This just sets internal variables that are used by setItemCurrentTime(). + * Please call that method to display the time. + */ + void setItemTotalTime(int time) { m_itemTotalTime = time; } + void setItemCurrentTime(int time) { m_itemCurrentTime = time; updateTime(); } + virtual void updateData(); + +signals: + void jumpButtonClicked(); + +private: + void updateTime(); + virtual bool eventFilter(QObject *o, QEvent *e); + + static QString formatTime(int minutes, int seconds); + + int m_itemTotalTime; + int m_itemCurrentTime; + bool m_showTimeRemaining; + + KSqueezedTextLabel *m_playlistLabel; + KSqueezedTextLabel *m_trackLabel; + QLabel *m_itemTimeLabel; +}; + +#endif diff --git a/juk/stringhash.h b/juk/stringhash.h new file mode 100644 index 00000000..75992eec --- /dev/null +++ b/juk/stringhash.h @@ -0,0 +1,312 @@ +/*************************************************************************** + begin : Sun Feb 2 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef STRINGHASH_H +#define STRINGHASH_H + +#include <qptrvector.h> + +#include "filehandle.h" + +/** + * A simple hash representing an (un-mapped) set of data. + */ + +template <class T> class Hash +{ + friend class Iterator; +public: + + Hash() : m_table(m_tableSize) {} + ~Hash(); + + /** + * To combine two operations into one (that takes the same amount as each + * independantly) this inserts an item and returns true if the item was + * already in the set or false if it did not. + */ + bool insert(T value); + + /** + * Returns true if the set contains the item \a value. + */ + bool contains(T value) const; + + /** + * Removes an item. Returns true if the item was present and false if not. + */ + bool remove(T value); + + QValueList<T> values() const; + + int hash(T key) const; + + static inline int tableSize() { return m_tableSize; } + +protected: + + struct Node + { + Node(T value) : key(value), next(0) {} + T key; + Node *next; + }; + +public: + + class Iterator + { + friend class Hash<T>; + public: + Iterator(const Hash<T> &hash) : m_hash(hash), m_index(0), m_node(hash.m_table[0]) {} + const T &operator*() const { return m_node->key; } + T &operator*() { return m_node->key; } + + bool operator==(const Iterator &it) const { return m_index == it.m_index && m_node == it.m_node; } + bool operator!=(const Iterator &it) const { return !(it == *this); } + + T &operator++(); + + private: + const Hash<T> &m_hash; + int m_index; + Node *m_node; + }; + + Iterator begin() const + { + Iterator it(*this); + while(!it.m_node && it.m_index < m_tableSize - 1) { + it.m_index++; + it.m_node = m_table[it.m_index]; + } + + return it; + } + + Iterator end() const + { + Iterator it(*this); + it.m_node = 0; + it.m_index = m_tableSize - 1; + return it; + } + +protected: + + void deleteNode(Node *n); + + QPtrVector<Node> m_table; + static const int m_tableSize = 5003; +}; + +//////////////////////////////////////////////////////////////////////////////// +// helper functions +//////////////////////////////////////////////////////////////////////////////// + +inline char hashStringAccess(const QString &in, int index) +{ + return in.unicode()[index].cell(); +} + +inline char hashStringAccess(const QCString &in, int index) +{ + return in[index]; +} + +// Based on QGDict's hash functions, Copyright (C) 1992-2000 Trolltech AS + +template <typename StringType> +inline int hashString(const StringType &s) +{ + uint h = 0; + uint g; + for(uint i = 0; i < s.length(); i++) + { + h = (h << 4) + hashStringAccess(s, i); + if((g = h & 0xf0000000)) + h ^= g >> 24; + h &= ~g; + } + int index = h; + if(index < 0) + index = -index; + return index; +} + +//////////////////////////////////////////////////////////////////////////////// +// specializations +//////////////////////////////////////////////////////////////////////////////// + +// StringHash + +template<> inline int Hash<QString>::hash(QString key) const +{ + return hashString(key) % tableSize(); +} +typedef Hash<QString> StringHash; + +// PtrHash + +template<> inline int Hash<void *>::hash(void *key) const +{ + return long(key) % tableSize(); +} +typedef Hash<void *> PtrHash; + +// FileHandleHash + +template<> inline int Hash<FileHandle>::hash(FileHandle key) const +{ + return hashString(key.absFilePath()) % tableSize(); +} + +class FileHandleHash : public Hash<FileHandle> +{ +public: + FileHandleHash() : Hash<FileHandle>() {} + + FileHandle value(const QString &key) const + { + int h = hashString(key) % tableSize(); + Node *i = m_table[h]; + + while(i && i->key.absFilePath() != key) + i = i->next; + + return i ? i->key : FileHandle::null(); + } +}; + + +//////////////////////////////////////////////////////////////////////////////// +// template method implementations +//////////////////////////////////////////////////////////////////////////////// + +template <class T> +Hash<T>::~Hash() +{ + for(int i = 0; i < m_tableSize; i++) + deleteNode(m_table[i]); +} + +template <class T> +bool Hash<T>::insert(T value) +{ + int h = hash(value); + Node *i = m_table[h]; + Node *j = 0; + + while(i) { + if(i->key == value) + return true; + else { + j = i; + i = i->next; + } + } + + if(j) + j->next = new Node(value); + else + m_table.insert(h, new Node(value)); + + return false; +} + +template <class T> +bool Hash<T>::contains(T value) const +{ + int h = hash(value); + Node *i = m_table[h]; + + while(i && i->key != value) + i = i->next; + + return bool(i); +} + +template <class T> +bool Hash<T>::remove(T value) +{ + int h = hash(value); + Node *previous = 0; + Node *i = m_table[h]; + + while(i && i->key != value) { + previous = i; + i = i->next; + } + + if(!i) + return false; + + if(previous) + previous->next = i->next; + else { + if(i->next) + m_table.insert(h, i->next); + else + m_table.remove(h); + } + + delete i; + + return true; +} + +template <class T> +QValueList<T> Hash<T>::values() const +{ + QValueList<T> l; + + Node *n; + + for(int i = 0; i < tableSize(); i++) { + n = m_table[i]; + while(n) { + l.append(n->key); + n = n->next; + } + } + + return l; +} + +template <class T> +void Hash<T>::deleteNode(Node *n) +{ + if(n) { + deleteNode(n->next); + delete n; + } +} + +template <class T> +T &Hash<T>::Iterator::operator++() +{ + if(m_node) + m_node = m_node->next; + + while(!m_node && m_index < m_tableSize - 1) { + m_index++; + m_node = m_hash.m_table[m_index]; + } + + return m_node->key; +} + + +#endif diff --git a/juk/stringshare.cpp b/juk/stringshare.cpp new file mode 100644 index 00000000..ddd6a1ba --- /dev/null +++ b/juk/stringshare.cpp @@ -0,0 +1,73 @@ +/*************************************************************************** + begin : Sat Oct 25 2003 + copyright : (C) 2003 by Maksim Orlovich + email : maksim.orlovich@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#include "stringshare.h" +#include "stringhash.h" + +const int SIZE = 5003; + +StringShare::Data* StringShare::s_data = 0; + +/** + * We store the strings in two simple direct-mapped (i.e. no collision handling, + * just replace) hashes, which contain strings or null objects. This costs only + * 4 bytes per slot on 32-bit archs, so with the default constant size we only + * really use 40K or so. + * + * The end result is that many strings end up pointing to the same underlying data + * object, instead of each one having its own little copy. + */ + +struct StringShare::Data +{ + QString qstringHash [SIZE]; + QCString qcstringHash[SIZE]; +}; + +StringShare::Data* StringShare::data() +{ + if (!s_data) + s_data = new Data; + return s_data; +} + +QString StringShare::tryShare(const QString& in) +{ + int index = hashString(in) % SIZE; + + Data* dat = data(); + if (dat->qstringHash[index] == in) //Match + return dat->qstringHash[index]; + else + { + //Else replace whatever was there before + dat->qstringHash[index] = in; + return in; + } +} + +QCString StringShare::tryShare(const QCString& in) +{ + int index = hashString(in) % SIZE; + + Data* dat = data(); + if (dat->qcstringHash[index] == in) //Match + return dat->qcstringHash[index]; + else + { + //Else replace whatever was there before + dat->qcstringHash[index] = in; + return in; + } +} diff --git a/juk/stringshare.h b/juk/stringshare.h new file mode 100644 index 00000000..743b2c1e --- /dev/null +++ b/juk/stringshare.h @@ -0,0 +1,37 @@ +/*************************************************************************** + begin : Sat Oct 25 2003 + copyright : (C) 2003 by Maksim Orlovich + email : maksim.orlovich@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef STRING_SHARE_H +#define STRING_SHARE_H + +#include <qstring.h> + +/** + This class attempts to normalize repeated occurances of strings to use + the same shared object, if possible, by using a small hash +*/ +class StringShare +{ + struct Data; +public: + static QString tryShare(const QString& in); + static QCString tryShare(const QCString& in); + +private: + static Data* data(); + static Data* s_data; +}; + +#endif diff --git a/juk/systemtray.cpp b/juk/systemtray.cpp new file mode 100644 index 00000000..1691bc91 --- /dev/null +++ b/juk/systemtray.cpp @@ -0,0 +1,639 @@ +/*************************************************************************** + copyright : (C) 2002 by Daniel Molkentin + email : molkentin@kde.org + + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <klocale.h> +#include <kiconloader.h> +#include <kpassivepopup.h> +#include <kiconeffect.h> +#include <kaction.h> +#include <kpopupmenu.h> +#include <kglobalsettings.h> +#include <kdebug.h> + +#include <qvbox.h> +#include <qtimer.h> +#include <qcolor.h> +#include <qpushbutton.h> +#include <qtooltip.h> +#include <qpainter.h> +#include <qvaluevector.h> +#include <qstylesheet.h> +#include <qpalette.h> + +#include <netwm.h> + +#include "tag.h" +#include "systemtray.h" +#include "actioncollection.h" +#include "playermanager.h" +#include "collectionlist.h" +#include "coverinfo.h" + +using namespace ActionCollection; + +static bool copyImage(QImage &dest, QImage &src, int x, int y); + +class FlickerFreeLabel : public QLabel +{ +public: + FlickerFreeLabel(const QString &text, QWidget *parent, const char *name = 0) : + QLabel(text, parent, name) + { + m_textColor = paletteForegroundColor(); + m_bgColor = parentWidget()->paletteBackgroundColor(); + setBackgroundMode(NoBackground); + } + + QColor textColor() const + { + return m_textColor; + } + + QColor backgroundColor() const + { + return m_bgColor; + } + +protected: + virtual void drawContents(QPainter *p) + { + // We want to intercept the drawContents call and draw on a pixmap + // instead of the window to keep flicker to an absolute minimum. + // Since Qt doesn't refresh the background, we need to do so + // ourselves. + + QPixmap pix(size()); + QPainter pixPainter(&pix); + + pixPainter.fillRect(rect(), m_bgColor); + QLabel::drawContents(&pixPainter); + + bitBlt(p->device(), QPoint(0, 0), &pix, rect(), CopyROP); + } + + private: + QColor m_textColor; + QColor m_bgColor; +}; + +PassiveInfo::PassiveInfo(QWidget *parent, const char *name) : + KPassivePopup(parent, name), m_timer(new QTimer), m_justDie(false) +{ + // I'm so sick and tired of KPassivePopup screwing this up + // that I'll just handle the timeout myself, thank you very much. + KPassivePopup::setTimeout(0); + + connect(m_timer, SIGNAL(timeout()), SLOT(timerExpired())); +} + +void PassiveInfo::setTimeout(int delay) +{ + m_timer->changeInterval(delay); +} + +void PassiveInfo::show() +{ + KPassivePopup::show(); + m_timer->start(3500); +} + +void PassiveInfo::timerExpired() +{ + // If m_justDie is set, we should just go, otherwise we should emit the + // signal and wait for the system tray to delete us. + if(m_justDie) + hide(); + else + emit timeExpired(); +} + +void PassiveInfo::enterEvent(QEvent *) +{ + m_timer->stop(); + emit mouseEntered(); +} + +void PassiveInfo::leaveEvent(QEvent *) +{ + m_justDie = true; + m_timer->start(50); +} + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +SystemTray::SystemTray(QWidget *parent, const char *name) : KSystemTray(parent, name), + m_popup(0), + m_fadeTimer(0), + m_fade(true) + +{ + // This should be initialized to the number of labels that are used. + + m_labels.reserve(3); + + m_appPix = loadIcon("juk_dock"); + + m_playPix = createPixmap("player_play"); + m_pausePix = createPixmap("player_pause"); + + m_forwardPix = loadIcon("player_end"); + m_backPix = loadIcon("player_start"); + + setPixmap(m_appPix); + + setToolTip(); + + // Just create this here so that it show up in the DCOP interface and the key + // bindings dialog. + + new KAction(i18n("Redisplay Popup"), KShortcut(), this, + SLOT(slotPlay()), actions(), "showPopup"); + + KPopupMenu *cm = contextMenu(); + + connect(PlayerManager::instance(), SIGNAL(signalPlay()), this, SLOT(slotPlay())); + connect(PlayerManager::instance(), SIGNAL(signalPause()), this, SLOT(slotPause())); + connect(PlayerManager::instance(), SIGNAL(signalStop()), this, SLOT(slotStop())); + + action("play")->plug(cm); + action("pause")->plug(cm); + action("stop")->plug(cm); + action("forward")->plug(cm); + action("back")->plug(cm); + + cm->insertSeparator(); + + // Pity the actionCollection doesn't keep track of what sub-menus it has. + + KActionMenu *menu = new KActionMenu(i18n("&Random Play"), this); + menu->insert(action("disableRandomPlay")); + menu->insert(action("randomPlay")); + menu->insert(action("albumRandomPlay")); + menu->plug(cm); + + action("togglePopups")->plug(cm); + + m_fadeTimer = new QTimer(this, "systrayFadeTimer"); + connect(m_fadeTimer, SIGNAL(timeout()), SLOT(slotNextStep())); + + if(PlayerManager::instance()->playing()) + slotPlay(); + else if(PlayerManager::instance()->paused()) + slotPause(); +} + +SystemTray::~SystemTray() +{ + +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void SystemTray::slotPlay() +{ + if(!PlayerManager::instance()->playing()) + return; + + QPixmap cover = PlayerManager::instance()->playingFile().coverInfo()->pixmap(CoverInfo::Thumbnail); + + setPixmap(m_playPix); + setToolTip(PlayerManager::instance()->playingString(), cover); + createPopup(); +} + +void SystemTray::slotTogglePopup() +{ + if(m_popup && m_popup->view()->isVisible()) + m_popup->setTimeout(50); + else + slotPlay(); +} + +void SystemTray::slotPopupLargeCover() +{ + if(!PlayerManager::instance()->playing()) + return; + + FileHandle playingFile = PlayerManager::instance()->playingFile(); + playingFile.coverInfo()->popup(); +} + +void SystemTray::slotStop() +{ + setPixmap(m_appPix); + setToolTip(); + + delete m_popup; + m_popup = 0; +} + +void SystemTray::slotPopupDestroyed() +{ + for(unsigned i = 0; i < m_labels.capacity(); ++i) + m_labels[i] = 0; +} + +void SystemTray::slotNextStep() +{ + QColor result; + + ++m_step; + + // If we're not fading, immediately show the labels + if(!m_fade) + m_step = STEPS; + + result = interpolateColor(m_step); + + for(unsigned i = 0; i < m_labels.capacity() && m_labels[i]; ++i) + m_labels[i]->setPaletteForegroundColor(result); + + if(m_step == STEPS) { + m_step = 0; + m_fadeTimer->stop(); + emit fadeDone(); + } +} + +void SystemTray::slotFadeOut() +{ + m_startColor = m_labels[0]->textColor(); + m_endColor = m_labels[0]->backgroundColor(); + + connect(this, SIGNAL(fadeDone()), m_popup, SLOT(hide())); + connect(m_popup, SIGNAL(mouseEntered()), this, SLOT(slotMouseInPopup())); + m_fadeTimer->start(1500 / STEPS); +} + +// If we receive this signal, it's because we were called during fade out. +// That means there is a single shot timer about to call slotNextStep, so we +// don't have to do it ourselves. +void SystemTray::slotMouseInPopup() +{ + m_endColor = m_labels[0]->textColor(); + disconnect(SIGNAL(fadeDone())); + + m_step = STEPS - 1; // Simulate end of fade to solid text + slotNextStep(); +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +QVBox *SystemTray::createPopupLayout(QWidget *parent, const FileHandle &file) +{ + QVBox *infoBox = 0; + + if(buttonsToLeft()) { + + // They go to the left because JuK is on that side + + createButtonBox(parent); + addSeparatorLine(parent); + + infoBox = new QVBox(parent); + + // Another line, and the cover, if there's a cover, and if + // it's selected to be shown + + if(file.coverInfo()->hasCover()) { + addSeparatorLine(parent); + addCoverButton(parent, file.coverInfo()->pixmap(CoverInfo::Thumbnail)); + } + } + else { + + // Like above, but reversed. + + if(file.coverInfo()->hasCover()) { + addCoverButton(parent, file.coverInfo()->pixmap(CoverInfo::Thumbnail)); + addSeparatorLine(parent); + } + + infoBox = new QVBox(parent); + + addSeparatorLine(parent); + createButtonBox(parent); + } + + infoBox->setSpacing(3); + infoBox->setMargin(3); + return infoBox; +} + +void SystemTray::createPopup() +{ + FileHandle playingFile = PlayerManager::instance()->playingFile(); + Tag *playingInfo = playingFile.tag(); + + // If the action exists and it's checked, do our stuff + + if(!action<KToggleAction>("togglePopups")->isChecked()) + return; + + delete m_popup; + m_popup = 0; + m_fadeTimer->stop(); + + // This will be reset after this function call by slot(Forward|Back) + // so it's safe to set it true here. + m_fade = true; + m_step = 0; + + m_popup = new PassiveInfo(this); + connect(m_popup, SIGNAL(destroyed()), SLOT(slotPopupDestroyed())); + connect(m_popup, SIGNAL(timeExpired()), SLOT(slotFadeOut())); + + QHBox *box = new QHBox(m_popup, "popupMainLayout"); + box->setSpacing(15); // Add space between text and buttons + + QVBox *infoBox = createPopupLayout(box, playingFile); + + for(unsigned i = 0; i < m_labels.capacity(); ++i) { + m_labels[i] = new FlickerFreeLabel(" ", infoBox); + m_labels[i]->setAlignment(AlignRight | AlignVCenter); + } + + // We don't want an autodelete popup. There are times when it will need + // to be hidden before the timeout. + + m_popup->setAutoDelete(false); + + // We have to set the text of the labels after all of the + // widgets have been added in order for the width to be calculated + // correctly. + + int labelCount = 0; + + QString title = QStyleSheet::escape(playingInfo->title()); + m_labels[labelCount++]->setText(QString("<qt><nobr><h2>%1</h2></nobr><qt>").arg(title)); + + if(!playingInfo->artist().isEmpty()) + m_labels[labelCount++]->setText(playingInfo->artist()); + + if(!playingInfo->album().isEmpty()) { + QString album = QStyleSheet::escape(playingInfo->album()); + QString s = playingInfo->year() > 0 + ? QString("<qt><nobr>%1 (%2)</nobr></qt>").arg(album).arg(playingInfo->year()) + : QString("<qt><nobr>%1</nobr></qt>").arg(album); + m_labels[labelCount++]->setText(s); + } + + m_startColor = m_labels[0]->backgroundColor(); + m_endColor = m_labels[0]->textColor(); + + slotNextStep(); + m_fadeTimer->start(1500 / STEPS); + + m_popup->setView(box); + m_popup->show(); +} + +bool SystemTray::buttonsToLeft() const +{ + // The following code was nicked from kpassivepopup.cpp + + NETWinInfo ni(qt_xdisplay(), winId(), qt_xrootwin(), + NET::WMIconGeometry | NET::WMKDESystemTrayWinFor); + NETRect frame, win; + ni.kdeGeometry(frame, win); + + QRect bounds = KGlobalSettings::desktopGeometry(QPoint(win.pos.x, win.pos.y)); + + // This seems to accurately guess what side of the icon that + // KPassivePopup will popup on. + return(win.pos.x < bounds.center().x()); +} + +QPixmap SystemTray::createPixmap(const QString &pixName) +{ + QPixmap bgPix = m_appPix; + QPixmap fgPix = SmallIcon(pixName); + + QImage bgImage = bgPix.convertToImage(); // Probably 22x22 + QImage fgImage = fgPix.convertToImage(); // Should be 16x16 + + KIconEffect::semiTransparent(bgImage); + copyImage(bgImage, fgImage, (bgImage.width() - fgImage.width()) / 2, + (bgImage.height() - fgImage.height()) / 2); + + bgPix.convertFromImage(bgImage); + return bgPix; +} + +void SystemTray::createButtonBox(QWidget *parent) +{ + QVBox *buttonBox = new QVBox(parent); + + buttonBox->setSpacing(3); + + QPushButton *forwardButton = new QPushButton(m_forwardPix, 0, buttonBox, "popup_forward"); + forwardButton->setFlat(true); + connect(forwardButton, SIGNAL(clicked()), SLOT(slotForward())); + + QPushButton *backButton = new QPushButton(m_backPix, 0, buttonBox, "popup_back"); + backButton->setFlat(true); + connect(backButton, SIGNAL(clicked()), SLOT(slotBack())); +} + +/** + * What happens here is that the action->activate() call will end up invoking + * createPopup(), which sets m_fade to true. Before the text starts fading + * control returns to this function, which resets m_fade to false. + */ +void SystemTray::slotBack() +{ + action("back")->activate(); + m_fade = false; +} + +void SystemTray::slotForward() +{ + action("forward")->activate(); + m_fade = false; +} + +void SystemTray::addSeparatorLine(QWidget *parent) +{ + QFrame *line = new QFrame(parent); + line->setFrameShape(QFrame::VLine); + + // Cover art takes up 80 pixels, make sure we take up at least 80 pixels + // even if we don't show the cover art for consistency. + + line->setMinimumHeight(80); +} + +void SystemTray::addCoverButton(QWidget *parent, const QPixmap &cover) +{ + QPushButton *coverButton = new QPushButton(parent); + + coverButton->setPixmap(cover); + coverButton->setFixedSize(cover.size()); + coverButton->setFlat(true); + + connect(coverButton, SIGNAL(clicked()), this, SLOT(slotPopupLargeCover())); +} + +QColor SystemTray::interpolateColor(int step, int steps) +{ + if(step < 0) + return m_startColor; + if(step >= steps) + return m_endColor; + + // TODO: Perhaps the algorithm here could be better? For example, it might + // make sense to go rather quickly from start to end and then slow down + // the progression. + return QColor( + (step * m_endColor.red() + (steps - step) * m_startColor.red()) / steps, + (step * m_endColor.green() + (steps - step) * m_startColor.green()) / steps, + (step * m_endColor.blue() + (steps - step) * m_startColor.blue()) / steps + ); +} + +void SystemTray::setToolTip(const QString &tip, const QPixmap &cover) +{ + QToolTip::remove(this); + + if(tip.isNull()) + QToolTip::add(this, i18n("JuK")); + else { + QPixmap myCover = cover; + if(cover.isNull()) + myCover = DesktopIcon("juk"); + + QImage coverImage = myCover.convertToImage(); + if(coverImage.size().width() > 32 || coverImage.size().height() > 32) + coverImage = coverImage.smoothScale(32, 32); + + QMimeSourceFactory::defaultFactory()->setImage("tipCover", coverImage); + + QString html = i18n("%1 is Cover Art, %2 is the playing track, %3 is the appname", + "<center><table cellspacing=\"2\"><tr><td valign=\"middle\">%1</td>" + "<td valign=\"middle\">%2</td></tr></table><em>%3</em></center>"); + html = html.arg("<img valign=\"middle\" src=\"tipCover\""); + html = html.arg(QString("<nobr>%1</nobr>").arg(tip), i18n("JuK")); + + QToolTip::add(this, html); + } +} + +void SystemTray::wheelEvent(QWheelEvent *e) +{ + if(e->orientation() == Horizontal) + return; + + // I already know the type here, but this file doesn't (and I don't want it + // to) know about the JuK class, so a static_cast won't work, and I was told + // that a reinterpret_cast isn't portable when combined with multiple + // inheritance. (This is why I don't check the result.) + + switch(e->state()) { + case ShiftButton: + if(e->delta() > 0) + action("volumeUp")->activate(); + else + action("volumeDown")->activate(); + break; + default: + if(e->delta() > 0) + action("forward")->activate(); + else + action("back")->activate(); + break; + } + e->accept(); +} + +/* + * Reimplemented this in order to use the middle mouse button + */ +void SystemTray::mousePressEvent(QMouseEvent *e) +{ + switch(e->button()) { + case LeftButton: + case RightButton: + default: + KSystemTray::mousePressEvent(e); + break; + case MidButton: + if(!rect().contains(e->pos())) + return; + if(action("pause")->isEnabled()) + action("pause")->activate(); + else + action("play")->activate(); + break; + } +} + +/* + * This function copies the entirety of src into dest, starting in + * dest at x and y. This function exists because I was unable to find + * a function like it in either QImage or kdefx + */ + +static bool copyImage(QImage &dest, QImage &src, int x, int y) +{ + if(dest.depth() != src.depth()) + return false; + if((x + src.width()) >= dest.width()) + return false; + if((y + src.height()) >= dest.height()) + return false; + + // We want to use KIconEffect::overlay to do this, since it handles + // alpha, but the images need to be the same size. We can handle that. + + QImage large_src(dest); + + // It would perhaps be better to create large_src based on a size, but + // this is the easiest way to make a new image with the same depth, size, + // etc. + + large_src.detach(); + + // However, we do have to specifically ensure that setAlphaBuffer is set + // to false + + large_src.setAlphaBuffer(false); + large_src.fill(0); // All transparent pixels + large_src.setAlphaBuffer(true); + + int w = src.width(); + int h = src.height(); + for(int dx = 0; dx < w; dx++) + for(int dy = 0; dy < h; dy++) + large_src.setPixel(dx + x, dy + y, src.pixel(dx, dy)); + + // Apply effect to image + + KIconEffect::overlay(dest, large_src); + + return true; +} + + +#include "systemtray.moc" + +// vim: et sw=4 ts=8 diff --git a/juk/systemtray.h b/juk/systemtray.h new file mode 100644 index 00000000..8dd66a62 --- /dev/null +++ b/juk/systemtray.h @@ -0,0 +1,133 @@ +/*************************************************************************** + copyright : (C) 2002 by Daniel Molkentin + email : molkentin@kde.org + + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef SYSTEMTRAY_H +#define SYSTEMTRAY_H + +#include <ksystemtray.h> +#include <kpassivepopup.h> + +#include <qvaluevector.h> +#include <qcolor.h> + +class FlickerFreeLabel; +class QTimer; +class QVBox; +class FileHandle; + +/** + * Subclass of KPassivePopup intended to more easily support JuK's particular + * usage pattern, including things like staying open while under the mouse. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class PassiveInfo : public KPassivePopup +{ + Q_OBJECT +public: + PassiveInfo(QWidget *parent = 0, const char *name = 0); + +public slots: + void setTimeout(int delay); + virtual void show(); + +signals: + void mouseEntered(); + void timeExpired(); + +protected: + virtual void enterEvent(QEvent *); + virtual void leaveEvent(QEvent *); + +private slots: + void timerExpired(); + +private: + QTimer *m_timer; + bool m_justDie; +}; + +class SystemTray : public KSystemTray +{ + Q_OBJECT + +public: + SystemTray(QWidget *parent = 0, const char *name = 0); + virtual ~SystemTray(); + +signals: + // Emitted when the fade process is complete. + void fadeDone(); + +private: + static const int STEPS = 20; ///< Number of intermediate steps for fading. + + virtual void wheelEvent(QWheelEvent *e); + void createPopup(); + void setToolTip(const QString &tip = QString::null, const QPixmap &cover = QPixmap()); + void mousePressEvent(QMouseEvent *e); + QPixmap createPixmap(const QString &pixName); + + // Returns true if the popup will need to have its buttons on the left + // (because the JuK icon is on the left side of the screen. + bool buttonsToLeft() const; + + void createButtonBox(QWidget *parent); + + // Creates the widget layout for the popup, returning the QVBox that + // holds the text labels. Uses buttonsToLeft() to figure out which + // order to create them in. @p file is used to grab the cover. + QVBox *createPopupLayout(QWidget *parent, const FileHandle &file); + + void addSeparatorLine(QWidget *parent); + void addCoverButton(QWidget *parent, const QPixmap &cover); + + // Interpolates from start color to end color. If @p step == 0, then + // m_startColor is returned, while @p step == @steps returns + // m_endColor. + QColor interpolateColor(int step, int steps = STEPS); + +private slots: + void slotPlay(); + void slotTogglePopup(); + void slotPause() { setPixmap(m_pausePix); } + void slotStop(); + void slotPopupDestroyed(); + void slotNextStep(); ///< This is the fading routine. + void slotPopupLargeCover(); + void slotForward(); + void slotBack(); + void slotFadeOut(); ///< Fades out the text + void slotMouseInPopup(); ///< Forces the text back to its normal color. + +private: + QPixmap m_playPix; + QPixmap m_pausePix; + QPixmap m_currentPix; + QPixmap m_backPix; + QPixmap m_forwardPix; + QPixmap m_appPix; + QColor m_startColor, m_endColor; + + PassiveInfo *m_popup; + QValueVector<FlickerFreeLabel *> m_labels; + QTimer *m_fadeTimer; + int m_step; + bool m_fade; +}; + +#endif // SYSTEMTRAY_H diff --git a/juk/tag.cpp b/juk/tag.cpp new file mode 100644 index 00000000..972b9284 --- /dev/null +++ b/juk/tag.cpp @@ -0,0 +1,279 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <kdebug.h> + +#include <qregexp.h> +#include <qfile.h> + +#include <taglib/tag.h> +#include <taglib/mpegfile.h> +#include <taglib/vorbisfile.h> +#include <taglib/flacfile.h> +#include <taglib/xiphcomment.h> +#include <taglib/id3v2framefactory.h> + +#if (TAGLIB_MAJOR_VERSION > 1) || \ + ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 2)) +#include <taglib/oggflacfile.h> +#define TAGLIB_1_2 +#endif +#if (TAGLIB_MAJOR_VERSION > 1) || \ + ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 3)) +#include <taglib/mpcfile.h> +#define TAGLIB_1_3 +#endif + +#include "cache.h" +#include "tag.h" +#include "mediafiles.h" +#include "stringshare.h" + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + + +Tag::Tag(const QString &fileName) : + m_fileName(fileName), + m_track(0), + m_year(0), + m_seconds(0), + m_bitrate(0), + m_isValid(false) +{ + // using qDebug here since we want this to show up in non-debug builds as well + + qDebug("Reading tag for %s", fileName.local8Bit().data()); + + if(MediaFiles::isMP3(fileName)) { + TagLib::MPEG::File file(QFile::encodeName(fileName).data()); + if(file.isValid()) + setup(&file); + } + + else if(MediaFiles::isFLAC(fileName)) { + TagLib::FLAC::File file(QFile::encodeName(fileName).data()); + if(file.isValid()) + setup(&file); + } +#ifdef TAGLIB_1_3 + else if(MediaFiles::isMPC(fileName)) { + kdDebug(65432) << "Trying to resolve Musepack file" << endl; + TagLib::MPC::File file(QFile::encodeName(fileName).data()); + if(file.isValid()) + setup(&file); + } +#endif +#ifdef TAGLIB_1_2 + else if(MediaFiles::isOggFLAC(fileName)) { + TagLib::Ogg::FLAC::File file(QFile::encodeName(fileName).data()); + if(file.isValid()) + setup(&file); + } +#endif + else if(MediaFiles::isVorbis(fileName)) { + TagLib::Vorbis::File file(QFile::encodeName(fileName).data()); + if(file.isValid()) + setup(&file); + } + + else { + kdError(65432) << "Couldn't resolve the mime type of \"" << + fileName << "\" -- this shouldn't happen." << endl; + } +} + +bool Tag::save() +{ + bool result; + TagLib::ID3v2::FrameFactory::instance()->setDefaultTextEncoding(TagLib::String::UTF8); + + TagLib::File *file = 0; + + if(MediaFiles::isMP3(m_fileName)) + file = new TagLib::MPEG::File(QFile::encodeName(m_fileName).data()); + else if(MediaFiles::isFLAC(m_fileName)) + file = new TagLib::FLAC::File(QFile::encodeName(m_fileName).data()); +#ifdef TAGLIB_1_3 + else if(MediaFiles::isMPC(m_fileName)) + file = new TagLib::MPC::File(QFile::encodeName(m_fileName).data()); +#endif +#ifdef TAGLIB_1_2 + else if(MediaFiles::isOggFLAC(m_fileName)) + file = new TagLib::Ogg::FLAC::File(QFile::encodeName(m_fileName).data()); +#endif + else if(MediaFiles::isVorbis(m_fileName)) + file = new TagLib::Vorbis::File(QFile::encodeName(m_fileName).data()); + + if(file && file->isValid() && file->tag() && !file->readOnly()) { + file->tag()->setTitle(QStringToTString(m_title)); + file->tag()->setArtist(QStringToTString(m_artist)); + file->tag()->setAlbum(QStringToTString(m_album)); + file->tag()->setGenre(QStringToTString(m_genre)); + file->tag()->setComment(QStringToTString(m_comment)); + file->tag()->setTrack(m_track); + file->tag()->setYear(m_year); +#ifdef TAGLIB_1_2 + result = file->save(); +#else + file->save(); + result = true; +#endif + } + else { + kdError(65432) << "Couldn't save file." << endl; + result = false; + } + + delete file; + return result; +} + +CacheDataStream &Tag::read(CacheDataStream &s) +{ + switch(s.cacheVersion()) { + case 1: { + Q_INT32 track; + Q_INT32 year; + Q_INT32 bitrate; + Q_INT32 seconds; + + s >> m_title + >> m_artist + >> m_album + >> m_genre + >> track + >> year + >> m_comment + >> bitrate + >> m_lengthString + >> seconds; + + m_track = track; + m_year = year; + m_bitrate = bitrate; + m_seconds = seconds; + break; + } + default: { + static QString dummyString; + static int dummyInt; + QString bitrateString; + + s >> dummyInt + >> m_title + >> m_artist + >> m_album + >> m_genre + >> dummyInt + >> m_track + >> dummyString + >> m_year + >> dummyString + >> m_comment + >> bitrateString + >> m_lengthString + >> m_seconds + >> dummyString; + + bool ok; + m_bitrate = bitrateString.toInt(&ok); + if(!ok) + m_bitrate = 0; + break; + } + } + + // Try to reduce memory usage: share tags that frequently repeat, squeeze others + + m_title.squeeze(); + m_lengthString.squeeze(); + + m_comment = StringShare::tryShare(m_comment); + m_artist = StringShare::tryShare(m_artist); + m_album = StringShare::tryShare(m_album); + m_genre = StringShare::tryShare(m_genre); + + return s; +} + +//////////////////////////////////////////////////////////////////////////////// +// private methods +//////////////////////////////////////////////////////////////////////////////// + +Tag::Tag(const QString &fileName, bool) : + m_fileName(fileName), + m_track(0), + m_year(0), + m_seconds(0), + m_bitrate(0), + m_isValid(true) +{ + +} + +void Tag::setup(TagLib::File *file) +{ + m_title = TStringToQString(file->tag()->title()).stripWhiteSpace(); + m_artist = TStringToQString(file->tag()->artist()).stripWhiteSpace(); + m_album = TStringToQString(file->tag()->album()).stripWhiteSpace(); + m_genre = TStringToQString(file->tag()->genre()).stripWhiteSpace(); + m_comment = TStringToQString(file->tag()->comment()).stripWhiteSpace(); + + m_track = file->tag()->track(); + m_year = file->tag()->year(); + + m_seconds = file->audioProperties()->length(); + m_bitrate = file->audioProperties()->bitrate(); + + const int seconds = m_seconds % 60; + const int minutes = (m_seconds - seconds) / 60; + + m_lengthString = QString::number(minutes) + (seconds >= 10 ? ":" : ":0") + QString::number(seconds); + + if(m_title.isEmpty()) { + int i = m_fileName.findRev('/'); + int j = m_fileName.findRev('.'); + m_title = i > 0 ? m_fileName.mid(i + 1, j - i - 1) : m_fileName; + } + + m_isValid = true; +} + +//////////////////////////////////////////////////////////////////////////////// +// related functions +//////////////////////////////////////////////////////////////////////////////// + +QDataStream &operator<<(QDataStream &s, const Tag &t) +{ + s << t.title() + << t.artist() + << t.album() + << t.genre() + << Q_INT32(t.track()) + << Q_INT32(t.year()) + << t.comment() + << Q_INT32(t.bitrate()) + << t.lengthString() + << Q_INT32(t.seconds()); + + return s; +} + +CacheDataStream &operator>>(CacheDataStream &s, Tag &t) +{ + return t.read(s); +} diff --git a/juk/tag.h b/juk/tag.h new file mode 100644 index 00000000..66c01d1e --- /dev/null +++ b/juk/tag.h @@ -0,0 +1,95 @@ +/*************************************************************************** + begin : Sun Feb 17 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef TAG_H +#define TAG_H + +#include <qfileinfo.h> + +namespace TagLib { class File; } + +class CacheDataStream; + +/*! + * This should really be called "metadata" and may at some point be titled as + * such. Right now it's mostly a Qt wrapper around TagLib. + */ + +class Tag +{ + friend class FileHandle; +public: + Tag(const QString &fileName); + /** + * Create an empty tag. Used in FileHandle for cache restoration. + */ + Tag(const QString &fileName, bool); + + bool save(); + + QString title() const { return m_title; } + QString artist() const { return m_artist; } + QString album() const { return m_album; } + QString genre() const { return m_genre; } + int track() const { return m_track; } + int year() const { return m_year; } + QString comment() const { return m_comment; } + + QString fileName() const { return m_fileName; } + + void setTitle(const QString &value) { m_title = value; } + void setArtist(const QString &value) { m_artist = value; } + void setAlbum(const QString &value) { m_album = value; } + void setGenre(const QString &value) { m_genre = value; } + void setTrack(int value) { m_track = value; } + void setYear(int value) { m_year = value; } + void setComment(const QString &value) { m_comment = value; } + + void setFileName(const QString &value) { m_fileName = value; } + + int seconds() const { return m_seconds; } + int bitrate() const { return m_bitrate; } + + bool isValid() const { return m_isValid; } + + /** + * As a convenience, since producing a length string from a number of second + * isn't a one liner, provide the lenght in string form. + */ + QString lengthString() const { return m_lengthString; } + CacheDataStream &read(CacheDataStream &s); + +private: + void setup(TagLib::File *file); + + QString m_fileName; + QString m_title; + QString m_artist; + QString m_album; + QString m_genre; + QString m_comment; + int m_track; + int m_year; + int m_seconds; + int m_bitrate; + QDateTime m_modificationTime; + QString m_lengthString; + bool m_isValid; +}; + +QDataStream &operator<<(QDataStream &s, const Tag &t); +CacheDataStream &operator>>(CacheDataStream &s, Tag &t); + +#endif diff --git a/juk/tageditor.cpp b/juk/tageditor.cpp new file mode 100644 index 00000000..a9bc764d --- /dev/null +++ b/juk/tageditor.cpp @@ -0,0 +1,800 @@ +/*************************************************************************** + begin : Sat Sep 7 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include "tageditor.h" +#include "collectionlist.h" +#include "playlistitem.h" +#include "tag.h" +#include "actioncollection.h" +#include "tagtransactionmanager.h" + +#include <kcombobox.h> +#include <klineedit.h> +#include <knuminput.h> +#include <keditcl.h> +#include <kmessagebox.h> +#include <kconfig.h> +#include <klocale.h> +#include <kdebug.h> +#include <kiconloader.h> +#include <kactionclasses.h> + +#include <qlabel.h> +#include <qcheckbox.h> +#include <qlayout.h> +#include <qdir.h> +#include <qvalidator.h> +#include <qtooltip.h> +#include <qeventloop.h> +#include <qdict.h> + +#include <id3v1genres.h> + +#undef KeyRelease + +using namespace ActionCollection; + +class FileNameValidator : public QValidator +{ +public: + FileNameValidator(QObject *parent, const char *name = 0) : + QValidator(parent, name) {} + + virtual void fixup(QString &s) const + { + s.remove('/'); + } + + virtual State validate(QString &s, int &) const + { + if(s.find('/') != -1) + return Invalid; + return Acceptable; + } +}; + +class FileBoxToolTip : public QToolTip +{ +public: + FileBoxToolTip(TagEditor *editor, QWidget *widget) : + QToolTip(widget), m_editor(editor) {} +protected: + virtual void maybeTip(const QPoint &) + { + tip(parentWidget()->rect(), m_editor->items().first()->file().absFilePath()); + } +private: + TagEditor *m_editor; +}; + +class FixedHLayout : public QHBoxLayout +{ +public: + FixedHLayout(QWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) : + QHBoxLayout(parent, margin, spacing, name), + m_width(-1) {} + FixedHLayout(QLayout *parentLayout, int spacing = -1, const char *name = 0) : + QHBoxLayout(parentLayout, spacing, name), + m_width(-1) {} + void setWidth(int w = -1) + { + m_width = w == -1 ? QHBoxLayout::minimumSize().width() : w; + } + virtual QSize minimumSize() const + { + QSize s = QHBoxLayout::minimumSize(); + s.setWidth(m_width); + return s; + } +private: + int m_width; +}; + +class CollectionObserver : public PlaylistObserver +{ +public: + CollectionObserver(TagEditor *parent) : + PlaylistObserver(CollectionList::instance()), + m_parent(parent) + { + } + + virtual void updateData() + { + if(m_parent && m_parent->m_currentPlaylist && m_parent->isVisible()) + m_parent->slotSetItems(m_parent->m_currentPlaylist->selectedItems()); + } + + virtual void updateCurrent() {} + +private: + TagEditor *m_parent; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public members +//////////////////////////////////////////////////////////////////////////////// + +TagEditor::TagEditor(QWidget *parent, const char *name) : + QWidget(parent, name), + m_currentPlaylist(0), + m_observer(0), + m_performingSave(false) +{ + setupActions(); + setupLayout(); + readConfig(); + m_dataChanged = false; + m_collectionChanged = false; +} + +TagEditor::~TagEditor() +{ + delete m_observer; + saveConfig(); +} + +void TagEditor::setupObservers() +{ + m_observer = new CollectionObserver(this); +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void TagEditor::slotSetItems(const PlaylistItemList &list) +{ + if(m_performingSave) + return; + + // Store the playlist that we're setting because saveChangesPrompt + // can delete the PlaylistItems in list. + + Playlist *itemPlaylist = 0; + if(!list.isEmpty()) + itemPlaylist = list.first()->playlist(); + + bool hadPlaylist = m_currentPlaylist != 0; + + saveChangesPrompt(); + + if(m_currentPlaylist) { + disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem *)), + this, SLOT(slotItemRemoved(PlaylistItem *))); + } + + if(hadPlaylist && !m_currentPlaylist || !itemPlaylist) { + m_currentPlaylist = 0; + m_items.clear(); + } + else { + m_currentPlaylist = itemPlaylist; + + // We can't use list here, it may not be valid + + m_items = itemPlaylist->selectedItems(); + } + + if(m_currentPlaylist) { + connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem *)), + this, SLOT(slotItemRemoved(PlaylistItem *))); + connect(m_currentPlaylist, SIGNAL(destroyed()), this, SLOT(slotPlaylistRemoved())); + } + + if(isVisible()) + slotRefresh(); + else + m_collectionChanged = true; +} + +void TagEditor::slotRefresh() +{ + // This method takes the list of currently selected m_items and tries to + // figure out how to show that in the tag editor. The current strategy -- + // the most common case -- is to just process the first item. Then we + // check after that to see if there are other m_items and adjust accordingly. + + if(m_items.isEmpty() || !m_items.first()->file().tag()) { + slotClear(); + setEnabled(false); + return; + } + + setEnabled(true); + + PlaylistItem *item = m_items.first(); + + Q_ASSERT(item); + + Tag *tag = item->file().tag(); + + QFileInfo fi(item->file().absFilePath()); + if(!fi.isWritable() && m_items.count() == 1) + setEnabled(false); + + m_artistNameBox->setEditText(tag->artist()); + m_trackNameBox->setText(tag->title()); + m_albumNameBox->setEditText(tag->album()); + + m_fileNameBox->setText(item->file().fileInfo().fileName()); + new FileBoxToolTip(this, m_fileNameBox); + m_bitrateBox->setText(QString::number(tag->bitrate())); + m_lengthBox->setText(tag->lengthString()); + + if(m_genreList.findIndex(tag->genre()) >= 0) + m_genreBox->setCurrentItem(m_genreList.findIndex(tag->genre()) + 1); + else { + m_genreBox->setCurrentItem(0); + m_genreBox->setEditText(tag->genre()); + } + + m_trackSpin->setValue(tag->track()); + m_yearSpin->setValue(tag->year()); + + m_commentBox->setText(tag->comment()); + + // Start at the second item, since we've already processed the first. + + PlaylistItemList::Iterator it = m_items.begin(); + ++it; + + // If there is more than one item in the m_items that we're dealing with... + + if(it != m_items.end()) { + + QValueListIterator<QWidget *> hideIt = m_hideList.begin(); + for(; hideIt != m_hideList.end(); ++hideIt) + (*hideIt)->hide(); + + BoxMap::Iterator boxIt = m_enableBoxes.begin(); + for(; boxIt != m_enableBoxes.end(); boxIt++) { + (*boxIt)->setChecked(true); + (*boxIt)->show(); + } + + // Yep, this is ugly. Loop through all of the files checking to see + // if their fields are the same. If so, by default, enable their + // checkbox. + + // Also, if there are more than 50 m_items, don't scan all of them. + + if(m_items.count() > 50) { + m_enableBoxes[m_artistNameBox]->setChecked(false); + m_enableBoxes[m_trackNameBox]->setChecked(false); + m_enableBoxes[m_albumNameBox]->setChecked(false); + m_enableBoxes[m_genreBox]->setChecked(false); + m_enableBoxes[m_trackSpin]->setChecked(false); + m_enableBoxes[m_yearSpin]->setChecked(false); + m_enableBoxes[m_commentBox]->setChecked(false); + } + else { + for(; it != m_items.end(); ++it) { + tag = (*it)->file().tag(); + + if(tag) { + + if(m_artistNameBox->currentText() != tag->artist() && + m_enableBoxes.contains(m_artistNameBox)) + { + m_artistNameBox->lineEdit()->clear(); + m_enableBoxes[m_artistNameBox]->setChecked(false); + } + if(m_trackNameBox->text() != tag->title() && + m_enableBoxes.contains(m_trackNameBox)) + { + m_trackNameBox->clear(); + m_enableBoxes[m_trackNameBox]->setChecked(false); + } + if(m_albumNameBox->currentText() != tag->album() && + m_enableBoxes.contains(m_albumNameBox)) + { + m_albumNameBox->lineEdit()->clear(); + m_enableBoxes[m_albumNameBox]->setChecked(false); + } + if(m_genreBox->currentText() != tag->genre() && + m_enableBoxes.contains(m_genreBox)) + { + m_genreBox->lineEdit()->clear(); + m_enableBoxes[m_genreBox]->setChecked(false); + } + if(m_trackSpin->value() != tag->track() && + m_enableBoxes.contains(m_trackSpin)) + { + m_trackSpin->setValue(0); + m_enableBoxes[m_trackSpin]->setChecked(false); + } + if(m_yearSpin->value() != tag->year() && + m_enableBoxes.contains(m_yearSpin)) + { + m_yearSpin->setValue(0); + m_enableBoxes[m_yearSpin]->setChecked(false); + } + if(m_commentBox->text() != tag->comment() && + m_enableBoxes.contains(m_commentBox)) + { + m_commentBox->clear(); + m_enableBoxes[m_commentBox]->setChecked(false); + } + } + } + } + } + else { + // Clean up in the case that we are only handling one item. + + QValueListIterator<QWidget *> showIt = m_hideList.begin(); + for(; showIt != m_hideList.end(); ++showIt) + (*showIt)->show(); + + BoxMap::iterator boxIt = m_enableBoxes.begin(); + for(; boxIt != m_enableBoxes.end(); boxIt++) { + (*boxIt)->setChecked(true); + (*boxIt)->hide(); + } + } + m_dataChanged = false; +} + +void TagEditor::slotClear() +{ + m_artistNameBox->lineEdit()->clear(); + m_trackNameBox->clear(); + m_albumNameBox->lineEdit()->clear(); + m_genreBox->setCurrentItem(0); + m_fileNameBox->clear(); + m_trackSpin->setValue(0); + m_yearSpin->setValue(0); + m_lengthBox->clear(); + m_bitrateBox->clear(); + m_commentBox->clear(); +} + +void TagEditor::slotUpdateCollection() +{ + if(isVisible()) + updateCollection(); + else + m_collectionChanged = true; +} + +void TagEditor::updateCollection() +{ + m_collectionChanged = false; + + CollectionList *list = CollectionList::instance(); + + if(!list) + return; + + QStringList artistList = list->uniqueSet(CollectionList::Artists); + artistList.sort(); + m_artistNameBox->listBox()->clear(); + m_artistNameBox->listBox()->insertStringList(artistList); + m_artistNameBox->completionObject()->setItems(artistList); + + QStringList albumList = list->uniqueSet(CollectionList::Albums); + albumList.sort(); + m_albumNameBox->listBox()->clear(); + m_albumNameBox->listBox()->insertStringList(albumList); + m_albumNameBox->completionObject()->setItems(albumList); + + // Merge the list of genres found in tags with the standard ID3v1 set. + + StringHash genreHash; + + m_genreList = list->uniqueSet(CollectionList::Genres); + + for(QStringList::ConstIterator it = m_genreList.begin(); it != m_genreList.end(); ++it) + genreHash.insert(*it); + + TagLib::StringList genres = TagLib::ID3v1::genreList(); + + for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) + genreHash.insert(TStringToQString((*it))); + + m_genreList = genreHash.values(); + m_genreList.sort(); + + m_genreBox->listBox()->clear(); + m_genreBox->listBox()->insertItem(QString::null); + m_genreBox->listBox()->insertStringList(m_genreList); + m_genreBox->completionObject()->setItems(m_genreList); +} + +//////////////////////////////////////////////////////////////////////////////// +// private members +//////////////////////////////////////////////////////////////////////////////// + +void TagEditor::readConfig() +{ + // combo box completion modes + + KConfigGroup config(KGlobal::config(), "TagEditor"); + if(m_artistNameBox && m_albumNameBox) { + readCompletionMode(&config, m_artistNameBox, "ArtistNameBoxMode"); + readCompletionMode(&config, m_albumNameBox, "AlbumNameBoxMode"); + readCompletionMode(&config, m_genreBox, "GenreBoxMode"); + } + + bool show = config.readBoolEntry("Show", false); + action<KToggleAction>("showEditor")->setChecked(show); + setShown(show); + + TagLib::StringList genres = TagLib::ID3v1::genreList(); + + for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) + m_genreList.append(TStringToQString((*it))); + m_genreList.sort(); + + m_genreBox->clear(); + m_genreBox->insertItem(QString::null); + m_genreBox->insertStringList(m_genreList); + m_genreBox->completionObject()->setItems(m_genreList); +} + +void TagEditor::readCompletionMode(KConfigBase *config, KComboBox *box, const QString &key) +{ + KGlobalSettings::Completion mode = + KGlobalSettings::Completion(config->readNumEntry(key, KGlobalSettings::CompletionAuto)); + + box->setCompletionMode(mode); +} + +void TagEditor::saveConfig() +{ + // combo box completion modes + + KConfigGroup config(KGlobal::config(), "TagEditor"); + + if(m_artistNameBox && m_albumNameBox) { + config.writeEntry("ArtistNameBoxMode", m_artistNameBox->completionMode()); + config.writeEntry("AlbumNameBoxMode", m_albumNameBox->completionMode()); + config.writeEntry("GenreBoxMode", m_genreBox->completionMode()); + } + config.writeEntry("Show", action<KToggleAction>("showEditor")->isChecked()); +} + +void TagEditor::setupActions() +{ + KToggleAction *show = new KToggleAction(i18n("Show &Tag Editor"), "edit", 0, actions(), "showEditor"); + show->setCheckedState(i18n("Hide &Tag Editor")); + connect(show, SIGNAL(toggled(bool)), this, SLOT(setShown(bool))); + + new KAction(i18n("&Save"), "filesave", "CTRL+t", this, SLOT(slotSave()), actions(), "saveItem"); +} + +void TagEditor::setupLayout() +{ + static const int horizontalSpacing = 12; + static const int verticalSpacing = 2; + + QHBoxLayout *layout = new QHBoxLayout(this, 6, horizontalSpacing); + + ////////////////////////////////////////////////////////////////////////////// + // define two columns of the bottem layout + ////////////////////////////////////////////////////////////////////////////// + QVBoxLayout *leftColumnLayout = new QVBoxLayout(layout, verticalSpacing); + QVBoxLayout *rightColumnLayout = new QVBoxLayout(layout, verticalSpacing); + + layout->setStretchFactor(leftColumnLayout, 2); + layout->setStretchFactor(rightColumnLayout, 3); + + ////////////////////////////////////////////////////////////////////////////// + // put stuff in the left column -- all of the field names are class wide + ////////////////////////////////////////////////////////////////////////////// + { // just for organization + + m_artistNameBox = new KComboBox(true, this, "artistNameBox"); + m_artistNameBox->setCompletionMode(KGlobalSettings::CompletionAuto); + addItem(i18n("&Artist name:"), m_artistNameBox, leftColumnLayout, "personal"); + + m_trackNameBox = new KLineEdit(this, "trackNameBox"); + addItem(i18n("&Track name:"), m_trackNameBox, leftColumnLayout, "player_play"); + + m_albumNameBox = new KComboBox(true, this, "albumNameBox"); + m_albumNameBox->setCompletionMode(KGlobalSettings::CompletionAuto); + addItem(i18n("Album &name:"), m_albumNameBox, leftColumnLayout, "cdrom_unmount"); + + m_genreBox = new KComboBox(true, this, "genreBox"); + addItem(i18n("&Genre:"), m_genreBox, leftColumnLayout, "knotify"); + + // this fills the space at the bottem of the left column + leftColumnLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, + QSizePolicy::Expanding)); + } + ////////////////////////////////////////////////////////////////////////////// + // put stuff in the right column + ////////////////////////////////////////////////////////////////////////////// + { // just for organization + + QHBoxLayout *fileNameLayout = new QHBoxLayout(rightColumnLayout, + horizontalSpacing); + + m_fileNameBox = new KLineEdit(this, "fileNameBox"); + m_fileNameBox->setValidator(new FileNameValidator(m_fileNameBox)); + + QLabel *fileNameIcon = new QLabel(this); + fileNameIcon->setPixmap(SmallIcon("sound")); + QWidget *fileNameLabel = addHidden(new QLabel(m_fileNameBox, i18n("&File name:"), this)); + + fileNameLayout->addWidget(addHidden(fileNameIcon)); + fileNameLayout->addWidget(fileNameLabel); + fileNameLayout->setStretchFactor(fileNameIcon, 0); + fileNameLayout->setStretchFactor(fileNameLabel, 0); + fileNameLayout->insertStretch(-1, 1); + rightColumnLayout->addWidget(addHidden(m_fileNameBox)); + + { // lay out the track row + FixedHLayout *trackRowLayout = new FixedHLayout(rightColumnLayout, + horizontalSpacing); + + m_trackSpin = new KIntSpinBox(0, 9999, 1, 0, 10, this, "trackSpin"); + addItem(i18n("T&rack:"), m_trackSpin, trackRowLayout); + m_trackSpin->installEventFilter(this); + + trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, + QSizePolicy::Minimum)); + + m_yearSpin = new KIntSpinBox(0, 9999, 1, 0, 10, this, "yearSpin"); + addItem(i18n("&Year:"), m_yearSpin, trackRowLayout); + m_yearSpin->installEventFilter(this); + + trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, + QSizePolicy::Minimum)); + + trackRowLayout->addWidget(addHidden(new QLabel(i18n("Length:"), this))); + m_lengthBox = new KLineEdit(this, "lengthBox"); + // addItem(i18n("Length:"), m_lengthBox, trackRowLayout); + m_lengthBox->setMinimumWidth(fontMetrics().width("00:00") + trackRowLayout->spacing()); + m_lengthBox->setMaximumWidth(50); + m_lengthBox->setAlignment(Qt::AlignRight); + m_lengthBox->setReadOnly(true); + trackRowLayout->addWidget(addHidden(m_lengthBox)); + + trackRowLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, + QSizePolicy::Minimum)); + + trackRowLayout->addWidget(addHidden(new QLabel(i18n("Bitrate:"), this))); + m_bitrateBox = new KLineEdit(this, "bitrateBox"); + // addItem(i18n("Bitrate:"), m_bitrateBox, trackRowLayout); + m_bitrateBox->setMinimumWidth(fontMetrics().width("000") + trackRowLayout->spacing()); + m_bitrateBox->setMaximumWidth(50); + m_bitrateBox->setAlignment(Qt::AlignRight); + m_bitrateBox->setReadOnly(true); + trackRowLayout->addWidget(addHidden(m_bitrateBox)); + + trackRowLayout->setWidth(); + } + + m_commentBox = new KEdit(this, "commentBox"); + m_commentBox->setTextFormat(Qt::PlainText); + addItem(i18n("&Comment:"), m_commentBox, rightColumnLayout, "edit"); + fileNameLabel->setMinimumHeight(m_trackSpin->height()); + + } + + connect(m_artistNameBox, SIGNAL(textChanged(const QString&)), + this, SLOT(slotDataChanged())); + + connect(m_trackNameBox, SIGNAL(textChanged(const QString&)), + this, SLOT(slotDataChanged())); + + connect(m_albumNameBox, SIGNAL(textChanged(const QString&)), + this, SLOT(slotDataChanged())); + + connect(m_genreBox, SIGNAL(activated(int)), + this, SLOT(slotDataChanged())); + + connect(m_genreBox, SIGNAL(textChanged(const QString&)), + this, SLOT(slotDataChanged())); + + connect(m_fileNameBox, SIGNAL(textChanged(const QString&)), + this, SLOT(slotDataChanged())); + + connect(m_yearSpin, SIGNAL(valueChanged(int)), + this, SLOT(slotDataChanged())); + + connect(m_trackSpin, SIGNAL(valueChanged(int)), + this, SLOT(slotDataChanged())); + + connect(m_commentBox, SIGNAL(textChanged()), + this, SLOT(slotDataChanged())); +} + +void TagEditor::save(const PlaylistItemList &list) +{ + if(!list.isEmpty() && m_dataChanged) { + + KApplication::setOverrideCursor(Qt::waitCursor); + m_dataChanged = false; + m_performingSave = true; + + // The list variable can become corrupted if the playlist holding its + // items dies, which is possible as we edit tags. So we need to copy + // the end marker. + + PlaylistItemList::ConstIterator end = list.end(); + + for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberatly missing */ ) { + + // Process items before we being modifying tags, as the dynamic + // playlists will try to modify the file we edit if the tag changes + // due to our alterations here. + + kapp->eventLoop()->processEvents(QEventLoop::ExcludeUserInput); + + PlaylistItem *item = *it; + + // The playlist can be deleted from under us if this is the last + // item and we edit it so that it doesn't match the search, which + // means we can't increment the iterator, so let's do it now. + + ++it; + + QString fileName = item->file().fileInfo().dirPath() + QDir::separator() + + m_fileNameBox->text(); + if(list.count() > 1) + fileName = item->file().fileInfo().absFilePath(); + + Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName); + + // A bit more ugliness. If there are multiple files that are + // being modified, they each have a "enabled" checkbox that + // says if that field is to be respected for the multiple + // files. We have to check to see if that is enabled before + // each field that we write. + + if(m_enableBoxes[m_artistNameBox]->isOn()) + tag->setArtist(m_artistNameBox->currentText()); + if(m_enableBoxes[m_trackNameBox]->isOn()) + tag->setTitle(m_trackNameBox->text()); + if(m_enableBoxes[m_albumNameBox]->isOn()) + tag->setAlbum(m_albumNameBox->currentText()); + if(m_enableBoxes[m_trackSpin]->isOn()) { + if(m_trackSpin->text().isEmpty()) + m_trackSpin->setValue(0); + tag->setTrack(m_trackSpin->value()); + } + if(m_enableBoxes[m_yearSpin]->isOn()) { + if(m_yearSpin->text().isEmpty()) + m_yearSpin->setValue(0); + tag->setYear(m_yearSpin->value()); + } + if(m_enableBoxes[m_commentBox]->isOn()) + tag->setComment(m_commentBox->text()); + + if(m_enableBoxes[m_genreBox]->isOn()) + tag->setGenre(m_genreBox->currentText()); + + TagTransactionManager::instance()->changeTagOnItem(item, tag); + } + + TagTransactionManager::instance()->commit(); + CollectionList::instance()->dataChanged(); + m_performingSave = false; + KApplication::restoreOverrideCursor(); + } +} + +void TagEditor::saveChangesPrompt() +{ + if(!isVisible() || !m_dataChanged || m_items.isEmpty()) + return; + + QStringList files; + + for(PlaylistItemList::Iterator it = m_items.begin(); it != m_items.end(); ++it) + files.append((*it)->file().absFilePath()); + + if(KMessageBox::questionYesNoList(this, + i18n("Do you want to save your changes to:\n"), + files, + i18n("Save Changes"), + KStdGuiItem::save(), + KStdGuiItem::discard(), + "tagEditor_showSaveChangesBox") == KMessageBox::Yes) + { + save(m_items); + } +} + +void TagEditor::addItem(const QString &text, QWidget *item, QBoxLayout *layout, const QString &iconName) +{ + if(!item || !layout) + return; + + QLabel *label = new QLabel(item, text, this); + QLabel *iconLabel = new QLabel(item, 0, this); + + if(!iconName.isNull()) + iconLabel->setPixmap(SmallIcon(iconName)); + + QCheckBox *enableBox = new QCheckBox(i18n("Enable"), this); + enableBox->setChecked(true); + + label->setMinimumHeight(enableBox->height()); + + if(layout->direction() == QBoxLayout::LeftToRight) { + layout->addWidget(iconLabel); + layout->addWidget(label); + layout->addWidget(item); + layout->addWidget(enableBox); + } + else { + QHBoxLayout *l = new QHBoxLayout(layout); + + l->addWidget(iconLabel); + l->addWidget(label); + l->setStretchFactor(label, 1); + + l->insertStretch(-1, 1); + + l->addWidget(enableBox); + l->setStretchFactor(enableBox, 0); + + layout->addWidget(item); + } + + enableBox->hide(); + + connect(enableBox, SIGNAL(toggled(bool)), item, SLOT(setEnabled(bool))); + m_enableBoxes.insert(item, enableBox); +} + +void TagEditor::showEvent(QShowEvent *e) +{ + if(m_collectionChanged) { + updateCollection(); + slotRefresh(); + } + + QWidget::showEvent(e); +} + +bool TagEditor::eventFilter(QObject *watched, QEvent *e) +{ + QKeyEvent *ke = static_cast<QKeyEvent*>(e); + if(watched->inherits("QSpinBox") && e->type() == QEvent::KeyRelease && ke->state() == 0) + slotDataChanged(); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// private slots +//////////////////////////////////////////////////////////////////////////////// + +void TagEditor::slotDataChanged(bool c) +{ + m_dataChanged = c; +} + +void TagEditor::slotItemRemoved(PlaylistItem *item) +{ + m_items.remove(item); + if(m_items.isEmpty()) + slotRefresh(); +} + +void TagEditor::slotPlaylistDestroyed(Playlist *p) +{ + if(m_currentPlaylist == p) { + m_currentPlaylist = 0; + slotSetItems(PlaylistItemList()); + } +} + +#include "tageditor.moc" diff --git a/juk/tageditor.h b/juk/tageditor.h new file mode 100644 index 00000000..e9d9b1bf --- /dev/null +++ b/juk/tageditor.h @@ -0,0 +1,118 @@ +/*************************************************************************** + begin : Sat Sep 7 2002 + copyright : (C) 2002 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef TAGEDITOR_H +#define TAGEDITOR_H + +#include <qwidget.h> + +class KComboBox; +class KLineEdit; +class KIntSpinBox; +class KEdit; +class KPushButton; +class KConfigBase; + +class QCheckBox; +class QBoxLayout; + +class Playlist; +class PlaylistItem; +typedef QValueList<PlaylistItem *> PlaylistItemList; + +class CollectionObserver; + +class TagEditor : public QWidget +{ + Q_OBJECT + +public: + TagEditor(QWidget *parent = 0, const char *name = 0); + virtual ~TagEditor(); + PlaylistItemList items() const { return m_items; } + void setupObservers(); + +public slots: + void slotSave() { save(m_items); } + void slotSetItems(const PlaylistItemList &list); + void slotRefresh(); + void slotClear(); + void slotPlaylistDestroyed(Playlist *p); + /** + * Update collection if we're visible, or defer otherwise + */ + void slotUpdateCollection(); + +private: + void updateCollection(); + + void setupActions(); + void setupLayout(); + void readConfig(); + void readCompletionMode(KConfigBase *config, KComboBox *box, const QString &key); + void saveConfig(); + void save(const PlaylistItemList &list); + void saveChangesPrompt(); + /** + * Adds an item to JuK's tagging layout. This handles the creation and + * placement of the "enable" box as well. + */ + void addItem(const QString &text, QWidget *item, QBoxLayout *layout, const QString &iconName = QString::null); + + /** + * Adds a widget to m_hideList and returns that widget. + */ + QWidget *addHidden(QWidget *w) { m_hideList.append(w); return w; } + + virtual void showEvent(QShowEvent *e); + virtual bool eventFilter(QObject *watched, QEvent *e); + +private slots: + void slotDataChanged(bool c = true); + void slotItemRemoved(PlaylistItem *item); + void slotPlaylistRemoved() { m_currentPlaylist = 0; } + +private: + typedef QMap<QWidget *, QCheckBox *> BoxMap; + BoxMap m_enableBoxes; + + QStringList m_genreList; + + KComboBox *m_artistNameBox; + KLineEdit *m_trackNameBox; + KComboBox *m_albumNameBox; + KComboBox *m_genreBox; + KLineEdit *m_fileNameBox; + KIntSpinBox *m_trackSpin; + KIntSpinBox *m_yearSpin; + KLineEdit *m_lengthBox; + KLineEdit *m_bitrateBox; + KEdit *m_commentBox; + + QValueList<QWidget *> m_hideList; + + PlaylistItemList m_items; + Playlist *m_currentPlaylist; + + CollectionObserver *m_observer; + + bool m_dataChanged; + bool m_collectionChanged; + bool m_performingSave; + + friend class CollectionObserver; +}; + +#endif diff --git a/juk/tagguesser.cpp b/juk/tagguesser.cpp new file mode 100644 index 00000000..41795155 --- /dev/null +++ b/juk/tagguesser.cpp @@ -0,0 +1,218 @@ +/* + * tagguesser.cpp - (c) 2003 Frerich Raabe <raabe@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. + */ +#include "tagguesser.h" + +#include <kapplication.h> +#include <kconfig.h> +#include <kdebug.h> +#include <kglobal.h> +#include <kmacroexpander.h> + +FileNameScheme::FileNameScheme(const QString &s) + : m_regExp(), + m_titleField(-1), + m_artistField(-1), + m_albumField(-1), + m_trackField(-1), + m_commentField(-1) +{ + int fieldNumber = 1; + int i = s.find('%'); + while (i > -1) { + switch (s[ i + 1 ]) { + case 't': m_titleField = fieldNumber++; + break; + case 'a': m_artistField = fieldNumber++; + break; + case 'A': m_albumField = fieldNumber++; + break; + case 'T': m_trackField = fieldNumber++; + break; + case 'c': m_commentField = fieldNumber++; + break; + default: + break; + } + i = s.find('%', i + 1); + } + m_regExp.setPattern(composeRegExp(s)); +} + +bool FileNameScheme::matches(const QString &fileName) const +{ + /* Strip extension ('.mp3') because '.' may be part of a title, and thus + * does not work as a separator. + */ + QString stripped = fileName; + stripped.truncate(stripped.findRev('.')); + return m_regExp.exactMatch(stripped); +} + +QString FileNameScheme::title() const +{ + if(m_titleField == -1) + return QString::null; + return m_regExp.capturedTexts()[ m_titleField ]; +} + +QString FileNameScheme::artist() const +{ + if(m_artistField == -1) + return QString::null; + return m_regExp.capturedTexts()[ m_artistField ]; +} + +QString FileNameScheme::album() const +{ + if(m_albumField == -1) + return QString::null; + return m_regExp.capturedTexts()[ m_albumField ]; +} + +QString FileNameScheme::track() const +{ + if(m_trackField == -1) + return QString::null; + return m_regExp.capturedTexts()[ m_trackField ]; +} + +QString FileNameScheme::comment() const +{ + if(m_commentField == -1) + return QString::null; + return m_regExp.capturedTexts()[ m_commentField ]; +} + +QString FileNameScheme::composeRegExp(const QString &s) const +{ + QMap<QChar, QString> substitutions; + + KConfigGroup config(KGlobal::config(), "TagGuesser"); + + substitutions[ 't' ] = config.readEntry("Title regexp", "([\\w\\s'&_,\\.]+)"); + substitutions[ 'a' ] = config.readEntry("Artist regexp", "([\\w\\s'&_,\\.]+)"); + substitutions[ 'A' ] = config.readEntry("Album regexp", "([\\w\\s'&_,\\.]+)"); + substitutions[ 'T' ] = config.readEntry("Track regexp", "(\\d+)"); + substitutions[ 'c' ] = config.readEntry("Comment regexp", "([\\w\\s_]+)"); + + QString regExp = QRegExp::escape(s.simplifyWhiteSpace()); + regExp = ".*" + regExp; + regExp.replace(' ', "\\s+"); + regExp = KMacroExpander::expandMacros(regExp, substitutions); + regExp += "[^/]*$"; + return regExp; +} + +QStringList TagGuesser::schemeStrings() +{ + QStringList schemes; + + KConfigGroup config(KGlobal::config(), "TagGuesser"); + schemes = config.readListEntry("Filename schemes"); + + if ( schemes.isEmpty() ) { + schemes += "%a - (%T) - %t [%c]"; + schemes += "%a - (%T) - %t (%c)"; + schemes += "%a - (%T) - %t"; + schemes += "%a - [%T] - %t [%c]"; + schemes += "%a - [%T] - %t (%c)"; + schemes += "%a - [%T] - %t"; + schemes += "%a - %T - %t [%c]"; + schemes += "%a - %T - %t (%c)"; + schemes += "%a - %T - %t"; + schemes += "(%T) %a - %t [%c]"; + schemes += "(%T) %a - %t (%c)"; + schemes += "(%T) %a - %t"; + schemes += "[%T] %a - %t [%c]"; + schemes += "[%T] %a - %t (%c)"; + schemes += "[%T] %a - %t"; + schemes += "%T %a - %t [%c]"; + schemes += "%T %a - %t (%c)"; + schemes += "%T %a - %t"; + schemes += "(%a) %t [%c]"; + schemes += "(%a) %t (%c)"; + schemes += "(%a) %t"; + schemes += "%a - %t [%c]"; + schemes += "%a - %t (%c)"; + schemes += "%a - %t"; + schemes += "%a/%A/[%T] %t [%c]"; + schemes += "%a/%A/[%T] %t (%c)"; + schemes += "%a/%A/[%T] %t"; + } + return schemes; +} + +void TagGuesser::setSchemeStrings(const QStringList &schemes) +{ + KConfig *cfg = kapp->config(); + { + KConfigGroupSaver saver(cfg, "TagGuesser"); + cfg->writeEntry("Filename schemes", schemes); + } + cfg->sync(); +} + +TagGuesser::TagGuesser() +{ + loadSchemes(); +} + +TagGuesser::TagGuesser(const QString &absFileName) +{ + loadSchemes(); + guess(absFileName); +} + +void TagGuesser::loadSchemes() +{ + const QStringList schemes = schemeStrings(); + QStringList::ConstIterator it = schemes.begin(); + QStringList::ConstIterator end = schemes.end(); + for ( ; it != end; ++it ) + m_schemes += FileNameScheme( *it ); +} + +void TagGuesser::guess(const QString &absFileName) +{ + m_title = m_artist = m_album = m_track = m_comment = QString::null; + + FileNameScheme::List::ConstIterator it = m_schemes.begin(); + FileNameScheme::List::ConstIterator end = m_schemes.end(); + for (; it != end; ++it) { + const FileNameScheme schema(*it); + if(schema.matches(absFileName)) { + m_title = capitalizeWords(schema.title().replace('_', " ")).stripWhiteSpace(); + m_artist = capitalizeWords(schema.artist().replace('_', " ")).stripWhiteSpace(); + m_album = capitalizeWords(schema.album().replace('_', " ")).stripWhiteSpace(); + m_track = schema.track().stripWhiteSpace(); + m_comment = schema.comment().replace('_', " ").stripWhiteSpace(); + break; + } + } +} + +QString TagGuesser::capitalizeWords(const QString &s) +{ + if(s.isEmpty()) + return s; + + QString result = s; + result[ 0 ] = result[ 0 ].upper(); + + const QRegExp wordRegExp("\\s\\w"); + int i = result.find( wordRegExp ); + while ( i > -1 ) { + result[ i + 1 ] = result[ i + 1 ].upper(); + i = result.find( wordRegExp, ++i ); + } + + return result; +} + +// vim:ts=4:sw=4:noet diff --git a/juk/tagguesser.h b/juk/tagguesser.h new file mode 100644 index 00000000..eb040a4b --- /dev/null +++ b/juk/tagguesser.h @@ -0,0 +1,74 @@ +/* + * tagguesser.h - (c) 2003 Frerich Raabe <raabe@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. + */ +#ifndef TAGGUESSER_H +#define TAGGUESSER_H + +#include <qregexp.h> + +class FileNameScheme +{ + public: + typedef QValueList<FileNameScheme> List; + + FileNameScheme() { } + FileNameScheme(const QString &s); + + bool matches(const QString &s) const; + + QString title() const; + QString artist() const; + QString album() const; + QString track() const; + QString comment() const; + + private: + QString composeRegExp(const QString &s) const; + + mutable QRegExp m_regExp; + int m_titleField; + int m_artistField; + int m_albumField; + int m_trackField; + int m_commentField; +}; + +class TagGuesser +{ + public: + + enum Type { FileName = 0, MusicBrainz = 1 }; + + static QStringList schemeStrings(); + static void setSchemeStrings(const QStringList &schemes); + + TagGuesser(); + TagGuesser(const QString &absFileName); + + void guess(const QString &absFileName); + + QString title() const { return m_title; } + QString artist() const { return m_artist; } + QString album() const { return m_album; } + QString track() const { return m_track; } + QString comment() const { return m_comment; } + + private: + void loadSchemes(); + QString capitalizeWords(const QString &s); + + FileNameScheme::List m_schemes; + QString m_title; + QString m_artist; + QString m_album; + QString m_track; + QString m_comment; +}; + +#endif // TAGGUESSER_H +// vim:ts=4:sw=4:noet diff --git a/juk/tagguesserconfigdlg.cpp b/juk/tagguesserconfigdlg.cpp new file mode 100644 index 00000000..f3af1f43 --- /dev/null +++ b/juk/tagguesserconfigdlg.cpp @@ -0,0 +1,122 @@ +/* + * tagguesserconfigdlg.cpp - (c) 2003 Frerich Raabe <raabe@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. + */ +#include "tagguesser.h" +#include "tagguesserconfigdlg.h" +#include "tagguesserconfigdlgwidget.h" + +#include <kiconloader.h> +#include <klistview.h> +#include <klocale.h> +#include <kpushbutton.h> +#include <klineedit.h> +#include <kapplication.h> + +#include <qtoolbutton.h> +#include <qevent.h> + +TagGuesserConfigDlg::TagGuesserConfigDlg(QWidget *parent, const char *name) + : KDialogBase(parent, name, true, i18n("Tag Guesser Configuration"), + Ok | Cancel, Ok, true) +{ + m_child = new TagGuesserConfigDlgWidget(this, "child"); + setMainWidget(m_child); + + m_child->lvSchemes->setItemsRenameable(true); + m_child->lvSchemes->setSorting(-1); + m_child->lvSchemes->setDefaultRenameAction(QListView::Accept); + m_child->bMoveUp->setIconSet(BarIconSet("1uparrow")); + m_child->bMoveDown->setIconSet(BarIconSet("1downarrow")); + + const QStringList schemes = TagGuesser::schemeStrings(); + QStringList::ConstIterator it = schemes.begin(); + QStringList::ConstIterator end = schemes.end(); + for (; it != end; ++it) { + KListViewItem *item = new KListViewItem(m_child->lvSchemes, *it); + item->moveItem(m_child->lvSchemes->lastItem()); + } + + connect(m_child->lvSchemes, SIGNAL(currentChanged(QListViewItem *)), + this, SLOT(slotCurrentChanged(QListViewItem *))); + connect(m_child->lvSchemes, SIGNAL(doubleClicked(QListViewItem *, const QPoint &, int)), + this, SLOT(slotRenameItem(QListViewItem *, const QPoint &, int))); + connect(m_child->bMoveUp, SIGNAL(clicked()), this, SLOT(slotMoveUpClicked())); + connect(m_child->bMoveDown, SIGNAL(clicked()), this, SLOT(slotMoveDownClicked())); + connect(m_child->bAdd, SIGNAL(clicked()), this, SLOT(slotAddClicked())); + connect(m_child->bModify, SIGNAL(clicked()), this, SLOT(slotModifyClicked())); + connect(m_child->bRemove, SIGNAL(clicked()), this, SLOT(slotRemoveClicked())); + + m_child->lvSchemes->setSelected(m_child->lvSchemes->firstChild(), true); + slotCurrentChanged(m_child->lvSchemes->currentItem()); + + resize( 400, 300 ); +} + +void TagGuesserConfigDlg::accept() +{ + if(m_child->lvSchemes->renameLineEdit()) { + QKeyEvent returnKeyPress(QEvent::KeyPress, Key_Return, 0, 0); + KApplication::sendEvent(m_child->lvSchemes->renameLineEdit(), &returnKeyPress); + } + + QStringList schemes; + for (QListViewItem *it = m_child->lvSchemes->firstChild(); it; it = it->nextSibling()) + schemes += it->text(0); + TagGuesser::setSchemeStrings(schemes); + KDialogBase::accept(); +} + +void TagGuesserConfigDlg::slotCurrentChanged(QListViewItem *item) +{ + m_child->bMoveUp->setEnabled(item != 0 && item->itemAbove() != 0); + m_child->bMoveDown->setEnabled(item != 0 && item->itemBelow() != 0); + m_child->bModify->setEnabled(item != 0); + m_child->bRemove->setEnabled(item != 0); +} + +void TagGuesserConfigDlg::slotRenameItem(QListViewItem *item, const QPoint &, int c) +{ + m_child->lvSchemes->rename(item, c); +} + +void TagGuesserConfigDlg::slotMoveUpClicked() +{ + QListViewItem *item = m_child->lvSchemes->currentItem(); + if(item->itemAbove() == m_child->lvSchemes->firstChild()) + item->itemAbove()->moveItem(item); + else + item->moveItem(item->itemAbove()->itemAbove()); + m_child->lvSchemes->ensureItemVisible(item); + slotCurrentChanged(item); +} + +void TagGuesserConfigDlg::slotMoveDownClicked() +{ + QListViewItem *item = m_child->lvSchemes->currentItem(); + item->moveItem(item->itemBelow()); + m_child->lvSchemes->ensureItemVisible(item); + slotCurrentChanged(item); +} + +void TagGuesserConfigDlg::slotAddClicked() +{ + KListViewItem *item = new KListViewItem(m_child->lvSchemes); + m_child->lvSchemes->rename(item, 0); +} + +void TagGuesserConfigDlg::slotModifyClicked() +{ + m_child->lvSchemes->rename(m_child->lvSchemes->currentItem(), 0); +} + +void TagGuesserConfigDlg::slotRemoveClicked() +{ + delete m_child->lvSchemes->currentItem(); +} + +#include "tagguesserconfigdlg.moc" diff --git a/juk/tagguesserconfigdlg.h b/juk/tagguesserconfigdlg.h new file mode 100644 index 00000000..353e743a --- /dev/null +++ b/juk/tagguesserconfigdlg.h @@ -0,0 +1,39 @@ +/* + * tagguesserconfigdlg.h - (c) 2003 Frerich Raabe <raabe@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. + */ +#ifndef TAGGUESSERCONFIGDLG_H +#define TAGGUESSERCONFIGDLG_H + +#include <kdialogbase.h> + +class QListViewItem; + +class TagGuesserConfigDlgWidget; +class TagGuesserConfigDlg : public KDialogBase +{ + Q_OBJECT + public: + TagGuesserConfigDlg(QWidget *parent, const char *name = 0); + + protected slots: + virtual void accept(); + + private slots: + void slotCurrentChanged(QListViewItem *item); + void slotRenameItem(QListViewItem *item, const QPoint &p, int c); + void slotMoveUpClicked(); + void slotMoveDownClicked(); + void slotAddClicked(); + void slotModifyClicked(); + void slotRemoveClicked(); + + private: + TagGuesserConfigDlgWidget *m_child; +}; + +#endif // TAGGUESSERCONFIGDLG_H diff --git a/juk/tagguesserconfigdlgwidget.ui b/juk/tagguesserconfigdlgwidget.ui new file mode 100644 index 00000000..25d86c0b --- /dev/null +++ b/juk/tagguesserconfigdlgwidget.ui @@ -0,0 +1,159 @@ +<!DOCTYPE UI><UI version="3.1" stdsetdef="1"> +<class>TagGuesserConfigDlgWidget</class> +<author>Frerich Raabe <raabe@kde.org></author> +<widget class="QWidget"> + <property name="name"> + <cstring>Form1</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>480</height> + </rect> + </property> + <grid> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KListView" row="0" column="0" rowspan="5" colspan="1"> + <column> + <property name="text"> + <string>File Name Scheme</string> + </property> + <property name="clickable"> + <bool>false</bool> + </property> + <property name="resizable"> + <bool>false</bool> + </property> + </column> + <property name="name"> + <cstring>lvSchemes</cstring> + </property> + <property name="frameShape"> + <enum>StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>Sunken</enum> + </property> + <property name="fullWidth"> + <bool>true</bool> + </property> + <property name="toolTip" stdset="0"> + <string>Currently used file name schemes</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Here you can see the currently configured file name schemes which the "Suggest" button in the tag editor uses to extract tag information from a file name. Each string may contain one of the following placeholders:<ul> +<li>%t: Title</li> +<li>%a: Artist</li> +<li>%A: Album</li> +<li>%T: Track</li> +<li>%c: Comment</li> +</ul> +For example, the file name scheme "[%T] %a - %t" would match "[01] Deep Purple - Smoke on the water" but not "(Deep Purple) Smoke on the water". For that second name, you would use the scheme "(%a) %t".<p/> +Note that the order in which the schemes appear in the list is relevant, since the tag guesser will go through the list from the top to the bottom, and use the first matching scheme.</string> + </property> + </widget> + <widget class="KPushButton" row="1" column="1" rowspan="1" colspan="3"> + <property name="name"> + <cstring>bAdd</cstring> + </property> + <property name="text"> + <string>&Add</string> + </property> + <property name="toolTip" stdset="0"> + <string>Add a new scheme</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Press this button to add a new file name scheme to the end of the list.</string> + </property> + </widget> + <widget class="QToolButton" row="0" column="1"> + <property name="name"> + <cstring>bMoveUp</cstring> + </property> + <property name="text"> + <string></string> + </property> + <property name="toolTip" stdset="0"> + <string>Move scheme up</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Press this button to move the currently selected scheme one step upwards.</string> + </property> + </widget> + <widget class="QToolButton" row="0" column="3"> + <property name="name"> + <cstring>bMoveDown</cstring> + </property> + <property name="text"> + <string></string> + </property> + <property name="toolTip" stdset="0"> + <string>Move scheme down</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Press this button to move the currently selected scheme one step downwards.</string> + </property> + </widget> + <widget class="KPushButton" row="2" column="1" rowspan="1" colspan="3"> + <property name="name"> + <cstring>bModify</cstring> + </property> + <property name="text"> + <string>&Modify</string> + </property> + <property name="toolTip" stdset="0"> + <string>Modify scheme</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Press this button to modify the currently selected scheme.</string> + </property> + </widget> + <widget class="KPushButton" row="3" column="1" rowspan="1" colspan="3"> + <property name="name"> + <cstring>bRemove</cstring> + </property> + <property name="text"> + <string>&Remove</string> + </property> + <property name="toolTip" stdset="0"> + <string>Remove scheme</string> + </property> + <property name="whatsThis" stdset="0"> + <string>Press this button to remove the currently selected scheme from the list.</string> + </property> + </widget> + <spacer row="4" column="2"> + <property name="name"> + <cstring>spacer1</cstring> + </property> + <property name="orientation"> + <enum>Vertical</enum> + </property> + <property name="sizeType"> + <enum>Expanding</enum> + </property> + <property name="sizeHint"> + <size> + <width>20</width> + <height>130</height> + </size> + </property> + </spacer> + </grid> +</widget> +<includes> + <include location="global" impldecl="in implementation">kdialog.h</include> +</includes> +<layoutdefaults spacing="6" margin="0"/> +<layoutfunctions spacing="KDialog::spacingHint"/> +<includehints> + <includehint>klistview.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kpushbutton.h</includehint> + <includehint>kpushbutton.h</includehint> +</includehints> +</UI> diff --git a/juk/tagguessertest.cpp b/juk/tagguessertest.cpp new file mode 100644 index 00000000..f726e01b --- /dev/null +++ b/juk/tagguessertest.cpp @@ -0,0 +1,128 @@ +// Copyright Frerich Raabe <raabe@kde.org>. +// This notice was added by Michael Pyne <michael.pyne@kdemail.net> +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "tagguesser.h" +#include <kaboutdata.h> +#include <kcmdlineargs.h> +#include <kapplication.h> +#include <qdir.h> +#include <iostream> + +#include <stdlib.h> + +using std::cout; +using std::endl; + +void check( const QString &filename, const QString &title, + const QString &artist, const QString &track, + const QString &comment, const QString &album = QString::null ) +{ + cout << "Checking " << filename.latin1() << "..."; + TagGuesser guesser( filename ); + if ( guesser.title() != title ) { + cout << "Error: In filename " << filename.latin1() << ", expected title " << title.latin1() << ", got title " << guesser.title().latin1() << endl; + exit( 1 ); + } + if ( guesser.artist() != artist ) { + cout << "Error: In filename " << filename.latin1() << ", expected artist " << artist.latin1() << ", got artist " << guesser.artist().latin1() << endl; + exit( 1 ); + } + if ( guesser.track() != track ) { + cout << "Error: In filename " << filename.latin1() << ", expected track " << track.latin1() << ", got track " << guesser.track().latin1() << endl; + exit( 1 ); + } + if ( guesser.comment() != comment ) { + cout << "Error: In filename " << filename.latin1() << ", expected comment " << comment.latin1() << ", got comment " << guesser.comment().latin1() << endl; + exit( 1 ); + } + if ( guesser.album() != album ) { + cout << "Error: In filename " << filename.latin1() << ", expected album " << album.latin1() << ", got album " << guesser.album().latin1() << endl; + exit( 1 ); + } + cout << "OK" << endl; +} + +int main( int argc, char **argv ) +{ + KAboutData aboutData("tagguessertest", "tagguessertest", "0.1"); + KCmdLineArgs::init(argc, argv, &aboutData); + KApplication app; + check( "/home/frerich/Chemical Brothers - (01) - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/Chemical Brothers - (01) - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/Chemical Brothers - (01) - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null ); + check( "/home/frerich/Chemical Brothers - [01] - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/Chemical Brothers - [01] - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/Chemical Brothers - [01] - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null ); + check( "/home/frerich/Chemical Brothers - 01 - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/Chemical Brothers - 01 - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/Chemical Brothers - 01 - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null ); + check( "/home/frerich/(01) Chemical Brothers - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/(01) Chemical Brothers - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/(01) Chemical Brothers - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null ); + check( "/home/frerich/[01] Chemical Brothers - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/[01] Chemical Brothers - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/[01] Chemical Brothers - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null ); + check( "/home/frerich/01 Chemical Brothers - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/01 Chemical Brothers - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", "Live" ); + check( "/home/frerich/01 Chemical Brothers - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null ); + check( "/home/frerich/(Chemical Brothers) Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", QString::null, "Live" ); + check( "/home/frerich/(Chemical Brothers) Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", QString::null, "Live" ); + check( "/home/frerich/(Chemical Brothers) Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", QString::null, QString::null ); + check( "/home/frerich/Chemical Brothers - Block rockin' beats [Live].mp3", + "Block Rockin' Beats", "Chemical Brothers", QString::null, "Live" ); + check( "/home/frerich/Chemical Brothers - Block rockin' beats (Live).mp3", + "Block Rockin' Beats", "Chemical Brothers", QString::null, "Live" ); + check( "/home/frerich/Chemical Brothers - Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", QString::null, QString::null ); + check( "/home/frerich/mp3/Chemical Brothers/Dig your own hole/[01] Block rockin' beats.mp3", + "Block Rockin' Beats", "Chemical Brothers", "01", QString::null, "Dig Your Own Hole"); + check( QDir::homeDirPath() + "/[01] Randy - Religion, religion.mp3", + "Religion, Religion", "Randy", "01", QString::null, QString::null ); + check( QDir::homeDirPath() + "/(3) Mr. Doe - Punk.mp3", + "Punk", "Mr. Doe", "3", QString::null, QString::null ); + check( "c:\\music\\mp3s\\(3) Mr. Doe - Punk.mp3", + "Punk", "Mr. Doe", "3", QString::null, QString::null ); + cout << "All OK" << endl; + return 0; +} diff --git a/juk/tagrenameroptions.cpp b/juk/tagrenameroptions.cpp new file mode 100644 index 00000000..4f7ceba8 --- /dev/null +++ b/juk/tagrenameroptions.cpp @@ -0,0 +1,158 @@ +/*************************************************************************** + begin : Thu Oct 28 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kdebug.h> +#include <kglobal.h> +#include <klocale.h> +#include <kconfig.h> +#include <kconfigbase.h> + +#include "tagrenameroptions.h" + +TagRenamerOptions::TagRenamerOptions() : + m_emptyAction(IgnoreEmptyTag), + m_trackWidth(0), + m_disabled(true), + m_category(Unknown) +{ +} + +TagRenamerOptions::TagRenamerOptions(const TagRenamerOptions &other) : + m_prefix(other.m_prefix), + m_suffix(other.m_suffix), + m_emptyAction(other.m_emptyAction), + m_emptyText(other.m_emptyText), + m_trackWidth(other.m_trackWidth), + m_disabled(other.m_disabled), + m_category(other.m_category) +{ +} + +TagRenamerOptions::TagRenamerOptions(const CategoryID &category) + : m_category(category.category) +{ + // Set some defaults + + bool disabled; + unsigned categoryNum = category.categoryNumber; + + switch(category.category) { + case Title: + case Artist: + case Genre: + case Year: + case Album: + case Track: + disabled = false; + break; + default: + disabled = true; + } + + // Make sure we don't use translated strings for the config file keys. + + QString typeKey = tagTypeText(category.category, false); + KConfigGroup config(KGlobal::config(), "FileRenamer"); + + if(categoryNum > 0) + typeKey.append(QString::number(categoryNum)); + + setSuffix(config.readEntry(QString("%1Suffix").arg(typeKey))); + setPrefix(config.readEntry(QString("%1Prefix").arg(typeKey))); + + // Default the emptyAction to ignoring the empty tag. + + const QString emptyAction = config.readEntry(QString("%1EmptyAction").arg(typeKey)).lower(); + setEmptyAction(IgnoreEmptyTag); + + if(emptyAction == "forceemptyinclude") + setEmptyAction(ForceEmptyInclude); + else if(emptyAction == "usereplacementvalue") + setEmptyAction(UseReplacementValue); + + setEmptyText(config.readEntry(QString("%1EmptyText").arg(typeKey))); + setTrackWidth(config.readUnsignedNumEntry(QString("%1TrackWidth").arg(typeKey))); + setDisabled(config.readBoolEntry(QString("%1Disabled").arg(typeKey), disabled)); +} + +QString TagRenamerOptions::tagTypeText(TagType type, bool translate) +{ + // These must be declared in the same order that they are defined in + // the TagType enum in test.h. We can dynamically translate these strings, + // so make sure that I18N_NOOP() is used instead of i18n(). + + const char *tags[] = { + I18N_NOOP("Title"), I18N_NOOP("Artist"), I18N_NOOP("Album"), + I18N_NOOP("Track"), I18N_NOOP("Genre"), I18N_NOOP("Year") + }; + + if(type < StartTag || type >= NumTypes) { + kdWarning() << "I don't know what category we're looking up, this is a problem." << endl; + kdWarning() << "The category ID is " << (unsigned) type << endl; + return translate ? i18n("Unknown") : "Unknown"; + } + + return translate ? i18n(tags[type]) : tags[type]; +} + +void TagRenamerOptions::saveConfig(unsigned categoryNum) const +{ + // Make sure we don't use translated strings for the config file keys. + + QString typeKey = tagTypeText(false); + if(categoryNum > 0) + typeKey.append(QString::number(categoryNum)); + + KConfigGroup config(KGlobal::config(), "FileRenamer"); + + config.writeEntry(QString("%1Suffix").arg(typeKey), suffix()); + config.writeEntry(QString("%1Prefix").arg(typeKey), prefix()); + + QString emptyStr; + + switch(emptyAction()) { + case ForceEmptyInclude: + emptyStr = "ForceEmptyInclude"; + break; + + case IgnoreEmptyTag: + emptyStr = "IgnoreEmptyTag"; + break; + + case UseReplacementValue: + emptyStr = "UseReplacementValue"; + break; + } + + config.writeEntry(QString("%1EmptyAction").arg(typeKey), emptyStr); + config.writeEntry(QString("%1EmptyText").arg(typeKey), emptyText()); + config.writeEntry(QString("%1Disabled").arg(typeKey), disabled()); + + if(category() == Track) + config.writeEntry(QString("%1TrackWidth").arg(typeKey), trackWidth()); + + config.sync(); +} + +TagType TagRenamerOptions::tagFromCategoryText(const QString &text, bool translate) +{ + for(unsigned i = StartTag; i < NumTypes; ++i) + if(tagTypeText(static_cast<TagType>(i), translate) == text) + return static_cast<TagType>(i); + + return Unknown; +} + +// vim: set et ts=4 sw=4: diff --git a/juk/tagrenameroptions.h b/juk/tagrenameroptions.h new file mode 100644 index 00000000..b62e9bf4 --- /dev/null +++ b/juk/tagrenameroptions.h @@ -0,0 +1,176 @@ +/*************************************************************************** + begin : Sun Oct 31 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef JUK_TAGRENAMEROPTIONS_H +#define JUK_TAGRENAMEROPTIONS_H + +// Insert all new tag types before NumTypes, that way NumTypes will always be +// the count of valid tag types. +enum TagType { + StartTag, Title = StartTag, Artist, Album, + Track, Genre, Year, NumTypes, Unknown +}; + +/** + * Class that uniquely identifies a user's category (since the user may have + * the same category more than once in their file renaming structure). + */ +struct CategoryID +{ + CategoryID() : category(Unknown), categoryNumber(0) + { + } + + CategoryID(const CategoryID &other) : category(other.category), + categoryNumber(other.categoryNumber) + { + } + + CategoryID(TagType cat, unsigned num) : category(cat), categoryNumber(num) + { + } + + CategoryID &operator=(const CategoryID &other) + { + if(this == &other) + return *this; + + category = other.category; + categoryNumber = other.categoryNumber; + + return *this; + } + + bool operator==(const CategoryID &other) const + { + return category == other.category && categoryNumber == other.categoryNumber; + } + + bool operator!=(const CategoryID &other) const + { + return !(*this == other); + } + + bool operator<(const CategoryID &other) const + { + if(category == other.category) + return categoryNumber < other.categoryNumber; + + return category < other.category; + } + + TagType category; + unsigned categoryNumber; +}; + +/** + * Defines options for a tag type. Used by FileRenamerTagOptions as its + * data type. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class TagRenamerOptions +{ +public: + enum EmptyActions { ForceEmptyInclude, IgnoreEmptyTag, UseReplacementValue }; + + TagRenamerOptions(); + + /** + * Construct the options by loading from KConfig. + * + * @param category The category to load the options for. + */ + TagRenamerOptions(const CategoryID &category); + TagRenamerOptions(const TagRenamerOptions &other); + + QString prefix() const { return m_prefix; } + QString suffix() const { return m_suffix; } + QString emptyText() const { return m_emptyText; } + EmptyActions emptyAction() const { return m_emptyAction; } + unsigned trackWidth() const { return m_trackWidth; } + bool disabled() const { return m_disabled; } + TagType category() const { return m_category; } + + void setPrefix(const QString &prefix) { m_prefix = prefix; } + void setSuffix(const QString &suffix) { m_suffix = suffix; } + void setEmptyText(const QString &emptyText) { m_emptyText = emptyText; } + void setEmptyAction(EmptyActions action) { m_emptyAction = action; } + void setTrackWidth(unsigned width) { m_trackWidth = width; } + void setDisabled(bool disabled) { m_disabled = disabled; } + void setCategory(TagType category) { m_category = category; } + + /** + * Maps \p type to a textual representation of its name. E.g. Track => "Track" + * + * @param type the category to retrieve a text representation of. + * @param translate if true, the string is translated (if possible). + * @return text representation of category. + */ + static QString tagTypeText(TagType category, bool translate = true); + + QString tagTypeText(bool translate = true) const + { + return tagTypeText(category(), translate); + } + + /** + * Function that tries to match a string back to its category. Uses + * the case-sensitive form of the string. If it fails it will return + * Unknown. + * + * @param translated If true, @p text is translated, if false, it is the untranslated + * version. + */ + static TagType tagFromCategoryText(const QString &text, bool translate = true); + + /** + * This saves the options to the global KConfig object. + * + * @param categoryNum The zero-based count of the number of this type of + * category. For example, this would be 1 for the + * second category of this type. The stored category + * number is not used in order to allow you to save with + * a different one (for compaction purposes perhaps). + */ + void saveConfig(unsigned categoryNum) const; + +private: + + // Member variables + + QString m_prefix; + QString m_suffix; + + /// Defines the action to take when the tag is empty. + EmptyActions m_emptyAction; + + /// If m_emptyAction is UseReplacementValue, this holds the text of the value + /// to use. + QString m_emptyText; + + /// Used only for the Track type. Defines the minimum track width when + /// expanding the track token. + unsigned m_trackWidth; + + /// This is true if this tag is always disabled when expanding file names. + bool m_disabled; + + TagType m_category; +}; + +#endif /* JUK_TAGRENAMEROPTIONS_H */ + +// vim: set et ts=4 sw=4: diff --git a/juk/tagtransactionmanager.cpp b/juk/tagtransactionmanager.cpp new file mode 100644 index 00000000..e6c8adef --- /dev/null +++ b/juk/tagtransactionmanager.cpp @@ -0,0 +1,214 @@ +/*************************************************************************** + begin : Wed Sep 22 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kdebug.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kaction.h> +#include <kapplication.h> + +#include <qfileinfo.h> +#include <qdir.h> + +#include "tagtransactionmanager.h" +#include "playlistitem.h" +#include "collectionlist.h" +#include "tag.h" +#include "actioncollection.h" + +using ActionCollection::action; + +TagTransactionManager *TagTransactionManager::m_manager = 0; + +TagTransactionAtom::TagTransactionAtom() : m_item(0), m_tag(0) +{ + action("edit_undo")->setEnabled(false); +} + +TagTransactionAtom::TagTransactionAtom(const TagTransactionAtom &other) : + m_item(other.m_item), m_tag(other.m_tag) +{ + other.m_tag = 0; // Only allow one owner +} + +TagTransactionAtom::TagTransactionAtom(PlaylistItem *item, Tag *tag) : + m_item(item), m_tag(tag) +{ +} + +TagTransactionAtom::~TagTransactionAtom() +{ + delete m_tag; +} + +TagTransactionAtom &TagTransactionAtom::operator=(const TagTransactionAtom &other) +{ + m_item = other.m_item; + m_tag = other.m_tag; + + other.m_tag = 0; // Only allow one owner + + return *this; +} + +TagTransactionManager *TagTransactionManager::instance() +{ + return m_manager; +} + +void TagTransactionManager::changeTagOnItem(PlaylistItem *item, Tag *newTag) +{ + if(!item) { + kdWarning(65432) << "Trying to change tag on null PlaylistItem.\n"; + return; + } + + // Save the CollectionListItem, as it is the most likely to survive long + // enough for the commit(). I should probably intercept the item deleted + // signals from CollectionList to ensure that the commit list and the + // playlists stay in sync. + + m_list.append(TagTransactionAtom(item->collectionItem(), newTag)); +} + +Tag *TagTransactionManager::duplicateTag(const Tag *tag, const QString &fileName) +{ + Q_ASSERT(tag); + + QString name = fileName.isNull() ? tag->fileName() : fileName; + Tag *newTag = new Tag(*tag); + + newTag->setFileName(name); + return newTag; +} + +bool TagTransactionManager::commit() +{ + m_undoList.clear(); + bool result = processChangeList(); + + m_list.clear(); + return result; +} + +void TagTransactionManager::forget() +{ + m_list.clear(); +} + +bool TagTransactionManager::undo() +{ + kdDebug(65432) << "Undoing " << m_undoList.count() << " changes.\n"; + + forget(); // Scrap our old changes (although the list should be empty + // anyways. + + bool result = processChangeList(true); + + m_undoList.clear(); + action("edit_undo")->setEnabled(false); + + return result; +} + +TagTransactionManager::TagTransactionManager(QWidget *parent) : QObject(parent, "tagmanager") +{ + m_manager = this; +} + +bool TagTransactionManager::renameFile(const QFileInfo &from, const QFileInfo &to) const +{ + if(!QFileInfo(to.dirPath()).isWritable() || !from.exists()) + return false; + + if(!to.exists() || + KMessageBox::warningContinueCancel( + static_cast<QWidget *>(parent()), + i18n("This file already exists.\nDo you want to replace it?"), + i18n("File Exists"),i18n("Replace")) == KMessageBox::Continue) + { + kdDebug(65432) << "Renaming " << from.absFilePath() << " to " << to.absFilePath() << endl; + QDir currentDir; + return currentDir.rename(from.absFilePath(), to.absFilePath()); + } + + return false; +} + +bool TagTransactionManager::processChangeList(bool undo) +{ + TagAlterationList::ConstIterator it, end; + QStringList errorItems; + + it = undo ? m_undoList.begin() : m_list.begin(); + end = undo ? m_undoList.end() : m_list.end(); + + emit signalAboutToModifyTags(); + + for(; it != end; ++it) { + PlaylistItem *item = (*it).item(); + Tag *tag = (*it).tag(); + + QFileInfo newFile(tag->fileName()); + + if(item->file().fileInfo().fileName() != newFile.fileName()) { + if(!renameFile(item->file().fileInfo(), newFile)) { + errorItems.append(item->text(1) + QString(" - ") + item->text(0)); + continue; + } + } + + if(tag->save()) { + if(!undo) + m_undoList.append(TagTransactionAtom(item, duplicateTag(item->file().tag()))); + + item->file().setFile(tag->fileName()); + item->refreshFromDisk(); + item->repaint(); + item->playlist()->dataChanged(); + item->playlist()->update(); + } + else { + Tag *errorTag = item->file().tag(); + QString str = errorTag->artist() + " - " + errorTag->title(); + + if(errorTag->artist().isEmpty()) + str = errorTag->title(); + + errorItems.append(str); + } + + kapp->processEvents(); + } + + undo ? m_undoList.clear() : m_list.clear(); + if(!undo && !m_undoList.isEmpty()) + action("edit_undo")->setEnabled(true); + else + action("edit_undo")->setEnabled(false); + + if(!errorItems.isEmpty()) + KMessageBox::errorList(static_cast<QWidget *>(parent()), + i18n("The following files were unable to be changed."), + errorItems, + i18n("Error")); + + emit signalDoneModifyingTags(); + return errorItems.isEmpty(); +} + +#include "tagtransactionmanager.moc" + +// vim: set et ts=4 sw=4 tw=0: diff --git a/juk/tagtransactionmanager.h b/juk/tagtransactionmanager.h new file mode 100644 index 00000000..ce39393f --- /dev/null +++ b/juk/tagtransactionmanager.h @@ -0,0 +1,213 @@ +/*************************************************************************** + begin : Wed Sep 22 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef _TAGTRANSACTIONMANAGER_H +#define _TAGTRANSACTIONMANAGER_H + + + +class PlaylistItem; +class QWidget; +class Tag; + +/** + * Class to encapsulate a change to the tag, and optionally the file name, of + * a PlaylistItem. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + * @see TagTransactionManager + */ +class TagTransactionAtom +{ + public: + /** + * Default constructor, for use by QValueList. + */ + TagTransactionAtom(); + + /** + * Copy constructor. This takes ownership of the m_tag pointer, so the + * object being copied no longer has access to the tag. This function also + * exists mainly for QValueList's benefit. + * + * @param other The TagTransactionAtom to copy. + */ + TagTransactionAtom(const TagTransactionAtom &other); + + /** + * Creates an atom detailing a change made by \p tag to \p item. + * + * @param tag Contains the new tag to apply to item. + * @param item The PlaylistItem to change. + */ + TagTransactionAtom(PlaylistItem *item, Tag *tag); + + /** + * Destroys the atom. This function deletes the tag, so make sure you've + * already copied out any data you need. The PlaylistItem is unaffected. + */ + ~TagTransactionAtom(); + + /** + * Assignment operator. This operator takes ownership of the m_tag pointer, + * so the object being assigned from no longer has access to the tag. This + * function exists mainly for the benefit of QValueList. + * + * @param other The TagTransactionAtom to copy from. + * @return The TagTransactionAtom being assigned to. + */ + TagTransactionAtom &operator=(const TagTransactionAtom &other); + + /** + * Accessor function to retrieve the PlaylistItem. + * + * @return The PlaylistItem being changed. + */ + PlaylistItem *item() const { return m_item; } + + /** + * Accessor function to retrieve the changed Tag. + * + * @return The Tag containing the changes to apply to item(). + */ + Tag *tag() const { return m_tag; } + + private: + PlaylistItem *m_item; + mutable Tag *m_tag; +}; + +typedef QValueList<TagTransactionAtom> TagAlterationList; + +/** + * This class manages alterations of a group of PlaylistItem's FileHandles. What this + * means in practice is that you will use this class to change the tags and/or + * filename of a PlaylistItem. + * + * This class supports a limited transactional interface. Once you commit a + * group of changes, you can call the undo() method to revert back to the way + * things were (except possibly for file renames). You can call forget() to + * forget a series of changes as well. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class TagTransactionManager : public QObject +{ + Q_OBJECT + + public: + /** + * Constructs a TagTransactionManager, owned by @p parent. + * + * @param parent The parent QWidget. + */ + TagTransactionManager(QWidget *parent = 0); + + /** + * Returns the global TagTransactionManager instance. + * + * @return The global TagTransactionManager. + */ + static TagTransactionManager *instance(); + + /** + * Adds a change to the list of changes to apply. Internally this + * function extracts the CollectionListItem of @p item, and uses that + * instead, so there is no need to do so yourself. + * + * @param item The PlaylistItem to change. + * @param newTag The Tag containing the changed data. + */ + void changeTagOnItem(PlaylistItem *item, Tag *newTag); + + /** + * Convienience function to duplicate a Tag object, since the Tag + * object doesn't have a decent copy constructor. + * + * @param tag The Tag to duplicate. + * @param fileName The filename to assign to the tag. If QString::null + * (the default) is passed, the filename of the existing tag is + * used. + * @bug Tag should have a correct copy ctor and assignment operator. + * @return The duplicate Tag. + */ + static Tag *duplicateTag(const Tag *tag, const QString &fileName = QString::null); + + /** + * Commits the changes to the PlaylistItems. It is important that the + * PlaylistItems still exist when you call this function, although this + * shouldn't be a problem in practice. After altering the tags, and + * renaming the files if necessary, you can call undo() to back out the + * changes. + * + * If any errors have occurred, the user will be notified with a dialog + * box, and those files which were unabled to be altered will be excluded + * from the undo set. + * + * @return true if no errors occurred, false otherwise. + */ + bool commit(); + + /** + * Clears the current update list. The current undo list is unaffected. + */ + void forget(); + + /** + * Undoes the changes caused by commit(). Like commit(), if any errors + * occur changing the state back (for example, it may be impossible to + * rename a file back to its original name), the user will be shown notified + * via a dialog box. + * + * After performing the undo operation, it is impossible to call undo() + * again on the same set of files. Namely, you can't repeatedly call + * undo() to switch between two different file states. + * + * @return true if no errors occurred, false otherwise. + */ + bool undo(); + + signals: + void signalAboutToModifyTags(); + void signalDoneModifyingTags(); + + private: + /** + * Renames the file identified by @p from to have the name given by @p to, + * prompting the user to confirm if necessary. + * + * @param from QFileInfo with the filename of the original file. + * @param to QFileInfo with the new filename. + * @return true if no errors occurred, false otherwise. + */ + bool renameFile(const QFileInfo &from, const QFileInfo &to) const; + + /** + * Used internally by commit() and undo(). Performs the work of updating + * the PlaylistItems and then updating the various GUI elements that need + * to be updated. + * + * @param undo true if operating in undo mode, false otherwise. + */ + bool processChangeList(bool undo = false); + + TagAlterationList m_list; ///< holds a list of changes to commit + TagAlterationList m_undoList; ///< holds a list of changes to undo + static TagTransactionManager *m_manager; ///< used by instance() +}; + +#endif /* _TAGTRANSACTIONMANAGER_H */ + +// vim: set et ts=4 sw=4 tw=0: diff --git a/juk/trackpickerdialog.cpp b/juk/trackpickerdialog.cpp new file mode 100644 index 00000000..44cf8f7a --- /dev/null +++ b/juk/trackpickerdialog.cpp @@ -0,0 +1,101 @@ +/*************************************************************************** + begin : Sat Sep 6 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#include <config.h> + +#if HAVE_MUSICBRAINZ + +#include <qlabel.h> + +#include <klistview.h> +#include <klocale.h> + +#include "trackpickerdialog.h" +#include "trackpickerdialogbase.h" + +#define NUMBER(x) (x == 0 ? QString::null : QString::number(x)) + +class TrackPickerItem : public KListViewItem +{ +public: + TrackPickerItem(KListView *parent, const KTRMResult &result) : + KListViewItem(parent, parent->lastChild(), + result.title(), result.artist(), result.album(), + NUMBER(result.track()), NUMBER(result.year())), + m_result(result) {} + KTRMResult result() const { return m_result; } + +private: + KTRMResult m_result; +}; + +//////////////////////////////////////////////////////////////////////////////// +// public methods +//////////////////////////////////////////////////////////////////////////////// + +TrackPickerDialog::TrackPickerDialog(const QString &name, + const KTRMResultList &results, + QWidget *parent) : + KDialogBase(parent, name.latin1(), true, i18n("Internet Tag Guesser"), Ok | Cancel, Ok, true) +{ + m_base = new TrackPickerDialogBase(this); + setMainWidget(m_base); + + m_base->fileLabel->setText(name); + m_base->trackList->setSorting(-1); + + for(KTRMResultList::ConstIterator it = results.begin(); it != results.end(); ++it) + new TrackPickerItem(m_base->trackList, *it); + + m_base->trackList->setSelected(m_base->trackList->firstChild(), true); + + connect(m_base->trackList, SIGNAL(doubleClicked(QListViewItem *, const QPoint &, int)), + this, SLOT(accept())); + + setMinimumWidth(kMax(400, width())); +} + +TrackPickerDialog::~TrackPickerDialog() +{ + +} + +KTRMResult TrackPickerDialog::result() const +{ + if(m_base->trackList->selectedItem()) + return static_cast<TrackPickerItem *>(m_base->trackList->selectedItem())->result(); + else + return KTRMResult(); +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +int TrackPickerDialog::exec() +{ + int dialogCode = KDialogBase::exec(); + + // Only return true if an item was selected. + + if(m_base->trackList->selectedItem()) + return dialogCode; + else + return Rejected; +} + +#include "trackpickerdialog.moc" + +#endif // HAVE_MUSICBRAINZ diff --git a/juk/trackpickerdialog.h b/juk/trackpickerdialog.h new file mode 100644 index 00000000..623944d8 --- /dev/null +++ b/juk/trackpickerdialog.h @@ -0,0 +1,51 @@ +/*************************************************************************** + begin : Sat Sep 6 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler + email : 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. * + * * + ***************************************************************************/ + +#ifndef TRACKPICKERDIALOG_H +#define TRACKPICKERDIALOG_H + +#include <config.h> + +#if HAVE_MUSICBRAINZ + +#include <kdialogbase.h> + +#include "musicbrainzquery.h" + +class TrackPickerDialogBase; + +class TrackPickerDialog : public KDialogBase +{ + Q_OBJECT + +public: + TrackPickerDialog(const QString &name, + const KTRMResultList &results, + QWidget *parent = 0); + + virtual ~TrackPickerDialog(); + + KTRMResult result() const; + +public slots: + int exec(); + +private: + TrackPickerDialogBase *m_base; +}; + +#endif // HAVE_MUSICBRAINZ + +#endif diff --git a/juk/trackpickerdialogbase.ui b/juk/trackpickerdialogbase.ui new file mode 100644 index 00000000..b938fb19 --- /dev/null +++ b/juk/trackpickerdialogbase.ui @@ -0,0 +1,179 @@ +<!DOCTYPE UI><UI version="3.2" stdsetdef="1"> +<class>TrackPickerDialogBase</class> +<widget class="QWidget"> + <property name="name"> + <cstring>trackPickerDialogBase</cstring> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>556</width> + <height>310</height> + </rect> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>fileLayout</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox"> + <property name="name"> + <cstring>fileInfoGroup</cstring> + </property> + <property name="title"> + <string>File Name</string> + </property> + <property name="alignment"> + <set>AlignTop</set> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QLabel"> + <property name="name"> + <cstring>fileLabel</cstring> + </property> + <property name="font"> + <font> + <bold>1</bold> + </font> + </property> + <property name="text"> + <string></string> + </property> + <property name="alignment"> + <set>AlignVCenter|AlignLeft</set> + </property> + <property name="indent"> + <number>9</number> + </property> + </widget> + </hbox> + </widget> + </hbox> + </widget> + <widget class="QLayoutWidget"> + <property name="name"> + <cstring>trackLayout</cstring> + </property> + <hbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="QGroupBox"> + <property name="name"> + <cstring>trackInfoGroup</cstring> + </property> + <property name="title"> + <string>Select Best Possible Match</string> + </property> + <property name="alignment"> + <set>AlignTop</set> + </property> + <vbox> + <property name="name"> + <cstring>unnamed</cstring> + </property> + <widget class="KListView"> + <column> + <property name="text"> + <string>Track Name</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>false</bool> + </property> + </column> + <column> + <property name="text"> + <string>Artist</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>false</bool> + </property> + </column> + <column> + <property name="text"> + <string>Album</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>false</bool> + </property> + </column> + <column> + <property name="text"> + <string>Track</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <column> + <property name="text"> + <string>Year</string> + </property> + <property name="clickable"> + <bool>true</bool> + </property> + <property name="resizable"> + <bool>true</bool> + </property> + </column> + <property name="name"> + <cstring>trackList</cstring> + </property> + <property name="sizePolicy"> + <sizepolicy> + <hsizetype>5</hsizetype> + <vsizetype>7</vsizetype> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="allColumnsShowFocus"> + <bool>true</bool> + </property> + <property name="itemMargin"> + <number>1</number> + </property> + <property name="resizeMode"> + <enum>LastColumn</enum> + </property> + <property name="fullWidth"> + <bool>true</bool> + </property> + </widget> + </vbox> + </widget> + </hbox> + </widget> + </vbox> +</widget> +<tabstops> + <tabstop>trackList</tabstop> +</tabstops> +<layoutdefaults spacing="6" margin="11"/> +<includehints> + <includehint>klistview.h</includehint> +</includehints> +</UI> diff --git a/juk/tracksequenceiterator.cpp b/juk/tracksequenceiterator.cpp new file mode 100644 index 00000000..2c71008a --- /dev/null +++ b/juk/tracksequenceiterator.cpp @@ -0,0 +1,310 @@ +/*************************************************************************** + begin : Thu Aug 19 2004 + copyright : (C) 2002 - 2004 by Michael Pyne + email : michael.pyne@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. * + * * + ***************************************************************************/ + +#include <kaction.h> +#include <kapplication.h> +#include <kdebug.h> + +#include "tracksequenceiterator.h" +#include "playlist.h" +#include "actioncollection.h" +#include "tag.h" +#include "filehandle.h" + +using namespace ActionCollection; + +TrackSequenceIterator::TrackSequenceIterator() : + m_current(0) +{ +} + +TrackSequenceIterator::TrackSequenceIterator(const TrackSequenceIterator &other) : + m_current(other.m_current) +{ +} + +TrackSequenceIterator::~TrackSequenceIterator() +{ +} + +void TrackSequenceIterator::setCurrent(PlaylistItem *current) +{ + m_current = current; +} + +void TrackSequenceIterator::playlistChanged() +{ +} + +void TrackSequenceIterator::itemAboutToDie(const PlaylistItem *) +{ +} + +DefaultSequenceIterator::DefaultSequenceIterator() : + TrackSequenceIterator() +{ +} + +DefaultSequenceIterator::DefaultSequenceIterator(const DefaultSequenceIterator &other) + : TrackSequenceIterator(other) +{ +} + +DefaultSequenceIterator::~DefaultSequenceIterator() +{ +} + +void DefaultSequenceIterator::advance() +{ + if(!current()) + return; + + bool isRandom = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked(); + bool loop = action("loopPlaylist") && action<KToggleAction>("loopPlaylist")->isChecked(); + bool albumRandom = action("albumRandomPlay") && action<KToggleAction>("albumRandomPlay")->isChecked(); + + if(isRandom || albumRandom) { + if(m_randomItems.isEmpty() && loop) { + + // Since refillRandomList will remove the currently playing item, + // we should clear it out first since that's not good for e.g. + // lists with 1-2 items. We need to remember the Playlist though. + + Playlist *playlist = current()->playlist(); + setCurrent(0); + + refillRandomList(playlist); + } + + if(m_randomItems.isEmpty()) { + setCurrent(0); + return; + } + + PlaylistItem *item; + + if(albumRandom) { + if(m_albumSearch.isNull() || m_albumSearch.matchedItems().isEmpty()) { + item = m_randomItems[KApplication::random() % m_randomItems.count()]; + initAlbumSearch(item); + } + + // This can be null if initAlbumSearch() left the m_albumSearch + // empty because the album text was empty. Since we initAlbumSearch() + // with an item, the matchedItems() should never be empty. + + if(!m_albumSearch.isNull()) { + PlaylistItemList albumMatches = m_albumSearch.matchedItems(); + if(albumMatches.isEmpty()) { + kdError(65432) << "Unable to initialize album random play.\n"; + kdError(65432) << "List of potential results is empty.\n"; + + return; // item is still set to random song from a few lines earlier. + } + + item = albumMatches[0]; + + // Pick first song remaining in list. + + for(unsigned i = 0; i < albumMatches.count(); ++i) + if(albumMatches[i]->file().tag()->track() < item->file().tag()->track()) + item = albumMatches[i]; + m_albumSearch.clearItem(item); + + if(m_albumSearch.matchedItems().isEmpty()) { + m_albumSearch.clearComponents(); + m_albumSearch.search(); + } + } + else + kdError(65432) << "Unable to perform album random play on " << *item << endl; + } + else + item = m_randomItems[KApplication::random() % m_randomItems.count()]; + + setCurrent(item); + m_randomItems.remove(item); + } + else { + PlaylistItem *next = current()->itemBelow(); + if(!next && loop) { + Playlist *p = current()->playlist(); + next = p->firstChild(); + while(next && !next->isVisible()) + next = static_cast<PlaylistItem *>(next->nextSibling()); + } + + setCurrent(next); + } +} + +void DefaultSequenceIterator::backup() +{ + if(!current()) + return; + + PlaylistItem *item = current()->itemAbove(); + + if(item) + setCurrent(item); +} + +void DefaultSequenceIterator::prepareToPlay(Playlist *playlist) +{ + bool random = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked(); + bool albumRandom = action("albumRandomPlay") && action<KToggleAction>("albumRandomPlay")->isChecked(); + + if(random || albumRandom) { + PlaylistItemList items = playlist->selectedItems(); + if(items.isEmpty()) + items = playlist->visibleItems(); + + PlaylistItem *newItem = 0; + if(!items.isEmpty()) + newItem = items[KApplication::random() % items.count()]; + + setCurrent(newItem); + refillRandomList(); + } + else { + QListViewItemIterator it(playlist, QListViewItemIterator::Visible | QListViewItemIterator::Selected); + if(!it.current()) + it = QListViewItemIterator(playlist, QListViewItemIterator::Visible); + + setCurrent(static_cast<PlaylistItem *>(it.current())); + } +} + +void DefaultSequenceIterator::reset() +{ + m_randomItems.clear(); + m_albumSearch.clearComponents(); + m_albumSearch.search(); + setCurrent(0); +} + +void DefaultSequenceIterator::playlistChanged() +{ + refillRandomList(); +} + +void DefaultSequenceIterator::itemAboutToDie(const PlaylistItem *item) +{ + PlaylistItem *stfu_gcc = const_cast<PlaylistItem *>(item); + + m_randomItems.remove(stfu_gcc); +} + +void DefaultSequenceIterator::setCurrent(PlaylistItem *current) +{ + PlaylistItem *oldCurrent = DefaultSequenceIterator::current(); + + TrackSequenceIterator::setCurrent(current); + + bool random = action("randomPlay") && action<KToggleAction>("randomPlay")->isChecked(); + bool albumRandom = action("albumRandomPlay") && action<KToggleAction>("albumRandomPlay")->isChecked(); + + if((albumRandom || random) && current && m_randomItems.isEmpty()) { + + // We're setting a current item, refill the random list now, and remove + // the current item. + + refillRandomList(); + } + + m_randomItems.remove(current); + + if(albumRandom && current && !oldCurrent) { + + // Same idea as above + + initAlbumSearch(current); + m_albumSearch.clearItem(current); + } +} + +DefaultSequenceIterator *DefaultSequenceIterator::clone() const +{ + return new DefaultSequenceIterator(*this); +} + +void DefaultSequenceIterator::refillRandomList(Playlist *p) +{ + if(!p) { + if (!current()) + return; + + p = current()->playlist(); + + if(!p) { + kdError(65432) << k_funcinfo << "Item has no playlist!\n"; + return; + } + } + + m_randomItems = p->visibleItems(); + m_randomItems.remove(current()); + m_albumSearch.clearComponents(); + m_albumSearch.search(); +} + +void DefaultSequenceIterator::initAlbumSearch(PlaylistItem *searchItem) +{ + if(!searchItem) + return; + + m_albumSearch.clearPlaylists(); + m_albumSearch.addPlaylist(searchItem->playlist()); + + ColumnList columns; + + m_albumSearch.setSearchMode(PlaylistSearch::MatchAll); + m_albumSearch.clearComponents(); + + // If the album name is empty, it will mess up the search, + // so ignore empty album names. + + if(searchItem->file().tag()->album().isEmpty()) + return; + + columns.append(PlaylistItem::AlbumColumn); + + m_albumSearch.addComponent(PlaylistSearch::Component( + searchItem->file().tag()->album(), + true, + columns, + PlaylistSearch::Component::Exact) + ); + + // If there is an Artist tag with the track, match against it as well + // to avoid things like multiple "Greatest Hits" albums matching the + // search. + + if(!searchItem->file().tag()->artist().isEmpty()) { + kdDebug(65432) << "Searching both artist and album.\n"; + columns[0] = PlaylistItem::ArtistColumn; + + m_albumSearch.addComponent(PlaylistSearch::Component( + searchItem->file().tag()->artist(), + true, + columns, + PlaylistSearch::Component::Exact) + ); + } + + m_albumSearch.search(); +} + +// vim: set et sw=4 tw=0: diff --git a/juk/tracksequenceiterator.h b/juk/tracksequenceiterator.h new file mode 100644 index 00000000..a2339f01 --- /dev/null +++ b/juk/tracksequenceiterator.h @@ -0,0 +1,232 @@ +/*************************************************************************** + begin : Thu Aug 19 2004 + copyright : (C) 2002 - 2004 by Michael Pyne + email : michael.pyne@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. * + * * + ***************************************************************************/ + +#ifndef _TRACKSEQUENCEITERATOR_H +#define _TRACKSEQUENCEITERATOR_H + +#include "playlistitem.h" +#include "playlistsearch.h" + +class Playlist; + +/** + * This abstract class defines an interface to be used by TrackSequenceManager, + * to iterate over the items in a playlist. Implement this class in a subclass + * in order to define your own ordering for playlist sequences. For an example, + * see the UpcomingPlaylist class. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + * @see UpcomingPlaylist + * @see TrackSequenceManager + */ +class TrackSequenceIterator +{ +public: + /** + * Default constructor. + */ + TrackSequenceIterator(); + + /** + * Default copy constructor. + * + * @param other the TrackSequenceIterator we are copying + */ + TrackSequenceIterator(const TrackSequenceIterator & other); + + /** + * Default destructor. + */ + virtual ~TrackSequenceIterator(); + + /** + * This function moves the current item to the next track. You must + * reimplement this function in your subclasses. + */ + virtual void advance() = 0; + + /** + * This function moves the current item to the previous track. This may + * not always make sense, and the history functionality of the Playlist + * class currently overrides this. You must reimplement this function in + * your subclass. + */ + virtual void backup() = 0; + + /** + * This function returns the current PlaylistItem, or 0 if the iterator is + * not pointing at any PlaylistItem. + * + * @return current track + */ + virtual PlaylistItem *current() const { return m_current; } + + /** + * This function creates a perfect copy of the object it is called on, to + * avoid the C++ slicing problem. When you reimplement this function, you + * should change the return type to the name of the subclass. + * + * @return pointer to a copy of the object + */ + virtual TrackSequenceIterator *clone() const = 0; + + /** + * This function is called by the TrackSequenceManager when current() returns + * 0, if the TrackSequenceManager has a playlist defined. This function + * should choose an appropriate starting track and set it as the current + * item. This function must be reimplemented in subclasses. + * + * @param playlist the playlist to iterate over + */ + virtual void prepareToPlay(Playlist *playlist) = 0; + + /** + * This function is called whenever the current playlist changes, such as + * having a new search applied, items added/removed, etc. If you need to + * update internal state, you should do so without affecting the current + * playing item. Default implementation does nothing. + */ + virtual void playlistChanged(); + + /** + * This function is called by the manager when \p item is about to be + * removed. Subclasses should ensure that they're not still holding a + * pointer to the item. The default implementation does nothing. + * + * @param item the item about to be removed. + */ + virtual void itemAboutToDie(const PlaylistItem *item); + + /** + * This function is called by the TrackSequenceManager is some situations, + * such as when playback is being stopped. If you subclass needs to reset + * any internal data members, do so in this function. This function must + * be reimplemented in subclasses. + */ + virtual void reset() = 0; + + /** + * This function is a public mutator to set the current item. + * + * @param current the new current item + */ + virtual void setCurrent(PlaylistItem *current); + +private: + PlaylistItem::Pointer m_current; ///< the current item +}; + +/** + * This is the default iterator for JuK, supporting normal, random, and album + * random playback with or without looping. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class DefaultSequenceIterator : public TrackSequenceIterator +{ +public: + /** + * Default constructor. + */ + DefaultSequenceIterator(); + + /** + * Default copy constructor. + * + * @param other the DefaultSequenceIterator to copy. + */ + DefaultSequenceIterator(const DefaultSequenceIterator &other); + + /** + * Default destructor. + */ + virtual ~DefaultSequenceIterator(); + + /** + * This function advances to the next item in the current sequence. The + * algorithm used depends on what playback mode is selected. + */ + virtual void advance(); + + /** + * This function moves to the previous item in the playlist. This occurs + * no matter what playback mode is selected. + */ + virtual void backup(); + + /** + * This function prepares the class for iterator. If no random play mode + * is selected, the first item in the given playlist is the starting item. + * Otherwise, an item is randomly picked to be the starting item. + * + * @param playlist The playlist to initialize for. + */ + virtual void prepareToPlay(Playlist *playlist); + + /** + * This function clears all internal state, including any random play lists, + * and what the current album is. + */ + virtual void reset(); + + /** + * This function recalculates the random lists, and is should be called + * whenever its current playlist changes (at least for searches). + */ + virtual void playlistChanged(); + + /** + * Called when \p item is about to be removed. This function ensures that + * it isn't remaining in the random play list. + */ + virtual void itemAboutToDie(const PlaylistItem *item); + + /** + * This function sets the current item, and initializes any internal lists + * that may be needed for playback. + * + * @param current The new current item. + */ + virtual void setCurrent(PlaylistItem *current); + + /** + * This function returns a perfect copy of the object it is called on, to + * get around the C++ slicing problem. + * + * @return A copy of the object the method is called on. + */ + virtual DefaultSequenceIterator *clone() const; + +private: + + /** + * Reinitializes the internal random play list based on the playlist given + * by \p p. The currently playing item, if any, is automatically removed + * from the list. + * + * @param p The Playlist to read items from. If p is 0, the playlist of + * the currently playing item is used instead. + */ + void refillRandomList(Playlist *p = 0); + void initAlbumSearch(PlaylistItem *searchItem); + +private: + PlaylistItemList m_randomItems; + PlaylistSearch m_albumSearch; +}; + +#endif /* _TRACKSEQUENCEITERATOR_H */ + +// vim: set et sw=4: diff --git a/juk/tracksequencemanager.cpp b/juk/tracksequencemanager.cpp new file mode 100644 index 00000000..7e05f825 --- /dev/null +++ b/juk/tracksequencemanager.cpp @@ -0,0 +1,183 @@ +/*************************************************************************** + begin : Thu Aug 19 2004 + copyright : (C) 2002 - 2004 by Michael Pyne + email : michael.pyne@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. * + * * + ***************************************************************************/ + +#include <kconfig.h> +#include <klocale.h> +#include <kaction.h> +#include <kpopupmenu.h> + +#include "actioncollection.h" +#include "tracksequencemanager.h" +#include "playlist.h" +#include "playlistitem.h" +#include "tracksequenceiterator.h" +#include "tag.h" +#include "filehandle.h" +#include "collectionlist.h" + +///////////////////////////////////////////////////////////////////////////// +// public functions +///////////////////////////////////////////////////////////////////////////// + +TrackSequenceManager::~TrackSequenceManager() +{ + // m_playlist and m_popupMenu don't belong to us, don't try to delete them + if(m_iterator == m_defaultIterator) + m_iterator = 0; + + delete m_iterator; + delete m_defaultIterator; +} + +bool TrackSequenceManager::installIterator(TrackSequenceIterator *iterator) +{ + PlaylistItem *oldItem = m_iterator ? m_iterator->current() : 0; + + if(m_iterator != m_defaultIterator) + delete m_iterator; + + m_iterator = m_defaultIterator; + if(iterator) + m_iterator = iterator; + + m_iterator->setCurrent(oldItem); + + return true; +} + +PlaylistItem *TrackSequenceManager::currentItem() const +{ + return m_iterator->current(); +} + +TrackSequenceIterator *TrackSequenceManager::takeIterator() +{ + TrackSequenceIterator *temp = m_iterator; + + m_iterator = 0; + return temp; +} + +TrackSequenceManager *TrackSequenceManager::instance() +{ + static TrackSequenceManager manager; + + if(!manager.m_initialized) + manager.initialize(); + + return &manager; +} + +PlaylistItem *TrackSequenceManager::nextItem() +{ + if(m_playNextItem) { + + // Force the iterator to reset state (such as random item lists) + + m_iterator->reset(); + m_iterator->setCurrent(m_playNextItem); + m_playNextItem = 0; + } + else if(m_iterator->current()) + m_iterator->advance(); + else if(currentPlaylist()) + m_iterator->prepareToPlay(currentPlaylist()); + else + m_iterator->prepareToPlay(CollectionList::instance()); + + return m_iterator->current(); +} + +PlaylistItem *TrackSequenceManager::previousItem() +{ + m_iterator->backup(); + return m_iterator->current(); +} + +///////////////////////////////////////////////////////////////////////////// +// public slots +///////////////////////////////////////////////////////////////////////////// + +void TrackSequenceManager::setNextItem(PlaylistItem *item) +{ + m_playNextItem = item; +} + +void TrackSequenceManager::setCurrentPlaylist(Playlist *list) +{ + if(m_playlist) + m_playlist->disconnect(this); + m_playlist = list; + + connect(m_playlist, SIGNAL(signalAboutToRemove(PlaylistItem *)), + this, SLOT(slotItemAboutToDie(PlaylistItem *))); +} + +void TrackSequenceManager::setCurrent(PlaylistItem *item) +{ + if(item != m_iterator->current()) { + m_iterator->setCurrent(item); + if(item) + setCurrentPlaylist(item->playlist()); + else + m_iterator->reset(); + } +} + +///////////////////////////////////////////////////////////////////////////// +// private functions +///////////////////////////////////////////////////////////////////////////// + +void TrackSequenceManager::initialize() +{ + CollectionList *collection = CollectionList::instance(); + + if(!collection) + return; + + // Make sure we don't use m_playNextItem if it's invalid. + connect(collection, SIGNAL(signalAboutToRemove(PlaylistItem *)), + this, SLOT(slotItemAboutToDie(PlaylistItem *))); + + m_initialized = true; +} + +TrackSequenceManager::TrackSequenceManager() : + QObject(), + m_playlist(0), + m_playNextItem(0), + m_popupMenu(0), + m_iterator(0), + m_initialized(false) +{ + m_defaultIterator = new DefaultSequenceIterator(); + m_iterator = m_defaultIterator; +} + +///////////////////////////////////////////////////////////////////////////// +// protected slots +///////////////////////////////////////////////////////////////////////////// + +void TrackSequenceManager::slotItemAboutToDie(PlaylistItem *item) +{ + if(item == m_playNextItem) + m_playNextItem = 0; + + m_iterator->itemAboutToDie(item); +} + +#include "tracksequencemanager.moc" + +// vim: set et sw=4 tw=0: diff --git a/juk/tracksequencemanager.h b/juk/tracksequencemanager.h new file mode 100644 index 00000000..3aa186cd --- /dev/null +++ b/juk/tracksequencemanager.h @@ -0,0 +1,192 @@ +/*************************************************************************** + begin : Thu Aug 19 2004 + copyright : (C) 2002 - 2004 by Michael Pyne + email : michael.pyne@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. * + * * + ***************************************************************************/ + +#ifndef _TRACKSEQUENCEMANAGER_H +#define _TRACKSEQUENCEMANAGER_H + +#include <qobject.h> + +class KPopupMenu; +class TrackSequenceIterator; +class PlaylistItem; +class Playlist; + +/** + * This class is responsible for managing the music play sequence for JuK. + * Instead of playlists deciding which song goes next, this class is used to + * do so. You can replace the iterator used as well, although the class + * provides a default iterator that supports random play and playlist looping. + * + * @see Playlist + * @see TrackSequenceIterator + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class TrackSequenceManager : public QObject +{ + Q_OBJECT + +public: + /** + * Destroys the track sequence manager. The sequence iterators will also + * be deleted, but the playlist, popup menu, and playlist items will not be + * touched. + */ + ~TrackSequenceManager(); + + /** + * This function installs a new iterator to be used instead of the old + * one. TrackSequenceManager will control the iterator after that, + * deleting the iterator when another is installed, or when the + * TrackSequenceManager is destroyed. + * + * @param iterator the iterator to install, or 0 for the default + * @return true if installation successfully happened + */ + bool installIterator(TrackSequenceIterator *iterator); + + /** + * @return currently selected iterator + */ + TrackSequenceIterator *iterator() const { return m_iterator; } + + /** + * This function returns a pointer to the currently set iterator, and + * then removes the TrackSequenceManager's pointer to the iterator without + * deleting the iterator. You should only do this if you are going to be + * using @see installIterator to give control of the iterator back to the + * TrackSequenceManager at some point. Also, you must install a + * replacement iterator before the TrackSequenceManager is otherwise + * used. If you use this function, you must manually set the current + * item of the iterator you replace the old one with (if you want). + * + * @see installIterator + * @return the currently set iterator. + */ + TrackSequenceIterator *takeIterator(); + + /** + * Returns the global TrackSequenceManager object. This is the only way to + * access the TrackSequenceManager. + * + * @return the global TrackSequenceManager + */ + static TrackSequenceManager *instance(); + + /** + * Returns the next track, and advances in the current sequence.. + * + * @return the next track in the current sequence, or 0 if the end has + * been reached + */ + PlaylistItem *nextItem(); + + /** + * Returns the previous track, and backs up in the current sequence. Note + * that if you have an item x, nextItem(previousItem(x)) is not guaranteed + * to equal x, even ignoring the effect of hitting the end of list. + * + * @return the previous track in the current sequence, or 0 if the + * beginning has been reached + */ + PlaylistItem *previousItem(); + + /** + * @return the current track in the current sequence, or 0 if there is no + * current track (for example, an empty playlist) + */ + PlaylistItem *currentItem() const; + + /** + * @return the current KPopupMenu used by the manager, or 0 if none is + * set + */ + KPopupMenu *popupMenu() const { return m_popupMenu; } + + /** + * @return the TrackSequenceManager's idea of the current playlist + */ + Playlist *currentPlaylist() const { return m_playlist; } + +public slots: + /** + * Set the next item to play to @p item + * + * @param item the next item to play + */ + void setNextItem(PlaylistItem *item); + + /** + * Sets the current playlist. This is necessary in order for some of the + * actions in the popup menu used by this class to work. Note that the + * current playlist is not necessarily the same as the playlist that is + * playlist. The TrackSequenceManager does not own @p list after this + * call. + * + * @param list the current playlist + */ + void setCurrentPlaylist(Playlist *list); + + /** + * Sets the current item to @p item. You should try to avoid calling this + * function, instead allowing the manager to perform its work. However, + * this function is useful for clearing the current item. Remember that + * you must have a valid playlist to iterate if you clear the current item. + * + * @param item the PlaylistItem that is currently playing. Set to 0 if + * there is no item playing. + */ + void setCurrent(PlaylistItem *item); + +private: + /** + * Sets up various connections, to be run after the GUI is running. + * Automatically run by instance(). + * + * @see instance + */ + void initialize(); + + /** + * Constructs the sequence manager. The constructor will work even before + * the GUI has been created. Note that you can't actually construct an + * object with this function, use instance(). + * + * @see instance + */ + TrackSequenceManager(); + +protected slots: + + /** + * This slot should be called when @a item is about to be deleted, so that + * the TrackSequenceManager can make sure that any pointers held pointing + * to @a item are corrected. + * + * @param item The PlaylistItem about to be deleted. + */ + void slotItemAboutToDie(PlaylistItem *item); + +private: + Playlist *m_playlist; + PlaylistItem *m_curItem, *m_playNextItem; + KPopupMenu *m_popupMenu; + TrackSequenceIterator *m_iterator; + TrackSequenceIterator *m_defaultIterator; + bool m_initialized; +}; + +#endif /* _TRACKSEQUENCEMANAGER_H */ + +// vim: set et sw=4: diff --git a/juk/treeviewitemplaylist.cpp b/juk/treeviewitemplaylist.cpp new file mode 100644 index 00000000..9096979a --- /dev/null +++ b/juk/treeviewitemplaylist.cpp @@ -0,0 +1,93 @@ +/*************************************************************************** + begin : Mon Jun 21 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kapplication.h> +#include <kdebug.h> +#include <kmessagebox.h> +#include <klocale.h> + +#include <qstringlist.h> +#include <qlistview.h> + +#include "collectionlist.h" +#include "treeviewitemplaylist.h" +#include "tag.h" +#include "playlistitem.h" +#include "playlistsearch.h" +#include "tagtransactionmanager.h" + +TreeViewItemPlaylist::TreeViewItemPlaylist(PlaylistCollection *collection, + const PlaylistSearch &search, + const QString &name) : + SearchPlaylist(collection, search, name, false) +{ + PlaylistSearch::Component component = *(search.components().begin()); + m_columnType = static_cast<PlaylistItem::ColumnType>(*(component.columns().begin())); +} + +void TreeViewItemPlaylist::retag(const QStringList &files, Playlist *) +{ + CollectionList *collection = CollectionList::instance(); + + if(files.isEmpty()) + return; + + QString changedTag = i18n("artist"); + if(m_columnType == PlaylistItem::GenreColumn) + changedTag = i18n("genre"); + else if(m_columnType == PlaylistItem::AlbumColumn) + changedTag = i18n("album"); + + if(KMessageBox::warningContinueCancelList( + this, + i18n("You are about to change the %1 on these files.").arg(changedTag), + files, + i18n("Changing Track Tags"), + KStdGuiItem::cont(), + "dragDropRetagWarn" + ) == KMessageBox::Cancel) + { + return; + } + + QStringList::ConstIterator it; + for(it = files.begin(); it != files.end(); ++it) { + CollectionListItem *item = collection->lookup(*it); + if(!item) + continue; + + Tag *tag = TagTransactionManager::duplicateTag(item->file().tag()); + switch(m_columnType) { + case PlaylistItem::ArtistColumn: + tag->setArtist(name()); + break; + + case PlaylistItem::AlbumColumn: + tag->setAlbum(name()); + break; + + case PlaylistItem::GenreColumn: + tag->setGenre(name()); + break; + + default: + kdDebug() << "Unhandled column type editing " << *it << endl; + } + + TagTransactionManager::instance()->changeTagOnItem(item, tag); + } +} + +#include "treeviewitemplaylist.moc" diff --git a/juk/treeviewitemplaylist.h b/juk/treeviewitemplaylist.h new file mode 100644 index 00000000..6b0e13c2 --- /dev/null +++ b/juk/treeviewitemplaylist.h @@ -0,0 +1,43 @@ +/*************************************************************************** + begin : Mon Jun 21 2004 + copyright : (C) 2004 by Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef TREEVIEWITEMPLAYLIST_H +#define TREEVIEWITEMPLAYLIST_H + +#include "searchplaylist.h" +#include "playlistitem.h" + +class QStringList; + +class TreeViewItemPlaylist : public SearchPlaylist +{ + Q_OBJECT + +public: + TreeViewItemPlaylist(PlaylistCollection *collection, + const PlaylistSearch &search = PlaylistSearch(), + const QString &name = QString::null); + + virtual bool searchIsEditable() const { return false; } + void retag(const QStringList &files, Playlist *donorPlaylist); + +signals: + void signalTagsChanged(); + +private: + PlaylistItem::ColumnType m_columnType; +}; + +#endif // TREEVIEWITEMPLAYLIST_H diff --git a/juk/upcomingplaylist.cpp b/juk/upcomingplaylist.cpp new file mode 100644 index 00000000..a9cdbcb7 --- /dev/null +++ b/juk/upcomingplaylist.cpp @@ -0,0 +1,277 @@ +/*************************************************************************** + begin : Thu Aug 19 2004 + copyright : (C) 2002 - 2004 by Michael Pyne + email : michael.pyne@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. * + * * + ***************************************************************************/ + +#include <kdebug.h> +#include <kapplication.h> +#include <kaction.h> + +#include "upcomingplaylist.h" +#include "playlistitem.h" +#include "playlistcollection.h" +#include "tracksequencemanager.h" +#include "collectionlist.h" +#include "actioncollection.h" + +using namespace ActionCollection; + +UpcomingPlaylist::UpcomingPlaylist(PlaylistCollection *collection, int defaultSize) : + Playlist(collection, true), + m_active(false), + m_oldIterator(0), + m_defaultSize(defaultSize) +{ + setName(i18n("Play Queue")); + setAllowDuplicates(true); + setSorting(-1); +} + +UpcomingPlaylist::~UpcomingPlaylist() +{ + removeIteratorOverride(); +} + +void UpcomingPlaylist::initialize() +{ + // Prevent duplicate initialization. + + if(m_active) + return; + + m_active = true; + + m_oldIterator = manager()->takeIterator(); + manager()->installIterator(new UpcomingSequenceIterator(this)); + + if(!m_oldIterator->current()) + m_oldIterator->prepareToPlay(CollectionList::instance()); + else + manager()->iterator()->setCurrent(m_oldIterator->current()); +} + +void UpcomingPlaylist::appendItems(const PlaylistItemList &itemList) +{ + initialize(); + + if(itemList.isEmpty()) + return; + + PlaylistItem *after = static_cast<PlaylistItem *>(lastItem()); + + for(PlaylistItemList::ConstIterator it = itemList.begin(); it != itemList.end(); ++it) { + after = createItem(*it, after); + m_playlistIndex.insert(after, (*it)->playlist()); + } + + dataChanged(); + slotWeightDirty(); +} + +void UpcomingPlaylist::playNext() +{ + initialize(); + + PlaylistItem *next = TrackSequenceManager::instance()->nextItem(); + + if(next) { + setPlaying(next); + Playlist *source = m_playlistIndex[next]; + if(source) { + PlaylistList l; + l.append(this); + source->synchronizePlayingItems(l, false); + } + } + else { + removeIteratorOverride(); + + // Normally we continue to play the currently playing item that way + // a user can continue to hear their song when deselecting Play Queue. + // However we're technically still "playing" when the queue empties and + // we reinstall the old iterator so in this situation manually advance + // to the next track. (Otherwise we hear the same song twice in a row + // during the transition. + + setPlaying(manager()->nextItem()); + } +} + +void UpcomingPlaylist::clearItem(PlaylistItem *item, bool emitChanged) +{ + m_playlistIndex.remove(item); + Playlist::clearItem(item, emitChanged); +} + +void UpcomingPlaylist::addFiles(const QStringList &files, PlaylistItem *after) +{ + CollectionList::instance()->addFiles(files, after); + + PlaylistItemList l; + for(QStringList::ConstIterator it = files.begin(); it != files.end(); ++it) { + FileHandle f(*it); + PlaylistItem *i = CollectionList::instance()->lookup(f.absFilePath()); + if(i) + l.append(i); + } + + appendItems(l); +} + +QMap< PlaylistItem::Pointer, QGuardedPtr<Playlist> > &UpcomingPlaylist::playlistIndex() +{ + return m_playlistIndex; +} + +void UpcomingPlaylist::removeIteratorOverride() +{ + if(!m_active) + return; + + m_active = false; // Allow re-initialization. + + if(!m_oldIterator) + return; + + // Install the old track iterator. + + manager()->installIterator(m_oldIterator); + + // If we have an item that is currently playing, allow it to keep playing. + // Otherwise, just reset to the default iterator (probably not playing + // anything.) + // XXX: Reset to the last playing playlist? + + m_oldIterator->reset(); + if(playingItem()) + m_oldIterator->setCurrent(playingItem()->collectionItem()); + + setPlaying(manager()->currentItem(), true); + + Watched::currentChanged(); +} + +TrackSequenceManager *UpcomingPlaylist::manager() const +{ + return TrackSequenceManager::instance(); +} + +UpcomingPlaylist::UpcomingSequenceIterator::UpcomingSequenceIterator(UpcomingPlaylist *playlist) : + TrackSequenceIterator(), m_playlist(playlist) +{ +} + +UpcomingPlaylist::UpcomingSequenceIterator::UpcomingSequenceIterator(const UpcomingSequenceIterator &other) : + TrackSequenceIterator(other), m_playlist(other.m_playlist) +{ +} + +UpcomingPlaylist::UpcomingSequenceIterator::~UpcomingSequenceIterator() +{ +} + +void UpcomingPlaylist::UpcomingSequenceIterator::advance() +{ + PlaylistItem *item = m_playlist->firstChild(); + + if(item) { + PlaylistItem *next = static_cast<PlaylistItem *>(item->nextSibling()); + m_playlist->clearItem(item); + setCurrent(next); + } +} + +void UpcomingPlaylist::UpcomingSequenceIterator::backup() +{ +} + +UpcomingPlaylist::UpcomingSequenceIterator *UpcomingPlaylist::UpcomingSequenceIterator::clone() const +{ + return new UpcomingSequenceIterator(*this); +} + +void UpcomingPlaylist::UpcomingSequenceIterator::setCurrent(PlaylistItem *currentItem) +{ + if(!currentItem) { + TrackSequenceIterator::setCurrent(currentItem); + return; + } + + // If the upcoming playlist is playing something, clear it out since + // apparently the user didn't want to hear it. + + PlaylistItem *playingItem = m_playlist->playingItem(); + if(playingItem && playingItem->playlist() == m_playlist && currentItem != playingItem) + m_playlist->clearItem(playingItem); + + // If a different playlist owns this item, add it to the upcoming playlist + + Playlist *p = currentItem->playlist(); + + if(p != m_playlist) { + PlaylistItem *i = m_playlist->createItem(currentItem, (PlaylistItem *) 0); + m_playlist->playlistIndex().insert(i, p); + m_playlist->dataChanged(); + m_playlist->slotWeightDirty(); + } + else { + // if(p == m_playlist) { + + // Bump this item up to the top + m_playlist->takeItem(currentItem); + m_playlist->insertItem(currentItem); + } + + TrackSequenceIterator::setCurrent(m_playlist->firstChild()); +} + +void UpcomingPlaylist::UpcomingSequenceIterator::reset() +{ + setCurrent(0); +} + +void UpcomingPlaylist::UpcomingSequenceIterator::prepareToPlay(Playlist *) +{ + if(!m_playlist->items().isEmpty()) + setCurrent(m_playlist->firstChild()); +} + +QDataStream &operator<<(QDataStream &s, const UpcomingPlaylist &p) +{ + PlaylistItemList l = const_cast<UpcomingPlaylist *>(&p)->items(); + + s << Q_INT32(l.count()); + + for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) + s << (*it)->file().absFilePath(); + + return s; +} + +QDataStream &operator>>(QDataStream &s, UpcomingPlaylist &p) +{ + QString fileName; + PlaylistItem *newItem = 0; + Q_INT32 count; + + s >> count; + + for(Q_INT32 i = 0; i < count; ++i) { + s >> fileName; + newItem = p.createItem(FileHandle(fileName), newItem, false); + } + + return s; +} + +// vim: set et ts=4 sw=4: diff --git a/juk/upcomingplaylist.h b/juk/upcomingplaylist.h new file mode 100644 index 00000000..ee0570f9 --- /dev/null +++ b/juk/upcomingplaylist.h @@ -0,0 +1,213 @@ +/*************************************************************************** + begin : Thu Aug 19 2004 + copyright : (C) 2002 - 2004 by Michael Pyne + email : michael.pyne@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. * + * * + ***************************************************************************/ + +#ifndef _UPCOMINGPLAYLIST_H +#define _UPCOMINGPLAYLIST_H + + +#include <qguardedptr.h> + +#include "playlist.h" +#include "tracksequenceiterator.h" + +class TrackSequenceManager; + +/** + * A class to implement upcoming playlist support in JuK. It is closely + * associated with the UpcomingSequenceIterator class. It works by using + * whatever iterator is currently being used by the TrackSequenceManager + * instance when this playlist is constructed to form a list of upcoming tracks. + * After the playlist is created, tracks are played from top to bottom until + * the list is empty. If loop playlist is enabled, tracks are automatically + * added as tracks are removed. Also, enabling this playlist causes the base + * Playlist class to add an item to the context-menu to add the selected items + * to this playlist. If the user double-clicks any track to force it to play, + * it is added to the top of this playlist automatically, replacing any + * currently playing song. + * + * @author Michael Pyne <michael.pyne@kdemail.net> + * @see UpcomingSequenceIterator + */ +class UpcomingPlaylist : public Playlist +{ +public: + /** + * Constructor for the UpcomingPlaylist object. You should only ever have + * one instance of this class. You should call the initialize() function + * before using the created object. + * + * @see initialize + * @param collection The PlaylistCollection that owns this playlist. + * @param defaultSize The default number of tracks to place in the playlist. + */ + UpcomingPlaylist(PlaylistCollection *collection, int defaultSize = 15); + /** + * Destructor for the UpcomingPlaylist. This destructor will restore the + * iterator for the TrackSequenceManager, and if a song is playing when + * this playlist is removed, it will remain playing after the playlist is + * destroyed. + */ + virtual ~UpcomingPlaylist(); + + /** + * This function initializes the upcoming playlist, so that you can create + * it before the GUI has been completely setup. If a song is playing when + * this function is called, then the song will be added to this playlist, + * automatically with no interruption in playback. + */ + void initialize(); + + /** + * Appends the given items to the end of the playlist. Use this function + * instead of createItems() since this function ensures that the items are + * added to the end of the playlist. + * + * @see createItems(const PlaylistItemList &, PlaylistItem *) + * @param itemList The list of PlaylistItems to append. + */ + void appendItems(const PlaylistItemList &itemList); + + /** + * Reimplemented to set the playing item in both the source playlist + * and the upcoming playlist. + */ + virtual void playNext(); + + /** + * Reimplemented to remove the item from the Playlist index. + */ + virtual void clearItem(PlaylistItem *item, bool emitChanged = true); + + virtual void addFiles(const QStringList &files, PlaylistItem *after = 0); + + /** + * Returns a reference to the index between items in the list and the + * playlist that they came from. This is used to remap the currently + * playing item to the source playlist. + */ + QMap<PlaylistItem::Pointer, QGuardedPtr<Playlist> > &playlistIndex(); + + bool active() const { return m_active; } + +private: + + /** + * Internal function to restore the TrackSequenceManager to the state + * it was in when the object was constructed, except for the playing + * item. + */ + void removeIteratorOverride(); + + /** + * This function returns the instance of the TrackSequenceManager. + * + * @return the TrackSequenceManager instance. + * @see TrackSequenceManager::instance() + */ + TrackSequenceManager *manager() const; + +private: + class UpcomingSequenceIterator; + friend class UpcomingSequenceIterator; + + bool m_active; + TrackSequenceIterator *m_oldIterator; + int m_defaultSize; + QMap<PlaylistItem::Pointer, QGuardedPtr<Playlist> > m_playlistIndex; +}; + +/** + * An implementation of TrackSequenceIterator designed to work with + * UpcomingPlaylist. It is installed by UpcomingPlaylist to ensure that the + * UpcomingPlaylist is in charge of the playback sequence. + * + * @see UpcomingPlaylist + * @see TrackSequenceManager + * @author Michael Pyne <michael.pyne@kdemail.net> + */ +class UpcomingPlaylist::UpcomingSequenceIterator : public TrackSequenceIterator +{ +public: + /** + * Default constructor. + * + * @param playlist The UpcomingPlaylist this iterator belongs to. + */ + UpcomingSequenceIterator(UpcomingPlaylist *playlist); + + /** + * Copy constructor. + * + * @param other The UpcomingSequenceIterator to copy. + */ + UpcomingSequenceIterator(const UpcomingSequenceIterator &other); + + /** + * Destructor. + */ + virtual ~UpcomingSequenceIterator(); + + /** + * Advances to the next song in the UpcomingPlaylist. + */ + virtual void advance(); + + /** + * This function does nothing, as the currently playing song in the + * UpcomingPlaylist is always the first song in the sequence. + */ + virtual void backup(); + + /** + * This function returns a perfect duplicate of the object it is called + * on, to avoid the C++ slicing problem. + * + * @return A pointer to a copy of the object it is called on. + */ + virtual UpcomingSequenceIterator *clone() const; + + /** + * This function sets the currently playing item to @a currentItem. If the + * item doesn't belong to the parent UpcomingPlaylist, it will be added to + * the UpcomingPlaylist, replacing any track that may be playing. + * Otherwise, it is moved up and set to play, replacing any track that may + * be playing. + * + * @param currentItem The PlaylistItem to play. + */ + virtual void setCurrent(PlaylistItem *currentItem); + + /** + * This function resets any internet state. + */ + virtual void reset(); + + /** + * This function readies the UpcomingSequenceIterator for playback, by + * making sure the parent UpcomingPlaylist has items to play if it is + * empty. + */ + virtual void prepareToPlay(Playlist *); + +private: + UpcomingPlaylist *m_playlist; +}; + +QDataStream &operator<<(QDataStream &s, const UpcomingPlaylist &p); +QDataStream &operator>>(QDataStream &s, UpcomingPlaylist &p); + +#endif /* _UPCOMINGPLAYLIST_H */ + +// vim: set et sw=4 ts=4: diff --git a/juk/viewmode.cpp b/juk/viewmode.cpp new file mode 100644 index 00000000..4bd4b9d8 --- /dev/null +++ b/juk/viewmode.cpp @@ -0,0 +1,425 @@ +/*************************************************************************** + begin : Sat Jun 7 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler, + email : 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. * + * * + ***************************************************************************/ + +#include <kiconloader.h> +#include <kstandarddirs.h> +#include <kdebug.h> + +#include <qpixmap.h> +#include <qpainter.h> +#include <qfile.h> +#include <qdir.h> +#include <qdatastream.h> + +#include "viewmode.h" +#include "playlistbox.h" +#include "searchplaylist.h" +#include "treeviewitemplaylist.h" +#include "collectionlist.h" + +//////////////////////////////////////////////////////////////////////////////// +// ViewMode +//////////////////////////////////////////////////////////////////////////////// + +ViewMode::ViewMode(PlaylistBox *b) : + QObject(b), + m_playlistBox(b), + m_visible(false), + m_needsRefresh(false) +{ + m_playlistBox->viewport()->installEventFilter(this); +} + +ViewMode::~ViewMode() +{ + +} + +void ViewMode::paintCell(PlaylistBox::Item *item, + QPainter *painter, + const QColorGroup &colorGroup, + int column, int width, int) +{ + if(width < item->pixmap(column)->width()) + return; + + if(m_needsRefresh) + updateHeights(); + + QFontMetrics fm = painter->fontMetrics(); + + int y = item->listView()->itemMargin() + border; + const QPixmap *pm = item->pixmap(column); + + if(item->isSelected()) { + + painter->eraseRect(0, 0, width, item->height()); + + QPen oldPen = painter->pen(); + QPen newPen = oldPen; + + newPen.setWidth(5); + newPen.setJoinStyle(RoundJoin); + newPen.setColor(QColorGroup::Highlight); + + painter->setPen(newPen); + painter->drawRect(border, border, width - border * 2, item->height() - border * 2 + 1); + painter->setPen(oldPen); + + painter->fillRect(border, border, width - border * 2, item->height() - border * 2 + 1, + colorGroup.brush(QColorGroup::Highlight)); + painter->setPen(colorGroup.highlightedText()); + } + else + painter->eraseRect(0, 0, width, item->height()); + + if (!pm->isNull()) { + int x = (width - pm->width()) / 2; + x = QMAX(x, item->listView()->itemMargin()); + painter->drawPixmap(x, y, *pm); + } + y += pm->height() + fm.height() - fm.descent(); + for(QStringList::Iterator it = m_lines[item].begin(); it != m_lines[item].end(); ++it) { + int x = (width - fm.width(*it)) / 2; + x = QMAX(x, item->listView()->itemMargin()); + painter->drawText(x, y, *it); + y += fm.height() - fm.descent(); + } + + if(item == item->listView()->dropItem()) + paintDropIndicator(painter, width, item->height()); +} + +bool ViewMode::eventFilter(QObject *watched, QEvent *e) +{ + if(m_visible && watched == m_playlistBox->viewport() && e->type() == QEvent::Resize) { + QResizeEvent *re = static_cast<QResizeEvent *>(e); + if(re->size().width() != re->oldSize().width()) + m_needsRefresh = true; + } + + if(e->type() == QEvent::Hide) + m_needsRefresh = true; + + return QObject::eventFilter(watched, e); +} + +void ViewMode::setShown(bool shown) +{ + m_visible = shown; + if(shown) { + updateIcons(32); + m_needsRefresh = true; + } +} + +void ViewMode::updateIcons(int size) +{ + for(QListViewItemIterator it(m_playlistBox); it.current(); ++it) { + PlaylistBox::Item *i = static_cast<PlaylistBox::Item *>(*it); + i->setPixmap(0, SmallIcon(i->iconName(), size)); + } +} + +void ViewMode::setupItem(PlaylistBox::Item *item) const +{ + const PlaylistBox *box = item->listView(); + const int width = box->width() - box->verticalScrollBar()->width() - border * 2; + const int baseHeight = 2 * box->itemMargin() + 32 + border * 2; + const QFontMetrics fm = box->fontMetrics(); + item->setHeight(baseHeight + (fm.height() - fm.descent()) * lines(item, fm, width).count()); +} + +void ViewMode::updateHeights() +{ + const int width = m_playlistBox->width() - m_playlistBox->verticalScrollBar()->width() - border * 2; + + const int baseHeight = 2 * m_playlistBox->itemMargin() + 32 + border * 2; + const QFontMetrics fm = m_playlistBox->fontMetrics(); + + for(QListViewItemIterator it(m_playlistBox); it.current(); ++it) { + PlaylistBox::Item *i = static_cast<PlaylistBox::Item *>(it.current()); + m_lines[i] = lines(i, fm, width); + const int height = baseHeight + (fm.height() - fm.descent()) * m_lines[i].count(); + i->setHeight(height); + } + + m_needsRefresh = false; +} + +void ViewMode::paintDropIndicator(QPainter *painter, int width, int height) // static +{ + static const int border = 1; + static const int lineWidth = 2; + + QPen oldPen = painter->pen(); + QPen newPen = oldPen; + + newPen.setWidth(lineWidth); + newPen.setStyle(DotLine); + + painter->setPen(newPen); + painter->drawRect(border, border, width - border * 2, height - border * 2); + painter->setPen(oldPen); +} + +QStringList ViewMode::lines(const PlaylistBox::Item *item, + const QFontMetrics &fm, + int width) +{ + // Here 32 is a bit arbitrary, but that's the width of the icons in this + // mode and seems to a reasonable lower bound. + + if(width < 32) + return QStringList(); + + QString line = item->text(); + + QStringList l; + + while(!line.isEmpty()) { + int textLength = line.length(); + while(textLength > 0 && + fm.width(line.mid(0, textLength).stripWhiteSpace()) + + item->listView()->itemMargin() * 2 > width) + { + int i = line.findRev(QRegExp( "\\W"), textLength - 1); + if(i > 0) + textLength = i; + else + textLength--; + } + + l.append(line.mid(0, textLength).stripWhiteSpace()); + line = line.mid(textLength); + } + return l; +} + +/////////////////////////////////////////////////////////////////////////////// +// CompactViewMode +//////////////////////////////////////////////////////////////////////////////// + +CompactViewMode::CompactViewMode(PlaylistBox *b) : ViewMode(b) +{ + +} + +CompactViewMode::~CompactViewMode() +{ + +} + +void CompactViewMode::paintCell(PlaylistBox::Item *item, + QPainter *painter, + const QColorGroup &colorGroup, + int column, int width, int align) +{ + item->KListViewItem::paintCell(painter, colorGroup, column, width, align); + if(item == item->listView()->dropItem()) + paintDropIndicator(painter, width, item->height()); +} + +void CompactViewMode::setShown(bool shown) +{ + setVisible(shown); + + if(shown) { + updateIcons(16); + updateHeights(); + } +} + +void CompactViewMode::updateHeights() +{ + for(QListViewItemIterator it(playlistBox()); it.current(); ++it) + it.current()->setup(); +} + +//////////////////////////////////////////////////////////////////////////////// +// TreeViewMode +//////////////////////////////////////////////////////////////////////////////// + +TreeViewMode::TreeViewMode(PlaylistBox *b) : CompactViewMode(b), + m_treeViewItems(5003, false), m_dynamicListsFrozen(false), m_setup(false) +{ + +} + +TreeViewMode::~TreeViewMode() +{ + +} + +void TreeViewMode::setShown(bool show) +{ + CompactViewMode::setShown(show); + + playlistBox()->setRootIsDecorated(show); + + if(show) { + PlaylistBox::Item *collectionItem = PlaylistBox::Item::collectionItem(); + + if(!collectionItem) + return; + + if(collectionItem && m_searchCategories.isEmpty()) + setupDynamicPlaylists(); + else { + for(QDictIterator<PlaylistBox::Item> it(m_searchCategories); it.current(); ++it) + it.current()->setVisible(true); + } + + if(!m_setup) { + m_setup = true; + playlistBox()->setSorting(-1); + CollectionList::instance()->setupTreeViewEntries(this); + playlistBox()->setSorting(0); + playlistBox()->sort(); + } + } + else { + for(QDictIterator<PlaylistBox::Item> it(m_searchCategories); it.current(); ++it) + it.current()->setVisible(false); + } +} + +void TreeViewMode::removeItem(const QString &item, unsigned column) +{ + if(!m_setup) + return; + + QString itemKey; + if(column == PlaylistItem::ArtistColumn) + itemKey = "artists" + item; + else if(column == PlaylistItem::GenreColumn) + itemKey = "genres" + item; + else if(column == PlaylistItem::AlbumColumn) + itemKey = "albums" + item; + else { + kdWarning() << k_funcinfo << "Unhandled column type " << column << endl; + return; + } + + if(!m_treeViewItems.find(itemKey)) + return; + + TreeViewItemPlaylist *itemPlaylist = m_treeViewItems[itemKey]; + + if(m_dynamicListsFrozen) { + m_pendingItemsToRemove << itemKey; + return; + } + + m_treeViewItems.remove(itemKey); + itemPlaylist->deleteLater(); + emit signalPlaylistDestroyed(itemPlaylist); +} + +void TreeViewMode::addItems(const QStringList &items, unsigned column) +{ + if(!m_setup) + return; + + QString searchCategory; + if(column == PlaylistItem::ArtistColumn) + searchCategory = "artists"; + else if(column == PlaylistItem::GenreColumn) + searchCategory = "genres"; + else if(column == PlaylistItem::AlbumColumn) + searchCategory = "albums"; + else { + kdWarning() << k_funcinfo << "Unhandled column type " << column << endl; + return; + } + + QValueList<int> columns; + columns.append(column); + + PlaylistSearch::Component::MatchMode mode = PlaylistSearch::Component::ContainsWord; + if(column != PlaylistItem::ArtistColumn) + mode = PlaylistSearch::Component::Exact; + + PlaylistSearch::ComponentList components; + PlaylistList playlists; + playlists.append(CollectionList::instance()); + + QString itemKey, item; + PlaylistBox::Item *itemParent = m_searchCategories[searchCategory]; + + for(QStringList::ConstIterator it = items.begin(); it != items.end(); ++it) { + item = *it; + itemKey = searchCategory + item; + + if(m_treeViewItems.find(itemKey)) + continue; + + components.clear(); + components.append(PlaylistSearch::Component(item, false, columns, mode)); + + PlaylistSearch s(playlists, components, PlaylistSearch::MatchAny, false); + + TreeViewItemPlaylist *p = new TreeViewItemPlaylist(playlistBox(), s, item); + playlistBox()->setupPlaylist(p, "midi", itemParent); + m_treeViewItems.insert(itemKey, p); + } +} + +void TreeViewMode::setDynamicListsFrozen(bool frozen) +{ + m_dynamicListsFrozen = frozen; + + if(frozen) + return; + + QStringList categories; + categories << "artists" << "albums" << "genres"; + + for(QStringList::ConstIterator it = m_pendingItemsToRemove.begin(); + it != m_pendingItemsToRemove.end(); + ++it) + { + m_treeViewItems[*it]->deleteLater(); + m_treeViewItems.remove(*it); + } + + m_pendingItemsToRemove.clear(); +} + +void TreeViewMode::setupDynamicPlaylists() +{ + PlaylistBox::Item *i; + PlaylistBox::Item *collectionItem = PlaylistBox::Item::collectionItem(); + + i = new PlaylistBox::Item(collectionItem, "cdimage", i18n("Artists")); + m_searchCategories.insert("artists", i); + + i = new PlaylistBox::Item(collectionItem, "cdimage", i18n("Albums")); + m_searchCategories.insert("albums", i); + + i = new PlaylistBox::Item(collectionItem, "cdimage", i18n("Genres")); + m_searchCategories.insert("genres", i); +} + +//////////////////////////////////////////////////////////////////////////////// +// CoverManagerMode +//////////////////////////////////////////////////////////////////////////////// + +CoverManagerMode::CoverManagerMode(PlaylistBox *b) : ViewMode(b) +{ + +} + +#include "viewmode.moc" diff --git a/juk/viewmode.h b/juk/viewmode.h new file mode 100644 index 00000000..dcb5574c --- /dev/null +++ b/juk/viewmode.h @@ -0,0 +1,159 @@ +/*************************************************************************** + begin : Sat Jun 7 2003 + copyright : (C) 2003 - 2004 by Scott Wheeler, + email : 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. * + * * + ***************************************************************************/ + +#ifndef VIEWMODE_H +#define VIEWMODE_H + + +#include <qdict.h> + +#include "playlistbox.h" + +class QPainter; +class QColorGroup; + +class SearchPlaylist; + +class ViewMode : public QObject +{ + Q_OBJECT + +public: + ViewMode(PlaylistBox *b); + virtual ~ViewMode(); + + virtual QString name() const { return i18n("Default"); } + virtual void setShown(bool shown); + + virtual void paintCell(PlaylistBox::Item *item, + QPainter *painter, + const QColorGroup &colorGroup, + int column, int width, int align); + + virtual bool eventFilter(QObject *watched, QEvent *e); + void queueRefresh() { m_needsRefresh = true; } + + virtual void setupItem(PlaylistBox::Item *item) const; + + virtual void setupDynamicPlaylists() {} + /** + * If the view mode has dynamic lists, this function is used to temporarily + * freeze them to prevent them from deleting dynamic elements. + */ + virtual void setDynamicListsFrozen(bool /* frozen */) {} + + /** + * Used for dynamic view modes. This function will be called when \p items + * are added to \p column (even if the view mode hasn't been shown yet). + */ + virtual void addItems(const QStringList &items, unsigned column) + { + (void) items; + (void) column; + } + + /** + * Used for dynamic view modes. This function will be called when \p item + * is removed from \p column (even if the view mode hasn't been shown yet). + */ + virtual void removeItem(const QString &item, unsigned column) + { + (void) item; + (void) column; + } + +protected: + PlaylistBox *playlistBox() const { return m_playlistBox; } + bool visible() const { return m_visible; } + void setVisible(bool v) { m_visible = v; } + void updateIcons(int size); + virtual void updateHeights(); + static void paintDropIndicator(QPainter *painter, int width, int height); + +private: + static QStringList lines(const PlaylistBox::Item *item, const QFontMetrics &fm, int width); + + PlaylistBox *m_playlistBox; + bool m_visible; + bool m_needsRefresh; + QMap<PlaylistBox::Item *, QStringList> m_lines; + static const int border = 4; +}; + +//////////////////////////////////////////////////////////////////////////////// + +class CompactViewMode : public ViewMode +{ +public: + CompactViewMode(PlaylistBox *b); + virtual ~CompactViewMode(); + + virtual QString name() const { return i18n("Compact"); } + virtual void setShown(bool shown); + + virtual void paintCell(PlaylistBox::Item *item, + QPainter *painter, + const QColorGroup &colorGroup, + int column, int width, int align); + + virtual void setupItem(PlaylistBox::Item *item) const { item->KListViewItem::setup(); } +protected: + virtual void updateHeights(); +}; + +//////////////////////////////////////////////////////////////////////////////// + +class TreeViewItemPlaylist; + +class TreeViewMode : public CompactViewMode +{ + Q_OBJECT + +public: + TreeViewMode(PlaylistBox *l); + virtual ~TreeViewMode(); + + virtual QString name() const { return i18n("Tree"); } + virtual void setShown(bool shown); + virtual void setupDynamicPlaylists(); + virtual void setDynamicListsFrozen(bool frozen); + + virtual void removeItem(const QString &item, unsigned column); + virtual void addItems(const QStringList &items, unsigned column); + +signals: + void signalPlaylistDestroyed(Playlist*); + +private: + QDict<PlaylistBox::Item> m_searchCategories; + QDict<TreeViewItemPlaylist> m_treeViewItems; + QStringList m_pendingItemsToRemove; + bool m_dynamicListsFrozen; + bool m_setup; +}; + +//////////////////////////////////////////////////////////////////////////////// + +class CoverManagerMode : public ViewMode +{ + Q_OBJECT + +public: + CoverManagerMode(PlaylistBox *b); + virtual QString name() const { return i18n("Cover Manager"); } + //virtual void setShown(bool shown); +}; + +#endif diff --git a/juk/webimagefetcher.cpp b/juk/webimagefetcher.cpp new file mode 100644 index 00000000..3e806bff --- /dev/null +++ b/juk/webimagefetcher.cpp @@ -0,0 +1,224 @@ +/*************************************************************************** + copyright : (C) 2004 Nathan Toone <nathan@toonetown.com> + copyright : (C) 2007 Michael Pyne <michael.pyne@kdemail.com> +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <qhttp.h> +#include <qdom.h> +#include <qwaitcondition.h> + +#include <kapplication.h> +#include <kstatusbar.h> +#include <kdebug.h> +#include <kmainwindow.h> +#include <klocale.h> +#include <kinputdialog.h> +#include <kurl.h> + +#include "covermanager.h" +#include "webimagefetcher.h" +#include "webimagefetcherdialog.h" +#include "tag.h" + +WebImage::WebImage() +{ +} + +WebImage::WebImage(const QString &imageURL, const QString &thumbURL, + int width, int height) : + m_imageURL(imageURL), + m_thumbURL(thumbURL), + m_size(QString("\n%1 x %2").arg(width).arg(height)) +{ +} + + +WebImageFetcher::WebImageFetcher(QObject *parent) + : QObject(parent), + m_connection(new QHttp(this)), + m_connectionId(-1), + m_dialog(0) +{ + connect(m_connection, SIGNAL(requestFinished(int,bool)), SLOT(slotWebRequestFinished(int,bool))); +} + +WebImageFetcher::~WebImageFetcher() +{ + delete m_dialog; +} + +void WebImageFetcher::setFile(const FileHandle &file) +{ + m_file = file; + m_searchString = QString(file.tag()->artist() + ' ' + file.tag()->album()); + + if(m_dialog) + m_dialog->setFile(file); +} + +void WebImageFetcher::abortSearch() +{ + m_connection->abort(); +} + +void WebImageFetcher::chooseCover() +{ + slotLoadImageURLs(); +} + +void WebImageFetcher::slotLoadImageURLs() +{ + m_imageList.clear(); + + KURL url("http://search.yahooapis.com/ImageSearchService/V1/imageSearch"); + url.addQueryItem("appid", "org.kde.juk/kde3"); + url.addQueryItem("query", m_searchString); + url.addQueryItem("results", "25"); + + kdDebug(65432) << "Using request " << url.encodedPathAndQuery() << endl; + + m_connection->setHost(url.host()); + m_connectionId = m_connection->get(url.encodedPathAndQuery()); + + // Wait for the results... +} + +void WebImageFetcher::slotWebRequestFinished(int id, bool error) +{ + if(id != m_connectionId) + return; + + if(error) { + kdError(65432) << "Error reading image results from Yahoo!\n"; + kdError(65432) << m_connection->errorString() << endl; + return; + } + + QDomDocument results("ResultSet"); + + QString errorStr; + int errorCol, errorLine; + if(!results.setContent(m_connection->readAll(), &errorStr, &errorLine, &errorCol)) { + kdError(65432) << "Unable to create XML document from Yahoo results.\n"; + kdError(65432) << "Line " << errorLine << ", " << errorStr << endl; + return; + } + + QDomNode n = results.documentElement(); + + bool hasNoResults = false; + + if(n.isNull()) { + kdDebug(65432) << "No document root in XML results??\n"; + hasNoResults = true; + } + else { + QDomElement result = n.toElement(); + if(result.attribute("totalResultsReturned").toInt() == 0) + kdDebug(65432) << "Search returned " << result.attribute("totalResultsAvailable") << " results.\n"; + + if(result.isNull() || !result.hasAttribute("totalResultsReturned") || + result.attribute("totalResultsReturned").toInt() == 0) + { + hasNoResults = true; + } + } + + if(hasNoResults) + { + kdDebug(65432) << "Search returned no results.\n"; + requestNewSearchTerms(true /* no results */); + return; + } + + // Go through each of the top (result) nodes + + n = n.firstChild(); + while(!n.isNull()) { + QDomNode resultUrl = n.namedItem("Url"); + QDomNode thumbnail = n.namedItem("Thumbnail"); + QDomNode height = n.namedItem("Height"); + QDomNode width = n.namedItem("Width"); + + // We have the necessary info, move to next node before we forget. + n = n.nextSibling(); + + if(resultUrl.isNull() || thumbnail.isNull() || height.isNull() || width.isNull()) { + kdError(65432) << "Invalid result returned, skipping.\n"; + continue; + } + + m_imageList.append( + WebImage( + resultUrl.toElement().text(), + thumbnail.namedItem("Url").toElement().text(), + width.toElement().text().toInt(), + height.toElement().text().toInt() + ) + ); + } + + // Have results, show them and pick one. + + if(!m_dialog) { + m_dialog = new WebImageFetcherDialog(m_imageList, m_file, 0); + m_dialog->setModal(true); + + connect(m_dialog, SIGNAL(coverSelected()), SLOT(slotCoverChosen())); + connect(m_dialog, SIGNAL(newSearchRequested()), SLOT(slotNewSearch())); + } + + m_dialog->refreshScreen(m_imageList); + m_dialog->show(); +} + +void WebImageFetcher::slotCoverChosen() +{ + QPixmap pixmap = m_dialog->result(); + if(pixmap.isNull()) { + kdError(65432) << "Selected pixmap is null for some reason.\n"; + return; + } + + kdDebug(65432) << "Adding new cover for " << m_file.tag()->fileName() << endl; + coverKey newId = CoverManager::addCover(pixmap, m_file.tag()->artist(), m_file.tag()->album()); + emit signalCoverChanged(newId); +} + +void WebImageFetcher::slotNewSearch() +{ + requestNewSearchTerms(); +} + +void WebImageFetcher::displayWaitMessage() +{ + KStatusBar *statusBar = static_cast<KMainWindow *>(kapp->mainWidget())->statusBar(); + statusBar->message(i18n("Searching for Images. Please Wait...")); + slotLoadImageURLs(); + statusBar->clear(); +} + +void WebImageFetcher::requestNewSearchTerms(bool noResults) +{ + bool ok; + QString search = KInputDialog::getText(i18n("Cover Downloader"), + noResults ? + i18n("No matching images found, please enter new search terms:") : + i18n("Enter new search terms:"), + m_searchString, &ok); + if(ok && !search.isEmpty()) { + m_searchString = search; + displayWaitMessage(); // This kicks off the new search. + } +} + +#include "webimagefetcher.moc" diff --git a/juk/webimagefetcher.h b/juk/webimagefetcher.h new file mode 100644 index 00000000..796e205e --- /dev/null +++ b/juk/webimagefetcher.h @@ -0,0 +1,93 @@ +/*************************************************************************** + copyright : (C) 2004 Nathan Toone + email : nathan@toonetown.com + copyright : (C) 2007 Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef WEBIMAGEFETCHER_H +#define WEBIMAGEFETCHER_H + +#include <kdialogbase.h> + +#include <qpixmap.h> +#include <qstringlist.h> +#include <qregexp.h> + +#include "filehandle.h" + +class KURL; + +class QHttp; + +class WebImageFetcherDialog; + +class WebImage +{ +public: + WebImage(); + + WebImage(const QString &imageURL, + const QString &thumbURL, + int width, int height); + + QString imageURL() const { return m_imageURL; } + QString thumbURL() const { return m_thumbURL; } + QString size() const { return m_size; } + +private: + QString m_imageURL; + QString m_thumbURL; + QString m_size; +}; + +typedef QValueList<WebImage> WebImageList; + +class WebImageFetcher : public QObject +{ + Q_OBJECT + +public: + WebImageFetcher(QObject *parent); + ~WebImageFetcher(); + + void setFile(const FileHandle &file); + void chooseCover(); + +public slots: + void abortSearch(); + +signals: + void signalNewSearch(WebImageList &images); + void signalCoverChanged(int coverId); + +private: + void displayWaitMessage(); + void requestNewSearchTerms(bool noResults = false); + +private slots: + void slotLoadImageURLs(); + void slotWebRequestFinished(int id, bool error); + void slotCoverChosen(); + void slotNewSearch(); + +private: + FileHandle m_file; + QString m_searchString; + QString m_loadedQuery; + WebImageList m_imageList; + uint m_selectedIndex; + QHttp *m_connection; + int m_connectionId; + WebImageFetcherDialog *m_dialog; +}; +#endif diff --git a/juk/webimagefetcherdialog.cpp b/juk/webimagefetcherdialog.cpp new file mode 100644 index 00000000..cc14ed61 --- /dev/null +++ b/juk/webimagefetcherdialog.cpp @@ -0,0 +1,235 @@ +/*************************************************************************** + copyright : (C) 2004 Nathan Toone + email : nathan@toonetown.com + copyright : (C) 2007 Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include <kapplication.h> +#include <kio/netaccess.h> +#include <klocale.h> +#include <kdebug.h> +#include <kmessagebox.h> +#include <krun.h> +#include <kcombobox.h> +#include <kiconloader.h> +#include <kurllabel.h> + +#include <qvbox.h> +#include <qlayout.h> +#include <qimage.h> +#include <qlabel.h> +#include <qpushbutton.h> +#include <qeventloop.h> + +#include "webimagefetcherdialog.h" +#include "tag.h" + +WebImageFetcherDialog::WebImageFetcherDialog(const WebImageList &imageList, + const FileHandle &file, + QWidget *parent) : + KDialogBase(parent, "internet_image_fetcher", true, QString::null, + Ok | Cancel | User1 , NoDefault, true), + m_pixmap(QPixmap()), + m_imageList(imageList), + m_file(file) +{ + disableResize(); + + QWidget *mainBox = new QWidget(this); + QBoxLayout *mainLayout = new QVBoxLayout(mainBox); + + m_iconWidget = new KIconView(mainBox); + m_iconWidget->setResizeMode(QIconView::Adjust); + m_iconWidget->setSpacing(10); + m_iconWidget->setFixedSize(500,550); + m_iconWidget->arrangeItemsInGrid(); + m_iconWidget->setItemsMovable(false); + mainLayout->addWidget(m_iconWidget); + connect(m_iconWidget, SIGNAL(executed(QIconViewItem *)), + this, SLOT(slotOk())); + + // Before changing the code below be sure to check the attribution terms + // of the Yahoo Image Search API. + // http://developer.yahoo.com/attribution/ + KURLLabel *logoLabel = new KURLLabel(mainBox); + logoLabel->setURL("http://developer.yahoo.com/about/"); + logoLabel->setPixmap(UserIcon("yahoo_credit")); + logoLabel->setMargin(15); // Allow large margin per attribution terms. + logoLabel->setUseTips(true); // Show URL in tooltip. + connect(logoLabel, SIGNAL(leftClickedURL(const QString &)), + SLOT(showCreditURL(const QString &))); + + QBoxLayout *creditLayout = new QHBoxLayout(mainLayout); + creditLayout->addStretch(); // Left spacer + creditLayout->addWidget(logoLabel); + creditLayout->addStretch(); // Right spacer + + setMainWidget(mainBox); + setButtonText(User1, i18n("New Search")); +} + +WebImageFetcherDialog::~WebImageFetcherDialog() +{ +} + +void WebImageFetcherDialog::showCreditURL(const QString &url) +{ + // Don't use static member since I'm sure that someday knowing my luck + // Yahoo will change their mimetype they serve. + (void) new KRun(KURL(url), topLevelWidget()); +} + +void WebImageFetcherDialog::setLayout() +{ + setCaption(QString("%1 - %2 (%3)") + .arg(m_file.tag()->artist()) + .arg(m_file.tag()->album()) + .arg(m_imageList.size())); + + m_iconWidget->clear(); + for(uint i = 0; i < m_imageList.size(); i++) + new CoverIconViewItem(m_iconWidget, m_imageList[i]); + + adjustSize(); +} + +void WebImageFetcherDialog::setImageList(const WebImageList &imageList) +{ + m_imageList = imageList; +} + +void WebImageFetcherDialog::setFile(const FileHandle &file) +{ + m_file = file; +} + +//////////////////////////////////////////////////////////////////////////////// +// public slots +//////////////////////////////////////////////////////////////////////////////// + +void WebImageFetcherDialog::refreshScreen(WebImageList &imageList) +{ + setImageList(imageList); + setLayout(); +} + +int WebImageFetcherDialog::exec() +{ + setLayout(); + return KDialogBase::exec(); +} + +void WebImageFetcherDialog::slotOk() +{ + uint selectedIndex = m_iconWidget->index(m_iconWidget->currentItem()); + m_pixmap = pixmapFromURL(m_imageList[selectedIndex].imageURL()); + + if(m_pixmap.isNull()) { + KMessageBox::sorry(this, + i18n("The cover you have selected is unavailable. Please select another."), + i18n("Cover Unavailable")); + QPixmap blankPix; + blankPix.resize(80, 80); + blankPix.fill(); + m_iconWidget->currentItem()->setPixmap(blankPix, true, true); + return; + } + + accept(); + emit coverSelected(); +} + +void WebImageFetcherDialog::slotCancel() +{ + m_pixmap = QPixmap(); + reject(); +} + +void WebImageFetcherDialog::slotUser1() +{ + m_pixmap = QPixmap(); + accept(); + emit newSearchRequested(); +} + +QPixmap WebImageFetcherDialog::fetchedImage(uint index) const +{ + return (index > m_imageList.count()) ? QPixmap() : pixmapFromURL(m_imageList[index].imageURL()); +} + +QPixmap WebImageFetcherDialog::pixmapFromURL(const KURL &url) const +{ + QString file; + + if(KIO::NetAccess::download(url, file, 0)) { + QPixmap pixmap = QPixmap(file); + KIO::NetAccess::removeTempFile(file); + return pixmap; + } + KIO::NetAccess::removeTempFile(file); + return QPixmap(); +} + +//////////////////////////////////////////////////////////////////////////////// +// CoverIconViewItem +//////////////////////////////////////////////////////////////////////////////// + +CoverIconViewItem::CoverIconViewItem(QIconView *parent, const WebImage &image) : + QObject(parent), KIconViewItem(parent, parent->lastItem(), image.size()), m_job(0) +{ + // Set up the iconViewItem + + QPixmap mainMap; + mainMap.resize(80, 80); + mainMap.fill(); + setPixmap(mainMap, true, true); + + // Start downloading the image. + + m_job = KIO::get(image.thumbURL(), false, false); + connect(m_job, SIGNAL(result(KIO::Job *)), this, SLOT(imageResult(KIO::Job *))); + connect(m_job, SIGNAL(data(KIO::Job *, const QByteArray &)), + this, SLOT(imageData(KIO::Job *, const QByteArray &))); +} + +CoverIconViewItem::~CoverIconViewItem() +{ + if(m_job) { + m_job->kill(); + + // Drain results issued by KIO before being deleted, + // and before deleting the job. + kapp->eventLoop()->processEvents(QEventLoop::ExcludeUserInput); + + delete m_job; + } +} + +void CoverIconViewItem::imageData(KIO::Job *, const QByteArray &data) +{ + int currentSize = m_buffer.size(); + m_buffer.resize(currentSize + data.size(), QGArray::SpeedOptim); + memcpy(&(m_buffer.data()[currentSize]), data.data(), data.size()); +} + +void CoverIconViewItem::imageResult(KIO::Job *job) +{ + if(job->error()) + return; + + QPixmap iconImage(m_buffer); + iconImage = QImage(iconImage.convertToImage()).smoothScale(80, 80, QImage::ScaleMin); + setPixmap(iconImage, true, true); +} + +#include "webimagefetcherdialog.moc" diff --git a/juk/webimagefetcherdialog.h b/juk/webimagefetcherdialog.h new file mode 100644 index 00000000..a4424a2f --- /dev/null +++ b/juk/webimagefetcherdialog.h @@ -0,0 +1,90 @@ +/*************************************************************************** + copyright : (C) 2004 Nathan Toone + email : nathan@toonetown.com + copyright : (C) 2007 Michael Pyne + email : michael.pyne@kdemail.net +***************************************************************************/ + +/*************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#ifndef WEBIMAGEFETCHERDIALOG_H +#define WEBIMAGEFETCHERDIALOG_H + +#include <kiconview.h> +#include <kio/job.h> + +#include "webimagefetcher.h" + +class KURL; + +class WebImageFetcherDialog : public KDialogBase +{ + Q_OBJECT + +public: + WebImageFetcherDialog(const WebImageList &urlList, + const FileHandle &file, + QWidget *parent = 0); + + virtual ~WebImageFetcherDialog(); + + QPixmap result() const { return m_pixmap; } + + void setLayout(); + void setImageList(const WebImageList &urlList); + void setFile(const FileHandle &file); + +signals: + void coverSelected(); + void newSearchRequested(); + +public slots: + int exec(); + void refreshScreen(WebImageList &list); + +protected slots: + void slotOk(); + void slotCancel(); + void slotUser1(); + void showCreditURL(const QString &url); + +private: + QPixmap fetchedImage(uint index) const; + QPixmap pixmapFromURL(const KURL &url) const; + + QPixmap m_pixmap; + WebImageList m_imageList; + KIconView *m_iconWidget; + FileHandle m_file; +}; + +namespace KIO +{ + class TransferJob; +} + +class CoverIconViewItem : public QObject, public KIconViewItem +{ + Q_OBJECT + +public: + CoverIconViewItem(QIconView *parent, const WebImage &image); + ~CoverIconViewItem(); + +private slots: + void imageData(KIO::Job *job, const QByteArray &data); + void imageResult(KIO::Job* job); + +private: + QByteArray m_buffer; + QGuardedPtr<KIO::TransferJob> m_job; +}; + +#endif |