diff --git a/src/QStringx.cpp b/src/QStringx.cpp index 92592e2ba9..0872777a8b 100644 --- a/src/QStringx.cpp +++ b/src/QStringx.cpp @@ -1,361 +1,361 @@ /**************************************************************************************** * Copyright (c) 2004 Shintaro Matsuoka * * Copyright (c) 2006 Martin Aumueller * * Copyright (c) 2011 Sergey Ivanov <123kash@gmail.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. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "QStringx.h" Amarok::QStringx::QStringx() { } Amarok::QStringx::QStringx( QChar ch ) : QString( ch ) { } Amarok::QStringx::QStringx( const QString &s ) : QString( s ) { } Amarok::QStringx::QStringx( const QByteArray &ba ) : QString( ba ) { } Amarok::QStringx::QStringx( const QChar *unicode, uint length ) : QString( unicode, length ) { } Amarok::QStringx::QStringx( const char *str ) : QString( str ) { } Amarok::QStringx::~QStringx() { } QString Amarok::QStringx::args( const QStringList &args ) const { const QStringList text = (*this).split( QRegExp( "%\\d+" ), QString::KeepEmptyParts ); QList::ConstIterator itrText = text.constBegin(); QList::ConstIterator itrArgs = args.constBegin(); QList::ConstIterator endText = text.constEnd(); QList::ConstIterator endArgs = args.constEnd(); QString merged = (*itrText); ++itrText; while( itrText != endText && itrArgs != endArgs ) { merged += (*itrArgs) + (*itrText); ++itrText; ++itrArgs; } Q_ASSERT( itrText == text.end() || itrArgs == args.end() ); return merged; } QString Amarok::QStringx::namedArgs( const QMap &args, bool opt ) const { // Screen all kindes of brackets and format string with namedOptArgs. QString formatString = *this; formatString.replace( QRegExp( "([\\[\\]{}])" ),"\\\\1" ); // Legacy code returned empty string if any token was empty, so do the same if( opt ) formatString = QLatin1Char( '{' ) + formatString + QLatin1Char( '}' ); - QStringx fmtx( formatString ); + QStringx fmtx( formatString ); return fmtx.namedOptArgs( args ); } QString Amarok::QStringx::namedOptArgs( const QMap &args ) const { int pos = 0; return parse( &pos, args ); } Amarok::QStringx::CharType Amarok::QStringx::testChar( int *pos ) const { if( *pos >= length() ) return CTNone; QChar c = this->at( *pos ); if( c == QLatin1Char( '\\' ) ) { ( *pos )++; return ( *pos >= length() ) ? CTNone : CTRegular; } if( c == QLatin1Char( '{' ) ) return CTBraceOpen; if( c == QLatin1Char( '}' ) ) return CTBraceClose; if( c == QLatin1Char( '[' ) ) return CTBracketOpen; if( c == QLatin1Char( ':' ) ) return CTBracketSeparator; if( c == QLatin1Char( ']' ) ) return CTBracketClose; if( c == QLatin1Char( '%' ) ) return CTToken; return CTRegular; } QString Amarok::QStringx::parseToken( int *pos, const QMap &dict ) const { if( testChar( pos ) != CTToken ) return QString(); ( *pos )++; CharType ct = testChar( pos ); QString key; while( ct == CTRegular ) { key += this->at( *pos ); ( *pos )++; ct = testChar( pos ); } if( ct == CTToken ) { ( *pos )++; return dict.value( key ); } *pos -= key.length(); return QLatin1String( "%" ); } QString Amarok::QStringx::parseBraces( int *pos, const QMap &dict ) const { if( testChar( pos ) != CTBraceOpen ) return QString(); ( *pos )++; int retPos = *pos; QString result; bool isPritable = true; CharType ct = testChar( pos ); while( ct != CTNone && ct != CTBraceClose ) { switch( ct ) { case CTBraceOpen: { result += parseBraces( pos, dict ); break; } case CTBracketOpen: { result += parseBrackets( pos, dict ); break; } case CTToken: { QString part = parseToken( pos, dict ); if( part.isEmpty() ) isPritable = false; result += part; break; } default: { result += this->at( *pos ); ( *pos )++; } } ct = testChar( pos ); } if( ct == CTBraceClose ) { ( *pos )++; if( isPritable ) return result; return QString(); } *pos = retPos; return QLatin1String( "{" ); } QString Amarok::QStringx::parseBrackets( int *pos, const QMap &dict ) const { if( testChar( pos ) != CTBracketOpen ) return QString(); ( *pos )++; // Check if next char is % if( testChar( pos ) != CTToken ) return QLatin1String( "[" ); int retPos = *pos; ( *pos )++; // Parse token manuly (not by calling parseToken), because we need token name. CharType ct = testChar( pos ); QString key; while( ct == CTRegular ) { key += this->at( *pos ); ( *pos )++; ct = testChar( pos ); } if( ct != CTToken || key.isEmpty() ) { *pos = retPos; return QLatin1String( "[" ); } ( *pos )++; QString replacement; // Parse replacement string if we have one if( testChar( pos ) == CTBracketSeparator ) { ( *pos )++; ct = testChar( pos ); while( ct != CTNone && ct != CTBracketClose ) { switch( ct ) { case CTBraceOpen: { replacement += parseBraces( pos, dict ); break; } case CTBracketOpen: { replacement += parseBrackets( pos, dict ); break; } case CTToken: { replacement += parseToken( pos, dict );; break; } default: { replacement += this->at( *pos ); ( *pos )++; } } ct = testChar( pos ); } if( ct == CTNone ) { *pos = retPos; return QLatin1String( "[" ); } } if( testChar( pos ) == CTBracketClose ) { ( *pos )++; if( !dict.value( key ).isEmpty() ) return dict.value( key ); if( !replacement.isEmpty() ) return replacement; if( !dict.value( QLatin1String( "default_" ) + key ).isEmpty() ) return dict.value( QLatin1String( "default_" ) + key ); return QLatin1String( "Unknown " ) + key; } *pos = retPos; return QLatin1String( "[" ); } QString Amarok::QStringx::parse( int *pos, const QMap &dict ) const { CharType ct = testChar( pos ); QString result; while( ct != CTNone ) { switch( ct ) { case CTBraceOpen: { result += parseBraces( pos, dict ); break; } case CTBracketOpen: { result += parseBrackets( pos, dict ); break; } case CTToken: { result += parseToken( pos, dict ); break; } default: { result += this->at( *pos ); ( *pos )++; } } ct = testChar( pos ); } return result; } diff --git a/src/browsers/playlistbrowser/PodcastModel.cpp b/src/browsers/playlistbrowser/PodcastModel.cpp index 6f9b935419..cf6e1268fe 100644 --- a/src/browsers/playlistbrowser/PodcastModel.cpp +++ b/src/browsers/playlistbrowser/PodcastModel.cpp @@ -1,376 +1,376 @@ /**************************************************************************************** * Copyright (c) 2007-2010 Bart Cerneels * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "PodcastModel.h" #include "AmarokMimeData.h" #include "context/popupdropper/libpud/PopupDropper.h" #include "context/popupdropper/libpud/PopupDropperItem.h" #include "core/podcasts/PodcastImageFetcher.h" #include "core/podcasts/PodcastMeta.h" #include "core/support/Debug.h" #include "playlistmanager/PlaylistManager.h" #include "playlistmanager/SyncedPodcast.h" #include "PodcastCategory.h" #include "SvgHandler.h" #include #include "widgets/PrettyTreeRoles.h" #include #include #include #include #include using namespace Podcasts; namespace The { PlaylistBrowserNS::PodcastModel* podcastModel() { return PlaylistBrowserNS::PodcastModel::instance(); } } PlaylistBrowserNS::PodcastModel* PlaylistBrowserNS::PodcastModel::s_instance = 0; PlaylistBrowserNS::PodcastModel* PlaylistBrowserNS::PodcastModel::instance() { return s_instance ? s_instance : new PodcastModel(); } void PlaylistBrowserNS::PodcastModel::destroy() { if ( s_instance ) { delete s_instance; s_instance = 0; } } PlaylistBrowserNS::PodcastModel::PodcastModel() : PlaylistBrowserModel( PlaylistManager::PodcastChannel ) { s_instance = this; } bool PlaylistBrowserNS::PodcastModel::isOnDisk( PodcastEpisodePtr episode ) const { bool isOnDisk = false; KUrl episodeFile( episode->localUrl() ); if( !episodeFile.isEmpty() ) { isOnDisk = QFileInfo( episodeFile.toLocalFile() ).exists(); // reset localUrl because the file is not there. // FIXME: changing a podcast in innoncent-looking getter method is convoluted if( !isOnDisk ) episode->setLocalUrl( KUrl() ); } return isOnDisk; } QVariant PlaylistBrowserNS::PodcastModel::icon( const PodcastChannelPtr &channel ) const { QStringList emblems; //TODO: only check visible episodes. For now those are all returned by episodes(). foreach( const Podcasts::PodcastEpisodePtr ep, channel->episodes() ) { if( ep->isNew() ) { emblems << "rating"; break; } } if( channel->hasImage() ) { QSize size( channel->image().size() ); QPixmap pixmap( 32, 32 ); pixmap.fill( Qt::transparent ); size.scale( 32, 32, Qt::KeepAspectRatio ); int x = 32 / 2 - size.width() / 2; int y = 32 / 2 - size.height() / 2; QPainter p( &pixmap ); p.drawPixmap( x, y, QPixmap::fromImage( channel->image().scaled( size, Qt::KeepAspectRatio, Qt::SmoothTransformation ) ) ); // if it's a new episode draw the overlay: if( !emblems.isEmpty() ) // draw the overlay the same way KIconLoader does: p.drawPixmap( 2, 32 - 16 - 2, KIcon( "rating" ).pixmap( 16, 16 ) ); p.end(); return pixmap; } else return KIcon( "podcast-amarok", 0, emblems ).pixmap( 32, 32 ); } QVariant PlaylistBrowserNS::PodcastModel::icon( const PodcastEpisodePtr &episode ) const { QStringList emblems; if( isOnDisk( episode ) ) emblems << "go-down"; if( episode->isNew() ) return KIcon( "rating", 0, emblems ).pixmap( 24, 24 ); else return KIcon( "podcast-amarok", 0, emblems ).pixmap( 24, 24 ); } QVariant PlaylistBrowserNS::PodcastModel::data( const QModelIndex &idx, int role ) const { if( !idx.isValid() ) return PlaylistBrowserModel::data( idx, role ); if( IS_TRACK(idx) ) return episodeData( episodeForIndex( idx ), idx, role ); else return channelData( channelForIndex( idx ), idx, role ); } QVariant PlaylistBrowserNS::PodcastModel::channelData( const PodcastChannelPtr &channel, const QModelIndex &idx, int role ) const { if( !channel ) return QVariant(); switch( role ) { case Qt::DisplayRole: case Qt::ToolTipRole: switch( idx.column() ) { case PlaylistBrowserModel::PlaylistItemColumn: return channel->title(); case SubtitleColumn: return channel->subtitle(); case AuthorColumn: return channel->author(); case KeywordsColumn: return channel->keywords(); case ImageColumn: { KUrl imageUrl( PodcastImageFetcher::cachedImagePath( channel ) ); if( !QFile( imageUrl.toLocalFile() ).exists() ) imageUrl = channel->imageUrl(); return imageUrl; } case DateColumn: - channel->subscribeDate(); + return channel->subscribeDate(); case IsEpisodeColumn: return false; } break; case PrettyTreeRoles::ByLineRole: if( idx.column() == PlaylistBrowserModel::ProviderColumn ) { Playlists::PlaylistProvider *provider = providerForIndex( idx ); if( provider ) return i18ncp( "number of podcasts from one source", "One Channel", "%1 channels", provider->playlists().count() ); } if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn ) return channel->description(); break; case PrettyTreeRoles::HasCoverRole: return idx.column() == PlaylistBrowserModel::PlaylistItemColumn; case Qt::DecorationRole: if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn ) return icon( channel ); break; } return PlaylistBrowserModel::data( idx, role ); } QVariant PlaylistBrowserNS::PodcastModel::episodeData( const PodcastEpisodePtr &episode, const QModelIndex &idx, int role ) const { if( !episode ) return QVariant(); switch( role ) { case Qt::DisplayRole: case Qt::ToolTipRole: switch( idx.column() ) { case PlaylistBrowserModel::PlaylistItemColumn: return episode->title(); case SubtitleColumn: return episode->subtitle(); case AuthorColumn: return episode->author(); case KeywordsColumn: return episode->keywords(); case FilesizeColumn: return episode->filesize(); case DateColumn: return episode->pubDate(); case IsEpisodeColumn: return true; } break; case PrettyTreeRoles::ByLineRole: if( idx.column() == PlaylistBrowserModel::ProviderColumn ) { Playlists::PlaylistProvider *provider = providerForIndex( idx ); if( provider ) return i18ncp( "number of podcasts from one source", "One Channel", "%1 channels", provider->playlists().count() ); } if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn ) return episode->description(); break; case PrettyTreeRoles::HasCoverRole: return ( idx.column() == PlaylistBrowserModel::PlaylistItemColumn ); case Qt::DecorationRole: if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn ) return icon( episode ); break; case EpisodeIsNewRole: return episode->isNew(); } return PlaylistBrowserModel::data( idx, role ); } bool PlaylistBrowserNS::PodcastModel::setData( const QModelIndex &idx, const QVariant &value, int role ) { PodcastEpisodePtr episode = episodeForIndex( idx ); if( !episode || !value.canConvert() || role != EpisodeIsNewRole ) { return PlaylistBrowserModel::setData( idx, value, role ); } bool checked = value.toBool(); episode->setNew( checked ); if( checked ) emit episodeMarkedAsNew( episode ); emit dataChanged( idx, idx ); return true; } int PlaylistBrowserNS::PodcastModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) return ColumnCount; } QVariant PlaylistBrowserNS::PodcastModel::headerData( int section, Qt::Orientation orientation, int role) const { if( orientation == Qt::Horizontal && role == Qt::DisplayRole ) { switch( section ) { case 0: return i18n("Type"); case 1: return i18n("Title"); case 2: return i18n("Summary"); default: return QVariant(); } } return QVariant(); } void PlaylistBrowserNS::PodcastModel::addPodcast() { debug() << "adding Podcast"; //TODO: request the user to which PodcastProvider he wants to add it in case // of multiple (enabled) Podcast Providers. Podcasts::PodcastProvider *podcastProvider = The::playlistManager()->defaultPodcasts(); if( podcastProvider ) { bool ok; QString url = QInputDialog::getText( 0, i18n("Add Podcast"), i18n("Enter RSS 1.0/2.0 or Atom feed URL:"), QLineEdit::Normal, QString(), &ok ); if( ok && !url.isEmpty() ) { // user entered something and pressed OK podcastProvider->addPodcast( Podcasts::PodcastProvider::toFeedUrl( url.trimmed() ) ); } else { // user entered nothing or pressed Cancel debug() << "invalid input or cancel"; } } else { debug() << "PodcastChannel provider is null"; } } void PlaylistBrowserNS::PodcastModel::refreshPodcasts() { foreach( Playlists::PlaylistProvider *provider, The::playlistManager()->providersForCategory( PlaylistManager::PodcastChannel ) ) { PodcastProvider *podcastProvider = dynamic_cast( provider ); if( podcastProvider ) podcastProvider->updateAll(); } } Podcasts::PodcastChannelPtr PlaylistBrowserNS::PodcastModel::channelForIndex( const QModelIndex &idx ) const { return Podcasts::PodcastChannelPtr::dynamicCast( playlistFromIndex( idx ) ); } Podcasts::PodcastEpisodePtr PlaylistBrowserNS::PodcastModel::episodeForIndex( const QModelIndex &idx ) const { return Podcasts::PodcastEpisodePtr::dynamicCast( trackFromIndex( idx ) ); } Meta::TrackList PlaylistBrowserNS::PodcastModel::podcastEpisodesToTracks( Podcasts::PodcastEpisodeList episodes ) { Meta::TrackList tracks; foreach( Podcasts::PodcastEpisodePtr episode, episodes ) tracks << Meta::TrackPtr::staticCast( episode ); return tracks; } diff --git a/src/context/applets/labels/LabelsApplet.cpp b/src/context/applets/labels/LabelsApplet.cpp index 90a50047f3..be48faed2f 100644 --- a/src/context/applets/labels/LabelsApplet.cpp +++ b/src/context/applets/labels/LabelsApplet.cpp @@ -1,875 +1,875 @@ /**************************************************************************************** * Copyright (c) 2007 Leo Franchi * * Copyright (c) 2009 simon.esneault * * Copyright (c) 2010 Daniel Faust * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "LabelsApplet" #include "LabelsApplet.h" #include "App.h" #include "EngineController.h" #include "PaletteHandler.h" #include "amarokurls/AmarokUrl.h" #include "context/Theme.h" #include "context/applets/labels/LabelGraphicsItem.h" #include "context/widgets/AppletHeader.h" #include "core/meta/Meta.h" #include "core/support/Debug.h" #include #include #include #include #include #include #include #include #define LabelsAppletMaxLabelLength 40 // if a downloaded label is longer than this, don't show it LabelsApplet::LabelsApplet( QObject *parent, const QVariantList &args ) : Context::Applet( parent, args ), m_lastLabelSize( QSizeF(0,0) ), m_lastLabelBottomAdded( false ) { setHasConfigurationInterface( true ); } LabelsApplet::~LabelsApplet() { DEBUG_BLOCK qDeleteAll( m_labelItems ); m_labelItems.clear(); qDeleteAll( m_labelAnimations ); m_labelAnimations.clear(); qDeleteAll( m_labelItemsToDelete ); m_labelItemsToDelete.clear(); qDeleteAll( m_labelAnimationsToDelete ); m_labelAnimationsToDelete.clear(); if( m_reloadIcon ) delete m_reloadIcon.data(); if( m_settingsIcon ) delete m_settingsIcon.data(); } void LabelsApplet::init() { DEBUG_BLOCK // Call the base implementation. Context::Applet::init(); setBackgroundHints( Plasma::Applet::NoBackground ); // properly set the size, asking for the whole cv size. resize( 500, -1 ); // this applet has to be on top of the applet below, otherwise the completion list of the combobox will shine through the other applet setZValue( zValue() + 100 ); // Create the title label enableHeader( true ); setHeaderText( i18n( "Labels" ) ); setCollapseHeight( m_header->height() ); setMinimumHeight( collapseHeight() ); // reload icon QAction *reloadAction = new QAction( this ); reloadAction->setIcon( KIcon( "view-refresh" ) ); reloadAction->setVisible( true ); reloadAction->setEnabled( true ); reloadAction->setText( i18n( "Reload" ) ); m_reloadIcon = addLeftHeaderAction( reloadAction ); m_reloadIcon.data()->setEnabled( false ); connect( m_reloadIcon.data(), SIGNAL(clicked()), this, SLOT(reload()) ); // settings icon QAction *settingsAction = new QAction( this ); settingsAction->setIcon( KIcon( "preferences-system" ) ); settingsAction->setVisible( true ); settingsAction->setEnabled( true ); settingsAction->setText( i18n( "Settings" ) ); m_settingsIcon = addRightHeaderAction( settingsAction ); connect( m_settingsIcon.data(), SIGNAL(clicked()), this, SLOT(showConfigurationInterface()) ); QGraphicsLinearLayout *layout = new QGraphicsLinearLayout( Qt::Vertical, this ); layout->addItem( m_header ); m_addLabelProxy = new QGraphicsProxyWidget( this ); m_addLabelProxy.data()->setAttribute( Qt::WA_NoSystemBackground ); - m_addLabel = new KComboBox( this ); + m_addLabel = new KComboBox( true ); m_addLabel.data()->setAttribute( Qt::WA_NoSystemBackground ); m_addLabel.data()->setAutoFillBackground( false ); QPalette p = m_addLabel.data()->palette(); QColor c = p.color( QPalette::Base ); c.setAlphaF( 0.4 ); p.setColor( QPalette::Base, c ); m_addLabel.data()->setPalette( p ); m_addLabel.data()->completionObject()->setIgnoreCase( true ); m_addLabel.data()->setCompletionMode( KGlobalSettings::CompletionPopup ); connect( m_addLabel.data(), SIGNAL(returnPressed()), this, SLOT(addLabelPressed()) ); m_addLabelProxy.data()->setWidget( m_addLabel.data() ); // Read config KConfigGroup config = Amarok::config("Labels Applet"); m_minCount = config.readEntry( "MinCount", 30 ); m_numLabels = config.readEntry( "NumLabels", 10 ); m_personalCount = config.readEntry( "PersonalCount", 70 ); m_autoAdd = config.readEntry( "AutoAdd", false ); m_minAutoAddCount = config.readEntry( "MinAutoAddCount", 60 ); m_selectedColor = config.readEntry( "SelectedColor", PaletteHandler::highlightColor( 2.0, 0.7 ) ); const QPalette pal; m_backgroundColor = config.readEntry( "BackgroundColor", pal.color( QPalette::Base ) ); m_matchArtist = config.readEntry( "MatchArtist", true ); m_matchTitle = config.readEntry( "MatchTitle", true ); m_matchAlbum = config.readEntry( "MatchAlbum", true ); m_blacklist = config.readEntry( "Blacklist", QStringList() ); const QStringList replacementList = config.readEntry( "ReplacementList", QStringList() ); foreach( const QString &replacement, replacementList ) { const QStringList parts = replacement.split( '|' ); QString label = parts.at(0); label = label.replace( "%s", "|" ); label = label.replace( "%p", "%" ); QString replacementValue = parts.at(1); replacementValue = replacementValue.replace( "%s", "|" ); replacementValue = replacementValue.replace( "%p", "%" ); m_replacementMap.insert( label, replacementValue ); } m_stoppedstate = false; // force an update setStoppedState( true ); connectSource( "labels" ); connect( dataEngine( "amarok-labels" ), SIGNAL(sourceAdded(QString)), this, SLOT(connectSource(QString)) ); } void LabelsApplet::setStoppedState( bool stopped ) { if( stopped == m_stoppedstate ) return; m_stoppedstate = stopped; m_userLabels.clear(); m_webLabels.clear(); if( !stopped ) { m_reloadIcon.data()->setEnabled( true ); m_titleText = i18n( "Labels" ); m_addLabelProxy.data()->show(); m_addLabel.data()->show(); m_addLabel.data()->clearEditText(); // not needed at the moment, since setStoppedState(false) is only called in dataUpdated() and we know that the engine never sends state=started without the user labels; // so the minimum size is set in constraintsEvent() // setCollapseOffHeight( m_header->height() + m_addLabelProxy.data()->size().height() + 2 * standardPadding() ); // setCollapseOff(); } else { m_reloadIcon.data()->setEnabled( false ); m_titleText = i18n( "Labels: No track playing" ); m_addLabelProxy.data()->hide(); m_addLabel.data()->hide(); setBusy( false ); qDeleteAll( m_labelItems ); m_labelItems.clear(); qDeleteAll( m_labelAnimations ); m_labelAnimations.clear(); setMinimumHeight( collapseHeight() ); setCollapseOn(); } } void LabelsApplet::reload() { DEBUG_BLOCK if( !m_stoppedstate ) dataEngine( "amarok-labels" )->query( QString( "reload" ) ); } void LabelsApplet::animationFinished() { if( QObject::sender() == 0 ) return; for( int i=0; iupdateHoverStatus(); m_labelAnimations.at(i)->setEasingCurve( QEasingCurve::InOutQuad ); return; } } prepareGeometryChange(); for( int i=0; i tempLabelsMap; QMap < QString, int > finalLabelsMap; // holds all counts of web labels that are added to the final list QList < int > webCounts; // add the user assigned labels directly to the final map for( int i = 0; i < m_userLabels.count(); i++ ) { finalLabelsMap.insert( m_userLabels.at( i ), m_personalCount ); } // add the downloaded labels to the temp map first (if they aren't already in the final map / update value in final map if necessary) QMapIterator < QString, QVariant > it_infos ( m_webLabels ); while( it_infos.hasNext() ) { it_infos.next(); if( !finalLabelsMap.contains( it_infos.key() ) && it_infos.value().toInt() >= m_minCount && QString(it_infos.key()).length() <= LabelsAppletMaxLabelLength && !m_blacklist.contains( it_infos.key() ) && !( m_matchArtist && QString(it_infos.key()).toLower() == m_artist.toLower() ) && !( m_matchTitle && QString(it_infos.key()).toLower() == m_title.toLower() ) && !( m_matchAlbum && QString(it_infos.key()).toLower() == m_album.toLower() ) ) { tempLabelsMap.insert( it_infos.key(), it_infos.value().toInt() ); } else if( finalLabelsMap.contains( it_infos.key() ) && it_infos.value().toInt() > finalLabelsMap[ it_infos.key() ] ) { finalLabelsMap[ it_infos.key() ] = it_infos.value().toInt(); webCounts += it_infos.value().toInt(); } } // then sort the values of the temp map QList < int > tempLabelsValues = tempLabelsMap.values(); qSort( tempLabelsValues.begin(), tempLabelsValues.end(), qGreater < int > () ); // and copy the highest rated labels to the final map until max. number is reached // if there are multiple items with equal low rating, move them to minList, sort it and add to the final map until max. number is reached const int additionalNum = m_numLabels - finalLabelsMap.count(); if( additionalNum > 0 && tempLabelsValues.count() > 0 ) { int minCount; QStringList minList; if( additionalNum <= tempLabelsValues.count() ) minCount = tempLabelsValues.at( additionalNum - 1 ); else minCount = tempLabelsValues.last(); QMapIterator < QString, int > it_temp ( tempLabelsMap ); while( it_temp.hasNext() ) { it_temp.next(); if( it_temp.value() > minCount ) { finalLabelsMap.insert( it_temp.key(), it_temp.value() ); webCounts += it_temp.value(); } else if( it_temp.value() == minCount ) { minList += it_temp.key(); } } minList.sort(); while( minList.count() > 0 && finalLabelsMap.count() < m_numLabels ) { finalLabelsMap.insert( minList.first(), minCount ); webCounts += minCount; minList.takeFirst(); } } // now make the label cloud nicer by determinating the quality of the web labels // a lot of different values (73,68,51) is good, equal values (66,66,33) look suspicious // 0.7 / 0.3 is a pretty moderate choice; 0.5 / 0.5 would be more extreme const float qualityFactor = ( webCounts.count() > 0 ) ? 0.7 + 0.3 * webCounts.toSet().count()/webCounts.count() : 1.0; // delete all unneeded label items for( int i=0; itext() ) ) { m_labelAnimations.at(i)->setEndValue( QPointF( size().width(), m_labelItems.at(i)->pos().y() ) ); m_labelAnimations.at(i)->setEasingCurve( QEasingCurve::InQuad ); m_labelAnimations.at(i)->start(); m_labelItemsToDelete.append( m_labelItems.at(i) ); m_labelAnimationsToDelete.append( m_labelAnimations.at(i) ); m_labelItems.removeAt(i); m_labelAnimations.removeAt(i); i--; } } // and finally create the LabelGraphicsItems // add them to a temp list first, so they are in the same order as the final label items map (sorted alphabetically) QList < LabelGraphicsItem * > tempLabelItems; QList < QPropertyAnimation * > tempLabelAnimations; QMapIterator < QString, int > it_final ( finalLabelsMap ); while( it_final.hasNext() ) { it_final.next(); if( it_final.key().isEmpty() ) // empty labels don't make sense but they cause a freeze continue; // quality of web labels adjusted value int adjustedCount = qualityFactor * it_final.value(); if( m_userLabels.contains( it_final.key() ) && adjustedCount < m_personalCount ) adjustedCount = m_personalCount; const qreal f_size = qMax( adjustedCount / 10.0 - 5.0, -2.0 ); LabelGraphicsItem *labelGraphics = 0; QPropertyAnimation *labelAnimation = 0; for( int i=0; itext() == it_final.key() ) { labelGraphics = m_labelItems.at(i); labelGraphics->setDeltaPointSize( f_size ); labelGraphics->setSelected( m_userLabels.contains( it_final.key() ) ); if( !m_lastLabelSize.isEmpty() && m_lastLabelName == m_labelItems.at(i)->text() ) { const qreal x = labelGraphics->pos().x() - ( labelGraphics->boundingRect().width() - m_lastLabelSize.width() ) / 2; const qreal y = labelGraphics->pos().y() - ( labelGraphics->boundingRect().height() - m_lastLabelSize.height() ) / 2; labelGraphics->setPos( x, y ); m_lastLabelSize = QSizeF( 0, 0 ); m_lastLabelName.clear(); } labelAnimation = m_labelAnimations.at(i); break; } } if( !labelGraphics ) { labelGraphics = new LabelGraphicsItem( it_final.key(), f_size, this ); labelGraphics->setSelectedColor( m_selectedColor ); labelGraphics->setBackgroundColor( m_backgroundColor ); labelGraphics->showBlacklistButton( !m_allLabels.contains(it_final.key()) ); labelGraphics->setSelected( m_userLabels.contains( it_final.key() ) ); if( m_lastLabelBottomAdded ) { labelGraphics->setPos( m_addLabelProxy.data()->pos().x(), m_addLabelProxy.data()->pos().y() + m_addLabelProxy.data()->size().height()/2 - labelGraphics->boundingRect().height()/2 ); m_lastLabelBottomAdded = false; } connect( labelGraphics, SIGNAL(toggled(QString)), SLOT(toggleLabel(QString)) ); connect( labelGraphics, SIGNAL(list(QString)), SLOT(listLabel(QString)) ); connect( labelGraphics, SIGNAL(blacklisted(QString)), SLOT(blacklistLabel(QString)) ); labelAnimation = new QPropertyAnimation( labelGraphics, "pos" ); labelAnimation->setEasingCurve( QEasingCurve::OutQuad ); connect( labelAnimation, SIGNAL(finished()), this, SLOT(animationFinished()) ); } tempLabelItems.append( labelGraphics ); tempLabelAnimations.append( labelAnimation ); } // copy the temp list to the final list m_labelItems = tempLabelItems; m_labelAnimations = tempLabelAnimations; // should be unnecessary, but better safe than sorry m_lastLabelName.clear(); m_lastLabelSize = QSizeF( 0, 0 ); m_lastLabelBottomAdded = false; constraintsEvent(); // don't use updateConstraints() in order to avoid labels displayed at pos. 0,0 for a moment } void LabelsApplet::constraintsEvent( Plasma::Constraints constraints ) { Context::Applet::constraintsEvent( constraints ); setHeaderText( m_titleText ); if( !m_stoppedstate ) { const qreal horzontalPadding = standardPadding() / 2; const qreal verticalPadding = standardPadding() / 2; qreal x_pos; qreal y_pos = m_header->boundingRect().bottom() + 1.5 * standardPadding(); qreal width = 0; qreal height = 0; int start_index = 0; int end_index = -1; qreal max_width = size().width() - 2 * standardPadding(); for( int i = 0; i < m_labelItems.count(); i++ ) { QRectF l_size = m_labelItems.at(i)->boundingRect(); if( width + l_size.width() + horzontalPadding <= max_width || i == 0 ) { width += l_size.width(); if( i != 0 ) width += horzontalPadding; if( l_size.height() > height ) height = l_size.height(); end_index = i; } else { x_pos = ( max_width - width ) / 2 + standardPadding(); for( int j = start_index; j <= end_index; j++ ) { const QRectF c_size = m_labelItems.at(j)->boundingRect(); const QPointF pos = QPointF( x_pos, y_pos + (height-c_size.height())/2 ); if( m_labelItems.at(j)->pos() == QPointF(0,0) ) m_labelItems.at(j)->setPos( -c_size.width(), pos.y() ); m_labelAnimations.at(j)->setEndValue( pos ); if( m_labelAnimations.at(j)->state() != QAbstractAnimation::Running ) m_labelAnimations.at(j)->start(); m_labelItems.at(j)->updateHoverStatus(); x_pos += c_size.width() + horzontalPadding; } y_pos += height + verticalPadding; width = l_size.width(); height = l_size.height(); start_index = i; end_index = i; } } x_pos = ( max_width - width ) / 2 + standardPadding(); for( int j = start_index; j <= end_index; j++ ) { const QRectF c_size = m_labelItems.at(j)->boundingRect(); const QPointF pos = QPointF( x_pos, y_pos + (height-c_size.height())/2 ); if( m_labelItems.at(j)->pos() == QPointF(0,0) ) m_labelItems.at(j)->setPos( -c_size.width(), pos.y() ); m_labelAnimations.at(j)->setEndValue( pos ); if( m_labelAnimations.at(j)->state() != QAbstractAnimation::Running ) m_labelAnimations.at(j)->start(); m_labelItems.at(j)->updateHoverStatus(); x_pos += c_size.width() + horzontalPadding; } if( m_labelItems.count() > 0 ) y_pos += height + standardPadding(); const qreal addLabelProxyWidth = qMin( size().width() - 2 * standardPadding(), (qreal)300.0 ); m_addLabelProxy.data()->setPos( ( size().width() - addLabelProxyWidth ) / 2, y_pos ); m_addLabelProxy.data()->setMinimumWidth( addLabelProxyWidth ); m_addLabelProxy.data()->setMaximumWidth( addLabelProxyWidth ); y_pos += m_addLabelProxy.data()->size().height() + standardPadding(); setMinimumHeight( y_pos ); setCollapseOffHeight( y_pos ); setCollapseOff(); } } void LabelsApplet::connectSource( const QString &source ) { if( source == "labels" ) dataEngine( "amarok-labels" )->connectSource( "labels", this ); } void LabelsApplet::dataUpdated( const QString &name, const Plasma::DataEngine::Data &data ) // SLOT { Q_UNUSED( name ) if( data.isEmpty() ) return; if( data.contains( "state" ) && data["state"].toString().contains("started") ) setStoppedState( false ); else if( data.contains( "state" ) && data["state"].toString().contains("stopped") ) setStoppedState( true ); if( data.contains( "message" ) && data["message"].toString().contains("fetching") ) { m_titleText = i18n( "Labels: Fetching..." ); if ( !data.contains( "user" ) ) // avoid calling update twice { constraintsEvent(); // don't use updateConstraints() in order to avoid labels displayed at pos. 0,0 for a moment } if( canAnimate() ) setBusy( true ); } else if( data.contains( "message" ) ) { m_titleText = i18n( "Labels: %1", data[ "message" ].toString() ); if( !data.contains( "user" ) ) // avoid calling update twice { constraintsEvent(); // don't use updateConstraints() in order to avoid labels displayed at pos. 0,0 for a moment } setBusy( false ); } if( data.contains( "artist" ) ) m_artist = data[ "artist" ].toString(); if( data.contains( "title" ) ) m_title = data[ "title" ].toString(); if( data.contains( "album" ) ) m_album = data[ "album" ].toString(); if( data.contains( "all" ) ) { m_allLabels = data[ "all" ].toStringList(); m_allLabels.sort(); const QString saveText = m_addLabel.data()->lineEdit()->text(); m_addLabel.data()->clear(); m_addLabel.data()->insertItems( 0, m_allLabels ); m_addLabel.data()->completionObject()->setItems( m_allLabels ); m_addLabel.data()->lineEdit()->setText( saveText ); } if( data.contains( "user" ) ) { // debug() << "new user labels:" << data[ "user" ].toStringList().join(", "); if( !m_stoppedstate ) // otherwise there's been an error { m_userLabels = data[ "user" ].toStringList(); m_webLabels.clear(); // we can saftly clear the web labels because user labels will never be updated without the web labels if( !data.contains( "web" ) ) // avoid calling updateLabels twice updateLabels(); } } if( data.contains( "web" ) ) { // debug() << "new web labels:" << QStringList(data[ "web" ].toMap().keys()).join(", "); if( !m_stoppedstate ) // otherwise there's been an error { if( !data.contains( "message" ) ) m_titleText = i18n( "Labels for %1 by %2", m_title, m_artist ); setBusy( false ); m_webLabels = data[ "web" ].toMap(); // rename/merge web labels if they are present in the replacement map QMap < QString, QVariant >::iterator it = m_webLabels.begin(); while( it != m_webLabels.end() ) { if( m_replacementMap.contains(it.key()) ) { const QString replacement = m_replacementMap.value( it.key() ); if( m_webLabels.contains(replacement) ) // we have to merge { m_webLabels[replacement] = qMin( 100, it.value().toInt() + m_webLabels.value(replacement).toInt() ); it = m_webLabels.erase( it ); } else // just replace { const int count = it.value().toInt(); it = m_webLabels.erase( it ); m_webLabels.insert( replacement, count ); } } else { ++it; } } // auto add labels if needed if( m_userLabels.isEmpty() && m_autoAdd ) { QMapIterator < QString, QVariant > it ( m_webLabels ); while( it.hasNext() ) { it.next(); if( it.value().toInt() >= m_minAutoAddCount && QString(it.key()).length() <= LabelsAppletMaxLabelLength && !m_blacklist.contains( it.key() ) && !( m_matchArtist && QString(it.key()).toLower() == m_artist.toLower() ) && !( m_matchTitle && QString(it.key()).toLower() == m_title.toLower() ) && !( m_matchAlbum && QString(it.key()).toLower() == m_album.toLower() ) ) toggleLabel( it.key() ); } } updateLabels(); } } } void LabelsApplet::addLabelPressed() { const QString label = m_addLabel.data()->currentText(); if( label.isEmpty() ) return; if( !m_userLabels.contains( label ) ) { toggleLabel( label ); m_addLabel.data()->clearEditText(); } } void LabelsApplet::toggleLabel( const QString &label ) { DEBUG_BLOCK if( label.isEmpty() ) return; Meta::TrackPtr track = The::engineController()->currentTrack(); if( !track ) return; Meta::LabelPtr labelPtr; foreach( const Meta::LabelPtr &labelIt, track->labels() ) { if( label == labelIt->name() ) { labelPtr = labelIt; break; } } for( int i=0; itext() == label ) { m_lastLabelSize = m_labelItems.at(i)->boundingRect().size(); m_lastLabelName = label; break; } } if( m_userLabels.contains( label ) ) { track->removeLabel( labelPtr ); m_userLabels.removeAll( label ); debug() << "removing label: " << label; } else { track->addLabel( label ); m_userLabels.append( label ); debug() << "adding label: " << label; m_lastLabelBottomAdded = true; } if( !m_allLabels.contains( label ) ) { m_allLabels.append( label ); m_allLabels.sort(); const QString saveText = m_addLabel.data()->lineEdit()->text(); m_addLabel.data()->clear(); m_addLabel.data()->insertItems( 0, m_allLabels ); m_addLabel.data()->completionObject()->setItems( m_allLabels ); m_addLabel.data()->lineEdit()->setText( saveText ); } // usuallay the engine keeps track of label changes of the playing track // (except if the lables get auto added, this is why we have to keep m_userLabels up to date) // but it doesn't work alway, so we update updateLabels(); } void LabelsApplet::listLabel( const QString &label ) { AmarokUrl bookmark( "amarok://navigate/collections?filter=label:" + AmarokUrl::escape( "=" ) + "%22" + AmarokUrl::escape( label ) + "%22" ); bookmark.run(); } void LabelsApplet::blacklistLabel( const QString &label ) { if( m_userLabels.contains( label ) ) toggleLabel( label ); m_blacklist << label; KConfigGroup config = Amarok::config("Labels Applet"); config.writeEntry( "Blacklist", m_blacklist ); updateLabels(); } void LabelsApplet::createConfigurationInterface( KConfigDialog *parent ) { DEBUG_BLOCK parent->setButtons( KDialog::Ok | KDialog::Cancel ); KConfigGroup configuration = config(); QWidget *generalSettings = new QWidget; ui_GeneralSettings.setupUi( generalSettings ); ui_GeneralSettings.resetColorsPushButton->setIcon( KIcon("fill-color") ); QWidget *blacklistSettings = new QWidget; ui_BlacklistSettings.setupUi( blacklistSettings ); QWidget *replacementSettings = new QWidget; ui_ReplacementSettings.setupUi( replacementSettings ); ui_ReplacementSettings.addPushButton->setIcon( KIcon("list-add") ); ui_ReplacementSettings.removePushButton->setIcon( KIcon("list-remove") ); parent->addPage( generalSettings, i18n( "General Settings" ), "preferences-system" ); parent->addPage( blacklistSettings, i18n( "Blacklist Settings" ), "flag-black" ); parent->addPage( replacementSettings, i18n( "Replacement Settings" ), "system-search" ); ui_GeneralSettings.minCountSpinBox->setValue( m_minCount ); ui_GeneralSettings.numLabelsSpinBox->setValue( m_numLabels ); ui_GeneralSettings.personalCountSpinBox->setValue( m_personalCount ); ui_GeneralSettings.autoAddCheckBox->setChecked( m_autoAdd ); ui_GeneralSettings.minAutoAddCountSpinBox->setValue( m_minAutoAddCount ); ui_GeneralSettings.selectedColorButton->setColor( m_selectedColor ); ui_GeneralSettings.backgroundColorButton->setColor( m_backgroundColor ); ui_BlacklistSettings.matchArtistCheckBox->setChecked( m_matchArtist ); ui_BlacklistSettings.matchTitleCheckBox->setChecked( m_matchTitle ); ui_BlacklistSettings.matchAlbumCheckBox->setChecked( m_matchAlbum ); ui_BlacklistSettings.blacklistEditListBox->insertStringList( m_blacklist ); QHashIterator < QString, QString > it ( m_replacementMap ); while( it.hasNext() ) { it.next(); new QTreeWidgetItem( ui_ReplacementSettings.replacementTreeWidget, QStringList() << it.key() << it.value() ); } connect( ui_GeneralSettings.resetColorsPushButton, SIGNAL(clicked()), this, SLOT(settingsResetColors()) ); connect( ui_ReplacementSettings.addPushButton, SIGNAL(clicked()), this, SLOT(settingsAddReplacement()) ); connect( ui_ReplacementSettings.removePushButton, SIGNAL(clicked()), this, SLOT(settingsRemoveReplacement()) ); connect( parent, SIGNAL(accepted()), this, SLOT(saveSettings()) ); } void LabelsApplet::saveSettings() { DEBUG_BLOCK KConfigGroup config = Amarok::config("Labels Applet"); m_minCount = ui_GeneralSettings.minCountSpinBox->value(); m_numLabels = ui_GeneralSettings.numLabelsSpinBox->value(); m_personalCount = ui_GeneralSettings.personalCountSpinBox->value(); m_autoAdd = ui_GeneralSettings.autoAddCheckBox->checkState() == Qt::Checked; m_minAutoAddCount = ui_GeneralSettings.minAutoAddCountSpinBox->value(); m_selectedColor = ui_GeneralSettings.selectedColorButton->color(); m_backgroundColor = ui_GeneralSettings.backgroundColorButton->color(); m_matchArtist = ui_BlacklistSettings.matchArtistCheckBox->checkState() == Qt::Checked; m_matchTitle = ui_BlacklistSettings.matchTitleCheckBox->checkState() == Qt::Checked; m_matchAlbum = ui_BlacklistSettings.matchAlbumCheckBox->checkState() == Qt::Checked; m_blacklist = ui_BlacklistSettings.blacklistEditListBox->items(); m_replacementMap.clear(); for( int i=0; itopLevelItemCount(); i++ ) { QTreeWidgetItem *item = ui_ReplacementSettings.replacementTreeWidget->topLevelItem( i ); m_replacementMap.insert( item->text(0), item->text(1) ); } config.writeEntry( "NumLabels", m_numLabels ); config.writeEntry( "MinCount", m_minCount ); config.writeEntry( "PersonalCount", m_personalCount ); config.writeEntry( "AutoAdd", m_autoAdd ); config.writeEntry( "MinAutoAddCount", m_minAutoAddCount ); config.writeEntry( "SelectedColor", m_selectedColor ); config.writeEntry( "BackgroundColor", m_backgroundColor ); config.writeEntry( "MatchArtist", m_matchArtist ); config.writeEntry( "MatchTitle", m_matchTitle ); config.writeEntry( "MatchAlbum", m_matchAlbum ); config.writeEntry( "Blacklist", m_blacklist ); QStringList replacementList; QHashIterator < QString, QString > it ( m_replacementMap ); while( it.hasNext() ) { it.next(); QString label = it.key(); label = label.replace( '%', "%p" ); label = label.replace( '|', "%s" ); QString replacement = it.value(); replacement = replacement.replace( '%', "%p" ); replacement = replacement.replace( '|', "%s" ); replacementList.append( label + '|' + replacement ); } config.writeEntry( "ReplacementList", replacementList ); for( int i=0; isetSelectedColor( m_selectedColor ); m_labelItems.at(i)->setBackgroundColor( m_backgroundColor ); } reload(); } void LabelsApplet::settingsResetColors() { ui_GeneralSettings.selectedColorButton->setColor( PaletteHandler::highlightColor( 2.0, 0.7 ) ); const QPalette pal; ui_GeneralSettings.backgroundColorButton->setColor( pal.color( QPalette::Base ) ); } void LabelsApplet::settingsAddReplacement() { const QString label = ui_ReplacementSettings.labelLineEdit->text(); const QString replacement = ui_ReplacementSettings.replacementLineEdit->text(); if( label.isEmpty() || replacement.isEmpty() ) return; new QTreeWidgetItem( ui_ReplacementSettings.replacementTreeWidget, QStringList() << label << replacement ); ui_ReplacementSettings.labelLineEdit->clear(); ui_ReplacementSettings.replacementLineEdit->clear(); } void LabelsApplet::settingsRemoveReplacement() { for( int i=0; itopLevelItemCount(); i++ ) { QTreeWidgetItem *item = ui_ReplacementSettings.replacementTreeWidget->topLevelItem( i ); if( item->isSelected() ) { ui_ReplacementSettings.replacementTreeWidget->takeTopLevelItem( i ); i--; } } } #include "LabelsApplet.moc" diff --git a/src/context/popupdropper/libpud/PopupDropper.cpp b/src/context/popupdropper/libpud/PopupDropper.cpp index eef983d546..96ddf68520 100644 --- a/src/context/popupdropper/libpud/PopupDropper.cpp +++ b/src/context/popupdropper/libpud/PopupDropper.cpp @@ -1,983 +1,983 @@ /*************************************************************************** * Copyright (c) 2008 Jeff Mitchell * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "PopupDropper.h" #include "PopupDropper_p.h" #include "PopupDropperItem.h" #include #include #include #include #include #include #include #include #include PopupDropperPrivate::PopupDropperPrivate( PopupDropper* parent, bool sa, QWidget* widget ) : QObject( parent ) , standalone( sa ) , widget( widget ) , scene( 0 ) , view( 0 ) , fade( PopupDropper::FadeInOut ) , fadeHideTimer() , fadeShowTimer() , fadeInTime( 800 ) , fadeOutTime( 300 ) , deleteTimer() , deleteTimeout( 1000 ) , frameMax( 30 ) , windowColor( 0, 0, 0, 64 ) , windowBackgroundBrush() , baseTextColor( Qt::white ) , hoveredTextColor( Qt::blue ) , hoveredBorderPen() , hoveredFillBrush() , file() , sharedRenderer( 0 ) , horizontalOffset( 30 ) , pdiItems() , overlayLevel( 1 ) , entered( false ) , submenuMap() , submenu( false ) , allItems() , quitOnDragLeave( false ) , onTop( true ) , widgetRect() , queuedHide( false ) , q( parent ) { if( widget ) widgetRect = widget->rect(); - windowBackgroundBrush.setColor( windowColor ); - hoveredBorderPen.setColor( Qt::blue ); + windowBackgroundBrush.setColor( windowColor ); + hoveredBorderPen.setColor( Qt::blue ); hoveredBorderPen.setWidth( 2 ); hoveredBorderPen.setStyle( Qt::SolidLine ); - QColor hoveredFillColor = QColor( Qt::blue ); + QColor hoveredFillColor = QColor( Qt::blue ); hoveredFillColor.setAlpha( 32 ); hoveredFillBrush.setColor( hoveredFillColor ); hoveredFillBrush.setStyle( Qt::SolidPattern ); scene = new QGraphicsScene( ( sa ? 0 : parent ) ); view = new PopupDropperView( parent, scene, ( sa ? 0 : widget ) ); //qDebug() << "on create, view size = " << view->size(); deleteTimer.setSingleShot( true ); fadeHideTimer.setDirection( QTimeLine::Backward ); connect( &fadeHideTimer, SIGNAL(frameChanged(int)), this, SLOT(fadeHideTimerFrameChanged(int)) ); connect( &fadeShowTimer, SIGNAL(frameChanged(int)), this, SLOT(fadeShowTimerFrameChanged(int)) ); connect( &fadeHideTimer, SIGNAL(finished()), this, SLOT(fadeHideTimerFinished()) ); connect( &fadeShowTimer, SIGNAL(finished()), this, SLOT(fadeShowTimerFinished()) ); connect( &deleteTimer, SIGNAL(timeout()), this, SLOT(deleteTimerFinished()) ); } PopupDropperPrivate::~PopupDropperPrivate() { } void PopupDropperPrivate::newSceneView( PopupDropper* pud ) { scene->deleteLater(); scene = new QGraphicsScene( pud ); //qDebug() << "new scene width in newSceneView = " << scene->width(); view = new PopupDropperView( pud, scene, widget ); //qDebug() << "on create, view size = " << view->size(); } void PopupDropperPrivate::setParent( QObject* parent ) { QObject::setParent( parent ); q = static_cast( parent ); } void PopupDropperPrivate::fadeHideTimerFrameChanged( int frame ) //SLOT { if( fadeHideTimer.state() == QTimeLine::Running ) { qreal val = ( frame * 1.0 ) / frameMax; QColor color = windowColor; int alpha = (int)( color.alpha() * val ); color.setAlpha( alpha ); q->setPalette( color ); foreach( PopupDropperItem* pdi, pdiItems ) pdi->setSubitemOpacity( val ); } } void PopupDropperPrivate::fadeShowTimerFrameChanged( int frame ) //SLOT { if( fadeShowTimer.state() == QTimeLine::Running ) { qreal val = ( frame * 1.0 ) / frameMax; QColor color = windowColor; int alpha = (int)( color.alpha() * val ); color.setAlpha( alpha ); q->setPalette( color ); foreach( PopupDropperItem* pdi, pdiItems ) pdi->setSubitemOpacity( val ); } } void PopupDropperPrivate::fadeHideTimerFinished() //SLOT { view->hide(); //qDebug() << "Emitting fadeHideFinished in d pointer " << this; emit q->fadeHideFinished(); } void PopupDropperPrivate::fadeShowTimerFinished() //SLOT { q->setPalette( windowColor ); queuedHide = false; foreach( PopupDropperItem* pdi, pdiItems ) pdi->setSubitemOpacity( 1.0 ); } void PopupDropperPrivate::dragEntered() { //qDebug() << "PopupDropperPrivate::dragEntered"; q->updateAllOverlays(); } void PopupDropperPrivate::dragLeft() { //qDebug() << "PopupDropperPrivate::dragLeft()"; //qDebug() << "PUD to be hidden or not hidden = " << q; if( view->entered() && quitOnDragLeave ) { view->setAcceptDrops( false ); //qDebug() << "View entered, hiding"; connect( q, SIGNAL(fadeHideFinished()), q, SLOT(subtractOverlay()) ); q->hide(); } q->updateAllOverlays(); } void PopupDropperPrivate::startDeleteTimer() { if( deleteTimeout == 0 ) return; view->setEntered( false ); //qDebug() << "Starting delete timer"; deleteTimer.start( deleteTimeout ); } void PopupDropperPrivate::deleteTimerFinished() //SLOT { //qDebug() << "Delete Timer Finished"; if( !view->entered() && quitOnDragLeave ) { connect( q, SIGNAL(fadeHideFinished()), q, SLOT(subtractOverlay()) ); q->hide(); } } void PopupDropperPrivate::reposItems() { qreal partitionsize, my_min, my_max; //qDebug() << "allItems.size() = " << allItems.size(); int counter = 0; for( int i = 0; i < allItems.size(); i++ ) { //qDebug() << "item " << i; int verticalmargin = 5; partitionsize = scene->height() / pdiItems.size(); //gives partition size...now center in this area my_min = ( counter * partitionsize ) + verticalmargin; my_max = ( ( counter + 1 ) * partitionsize ) - verticalmargin; //qDebug() << "my_min = " << my_min << ", my_max = " << my_max; PopupDropperItem* pItem = dynamic_cast( allItems.at( i ) ); QGraphicsLineItem* qglItem = 0; if( pItem ) { pItem->setPopupDropper( q ); //safety //qDebug() << "item " << i << " is a PDI "; //If the svgElementRect is too high, resize it to fit pItem->svgElementRect().setHeight( my_max - my_min - ( 2 * verticalmargin ) ); pItem->setPos( 0, my_min ); pItem->borderRectItem()->setRect( 0 - pItem->borderWidth(), 0, scene->width() + 2*pItem->borderWidth(), my_max - my_min ); pItem->scaleAndReposSvgItem(); pItem->reposTextItem(); pItem->reposHoverFillRects(); pItem->update(); //qDebug() << "size of view frame = " << view->size(); ++counter; } else if( ( qglItem = dynamic_cast( allItems.at( i ) ) ) ) { //qDebug() << "item " << i << " is a QGLI"; qglItem->setLine( horizontalOffset, (my_max-partitionsize), scene->width() - horizontalOffset, (my_max-partitionsize) ); } } } bool PopupDropperPrivate::amIOnTop( PopupDropperView* pdv ) { if( onTop && pdv == view ) return true; return false; } ////////////////////////////////////////////////////////////// PopupDropper::PopupDropper( QWidget *parent, bool standalone ) : QObject( parent ) , d( new PopupDropperPrivate( this, standalone, parent ) ) { if( !parent ) { parent = d->view; d->widget = parent; } QObject::setParent( parent ); initOverlay( parent ); setColors( d->windowColor, d->baseTextColor, d->hoveredTextColor, d->hoveredBorderPen.color(), d->hoveredFillBrush.color() ); d->sharedRenderer = new QSvgRenderer( this ); d->overlayLevel = 1; //qDebug() << "Popup Dropper created!"; } PopupDropper::~PopupDropper() { //qDebug() << "Popup Dropper destroyed!"; } int PopupDropper::overlayLevel() const { return d->overlayLevel; } void PopupDropper::initOverlay( QWidget* parent, PopupDropperPrivate* priv ) { PopupDropperPrivate *pdp = priv ? priv : d; //qDebug() << "PUD Overlay being created, d pointer is " << d; pdp->scene->setSceneRect( QRectF( parent->rect() ) ); //qDebug() << "Scene width = " << pdp->scene->width(); pdp->scene->setItemIndexMethod( QGraphicsScene::NoIndex ); pdp->view->setFixedSize( parent->size() ); pdp->view->setLineWidth( 0 ); pdp->view->setFrameStyle( QFrame::NoFrame ); pdp->view->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); pdp->view->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); pdp->view->setBackgroundRole( QPalette::Window ); pdp->view->setAutoFillBackground( true ); pdp->fadeHideTimer.setFrameRange( 0, pdp->frameMax ); pdp->fadeHideTimer.setUpdateInterval( 20 ); // 50 fps pdp->fadeShowTimer.setFrameRange( 0, pdp->frameMax ); pdp->fadeShowTimer.setUpdateInterval( 20 ); // 50 fps } void PopupDropper::addOverlay() { d->onTop = false; m_viewStack.push( d ); PopupDropperPrivate* old_d = d; d = new PopupDropperPrivate( this, false, old_d->view ); d->sharedRenderer = old_d->sharedRenderer; //qDebug() << "Adding overlay: "; initOverlay( old_d->view ); setColors( d->windowColor, d->baseTextColor, d->hoveredTextColor, d->hoveredBorderPen.color(), d->hoveredFillBrush.color() ); d->quitOnDragLeave = true; d->overlayLevel = old_d->overlayLevel + 1; old_d->view->deactivateHover(); } //note: Used when activating a submenu; does not set default colors, should have done that when creating submenu void PopupDropper::addOverlay( PopupDropperPrivate* newD ) { d->onTop = false; //qDebug() << "right before push, m_viewStack.size() is " << m_viewStack.size(); m_viewStack.push( d ); //qDebug() << "right after push, m_viewStack.size() is " << m_viewStack.size(); PopupDropperPrivate* old_d = d; d = newD; d->onTop = true; d->sharedRenderer = old_d->sharedRenderer; d->quitOnDragLeave = true; d->overlayLevel = old_d->overlayLevel + 1; //qDebug() << "new d d->overlayLevel = " << d->overlayLevel; //qDebug() << "in PopupDropper " << this; } //NOTE: just like you have to show the overlay when adding, this function does not hide the overlay; you must do that yourself //with hide() (which will hide just one level) bool PopupDropper::subtractOverlay() { //qDebug() << "subtractOverlay, with d pointer " << d; disconnect( this, SLOT(subtractOverlay()) ); while( !isHidden() && d->fadeHideTimer.state() == QTimeLine::Running ) { //qDebug() << "singleshotting subtractOverlay"; QTimer::singleShot( 0, this, SLOT(subtractOverlay()) ); return false; } //qDebug() << "in PopupDropper " << this; //qDebug() << "overlay d->overlayLevel = " << d->overlayLevel; if( d->overlayLevel == 1 ) return false; PopupDropper::Fading currFadeValue = d->fade; d->fade = PopupDropper::NoFade; d->onTop = false; PopupDropperPrivate* old_d = d; //qDebug() << "right before pop, m_viewStack.size() is " << m_viewStack.size(); d = m_viewStack.pop(); d->onTop = true; if( !old_d->submenu ) { //qDebug() << "not submenu, deleting"; old_d->deleteLater(); } else { foreach( QGraphicsItem* item, old_d->pdiItems ) old_d->scene->removeItem( item ); //qDebug() << "not deleting, submenu"; old_d->fade = currFadeValue; old_d->view->resetView(); } d->startDeleteTimer(); return true; } PopupDropperItem* PopupDropper::addSubmenu( PopupDropper** pd, const QString &text ) { //qDebug() << "addSubmenu, this is " << this << " and passed-in PopupDropper is " << (*pd); if( !(*pd) ) { qWarning() << "Did not pass in a valid PUD!"; return 0; } PopupDropperPrivate* newD = (*pd)->d; newD->submenu = true; newD->widget = d->widget; newD->setParent( this ); foreach( PopupDropperItem* item, newD->pdiItems ) newD->scene->removeItem( item ); newD->newSceneView( this ); initOverlay( d->widget, newD ); PopupDropperItem* pdi = new PopupDropperItem(); QAction* action = new QAction( text, this ); connect( action, SIGNAL(hovered()), this, SLOT(activateSubmenu()) ); pdi->setAction( action ); pdi->setSubmenuTrigger( true ); pdi->setHoverIndicatorShowStyle( PopupDropperItem::OnHover ); d->submenuMap[action] = newD; delete (*pd); (*pd) = 0; foreach( PopupDropperItem* item, newD->pdiItems ) item->setPopupDropper( this ); //qDebug() << "d->submenuMap[pda] = " << d->submenuMap[pda]; addItem( pdi ); return pdi; } void PopupDropper::activateSubmenu() { //qDebug() << "Sender is " << QObject::sender(); if( isHidden() || d->fadeHideTimer.state() == QTimeLine::Running ) return; PopupDropperPrivate* oldd = d; addOverlay( d->submenuMap[static_cast(QObject::sender())] ); //qDebug() << "d->pdiItems.size() = " << d->pdiItems.size() << " for " << d; foreach( PopupDropperItem* item, d->pdiItems ) addItem( item, false, false ); oldd->view->deactivateHover(); show(); } bool PopupDropper::addMenu( const QMenu *menu ) { Q_UNUSED(menu) if( !menu ) return false; if( menu->actions().size() == 0 ) return true; PopupDropperItem *pdi = 0; foreach( QAction *action, menu->actions() ) { if( !action->menu() ) { pdi = new PopupDropperItem(); pdi->setAction( action ); addItem( pdi ); } else { PopupDropper *pd = new PopupDropper( 0 ); bool success = pd->addMenu( action->menu() ); if( success ) pdi = addSubmenu( &pd, action->text() ); } pdi = 0; } return true; } bool PopupDropper::standalone() const { return d->standalone; } void PopupDropper::show() { if( !d->sharedRenderer ) { //qDebug() << "No shared renderer set!"; return; } if( d->widget && d->widget->rect() != d->widgetRect ) { d->widgetRect = d->widget->rect(); d->scene->setSceneRect( d->widget->rect() ); d->view->setFixedSize( d->widget->size() ); update(); } //qDebug() << "Showing PopupDropper"; d->fadeShowTimer.stop(); if( ( d->fade == PopupDropper::FadeIn || d->fade == PopupDropper::FadeInOut ) && d->fadeInTime > 0 ) { //qDebug() << "Animating!"; d->fadeShowTimer.setDuration( d->fadeInTime ); d->fadeShowTimer.setCurrentTime( 0 ); d->fadeShowTimer.setCurveShape( QTimeLine::EaseOutCurve ); QColor color = d->windowColor; color.setAlpha( 0 ); setPalette( color ); foreach( PopupDropperItem* pdi, d->pdiItems ) pdi->setSubitemOpacity( 0.0 ); d->fadeShowTimer.start(); //qDebug() << "Timer started"; } d->view->show(); } void PopupDropper::showAllOverlays() { show(); for( int i = m_viewStack.size() - 1; i >= 0; --i ) { PopupDropperPrivate* pdi = m_viewStack.at( i ); if( pdi != d ) d->view->show(); } } //returns immediately! void PopupDropper::hide() { //qDebug() << "PopupDropper::hide entered, d pointer is = " << d; //if hide is called and the view is already hidden, it's likely spurious if( isHidden() ) { //qDebug() << "ishidden, returning"; return; } //queuedHide is to make sure that fadeShowTimerFinished executes before this next hide() if( d->fadeShowTimer.state() == QTimeLine::Running ) { //qDebug() << "show timer running, queueing hide"; d->fadeShowTimer.stop(); d->queuedHide = true; QTimer::singleShot( 0, d, SLOT(fadeShowTimerFinished()) ); QTimer::singleShot( 0, this, SLOT(hide()) ); return; } //queuedHide will be set to false from fadeShowTimerFinished...so if this came up first, //then wait if( d->fadeHideTimer.state() == QTimeLine::Running || d->queuedHide ) { //qDebug() << "hide timer running or queued hide"; QTimer::singleShot( 0, this, SLOT(hide()) ); return; } if( ( d->fade == PopupDropper::FadeOut || d->fade == PopupDropper::FadeInOut ) && d->fadeOutTime > 0 ) { //qDebug() << "Starting fade out"; d->fadeHideTimer.setDuration( d->fadeOutTime ); d->fadeHideTimer.setCurveShape( QTimeLine::LinearCurve ); d->fadeHideTimer.start(); //qDebug() << "Timer started"; return; } else //time is zero, or no fade { //qDebug() << "time is zero, or no fade"; QTimer::singleShot( 0, d, SLOT(fadeHideTimerFinished()) ); return; } } void PopupDropper::hideAllOverlays() { //qDebug() << "Entered hideAllOverlays"; connect( this, SIGNAL(fadeHideFinished()), this, SLOT(slotHideAllOverlays()) ); hide(); //qDebug() << "Leaving hideAllOverlays"; } void PopupDropper::slotHideAllOverlays() { //qDebug() << "Entered slotHideAllOverlays()"; disconnect( this, SIGNAL(fadeHideFinished()), this, SLOT(slotHideAllOverlays()) ); //qDebug() << "m_viewStack.size() = " << m_viewStack.size(); for( int i = m_viewStack.size() - 1; i >= 0; --i ) { PopupDropperPrivate* pdp = m_viewStack.at( i ); //qDebug() << "checking pdp = " << (QObject*)pdp << ", d is " << (QObject*)d; if( pdp != d ) pdp->view->hide(); } //qDebug() << "Leaving slotHideAllOverlays"; } void PopupDropper::update() { d->reposItems(); d->view->update(); } void PopupDropper::updateAllOverlays() { for( int i = m_viewStack.size() - 1; i >= 0; --i ) { PopupDropperPrivate* pdp = m_viewStack.at( i ); pdp->view->update(); } d->view->update(); } bool PopupDropper::isHidden() const { return d->view->isHidden(); } void PopupDropper::clear() { while( !isHidden() && d->fadeHideTimer.state() == QTimeLine::Running ) { QTimer::singleShot(0, this, SLOT(clear()) ); return; } //qDebug() << "Clear happening!"; disconnect( this, SLOT(clear()) ); do { foreach( QGraphicsItem* item, d->allItems ) { if( dynamic_cast(item) ) { if( dynamic_cast(item)->isSubmenuTrigger() ) { //qDebug() << "Disconnecting action"; disconnect( dynamic_cast(item)->action(), SIGNAL(hovered()), this, SLOT(activateSubmenu()) ); } dynamic_cast(item)->deleteLater(); } else delete item; } d->pdiItems.clear(); d->allItems.clear(); //qDebug() << "Size of pdiItems is now " << d->pdiItems.size(); //qDebug() << "Size of allItems is now " << d->allItems.size(); d->view->hide(); d->view->resetView(); } while ( subtractOverlay() ); } bool PopupDropper::isEmpty( bool allItems ) const { if( allItems ) return d->allItems.empty(); else return d->pdiItems.empty(); } bool PopupDropper::quitOnDragLeave() const { return d->quitOnDragLeave; } void PopupDropper::setQuitOnDragLeave( bool quit ) { d->quitOnDragLeave = quit; } int PopupDropper::fadeInTime() const { return d->fadeInTime; } void PopupDropper::setFadeInTime( const int msecs ) { d->fadeInTime = msecs; } int PopupDropper::fadeOutTime() const { return d->fadeOutTime; } void PopupDropper::setFadeOutTime( const int msecs ) { d->fadeOutTime = msecs; } PopupDropper::Fading PopupDropper::fading() const { return d->fade; } void PopupDropper::setFading( PopupDropper::Fading fade ) { d->fade = fade; } const QTimeLine* PopupDropper::fadeHideTimer() const { return &d->fadeHideTimer; } const QTimeLine* PopupDropper::fadeShowTimer() const { return &d->fadeShowTimer; } int PopupDropper::deleteTimeout() const { return d->deleteTimeout; } void PopupDropper::setDeleteTimeout( int msecs ) { d->deleteTimeout = msecs; } QColor PopupDropper::windowColor() const { return d->windowColor; } void PopupDropper::setWindowColor( const QColor &window ) { d->windowColor = window; setPalette( window ); } QBrush PopupDropper::windowBackgroundBrush() const { return d->windowBackgroundBrush; } void PopupDropper::setWindowBackgroundBrush( const QBrush &window ) { d->windowBackgroundBrush = window; d->view->setBackgroundBrush( window ); } QColor PopupDropper::baseTextColor() const { return d->baseTextColor; } void PopupDropper::setBaseTextColor( const QColor &text ) { d->baseTextColor = text; foreach( PopupDropperItem *item, d->pdiItems ) item->setBaseTextColor( text ); } QColor PopupDropper::hoveredTextColor() const { return d->hoveredTextColor; } void PopupDropper::setHoveredTextColor( const QColor &text ) { d->hoveredTextColor = text; foreach( PopupDropperItem *item, d->pdiItems ) item->setHoveredTextColor( text ); } QPen PopupDropper::hoveredBorderPen() const { return d->hoveredBorderPen; } void PopupDropper::setHoveredBorderPen( const QPen &border ) { d->hoveredBorderPen = border; foreach( PopupDropperItem *item, d->pdiItems ) item->setHoveredBorderPen( border ); } QBrush PopupDropper::hoveredFillBrush() const { return d->hoveredFillBrush; } void PopupDropper::setHoveredFillBrush( const QBrush &fill ) { d->hoveredFillBrush = fill; foreach( PopupDropperItem *item, d->pdiItems ) item->setHoveredFillBrush( fill ); } void PopupDropper::setColors( const QColor &window, const QColor &baseText, const QColor &hoveredText, const QColor &hoveredBorder, const QColor &hoveredFill ) { d->windowColor = window; d->baseTextColor = baseText; d->hoveredTextColor = hoveredText; d->hoveredBorderPen.setColor( hoveredBorder ); d->hoveredFillBrush.setColor( hoveredFill ); setPalette( window, baseText, hoveredText, hoveredBorder, hoveredFill ); } void PopupDropper::setPalette( const QColor &window ) { QPalette p = d->view->palette(); p.setColor( QPalette::Window, window ); d->view->setPalette( p ); updateAllOverlays(); } void PopupDropper::setPalette( const QColor &window, const QColor &baseText, const QColor &hoveredText, const QColor &hoveredBorder, const QColor &hoveredFill ) { QPalette p = d->view->palette(); p.setColor( QPalette::Window, window ); d->view->setPalette( p ); QPen pen; QBrush brush; foreach( PopupDropperItem *item, d->pdiItems ) { item->setBaseTextColor( baseText ); item->setHoveredTextColor( hoveredText ); pen = item->hoveredBorderPen(); pen.setColor( hoveredBorder ); item->setHoveredBorderPen( pen ); brush = item->hoveredFillBrush(); brush.setColor( hoveredFill ); item->setHoveredFillBrush( brush ); } updateAllOverlays(); } QString PopupDropper::windowTitle() const { return d->view->windowTitle(); } void PopupDropper::setWindowTitle( const QString &title ) { d->view->setWindowTitle( title ); d->view->update(); } QString PopupDropper::svgFile() const { return d->file; } void PopupDropper::setSvgFile( const QString &file ) { if( d->sharedRenderer ) { if( !d->sharedRenderer->load( file ) ) qWarning() << "Could not load SVG file " << file; else { d->file = file; //qDebug() << "Loaded SVG file!"; } } else qWarning() << "No shared renderer!"; } QSvgRenderer* PopupDropper::svgRenderer() { return d->sharedRenderer; } void PopupDropper::setSvgRenderer( QSvgRenderer *renderer ) { d->sharedRenderer = renderer; } int PopupDropper::horizontalOffset() const { return d->horizontalOffset; } void PopupDropper::setHorizontalOffset( int pixels ) { d->horizontalOffset = pixels; } const QSize PopupDropper::viewSize() const { if( d && d->view ) return d->view->size(); else return QSize( 0, 0 ); } void PopupDropper::addItem( PopupDropperItem *item, bool useSharedRenderer ) { addItem( item, useSharedRenderer, true ); } void PopupDropper::addItem( PopupDropperItem *item, bool useSharedRenderer, bool appendToList ) { //qDebug() << "adding item"; //FIXME: Make separators do something graphical instead of just ignoring them PopupDropperItem *pItem = static_cast( item ); if( pItem->isSeparator() ) return; if( useSharedRenderer ) pItem->setSharedRenderer( d->sharedRenderer ); //qDebug() << "Checking appendToList"; if( appendToList ) { d->pdiItems.append( pItem ); d->allItems.append( pItem ); //qDebug() << "pdiItems list is now size " << d->pdiItems.size() << " for " << d; //qDebug() << "allItems list is now size " << d->allItems.size() << " for " << d; } if( !pItem->textItem() ) { QGraphicsTextItem *textItem = new QGraphicsTextItem( pItem->text(), pItem ); pItem->setTextItem( textItem ); if( !pItem->customBaseTextColor() || !pItem->baseTextColor().isValid() ) { pItem->setBaseTextColor( d->baseTextColor ); //qDebug() << "Using PD's base text color"; } else { //qDebug() << "Using the item's base text color"; pItem->textItem()->setDefaultTextColor( pItem->baseTextColor() ); } if( !pItem->customHoveredTextColor() ) pItem->setHoveredTextColor( d->hoveredTextColor ); } if( !pItem->borderRectItem() ) { QGraphicsRectItem *borderRectItem = new QGraphicsRectItem( pItem ); borderRectItem->setZValue( -5 ); pItem->setBorderRectItem( borderRectItem ); if( !pItem->customHoveredBorderPen() ) pItem->setHoveredBorderPen( d->hoveredBorderPen ); if( !pItem->customHoveredFillBrush() ) pItem->setHoveredFillBrush( d->hoveredFillBrush ); } d->reposItems(); pItem->setPopupDropper( this ); d->scene->addItem( pItem ); } QList PopupDropper::items() const { QList list; foreach( PopupDropperItem *item, d->pdiItems ) list.append( item ); return list; } //Won't currently work for > 1 level of submenu! //TODO: Figure out a better way. (Does anything else work > 1 level?) QList PopupDropper::submenuItems( const PopupDropperItem *item ) const { QList list; if( !item || !item->isSubmenuTrigger() || !d->submenuMap.contains( item->action() ) ) return list; PopupDropperPrivate *pdp = d->submenuMap[item->action()]; foreach( PopupDropperItem *pdi, pdp->pdiItems ) list.append( pdi ); return list; } //Goes through and calls the callback on all items, including submenuItems //which can be adjusted differently by checking isSubmenuTrigger() void PopupDropper::forEachItem( void callback(void*) ) { forEachItemPrivate( d, callback ); } void PopupDropper::forEachItemPrivate( PopupDropperPrivate *pdp, void callback(void* item) ) { foreach( PopupDropperItem *item, pdp->pdiItems ) callback( item ); foreach( QAction *action, pdp->submenuMap.keys() ) forEachItemPrivate( pdp->submenuMap[action], callback ); } void PopupDropper::addSeparator( PopupDropperItem* separator ) { if( !separator ) { //qDebug() << "Action is not a separator!"; return; } separator->setSeparator( true ); if( separator->separatorStyle() == PopupDropperItem::TextSeparator ) { //qDebug() << "Separator style is text"; addItem( separator ); } //qDebug() << "Separator style is line"; QPen linePen; if( separator && separator->hasLineSeparatorPen() ) linePen = separator->lineSeparatorPen(); else { linePen.setWidth( 2 ); linePen.setCapStyle( Qt::RoundCap ); linePen.setStyle( Qt::DotLine ); linePen.setColor( QColor( 255, 255, 255 ) ); } //qDebug() << "scene width = " << d->scene->width() << ", horizontalOffset = " << d->horizontalOffset; //qDebug() << "right side = " << qreal(d->scene->width() - d->horizontalOffset); QGraphicsLineItem* lineItem = new QGraphicsLineItem( 0, 0, 0, 0 ); d->allItems.append( lineItem ); lineItem->setPen( linePen ); d->reposItems(); d->scene->addItem( lineItem ); } #include "PopupDropper_p.moc" #include "PopupDropper.moc" diff --git a/src/context/popupdropper/libpud/PopupDropperItem.cpp b/src/context/popupdropper/libpud/PopupDropperItem.cpp index b1fb824934..71da78123b 100644 --- a/src/context/popupdropper/libpud/PopupDropperItem.cpp +++ b/src/context/popupdropper/libpud/PopupDropperItem.cpp @@ -1,885 +1,885 @@ /*************************************************************************** * Copyright (c) 2008 Jeff Mitchell * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "PopupDropperItem.h" #include "PopupDropperItem_p.h" #include "PopupDropper.h" #include #include #include #include #include /////////////////////////////////////////////////////////// PopupDropperItemPrivate::PopupDropperItemPrivate( PopupDropperItem *parent ) : action( 0 ) , text( QString() ) , hoverTimer( 500, parent ) , elementId( QString() ) , textItem( 0 ) , borderRectItem( 0 ) , svgItem( 0 ) , hoverIndicatorRectItem( 0 ) , hoverIndicatorRectFillItem( 0 ) , borderWidth( 2 ) , hoverIndicatorRectWidth( 15 ) , font() , submenuTrigger( false ) , baseTextColor() , hoveredTextColor() , hoveredBorderPen() , hoveredFillBrush() , hoverIndicatorRectFillBrush() , hoveredOver( false ) , customBaseTextColor( false ) , customHoveredTextColor( false ) , customHoveredBorderPen( false ) , customHoveredFillBrush( false ) , subitemOpacity( 0.0 ) , file( QString() ) , svgElementRect( 0, 0, 50, 50 ) , sharedRenderer( 0 ) , horizontalOffset( 30 ) , textOffset( 30 ) , separator( false ) , hasLineSeparatorPen( false ) , lineSeparatorPen() , hoverIndicatorShowStyle( PopupDropperItem::Never ) , orientation( PopupDropperItem::Left ) , textProtection( PopupDropperItem::ScaleFont ) , separatorStyle( PopupDropperItem::TextSeparator ) , pd( 0 ) , q( parent ) { hoverTimer.setFrameRange( 0, 30 ); hoverTimer.setUpdateInterval( 20 ); // 50 fps q->setAcceptDrops( true ); hoverIndicatorRectFillBrush.setColor( Qt::white ); hoveredBorderPen.setWidth( borderWidth ); hoveredBorderPen.setColor( Qt::white ); hoveredBorderPen.setStyle( Qt::SolidLine ); hoveredFillBrush.setColor( Qt::white ); hoveredFillBrush.setStyle( Qt::SolidPattern ); } PopupDropperItemPrivate::~PopupDropperItemPrivate() { } /////////////////////////////////////////////////////////// PopupDropperItem::PopupDropperItem( QGraphicsItem *parent ) : QObject() , QAbstractGraphicsShapeItem( parent ) , d( new PopupDropperItemPrivate( this ) ) { connect( &d->hoverTimer, SIGNAL(finished()), this, SLOT(hoverFinished()) ); connect( &d->hoverTimer, SIGNAL(frameChanged(int)), this, SLOT(hoverFrameChanged(int)) ); } PopupDropperItem::PopupDropperItem( const QString &file, QGraphicsItem *parent ) : QObject() , QAbstractGraphicsShapeItem( parent ) , d( new PopupDropperItemPrivate( this ) ) { d->file = file; connect( &d->hoverTimer, SIGNAL(finished()), this, SLOT(hoverFinished()) ); connect( &d->hoverTimer, SIGNAL(frameChanged(int)), this, SLOT(hoverFrameChanged(int)) ); } PopupDropperItem::~PopupDropperItem() { delete d; } void PopupDropperItem::show() { } QAction* PopupDropperItem::action() const { return d->action; } void PopupDropperItem::setAction( QAction *action ) { if( !action ) return; //note that this also sets the text d->action = action; d->text = action->text(); if( action ) { if( !d->svgItem ) { if( !d->file.isEmpty() ) d->svgItem = new QGraphicsSvgItem( d->file, this ); else d->svgItem = new QGraphicsSvgItem( this ); } if( d->sharedRenderer ) d->svgItem->setSharedRenderer( d->sharedRenderer ); if( d->elementId.isEmpty() ) d->elementId = action->property( "popupdropper_svg_id" ).toString(); if( !d->elementId.isEmpty() ) { if( d->svgItem->renderer() && d->svgItem->renderer()->elementExists( d->elementId ) ) d->svgItem->setElementId( d->elementId ); } if( !d->svgItem->elementId().isEmpty() && d->svgItem->renderer()->elementExists( d->svgItem->elementId() ) ) d->svgItem->show(); else d->svgItem->hide(); if( action->isSeparator() ) d->separator = true; scaleAndReposSvgItem(); d->hoverIndicatorRectItem = new QGraphicsRectItem( this ); QPen pen = d->hoveredBorderPen; QColor color( pen.color() ); color.setAlpha( 255 ); pen.setColor( color ); d->hoverIndicatorRectItem->setPen( pen ); QBrush brush = d->hoverIndicatorRectItem->brush(); brush.setStyle( Qt::NoBrush ); d->hoverIndicatorRectItem->setBrush( brush ); d->hoverIndicatorRectFillItem = new QGraphicsRectItem( this ); pen = d->hoverIndicatorRectFillItem->pen(); pen.setStyle( Qt::NoPen ); d->hoverIndicatorRectFillItem->setPen( pen ); d->hoverIndicatorRectFillBrush.setStyle( Qt::SolidPattern ); if( d->hoverIndicatorShowStyle == PopupDropperItem::AlwaysShow ) d->hoverIndicatorRectItem->show(); else d->hoverIndicatorRectItem->hide(); d->hoverIndicatorRectFillItem->hide(); reposHoverFillRects(); } if( d->pd ) d->pd->updateAllOverlays(); } PopupDropperItem::HoverIndicatorShowStyle PopupDropperItem::hoverIndicatorShowStyle() const { return d->hoverIndicatorShowStyle; } void PopupDropperItem::setHoverIndicatorShowStyle( HoverIndicatorShowStyle hover ) { d->hoverIndicatorShowStyle = hover; if( !d->hoveredOver ) { if( d->hoverIndicatorShowStyle == PopupDropperItem::AlwaysShow ) d->hoverIndicatorRectItem->show(); else d->hoverIndicatorRectItem->hide(); } } PopupDropperItem::Orientation PopupDropperItem::orientation() const { return d->orientation; } void PopupDropperItem::setOrientation( const Orientation orientation ) { d->orientation = orientation; fullUpdate(); } PopupDropperItem::TextProtection PopupDropperItem::textProtection() const { return d->textProtection; } void PopupDropperItem::setTextProtection( const TextProtection textProtection ) { d->textProtection = textProtection; fullUpdate(); } QString PopupDropperItem::text() const { return d->text; } void PopupDropperItem::setText( const QString &text ) { d->text = text; if( d->textItem ) d->textItem->setHtml( text ); reposTextItem(); } QFont PopupDropperItem::font() const { return d->font; } void PopupDropperItem::setFont( const QFont &font ) { d->font = font; if( d->textItem ) d->textItem->setFont( font ); reposTextItem(); } QColor PopupDropperItem::baseTextColor() const { return d->baseTextColor; } void PopupDropperItem::setBaseTextColor( const QColor &color ) { if( !d->hoveredOver && d->textItem ) d->textItem->setDefaultTextColor( color ); d->baseTextColor = color; d->customBaseTextColor = true; } QColor PopupDropperItem::hoveredTextColor() const { return d->hoveredTextColor; } void PopupDropperItem::setHoveredTextColor( const QColor &color ) { if( d->textItem && d->hoveredOver && d->hoverTimer.state() != QTimeLine::Running ) d->textItem->setDefaultTextColor( color ); d->hoveredTextColor = color; d->customHoveredTextColor = true; } QPen PopupDropperItem::hoveredBorderPen() const { return d->hoveredBorderPen; } void PopupDropperItem::setHoveredBorderPen( const QPen &pen ) { d->hoveredBorderPen = pen; d->customHoveredBorderPen = true; if( d->borderRectItem && ( !d->hoveredOver || ( d->hoveredOver && d->hoverTimer.state() != QTimeLine::Running ) ) ) { QPen borderPen = pen; if( !d->hoveredOver ) { QColor pencolor = borderPen.color(); pencolor.setAlpha( 0 ); borderPen.setColor( pencolor ); } d->borderRectItem->setPen( borderPen ); } if( d->hoverIndicatorRectItem && ( !d->hoveredOver || ( d->hoveredOver && d->hoverTimer.state() != QTimeLine::Running ) ) ) { QPen borderPen = d->hoveredBorderPen; QColor color = borderPen.color(); color.setAlpha( 255 ); borderPen.setColor( color ); d->hoverIndicatorRectItem->setPen( borderPen ); } } QBrush PopupDropperItem::hoveredFillBrush() const { return d->hoveredFillBrush; } void PopupDropperItem::setHoveredFillBrush( const QBrush &brush ) { d->hoveredFillBrush = brush; d->customHoveredFillBrush = true; if( d->borderRectItem && ( !d->hoveredOver || ( d->hoveredOver && d->hoverTimer.state() != QTimeLine::Running ) ) ) { QBrush borderBrush = brush; if( !d->hoveredOver ) { QColor brushColor = borderBrush.color(); brushColor.setAlpha( 0 ); borderBrush.setColor( brushColor ); } d->borderRectItem->setBrush( borderBrush ); } } QBrush PopupDropperItem::hoverIndicatorFillBrush() const { return d->hoverIndicatorRectFillBrush; } void PopupDropperItem::setHoverIndicatorFillBrush( const QBrush &brush ) { d->hoverIndicatorRectFillBrush = brush; if( d->hoverIndicatorRectFillItem && ( d->hoveredOver && d->hoverTimer.state() != QTimeLine::Running ) ) d->hoverIndicatorRectFillItem->setBrush( d->hoverIndicatorRectFillBrush ); } bool PopupDropperItem::customBaseTextColor() const { return d->customBaseTextColor; } bool PopupDropperItem::customHoveredTextColor() const { return d->customHoveredTextColor; } bool PopupDropperItem::customHoveredBorderPen() const { return d->customHoveredBorderPen; } bool PopupDropperItem::customHoveredFillBrush() const { return d->customHoveredFillBrush; } qreal PopupDropperItem::subitemOpacity() const { return d->subitemOpacity; } void PopupDropperItem::setSubitemOpacity( qreal opacity ) { if( d->svgItem ) d->svgItem->setOpacity( opacity ); if( d->textItem ) d->textItem->setOpacity( opacity ); if( d->borderRectItem ) d->borderRectItem->setOpacity( opacity ); if( d->hoverIndicatorRectItem ) d->hoverIndicatorRectItem->setOpacity( opacity ); if( d->hoverIndicatorRectFillItem ) d->hoverIndicatorRectFillItem->setOpacity( opacity ); } QGraphicsTextItem* PopupDropperItem::textItem() const { return d->textItem; } void PopupDropperItem::setTextItem( QGraphicsTextItem *textItem ) { d->textItem = textItem; if( d->textItem ) d->textItem->setHtml( d->text ); } QGraphicsRectItem* PopupDropperItem::borderRectItem() const { return d->borderRectItem; } void PopupDropperItem::setBorderRectItem( QGraphicsRectItem *borderRectItem ) { if( !borderRectItem ) return; d->borderRectItem = borderRectItem; if( !d->hoveredOver ) { QPen pen = d->hoveredBorderPen; QColor color = pen.color(); color.setAlpha( 0 ); pen.setColor( color ); d->borderRectItem->setPen( pen ); QBrush brush = d->hoveredFillBrush; color = brush.color(); color.setAlpha( 0 ); brush.setColor( color ); d->borderRectItem->setBrush( brush ); } } QGraphicsSvgItem* PopupDropperItem::svgItem() const { return d->svgItem; } void PopupDropperItem::scaleAndReposSvgItem() { if( !d->svgItem || !d->borderRectItem ) return; if( d->separator ) { d->svgItem->scale( 0, 0 ); d->svgItem->setPos( 0, 0 ); return; } //Need to scale if it is too tall or wide qreal maxheight = d->svgElementRect.height() - ( 2 * d->borderRectItem->pen().width() ); qreal maxwidth = d->svgElementRect.width() - ( 2 * d->borderRectItem->pen().width() ); qreal vertScaleValue = maxheight / d->svgItem->sceneBoundingRect().height(); qreal horizScaleValue = maxwidth / d->svgItem->sceneBoundingRect().width(); qreal scaleValue = vertScaleValue < horizScaleValue ? vertScaleValue : horizScaleValue; d->svgItem->scale( scaleValue, scaleValue ); qreal item_center = ( d->borderRectItem->sceneBoundingRect().height() / 2 ) + d->borderRectItem->pos().y(); if( d->orientation == PopupDropperItem::Left ) { d->svgItem->setPos( d->horizontalOffset, item_center - ( d->svgElementRect.height() / 2 ) ); } else { int rightside; if( !d->pd || d->pd->viewSize().width() == 0 ) rightside = sceneBoundingRect().width(); else rightside = d->pd->viewSize().width(); - d->svgItem->setPos( - rightside - - d->svgItem->sceneBoundingRect().width() - - d->horizontalOffset - , item_center - ( d->svgElementRect.height() / 2 ) ); + d->svgItem->setPos( + rightside + - d->svgItem->sceneBoundingRect().width() + - d->horizontalOffset + , item_center - ( d->svgElementRect.height() / 2 ) ); } } void PopupDropperItem::reposTextItem() { if( !d->textItem || !d->borderRectItem ) return; d->textItem->setFont( d->font ); qreal item_vert_center = ( d->borderRectItem->sceneBoundingRect().height() / 2 ) + d->borderRectItem->pos().y(); if( d->separator ) { if( d->text.isEmpty() ) return; qreal width = d->textItem->textWidth(); if( width > d->borderRectItem->sceneBoundingRect().width() ) d->textItem->setTextWidth( d->borderRectItem->sceneBoundingRect().width() ); qreal offset = ( d->borderRectItem->sceneBoundingRect().width() - width ) / 2; d->textItem->setPos( offset, item_vert_center - ( d->textItem->sceneBoundingRect().height() / 2 ) ); return; } int offsetPos = d->horizontalOffset + d->textOffset + d->svgElementRect.width(); d->textItem->setPos( ( d->orientation == PopupDropperItem::Left ? offsetPos : 0 ) , item_vert_center - ( d->textItem->sceneBoundingRect().height() / 2 ) ); if( d->textProtection == PopupDropperItem::ScaleFont ) { QFontMetrics fm( d->textItem->font() ); qreal desiredWidth = d->borderRectItem->sceneBoundingRect().width() - offsetPos; while( d->textItem->font().pointSize() > 1 && ( fm.width( d->textItem->toPlainText() ) > desiredWidth || fm.height() > d->textItem->boundingRect().height() ) ) { QFont font = d->textItem->font(); font.setPointSize( font.pointSize() - 1 ); d->textItem->setFont( font ); fm = QFontMetrics( font ); } } else if( d->textProtection == PopupDropperItem::MultiLine && ( d->textItem->textWidth() == -1 || d->textItem->textWidth() > ( d->borderRectItem->sceneBoundingRect().width() - offsetPos ) ) ) { d->textItem->setTextWidth( d->borderRectItem->sceneBoundingRect().width() - offsetPos ); reposTextItem(); } } void PopupDropperItem::reposHoverFillRects() { if( !d->hoverIndicatorRectItem || !d->hoverIndicatorRectFillItem || !d->textItem || !d->borderRectItem ) return; if( d->separator ) { d->hoverIndicatorRectItem->setRect( 0, 0, 0, 0 ); d->hoverIndicatorRectFillItem->setRect( 0, 0, 0, 0 ); return; } //qDebug() << "\n\nPUDItem boundingRect().width() = " << boundingRect().width(); qreal startx, starty, endx, endy, item_center; //int rightside = d->borderRectItem ? d->borderRectItem->boundingRect().width() : boundingRect().width(); if( d->orientation == PopupDropperItem::Left ) { startx = d->horizontalOffset - d->hoverIndicatorRectWidth - ( 2 * d->hoverIndicatorRectItem->pen().width() ); } else { int rightside = (!d->pd || d->pd->viewSize().width() == 0 ) ? sceneBoundingRect().width() : d->pd->viewSize().width(); //qDebug() << "right side = " << rightside; startx = rightside - d->horizontalOffset + d->hoverIndicatorRectWidth - ( 2 * d->hoverIndicatorRectItem->pen().width() ); } item_center = ( d->borderRectItem->sceneBoundingRect().height() / 2 ) + d->borderRectItem->pos().y(); starty = item_center - ( d->svgElementRect.height() / 2 ); endx = d->hoverIndicatorRectWidth - ( 2 * d->hoverIndicatorRectItem->pen().width() ); endy = d->svgElementRect.height(); QRectF indicatorRect( startx, starty, endx, endy ); d->hoverIndicatorRectItem->setRect( indicatorRect ); QRectF indicatorFillRect( indicatorRect.left() + d->hoverIndicatorRectItem->pen().width() , indicatorRect.bottom() - d->hoverIndicatorRectItem->pen().width() , indicatorRect.width() - ( 2 * d->hoverIndicatorRectItem->pen().width() ) , 0 ); d->hoverIndicatorRectFillItem->setRect( indicatorFillRect ); } QSvgRenderer* PopupDropperItem::sharedRenderer() const { return d->sharedRenderer; } void PopupDropperItem::setSharedRenderer( QSvgRenderer *renderer ) { d->sharedRenderer = renderer; if( renderer && d->svgItem ) { d->svgItem->setSharedRenderer( renderer ); d->svgItem->setElementId( d->elementId ); if( !d->svgItem->elementId().isEmpty() && d->svgItem->renderer()->elementExists( d->svgItem->elementId() ) ) { d->svgItem->show(); fullUpdate(); } } } QString PopupDropperItem::elementId() const { return d->elementId; } void PopupDropperItem::setElementId( const QString &id ) { //qDebug() << "Element ID being set: " << id; d->elementId = id; if( id.isEmpty() ) { d->svgItem->hide(); fullUpdate(); } else if( d->svgItem && d->svgItem->renderer() && d->svgItem->renderer()->elementExists( id )) { d->svgItem->setElementId( id ); d->svgItem->show(); fullUpdate(); } } QRect PopupDropperItem::svgElementRect() const { return d->svgElementRect; } void PopupDropperItem::setSvgElementRect( const QRect &rect ) { d->svgElementRect = rect; } int PopupDropperItem::horizontalOffset() const { return d->horizontalOffset; } void PopupDropperItem::setHorizontalOffset( int offset ) { d->horizontalOffset = offset; } int PopupDropperItem::textOffset() const { return d->textOffset; } void PopupDropperItem::setTextOffset( int offset ) { d->textOffset = offset; } bool PopupDropperItem::isSeparator() const { return d->separator; } void PopupDropperItem::setSeparator( bool separator ) { d->separator = separator; } PopupDropperItem::SeparatorStyle PopupDropperItem::separatorStyle() const { return d->separatorStyle; } void PopupDropperItem::setSeparatorStyle( SeparatorStyle style ) { d->separatorStyle = style; } bool PopupDropperItem::hasLineSeparatorPen() const { return d->hasLineSeparatorPen; } QPen PopupDropperItem::lineSeparatorPen() const { return d->lineSeparatorPen; } void PopupDropperItem::setLineSeparatorPen( const QPen &pen ) { d->lineSeparatorPen = pen; } void PopupDropperItem::clearLineSeparatorPen() { d->lineSeparatorPen = QPen(); d->hasLineSeparatorPen = false; } int PopupDropperItem::hoverMsecs() const { return d->hoverTimer.duration(); } void PopupDropperItem::setHoverMsecs( const int msecs ) { d->hoverTimer.setDuration( msecs ); } void PopupDropperItem::hoverEntered() { if( d->hoverIndicatorRectItem && d->hoverIndicatorRectFillItem && d->hoverIndicatorShowStyle != PopupDropperItem::Never ) { d->hoverIndicatorRectFillItem->show(); } d->hoverTimer.stop(); d->hoverTimer.setDirection( QTimeLine::Forward ); d->hoveredOver = true; d->hoverTimer.start(); } void PopupDropperItem::hoverLeft() { d->hoverTimer.stop(); d->hoverTimer.setDirection( QTimeLine::Backward ); d->hoveredOver = false; if( d->hoverTimer.currentFrame() != 0 ) d->hoverTimer.start(); } int PopupDropperItem::borderWidth() const { return d->borderWidth; } void PopupDropperItem::setBorderWidth( int borderWidth ) { d->borderWidth = borderWidth; d->hoveredBorderPen.setWidth( borderWidth ); if( d->borderRectItem ) { d->borderRectItem->setPen( d->hoveredBorderPen ); } } int PopupDropperItem::hoverIndicatorRectWidth() const { return d->hoverIndicatorRectWidth; } void PopupDropperItem::setHoverIndicatorRectWidth( int hoverIndicatorRectWidth ) { d->hoverIndicatorRectWidth = hoverIndicatorRectWidth; if( d->hoverIndicatorRectItem ) { QPen pen = d->hoverIndicatorRectItem->pen(); pen.setWidth( d->hoverIndicatorRectWidth ); d->hoverIndicatorRectItem->setPen( pen ); } } bool PopupDropperItem::isSubmenuTrigger() const { return d->submenuTrigger; } void PopupDropperItem::setSubmenuTrigger( bool trigger ) { d->submenuTrigger = trigger; } void PopupDropperItem::setPopupDropper( PopupDropper* pd ) { d->pd = pd; } //bool PopupDropperItem::operator<( const PopupDropperItem &other ) const //{ // return d->text < other.text(); //} void PopupDropperItem::dropped( QDropEvent *event ) //virtual SLOT { Q_UNUSED( event ); d->hoverTimer.stop(); //qDebug() << "PopupDropperItem drop detected"; if( d->action ) { //qDebug() << "Triggering action"; d->action->activate( QAction::Trigger ); } } void PopupDropperItem::hoverFinished() //SLOT { if( d->separator ) return; //qDebug() << "direction = forwards ? " << ( d->hoverTimer.direction() == QTimeLine::Forward ? "yes" : "no" ); if( d->action && d->hoverTimer.direction() == QTimeLine::Forward ) d->action->activate( QAction::Hover ); if( d->hoverTimer.direction() == QTimeLine::Forward ) d->textItem->setDefaultTextColor( d->hoveredTextColor ); else d->textItem->setDefaultTextColor( d->baseTextColor ); //Something is messed up in QTimeLine...I get hoverFinished immediately after doing a hoverLeft, but only sometimes...hence the check //to see if the timeline isn't running if( d->hoverIndicatorRectFillItem && d->hoverTimer.state() == QTimeLine::NotRunning && d->hoverTimer.direction() == QTimeLine::Backward ) { d->hoverIndicatorRectFillItem->hide(); if( d->hoverIndicatorRectItem && d->hoverIndicatorShowStyle != PopupDropperItem::AlwaysShow ) d->hoverIndicatorRectItem->hide(); } if( d->pd ) d->pd->updateAllOverlays(); } void PopupDropperItem::hoverFrameChanged( int frame ) //SLOT { if( d->separator ) return; //qDebug() << "hoverFrameChanged for " << static_cast(this) << ", frame = " << frame; int range = d->hoverTimer.endFrame() - d->hoverTimer.startFrame(); qreal multiplier = ( 1.0 * frame ) / range; int r = (int)( ( d->hoveredTextColor.red() - d->baseTextColor.red() ) * multiplier ) + d->baseTextColor.red(); int g = (int)( ( d->hoveredTextColor.green() - d->baseTextColor.green() ) * multiplier ) + d->baseTextColor.green(); int b = (int)( ( d->hoveredTextColor.blue() - d->baseTextColor.blue() ) * multiplier ) + d->baseTextColor.blue(); int a = (int)( ( d->hoveredTextColor.alpha() - d->baseTextColor.alpha() ) * multiplier ) + d->baseTextColor.alpha(); d->textItem->setDefaultTextColor( QColor( r, g, b, a ) ); QColor borderColor = d->hoveredBorderPen.color(); borderColor.setAlpha( (int)( borderColor.alpha() * multiplier ) ); QPen pen = d->borderRectItem->pen(); pen.setColor( borderColor ); d->borderRectItem->setPen( pen ); if( d->hoverIndicatorRectItem && d->hoverIndicatorShowStyle == PopupDropperItem::OnHover ) { d->hoverIndicatorRectItem->setPen( pen ); d->hoverIndicatorRectItem->show(); } QColor fillColor = d->hoveredFillBrush.color(); QBrush brush = d->borderRectItem->brush(); fillColor.setAlpha( (int)( fillColor.alpha() * multiplier ) ); brush.setColor( fillColor ); d->borderRectItem->setBrush( brush ); if( d->hoverIndicatorRectItem && d->hoverIndicatorRectFillItem && d->hoverIndicatorShowStyle != PopupDropperItem::Never ) { int hoverIndicatorPenWidth = d->hoverIndicatorRectItem->pen().width(); QRectF rect = d->hoverIndicatorRectFillItem->rect(); QRectF outerRect = d->hoverIndicatorRectItem->rect(); rect.setTop( ( multiplier * -1 * ( outerRect.bottom() - outerRect.top() - ( 2 * hoverIndicatorPenWidth ) ) ) + outerRect.bottom() - hoverIndicatorPenWidth ); d->hoverIndicatorRectFillItem->setRect( rect ); d->hoverIndicatorRectFillItem->setBrush( d->hoverIndicatorRectFillBrush ); d->hoverIndicatorRectFillItem->show(); //qDebug() << "hoverIndicatorRectFillItem = " << d->hoverIndicatorRectFillItem; } if( d->pd ) d->pd->updateAllOverlays(); } void PopupDropperItem::fullUpdate() { scaleAndReposSvgItem(); reposTextItem(); reposHoverFillRects(); if( d->pd ) d->pd->updateAllOverlays(); } QRectF PopupDropperItem::boundingRect() const { if( d->borderRectItem ) return d->borderRectItem->boundingRect(); else if( d->pd && d->pd->viewSize().width() != 0 ) return QRectF( 0, 0, d->pd->viewSize().width(), d->svgElementRect.height() ); else return QRectF( 0, 0, d->svgElementRect.width(), d->svgElementRect.height() ); } void PopupDropperItem::paint( QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget ) { Q_UNUSED( painter ) Q_UNUSED( option ) Q_UNUSED( widget ) return; } #include "PopupDropperItem.moc" diff --git a/src/core-impl/collections/db/sql/DatabaseUpdater.cpp b/src/core-impl/collections/db/sql/DatabaseUpdater.cpp index 59a78f568b..184a89038d 100644 --- a/src/core-impl/collections/db/sql/DatabaseUpdater.cpp +++ b/src/core-impl/collections/db/sql/DatabaseUpdater.cpp @@ -1,1097 +1,1110 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "DatabaseUpdater.h" #include "amarokconfig.h" #include "core/support/Debug.h" #include #include "SqlCollection.h" #include #include #include #include #include #include #include static const int DB_VERSION = 15; int DatabaseUpdater::expectedDatabaseVersion() { return DB_VERSION; } DatabaseUpdater::DatabaseUpdater( Collections::SqlCollection *collection ) : m_collection( collection ) , m_debugDatabaseContent( false ) { m_debugDatabaseContent = KGlobal::config()->group( "SqlCollection" ).readEntry( "DebugDatabaseContent", false ); } DatabaseUpdater::~DatabaseUpdater() { //nothing to do } bool DatabaseUpdater::needsUpdate() const { return adminValue( "DB_VERSION" ) != DB_VERSION; } bool DatabaseUpdater::schemaExists() const { return adminValue( "DB_VERSION" ) != 0; } bool DatabaseUpdater::update() { DEBUG_BLOCK int dbVersion = adminValue( "DB_VERSION" ); debug() << "Database version: " << dbVersion; if( dbVersion == 0 ) { createTables(); QString query = QString( "INSERT INTO admin(component, version) VALUES ('DB_VERSION', %1);" ).arg( DB_VERSION ); m_collection->sqlStorage()->query( query ); return true; } if( dbVersion < DB_VERSION ) { debug() << "Database out of date: database version is" << dbVersion << ", current version is" << DB_VERSION; switch( dbVersion ) { case 1: upgradeVersion1to2(); + /* Falls through. */ case 2: upgradeVersion2to3(); + /* Falls through. */ case 3: upgradeVersion3to4(); + /* Falls through. */ case 4: upgradeVersion4to5(); + /* Falls through. */ case 5: upgradeVersion5to6(); + /* Falls through. */ case 6: upgradeVersion6to7(); + /* Falls through. */ case 7: upgradeVersion7to8(); + /* Falls through. */ case 8: //removes stray rows from albums that were caused by the initial full scan upgradeVersion8to9(); + /* Falls through. */ case 9: //removes stray rows from albums that were caused by the initial full scan upgradeVersion9to10(); + /* Falls through. */ case 10: upgradeVersion10to11(); + /* Falls through. */ case 11: upgradeVersion11to12(); + /* Falls through. */ case 12: upgradeVersion12to13(); + /* Falls through. */ case 13: upgradeVersion13to14(); + /* Falls through. */ case 14: upgradeVersion14to15(); dbVersion = 15; // be sure to update this manually when introducing new version! } QString query = QString( "UPDATE admin SET version = %1 WHERE component = 'DB_VERSION';" ).arg( dbVersion ); m_collection->sqlStorage()->query( query ); //NOTE: A rescan will be triggered automatically as a result of an upgrade. Don't trigger it here, as the //collection isn't fully initialized and this will trigger a crash/assert. return true; } if( dbVersion > DB_VERSION ) { KMessageBox::error(0, "

The Amarok collection database was created by a newer version of Amarok, " "and this version of Amarok cannot use it.

", "Database Type Unknown"); // FIXME: maybe we should tell them how to delete the database? // FIXME: exit() may be a little harsh, but QCoreApplication::exit() doesn't seem to work exit(1); } return false; } void DatabaseUpdater::upgradeVersion1to2() { DEBUG_BLOCK m_collection->sqlStorage()->query( "ALTER TABLE tracks " "ADD COLUMN albumgain FLOAT, " "ADD COLUMN albumpeakgain FLOAT, " "ADD COLUMN trackgain FLOAT," "ADD COLUMN trackpeakgain FLOAT;" ); } void DatabaseUpdater::upgradeVersion2to3() { DEBUG_BLOCK; SqlStorage *storage = m_collection->sqlStorage(); storage->query( "DROP TABLE devices;" ); QString create = "CREATE TABLE devices " "(id " + storage->idType() + ",type " + storage->textColumnType() + ",label " + storage->textColumnType() + ",lastmountpoint " + storage->textColumnType() + ",uuid " + storage->textColumnType() + ",servername " + storage->textColumnType() + ",sharename " + storage->textColumnType() + ");"; storage->query( create ); storage->query( "CREATE INDEX devices_type ON devices( type );" ); storage->query( "CREATE UNIQUE INDEX devices_uuid ON devices( uuid );" ); storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); } void DatabaseUpdater::upgradeVersion3to4() { SqlStorage *storage = m_collection->sqlStorage(); storage->query( "CREATE TABLE statistics_permanent " "(url " + storage->exactTextColumnType() + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER)" ); storage->query( "CREATE UNIQUE INDEX ON statistics_permanent(url)" ); //Note: the above index query is invalid, but kept here for posterity storage->query( "CREATE TABLE statistics_tag " "(name " + storage->textColumnType() + ",artist " + storage->textColumnType() + ",album " + storage->textColumnType() + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER)" ); storage->query( "CREATE UNIQUE INDEX ON statistics_tag(name,artist,album)" ); //Note: the above index query is invalid, but kept here for posterity } void DatabaseUpdater::upgradeVersion4to5() { SqlStorage *storage = m_collection->sqlStorage(); //first the database storage->query( "ALTER DATABASE amarok DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci" ); //now the tables //first, drop tables that can easily be recreated by doing an update QStringList dropTables; dropTables << "jamendo_albums" << "jamendo_artists" << "jamendo_genre" << "jamendo_tracks"; dropTables << "magnatune_albums" << "magnatune_artists" << "magnatune_genre" << "magnatune_moods" << "magnatune_tracks"; dropTables << "opmldirectory_albums" << "opmldirectory_artists" << "opmldirectory_genre" << "opmldirectory_tracks"; foreach( const QString &table, dropTables ) storage->query( "DROP TABLE " + table ); //now, the rest of them QStringList tables; tables << "admin" << "albums" << "amazon" << "artists" << "bookmark_groups" << "bookmarks"; tables << "composers" << "devices" << "directories" << "genres" << "images" << "labels" << "lyrics"; tables << "playlist_groups" << "playlist_tracks" << "playlists"; tables << "podcastchannels" << "podcastepisodes"; tables << "statistics" << "statistics_permanent" << "statistics_tag"; tables << "tracks" << "urls" << "urls_labels" << "years"; foreach( const QString &table, tables ) storage->query( "ALTER TABLE " + table + " DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci" ); //now the columns (ugh) //first, varchar typedef QPair vcpair; QMultiMap columns; columns.insert( "admin", vcpair( "component", 255 ) ); columns.insert( "albums", vcpair( "name", textColumnLength() ) ); columns.insert( "amazon", vcpair( "asin", 20 ) ); columns.insert( "amazon", vcpair( "locale", 2 ) ); columns.insert( "amazon", vcpair( "filename", 33 ) ); columns.insert( "artists", vcpair( "name", textColumnLength() ) ); columns.insert( "bookmark_groups", vcpair( "name", 255 ) ); columns.insert( "bookmark_groups", vcpair( "description", 255 ) ); columns.insert( "bookmark_groups", vcpair( "custom", 255 ) ); columns.insert( "bookmarks", vcpair( "name", 255 ) ); columns.insert( "bookmarks", vcpair( "url", 1024 ) ); columns.insert( "bookmarks", vcpair( "description", 1024 ) ); columns.insert( "bookmarks", vcpair( "custom", 255 ) ); columns.insert( "composers", vcpair( "name", textColumnLength() ) ); columns.insert( "devices", vcpair( "type", 255 ) ); columns.insert( "devices", vcpair( "label", 255 ) ); columns.insert( "devices", vcpair( "lastmountpoint", 255 ) ); columns.insert( "devices", vcpair( "uuid", 255 ) ); columns.insert( "devices", vcpair( "servername", 255 ) ); columns.insert( "devices", vcpair( "sharename", 255 ) ); columns.insert( "directories", vcpair( "dir", 1024 ) ); columns.insert( "genres", vcpair( "name", 255 ) ); columns.insert( "images", vcpair( "path", 255 ) ); columns.insert( "labels", vcpair( "label", textColumnLength() ) ); columns.insert( "lyrics", vcpair( "url", 1024 ) ); columns.insert( "playlist_groups", vcpair( "name", 255 ) ); columns.insert( "playlist_groups", vcpair( "description", 255 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1024 ) ); columns.insert( "playlist_tracks", vcpair( "title", 255 ) ); columns.insert( "playlist_tracks", vcpair( "album", 255 ) ); columns.insert( "playlist_tracks", vcpair( "artist", 255 ) ); columns.insert( "playlist_tracks", vcpair( "uniqueid", 128 ) ); columns.insert( "playlists", vcpair( "name", 255 ) ); columns.insert( "playlists", vcpair( "description", 255 ) ); columns.insert( "playlists", vcpair( "urlid", 1024 ) ); columns.insert( "podcastchannels", vcpair( "copyright", 255 ) ); columns.insert( "podcastchannels", vcpair( "directory", 255 ) ); columns.insert( "podcastchannels", vcpair( "labels", 255 ) ); columns.insert( "podcastchannels", vcpair( "subscribedate", 255 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1024 ) ); columns.insert( "podcastepisodes", vcpair( "mimetype", 255 ) ); columns.insert( "podcastepisodes", vcpair( "pubdate", 255 ) ); columns.insert( "statistics_permanent", vcpair( "url", 1024 ) ); columns.insert( "statistics_tag", vcpair( "name", 255 ) ); columns.insert( "statistics_tag", vcpair( "artist", 255 ) ); columns.insert( "tracks", vcpair( "title", textColumnLength() ) ); columns.insert( "urls", vcpair( "rpath", 1024 ) ); columns.insert( "urls", vcpair( "uniqueid", 128 ) ); columns.insert( "years", vcpair( "name", textColumnLength() ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " VARBINARY(" + QString::number( i.value().second ) + ')' ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL" ); } columns.clear(); //text fields, not varchars columns.insert( "lyrics", vcpair( "lyrics", 0 ) ); columns.insert( "podcastchannels", vcpair( "url", 0 ) ); columns.insert( "podcastchannels", vcpair( "title", 0 ) ); columns.insert( "podcastchannels", vcpair( "weblink", 0 ) ); columns.insert( "podcastchannels", vcpair( "image", 0 ) ); columns.insert( "podcastchannels", vcpair( "description", 0 ) ); columns.insert( "podcastepisodes", vcpair( "url", 0 ) ); columns.insert( "podcastepisodes", vcpair( "localurl", 0 ) ); columns.insert( "podcastepisodes", vcpair( "title", 0 ) ); columns.insert( "podcastepisodes", vcpair( "subtitle", 0 ) ); columns.insert( "podcastepisodes", vcpair( "description", 0 ) ); columns.insert( "tracks", vcpair( "comment", 0 ) ); storage->query( "DROP INDEX url_podchannel ON podcastchannels" ); storage->query( "DROP INDEX url_podepisode ON podcastepisodes" ); storage->query( "DROP INDEX localurl_podepisode ON podcastepisodes" ); for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " BLOB" ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " TEXT CHARACTER SET utf8 NOT NULL" ); } storage->query( "CREATE FULLTEXT INDEX url_podchannel ON podcastchannels( url )" ); storage->query( "CREATE FULLTEXT INDEX url_podepisode ON podcastepisodes( url )" ); storage->query( "CREATE FULLTEXT INDEX localurl_podepisode ON podcastepisodes( localurl )" ); } void DatabaseUpdater::upgradeVersion5to6() { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); //first, drop tables that can easily be recreated by doing an update QStringList dropTables; dropTables << "jamendo_albums" << "jamendo_artists" << "jamendo_genre" << "jamendo_tracks"; dropTables << "magnatune_albums" << "magnatune_artists" << "magnatune_genre" << "magnatune_moods" << "magnatune_tracks"; dropTables << "opmldirectory_albums" << "opmldirectory_artists" << "opmldirectory_genre" << "opmldirectory_tracks"; foreach( const QString &table, dropTables ) storage->query( "DROP TABLE " + table ); //now, the rest of them QStringList tables; tables << "admin" << "albums" << "amazon" << "artists" << "bookmark_groups" << "bookmarks"; tables << "composers" << "devices" << "directories" << "genres" << "images" << "labels" << "lyrics"; tables << "playlist_groups" << "playlist_tracks" << "playlists"; tables << "podcastchannels" << "podcastepisodes"; tables << "statistics" << "statistics_permanent" << "statistics_tag"; tables << "tracks" << "urls" << "urls_labels" << "years"; foreach( const QString &table, tables ) storage->query( "ALTER TABLE " + table + " ENGINE = MyISAM" ); typedef QPair vcpair; QMultiMap columns; columns.insert( "bookmarks", vcpair( "url", 1000 ) ); columns.insert( "bookmarks", vcpair( "description", 1000 ) ); columns.insert( "directories", vcpair( "dir", 1000 ) ); columns.insert( "lyrics", vcpair( "url", 324 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1000 ) ); columns.insert( "playlists", vcpair( "urlid", 1000 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1000 ) ); columns.insert( "statistics_permanent", vcpair( "url", 324 ) ); columns.insert( "urls", vcpair( "rpath", 324 ) ); columns.insert( "devices", vcpair( "servername", 80 ) ); columns.insert( "devices", vcpair( "sharename", 240 ) ); columns.insert( "statistics_tag", vcpair( "name", 108 ) ); columns.insert( "statistics_tag", vcpair( "artist", 108 ) ); columns.insert( "statistics_tag", vcpair( "album", 108 ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") " ); storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); storage->query( "CREATE UNIQUE INDEX lyrics_url ON lyrics(url);" ); storage->query( "CREATE UNIQUE INDEX urls_id_rpath ON urls(deviceid, rpath);" ); storage->query( "CREATE UNIQUE INDEX stats_tag_name_artist_album ON statistics_tag(name,artist,album)" ); } void DatabaseUpdater::upgradeVersion6to7() { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); typedef QPair vcpair; QMultiMap columns; columns.insert( "directories", vcpair( "dir", 1000 ) ); columns.insert( "urls", vcpair( "rpath", 324 ) ); columns.insert( "statistics_permanent", vcpair( "url", 324 ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") COLLATE utf8_bin NOT NULL" ); } columns.clear(); } void DatabaseUpdater::upgradeVersion7to8() { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); QHash< int, int > trackLengthHash; // First, get the lengths from the db and insert them into a hash const QStringList result = storage->query( "SELECT id, length FROM tracks" ); QListIterator iter(result); while( iter.hasNext() ) trackLengthHash.insert( iter.next().toInt(), iter.next().toInt() ); // Now Iterate over the hash, and insert each track back in, changing the length to milliseconds QHashIterator iter2( trackLengthHash ); const QString updateString = QString( "UPDATE tracks SET length=%1 WHERE id=%2 ;"); while( iter2.hasNext() ) { iter2.next(); debug() << "Running the following query: " << updateString.arg( QString::number( iter2.value() * 1000 ), QString::number( iter2.key() ) ); storage->query( updateString.arg( QString::number( iter2.value() * 1000 ), QString::number( iter2.key() ) ) ); } } void DatabaseUpdater::upgradeVersion8to9() { deleteAllRedundant( "album" ); } void DatabaseUpdater::upgradeVersion9to10() { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); //first the database storage->query( "ALTER DATABASE amarok DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin" ); //now the tables //first, drop tables that can easily be recreated by doing an update QStringList dropTables; dropTables << "jamendo_albums" << "jamendo_artists" << "jamendo_genre" << "jamendo_tracks"; dropTables << "magnatune_albums" << "magnatune_artists" << "magnatune_genre" << "magnatune_moods" << "magnatune_tracks"; dropTables << "opmldirectory_albums" << "opmldirectory_artists" << "opmldirectory_genre" << "opmldirectory_tracks"; foreach( const QString &table, dropTables ) storage->query( "DROP TABLE " + table ); //now, the rest of them QStringList tables; tables << "admin" << "albums" << "amazon" << "artists" << "bookmark_groups" << "bookmarks"; tables << "composers" << "devices" << "directories" << "genres" << "images" << "labels" << "lyrics"; tables << "playlist_groups" << "playlist_tracks" << "playlists"; tables << "podcastchannels" << "podcastepisodes"; tables << "statistics" << "statistics_permanent" << "statistics_tag"; tables << "tracks" << "urls" << "urls_labels" << "years"; foreach( const QString &table, tables ) storage->query( "ALTER TABLE " + table + " DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_bin COLLATE utf8_bin ENGINE = MyISAM" ); //now the columns (ugh) //first, varchar typedef QPair vcpair; QMultiMap columns; columns.insert( "admin", vcpair( "component", 255 ) ); columns.insert( "albums", vcpair( "name", textColumnLength() ) ); columns.insert( "amazon", vcpair( "asin", 20 ) ); columns.insert( "amazon", vcpair( "locale", 2 ) ); columns.insert( "amazon", vcpair( "filename", 33 ) ); columns.insert( "artists", vcpair( "name", textColumnLength() ) ); columns.insert( "bookmark_groups", vcpair( "name", 255 ) ); columns.insert( "bookmark_groups", vcpair( "description", 255 ) ); columns.insert( "bookmark_groups", vcpair( "custom", 255 ) ); columns.insert( "bookmarks", vcpair( "name", 255 ) ); columns.insert( "bookmarks", vcpair( "url", 1000 ) ); columns.insert( "bookmarks", vcpair( "description", 1000 ) ); columns.insert( "bookmarks", vcpair( "custom", 255 ) ); columns.insert( "composers", vcpair( "name", textColumnLength() ) ); columns.insert( "devices", vcpair( "type", 255 ) ); columns.insert( "devices", vcpair( "label", 255 ) ); columns.insert( "devices", vcpair( "lastmountpoint", 255 ) ); columns.insert( "devices", vcpair( "uuid", 255 ) ); columns.insert( "devices", vcpair( "servername", 80 ) ); columns.insert( "devices", vcpair( "sharename", 240 ) ); columns.insert( "directories", vcpair( "dir", 1000 ) ); columns.insert( "genres", vcpair( "name", textColumnLength() ) ); columns.insert( "images", vcpair( "path", 255 ) ); columns.insert( "labels", vcpair( "label", textColumnLength() ) ); columns.insert( "lyrics", vcpair( "url", 324 ) ); columns.insert( "playlist_groups", vcpair( "name", 255 ) ); columns.insert( "playlist_groups", vcpair( "description", 255 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1000 ) ); columns.insert( "playlist_tracks", vcpair( "title", 255 ) ); columns.insert( "playlist_tracks", vcpair( "album", 255 ) ); columns.insert( "playlist_tracks", vcpair( "artist", 255 ) ); columns.insert( "playlist_tracks", vcpair( "uniqueid", 128 ) ); columns.insert( "playlists", vcpair( "name", 255 ) ); columns.insert( "playlists", vcpair( "description", 255 ) ); columns.insert( "playlists", vcpair( "urlid", 1000 ) ); columns.insert( "podcastchannels", vcpair( "copyright", 255 ) ); columns.insert( "podcastchannels", vcpair( "directory", 255 ) ); columns.insert( "podcastchannels", vcpair( "labels", 255 ) ); columns.insert( "podcastchannels", vcpair( "subscribedate", 255 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1000 ) ); columns.insert( "podcastepisodes", vcpair( "mimetype", 255 ) ); columns.insert( "podcastepisodes", vcpair( "pubdate", 255 ) ); columns.insert( "statistics_permanent", vcpair( "url", 324 ) ); columns.insert( "statistics_tag", vcpair( "name", 108 ) ); columns.insert( "statistics_tag", vcpair( "artist", 108 ) ); columns.insert( "statistics_tag", vcpair( "album", 108 ) ); columns.insert( "tracks", vcpair( "title", textColumnLength() ) ); columns.insert( "urls", vcpair( "rpath", 324 ) ); columns.insert( "urls", vcpair( "uniqueid", 128 ) ); columns.insert( "years", vcpair( "name", textColumnLength() ) ); QMultiMap::const_iterator i, iEnd; for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " VARBINARY(" + QString::number( i.value().second ) + ')' ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " VARCHAR(" + QString::number( i.value().second ) + ") CHARACTER SET utf8 COLLATE utf8_bin NOT NULL" ); } storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); storage->query( "CREATE UNIQUE INDEX lyrics_url ON lyrics(url);" ); storage->query( "CREATE UNIQUE INDEX urls_id_rpath ON urls(deviceid, rpath);" ); storage->query( "CREATE UNIQUE INDEX stats_tag_name_artist_album ON statistics_tag(name,artist,album)" ); columns.clear(); //text fields, not varchars columns.insert( "lyrics", vcpair( "lyrics", 0 ) ); columns.insert( "podcastchannels", vcpair( "url", 0 ) ); columns.insert( "podcastchannels", vcpair( "title", 0 ) ); columns.insert( "podcastchannels", vcpair( "weblink", 0 ) ); columns.insert( "podcastchannels", vcpair( "image", 0 ) ); columns.insert( "podcastchannels", vcpair( "description", 0 ) ); columns.insert( "podcastepisodes", vcpair( "url", 0 ) ); columns.insert( "podcastepisodes", vcpair( "localurl", 0 ) ); columns.insert( "podcastepisodes", vcpair( "title", 0 ) ); columns.insert( "podcastepisodes", vcpair( "subtitle", 0 ) ); columns.insert( "podcastepisodes", vcpair( "description", 0 ) ); columns.insert( "tracks", vcpair( "comment", 0 ) ); storage->query( "DROP INDEX url_podchannel ON podcastchannels" ); storage->query( "DROP INDEX url_podepisode ON podcastepisodes" ); storage->query( "DROP INDEX localurl_podepisode ON podcastepisodes" ); for( i = columns.constBegin(), iEnd = columns.constEnd(); i != iEnd; ++i ) { storage->query( "ALTER TABLE " + i.key() + " MODIFY " + i.value().first + " BLOB" ); storage->query( "ALTER IGNORE TABLE " + i.key() + " MODIFY " + i.value().first + " TEXT CHARACTER SET utf8 COLLATE utf8_bin NOT NULL" ); } storage->query( "CREATE FULLTEXT INDEX url_podchannel ON podcastchannels( url )" ); storage->query( "CREATE FULLTEXT INDEX url_podepisode ON podcastepisodes( url )" ); storage->query( "CREATE FULLTEXT INDEX localurl_podepisode ON podcastepisodes( localurl )" ); } void DatabaseUpdater::upgradeVersion10to11() { DEBUG_BLOCK //OK, this isn't really a database upgrade, but it does affect scanning. //New default is for the charset detector not to run; but those that have existing collection //won't like it if suddenly that changes their behavior, so set to true for existing collections AmarokConfig::setUseCharsetDetector( true ); } void DatabaseUpdater::upgradeVersion11to12() { DEBUG_BLOCK //Counteract the above -- force it off for everyone except those explicitly enabling it. AmarokConfig::setUseCharsetDetector( false ); } void DatabaseUpdater::upgradeVersion12to13() { DEBUG_BLOCK m_collection->sqlStorage()->query( "UPDATE urls SET uniqueid = REPLACE(uniqueid, 'MB_', 'mb-');" ); } void DatabaseUpdater::upgradeVersion13to14() { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); /* Following commands transition lyrics table from text-based urls (in fact just rpath * parts) to references to urls table. */ // first, rename column storage->query( "ALTER TABLE lyrics CHANGE url rpath VARCHAR(324) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL" ); // add integer column for url id storage->query( "ALTER TABLE lyrics ADD COLUMN url INT NULL DEFAULT NULL FIRST" ); // try to extract url id from urls table using rpath storage->query( "UPDATE lyrics l SET l.url = (SELECT u.id FROM urls u WHERE u.rpath = l.rpath LIMIT 1)" ); // delete entries with no matches in urls table; these should be just stale ones storage->query( "DELETE FROM lyrics WHERE url IS NULL" ); // make the url columnt non-null storage->query( "ALTER TABLE lyrics MODIFY url INT NOT NULL" ); // select duplicate ids into temporary table storage->query( "CREATE TEMPORARY TABLE duplicate_lyrics_ids ( id INT NOT NULL ) " "ENGINE=MEMORY SELECT dupl.id FROM lyrics orig " "LEFT JOIN lyrics dupl ON dupl.url = orig.url AND dupl.id > orig.id" ); // delete duplicate lyrics entries storage->query( "DELETE FROM lyrics WHERE id IN (SELECT id FROM duplicate_lyrics_ids)" ); // drop unwanted columns along with indexes defined on them storage->query( "ALTER TABLE lyrics DROP id, DROP rpath" ); // add primary key; should definitely not fail as we have removed duplicate entries storage->query( "ALTER TABLE lyrics ADD PRIMARY KEY(url)" ); } void DatabaseUpdater::upgradeVersion14to15() { /* This update solves bug 302837. In short, updates * 4 -> 5, 5 -> 6, 6 -> 7 and 9 -> 10 ignored NULL status of some columns and replaced * them with NOT NULL columns, causing various consequences, one of them is Dynamic * Collection not working. Fix it back. * * A list of columns to fix was obtained by comparing a database created by * Amarok 2.1.1 and then upgraded to current version with a db freshly created by * Amarok 2.6-git. */ DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); // zero length = TEXT datatype typedef QPair vcpair; QMultiMap columns; columns.insert( "admin", vcpair( "component", 255 ) ); columns.insert( "devices", vcpair( "type", 255 ) ); columns.insert( "devices", vcpair( "label", 255 ) ); columns.insert( "devices", vcpair( "lastmountpoint", 255 ) ); columns.insert( "devices", vcpair( "uuid", 255 ) ); columns.insert( "devices", vcpair( "servername", 80 ) ); columns.insert( "devices", vcpair( "sharename", 240 ) ); columns.insert( "labels", vcpair( "label", textColumnLength() ) ); columns.insert( "lyrics", vcpair( "lyrics", 0 ) ); columns.insert( "playlists", vcpair( "name", 255 ) ); columns.insert( "playlists", vcpair( "description", 255 ) ); columns.insert( "playlists", vcpair( "urlid", 1000 ) ); columns.insert( "playlist_groups", vcpair( "name", 255 ) ); columns.insert( "playlist_groups", vcpair( "description", 255 ) ); columns.insert( "playlist_tracks", vcpair( "url", 1000 ) ); columns.insert( "playlist_tracks", vcpair( "title", 255 ) ); columns.insert( "playlist_tracks", vcpair( "album", 255 ) ); columns.insert( "playlist_tracks", vcpair( "artist", 255 ) ); columns.insert( "playlist_tracks", vcpair( "uniqueid", 128 ) ); columns.insert( "podcastchannels", vcpair( "url", 0 ) ); columns.insert( "podcastchannels", vcpair( "title", 0 ) ); columns.insert( "podcastchannels", vcpair( "weblink", 0 ) ); columns.insert( "podcastchannels", vcpair( "image", 0 ) ); columns.insert( "podcastchannels", vcpair( "description", 0 ) ); columns.insert( "podcastchannels", vcpair( "copyright", 255 ) ); columns.insert( "podcastchannels", vcpair( "directory", 255 ) ); columns.insert( "podcastchannels", vcpair( "labels", 255 ) ); columns.insert( "podcastchannels", vcpair( "subscribedate", 255 ) ); columns.insert( "podcastepisodes", vcpair( "url", 0 ) ); columns.insert( "podcastepisodes", vcpair( "localurl", 0 ) ); columns.insert( "podcastepisodes", vcpair( "guid", 1000 ) ); columns.insert( "podcastepisodes", vcpair( "title", 0 ) ); columns.insert( "podcastepisodes", vcpair( "subtitle", 0 ) ); columns.insert( "podcastepisodes", vcpair( "description", 0 ) ); columns.insert( "podcastepisodes", vcpair( "mimetype", 255 ) ); columns.insert( "podcastepisodes", vcpair( "pubdate", 255 ) ); columns.insert( "statistics_tag", vcpair( "name", 108 ) ); columns.insert( "statistics_tag", vcpair( "artist", 108 ) ); columns.insert( "statistics_tag", vcpair( "album", 108 ) ); columns.insert( "tracks", vcpair( "title", textColumnLength() ) ); columns.insert( "tracks", vcpair( "comment", 0 ) ); columns.insert( "urls", vcpair( "uniqueid", 128 ) ); QMapIterator it( columns ); while( it.hasNext() ) { it.next(); QString table = it.key(); QString column = it.value().first; int length = it.value().second; QString query; if( length > 0 ) query = QString( "ALTER TABLE `%1` CHANGE `%2` `%2` VARCHAR(%3) CHARACTER SET utf8 " "COLLATE utf8_bin NULL DEFAULT NULL" ).arg( table, column ).arg( length ); else query = QString( "ALTER TABLE `%1` CHANGE `%2` `%2` TEXT CHARACTER SET utf8 " "COLLATE utf8_bin" ).arg( table, column ); storage->query( query ); } // there may be a stale unique index on the urls table, remove it if it is there: QStringList results = storage->query( "SHOW CREATE TABLE urls" ); bool oldIndexFound = results.value( 1 ).contains( "UNIQUE KEY `uniqueid`" ); if( oldIndexFound ) { debug() << "dropping obsolete INDEX uniqueid on table urls"; storage->query( "DROP INDEX uniqueid ON urls" ); } } void DatabaseUpdater::cleanupDatabase() { // maybe clean up redundant information here? } void DatabaseUpdater::checkTables( bool full ) { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); QStringList res = storage->query( "SHOW TABLES" ); if( res.count() > 0 ) { foreach( const QString &table, res ) storage->query( "CHECK TABLE " + table + ( full ? " EXTENDED;" : " MEDIUM;" ) ); } } void DatabaseUpdater::createTables() const { DEBUG_BLOCK SqlStorage *storage = m_collection->sqlStorage(); // see docs/database/amarokTables.svg for documentation about database layout { QString c = "CREATE TABLE admin (component " + storage->textColumnType() + ", version INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); } { QString create = "CREATE TABLE devices " "(id " + storage->idType() + ",type " + storage->textColumnType() + ",label " + storage->textColumnType() + ",lastmountpoint " + storage->textColumnType() + ",uuid " + storage->textColumnType() + ",servername " + storage->textColumnType(80) + ",sharename " + storage->textColumnType(240) + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE INDEX devices_type ON devices( type );" ); storage->query( "CREATE UNIQUE INDEX devices_uuid ON devices( uuid );" ); storage->query( "CREATE INDEX devices_rshare ON devices( servername, sharename );" ); } { QString create = "CREATE TABLE urls " "(id " + storage->idType() + ",deviceid INTEGER" ",rpath " + storage->exactIndexableTextColumnType() + " NOT NULL" + ",directory INTEGER" ",uniqueid " + storage->exactTextColumnType(128) + " UNIQUE) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX urls_id_rpath ON urls(deviceid, rpath);" ); storage->query( "CREATE INDEX urls_uniqueid ON urls(uniqueid);" ); storage->query( "CREATE INDEX urls_directory ON urls(directory);" ); } { QString create = "CREATE TABLE directories " "(id " + storage->idType() + ",deviceid INTEGER" ",dir " + storage->exactTextColumnType() + " NOT NULL" + ",changedate INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE INDEX directories_deviceid ON directories(deviceid);" ); } { QString create = "CREATE TABLE artists " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX artists_name ON artists(name);" ); } { QString create = "CREATE TABLE images " "(id " + storage->idType() + ",path " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX images_name ON images(path);" ); } { QString c = "CREATE TABLE albums " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL" ",artist INTEGER" + ",image INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); storage->query( "CREATE INDEX albums_name ON albums(name);" ); storage->query( "CREATE INDEX albums_artist ON albums(artist);" ); storage->query( "CREATE INDEX albums_image ON albums(image);" ); storage->query( "CREATE UNIQUE INDEX albums_name_artist ON albums(name,artist);" ); //the index below should not be necessary. uncomment if a query plan shows it is //storage->query( "CREATE UNIQUE INDEX albums_artist_name ON albums(artist,name);" ); } { QString create = "CREATE TABLE genres " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX genres_name ON genres(name);" ); } { QString create = "CREATE TABLE composers " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX composers_name ON composers(name);" ); } { QString create = "CREATE TABLE years " "(id " + storage->idType() + ",name " + storage->textColumnType() + " NOT NULL) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( create ); storage->query( "CREATE UNIQUE INDEX years_name ON years(name);" ); } { QString c = "CREATE TABLE tracks " "(id " + storage->idType() + ",url INTEGER" ",artist INTEGER" ",album INTEGER" ",genre INTEGER" ",composer INTEGER" ",year INTEGER" ",title " + storage->textColumnType() + ",comment " + storage->longTextColumnType() + ",tracknumber INTEGER" ",discnumber INTEGER" ",bitrate INTEGER" ",length INTEGER" ",samplerate INTEGER" ",filesize INTEGER" ",filetype INTEGER" //does this still make sense? ",bpm FLOAT" ",createdate INTEGER" // this is the track creation time ",modifydate INTEGER" // UNUSED currently ",albumgain FLOAT" ",albumpeakgain FLOAT" // decibels, relative to albumgain ",trackgain FLOAT" ",trackpeakgain FLOAT" // decibels, relative to trackgain ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); storage->query( "CREATE UNIQUE INDEX tracks_url ON tracks(url);" ); QStringList indices; indices << "id" << "artist" << "album" << "genre" << "composer" << "year" << "title"; indices << "discnumber" << "createdate" << "length" << "bitrate" << "filesize"; foreach( const QString &index, indices ) { QString query = QString( "CREATE INDEX tracks_%1 ON tracks(%2);" ).arg( index, index ); storage->query( query ); } } { QString c = "CREATE TABLE statistics " "(id " + storage->idType() + ",url INTEGER NOT NULL" ",createdate INTEGER" // this is the first played time ",accessdate INTEGER" // this is the last played time ",score FLOAT" ",rating INTEGER NOT NULL DEFAULT 0" // the "default" undefined rating is 0. We cannot display anything else. ",playcount INTEGER NOT NULL DEFAULT 0" // a track is either played or not. ",deleted BOOL NOT NULL DEFAULT " + storage->boolFalse() + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( c ); storage->query( "CREATE UNIQUE INDEX statistics_url ON statistics(url);" ); QStringList indices; indices << "createdate" << "accessdate" << "score" << "rating" << "playcount"; foreach( const QString &index, indices ) { QString q = QString( "CREATE INDEX statistics_%1 ON statistics(%2);" ).arg( index, index ); storage->query( q ); } } { QString q = "CREATE TABLE labels " "(id " + storage->idType() + ",label " + storage->textColumnType() + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( q ); storage->query( "CREATE UNIQUE INDEX labels_label ON labels(label);" ); QString r = "CREATE TABLE urls_labels(url INTEGER, label INTEGER);"; storage->query( r ); storage->query( "CREATE INDEX urlslabels_url ON urls_labels(url);" ); storage->query( "CREATE INDEX urlslabels_label ON urls_labels(label);" ); } { QString q = "CREATE TABLE amazon (" "asin " + storage->textColumnType( 20 ) + ",locale " + storage->textColumnType( 2 ) + ",filename " + storage->textColumnType( 33 ) + ",refetchdate INTEGER ) COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( q ); storage->query( "CREATE INDEX amazon_date ON amazon(refetchdate);" ); } { QString q = "CREATE TABLE lyrics (" "url INTEGER PRIMARY KEY" ",lyrics " + storage->longTextColumnType() + ") COLLATE = utf8_bin ENGINE = MyISAM;"; storage->query( q ); } storage->query( "INSERT INTO admin(component,version) " "VALUES('AMAROK_TRACK'," + QString::number( DB_VERSION ) + ");" ); { storage->query( "CREATE TABLE statistics_permanent " "(url " + storage->exactIndexableTextColumnType() + " NOT NULL" + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER) COLLATE = utf8_bin ENGINE = MyISAM;" ); //Below query is invalid! Fix it, and then put the proper query in an upgrade function! storage->query( "CREATE UNIQUE INDEX stats_perm_url ON statistics_permanent(url)" ); storage->query( "CREATE TABLE statistics_tag " "(name " + storage->textColumnType(108) + ",artist " + storage->textColumnType(108) + ",album " + storage->textColumnType(108) + ",firstplayed DATETIME" ",lastplayed DATETIME" ",score FLOAT" ",rating INTEGER DEFAULT 0" ",playcount INTEGER) COLLATE = utf8_bin ENGINE = MyISAM" ); //Below query is invalid! Fix it, and then put the proper query in an upgrade function! storage->query( "CREATE UNIQUE INDEX stats_tag_name_artist_album ON statistics_tag(name,artist,album)" ); } } int DatabaseUpdater::adminValue( const QString &key ) const { SqlStorage *storage = m_collection->sqlStorage(); QStringList columns = storage->query( QString( "SELECT column_name FROM INFORMATION_SCHEMA.columns " "WHERE table_name='admin'" ) ); if( columns.isEmpty() ) return 0; //no table with that name QStringList values = storage->query( QString( "SELECT version FROM admin WHERE component = '%1';") .arg(storage->escape( key ) ) ); if( values.isEmpty() ) return 0; return values.first().toInt(); } void DatabaseUpdater::deleteAllRedundant( const QString &type ) { SqlStorage *storage = m_collection->sqlStorage(); const QString tablename = type + 's'; if( type == "artist" ) storage->query( QString( "DELETE FROM artists " "WHERE id NOT IN ( SELECT artist FROM tracks WHERE artist IS NOT NULL ) AND " "id NOT IN ( SELECT artist FROM albums WHERE artist IS NOT NULL )") ); else storage->query( QString( "DELETE FROM %1 " "WHERE id NOT IN ( SELECT %2 FROM tracks WHERE %2 IS NOT NULL )" ). arg( tablename, type ) ); } void DatabaseUpdater::deleteOrphanedByDirectory( const QString &table ) { SqlStorage *storage = m_collection->sqlStorage(); QString query( "DELETE FROM %1 WHERE directory NOT IN ( SELECT id FROM directories )" ); storage->query( query.arg( table ) ); } void DatabaseUpdater::deleteOrphanedByUrl( const QString &table ) { SqlStorage *storage = m_collection->sqlStorage(); QString query( "DELETE FROM %1 WHERE url NOT IN ( SELECT id FROM urls )" ); storage->query( query.arg( table ) ); } void DatabaseUpdater::removeFilesInDir( int deviceid, const QString &rdir ) { SqlStorage *storage = m_collection->sqlStorage(); QString select = QString( "SELECT urls.id FROM urls LEFT JOIN directories ON urls.directory = directories.id " "WHERE directories.deviceid = %1 AND directories.dir = '%2';" ) .arg( QString::number( deviceid ), storage->escape( rdir ) ); QStringList idResult = storage->query( select ); if( !idResult.isEmpty() ) { QString id; QString ids; QStringList::ConstIterator it = idResult.constBegin(), end = idResult.constEnd(); while( it != end ) { id = (*(it++)); if( !ids.isEmpty() ) ids += ','; ids += id; } QString drop = QString( "DELETE FROM tracks WHERE url IN (%1);" ).arg( ids ); storage->query( drop ); } } void DatabaseUpdater::writeCSVFile( const QString &table, const QString &filename, bool forceDebug ) { SqlStorage *storage = m_collection->sqlStorage(); if( !forceDebug && !m_debugDatabaseContent ) return; QString ctable = table; QStringList columns = storage->query( QString( "SELECT column_name FROM INFORMATION_SCHEMA.columns WHERE table_name='%1'" ) .arg( storage->escape( ctable ) ) ); if( columns.isEmpty() ) return; //no table with that name // ok. it was probably a little bit unlucky to name a table statistics // that clashes with INFORMATION_SCHEMA.statistics, a build in table. if( table == "statistics" && columns.count() > 15 ) { // delete all columns with full upper case name. Those are the buildins. for( int i = columns.count()-1; i>= 0; --i ) { if( columns[i].toUpper() == columns[i] ) columns.removeAt( i ); } } QString select; foreach( const QString &column, columns ) { if( !select.isEmpty() ) select.append( ',' ); select.append( column ); } QString query = "SELECT %1 FROM %2"; QStringList result = storage->query( query.arg( select, storage->escape( table ) ) ); QFile file( filename ); if( file.open( QFile::WriteOnly | QFile::Text | QFile::Truncate ) ) { QTextStream stream( &file ); int i = 0; QString line; //write header foreach( const QString &column, columns ) { stream << column; stream << ';'; } stream << '\n'; foreach( const QString &data, result ) { stream << data; stream << ';'; ++i; if( i % columns.count() == 0 ) stream << '\n'; } file.close(); } } diff --git a/src/core-impl/collections/db/sql/SqlQueryMaker.cpp b/src/core-impl/collections/db/sql/SqlQueryMaker.cpp index 6984b653e7..40bb60374a 100644 --- a/src/core-impl/collections/db/sql/SqlQueryMaker.cpp +++ b/src/core-impl/collections/db/sql/SqlQueryMaker.cpp @@ -1,1157 +1,1158 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Daniel Winter * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "SqlQueryMaker" #include "SqlQueryMaker.h" #include "SqlCollection.h" #include "SqlQueryMakerInternal.h" #include #include "core/support/Debug.h" #include "core-impl/collections/db/MountPointManager.h" #include #include #include #include using namespace Collections; class SqlWorkerThread : public ThreadWeaver::Job { public: SqlWorkerThread( SqlQueryMakerInternal *queryMakerInternal ) : ThreadWeaver::Job() , m_queryMakerInternal( queryMakerInternal ) , m_aborted( false ) { //nothing to do } virtual ~SqlWorkerThread() { delete m_queryMakerInternal; } virtual void requestAbort() { m_aborted = true; } SqlQueryMakerInternal* queryMakerInternal() const { return m_queryMakerInternal; } protected: virtual void run() { m_queryMakerInternal->run(); setFinished( !m_aborted ); } private: SqlQueryMakerInternal *m_queryMakerInternal; bool m_aborted; }; struct SqlQueryMaker::Private { enum { TAGS_TAB = 1, ARTIST_TAB = 2, ALBUM_TAB = 4, GENRE_TAB = 8, COMPOSER_TAB = 16, YEAR_TAB = 32, STATISTICS_TAB = 64, URLS_TAB = 128, ALBUMARTIST_TAB = 256, LABELS_TAB = 1024 }; int linkedTables; QueryMaker::QueryType queryType; QString query; QString queryReturnValues; QString queryFrom; QString queryMatch; QString queryFilter; QString queryOrderBy; bool withoutDuplicates; int maxResultSize; AlbumQueryMode albumMode; LabelQueryMode labelMode; SqlWorkerThread *worker; QStack andStack; QStringList blockingCustomData; Meta::TrackList blockingTracks; Meta::AlbumList blockingAlbums; Meta::ArtistList blockingArtists; Meta::GenreList blockingGenres; Meta::ComposerList blockingComposers; Meta::YearList blockingYears; Meta::LabelList blockingLabels; bool blocking; bool used; qint64 returnValueType; }; SqlQueryMaker::SqlQueryMaker( SqlCollection* collection ) : QueryMaker() , m_collection( collection ) , d( new Private ) { d->worker = 0; d->queryType = QueryMaker::None; d->linkedTables = 0; d->withoutDuplicates = false; d->albumMode = AllAlbums; d->labelMode = QueryMaker::NoConstraint; d->maxResultSize = -1; d->andStack.clear(); d->andStack.push( true ); //and is default d->blocking = false; d->used = false; d->returnValueType = 0; } SqlQueryMaker::~SqlQueryMaker() { disconnect(); abortQuery(); if( d->worker ) d->worker->deleteLater(); delete d; } void SqlQueryMaker::abortQuery() { if( d->worker ) { d->worker->requestAbort(); d->worker->disconnect( this ); if( d->worker->queryMakerInternal() ) d->worker->queryMakerInternal()->disconnect( this ); } } void SqlQueryMaker::run() { if( d->queryType == QueryMaker::None || (d->blocking && d->used) ) { debug() << "sql querymaker used without reset or initialization" << endl; return; //better error handling? } if( d->worker && !d->worker->isFinished() ) { //the worker thread seems to be running //TODO: wait or job to complete } else { SqlQueryMakerInternal *qmi = new SqlQueryMakerInternal( m_collection ); qmi->setQuery( query() ); qmi->setQueryType( d->queryType ); if ( !d->blocking ) { connect( qmi, SIGNAL(newResultReady(Meta::AlbumList)), SIGNAL(newResultReady(Meta::AlbumList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::ArtistList)), SIGNAL(newResultReady(Meta::ArtistList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::GenreList)), SIGNAL(newResultReady(Meta::GenreList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::ComposerList)), SIGNAL(newResultReady(Meta::ComposerList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::YearList)), SIGNAL(newResultReady(Meta::YearList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::TrackList)), SIGNAL(newResultReady(Meta::TrackList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(QStringList)), SIGNAL(newResultReady(QStringList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::LabelList)), SIGNAL(newResultReady(Meta::LabelList)), Qt::DirectConnection ); d->worker = new SqlWorkerThread( qmi ); connect( d->worker, SIGNAL(done(ThreadWeaver::Job*)), SLOT(done(ThreadWeaver::Job*)) ); ThreadWeaver::Weaver::instance()->enqueue( d->worker ); } else //use it blocking { connect( qmi, SIGNAL(newResultReady(Meta::AlbumList)), SLOT(blockingNewResultReady(Meta::AlbumList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::ArtistList)), SLOT(blockingNewResultReady(Meta::ArtistList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::GenreList)), SLOT(blockingNewResultReady(Meta::GenreList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::ComposerList)), SLOT(blockingNewResultReady(Meta::ComposerList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::YearList)), SLOT(blockingNewResultReady(Meta::YearList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::TrackList)), SLOT(blockingNewResultReady(Meta::TrackList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(QStringList)), SLOT(blockingNewResultReady(QStringList)), Qt::DirectConnection ); connect( qmi, SIGNAL(newResultReady(Meta::LabelList)), SLOT(blockingNewResultReady(Meta::LabelList)), Qt::DirectConnection ); qmi->run(); delete qmi; } } d->used = true; } void SqlQueryMaker::done( ThreadWeaver::Job *job ) { job->deleteLater(); d->worker = 0; // d->worker *is* the job, prevent stale pointer emit queryDone(); } QueryMaker* SqlQueryMaker::setQueryType( QueryType type ) { // we need the unchanged m_queryType in the blocking result methods so prevent // reseting queryType without reseting the QM if ( d->blocking && d->used ) return this; switch( type ) { case QueryMaker::Track: //make sure to keep this method in sync with handleTracks(QStringList) and the SqlTrack ctor if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Track; d->linkedTables |= Private::URLS_TAB; d->linkedTables |= Private::TAGS_TAB; d->linkedTables |= Private::GENRE_TAB; d->linkedTables |= Private::ARTIST_TAB; d->linkedTables |= Private::ALBUM_TAB; d->linkedTables |= Private::COMPOSER_TAB; d->linkedTables |= Private::YEAR_TAB; d->linkedTables |= Private::STATISTICS_TAB; d->queryReturnValues = Meta::SqlTrack::getTrackReturnValues(); } return this; case QueryMaker::Artist: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Artist; d->withoutDuplicates = true; d->linkedTables |= Private::ARTIST_TAB; //reading the ids from the database means we don't have to query for them later d->queryReturnValues = "artists.name, artists.id"; } return this; case QueryMaker::Album: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Album; d->withoutDuplicates = true; d->linkedTables |= Private::ALBUM_TAB; //add whatever is necessary to identify compilations d->queryReturnValues = "albums.name, albums.id, albums.artist"; } return this; case QueryMaker::AlbumArtist: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::AlbumArtist; d->withoutDuplicates = true; d->linkedTables |= Private::ALBUMARTIST_TAB; d->linkedTables |= Private::ALBUM_TAB; d->queryReturnValues = "albumartists.name, albumartists.id"; } return this; case QueryMaker::Composer: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Composer; d->withoutDuplicates = true; d->linkedTables |= Private::COMPOSER_TAB; d->queryReturnValues = "composers.name, composers.id"; } return this; case QueryMaker::Genre: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Genre; d->withoutDuplicates = true; d->linkedTables |= Private::GENRE_TAB; d->queryReturnValues = "genres.name, genres.id"; } return this; case QueryMaker::Year: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Year; d->withoutDuplicates = true; d->linkedTables |= Private::YEAR_TAB; d->queryReturnValues = "years.name, years.id"; } return this; case QueryMaker::Custom: if( d->queryType == QueryMaker::None ) d->queryType = QueryMaker::Custom; return this; case QueryMaker::Label: if( d->queryType == QueryMaker::None ) { d->queryType = QueryMaker::Label; d->withoutDuplicates = true; d->queryReturnValues = "labels.label,labels.id"; d->linkedTables |= Private::LABELS_TAB; } + return this; case QueryMaker::None: return this; } return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::TrackPtr &track ) { QString url = track->uidUrl(); if( !url.isEmpty() ) /* KUrl kurl( url ); if( kurl.protocol() == "amarok-sqltrackuid" ) */ { d->queryMatch += QString( " AND urls.uniqueid = '%1' " ).arg( url /*kurl.url()*/ ); } else { QString path; /* if( kurl.isLocalFile() ) { path = kurl.path(); } else */ { path = track->playableUrl().path(); } int deviceid = m_collection->mountPointManager()->getIdForUrl( path ); QString rpath = m_collection->mountPointManager()->getRelativePath( deviceid, path ); d->queryMatch += QString( " AND urls.deviceid = %1 AND urls.rpath = '%2'" ) .arg( QString::number( deviceid ), escape( rpath ) ); } return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::ArtistPtr &artist, ArtistMatchBehaviour behaviour ) { d->linkedTables |= Private::ARTIST_TAB; if( behaviour == AlbumArtists || behaviour == AlbumOrTrackArtists ) d->linkedTables |= Private::ALBUMARTIST_TAB; QString artistQuery; QString albumArtistQuery; if( artist && !artist->name().isEmpty() ) { artistQuery = QString("artists.name = '%1'").arg( escape( artist->name() ) ); albumArtistQuery = QString("albumartists.name = '%1'").arg( escape( artist->name() ) ); } else { artistQuery = "( artists.name IS NULL OR artists.name = '')"; albumArtistQuery = "( albumartists.name IS NULL OR albumartists.name = '')"; } switch( behaviour ) { case TrackArtists: d->queryMatch += " AND " + artistQuery; break; case AlbumArtists: d->queryMatch += " AND " + albumArtistQuery; break; case AlbumOrTrackArtists: d->queryMatch += " AND ( (" + artistQuery + " ) OR ( " + albumArtistQuery + " ) )"; break; } return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::AlbumPtr &album ) { d->linkedTables |= Private::ALBUM_TAB; // handle singles if( !album || album->name().isEmpty() ) d->queryMatch += QString( " AND ( albums.name IS NULL OR albums.name = '' )" ); else d->queryMatch += QString( " AND albums.name = '%1'" ).arg( escape( album->name() ) ); if( album ) { //handle compilations Meta::ArtistPtr albumArtist = album->albumArtist(); if( albumArtist ) { d->linkedTables |= Private::ALBUMARTIST_TAB; d->queryMatch += QString( " AND albumartists.name = '%1'" ).arg( escape( albumArtist->name() ) ); } else { d->queryMatch += " AND albums.artist IS NULL"; } } return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::GenrePtr &genre ) { d->linkedTables |= Private::GENRE_TAB; d->queryMatch += QString( " AND genres.name = '%1'" ).arg( escape( genre->name() ) ); return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::ComposerPtr &composer ) { d->linkedTables |= Private::COMPOSER_TAB; d->queryMatch += QString( " AND composers.name = '%1'" ).arg( escape( composer->name() ) ); return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::YearPtr &year ) { // handle tracks without a year if( !year ) { d->queryMatch += " AND year IS NULL"; } else { d->linkedTables |= Private::YEAR_TAB; d->queryMatch += QString( " AND years.name = '%1'" ).arg( escape( year->name() ) ); } return this; } QueryMaker* SqlQueryMaker::addMatch( const Meta::LabelPtr &label ) { KSharedPtr sqlLabel = KSharedPtr::dynamicCast( label ); QString labelSubQuery; if( sqlLabel ) { labelSubQuery = "SELECT url FROM urls_labels WHERE label = %1"; labelSubQuery = labelSubQuery.arg( sqlLabel->id() ); } else { labelSubQuery = "SELECT a.url FROM urls_labels a INNER JOIN labels b ON a.label = b.id WHERE b.label = '%1'"; labelSubQuery = labelSubQuery.arg( escape( label->name() ) ); } d->linkedTables |= Private::TAGS_TAB; QString match = " AND tracks.url in (%1)"; d->queryMatch += match.arg( labelSubQuery ); return this; } QueryMaker* SqlQueryMaker::addFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd ) { // special case for albumartist... if( value == Meta::valAlbumArtist && filter.isEmpty() ) { d->linkedTables |= Private::ALBUMARTIST_TAB; d->linkedTables |= Private::ALBUM_TAB; d->queryFilter += QString( " %1 ( albums.artist IS NULL or albumartists.name = '') " ).arg( andOr() ); } else if( value == Meta::valLabel ) { d->linkedTables |= Private::TAGS_TAB; QString like = likeCondition( filter, !matchBegin, !matchEnd ); QString filter = " %1 tracks.url IN (SELECT a.url FROM urls_labels a INNER JOIN labels b ON a.label = b.id WHERE b.label %2) "; d->queryFilter += filter.arg( andOr(), like ); } else { QString like = likeCondition( filter, !matchBegin, !matchEnd ); d->queryFilter += QString( " %1 %2 %3 " ).arg( andOr(), nameForValue( value ), like ); } return this; } QueryMaker* SqlQueryMaker::excludeFilter( qint64 value, const QString &filter, bool matchBegin, bool matchEnd ) { // special case for album... if( value == Meta::valAlbumArtist && filter.isEmpty() ) { d->linkedTables |= Private::ALBUMARTIST_TAB; d->queryFilter += QString( " %1 NOT ( albums.artist IS NULL or albumartists.name = '') " ).arg( andOr() ); } else if( value == Meta::valLabel ) { d->linkedTables |= Private::TAGS_TAB; QString like = likeCondition( filter, !matchBegin, !matchEnd ); QString filter = " %1 tracks.url NOT IN (SELECT a.url FROM urls_labels a INNER JOIN labels b ON a.label = b.id WHERE b.label %2) "; d->queryFilter += filter.arg( andOr(), like ); } else { QString like = likeCondition( filter, !matchBegin, !matchEnd ); d->queryFilter += QString( " %1 NOT %2 %3 " ).arg( andOr(), nameForValue( value ), like ); } return this; } QueryMaker* SqlQueryMaker::addNumberFilter( qint64 value, qint64 filter, QueryMaker::NumberComparison compare ) { QString comparison; switch( compare ) { case QueryMaker::Equals: comparison = '='; break; case QueryMaker::GreaterThan: comparison = '>'; break; case QueryMaker::LessThan: comparison = '<'; break; } // note: a NULL value in the database means undefined and not 0! d->queryFilter += QString( " %1 %2 %3 %4 " ).arg( andOr(), nameForValue( value ), comparison, QString::number( filter ) ); return this; } QueryMaker* SqlQueryMaker::excludeNumberFilter( qint64 value, qint64 filter, QueryMaker::NumberComparison compare ) { QString comparison; switch( compare ) { case QueryMaker::Equals: comparison = "!="; break; case QueryMaker::GreaterThan: //negating greater than is less or equal comparison = "<="; break; case QueryMaker::LessThan: //negating less than is greater or equal comparison = ">="; break; } // note: a NULL value in the database means undefined and not 0! // We can't exclude NULL values here because they are not defined! d->queryFilter += QString( " %1 (%2 %3 %4 or %2 is null)" ).arg( andOr(), nameForValue( value ), comparison, QString::number( filter ) ); return this; } QueryMaker* SqlQueryMaker::addReturnValue( qint64 value ) { if( d->queryType == QueryMaker::Custom ) { if ( !d->queryReturnValues.isEmpty() ) d->queryReturnValues += ','; d->queryReturnValues += nameForValue( value ); d->returnValueType = value; } return this; } QueryMaker* SqlQueryMaker::addReturnFunction( ReturnFunction function, qint64 value ) { if( d->queryType == QueryMaker::Custom ) { if( !d->queryReturnValues.isEmpty() ) d->queryReturnValues += ','; QString sqlfunction; switch( function ) { case QueryMaker::Count: sqlfunction = "COUNT"; break; case QueryMaker::Sum: sqlfunction = "SUM"; break; case QueryMaker::Max: sqlfunction = "MAX"; break; case QueryMaker::Min: sqlfunction = "MIN"; break; default: sqlfunction = "Unknown function in SqlQueryMaker::addReturnFunction, function was: " + QString::number( function ); } d->queryReturnValues += QString( "%1(%2)" ).arg( sqlfunction, nameForValue( value ) ); d->returnValueType = value; } return this; } QueryMaker* SqlQueryMaker::orderBy( qint64 value, bool descending ) { if ( d->queryOrderBy.isEmpty() ) d->queryOrderBy = " ORDER BY "; else d->queryOrderBy += ','; d->queryOrderBy += nameForValue( value ); d->queryOrderBy += QString( " %1 " ).arg( descending ? "DESC" : "ASC" ); return this; } QueryMaker* SqlQueryMaker::limitMaxResultSize( int size ) { d->maxResultSize = size; return this; } QueryMaker* SqlQueryMaker::setAlbumQueryMode( AlbumQueryMode mode ) { if( mode != AllAlbums ) { d->linkedTables |= Private::ALBUM_TAB; } d->albumMode = mode; return this; } QueryMaker* SqlQueryMaker::setLabelQueryMode( LabelQueryMode mode ) { d->labelMode = mode; return this; } QueryMaker* SqlQueryMaker::beginAnd() { d->queryFilter += andOr(); d->queryFilter += " ( 1 "; d->andStack.push( true ); return this; } QueryMaker* SqlQueryMaker::beginOr() { d->queryFilter += andOr(); d->queryFilter += " ( 0 "; d->andStack.push( false ); return this; } QueryMaker* SqlQueryMaker::endAndOr() { d->queryFilter += ')'; d->andStack.pop(); return this; } void SqlQueryMaker::linkTables() { switch( d->queryType ) { case QueryMaker::Track: { d->queryFrom += " tracks"; if( d->linkedTables & Private::TAGS_TAB ) d->linkedTables ^= Private::TAGS_TAB; break; } case QueryMaker::Artist: { d->queryFrom += " artists"; if( d->linkedTables != Private::ARTIST_TAB ) d->queryFrom += " JOIN tracks ON tracks.artist = artists.id"; if( d->linkedTables & Private::ARTIST_TAB ) d->linkedTables ^= Private::ARTIST_TAB; break; } case QueryMaker::Album: case QueryMaker::AlbumArtist: { d->queryFrom += " albums"; if( d->linkedTables != Private::ALBUM_TAB && d->linkedTables != ( Private::ALBUM_TAB | Private::ALBUMARTIST_TAB ) ) d->queryFrom += " JOIN tracks ON tracks.album = albums.id"; if( d->linkedTables & Private::ALBUM_TAB ) d->linkedTables ^= Private::ALBUM_TAB; break; } case QueryMaker::Genre: { d->queryFrom += " genres"; if( d->linkedTables != Private::GENRE_TAB ) d->queryFrom += " INNER JOIN tracks ON tracks.genre = genres.id"; if( d->linkedTables & Private::GENRE_TAB ) d->linkedTables ^= Private::GENRE_TAB; break; } case QueryMaker::Composer: { d->queryFrom += " composers"; if( d->linkedTables != Private::COMPOSER_TAB ) d->queryFrom += " JOIN tracks ON tracks.composer = composers.id"; if( d->linkedTables & Private::COMPOSER_TAB ) d->linkedTables ^= Private::COMPOSER_TAB; break; } case QueryMaker::Year: { d->queryFrom += " years"; if( d->linkedTables != Private::YEAR_TAB ) d->queryFrom += " JOIN tracks on tracks.year = years.id"; if( d->linkedTables & Private::YEAR_TAB ) d->linkedTables ^= Private::YEAR_TAB; break; } case QueryMaker::Label: { d->queryFrom += " labels"; if( d->linkedTables != Private::LABELS_TAB ) d->queryFrom += " INNER JOIN urls_labels ON labels.id = urls_labels.label" " INNER JOIN tracks ON urls_labels.url = tracks.url"; if( d->linkedTables & Private::LABELS_TAB ) d->linkedTables ^= Private::LABELS_TAB; break; } case QueryMaker::Custom: { switch( d->returnValueType ) { default: case Meta::valUrl: { d->queryFrom += " tracks"; if( d->linkedTables & Private::TAGS_TAB ) d->linkedTables ^= Private::TAGS_TAB; break; } case Meta::valAlbum: { d->queryFrom += " albums"; if( d->linkedTables & Private::ALBUM_TAB ) d->linkedTables ^= Private::ALBUM_TAB; if( d->linkedTables & Private::URLS_TAB ) d->linkedTables ^= Private::URLS_TAB; break; } case Meta::valArtist: { d->queryFrom += " artists"; if( d->linkedTables & Private::ARTIST_TAB ) d->linkedTables ^= Private::ARTIST_TAB; if( d->linkedTables & Private::URLS_TAB ) d->linkedTables ^= Private::URLS_TAB; break; } case Meta::valGenre: { d->queryFrom += " genres"; if( d->linkedTables & Private::GENRE_TAB ) d->linkedTables ^= Private::GENRE_TAB; if( d->linkedTables & Private::URLS_TAB ) d->linkedTables ^= Private::URLS_TAB; break; } } } case QueryMaker::None: { //??? break; } } if( !d->linkedTables ) return; if( d->linkedTables & Private::URLS_TAB ) d->queryFrom += " INNER JOIN urls ON tracks.url = urls.id"; if( d->linkedTables & Private::ARTIST_TAB ) d->queryFrom += " LEFT JOIN artists ON tracks.artist = artists.id"; if( d->linkedTables & Private::ALBUM_TAB ) d->queryFrom += " LEFT JOIN albums ON tracks.album = albums.id"; if( d->linkedTables & Private::ALBUMARTIST_TAB ) d->queryFrom += " LEFT JOIN artists AS albumartists ON albums.artist = albumartists.id"; if( d->linkedTables & Private::GENRE_TAB ) d->queryFrom += " LEFT JOIN genres ON tracks.genre = genres.id"; if( d->linkedTables & Private::COMPOSER_TAB ) d->queryFrom += " LEFT JOIN composers ON tracks.composer = composers.id"; if( d->linkedTables & Private::YEAR_TAB ) d->queryFrom += " LEFT JOIN years ON tracks.year = years.id"; if( d->linkedTables & Private::STATISTICS_TAB ) { if( d->linkedTables & Private::URLS_TAB ) { d->queryFrom += " LEFT JOIN statistics ON urls.id = statistics.url"; } else { d->queryFrom += " LEFT JOIN statistics ON tracks.url = statistics.url"; } } } void SqlQueryMaker::buildQuery() { //URLS is always required for dynamic collection d->linkedTables |= Private::URLS_TAB; linkTables(); QString query = "SELECT "; if ( d->withoutDuplicates ) query += "DISTINCT "; query += d->queryReturnValues; query += " FROM "; query += d->queryFrom; // dynamic collection (only mounted file systems are considered) if( (d->linkedTables & Private::URLS_TAB) && m_collection->mountPointManager() ) { query += " WHERE 1 "; IdList list = m_collection->mountPointManager()->getMountedDeviceIds(); if( !list.isEmpty() ) { QString commaSeparatedIds; foreach( int id, list ) { if( !commaSeparatedIds.isEmpty() ) commaSeparatedIds += ','; commaSeparatedIds += QString::number( id ); } query += QString( " AND urls.deviceid in (%1)" ).arg( commaSeparatedIds ); } } switch( d->albumMode ) { case OnlyNormalAlbums: query += " AND albums.artist IS NOT NULL "; break; case OnlyCompilations: query += " AND albums.artist IS NULL "; break; case AllAlbums: //do nothing break; } if( d->labelMode != QueryMaker::NoConstraint ) { switch( d->labelMode ) { case QueryMaker::OnlyWithLabels: query += " AND tracks.url IN "; break; case QueryMaker::OnlyWithoutLabels: query += " AND tracks.url NOT IN "; break; case QueryMaker::NoConstraint: //do nothing, will never be called break; } query += " (SELECT DISTINCT url FROM urls_labels) "; } query += d->queryMatch; if ( !d->queryFilter.isEmpty() ) { query += " AND ( 1 "; query += d->queryFilter; query += " ) "; } query += d->queryOrderBy; if ( d->maxResultSize > -1 ) query += QString( " LIMIT %1 OFFSET 0 " ).arg( d->maxResultSize ); query += ';'; d->query = query; } QString SqlQueryMaker::query() { if ( d->query.isEmpty() ) buildQuery(); return d->query; } QStringList SqlQueryMaker::runQuery( const QString &query ) { return m_collection->sqlStorage()->query( query ); } void SqlQueryMaker::setBlocking( bool enabled ) { d->blocking = enabled; } QStringList SqlQueryMaker::collectionIds() const { QStringList list; list << m_collection->collectionId(); return list; } Meta::TrackList SqlQueryMaker::tracks() const { return d->blockingTracks; } Meta::AlbumList SqlQueryMaker::albums() const { return d->blockingAlbums; } Meta::ArtistList SqlQueryMaker::artists() const { return d->blockingArtists; } Meta::GenreList SqlQueryMaker::genres() const { return d->blockingGenres; } Meta::ComposerList SqlQueryMaker::composers() const { return d->blockingComposers; } Meta::YearList SqlQueryMaker::years() const { return d->blockingYears; } QStringList SqlQueryMaker::customData() const { return d->blockingCustomData; } Meta::LabelList SqlQueryMaker::labels() const { return d->blockingLabels; } QString SqlQueryMaker::nameForValue( qint64 value ) { switch( value ) { case Meta::valUrl: d->linkedTables |= Private::URLS_TAB; return "urls.rpath"; //TODO figure out how to handle deviceid case Meta::valTitle: d->linkedTables |= Private::TAGS_TAB; return "tracks.title"; case Meta::valArtist: d->linkedTables |= Private::ARTIST_TAB; return "artists.name"; case Meta::valAlbum: d->linkedTables |= Private::ALBUM_TAB; return "albums.name"; case Meta::valGenre: d->linkedTables |= Private::GENRE_TAB; return "genres.name"; case Meta::valComposer: d->linkedTables |= Private::COMPOSER_TAB; return "composers.name"; case Meta::valYear: d->linkedTables |= Private::YEAR_TAB; return "years.name"; case Meta::valBpm: d->linkedTables |= Private::TAGS_TAB; return "tracks.bpm"; case Meta::valComment: d->linkedTables |= Private::TAGS_TAB; return "tracks.comment"; case Meta::valTrackNr: d->linkedTables |= Private::TAGS_TAB; return "tracks.tracknumber"; case Meta::valDiscNr: d->linkedTables |= Private::TAGS_TAB; return "tracks.discnumber"; case Meta::valLength: d->linkedTables |= Private::TAGS_TAB; return "tracks.length"; case Meta::valBitrate: d->linkedTables |= Private::TAGS_TAB; return "tracks.bitrate"; case Meta::valSamplerate: d->linkedTables |= Private::TAGS_TAB; return "tracks.samplerate"; case Meta::valFilesize: d->linkedTables |= Private::TAGS_TAB; return "tracks.filesize"; case Meta::valFormat: d->linkedTables |= Private::TAGS_TAB; return "tracks.filetype"; case Meta::valCreateDate: d->linkedTables |= Private::TAGS_TAB; return "tracks.createdate"; case Meta::valScore: d->linkedTables |= Private::STATISTICS_TAB; return "statistics.score"; case Meta::valRating: d->linkedTables |= Private::STATISTICS_TAB; return "statistics.rating"; case Meta::valFirstPlayed: d->linkedTables |= Private::STATISTICS_TAB; return "statistics.createdate"; case Meta::valLastPlayed: d->linkedTables |= Private::STATISTICS_TAB; return "statistics.accessdate"; case Meta::valPlaycount: d->linkedTables |= Private::STATISTICS_TAB; return "statistics.playcount"; case Meta::valUniqueId: d->linkedTables |= Private::URLS_TAB; return "urls.uniqueid"; case Meta::valAlbumArtist: d->linkedTables |= Private::ALBUMARTIST_TAB; //albumartist_tab means that the artist table is joined to the albums table //so add albums as well d->linkedTables |= Private::ALBUM_TAB; return "albumartists.name"; case Meta::valModified: d->linkedTables |= Private::TAGS_TAB; return "tracks.modifydate"; default: return "ERROR: unknown value in SqlQueryMaker::nameForValue(qint64): value=" + QString::number( value ); } } QString SqlQueryMaker::andOr() const { return d->andStack.top() ? " AND " : " OR "; } QString SqlQueryMaker::escape( QString text ) const //krazy:exclude=constref { return m_collection->sqlStorage()->escape( text ); } QString SqlQueryMaker::likeCondition( const QString &text, bool anyBegin, bool anyEnd ) const { if( anyBegin || anyEnd ) { QString escaped = text; //according to http://dev.mysql.com/doc/refman/5.0/en/string-comparison-functions.html //the escape character (\ as we are using the default) is escaped twice when using like. //mysql_real_escape will escape it once, so we have to escape it another time here escaped = escaped.replace( '\\', "\\\\" ); // "////" will result in two backslahes escaped = escape( escaped ); //as we are in pattern matching mode '_' and '%' have to be escaped //mysql_real_excape_string does not do that for us //see http://dev.mysql.com/doc/refman/5.0/en/string-syntax.html //and http://dev.mysql.com/doc/refman/5.0/en/mysql-real-escape-string.html //replace those characters after calling escape(), which calls the mysql //function in turn, so that mysql does not escape the escape backslashes escaped.replace( '%', "\\%" ).replace( '_', "\\_" ); QString ret = " LIKE "; ret += '\''; if ( anyBegin ) ret += '%'; ret += escaped; if ( anyEnd ) ret += '%'; ret += '\''; //Case insensitive collation for queries ret += " COLLATE utf8_unicode_ci "; //Use \ as the escape character //ret += " ESCAPE '\\' "; return ret; } else { return QString( " = '%1' COLLATE utf8_unicode_ci " ).arg( escape( text ) ); } } void SqlQueryMaker::blockingNewResultReady(const Meta::AlbumList &albums) { d->blockingAlbums = albums; } void SqlQueryMaker::blockingNewResultReady(const Meta::ArtistList &artists) { d->blockingArtists = artists; } void SqlQueryMaker::blockingNewResultReady(const Meta::GenreList &genres) { d->blockingGenres = genres; } void SqlQueryMaker::blockingNewResultReady(const Meta::ComposerList &composers) { d->blockingComposers = composers; } void SqlQueryMaker::blockingNewResultReady(const Meta::YearList &years) { d->blockingYears = years; } void SqlQueryMaker::blockingNewResultReady(const Meta::TrackList &tracks) { d->blockingTracks = tracks; } void SqlQueryMaker::blockingNewResultReady(const QStringList &customData) { d->blockingCustomData = customData; } void SqlQueryMaker::blockingNewResultReady(const Meta::LabelList &labels ) { d->blockingLabels = labels; } #include "SqlQueryMaker.moc" diff --git a/src/core-impl/collections/ipodcollection/jobs/IpodCopyTracksJob.cpp b/src/core-impl/collections/ipodcollection/jobs/IpodCopyTracksJob.cpp index a7fd97ee83..c9208e340f 100644 --- a/src/core-impl/collections/ipodcollection/jobs/IpodCopyTracksJob.cpp +++ b/src/core-impl/collections/ipodcollection/jobs/IpodCopyTracksJob.cpp @@ -1,413 +1,413 @@ /**************************************************************************************** * Copyright (c) 2012 Matěj Laitl * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "IpodCopyTracksJob.h" #include "IpodMeta.h" #include "core/collections/QueryMaker.h" #include "core/interfaces/Logger.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "core/transcoding/TranscodingController.h" #include "MetaTagLib.h" #include "FileType.h" #include "transcoding/TranscodingJob.h" #include #include #include #include #include #include // fsync() IpodCopyTracksJob::IpodCopyTracksJob( const QMap &sources, const QWeakPointer &collection, const Transcoding::Configuration &configuration, bool goingToRemoveSources ) : Job() , m_coll( collection ) , m_transcodingConfig( configuration ) , m_sources( sources ) , m_aborted( false ) , m_goingToRemoveSources( goingToRemoveSources ) { connect( this, SIGNAL(startDuplicateTrackSearch(Meta::TrackPtr)), SLOT(slotStartDuplicateTrackSearch(Meta::TrackPtr)) ); connect( this, SIGNAL(startCopyOrTranscodeJob(KUrl,KUrl,bool)), SLOT(slotStartCopyOrTranscodeJob(KUrl,KUrl,bool)) ); connect( this, SIGNAL(displaySorryDialog()), SLOT(slotDisplaySorryDialog()) ); } void IpodCopyTracksJob::run() { if( !m_coll ) return; // destructed behind our back float totalSafeCapacity = m_coll.data()->totalCapacity() - m_coll.data()->capacityMargin(); QByteArray mountPoint = QFile::encodeName( m_coll.data()->mountPoint() ); QString collectionPrettyName = m_coll.data()->prettyName(); itdb_start_sync( m_coll.data()->m_itdb ); QMapIterator it( m_sources ); while( it.hasNext() ) { if( m_aborted || !m_coll ) break; it.next(); Meta::TrackPtr track = it.key(); KUrl sourceUrl = it.value(); emit startDuplicateTrackSearch( track ); // wait for searching to finish: m_searchingForDuplicates.acquire( 1 ); if( m_duplicateTrack ) { trackProcessed( Duplicate, track, m_duplicateTrack ); continue; } if( !m_coll ) break; // destructed behind our back - bool isJustCopy = m_transcodingConfig.isJustCopy( track, m_coll.data()->supportedFormats() ); + bool isJustCopy = m_transcodingConfig.isJustCopy( track, m_coll.data()->supportedFormats() ); if( isJustCopy // if not copying, we catch big files later && track->filesize() > totalSafeCapacity - m_coll.data()->usedCapacity() ) { // this is a best effort check, we do one definite one after the file is copied debug() << "Refusing to copy" << track->prettyUrl() << "to iPod: there are only" << totalSafeCapacity - m_coll.data()->usedCapacity() << "free bytes (not" << "counting a safety margin) on iPod and track has" << track->filesize() << "bytes."; trackProcessed( ExceededingSafeCapacity, track ); continue; } QString fileExtension; if( isJustCopy ) fileExtension = track->type(); else fileExtension = Amarok::Components::transcodingController()->format( m_transcodingConfig.encoder() )->fileExtension(); if( !m_coll.data()->supportedFormats().contains( fileExtension ) ) { m_notPlayableFormats.insert( fileExtension ); trackProcessed( NotPlayable, track ); continue; } QByteArray fakeSrcName( "filename." ); // only for file extension fakeSrcName.append( QFile::encodeName( fileExtension ) ); /* determine destination filename; we cannot use ipodTrack because as it has no itdb * (and thus mountpoint) set */ GError *error = 0; gchar *destFilename = itdb_cp_get_dest_filename( 0, mountPoint, fakeSrcName, &error ); if( error ) { warning() << "Cannot construct iPod track filename:" << error->message; g_error_free( error ); error = 0; } if( !destFilename ) { trackProcessed( InternalError, track ); continue; } // start the physical copying KUrl destUrl = KUrl( QFile::decodeName( destFilename ) ); emit startCopyOrTranscodeJob( sourceUrl, destUrl, isJustCopy ); // wait for copying to finish: m_copying.acquire( 1 ); /* fsync so that progress bar gives correct info and user doesn't remove the iPod * prematurely */ QFile destFile( QFile::decodeName( destFilename ) ); if( !destFile.exists() ) { debug() << destFile.fileName() << "does not exist even though we thought we copied it to iPod"; trackProcessed( CopyingFailed, track ); continue; } if( !m_coll ) break; // destructed behind our back if( m_coll.data()->usedCapacity() > totalSafeCapacity ) { debug() << "We exceeded total safe-to-use capacity on iPod (safe-to-use:" << totalSafeCapacity << "B, used:" << m_coll.data()->usedCapacity() << "): removing copied track from iPod"; destFile.remove(); trackProcessed( ExceededingSafeCapacity, track ); continue; } // fsync needs a file opened for writing, and without Apped it truncates files (?) if( !destFile.open( QIODevice::WriteOnly | QIODevice::Append ) ) { warning() << "Cannot open file copied to ipod (for writing):" << destFile.fileName() << ": removing it"; destFile.remove(); trackProcessed( InternalError, track ); continue; } if( destFile.size() ) fsync( destFile.handle() ); // should flush all kernel buffers to disk destFile.close(); // create a new track object by copying meta-data from existing one: IpodMeta::Track *ipodTrack = new IpodMeta::Track( track ); // tell the track it has been copied: bool accepted = ipodTrack->finalizeCopying( mountPoint, destFilename ); g_free( destFilename ); destFilename = 0; if( !accepted ) { debug() << "ipodTrack->finalizeCopying( destFilename ) returned false!"; delete ipodTrack; trackProcessed( InternalError, track ); continue; } if( !isJustCopy ) { // we need to reread some metadata in case the file was transcoded Meta::FieldHash fields = Meta::Tag::readTags( destFile.fileName() ); ipodTrack->setBitrate( fields.value( Meta::valBitrate, 0 ).toInt() ); ipodTrack->setLength( fields.value( Meta::valLength, 0 ).toLongLong() ); ipodTrack->setSampleRate( fields.value( Meta::valSamplerate, 0 ).toInt() ); Amarok::FileType type = Amarok::FileType( fields.value( Meta::valFormat, 0 ).toInt() ); ipodTrack->setType( Amarok::FileTypeSupport::toString( type ) ); // we retain ReplayGain, tags etc - these shouldn't change; size is read // in finalizeCopying() } // add the track to collection if( !m_coll ) { delete ipodTrack; break; // we were waiting for copying, m_coll may got destoryed } Meta::TrackPtr newTrack = m_coll.data()->addTrack( ipodTrack ); if( !newTrack ) { destFile.remove(); trackProcessed( InternalError, track ); continue; } trackProcessed( Success, track, newTrack ); } if( m_coll ) itdb_stop_sync( m_coll.data()->m_itdb ); emit endProgressOperation( this ); int sourceSize = m_sources.size(); int successCount = m_sourceTrackStatus.count( Success ); int duplicateCount = m_sourceTrackStatus.count( Duplicate ); QString transferredText; if ( m_transcodingConfig.isJustCopy() ) transferredText = i18ncp( "%2 is collection name", "Transferred one track to %2.", "Transferred %1 tracks to %2.", successCount, collectionPrettyName ); else transferredText = i18ncp( "%2 is collection name", "Transcoded one track to %2.", "Transcoded %1 tracks to %2.", successCount, collectionPrettyName ); if( successCount == sourceSize ) { Amarok::Components::logger()->shortMessage( transferredText ); } else if( m_aborted ) { QString text = i18np( "Transfer aborted. Managed to transfer one track.", "Transfer aborted. Managed to transfer %1 tracks.", successCount ); Amarok::Components::logger()->longMessage( text ); } else if( successCount + duplicateCount == sourceSize ) { QString text = i18ncp( "%2 is the 'Transferred 123 tracks to Some collection.' message", "%2 One track was already there.", "%2 %1 tracks were already there.", duplicateCount, transferredText ); Amarok::Components::logger()->longMessage( text ); } else { // somethig more severe failed, notify user using a dialog emit displaySorryDialog(); } } void IpodCopyTracksJob::abort() { m_aborted = true; } void IpodCopyTracksJob::slotStartDuplicateTrackSearch( const Meta::TrackPtr &track ) { Collections::QueryMaker *qm = m_coll.data()->queryMaker(); qm->setQueryType( Collections::QueryMaker::Track ); // we cannot qm->addMatch( track ) - it matches by uidUrl() qm->addFilter( Meta::valTitle, track->name(), true, true ); qm->addMatch( track->album() ); qm->addMatch( track->artist(), Collections::QueryMaker::TrackArtists ); qm->addMatch( track->composer() ); qm->addMatch( track->year() ); qm->addNumberFilter( Meta::valTrackNr, track->trackNumber(), Collections::QueryMaker::Equals ); qm->addNumberFilter( Meta::valDiscNr, track->discNumber(), Collections::QueryMaker::Equals ); // we don't want to match by filesize, track length, filetype etc - these change during // transcoding. We don't match album artist because handling of it is inconsistent connect( qm, SIGNAL(newResultReady(Meta::TrackList)), SLOT(slotDuplicateTrackSearchNewResult(Meta::TrackList)) ); connect( qm, SIGNAL(queryDone()), SLOT(slotDuplicateTrackSearchQueryDone()) ); qm->setAutoDelete( true ); m_duplicateTrack = Meta::TrackPtr(); // reset duplicate track from previous query qm->run(); } void IpodCopyTracksJob::slotDuplicateTrackSearchNewResult( const Meta::TrackList &tracks ) { if( !tracks.isEmpty() ) // we don't really know which one, but be sure to allow multiple results m_duplicateTrack = tracks.last(); } void IpodCopyTracksJob::slotDuplicateTrackSearchQueryDone() { m_searchingForDuplicates.release( 1 ); // wakeup run() } void IpodCopyTracksJob::slotStartCopyOrTranscodeJob( const KUrl &sourceUrl, const KUrl &destUrl, bool isJustCopy ) { KJob *job = 0; if( isJustCopy ) { if( m_goingToRemoveSources && m_coll && sourceUrl.toLocalFile().startsWith( m_coll.data()->mountPoint() ) ) { // special case for "add orphaned tracks" to either save space and significantly // speed-up the process: debug() << "Moving from" << sourceUrl << "to" << destUrl; job = KIO::file_move( sourceUrl, destUrl, -1, KIO::HideProgressInfo | KIO::Overwrite ); } else { debug() << "Copying from" << sourceUrl << "to" << destUrl; job = KIO::file_copy( sourceUrl, destUrl, -1, KIO::HideProgressInfo | KIO::Overwrite ); } } else { debug() << "Transcoding from" << sourceUrl << "to" << destUrl; job = new Transcoding::Job( sourceUrl, destUrl, m_transcodingConfig ); } job->setUiDelegate( 0 ); // be non-interactive connect( job, SIGNAL(finished(KJob*)), // we must use this instead of result() to prevent deadlock SLOT(slotCopyOrTranscodeJobFinished(KJob*)) ); job->start(); // no-op for KIO job, but matters for transcoding job } void IpodCopyTracksJob::slotCopyOrTranscodeJobFinished( KJob *job ) { if( job->error() != 0 && m_copyErrors.count() < 10 ) m_copyErrors.insert( job->errorString() ); m_copying.release( 1 ); // wakeup run() } void IpodCopyTracksJob::slotDisplaySorryDialog() { int sourceSize = m_sources.size(); int successCount = m_sourceTrackStatus.count( Success ); // match string with IpodCollectionLocation::prettyLocation() QString collName = m_coll ? m_coll.data()->prettyName() : i18n( "Disconnected iPod/iPad/iPhone" ); QString caption = i18nc( "%1 is collection pretty name, e.g. My Little iPod", "Transferred Tracks to %1", collName ); QString text; if( successCount ) text = i18np( "One track successfully transferred, but transfer of some other tracks failed.", "%1 tracks successfully transferred, but transfer of some other tracks failed.", successCount ); else text = i18n( "Transfer of tracks failed." ); QString details; int exceededingSafeCapacityCount = m_sourceTrackStatus.count( ExceededingSafeCapacity ); if( exceededingSafeCapacityCount ) { details += i18np( "One track was not transferred because it would exceed iPod capacity.
", "%1 tracks were not transferred because it would exceed iPod capacity.
", exceededingSafeCapacityCount ); QString reservedSpace = m_coll ? KGlobal::locale()->formatByteSize( m_coll.data()->capacityMargin(), 1 ) : QString( "???" ); // improbable, don't bother translators details += i18nc( "Example of %1 would be: 20.0 MiB", "Amarok reserves %1 on iPod for iTunes database writing.
", reservedSpace ); } int notPlayableCount = m_sourceTrackStatus.count( NotPlayable ); if( notPlayableCount ) { QString formats = QStringList( m_notPlayableFormats.toList() ).join( ", " ); details += i18np( "One track was not copied because it wouldn't be playable - its " " %2 format is unsupported.
", "%1 tracks were not copied because they wouldn't be playable - " "they are in unsupported formats (%2).
", notPlayableCount, formats ); } int copyingFailedCount = m_sourceTrackStatus.count( CopyingFailed ); if( copyingFailedCount ) { details += i18np( "Copy/move/transcode of one file failed.
", "Copy/move/transcode of %1 files failed.
", copyingFailedCount ); } int internalErrorCount = m_sourceTrackStatus.count( InternalError ); if( internalErrorCount ) { details += i18np( "One track was not transferred due to an internal Amarok error.
", "%1 tracks were not transferred due to an internal Amarok error.
", internalErrorCount ); details += i18n( "You can find details in Amarok debugging output.
" ); } if( m_sourceTrackStatus.size() != sourceSize ) { // aborted case was already caught in run() details += i18n( "The rest was not transferred because iPod collection disappeared.
" ); } if( !m_copyErrors.isEmpty() ) { details += i18nc( "%1 is a list of errors that occurred during copying of tracks", "Error causes: %1
", QStringList( m_copyErrors.toList() ).join( "
" ) ); } KMessageBox::detailedSorry( 0, text, details, caption ); } void IpodCopyTracksJob::trackProcessed( CopiedStatus status, Meta::TrackPtr srcTrack, Meta::TrackPtr destTrack ) { m_sourceTrackStatus.insert( status, srcTrack ); emit incrementProgress(); emit signalTrackProcessed( srcTrack, destTrack, status ); } #include "IpodCopyTracksJob.moc" diff --git a/src/core-impl/collections/playdarcollection/PlaydarCollection.cpp b/src/core-impl/collections/playdarcollection/PlaydarCollection.cpp index e4d5acffe4..e2b5c6cc51 100644 --- a/src/core-impl/collections/playdarcollection/PlaydarCollection.cpp +++ b/src/core-impl/collections/playdarcollection/PlaydarCollection.cpp @@ -1,375 +1,375 @@ /**************************************************************************************** * Copyright (c) 2010 Andrew Coder * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "PlaydarCollection" #include "PlaydarCollection.h" #include "core/collections/Collection.h" #include "core-impl/collections/support/MemoryCollection.h" #include "core-impl/collections/support/MemoryQueryMaker.h" #include "core-impl/collections/support/CollectionManager.h" #include "core-impl/meta/proxy/MetaProxy.h" #include "PlaydarQueryMaker.h" #include "support/Controller.h" #include "support/ProxyResolver.h" #include "core/support/Debug.h" #include #include #include #include namespace Collections { AMAROK_EXPORT_COLLECTION( PlaydarCollectionFactory, playdarcollection ) PlaydarCollectionFactory::PlaydarCollectionFactory( QObject* parent, const QVariantList &args ) : CollectionFactory( parent, args ) , m_controller( 0 ) , m_collectionIsManaged( false ) { m_info = KPluginInfo( "amarok_collection-playdarcollection.desktop", "services" ); DEBUG_BLOCK } PlaydarCollectionFactory::~PlaydarCollectionFactory() { DEBUG_BLOCK CollectionManager::instance()->removeTrackProvider( m_collection.data() ); delete m_collection.data(); delete m_controller; } void PlaydarCollectionFactory::init() { DEBUG_BLOCK - m_controller = new Playdar::Controller( this ); + m_controller = new Playdar::Controller(); connect( m_controller, SIGNAL(playdarReady()), this, SLOT(playdarReady()) ); connect( m_controller, SIGNAL(playdarError(Playdar::Controller::ErrorState)), this, SLOT(slotPlaydarError(Playdar::Controller::ErrorState)) ); checkStatus(); m_collection = new PlaydarCollection; connect( m_collection.data(), SIGNAL(remove()), this, SLOT(collectionRemoved()) ); CollectionManager::instance()->addTrackProvider( m_collection.data() ); m_initialized = true; } void PlaydarCollectionFactory::checkStatus() { m_controller->status(); } void PlaydarCollectionFactory::playdarReady() { DEBUG_BLOCK if( !m_collection ) { m_collection = new PlaydarCollection(); connect( m_collection.data(), SIGNAL(remove()), this, SLOT(collectionRemoved()) ); } if( !m_collectionIsManaged ) { m_collectionIsManaged = true; emit newCollection( m_collection.data() ); } } void PlaydarCollectionFactory::slotPlaydarError( Playdar::Controller::ErrorState error ) { // DEBUG_BLOCK if( error == Playdar::Controller::ErrorState( 1 ) ) { if( m_collection && !m_collectionIsManaged ) CollectionManager::instance()->removeTrackProvider( m_collection.data() ); QTimer::singleShot( 10 * 60 * 1000, this, SLOT(checkStatus()) ); } } void PlaydarCollectionFactory::collectionRemoved() { DEBUG_BLOCK m_collectionIsManaged = false; QTimer::singleShot( 10000, this, SLOT(checkStatus()) ); } PlaydarCollection::PlaydarCollection() : m_collectionId( i18n( "Playdar Collection" ) ) , m_memoryCollection( new MemoryCollection ) { DEBUG_BLOCK } PlaydarCollection::~PlaydarCollection() { DEBUG_BLOCK } QueryMaker* PlaydarCollection::queryMaker() { DEBUG_BLOCK PlaydarQueryMaker *freshQueryMaker = new PlaydarQueryMaker( this ); connect( freshQueryMaker, SIGNAL(playdarError(Playdar::Controller::ErrorState)), this, SLOT(slotPlaydarError(Playdar::Controller::ErrorState)) ); return freshQueryMaker; } Playlists::UserPlaylistProvider* PlaydarCollection::userPlaylistProvider() { DEBUG_BLOCK return 0; } QString PlaydarCollection::uidUrlProtocol() const { return QString( "playdar" ); } QString PlaydarCollection::collectionId() const { return m_collectionId; } QString PlaydarCollection::prettyName() const { return collectionId(); } KIcon PlaydarCollection::icon() const { return KIcon( "network-server" ); } bool PlaydarCollection::isWritable() const { DEBUG_BLOCK return false; } bool PlaydarCollection::isOrganizable() const { DEBUG_BLOCK return false; } bool PlaydarCollection::possiblyContainsTrack( const KUrl &url ) const { DEBUG_BLOCK if( url.protocol() == uidUrlProtocol() && url.hasQueryItem( QString( "artist" ) ) && url.hasQueryItem( QString( "album" ) ) && url.hasQueryItem( QString( "title" ) ) ) return true; else return false; } Meta::TrackPtr PlaydarCollection::trackForUrl( const KUrl &url ) { DEBUG_BLOCK m_memoryCollection->acquireReadLock(); if( m_memoryCollection->trackMap().contains( url.url() ) ) { Meta::TrackPtr track = m_memoryCollection->trackMap().value( url.url() ); m_memoryCollection->releaseLock(); return track; } else { m_memoryCollection->releaseLock(); MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( url ) ); proxyTrack->setArtist( url.queryItem( "artist" ) ); proxyTrack->setAlbum( url.queryItem( "album" ) ); proxyTrack->setTitle( url.queryItem( "title" ) ); Playdar::ProxyResolver *proxyResolver = new Playdar::ProxyResolver( this, url, proxyTrack ); connect( proxyResolver, SIGNAL(playdarError(Playdar::Controller::ErrorState)), this, SLOT(slotPlaydarError(Playdar::Controller::ErrorState)) ); return Meta::TrackPtr::staticCast( proxyTrack ); } } bool PlaydarCollection::hasCapabilityInterface( Capabilities::Capability::Type type ) const { //TODO: Make this work once capabilities are set. Q_UNUSED( type ); return false; } Capabilities::Capability* PlaydarCollection::createCapabilityInterface( Capabilities::Capability::Type type ) { //TODO: Make this work once capabilities are set. Q_UNUSED( type ); return 0; } void PlaydarCollection::addNewTrack( Meta::PlaydarTrackPtr track ) { DEBUG_BLOCK m_memoryCollection->acquireReadLock(); if( !m_memoryCollection->trackMap().contains( track->uidUrl() ) ) { m_memoryCollection->releaseLock(); m_memoryCollection->acquireWriteLock(); Meta::PlaydarArtistPtr artistPtr; if( m_memoryCollection->artistMap().contains( track->artist()->name() ) ) { Meta::ArtistPtr artist = m_memoryCollection->artistMap().value( track->artist()->name() ); artistPtr = Meta::PlaydarArtistPtr::staticCast( artist ); } else { artistPtr = track->playdarArtist(); Meta::ArtistPtr artist = Meta::ArtistPtr::staticCast( artistPtr ); m_memoryCollection->addArtist( artist ); } artistPtr->addTrack( track ); track->setArtist( artistPtr ); Meta::PlaydarAlbumPtr albumPtr; if( m_memoryCollection->albumMap().contains( track->album()->name(), artistPtr->name() ) ) { Meta::AlbumPtr album = m_memoryCollection->albumMap().value( track->album()->name(), artistPtr->name() ); albumPtr = Meta::PlaydarAlbumPtr::staticCast( album ); } else { albumPtr = track->playdarAlbum(); albumPtr->setAlbumArtist( artistPtr ); artistPtr->addAlbum( albumPtr ); Meta::AlbumPtr album = Meta::AlbumPtr::staticCast( albumPtr ); m_memoryCollection->addAlbum( album ); } albumPtr->addTrack( track ); track->setAlbum( albumPtr ); Meta::PlaydarGenrePtr genrePtr; if( m_memoryCollection->genreMap().contains( track->genre()->name() ) ) { Meta::GenrePtr genre = m_memoryCollection->genreMap().value( track->genre()->name() ); genrePtr = Meta::PlaydarGenrePtr::staticCast( genre ); } else { genrePtr = track->playdarGenre(); Meta::GenrePtr genre = Meta::GenrePtr::staticCast( genrePtr ); m_memoryCollection->addGenre( genre ); } genrePtr->addTrack( track ); track->setGenre( genrePtr ); Meta::PlaydarComposerPtr composerPtr; if( m_memoryCollection->composerMap().contains( track->composer()->name() ) ) { Meta::ComposerPtr composer = m_memoryCollection->composerMap().value( track->composer()->name() ); composerPtr = Meta::PlaydarComposerPtr::staticCast( composer ); } else { composerPtr = track->playdarComposer(); Meta::ComposerPtr composer = Meta::ComposerPtr::staticCast( composerPtr ); m_memoryCollection->addComposer( composer ); } composerPtr->addTrack( track ); track->setComposer( composerPtr ); Meta::PlaydarYearPtr yearPtr; if( m_memoryCollection->yearMap().contains( track->year()->year() ) ) { Meta::YearPtr year = m_memoryCollection->yearMap().value( track->year()->year() ); yearPtr = Meta::PlaydarYearPtr::staticCast( year ); } else { yearPtr = track->playdarYear(); Meta::YearPtr year = Meta::YearPtr::staticCast( yearPtr ); m_memoryCollection->addYear( year ); } yearPtr->addTrack( track ); track->setYear( yearPtr ); m_memoryCollection->addTrack( Meta::TrackPtr::staticCast( track ) ); foreach( Meta::PlaydarLabelPtr label, track->playdarLabels() ) { m_memoryCollection->addLabelToTrack( Meta::LabelPtr::staticCast( label ), Meta::TrackPtr::staticCast( track ) ); } m_memoryCollection->releaseLock(); emit updated(); } else m_memoryCollection->releaseLock(); } QSharedPointer< MemoryCollection > PlaydarCollection::memoryCollection() { return m_memoryCollection; } void PlaydarCollection::slotPlaydarError( Playdar::Controller::ErrorState error ) { if( error == Playdar::Controller::ErrorState( 1 ) ) emit remove(); } } diff --git a/src/core-impl/collections/support/MemoryMatcher.cpp b/src/core-impl/collections/support/MemoryMatcher.cpp index b545554d7b..1da6016c65 100644 --- a/src/core-impl/collections/support/MemoryMatcher.cpp +++ b/src/core-impl/collections/support/MemoryMatcher.cpp @@ -1,366 +1,366 @@ /**************************************************************************************** * Copyright (c) 2007 Nikolaj Hald Nielsen * * Copyright (c) 2007 Maximilian Kossick * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "MemoryMatcher.h" using namespace Meta; MemoryMatcher::MemoryMatcher() : m_next( 0 ) { } MemoryMatcher::~MemoryMatcher() { delete m_next; } bool MemoryMatcher::isLast() const { return !m_next; } MemoryMatcher* MemoryMatcher::next() const { return m_next; } void MemoryMatcher::setNext( MemoryMatcher *next ) { delete m_next; m_next = next; } TrackMatcher::TrackMatcher( TrackPtr track ) : MemoryMatcher() , m_track( track ) {} TrackList TrackMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_track || !memColl ) return TrackList(); TrackMap trackMap = memColl->trackMap(); TrackList result; if ( trackMap.contains( m_track->uidUrl() ) ) result.append( trackMap.value( m_track->uidUrl() ) ); return result; //checking for another matcher is not necessary } TrackList TrackMatcher::match( const TrackList &tracks ) { if( !m_track ) return TrackList(); TrackList result; QString url = m_track->uidUrl(); foreach( TrackPtr track, tracks ) if ( track->uidUrl() == url ) { result.append( track ); break; } return result; //checking for another matcher is not necessary } ArtistMatcher::ArtistMatcher( ArtistPtr artist, Collections::QueryMaker::ArtistMatchBehaviour artistMode ) : MemoryMatcher() , m_artist( artist ) , m_queryMode( artistMode ) {} TrackList ArtistMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_artist || !memColl ) return TrackList(); if( !memColl->artistMap().contains( m_artist->name() ) ) return TrackList(); ArtistPtr artist = memColl->artistMap().value( m_artist->name() ); TrackList matchingTracks; switch( m_queryMode ) { case Collections::QueryMaker::AlbumOrTrackArtists: case Collections::QueryMaker::AlbumArtists: foreach( AlbumPtr album, memColl->albumMap() ) if( album->albumArtist() == artist ) matchingTracks.append( album->tracks() ); - if( m_queryMode != Collections::QueryMaker::AlbumOrTrackArtists ) break; - + /* Falls through. */ case Collections::QueryMaker::TrackArtists: matchingTracks.append( artist->tracks() ); } if( isLast() || matchingTracks.isEmpty() ) return matchingTracks; else return next()->match( matchingTracks ); } TrackList ArtistMatcher::match( const TrackList &tracks ) { if( !m_artist ) return TrackList(); TrackList matchingTracks; QString name = m_artist->name(); foreach( TrackPtr track, tracks ) switch( m_queryMode ) { case Collections::QueryMaker::AlbumOrTrackArtists: case Collections::QueryMaker::AlbumArtists: if( track->album()->hasAlbumArtist() && track->album()->albumArtist()->name() == name ) matchingTracks.append( track ); if( m_queryMode != Collections::QueryMaker::AlbumOrTrackArtists ) break; + /* Falls through. */ case Collections::QueryMaker::TrackArtists: if( track->artist()->name() == name ) matchingTracks.append( track ); } if( isLast() || matchingTracks.isEmpty() ) return matchingTracks; else return next()->match( matchingTracks ); } AlbumMatcher::AlbumMatcher( AlbumPtr album ) : MemoryMatcher() , m_album( album ) {} TrackList AlbumMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_album || !memColl ) return TrackList(); AlbumMap albumMap = memColl->albumMap(); if ( albumMap.contains( m_album ) ) // compares albums by value { AlbumPtr album = albumMap.value( m_album ); // compares albums by value, too TrackList matchingTracks = album->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList AlbumMatcher::match( const TrackList &tracks ) { if( !m_album ) return TrackList(); TrackList matchingTracks; QString name = m_album->name(); foreach( TrackPtr track, tracks ) if ( track->album()->name() == name ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } GenreMatcher::GenreMatcher( GenrePtr genre ) : MemoryMatcher() , m_genre( genre ) {} TrackList GenreMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_genre || !memColl ) return TrackList(); GenreMap genreMap = memColl->genreMap(); if ( genreMap.contains( m_genre->name() ) ) { GenrePtr genre = genreMap.value( m_genre->name() ); TrackList matchingTracks = genre->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList GenreMatcher::match( const TrackList &tracks ) { if( !m_genre ) return TrackList(); TrackList matchingTracks; QString name = m_genre->name(); foreach( TrackPtr track, tracks ) if ( track->genre()->name() == name ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } ComposerMatcher::ComposerMatcher( ComposerPtr composer ) : MemoryMatcher() , m_composer( composer ) {} TrackList ComposerMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_composer || !memColl ) return TrackList(); ComposerMap composerMap = memColl->composerMap(); if ( composerMap.contains( m_composer->name() ) ) { ComposerPtr composer = composerMap.value( m_composer->name() ); TrackList matchingTracks = composer->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList ComposerMatcher::match( const TrackList &tracks ) { if( !m_composer ) return TrackList(); TrackList matchingTracks; QString name = m_composer->name(); foreach( TrackPtr track, tracks ) if ( track->composer()->name() == name ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } YearMatcher::YearMatcher( YearPtr year ) : MemoryMatcher() , m_year( year ) {} TrackList YearMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_year || !memColl ) return TrackList(); YearMap yearMap = memColl->yearMap(); if ( yearMap.contains( m_year->year() ) ) { YearPtr year = yearMap.value( m_year->year() ); TrackList matchingTracks = year->tracks(); if ( isLast() ) return matchingTracks; else return next()->match( matchingTracks ); } else return TrackList(); } TrackList YearMatcher::match( const TrackList &tracks ) { if( !m_year ) return TrackList(); TrackList matchingTracks; int year = m_year->year(); foreach( TrackPtr track, tracks ) if ( track->year()->year() == year ) matchingTracks.append( track ); if ( isLast() || matchingTracks.count() == 0) return matchingTracks; else return next()->match( matchingTracks ); } LabelMatcher::LabelMatcher( const Meta::LabelPtr &label ) : MemoryMatcher() , m_label( label ) { //nothing to do } Meta::TrackList LabelMatcher::match( const Meta::TrackList &tracks ) { if( !m_label ) return Meta::TrackList(); Meta::TrackList matchingTracks; QString name = m_label->name(); //not really efficient... foreach( const Meta::TrackPtr &track, tracks ) { foreach( const Meta::LabelPtr &label, track->labels() ) { if( name == label->name() ) { matchingTracks << track; break; } } } if( isLast() || matchingTracks.count() == 0 ) return matchingTracks; else return next()->match( matchingTracks ); } Meta::TrackList LabelMatcher::match( Collections::MemoryCollection *memColl ) { if( !m_label ) return Meta::TrackList(); Meta::TrackList matchingTracks; if( memColl->labelMap().contains( m_label->name() ) ) { //m_label might actually be a proxy label Meta::LabelPtr realLabel = memColl->labelMap().value( m_label->name() ); matchingTracks = memColl->labelToTrackMap().value( realLabel ); } if( isLast() || matchingTracks.count() == 0 ) return matchingTracks; else return next()->match( matchingTracks ); } diff --git a/src/core-impl/meta/file/File.cpp b/src/core-impl/meta/file/File.cpp index be6bf15f24..14c8b89107 100644 --- a/src/core-impl/meta/file/File.cpp +++ b/src/core-impl/meta/file/File.cpp @@ -1,596 +1,595 @@ /**************************************************************************************** * Copyright (c) 2007 Maximilian Kossick * * Copyright (c) 2008 Seb Ruiz * * Copyright (c) 2012 Matěj Laitl * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "File.h" #include "File_p.h" #include #ifdef HAVE_LIBLASTFM #include "LastfmReadLabelCapability.h" #endif #include "MainWindow.h" #include "amarokurls/BookmarkMetaActions.h" #include "amarokurls/PlayUrlRunner.h" #include "browsers/filebrowser/FileBrowser.h" #include "core/capabilities/BookmarkThisCapability.h" #include "core/capabilities/FindInSourceCapability.h" #include "core/meta/Meta.h" #include "core/meta/support/MetaUtility.h" #include "core/playlists/PlaylistFormat.h" #include "core/support/Amarok.h" #include "core-impl/capabilities/timecode/TimecodeWriteCapability.h" #include "core-impl/capabilities/timecode/TimecodeLoadCapability.h" #include "core-impl/support/UrlStatisticsStore.h" #include #include #include #include #include #include using namespace MetaFile; class TimecodeWriteCapabilityImpl : public Capabilities::TimecodeWriteCapability { public: TimecodeWriteCapabilityImpl( MetaFile::Track *track ) : Capabilities::TimecodeWriteCapability() , m_track( track ) {} virtual bool writeTimecode ( qint64 miliseconds ) { DEBUG_BLOCK return Capabilities::TimecodeWriteCapability::writeTimecode( miliseconds, Meta::TrackPtr( m_track.data() ) ); } virtual bool writeAutoTimecode ( qint64 miliseconds ) { DEBUG_BLOCK return Capabilities::TimecodeWriteCapability::writeAutoTimecode( miliseconds, Meta::TrackPtr( m_track.data() ) ); } private: KSharedPtr m_track; }; class TimecodeLoadCapabilityImpl : public Capabilities::TimecodeLoadCapability { public: TimecodeLoadCapabilityImpl( MetaFile::Track *track ) : Capabilities::TimecodeLoadCapability() , m_track( track ) {} virtual bool hasTimecodes() { if ( loadTimecodes().size() > 0 ) return true; return false; } virtual BookmarkList loadTimecodes() { BookmarkList list = PlayUrlRunner::bookmarksFromUrl( m_track->playableUrl() ); return list; } private: KSharedPtr m_track; }; class FindInSourceCapabilityImpl : public Capabilities::FindInSourceCapability { public: FindInSourceCapabilityImpl( MetaFile::Track *track ) : Capabilities::FindInSourceCapability() , m_track( track ) {} virtual void findInSource( QFlags tag ) { Q_UNUSED( tag ) //first show the filebrowser AmarokUrl url; url.setCommand( "navigate" ); url.setPath( "files" ); url.run(); //then navigate to the correct directory BrowserCategory * fileCategory = The::mainWindow()->browserDock()->list()->activeCategoryRecursive(); if( fileCategory ) { FileBrowser * fileBrowser = dynamic_cast( fileCategory ); if( fileBrowser ) { //get the path of the parent directory of the file KUrl playableUrl = m_track->playableUrl(); fileBrowser->setDir( playableUrl.directory() ); } } } private: KSharedPtr m_track; }; Track::Track( const KUrl &url ) : Meta::Track() , d( new Track::Private( this ) ) { d->url = url; d->readMetaData(); d->album = Meta::AlbumPtr( new MetaFile::FileAlbum( d ) ); d->artist = Meta::ArtistPtr( new MetaFile::FileArtist( d ) ); d->albumArtist = Meta::ArtistPtr( new MetaFile::FileArtist( d, true ) ); d->genre = Meta::GenrePtr( new MetaFile::FileGenre( d ) ); d->composer = Meta::ComposerPtr( new MetaFile::FileComposer( d ) ); d->year = Meta::YearPtr( new MetaFile::FileYear( d ) ); } Track::~Track() { delete d; } QString Track::name() const { if( d ) { const QString trackName = d->m_data.title; return trackName; } return "This is a bug!"; } KUrl Track::playableUrl() const { return d->url; } QString Track::prettyUrl() const { if(d->url.isLocalFile()) { return d->url.toLocalFile(); } else { return d->url.path(); } } QString Track::uidUrl() const { return d->url.url(); } QString Track::notPlayableReason() const { return localFileNotPlayableReason( playableUrl().toLocalFile() ); } bool Track::isEditable() const { QFileInfo info = QFileInfo( playableUrl().pathOrUrl() ); return info.isFile() && info.isWritable(); } Meta::AlbumPtr Track::album() const { return d->album; } Meta::ArtistPtr Track::artist() const { return d->artist; } Meta::GenrePtr Track::genre() const { return d->genre; } Meta::ComposerPtr Track::composer() const { return d->composer; } Meta::YearPtr Track::year() const { return d->year; } void Track::setAlbum( const QString &newAlbum ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valAlbum, newAlbum ); } void Track::setAlbumArtist( const QString &newAlbumArtist ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valAlbumArtist, newAlbumArtist ); } void Track::setArtist( const QString &newArtist ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valArtist, newArtist ); } void Track::setGenre( const QString &newGenre ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valGenre, newGenre ); } void Track::setComposer( const QString &newComposer ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valComposer, newComposer ); } void Track::setYear( int newYear ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valYear, newYear ); } void Track::setTitle( const QString &newTitle ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valTitle, newTitle ); } void Track::setBpm( const qreal newBpm ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valBpm, newBpm ); } qreal Track::bpm() const { const qreal bpm = d->m_data.bpm; return bpm; } QString Track::comment() const { const QString commentName = d->m_data.comment; if( !commentName.isEmpty() ) return commentName; return QString(); } void Track::setComment( const QString& newComment ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valComment, newComment ); } int Track::trackNumber() const { return d->m_data.trackNumber; } void Track::setTrackNumber( int newTrackNumber ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valTrackNr, newTrackNumber ); } int Track::discNumber() const { return d->m_data.discNumber; } void Track::setDiscNumber( int newDiscNumber ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valDiscNr, newDiscNumber ); } qint64 Track::length() const { qint64 length = d->m_data.length; if( length == -2 /*Undetermined*/ ) length = 0; return length; } int Track::filesize() const { return d->m_data.fileSize; } int Track::sampleRate() const { int sampleRate = d->m_data.sampleRate; if( sampleRate == -2 /*Undetermined*/ ) sampleRate = 0; return sampleRate; } int Track::bitrate() const { int bitrate = d->m_data.bitRate; if( bitrate == -2 /*Undetermined*/ ) bitrate = 0; return bitrate; } QDateTime Track::createDate() const { if( d->m_data.created > 0 ) return QDateTime::fromTime_t(d->m_data.created); else return QDateTime(); } qreal Track::replayGain( Meta::ReplayGainTag mode ) const { switch( mode ) { case Meta::ReplayGain_Track_Gain: return d->m_data.trackGain; case Meta::ReplayGain_Track_Peak: return d->m_data.trackPeak; case Meta::ReplayGain_Album_Gain: return d->m_data.albumGain; case Meta::ReplayGain_Album_Peak: return d->m_data.albumPeak; } return 0.0; } QString Track::type() const { return Amarok::extension( d->url.fileName() ); } bool Track::isTrack( const KUrl &url ) { // some playlists lay under audio/ mime category, filter them if( Playlists::isPlaylist( url ) ) return false; // accept remote files, it's too slow to check them at this point if( !url.isLocalFile() ) return true; QFileInfo fileInfo( url.toLocalFile() ); if( fileInfo.size() <= 0 ) return false; // We can't play directories if( fileInfo.isDir() ) return false; const KMimeType::Ptr mimeType = KMimeType::findByPath( url.toLocalFile() ); const QString name = mimeType->name(); return name.startsWith( "audio/" ) || name.startsWith( "video/" ); } void Track::beginUpdate() { QWriteLocker locker( &d->lock ); d->batchUpdate++; } void Track::endUpdate() { QWriteLocker locker( &d->lock ); Q_ASSERT( d->batchUpdate > 0 ); d->batchUpdate--; commitIfInNonBatchUpdate(); } bool Track::inCollection() const { return d->collection; // calls QWeakPointer's (bool) operator } Collections::Collection* Track::collection() const { return d->collection.data(); } void Track::setCollection( Collections::Collection *newCollection ) { d->collection = newCollection; } bool Track::hasCapabilityInterface( Capabilities::Capability::Type type ) const { bool readlabel = false; #ifdef HAVE_LIBLASTFM readlabel = true; #endif return type == Capabilities::Capability::BookmarkThis || type == Capabilities::Capability::WriteTimecode || type == Capabilities::Capability::LoadTimecode || ( type == Capabilities::Capability::ReadLabel && readlabel ) || type == Capabilities::Capability::FindInSource; } Capabilities::Capability* Track::createCapabilityInterface( Capabilities::Capability::Type type ) { switch( type ) { case Capabilities::Capability::BookmarkThis: return new Capabilities::BookmarkThisCapability( new BookmarkCurrentTrackPositionAction( 0 ) ); case Capabilities::Capability::WriteTimecode: return new TimecodeWriteCapabilityImpl( this ); case Capabilities::Capability::LoadTimecode: return new TimecodeLoadCapabilityImpl( this ); case Capabilities::Capability::FindInSource: return new FindInSourceCapabilityImpl( this ); #ifdef HAVE_LIBLASTFM - case Capabilities::Capability::ReadLabel: - if( !d->readLabelCapability ) - d->readLabelCapability = new Capabilities::LastfmReadLabelCapability( this ); + case Capabilities::Capability::ReadLabel: + if( !d->readLabelCapability ) + d->readLabelCapability = new Capabilities::LastfmReadLabelCapability( this ); + return 0; #endif - default: // fall-through - - - return 0; + default: + return 0; } } Meta::TrackEditorPtr Track::editor() { return Meta::TrackEditorPtr( isEditable() ? this : 0 ); } Meta::StatisticsPtr Track::statistics() { return Meta::StatisticsPtr( this ); } double Track::score() const { return d->m_data.score; } void Track::setScore( double newScore ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valScore, newScore ); } int Track::rating() const { return d->m_data.rating; } void Track::setRating( int newRating ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valRating, newRating ); } int Track::playCount() const { return d->m_data.playCount; } void Track::setPlayCount( int newPlayCount ) { QWriteLocker locker( &d->lock ); commitIfInNonBatchUpdate( Meta::valPlaycount, newPlayCount ); } QImage Track::getEmbeddedCover() const { if( d->m_data.embeddedImage ) return Meta::Tag::embeddedCover( d->url.path() ); return QImage(); } void Track::commitIfInNonBatchUpdate( qint64 field, const QVariant &value ) { d->changes.insert( field, value ); commitIfInNonBatchUpdate(); } void Track::commitIfInNonBatchUpdate() { static const QSet statFields = ( QSet() << Meta::valFirstPlayed << Meta::valLastPlayed << Meta::valPlaycount << Meta::valScore << Meta::valRating ); if( d->batchUpdate > 0 || d->changes.isEmpty() ) return; // special case (shortcut) when writing statistics is disabled if( !AmarokConfig::writeBackStatistics() && (QSet::fromList( d->changes.keys() ) - statFields).isEmpty() ) { d->changes.clear(); return; } d->writeMetaData(); // clears d->chages d->lock.unlock(); // rather call notifyObservers() without a lock notifyObservers(); d->lock.lockForWrite(); // return to original state } #include "File_p.moc" diff --git a/src/playlist/proxymodels/SortAlgorithms.cpp b/src/playlist/proxymodels/SortAlgorithms.cpp index 52658259b0..d3e0a3d5c6 100644 --- a/src/playlist/proxymodels/SortAlgorithms.cpp +++ b/src/playlist/proxymodels/SortAlgorithms.cpp @@ -1,172 +1,175 @@ /**************************************************************************************** * Copyright (c) 2009 Téo Mrnjavac * * Copyright (c) 2010 Nanno Langstraat * * Copyright (c) 2013 Konrad Zemek * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "SortAlgorithms.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/support/Debug.h" #include "playlist/proxymodels/AbstractModel.h" #include namespace Playlist { void multilevelLessThan::setSortScheme( const SortScheme & scheme ) { m_scheme = scheme; m_randomSalt = qrand(); //! Do a different random sort order every time. } bool multilevelLessThan::operator()( const QAbstractItemModel* sourceModel, int sourceModelRowA, int sourceModelRowB ) const { // Handle "Last Played" as a special case because the time since last played is not // reported as an int in the data columns. Handle Title, Album, Artist as special // cases with Meta::Base::sortableName(). This is necessary in order to have the same // sort order policy regarding "The" in both the playlist and the collection browser. QSet< Playlist::Column > specialCases; specialCases << Playlist::LastPlayed << Playlist::Title << Playlist::Album << Playlist::Artist << Playlist::AlbumArtist; foreach( const SortLevel &level, m_scheme ) { const bool inverted = ( level.order() == Qt::DescendingOrder ); const Playlist::Column currentCategory = level.category(); const QModelIndex indexA = sourceModel->index( sourceModelRowA, currentCategory ); const QModelIndex indexB = sourceModel->index( sourceModelRowB, currentCategory ); const Meta::TrackPtr trackA = indexA.data( TrackRole ).value(); const Meta::TrackPtr trackB = indexB.data( TrackRole ).value(); if( trackA && trackB && specialCases.contains( currentCategory ) ) { switch( currentCategory ) { case Playlist::LastPlayed: { const QDateTime lastPlayedA = trackA->statistics()->lastPlayed(); const QDateTime lastPlayedB = trackB->statistics()->lastPlayed(); // The track with higher lastPlayed value was played more recently // // '!=' is the XOR operation; it simply negates the result if 'inverted' // is true. It isn't necessarry to do it this way, although later on it will // ease figuring out what's actually being returned. if( lastPlayedA != lastPlayedB ) return ( lastPlayedA > lastPlayedB ) != inverted; break; } case Playlist::Title: { const int compareResult = compareBySortableName( trackA, trackB ); + if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; break; } case Playlist::Album: { const int compareResult = compareBySortableName( trackA->album(), trackB->album() ); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; // Fall through to sorting by album artist if albums have same name + __attribute__ ((fallthrough)); } case Playlist::AlbumArtist: { const Meta::ArtistPtr artistA = (trackA->album() ? trackA->album()->albumArtist() : Meta::ArtistPtr()); const Meta::ArtistPtr artistB = (trackB->album() ? trackB->album()->albumArtist() : Meta::ArtistPtr()); const int compareResult = compareBySortableName( artistA, artistB ); + if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; break; } case Playlist::Artist: { const int compareResult = compareBySortableName( trackA->artist(), trackB->artist() ); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; break; } default: warning() << "One of the cases in specialCases set has not received special treatment!"; break; } } else // either it's not a special case, or we don't have means (TrackPtrs) to handle it { const QVariant dataA = indexA.data( Qt::DisplayRole ); const QVariant dataB = indexB.data( Qt::DisplayRole ); if( level.isString() ) { const int compareResult = dataA.toString().compare(dataB.toString(), Qt::CaseInsensitive); if( compareResult != 0 ) return ( compareResult < 0 ) != inverted; } else if( level.isFloat() ) { if( dataA.toDouble() != dataB.toDouble() ) return ( dataA.toDouble() < dataB.toDouble() ) != inverted; } else // if it's neither a string nor a float ==> it's an integer { if( dataA.toInt() != dataB.toInt() ) return ( dataA.toInt() < dataB.toInt() ) != inverted; } } } // Tie breaker: order by row number return ( sourceModelRowA < sourceModelRowB ); } template int multilevelLessThan::compareBySortableName( const KSharedPtr &left, const KSharedPtr &right ) const { if( !left && right ) return -1; else if( left && !right ) return 1; else if( left && right ) return left->sortableName().compare( right->sortableName(), Qt::CaseInsensitive ); return 0; } } //namespace Playlist diff --git a/src/playlistgenerator/ConstraintSolver.cpp b/src/playlistgenerator/ConstraintSolver.cpp index ba58a9cf7b..9fc91e4f07 100644 --- a/src/playlistgenerator/ConstraintSolver.cpp +++ b/src/playlistgenerator/ConstraintSolver.cpp @@ -1,408 +1,413 @@ /**************************************************************************************** * Copyright (c) 2008-2012 Soren Harward * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "APG::ConstraintSolver" // WORKAROUND for QTBUG-25960. Required for Qt versions < 4.8.5 in combination with libc++. #define QT_NO_STL 1 #include #undef QT_NO_STL #include "ConstraintSolver.h" #include "Constraint.h" #include "core/collections/QueryMaker.h" #include "core/meta/Meta.h" #include "core/support/Debug.h" #include "core/support/Components.h" #include "core/interfaces/Logger.h" #include "core-impl/collections/support/CollectionManager.h" #include "playlist/PlaylistModel.h" #include #include #include #include #include #include #include // STL algorithms #include #include const int APG::ConstraintSolver::QUALITY_RANGE = 10; APG::ConstraintSolver::ConstraintSolver( ConstraintNode* r, int qualityFactor ) : m_satisfactionThreshold( 0.95 ) , m_finalSatisfaction( 0.0 ) , m_constraintTreeRoot( r ) , m_domainReductionFailed( false ) , m_readyToRun( false ) , m_abortRequested( false ) , m_maxGenerations( 100 ) , m_populationSize( 40 ) , m_suggestedPlaylistSize( 15 ) { Q_UNUSED( qualityFactor); // FIXME m_serialNumber = KRandom::random(); if ( !m_constraintTreeRoot ) { error() << "No constraint tree was passed to the solver. Aborting."; m_readyToRun = true; m_abortRequested = true; return; } m_qm = CollectionManager::instance()->queryMaker(); if ( m_qm ) { debug() << "New ConstraintSolver with serial number" << m_serialNumber; m_qm->setQueryType( Collections::QueryMaker::Track ); connect( m_qm, SIGNAL(newResultReady(Meta::TrackList)), this, SLOT(receiveQueryMakerData(Meta::TrackList)), Qt::QueuedConnection ); connect( m_qm, SIGNAL(queryDone()), this, SLOT(receiveQueryMakerDone()), Qt::QueuedConnection ); m_constraintTreeRoot->initQueryMaker( m_qm ); m_qm->run(); } else { debug() << "The ConstraintSolver could not find any queryable collections. No results will be returned."; m_readyToRun = true; m_abortRequested = true; } } APG::ConstraintSolver::~ConstraintSolver() { if ( m_qm ) { m_qm->abortQuery(); m_qm->deleteLater(); m_qm = 0; } } Meta::TrackList APG::ConstraintSolver::getSolution() const { return m_solvedPlaylist; } bool APG::ConstraintSolver::satisfied() const { return m_finalSatisfaction > m_satisfactionThreshold; } bool APG::ConstraintSolver::canBeExecuted() { /* This is a hopefully superfluous check to ensure that the Solver job * doesn't get run until it's ready (ie, when QueryMaker has finished). * This shouldn't ever return false, because hopefully the job won't even * get queued until it's ready to run. See the comments in * Preset::queueSolver() for more information. -- sth */ return m_readyToRun; } void APG::ConstraintSolver::requestAbort() { if ( m_qm ) { m_qm->abortQuery(); m_qm->deleteLater(); m_qm = 0; } m_abortRequested = true; } bool APG::ConstraintSolver::success() const { return !m_abortRequested; } void APG::ConstraintSolver::run() { if ( !m_readyToRun ) { error() << "DANGER WILL ROBINSON! A ConstraintSolver (serial no:" << m_serialNumber << ") tried to run before its QueryMaker finished!"; m_abortRequested = true; return; } if ( m_domain.empty() ) { debug() << "The QueryMaker returned no tracks"; return; } else { debug() << "Domain has" << m_domain.size() << "tracks"; } debug() << "Running ConstraintSolver" << m_serialNumber; emit totalSteps( m_maxGenerations ); // GENETIC ALGORITHM LOOP Population population; quint32 generation = 0; Meta::TrackList* best = NULL; while ( !m_abortRequested && ( generation < m_maxGenerations ) ) { quint32 s = m_constraintTreeRoot->suggestPlaylistSize(); m_suggestedPlaylistSize = (s > 0) ? s : m_suggestedPlaylistSize; fill_population( population ); best = find_best( population ); if ( population.value( best ) < m_satisfactionThreshold ) { select_population( population, best ); mutate_population( population ); generation++; emit incrementProgress(); } else { break; } } debug() << "solution at" << (void*)(best); m_solvedPlaylist = best->mid( 0 ); m_finalSatisfaction = m_constraintTreeRoot->satisfaction( m_solvedPlaylist ); /* clean up */ Population::iterator it = population.begin(); while ( it != population.end() ) { delete it.key(); it = population.erase( it ); } emit endProgressOperation( this ); } void APG::ConstraintSolver::receiveQueryMakerData( Meta::TrackList results ) { m_domainMutex.lock(); m_domain += results; m_domainMutex.unlock(); } void APG::ConstraintSolver::receiveQueryMakerDone() { m_qm->deleteLater(); m_qm = 0; if (( m_domain.size() > 0 ) || m_domainReductionFailed ) { if ( m_domain.size() <= 0 ) { Amarok::Components::logger()->shortMessage( i18n("The playlist generator failed to load any tracks from the collection.") ); } m_readyToRun = true; emit readyToRun(); } else { Amarok::Components::logger()->longMessage( i18n("There are no tracks that match all constraints. " \ "The playlist generator will find the tracks that match best, " \ "but you may want to consider loosening the constraints to find more tracks.") ); m_domainReductionFailed = true; // need a new query maker without constraints m_qm = CollectionManager::instance()->queryMaker(); if ( m_qm ) { connect( m_qm, SIGNAL(newResultReady(Meta::TrackList)), this, SLOT(receiveQueryMakerData(Meta::TrackList)), Qt::QueuedConnection ); connect( m_qm, SIGNAL(queryDone()), this, SLOT(receiveQueryMakerDone()), Qt::QueuedConnection ); m_qm->setQueryType( Collections::QueryMaker::Track ); m_qm->run(); } } } void APG::ConstraintSolver::fill_population( Population& population ) { for ( int i = population.size(); quint32(i) < m_populationSize; i++ ) { Meta::TrackList* tl = new Meta::TrackList( sample( m_domain, playlist_size() ) ); double s = m_constraintTreeRoot->satisfaction( (*tl) ); population.insert( tl, s ); } } Meta::TrackList* APG::ConstraintSolver::find_best(const APG::ConstraintSolver::Population& population ) const { Population::const_iterator it = std::max_element( population.constBegin(), population.constEnd(), &pop_comp ); return it.key(); } void APG::ConstraintSolver::select_population( APG::ConstraintSolver::Population& population, Meta::TrackList* best ) { Population::Iterator it = population.begin(); while ( it != population.end() ) { if ( it.key() == best ) { ++it;// Always keep the best solution, no matter how bad it is if ( it == population.end() ) break; } if ( select( it.value() ) ) { ++it; } else { delete it.key(); it = population.erase( it ); } } } void APG::ConstraintSolver::mutate_population( APG::ConstraintSolver::Population& population ) { if ( population.size() < 1 ) return; const double mutantPercentage = 0.35; // TODO: tune this parameter QList parents( population.keys() ); int maxMutants = (int)( mutantPercentage * (double)(m_populationSize) ); for ( int i = parents.size(); i < maxMutants; i++ ) { int idx = KRandom::random() % parents.size(); Meta::TrackList* child = new Meta::TrackList( *(parents.at( idx )) ); int op = KRandom::random() % 5; int s = child->size(); switch (op) { case 0: child->removeAt( KRandom::random() % s ); + break; case 1: child->insert( KRandom::random() % ( s + 1 ), random_track_from_domain() ); + break; case 2: child->replace( KRandom::random() % s, random_track_from_domain() ); + break; case 3: child->swap( KRandom::random() % s, KRandom::random() % s ); + break; case 4: child = crossover( child, parents.at( KRandom::random() % parents.size() ) ); + break; default: (void)0; // effectively a no-op. the default is here so that the compiler doesn't complain about missing default in switch } population.insert( child, m_constraintTreeRoot->satisfaction( *child ) ); } return; } Meta::TrackList* APG::ConstraintSolver::crossover( Meta::TrackList* top, Meta::TrackList* bot ) const { const double crossoverPt = 0.5; // TODO: choose different values int topV = (int)( crossoverPt * (double)top->size() ); int botV = (int)( crossoverPt * (double)bot->size() ); Meta::TrackList* newlist = new Meta::TrackList( top->mid( 0, topV ) ); newlist->append( bot->mid( botV ) ); delete top; return newlist; } bool APG::ConstraintSolver::pop_comp( double a, double b ) { return ( a < b ); } Meta::TrackPtr APG::ConstraintSolver::random_track_from_domain() const { return m_domain.at( KRandom::random() % m_domain.size() ); } Meta::TrackList APG::ConstraintSolver::sample( Meta::TrackList domain, const int sampleSize ) const { std::random_shuffle( domain.begin(), domain.end() ); return domain.mid( 0, sampleSize ); } quint32 APG::ConstraintSolver::playlist_size() const { return rng_poisson( (double)m_suggestedPlaylistSize ); } bool APG::ConstraintSolver::select( const double satisfaction ) const { double x = (double)KRandom::random()/(double)RAND_MAX; const double scale = -30.0; // TODO: make adjustable return ( x < 1.0 / ( 1.0 + exp( scale * (satisfaction-0.8) ) ) ); } void APG::ConstraintSolver::dump_population( const Population& population ) const { DEBUG_BLOCK for ( Population::ConstIterator it = population.constBegin(); it != population.constEnd(); ++it ) { Meta::TrackList* tl = it.key(); debug() << "at" << (void*)(tl) << "satisfaction:" << it.value(); foreach ( Meta::TrackPtr t, (*tl) ) { debug() << "\ttrack:" << t->prettyName(); } } } double APG::ConstraintSolver::rng_gaussian( const double mu, const double sigma ) const { /* adapted from randist/gauss.c in GNU Scientific Library 1.14 */ double u, v, x, y, Q; const double s = 0.449871; const double t = -0.386595; const double a = 0.19600; const double b = 0.25472; const double r1 = 0.27597; const double r2 = 0.27846; do { u = 1 - rng_uniform(); v = ( rng_uniform() - 0.5 ) * 1.7156; x = u - s; y = fabs (v) - t; Q = x * x + y * (a * y - b * x); } while (Q >= r1 && (Q > r2 || v * v > -4 * u * u * log (u))); return mu + ( sigma * (v / u) ); } quint32 APG::ConstraintSolver::rng_poisson( const double mu ) const { if ( mu >= 25.0 ) { double v = rng_gaussian( mu, sqrt( mu ) ); return ( v < 0.0 ) ? 0 : (quint32)v; } const double emu = exp( -mu ); double prod = 1.0; quint32 k = 0; do { prod *= rng_uniform(); k++; } while ( prod > emu ); return k - 1; } double APG::ConstraintSolver::rng_uniform() const { return ( (double)KRandom::random() / (double)(RAND_MAX) ); } diff --git a/src/scripting/scriptengine/exporters/ScriptableBiasExporter.cpp b/src/scripting/scriptengine/exporters/ScriptableBiasExporter.cpp index 09d6b6a4c9..7fcf222375 100644 --- a/src/scripting/scriptengine/exporters/ScriptableBiasExporter.cpp +++ b/src/scripting/scriptengine/exporters/ScriptableBiasExporter.cpp @@ -1,559 +1,561 @@ /**************************************************************************************** * Copyright (c) 2013 Anmol Ahuja * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "ScriptableBias" #include "ScriptableBiasExporter.h" #include "core/support/Debug.h" #include "core/meta/Meta.h" #include "core-impl/collections/support/CollectionManager.h" #include #include #include #include #include #include using namespace AmarokScript; void ScriptableBiasFactory::init( QScriptEngine *engine ) { TrackSetExporter::init( engine ); engine->globalObject().setProperty( "BiasFactory", engine->newFunction( biasCtor ), QScriptValue:: Undeletable | QScriptValue::ReadOnly ); engine->globalObject().setProperty( "GroupBiasFactory", engine->newFunction( groupBiasCtor ), QScriptValue:: Undeletable | QScriptValue::ReadOnly ); } QScriptValue ScriptableBiasFactory::biasCtor( QScriptContext *context, QScriptEngine *engine ) { Q_UNUSED( context ) const QScriptValue biasFactoryObject = engine->newQObject( new ScriptableBiasFactory( engine ) , QScriptEngine::ScriptOwnership , QScriptEngine::ExcludeSuperClassContents | QScriptEngine::ExcludeChildObjects ); return biasFactoryObject; } QScriptValue ScriptableBiasFactory::groupBiasCtor( QScriptContext *context, QScriptEngine *engine ) { Q_UNUSED( context ) ScriptableBiasFactory *factory = new ScriptableBiasFactory( engine, true ); QScriptValue biasFactoryObject = engine->newQObject( factory , QScriptEngine::ScriptOwnership , QScriptEngine::ExcludeSuperClassContents | QScriptEngine::ExcludeChildObjects ); return biasFactoryObject; } ScriptableBiasFactory::ScriptableBiasFactory( QScriptEngine *engine, bool groupBias ) : QObject( engine ) , m_groupBias( groupBias ) , m_engine( engine ) , m_enabled( false ) {} ScriptableBiasFactory::~ScriptableBiasFactory() { Dynamic::BiasFactory::instance()->removeBiasFactory( this ); } Dynamic::BiasPtr ScriptableBiasFactory::createBias() { ScriptableBias *bias; //if( m_groupBias ) // return new ScriptableGroupBias( this ); //else bias = new ScriptableBias( this ); Dynamic::BiasPtr biasPtr = Dynamic::BiasPtr( bias ); QScriptValue biasObject = bias->scriptObject(); if( m_initFunction.isFunction() ) m_initFunction.call( biasObject, QScriptValueList() << biasObject ); return biasPtr; } // private QScriptEngine* ScriptableBiasFactory::engine() const { return m_engine; } void ScriptableBiasFactory::setEnabled( bool enabled ) { if( enabled ) { if( !m_enabled ) Dynamic::BiasFactory::instance()->registerNewBiasFactory( this ); } else Dynamic::BiasFactory::instance()->removeBiasFactory( this ); m_enabled = enabled; } bool ScriptableBiasFactory::enabled() const { return m_enabled; } void ScriptableBiasFactory::setName( const QString &name ) { m_name = name; } QString ScriptableBiasFactory::name() const { return m_name; } QString ScriptableBiasFactory::i18nName() const { return m_i18nName; } QString ScriptableBiasFactory::i18nDescription() const { return m_description; } QScriptValue ScriptableBiasFactory::initFunction() const { return m_initFunction; } void ScriptableBiasFactory::setInitFunction( const QScriptValue &value ) { m_initFunction = value; } void ScriptableBiasFactory::setI18nDescription( const QString &description ) { m_description = description; } void ScriptableBiasFactory::setI18nName( const QString &i18nName ) { m_i18nName = i18nName; } QScriptValue ScriptableBiasFactory::widgetFunction() const { return m_widgetFunction; } void ScriptableBiasFactory::setWidgetFunction( const QScriptValue &value ) { // throw exception? //if( !value.isFunction() ) m_widgetFunction = value; } void ScriptableBiasFactory::setFromXmlFunction( const QScriptValue &value ) { m_fromXmlFunction = value; } void ScriptableBiasFactory::setToXmlFunction( const QScriptValue &value ) { m_toXmlFunction = value; } QScriptValue ScriptableBiasFactory::fromXmlFunction() const { return m_fromXmlFunction; } QScriptValue ScriptableBiasFactory::toXmlFunction() const { return m_toXmlFunction; } QScriptValue ScriptableBiasFactory::matchingTracksFunction() const { return m_matchingTracksFunction; } void ScriptableBiasFactory::setMatchingTracksFunction( const QScriptValue &value ) { m_matchingTracksFunction = value; } void ScriptableBiasFactory::setTrackMatchesFunction( const QScriptValue &value ) { m_trackMatchesFunction = value; } QScriptValue ScriptableBiasFactory::trackMatchesFunction() const { return m_trackMatchesFunction; } void ScriptableBiasFactory::setToStringFunction( const QScriptValue &value ) { m_toStringFunction = value; } QScriptValue ScriptableBiasFactory::toStringFunction() const { return m_toStringFunction; } /********************************************************************************* // ScriptableBias **********************************************************************************/ void ScriptableBias::toXml( QXmlStreamWriter *writer ) const { if( m_scriptBias.data()->toXmlFunction().isFunction() ) m_scriptBias.data()->fromXmlFunction().call( m_biasObject, QScriptValueList() << m_engine->toScriptValue( writer ) ); else Dynamic::AbstractBias::toXml( writer ); } void ScriptableBias::fromXml( QXmlStreamReader *reader ) { if( m_scriptBias.data()->fromXmlFunction().isFunction() ) m_scriptBias.data()->fromXmlFunction().call( m_biasObject, QScriptValueList() << m_engine->toScriptValue( reader ) ); else Dynamic::AbstractBias::fromXml( reader ); } QWidget* ScriptableBias::widget( QWidget *parent ) { QWidget *widget = dynamic_cast( m_scriptBias.data()->widgetFunction().call( m_biasObject, m_scriptBias.data()->engine()->newQObject( parent ) ).toQObject() ); if( widget ) return widget; return Dynamic::AbstractBias::widget( parent ); } void ScriptableBias::invalidate() { Dynamic::AbstractBias::invalidate(); } Dynamic::TrackSet ScriptableBias::matchingTracks( const Meta::TrackList &playlist, int contextCount, int finalCount, const Dynamic::TrackCollectionPtr universe ) const { DEBUG_BLOCK if( QThread::currentThread() == QCoreApplication::instance()->thread() ) return slotMatchingTracks( playlist, contextCount, finalCount, universe ); Dynamic::TrackSet retVal; Q_ASSERT( QMetaObject::invokeMethod( const_cast( this ), "slotMatchingTracks", Qt::BlockingQueuedConnection, Q_RETURN_ARG( Dynamic::TrackSet, retVal), Q_ARG( Meta::TrackList, playlist ), Q_ARG( int, contextCount ), Q_ARG( int, finalCount ), Q_ARG( Dynamic::TrackCollectionPtr, universe ) ) ); debug() << "Returning trackSet, trackCount " << retVal.trackCount() << ", isOutstanding " << retVal.isOutstanding(); return retVal; } Dynamic::TrackSet ScriptableBias::slotMatchingTracks( const Meta::TrackList &playlist, int contextCount, int finalCount, const Dynamic::TrackCollectionPtr universe ) const { Q_ASSERT( QThread::currentThread() == QCoreApplication::instance()->thread() ); if( m_scriptBias.data()->matchingTracksFunction().isFunction() ) { QScriptValue trackSetVal = m_scriptBias.data()->matchingTracksFunction().call( m_biasObject, QScriptValueList() << m_engine->toScriptValue( playlist ) << contextCount << finalCount << m_engine->toScriptValue( universe->uids() ) ); TrackSetExporter *trackSetExporter = dynamic_cast( trackSetVal.toQObject() ); if( trackSetExporter ) return Dynamic::TrackSet( *trackSetExporter ); } debug() << "Invalid trackSet received"; return Dynamic::TrackSet( universe, false ); } QString ScriptableBias::name() const { QString name; if( m_scriptBias ) name = m_scriptBias.data()->name(); return name.isEmpty() ? Dynamic::AbstractBias::name() : name; } void ScriptableBias::ready( const Dynamic::TrackSet &trackSet ) { debug() << "Received trackset, count: " << trackSet.trackCount() << "Is outstanding:" << trackSet.isOutstanding(); emit resultReady( trackSet ); } void ScriptableBias::paintOperator( QPainter *painter, const QRect &rect, Dynamic::AbstractBias *bias ) { Dynamic::AbstractBias::paintOperator( painter, rect, bias ); } void ScriptableBias::replace( Dynamic::BiasPtr newBias ) { Dynamic::AbstractBias::replace( newBias ); } QString ScriptableBias::toString() const { return m_scriptBias.data()->toStringFunction().call( m_biasObject ).toString(); } bool ScriptableBias::trackMatches( int position, const Meta::TrackList& playlist, int contextCount ) const { if( m_scriptBias.data()->trackMatchesFunction().isFunction() ) return m_scriptBias.data()->trackMatchesFunction().call( m_biasObject, QScriptValueList() << position << m_engine->toScriptValue( playlist ) << contextCount ).toBool(); return true; } ScriptableBias::ScriptableBias( ScriptableBiasFactory *biasProto ) : m_scriptBias( biasProto ) , m_engine( biasProto->engine() ) { m_biasObject = m_engine->newQObject( this, QScriptEngine::QtOwnership, QScriptEngine::ExcludeDeleteLater ); connect( m_engine, SIGNAL(destroyed(QObject*)), SLOT(removeBias()) ); } ScriptableBias::~ScriptableBias() {} void ScriptableBias::removeBias() { replace( Dynamic::BiasPtr( new Dynamic::ReplacementBias( name() ) ) ); } ///////////////////////////////////////////////////////////////////////////////////////// // TrackSetExporter ///////////////////////////////////////////////////////////////////////////////////////// void TrackSetExporter::init( QScriptEngine *engine ) { qScriptRegisterMetaType( engine, toScriptValue, fromScriptValue ); engine->globalObject().setProperty( "TrackSet", engine->newFunction( trackSetConstructor ), QScriptValue:: Undeletable | QScriptValue::ReadOnly ); } void TrackSetExporter::fromScriptValue( const QScriptValue &obj, Dynamic::TrackSet &trackSet ) { DEBUG_BLOCK TrackSetExporter *trackSetProto = dynamic_cast( obj.toQObject() ); if( !trackSetProto ) trackSet = Dynamic::TrackSet( Dynamic::TrackCollectionPtr( new Dynamic::TrackCollection( QStringList() ) ), false ); else trackSet = *trackSetProto; } QScriptValue TrackSetExporter::toScriptValue( QScriptEngine *engine, const Dynamic::TrackSet &trackSet ) { DEBUG_BLOCK TrackSetExporter *trackProto = new TrackSetExporter( trackSet ); QScriptValue val = engine->newQObject( trackProto, QScriptEngine::ScriptOwnership, QScriptEngine::ExcludeSuperClassContents ); return val; } bool TrackSetExporter::containsUid( const QString &uid ) const { return Dynamic::TrackSet::contains( uid ); } QScriptValue TrackSetExporter::trackSetConstructor( QScriptContext *context, QScriptEngine *engine ) { DEBUG_BLOCK // if( !context->isCalledAsConstructor() ) throw exception? Dynamic::TrackSet trackSet; bool invalid = false; switch( context->argumentCount() ) { case 0: break; case 1: { TrackSetExporter *trackSetPrototype = dynamic_cast( context->argument( 0 ).toQObject() ); if( trackSetPrototype ) trackSet = Dynamic::TrackSet( *trackSetPrototype ); else invalid = true; break; } case 2: if( context->argument( 1 ).isBool() ) { bool isFull = context->argument( 1 ).toBool(); QScriptValue arg0 = context->argument( 0 ); QStringList uidList; Meta::TrackList trackList; if( arg0.toVariant().canConvert() ) { uidList = arg0.toVariant().toStringList(); Q_ASSERT( !arg0.toVariant().canConvert() ); trackSet = Dynamic::TrackSet( Dynamic::TrackCollectionPtr( new Dynamic::TrackCollection( uidList ) ), isFull ); } else if( arg0.toVariant().canConvert() ) { debug() << "In Meta::Tracklist TrackSet ctor"; trackList = qscriptvalue_cast( arg0 ); foreach( const Meta::TrackPtr &track, trackList ) { if( track ) uidList << track->uidUrl(); } trackSet = Dynamic::TrackSet( Dynamic::TrackCollectionPtr( new Dynamic::TrackCollection( uidList ) ), isFull ); } else invalid = true; - break; } + else + invalid = true; + break; default: invalid = true; } if( invalid ) { context->throwError( QScriptContext::SyntaxError, "Invalid arguments for TrackSet!" ); return engine->undefinedValue(); } const QScriptValue trackSetObject = engine->newQObject( new TrackSetExporter( trackSet ) , QScriptEngine::ScriptOwnership , QScriptEngine::ExcludeSuperClassContents ); return trackSetObject; } void TrackSetExporter::reset( bool value ) { Dynamic::TrackSet::reset( value ); } void TrackSetExporter::intersectTrackSet( const Dynamic::TrackSet &trackSet) { Dynamic::TrackSet::intersect( trackSet ); } void TrackSetExporter::intersectUids( const QStringList &uids ) { Dynamic::TrackSet::intersect( uids ); } void TrackSetExporter::subtractTrack( const Meta::TrackPtr &track ) { Dynamic::TrackSet::subtract( track ); } void TrackSetExporter::subtractTrackSet( const Dynamic::TrackSet &trackSet ) { Dynamic::TrackSet::subtract( trackSet ); } void TrackSetExporter::subtractUids( const QStringList &uids ) { Dynamic::TrackSet::subtract( uids ); } void TrackSetExporter::uniteTrack( const Meta::TrackPtr &track ) { Dynamic::TrackSet::unite( track ); } void TrackSetExporter::uniteTrackSet( const Dynamic::TrackSet &trackSet ) { Dynamic::TrackSet::unite( trackSet ); } void TrackSetExporter::uniteUids( const QStringList &uids ) { Dynamic::TrackSet::unite( uids ); } Meta::TrackPtr TrackSetExporter::getRandomTrack() const { return CollectionManager::instance()->trackForUrl( KUrl( Dynamic::TrackSet::getRandomTrack() ) ); } bool TrackSetExporter::containsTrack( const Meta::TrackPtr track ) const { return Dynamic::TrackSet::contains( track ); } // private TrackSetExporter::TrackSetExporter( const Dynamic::TrackSet &trackSet ) : QObject( 0 ) , TrackSet( trackSet ) {} diff --git a/src/services/lastfm/LastFmServiceConfig.cpp b/src/services/lastfm/LastFmServiceConfig.cpp index 9912b22390..ca109b8195 100644 --- a/src/services/lastfm/LastFmServiceConfig.cpp +++ b/src/services/lastfm/LastFmServiceConfig.cpp @@ -1,285 +1,285 @@ /**************************************************************************************** * Copyright (c) 2007 Shane King * * Copyright (c) 2009 Leo Franchi * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "lastfm" #include "LastFmServiceConfig.h" #include "App.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include #include #include #include QWeakPointer LastFmServiceConfig::s_instance; LastFmServiceConfigPtr LastFmServiceConfig::instance() { Q_ASSERT( QThread::currentThread() == QCoreApplication::instance()->thread() ); LastFmServiceConfigPtr strongRef = s_instance.toStrongRef(); if( strongRef ) return strongRef; LastFmServiceConfigPtr newStrongRef( new LastFmServiceConfig() ); s_instance = newStrongRef; return newStrongRef; } LastFmServiceConfig::LastFmServiceConfig() : m_askDiag( 0 ) , m_wallet( 0 ) { DEBUG_BLOCK KConfigGroup config = KGlobal::config()->group( configSectionName() ); m_sessionKey = config.readEntry( "sessionKey", QString() ); m_scrobble = config.readEntry( "scrobble", defaultScrobble() ); m_fetchSimilar = config.readEntry( "fetchSimilar", defaultFetchSimilar() ); m_scrobbleComposer = config.readEntry( "scrobbleComposer", defaultScrobbleComposer() ); m_useFancyRatingTags = config.readEntry( "useFancyRatingTags", defaultUseFancyRatingTags() ); m_announceCorrections = config.readEntry( "announceCorrections", defaultAnnounceCorrections() ); m_filterByLabel = config.readEntry( "filterByLabel", defaultFilterByLabel() ); m_filteredLabel = config.readEntry( "filteredLabel", defaultFilteredLabel() ); if( config.hasKey( "kWalletUsage" ) ) m_kWalletUsage = KWalletUsage( config.readEntry( "kWalletUsage", int( NoPasswordEnteredYet ) ) ); else { // migrate from the old config that used "ignoreWallet" key set to yes/no if( config.readEntry( "ignoreWallet", "" ) == "yes" ) m_kWalletUsage = PasswordInAscii; else if( config.hasKey( "scrobble" ) ) // assume password was saved in KWallet if the config was once written m_kWalletUsage = PasswodInKWallet; else m_kWalletUsage = NoPasswordEnteredYet; // config not yet saved, assume unused } switch( m_kWalletUsage ) { case NoPasswordEnteredYet: break; case PasswodInKWallet: openWalletToRead(); break; case PasswordInAscii: m_username = config.readEntry( "username", QString() ); m_password = config.readEntry( "password", QString() ); break; } } LastFmServiceConfig::~LastFmServiceConfig() { DEBUG_BLOCK if( m_askDiag ) m_askDiag->deleteLater(); if( m_wallet ) m_wallet->deleteLater(); } void LastFmServiceConfig::save() { KConfigGroup config = KGlobal::config()->group( configSectionName() ); // if username and password is empty, reset to NoPasswordEnteredYet; this enables // going from PasswordInAscii to PasswodInKWallet if( m_username.isEmpty() && m_password.isEmpty() ) { m_kWalletUsage = NoPasswordEnteredYet; config.deleteEntry( "username" ); // prevent possible stray credentials config.deleteEntry( "password" ); } config.writeEntry( "sessionKey", m_sessionKey ); config.writeEntry( "scrobble", m_scrobble ); config.writeEntry( "fetchSimilar", m_fetchSimilar ); config.writeEntry( "scrobbleComposer", m_scrobbleComposer ); config.writeEntry( "useFancyRatingTags", m_useFancyRatingTags ); config.writeEntry( "announceCorrections", m_announceCorrections ); config.writeEntry( "kWalletUsage", int( m_kWalletUsage ) ); config.writeEntry( "filterByLabel", m_filterByLabel ); config.writeEntry( "filteredLabel", m_filteredLabel ); config.deleteEntry( "ignoreWallet" ); // remove old settings switch( m_kWalletUsage ) { case NoPasswordEnteredYet: if( m_username.isEmpty() && m_password.isEmpty() ) break; // stay in this state - // otherwise + /* Falls through. */ case PasswodInKWallet: openWalletToWrite(); config.deleteEntry( "username" ); // prevent possible stray credentials config.deleteEntry( "password" ); break; case PasswordInAscii: config.writeEntry( "username", m_username ); config.writeEntry( "password", m_password ); break; } config.sync(); emit updated(); } void LastFmServiceConfig::openWalletToRead() { if( m_wallet && m_wallet->isOpen() ) { slotWalletOpenedToRead( true ); return; } if( m_wallet ) disconnect( m_wallet, 0, this, 0 ); else { openWalletAsync(); if( !m_wallet ) // can happen, see bug 322964 { slotWalletOpenedToRead( false ); return; } } connect( m_wallet, SIGNAL(walletOpened(bool)), SLOT(slotWalletOpenedToRead(bool)) ); } void LastFmServiceConfig::openWalletToWrite() { if( m_wallet && m_wallet->isOpen() ) { slotWalletOpenedToWrite( true ); return; } if( m_wallet ) disconnect( m_wallet, 0, this, 0 ); else { openWalletAsync(); if( !m_wallet ) // can happen, see bug 322964 { slotWalletOpenedToWrite( false ); return; } } connect( m_wallet, SIGNAL(walletOpened(bool)), SLOT(slotWalletOpenedToWrite(bool)) ); } void LastFmServiceConfig::openWalletAsync() { Q_ASSERT( !m_wallet ); using namespace KWallet; m_wallet = Wallet::openWallet( Wallet::NetworkWallet(), 0, Wallet::Asynchronous ); } void LastFmServiceConfig::prepareOpenedWallet() { if( !m_wallet->hasFolder( "Amarok" ) ) m_wallet->createFolder( "Amarok" ); m_wallet->setFolder( "Amarok" ); } void LastFmServiceConfig::slotWalletOpenedToRead( bool success ) { if( !success ) { warning() << __PRETTY_FUNCTION__ << "failed to open wallet"; QString message = i18n( "Failed to open KDE Wallet to read Last.fm credentials" ); Amarok::Components::logger()->longMessage( message, Amarok::Logger::Warning ); if( m_wallet ) m_wallet->deleteLater(); // no point in having invalid wallet around m_wallet = 0; return; } Q_ASSERT( m_wallet ); prepareOpenedWallet(); if( m_wallet->readPassword( "lastfm_password", m_password ) > 0 ) warning() << "Failed to read lastfm password from kwallet"; QByteArray rawUsername; if( m_wallet->readEntry( "lastfm_username", rawUsername ) > 0 ) warning() << "Failed to read last.fm username from kwallet"; else m_username = QString::fromUtf8( rawUsername ); emit updated(); } void LastFmServiceConfig::slotWalletOpenedToWrite( bool success ) { if( !success ) { askAboutMissingKWallet(); if( m_wallet ) m_wallet->deleteLater(); // no point in having invalid wallet around m_wallet = 0; return; } Q_ASSERT( m_wallet ); prepareOpenedWallet(); if( m_wallet->writePassword( "lastfm_password", m_password ) > 0 ) warning() << "Failed to save last.fm password to kwallet"; if( m_wallet->writeEntry( "lastfm_username", m_username.toUtf8() ) > 0 ) warning() << "Failed to save last.fm username to kwallet"; m_kWalletUsage = PasswodInKWallet; KConfigGroup config = KGlobal::config()->group( configSectionName() ); config.writeEntry( "kWalletUsage", int( m_kWalletUsage ) ); config.sync(); } void LastFmServiceConfig::askAboutMissingKWallet() { if ( !m_askDiag ) { m_askDiag = new KDialog( 0 ); m_askDiag->setCaption( i18n( "Last.fm credentials" ) ); m_askDiag->setMainWidget( new QLabel( i18n( "No running KWallet found. Would you like Amarok to save your Last.fm credentials in plaintext?" ) ) ); m_askDiag->setButtons( KDialog::Yes | KDialog::No ); connect( m_askDiag, SIGNAL(yesClicked()), this, SLOT(slotStoreCredentialsInAscii()) ); // maybe connect SIGNAL(noClicked()) to a message informing the user the password will // be forgotten on Amarok restart } m_askDiag->show(); } void LastFmServiceConfig::slotStoreCredentialsInAscii() //SLOT { DEBUG_BLOCK m_kWalletUsage = PasswordInAscii; save(); } diff --git a/src/services/lastfm/LastFmTreeModel.cpp b/src/services/lastfm/LastFmTreeModel.cpp index 620e9729cb..31cb7beacf 100644 --- a/src/services/lastfm/LastFmTreeModel.cpp +++ b/src/services/lastfm/LastFmTreeModel.cpp @@ -1,711 +1,710 @@ /**************************************************************************************** * Copyright (c) 2008 Casey Link * * Copyright (c) 2009 Nikolaj Hald Nielsen * * Copyright (c) 2009 Mark Kretschmann * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #define DEBUG_PREFIX "LastFmTreeModel" #include "core/support/Debug.h" #include "LastFmTreeModel.h" #include "AvatarDownloader.h" #include "core-impl/collections/support/CollectionManager.h" #include "AmarokMimeData.h" #include #include #include #include using namespace LastFm; LastFmTreeModel::LastFmTreeModel( QObject *parent ) : QAbstractItemModel( parent ) { m_rootItem = new LastFmTreeItem( LastFm::Root, "Hello" ); setupModelData( m_rootItem ); QNetworkReply *reply; reply = m_user.getNeighbours(); connect(reply, SIGNAL(finished()), this, SLOT(slotAddNeighbors()) ); reply = m_user.getFriends(); connect( reply, SIGNAL(finished()), this, SLOT(slotAddFriends()) ); reply = m_user.getTopTags(); connect( reply, SIGNAL(finished()), this, SLOT(slotAddTags()) ); reply = m_user.getTopArtists(); connect( reply, SIGNAL(finished()), this, SLOT(slotAddTopArtists()) ); } LastFmTreeModel::~LastFmTreeModel() { delete m_rootItem; } void LastFmTreeModel::slotAddNeighbors() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { QList children = lfm[ "neighbours" ].children( "user" ); int start = m_myNeighbors->childCount(); QModelIndex parent = index( m_myNeighbors->row(), 0 ); beginInsertRows( parent, start, start + children.size() ); foreach( const lastfm::XmlQuery &e, children ) { const QString name = e[ "name" ].text(); LastFmTreeItem* neighbor = new LastFmTreeItem( mapTypeToUrl(LastFm::NeighborsChild, name), LastFm::NeighborsChild, name, m_myNeighbors ); KUrl avatarUrl( e[ QLatin1String("image size=small") ].text() ); if( !avatarUrl.isEmpty() ) neighbor->setAvatarUrl( avatarUrl ); m_myNeighbors->appendChild( neighbor ); appendUserStations( neighbor, name ); } endInsertRows(); emit dataChanged( parent, parent ); } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } } void LastFmTreeModel::slotAddFriends() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { QList children = lfm[ "friends" ].children( "user" ); int start = m_myFriends->childCount(); QModelIndex parent = index( m_myFriends->row(), 0 ); beginInsertRows( parent, start, start + children.size() ); foreach( const lastfm::XmlQuery &e, children ) { const QString name = e[ "name" ].text(); LastFmTreeItem* afriend = new LastFmTreeItem( mapTypeToUrl(LastFm::FriendsChild, name), LastFm::FriendsChild, name, m_myFriends ); KUrl avatarUrl( e[ QLatin1String("image size=small") ].text() ); if( !avatarUrl.isEmpty() ) afriend->setAvatarUrl( avatarUrl ); m_myFriends->appendChild( afriend ); appendUserStations( afriend, name ); } endInsertRows(); emit dataChanged( parent, parent ); } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } } void LastFmTreeModel::slotAddTags() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); QMap listWithWeights = lastfm::Tag::list( reply ); int start = m_myTags->childCount(); QModelIndex parent = index( m_myTags->row(), 0 ); beginInsertRows( parent, start, start + listWithWeights.size() ); QMapIterator it( listWithWeights ); it.toBack(); while( it.hasPrevious() ) { it.previous(); int count = it.key(); QString text = it.value(); QString prettyText = i18nc( "%1 is Last.fm tag name, %2 is its usage count", "%1 (%2)", text, count ); LastFmTreeItem *tag = new LastFmTreeItem( mapTypeToUrl( LastFm::MyTagsChild, text ), LastFm::MyTagsChild, prettyText, m_myTags ); m_myTags->appendChild( tag ); } endInsertRows(); emit dataChanged( parent, parent ); } void LastFmTreeModel::slotAddTopArtists() { QNetworkReply *reply = qobject_cast( sender() ); if( !reply ) { debug() << __PRETTY_FUNCTION__ << "null reply!"; return; } reply->deleteLater(); QMultiMap playcountArtists; lastfm::XmlQuery lfm; if( lfm.parse( reply->readAll() ) ) { foreach( const lastfm::XmlQuery &e, lfm[ "topartists" ].children( "artist" ) ) { QString name = e[ "name" ].text(); int playcount = e[ "playcount" ].text().toInt(); playcountArtists.insert( playcount, name ); } } else { debug() << "Got exception in parsing from last.fm:" << lfm.parseError().message(); return; } int start = m_myTopArtists->childCount(); QModelIndex parent = index( m_myTopArtists->row(), 0 ); beginInsertRows( parent, start, start + playcountArtists.size() ); QMapIterator it( playcountArtists ); it.toBack(); while( it.hasPrevious() ) { it.previous(); int count = it.key(); QString text = it.value(); QString prettyText = i18ncp( "%2 is artist name, %1 is number of plays", "%2 (%1 play)", "%2 (%1 plays)", count, text ); LastFmTreeItem *artist = new LastFmTreeItem( mapTypeToUrl( LastFm::ArtistsChild, text ), LastFm::ArtistsChild, prettyText, m_myTopArtists ); m_myTopArtists->appendChild( artist ); } endInsertRows(); emit dataChanged( parent, parent ); } void LastFmTreeModel::appendUserStations( LastFmTreeItem* item, const QString &user ) { // no need to call begin/endInsertRows() or dataChanged(), we're being called inside // beginInsertRows(). LastFmTreeItem* personal = new LastFmTreeItem( mapTypeToUrl( LastFm::UserChildPersonal, user ), LastFm::UserChildPersonal, i18n( "Personal Radio" ), item ); LastFmTreeItem* neigh = new LastFmTreeItem( mapTypeToUrl( LastFm::UserChildNeighborhood, user ), LastFm::UserChildNeighborhood, i18n( "Neighborhood" ), item ); item->appendChild( personal ); item->appendChild( neigh ); } void LastFmTreeModel::prepareAvatar( QPixmap &avatar, int size ) { // This code is here to stop Qt from crashing on certain weirdly shaped avatars. // We had a case were an avatar got a height of 1px after scaling and it would // crash in the rendering code. This here just fills in the background with // transparency first. if( avatar.width() < size || avatar.height() < size ) { QImage finalAvatar( size, size, QImage::Format_ARGB32 ); finalAvatar.fill( 0 ); QPainter p( &finalAvatar ); QRect r; if( avatar.width() < size ) r = QRect( ( size - avatar.width() ) / 2, 0, avatar.width(), avatar.height() ); else r = QRect( 0, ( size - avatar.height() ) / 2, avatar.width(), avatar.height() ); p.drawPixmap( r, avatar ); p.end(); avatar = QPixmap::fromImage( finalAvatar ); } } void LastFmTreeModel::onAvatarDownloaded( const QString &username, QPixmap avatar ) { sender()->deleteLater(); if( avatar.isNull() || avatar.height() <= 0 || avatar.width() <= 0 ) return; if( username == m_user.name() ) return; int m = avatarSize(); avatar = avatar.scaled( m, m, Qt::KeepAspectRatio, Qt::SmoothTransformation ); prepareAvatar( avatar, m ); m_avatars.insert( username, avatar ); // these 2 categories have a chance to be updated: QList categories; categories << m_myFriends << m_myNeighbors; // now go through all children of the categories and notify view as appropriate foreach( LastFmTreeItem *category, categories ) { QModelIndex parentIdx = index( category->row(), 0 ); for( int i = 0; i < category->childCount(); i++ ) { LastFmTreeItem *item = category->child( i ); if( !item ) continue; if( item->data() == username ) { QModelIndex idx = index( i, 0, parentIdx ); emit dataChanged( idx, idx ); break; // no user is twice in a single category } } } } QIcon LastFmTreeModel::avatar( const QString &username, const KUrl &avatarUrl ) const { KIcon defaultIcon( "filename-artist-amarok" ); if( username.isEmpty() ) return defaultIcon; if( m_avatars.contains(username) ) return m_avatars.value( username ); if( !avatarUrl.isValid() ) return defaultIcon; // insert placeholder so that we don't request the save avatar twice; const_cast( this )->m_avatars.insert( username, defaultIcon ); AvatarDownloader* downloader = new AvatarDownloader(); downloader->downloadAvatar( username, avatarUrl ); connect( downloader, SIGNAL(avatarDownloaded(QString,QPixmap)), SLOT(onAvatarDownloaded(QString,QPixmap)) ); return defaultIcon; } int LastFmTreeModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) return 1; } int LastFmTreeModel::avatarSize() { return 32; } QVariant LastFmTreeModel::data( const QModelIndex &index, int role ) const { if( !index.isValid() ) return QVariant(); LastFmTreeItem *i = static_cast( index.internalPointer() ); if( role == Qt::DisplayRole ) switch( i->type() ) { case MyRecommendations: return i18n( "My Recommendations" ); case PersonalRadio: return i18n( "My Radio Station" ); case MixRadio: return i18n( "My Mix Radio" ); case NeighborhoodRadio: return i18n( "My Neighborhood" ); // case RecentlyPlayed: return tr("Recently Played"); // case RecentlyLoved: return tr("Recently Loved"); // case RecentlyBanned: return tr("Recently Banned"); case TopArtists: return i18n( "My Top Artists" ); case MyTags: return i18n( "My Tags" ); case Friends: return i18n( "Friends" ); case Neighbors: return i18n( "Neighbors" ); // case History: return tr("History"); // case RecentlyPlayedTrack: return m_played.value( index.row() ); // case RecentlyLovedTrack: return m_loved.value( index.row() ); // case RecentlyBannedTrack: return m_banned.value( index.row() ); // case MyTagsChild: return m_tags.value( index.row() ); case FriendsChild: case ArtistsChild: case NeighborsChild: case UserChildPersonal: case UserChildNeighborhood: case MyTagsChild: return i->data(); default: break; } if( role == Qt::DecorationRole ) switch( i->type() ) { case MyRecommendations: return KIcon( "lastfm-recommended-radio-amarok" ); case TopArtists: case PersonalRadio: return KIcon( "lastfm-personal-radio-amarok" ); case MixRadio: return KIcon( "lastfm-mix-radio-amarok" ); case NeighborhoodRadio: return KIcon( "lastfm-neighbour-radio-amarok" ); // case RecentlyPlayed: return KIcon( "lastfm-recent-tracks-amarok" ); // case RecentlyLoved: return KIcon( "lastfm-recently-loved-amarok" ); // case RecentlyBanned: return KIcon( "lastfm-recently-banned-amarok" ); case MyTags: return KIcon( "lastfm-my-tags-amarok" ); case Friends: return KIcon( "lastfm-my-friends-amarok" ); case Neighbors: return KIcon( "lastfm-my-neighbours-amarok" ); case RecentlyPlayedTrack: //FALL THROUGH case RecentlyLovedTrack: //FALL THROUGH case RecentlyBannedTrack: return KIcon( "icon_track" ); case MyTagsChild: return KIcon( "lastfm-tag-amarok" ); case FriendsChild: return avatar( i->data().toString(), i->avatarUrl() ); case UserChildPersonal: return KIcon( "lastfm-personal-radio-amarok" ); case UserChildNeighborhood: return KIcon( "lastfm-neighbour-radio-amarok" ); case NeighborsChild: return avatar( i->data().toString(), i->avatarUrl() ); case HistoryStation: return KIcon( "icon_radio" ); default: break; } - if( role == LastFm::TrackRole ) + if( role == LastFm::TrackRole ) + switch( i->type() ) { - switch( i->type() ) - { - case LastFm::MyRecommendations: - case LastFm::PersonalRadio: - case LastFm::MixRadio: - case LastFm::NeighborhoodRadio: - case LastFm::FriendsChild: - case LastFm::NeighborsChild: - case LastFm::MyTagsChild: - case LastFm::ArtistsChild: - case LastFm::UserChildPersonal: - case LastFm::UserChildNeighborhood: - return QVariant::fromValue( i->track() ); - default: - break; - } + case LastFm::MyRecommendations: + case LastFm::PersonalRadio: + case LastFm::MixRadio: + case LastFm::NeighborhoodRadio: + case LastFm::FriendsChild: + case LastFm::NeighborsChild: + case LastFm::MyTagsChild: + case LastFm::ArtistsChild: + case LastFm::UserChildPersonal: + case LastFm::UserChildNeighborhood: + return QVariant::fromValue( i->track() ); + default: + break; } - if( role == LastFm::TypeRole ) - return i->type(); + + if( role == LastFm::TypeRole ) + return i->type(); return QVariant(); } Qt::ItemFlags LastFmTreeModel::flags( const QModelIndex &index ) const { if( !index.isValid() ) return 0; Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsDropEnabled; LastFmTreeItem *i = static_cast( index.internalPointer() ); switch( i->type() ) { case MyRecommendations: case PersonalRadio: case MixRadio: case NeighborhoodRadio: case RecentlyPlayedTrack: case RecentlyLovedTrack: case RecentlyBannedTrack: case MyTagsChild: case FriendsChild: case ArtistsChild: case NeighborsChild: case HistoryStation: case UserChildPersonal: case UserChildNeighborhood: flags |= Qt::ItemIsSelectable; break; default: break; } switch( i->type() ) { case UserChildPersonal: case UserChildNeighborhood: case MyTagsChild: case ArtistsChild: case MyRecommendations: case PersonalRadio: case MixRadio: case NeighborhoodRadio: flags |= Qt::ItemIsDragEnabled; default: break; } return flags; } QModelIndex LastFmTreeModel::index( int row, int column, const QModelIndex &parent ) const { if( !hasIndex( row, column, parent ) ) return QModelIndex(); LastFmTreeItem *parentItem; if( !parent.isValid() ) parentItem = m_rootItem; else parentItem = static_cast( parent.internalPointer() ); LastFmTreeItem *childItem = parentItem->child( row ); if( childItem ) return createIndex( row, column, childItem ); else return QModelIndex(); } QModelIndex LastFmTreeModel::parent( const QModelIndex &index ) const { if( !index.isValid() ) return QModelIndex(); LastFmTreeItem *childItem = static_cast( index.internalPointer() ); LastFmTreeItem *parentItem = childItem->parent(); if( parentItem == m_rootItem ) return QModelIndex(); return createIndex( parentItem->row(), 0, parentItem ); } int LastFmTreeModel::rowCount( const QModelIndex &parent ) const { LastFmTreeItem *parentItem; if( parent.column() > 0 ) return 0; if( !parent.isValid() ) parentItem = m_rootItem; else parentItem = static_cast( parent.internalPointer() ); return parentItem->childCount(); } void LastFmTreeModel::setupModelData( LastFmTreeItem *parent ) { // no need to call beginInsertRows() here, this is only called from constructor parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MyRecommendations ), LastFm::MyRecommendations, parent ) ); parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::PersonalRadio ), LastFm::PersonalRadio, parent ) ); parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::MixRadio ), LastFm::MixRadio, parent ) ); parent->appendChild( new LastFmTreeItem( mapTypeToUrl( LastFm::NeighborhoodRadio ), LastFm::NeighborhoodRadio, parent ) ); m_myTopArtists = new LastFmTreeItem( LastFm::TopArtists, parent ); parent->appendChild( m_myTopArtists ); m_myTags = new LastFmTreeItem( LastFm::MyTags, parent ); parent->appendChild( m_myTags ); m_myFriends = new LastFmTreeItem( LastFm::Friends, parent ); parent->appendChild( m_myFriends ); m_myNeighbors = new LastFmTreeItem( LastFm::Neighbors, parent ); parent->appendChild( m_myNeighbors ); } QString LastFmTreeModel::mapTypeToUrl( LastFm::Type type, const QString &key ) { QString const encoded_username = KUrl::toPercentEncoding( m_user.name() ); switch( type ) { case MyRecommendations: return "lastfm://user/" + encoded_username + "/recommended"; case PersonalRadio: return "lastfm://user/" + encoded_username + "/personal"; case MixRadio: return "lastfm://user/" + encoded_username + "/mix"; case NeighborhoodRadio: return "lastfm://user/" + encoded_username + "/neighbours"; case MyTagsChild: return "lastfm://usertags/" + encoded_username + "/" + KUrl::toPercentEncoding( key ); case FriendsChild: return "lastfm://user/" + KUrl::toPercentEncoding( key ) + "/personal"; case ArtistsChild: return "lastfm://artist/" + KUrl::toPercentEncoding( key ) + "/similarartists"; case NeighborsChild: return "lastfm://user/" + KUrl::toPercentEncoding( key ) + "/personal"; case UserChildPersonal: return "lastfm://user/" + KUrl::toPercentEncoding( key ) + "/personal"; case UserChildNeighborhood: return "lastfm://user/" + KUrl::toPercentEncoding( key ) + "/neighbours"; default: return ""; } } LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ), itemData( data ) { } LastFmTreeItem::LastFmTreeItem( const LastFm::Type &type, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ) { } LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ), mUrl( url ) { } LastFmTreeItem::LastFmTreeItem( const QString &url, const LastFm::Type &type, const QVariant &data, LastFmTreeItem *parent ) : mType( type ), parentItem( parent ), itemData( data ), mUrl( url ) { } LastFmTreeItem::~LastFmTreeItem() { qDeleteAll( childItems ); } void LastFmTreeItem::appendChild( LastFmTreeItem *item ) { childItems.append( item ); } LastFmTreeItem * LastFmTreeItem::child( int row ) { return childItems.value( row ); } int LastFmTreeItem::childCount() const { return childItems.count(); } QVariant LastFmTreeItem::data() const { return itemData; } Meta::TrackPtr LastFmTreeItem::track() const { Meta::TrackPtr track; if( mUrl.isEmpty() ) return track; KUrl url( mUrl ); track = CollectionManager::instance()->trackForUrl( url ); return track; } LastFmTreeItem *LastFmTreeItem::parent() { return parentItem; } int LastFmTreeItem::row() const { if( parentItem ) return parentItem->childItems.indexOf( const_cast( this ) ); return 0; } QMimeData* LastFmTreeModel::mimeData( const QModelIndexList &indices ) const { debug() << "LASTFM drag items : " << indices.size(); Meta::TrackList list; foreach( const QModelIndex &item, indices ) { Meta::TrackPtr track = data( item, LastFm::TrackRole ).value< Meta::TrackPtr >(); if( track ) list << track; } qStableSort( list.begin(), list.end(), Meta::Track::lessThan ); AmarokMimeData *mimeData = new AmarokMimeData(); mimeData->setTracks( list ); return mimeData; } diff --git a/src/services/opmldirectory/OpmlDirectoryModel.cpp b/src/services/opmldirectory/OpmlDirectoryModel.cpp index e4c5d9f131..79b733a457 100644 --- a/src/services/opmldirectory/OpmlDirectoryModel.cpp +++ b/src/services/opmldirectory/OpmlDirectoryModel.cpp @@ -1,514 +1,513 @@ /**************************************************************************************** * Copyright (c) 2010 Bart Cerneels . * ****************************************************************************************/ #include "OpmlDirectoryModel.h" #include "core/support/Amarok.h" #include "MainWindow.h" #include "OpmlParser.h" #include "OpmlWriter.h" #include "core/support/Debug.h" //included to access defaultPodcasts() #include "playlistmanager/PlaylistManager.h" #include "core/podcasts/PodcastProvider.h" #include "ui_AddOpmlWidget.h" #include #include #include OpmlDirectoryModel::OpmlDirectoryModel( KUrl outlineUrl, QObject *parent ) : QAbstractItemModel( parent ) , m_rootOpmlUrl( outlineUrl ) { //fetchMore will be called by the view m_addOpmlAction = new QAction( KIcon( "list-add" ), i18n( "Add OPML" ), this ); connect( m_addOpmlAction, SIGNAL(triggered()), SLOT(slotAddOpmlAction()) ); m_addFolderAction = new QAction( KIcon( "folder-add" ), i18n( "Add Folder"), this ); connect( m_addFolderAction, SIGNAL(triggered()), SLOT(slotAddFolderAction()) ); } OpmlDirectoryModel::~OpmlDirectoryModel() { } QModelIndex OpmlDirectoryModel::index( int row, int column, const QModelIndex &parent ) const { if( !parent.isValid() ) { if( m_rootOutlines.isEmpty() || m_rootOutlines.count() <= row ) return QModelIndex(); else return createIndex( row, column, m_rootOutlines[row] ); } OpmlOutline *parentOutline = static_cast( parent.internalPointer() ); if( !parentOutline ) return QModelIndex(); if( !parentOutline->hasChildren() || parentOutline->children().count() <= row ) return QModelIndex(); return createIndex( row, column, parentOutline->children()[row] ); } Qt::ItemFlags OpmlDirectoryModel::flags( const QModelIndex &idx ) const { if( !idx.isValid() ) return Qt::ItemIsDropEnabled; OpmlOutline *outline = static_cast( idx.internalPointer() ); if( outline && !outline->attributes().contains( "type" ) ) //probably a folder return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; return QAbstractItemModel::flags( idx ); } QModelIndex OpmlDirectoryModel::parent( const QModelIndex &idx ) const { if( !idx.isValid() ) return QModelIndex(); debug() << idx; OpmlOutline *outline = static_cast( idx.internalPointer() ); if( outline->isRootItem() ) return QModelIndex(); OpmlOutline *parentOutline = outline->parent(); int childIndex; if( parentOutline->isRootItem() ) childIndex = m_rootOutlines.indexOf( parentOutline ); else childIndex = parentOutline->parent()->children().indexOf( parentOutline ); return createIndex( childIndex, 0, parentOutline ); } int OpmlDirectoryModel::rowCount( const QModelIndex &parent ) const { if( !parent.isValid() ) return m_rootOutlines.count(); OpmlOutline *outline = static_cast( parent.internalPointer() ); if( !outline || !outline->hasChildren() ) return 0; else return outline->children().count(); } bool OpmlDirectoryModel::hasChildren( const QModelIndex &parent ) const { debug() << parent; if( !parent.isValid() ) return !m_rootOutlines.isEmpty(); OpmlOutline *outline = static_cast( parent.internalPointer() ); if( !outline ) return false; if( outline->hasChildren() ) return true; return outline->attributes().value( "type" ) == "include"; } int OpmlDirectoryModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED(parent) return 1; } QVariant OpmlDirectoryModel::data( const QModelIndex &idx, int role ) const { if( !idx.isValid() ) { if( role == ActionRole ) { QList actions; actions << m_addOpmlAction << m_addFolderAction; return QVariant::fromValue( actions ); } return QVariant(); } OpmlOutline *outline = static_cast( idx.internalPointer() ); if( !outline ) return QVariant(); switch( role ) { case Qt::DisplayRole: return outline->attributes()["text"]; case Qt::DecorationRole: return m_imageMap.contains( outline ) ? m_imageMap.value( outline ) : QVariant(); case ActionRole: - { if( outline->opmlNodeType() == RegularNode ) //probably a folder { //store the index the new item should get added to m_addOpmlAction->setData( QVariant::fromValue( idx ) ); m_addFolderAction->setData( QVariant::fromValue( idx ) ); return QVariant::fromValue( QActionList() << m_addOpmlAction << m_addFolderAction ); } - } + return QVariant(); default: return QVariant(); } return QVariant(); } bool OpmlDirectoryModel::setData( const QModelIndex &idx, const QVariant &value, int role ) { Q_UNUSED(role); if( !idx.isValid() ) return false; OpmlOutline *outline = static_cast( idx.internalPointer() ); if( !outline ) return false; outline->mutableAttributes()["text"] = value.toString(); saveOpml( m_rootOpmlUrl ); return true; } bool OpmlDirectoryModel::removeRows( int row, int count, const QModelIndex &parent ) { if( !parent.isValid() ) { if( m_rootOutlines.count() >= ( row + count ) ) { beginRemoveRows( parent, row, row + count - 1 ); for( int i = 0; i < count; i++ ) m_rootOutlines.removeAt( row ); endRemoveRows(); saveOpml( m_rootOpmlUrl ); return true; } return false; } OpmlOutline *outline = static_cast( parent.internalPointer() ); if( !outline ) return false; if( !outline->hasChildren() || outline->children().count() < ( row + count ) ) return false; beginRemoveRows( parent, row, row + count -1 ); for( int i = 0; i < count - 1; i++ ) outline->mutableChildren().removeAt( row ); endRemoveRows(); saveOpml( m_rootOpmlUrl ); return true; } void OpmlDirectoryModel::saveOpml( const KUrl &saveLocation ) { if( !saveLocation.isLocalFile() ) { //TODO:implement error() << "can not save OPML to remote location"; return; } QFile *opmlFile = new QFile( saveLocation.toLocalFile(), this ); if( !opmlFile->open( QIODevice::WriteOnly | QIODevice::Truncate ) ) { error() << "could not open OPML file for writing " << saveLocation.url(); return; } QMap headerData; //TODO: set header data such as date OpmlWriter *opmlWriter = new OpmlWriter( m_rootOutlines, headerData, opmlFile ); connect( opmlWriter, SIGNAL(result(int)), SLOT(slotOpmlWriterDone(int)) ); opmlWriter->run(); } void OpmlDirectoryModel::slotOpmlWriterDone( int result ) { Q_UNUSED( result ) OpmlWriter *writer = qobject_cast( QObject::sender() ); Q_ASSERT( writer ); writer->device()->close(); delete writer; } OpmlNodeType OpmlDirectoryModel::opmlNodeType( const QModelIndex &idx ) const { OpmlOutline *outline = static_cast( idx.internalPointer() ); return outline->opmlNodeType(); } void OpmlDirectoryModel::slotAddOpmlAction() { QModelIndex parentIdx = QModelIndex(); QAction *action = qobject_cast( sender() ); if( action ) { parentIdx = action->data().value(); } KDialog *dialog = new KDialog( The::mainWindow() ); dialog->setCaption( i18nc( "Heading of Add OPML dialog", "Add OPML" ) ); dialog->setButtons( KDialog::Ok | KDialog::Cancel ); QWidget *opmlAddWidget = new QWidget( dialog ); Ui::AddOpmlWidget widget; widget.setupUi( opmlAddWidget ); widget.urlEdit->setMode( KFile::File ); dialog->setMainWidget( opmlAddWidget ); if( dialog->exec() != QDialog::Accepted ) return; QString url = widget.urlEdit->url().url(); QString title = widget.titleEdit->text(); debug() << QString( "creating a new OPML outline with url = %1 and title \"%2\"." ).arg( url, title ); OpmlOutline *outline = new OpmlOutline(); outline->addAttribute( "type", "include" ); outline->addAttribute( "url", url ); if( !title.isEmpty() ) outline->addAttribute( "text", title ); //Folder icon with down-arrow emblem m_imageMap.insert( outline, KIcon( "folder", 0, QStringList( "go-down" ) ).pixmap( 24, 24 ) ); QModelIndex newIdx = addOutlineToModel( parentIdx, outline ); //TODO: force the view to expand the folder (parentIdx) so the new node is shown //if the title is missing, start parsing the OPML so we can get it from the feed if( outline->attributes().contains( "text" ) ) saveOpml( m_rootOpmlUrl ); else fetchMore( newIdx ); //saves OPML after receiving the title. delete dialog; } void OpmlDirectoryModel::slotAddFolderAction() { QModelIndex parentIdx = QModelIndex(); QAction *action = qobject_cast( sender() ); if( action ) { parentIdx = action->data().value(); } OpmlOutline *outline = new OpmlOutline(); outline->addAttribute( "text", i18n( "New Folder" ) ); m_imageMap.insert( outline, KIcon( "folder" ).pixmap( 24, 24 ) ); addOutlineToModel( parentIdx, outline ); //TODO: trigger edit of the new folder saveOpml( m_rootOpmlUrl ); } bool OpmlDirectoryModel::canFetchMore( const QModelIndex &parent ) const { debug() << parent; //already fetched or just started? if( rowCount( parent ) || m_currentFetchingMap.values().contains( parent ) ) return false; if( !parent.isValid() ) return m_rootOutlines.isEmpty(); OpmlOutline *outline = static_cast( parent.internalPointer() ); return outline && ( outline->attributes().value( "type" ) == "include" ); } void OpmlDirectoryModel::fetchMore( const QModelIndex &parent ) { debug() << parent; if( m_currentFetchingMap.values().contains( parent ) ) { error() << "trying to start second fetch job for same item"; return; } KUrl urlToFetch; if( !parent.isValid() ) { urlToFetch = m_rootOpmlUrl; } else { OpmlOutline *outline = static_cast( parent.internalPointer() ); if( !outline ) return; if( outline->attributes().value( "type" ) != "include" ) return; urlToFetch = KUrl( outline->attributes()["url"] ); } if( !urlToFetch.isValid() ) return; OpmlParser *parser = new OpmlParser( urlToFetch ); connect( parser, SIGNAL(headerDone()), SLOT(slotOpmlHeaderDone()) ); connect( parser, SIGNAL(outlineParsed(OpmlOutline*)), SLOT(slotOpmlOutlineParsed(OpmlOutline*)) ); connect( parser, SIGNAL(doneParsing()), SLOT(slotOpmlParsingDone()) ); m_currentFetchingMap.insert( parser, parent ); // ThreadWeaver::Weaver::instance()->enqueue( parser ); parser->run(); } void OpmlDirectoryModel::slotOpmlHeaderDone() { OpmlParser *parser = qobject_cast( QObject::sender() ); QModelIndex idx = m_currentFetchingMap.value( parser ); if( !idx.isValid() ) //header data of the root not required. return; OpmlOutline *outline = static_cast( idx.internalPointer() ); if( !outline->attributes().contains("text") ) { if( parser->headerData().contains( "title" ) ) outline->addAttribute( "text", parser->headerData()["title"] ); else outline->addAttribute( "text", parser->url().fileName() ); //force a view update emit dataChanged( idx, idx ); saveOpml( m_rootOpmlUrl ); } } void OpmlDirectoryModel::slotOpmlOutlineParsed( OpmlOutline *outline ) { OpmlParser *parser = qobject_cast( QObject::sender() ); QModelIndex idx = m_currentFetchingMap.value( parser ); addOutlineToModel( idx, outline ); //TODO: begin image fetch switch( outline->opmlNodeType() ) { case RegularNode: m_imageMap.insert( outline, KIcon( "folder" ).pixmap( 24, 24 ) ); break; case IncludeNode: { m_imageMap.insert( outline, KIcon( "folder", 0, QStringList( "go-down" ) ).pixmap( 24, 24 ) ); break; } case RssUrlNode: default: break; } } void OpmlDirectoryModel::slotOpmlParsingDone() { OpmlParser *parser = qobject_cast( QObject::sender() ); m_currentFetchingMap.remove( parser ); parser->deleteLater(); } void OpmlDirectoryModel::subscribe( const QModelIndexList &indexes ) const { QList outlines; foreach( const QModelIndex &idx, indexes ) outlines << static_cast( idx.internalPointer() ); foreach( const OpmlOutline *outline, outlines ) { if( !outline ) continue; KUrl url; if( outline->attributes().contains( "xmlUrl" ) ) url = KUrl( outline->attributes()["xmlUrl"] ); else if( outline->attributes().contains( "url" ) ) url = KUrl( outline->attributes()["url"] ); if( url.isEmpty() ) continue; The::playlistManager()->defaultPodcasts()->addPodcast( url ); } } QModelIndex OpmlDirectoryModel::addOutlineToModel( QModelIndex parentIdx, OpmlOutline *outline ) { int newRow = rowCount( parentIdx ); beginInsertRows( parentIdx, newRow, newRow ); //no reparenting required when the item is already parented. if( outline->isRootItem() ) { if( parentIdx.isValid() ) { OpmlOutline * parentOutline = static_cast( parentIdx.internalPointer() ); Q_ASSERT(parentOutline); outline->setParent( parentOutline ); parentOutline->addChild( outline ); parentOutline->setHasChildren( true ); } else { m_rootOutlines << outline; } } endInsertRows(); return index( newRow, 0, parentIdx ); } diff --git a/src/statsyncing/jobs/SynchronizeTracksJob.cpp b/src/statsyncing/jobs/SynchronizeTracksJob.cpp index 9ddf0c2ca0..40ee42315d 100644 --- a/src/statsyncing/jobs/SynchronizeTracksJob.cpp +++ b/src/statsyncing/jobs/SynchronizeTracksJob.cpp @@ -1,151 +1,153 @@ /**************************************************************************************** * Copyright (c) 2012 Matěj Laitl * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 2 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "SynchronizeTracksJob.h" #include "core/meta/Meta.h" #include "core/meta/Statistics.h" #include "core/support/Components.h" #include "core/support/Debug.h" #include "statsyncing/Controller.h" #include "statsyncing/TrackTuple.h" #include using namespace StatSyncing; static const int denom = 20; // emit incementProgress() signal each N tracks static const int fuzz = denom / 2; SynchronizeTracksJob::SynchronizeTracksJob( const QList &tuples, const TrackList &tracksToScrobble, const Options &options, QObject *parent ) : Job( parent ) , m_abort( false ) , m_tuples( tuples ) , m_tracksToScrobble( tracksToScrobble ) , m_updatedTracksCount( 0 ) , m_options( options ) { } void SynchronizeTracksJob::abort() { m_abort = true; } void SynchronizeTracksJob::run() { emit totalSteps( ( m_tuples.size() + fuzz ) / denom ); Controller *controller = Amarok::Components::statSyncingController(); if( controller ) { connect( this, SIGNAL(scrobble(Meta::TrackPtr,double,QDateTime)), controller, SLOT(scrobble(Meta::TrackPtr,double,QDateTime)) ); // we don't run an event loop, we must use direct connection for controller to talk to us connect( controller, SIGNAL(trackScrobbled(ScrobblingServicePtr,Meta::TrackPtr)), SLOT(slotTrackScrobbled(ScrobblingServicePtr,Meta::TrackPtr)), Qt::DirectConnection ); connect( controller, SIGNAL(scrobbleFailed(ScrobblingServicePtr,Meta::TrackPtr,int)), SLOT(slotScrobbleFailed(ScrobblingServicePtr,Meta::TrackPtr,int)), Qt::DirectConnection ); } else warning() << __PRETTY_FUNCTION__ << "StatSyncing::Controller not available!"; // first, queue tracks for scrobbling, because after syncing their recent playcount is // reset foreach( const TrackPtr &track, m_tracksToScrobble ) { Meta::TrackPtr metaTrack = track->metaTrack(); int playcount = track->recentPlayCount(); if( metaTrack && playcount > 0 ) { m_scrobbledTracks << metaTrack; emit scrobble( metaTrack, playcount, track->lastPlayed() ); } } ProviderPtrSet updatedProviders; int i = 0; foreach( const TrackTuple &tuple, m_tuples ) { if( m_abort ) break; // no point in checking for hasUpdate() here, synchronize() is witty enough const ProviderPtrSet tupleUpdatedProviders = tuple.synchronize( m_options ); updatedProviders |= tupleUpdatedProviders; m_updatedTracksCount += tupleUpdatedProviders.count(); if( ( i + fuzz ) % denom == 0 ) emit incrementProgress(); i++; } foreach( ProviderPtr provider, updatedProviders ) provider->commitTracks(); // we need to reset playCount of scrobbled tracks to reset their recent play count foreach( Meta::TrackPtr track, m_scrobbledTracks ) { Meta::StatisticsPtr statistics = track->statistics(); statistics->setPlayCount( statistics->playCount() ); } if( !m_tracksToScrobble.isEmpty() ) // wait 3 seconds so that we have chance to catch slotTrackScrobbled().. thread()->msleep( 3000 ); if( controller ) + { disconnect( controller, SIGNAL(trackScrobbled(ScrobblingServicePtr,Meta::TrackPtr)), this, 0 ); disconnect( controller, SIGNAL(scrobbleFailed(ScrobblingServicePtr,Meta::TrackPtr,int)), this, 0 ); + } emit endProgressOperation( this ); } void SynchronizeTracksJob::slotTrackScrobbled( const ScrobblingServicePtr &service, const Meta::TrackPtr &track ) { slotScrobbleFailed( service, track, ScrobblingService::NoError ); } void SynchronizeTracksJob::slotScrobbleFailed( const ScrobblingServicePtr &service, const Meta::TrackPtr &track, int error ) { // only count tracks scrobbled by us. Still chance for false-positives, though if( m_scrobbledTracks.contains( track ) ) { ScrobblingService::ScrobbleError errorEnum = ScrobblingService::ScrobbleError( error ); m_scrobbles[ service ][ errorEnum ]++; } } int SynchronizeTracksJob::updatedTracksCount() const { return m_updatedTracksCount; } QMap > SynchronizeTracksJob::scrobbles() { return m_scrobbles; } diff --git a/src/widgets/MetaQueryWidget.cpp b/src/widgets/MetaQueryWidget.cpp index 40f15d64a5..0dc5cbe687 100644 --- a/src/widgets/MetaQueryWidget.cpp +++ b/src/widgets/MetaQueryWidget.cpp @@ -1,1215 +1,1221 @@ /**************************************************************************************** * Copyright (c) 2008 Daniel Caleb Jones * * Copyright (c) 2009 Mark Kretschmann * * Copyright (c) 2010 Ralf Engels * * * * 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) version 3 or * * any later version accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see . * ****************************************************************************************/ #include "core-impl/collections/support/CollectionManager.h" #include "core/collections/MetaQueryMaker.h" #include "core/collections/QueryMaker.h" #include "widgets/MetaQueryWidget.h" #include "widgets/kdatecombo.h" #include "FileType.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Amarok; static const int maxHours = 24; TimeDistanceWidget::TimeDistanceWidget( QWidget *parent ) : QWidget( parent ) { m_timeEdit = new KIntSpinBox(this); m_timeEdit->setMinimum( 0 ); m_timeEdit->setMaximum( 600 ); m_unitSelection = new KComboBox(this); connect( m_timeEdit, SIGNAL(valueChanged(int)), this, SLOT(slotUpdateComboBoxLabels(int)) ); for (int i = 0; i < 7; ++i) { m_unitSelection->addItem( QString() ); } slotUpdateComboBoxLabels( 0 ); QHBoxLayout *hLayout = new QHBoxLayout(this); hLayout->setContentsMargins(0, 0, 0, 0); hLayout->addWidget( m_timeEdit ); hLayout->addWidget( m_unitSelection ); } qint64 TimeDistanceWidget::timeDistance() const { qint64 time = m_timeEdit->value(); switch( m_unitSelection->currentIndex() ) { case 6: time *= 365*24*60*60; // years break; case 5: time *= 30*24*60*60; // months break; case 4: time *= 7*24*60*60; // weeks break; case 3: - time *= 24; // days + time *= 24*60*60; // days + break; case 2: - time *= 60; // hours + time *= 60*60; // hours + break; case 1: - time *= 60; // minutes + time *= 60; // minutes + break; } return time; } void TimeDistanceWidget::setTimeDistance( qint64 value ) { // as we don't store the time unit we try to reconstuct it int unit = 0; if( value > 600 || !(value % 60) ) { unit = 1; value /= 60; if( value > 600 || !(value % 60) ) { unit = 2; value /= 60; if( value > 72 || !(value % 24) ) { unit = 3; value /= 24; if( !(value % 365) ) { unit = 6; value /= 365; } else if( !(value % 30) ) { unit = 5; value /= 30; } else if( !(value % 7) ) { unit = 4; value /= 7; } } } } m_unitSelection->setCurrentIndex( unit ); m_timeEdit->setValue( value ); } void TimeDistanceWidget::connectChanged( QObject *receiver, const char *slot ) { connect( m_timeEdit, SIGNAL(valueChanged(QString)), receiver, slot ); connect( m_unitSelection, SIGNAL(currentIndexChanged(int)), receiver, slot ); } void TimeDistanceWidget::slotUpdateComboBoxLabels( int value ) { m_unitSelection->setItemText(0, i18np("second", "seconds", value)); m_unitSelection->setItemText(1, i18np("minute", "minutes", value)); m_unitSelection->setItemText(2, i18np("hour", "hours", value)); m_unitSelection->setItemText(3, i18np("day", "days", value)); m_unitSelection->setItemText(4, i18np("week", "weeks", value)); m_unitSelection->setItemText(5, i18np("month", "months", value)); m_unitSelection->setItemText(6, i18np("year", "years", value)); } void MetaQueryWidget::Filter::setField( qint64 newField ) { if( m_field == newField ) return; // -- reset the value and the condition if the new filter has another type if( MetaQueryWidget::isNumeric( m_field ) != MetaQueryWidget::isNumeric( newField ) ) { value.clear(); if( MetaQueryWidget::isNumeric( newField ) ) condition = Equals; else condition = Contains; } if( !MetaQueryWidget::isDate( m_field ) && MetaQueryWidget::isDate( newField ) ) { numValue = QDateTime::currentDateTime().toTime_t(); numValue2 = QDateTime::currentDateTime().toTime_t(); } else { numValue = 0; numValue2 = 0; } if (numValue < minimumValue( newField ) || numValue > maximumValue( newField ) ) numValue = defaultValue( newField ); if (numValue2 < minimumValue( newField ) || numValue2 > maximumValue( newField ) ) numValue2 = defaultValue( newField ); m_field = newField; } qint64 MetaQueryWidget::Filter::minimumValue( quint64 field ) { switch( field ) { case Meta::valYear: return 1900; case Meta::valTrackNr: return 0; case Meta::valDiscNr: return 0; case Meta::valBpm: return 60; case Meta::valBitrate: return 60; case Meta::valSamplerate: return 8000; case Meta::valFilesize: return 0; case Meta::valScore: return 0; case Meta::valPlaycount: return 0; case Meta::valRating: return 0; case Meta::valLength: return 0; default: return 0; } } qint64 MetaQueryWidget::Filter::maximumValue( quint64 field ) { switch( field ) { case Meta::valYear: return 2300; case Meta::valTrackNr: return 100; case Meta::valDiscNr: return 10; case Meta::valBpm: return 200; case Meta::valBitrate: return 2000; case Meta::valSamplerate: return 48000; case Meta::valFilesize: return 1000; case Meta::valScore: return 100; case Meta::valPlaycount: return 1000; case Meta::valRating: return 10; case Meta::valLength: return maxHours * 60 * 60 - 1; default: return 0; } } qint64 MetaQueryWidget::Filter::defaultValue( quint64 field ) { switch( field ) { case Meta::valYear: return 1976; case Meta::valTrackNr: return 0; case Meta::valDiscNr: return 0; case Meta::valBpm: return 80; case Meta::valBitrate: return 160; case Meta::valSamplerate: return 44100; case Meta::valFilesize: return 10; case Meta::valScore: return 0; case Meta::valPlaycount: return 00; case Meta::valRating: return 0; case Meta::valLength: return 3 * 60 + 59; default: return 0; } } MetaQueryWidget::MetaQueryWidget( QWidget* parent, bool onlyNumeric, bool noCondition ) : QWidget( parent ) , m_onlyNumeric( onlyNumeric ) , m_noCondition( noCondition ) , m_settingFilter( false ) , m_andLabel(0) , m_compareSelection(0) , m_valueSelection1(0) , m_valueSelection2(0) { // note: we are using the strange layout structure because the KRatingWidget size depends on the height. m_layoutMain = new QVBoxLayout( this ); m_layoutMain->setContentsMargins(0, 0, 0, 0); makeFieldSelection(); m_layoutMain->addWidget( m_fieldSelection ); m_layoutValue = new QHBoxLayout(); m_layoutMain->addLayout(m_layoutValue); m_layoutValueLabels = new QVBoxLayout(); m_layoutValue->addLayout(m_layoutValueLabels, 0); m_layoutValueValues = new QVBoxLayout(); m_layoutValue->addLayout(m_layoutValueValues, 1); if( m_onlyNumeric ) m_filter.setField( Meta::valYear ); else m_filter.setField( 0 ); setFilter(m_filter); } MetaQueryWidget::~MetaQueryWidget() { } MetaQueryWidget::Filter MetaQueryWidget::filter() const { // special handling for between if( m_filter.condition == Contains ) { Filter f = m_filter; f.numValue = qMin(m_filter.numValue, m_filter.numValue2) - 1; f.numValue2 = qMax(m_filter.numValue, m_filter.numValue2) + 1; } return m_filter; } void MetaQueryWidget::setFilter( const MetaQueryWidget::Filter &value ) { m_settingFilter = true; m_filter = value; int index = m_fieldSelection->findData( int(m_filter.field()) ); m_fieldSelection->setCurrentIndex( index == -1 ? 0 : index ); if( !m_noCondition ) makeCompareSelection(); makeValueSelection(); setValueSelection(); m_settingFilter = false; emit changed(m_filter); } static void addIconItem( KComboBox *box, qint64 field ) { QString icon = Meta::iconForField( field ); QString text = Meta::i18nForField( field ); if( icon.isEmpty() ) box->addItem( text, field ); else box->addItem( KIcon( icon ), text, field ); } void MetaQueryWidget::makeFieldSelection() { m_fieldSelection = new KComboBox( this ); if (!m_onlyNumeric) { m_fieldSelection->addItem( i18n( "Simple Search" ), 0 ); addIconItem( m_fieldSelection, Meta::valUrl ); // note: what about directory? addIconItem( m_fieldSelection, Meta::valTitle ); addIconItem( m_fieldSelection, Meta::valArtist ); addIconItem( m_fieldSelection, Meta::valAlbumArtist ); addIconItem( m_fieldSelection, Meta::valAlbum ); addIconItem( m_fieldSelection, Meta::valGenre ); addIconItem( m_fieldSelection, Meta::valComposer ); } addIconItem( m_fieldSelection, Meta::valYear ); if (!m_onlyNumeric) addIconItem( m_fieldSelection, Meta::valComment ); addIconItem( m_fieldSelection, Meta::valTrackNr ); addIconItem( m_fieldSelection, Meta::valDiscNr ); addIconItem( m_fieldSelection, Meta::valBpm ); addIconItem( m_fieldSelection, Meta::valLength ); addIconItem( m_fieldSelection, Meta::valBitrate ); addIconItem( m_fieldSelection, Meta::valSamplerate ); addIconItem( m_fieldSelection, Meta::valFilesize ); if (!m_onlyNumeric) addIconItem( m_fieldSelection, Meta::valFormat ); addIconItem( m_fieldSelection, Meta::valCreateDate ); addIconItem( m_fieldSelection, Meta::valScore ); addIconItem( m_fieldSelection, Meta::valRating ); addIconItem( m_fieldSelection, Meta::valFirstPlayed ); addIconItem( m_fieldSelection, Meta::valLastPlayed ); addIconItem( m_fieldSelection, Meta::valPlaycount ); if (!m_onlyNumeric) addIconItem( m_fieldSelection, Meta::valLabel ); addIconItem( m_fieldSelection, Meta::valModified ); connect( m_fieldSelection, SIGNAL(currentIndexChanged(int)), this, SLOT(fieldChanged(int)) ); } void MetaQueryWidget::fieldChanged( int i ) { if( m_settingFilter ) return; qint64 field = 0; if( i<0 || i>=m_fieldSelection->count() ) field = m_fieldSelection->itemData( 0 ).toInt(); else field = m_fieldSelection->itemData( i ).toInt(); m_filter.setField( field ); // in the fieldChanged slot we assume that the field was really changed, // so we don't have a problem with throwing away all the old widgets if( !m_noCondition ) makeCompareSelection(); makeValueSelection(); setValueSelection(); emit changed(m_filter); } void MetaQueryWidget::compareChanged( int index ) { FilterCondition condition = FilterCondition( m_compareSelection->itemData( index ).toInt() ); if( m_filter.condition == condition ) return; // nothing to do if( m_filter.isDate() ) { if( ( condition == OlderThan || condition == NewerThan ) && m_filter.condition != OlderThan && m_filter.condition != NewerThan ) { // fix some inaccuracies caused by the conversion absoulte/relative time specifications // this is actually just for visual consistency int unit = 0; qint64 value = QDateTime::currentDateTime().toTime_t() - m_filter.numValue; if( value > 600 || !(value % 60) ) { unit = 1; value /= 60; if( value > 600 || !(value % 60) ) { unit = 2; value /= 60; if( value > 72 || !(value % 24) ) { unit = 3; value /= 24; if( !(value % 365) ) { unit = 6; value /= 365; } else if( !(value % 30) ) { unit = 5; value /= 30; } else if( !(value % 7) ) { unit = 4; value /= 7; } } } } switch( unit ) { case 6: value *= 365*24*60*60; // years break; case 5: value *= 30*24*60*60; // months break; case 4: value *= 7*24*60*60; // weeks break; case 3: - value *= 24; // days + value *= 24*60*60; // days + break; case 2: - value *= 60; // hours + value *= 60*60; // hours + break; case 1: - value *= 60; // minutes + value *= 60; // minutes + break; } m_filter.numValue = value; } else if( condition != OlderThan && condition != NewerThan && ( m_filter.condition == OlderThan || m_filter.condition == NewerThan ) ) { m_filter.numValue = QDateTime::currentDateTime().toTime_t() - m_filter.numValue; } } m_filter.condition = condition; // need to re-generate the value selection fields makeValueSelection(); setValueSelection(); emit changed(m_filter); } void MetaQueryWidget::valueChanged( const QString& value ) { m_filter.value = value; emit changed(m_filter); } void MetaQueryWidget::numValueChanged( int value ) { m_filter.numValue = value; emit changed(m_filter); } void MetaQueryWidget::numValue2Changed( int value ) { m_filter.numValue2 = value; emit changed(m_filter); } void MetaQueryWidget::numValueChanged( qint64 value ) { m_filter.numValue = value; emit changed(m_filter); } void MetaQueryWidget::numValue2Changed( qint64 value ) { m_filter.numValue2 = value; emit changed(m_filter); } void MetaQueryWidget::numValueChanged( const QTime& value ) { m_filter.numValue = qAbs( value.secsTo( QTime(0,0,0) ) ); emit changed(m_filter); } void MetaQueryWidget::numValue2Changed( const QTime& value ) { m_filter.numValue2 = qAbs( value.secsTo( QTime(0,0,0) ) ); emit changed(m_filter); } void MetaQueryWidget::numValueDateChanged() { KDateCombo* dateSelection = qobject_cast( sender() ); if( dateSelection ) { QDate date; dateSelection->getDate( &date ); m_filter.numValue = QDateTime( date ).toTime_t(); emit changed(m_filter); } } void MetaQueryWidget::numValue2DateChanged() { KDateCombo* dateSelection = qobject_cast( sender() ); if( dateSelection ) { QDate date; dateSelection->getDate( &date ); m_filter.numValue2 = QDateTime( date ).toTime_t(); emit changed(m_filter); } } void MetaQueryWidget::numValueTimeDistanceChanged() { if( !sender() ) return; // static_cast. Remember: the TimeDistanceWidget does not have a Q_OBJECT macro TimeDistanceWidget* distanceSelection = static_cast( sender()->parent() ); if( distanceSelection ) { m_filter.numValue = distanceSelection->timeDistance(); emit changed(m_filter); } } void MetaQueryWidget::numValueFormatChanged(int index) { KComboBox* combo = static_cast(sender()); if( combo ) { m_filter.numValue = combo->itemData( index ).toInt(); emit changed(m_filter); } } void MetaQueryWidget::setValueSelection() { if( m_compareSelection ) m_layoutValueLabels->addWidget( m_compareSelection ); if( m_filter.condition == Between ) { delete m_andLabel; // delete the old label m_andLabel = new QLabel( i18n( "and" ), this ); m_layoutValueLabels->addWidget( m_andLabel ); } else { delete m_andLabel; m_andLabel = 0; } if( m_valueSelection1 ) m_layoutValueValues->addWidget( m_valueSelection1 ); if( m_valueSelection2 ) m_layoutValueValues->addWidget( m_valueSelection2 ); } void MetaQueryWidget::makeCompareSelection() { delete m_compareSelection; m_compareSelection = 0; qint64 field = m_filter.field(); if( field == Meta::valFormat ) return; // the field is fixed else if( isDate(field) ) { m_compareSelection = new KComboBox(); m_compareSelection->addItem( conditionToString( Equals, field ), (int)Equals ); m_compareSelection->addItem( conditionToString( LessThan, field ), (int)LessThan ); m_compareSelection->addItem( conditionToString( GreaterThan, field ), (int)GreaterThan ); m_compareSelection->addItem( conditionToString( Between, field ), (int)Between ); m_compareSelection->addItem( conditionToString( OlderThan, field ), (int)OlderThan ); m_compareSelection->addItem( conditionToString( NewerThan, field ), (int)NewerThan ); } else if( isNumeric(field) ) { m_compareSelection = new KComboBox(); m_compareSelection->addItem( conditionToString( Equals, field ), (int)Equals ); m_compareSelection->addItem( conditionToString( LessThan, field ), (int)LessThan ); m_compareSelection->addItem( conditionToString( GreaterThan, field ), (int)GreaterThan ); m_compareSelection->addItem( conditionToString( Between, field ), (int)Between ); } else { m_compareSelection = new KComboBox(); m_compareSelection->addItem( conditionToString( Contains, field ), (int)Contains ); m_compareSelection->addItem( conditionToString( Equals, field ), (int)Equals ); } // -- select the correct entry (even if the condition is not one of the selection) int index = m_compareSelection->findData( int(m_filter.condition) ); if( index == -1 ) { index = 0; m_filter.condition = FilterCondition(m_compareSelection->itemData( index ).toInt()); compareChanged(index); } m_compareSelection->setCurrentIndex( index == -1 ? 0 : index ); connect( m_compareSelection, SIGNAL(currentIndexChanged(int)), SLOT(compareChanged(int)) ); } void MetaQueryWidget::makeValueSelection() { delete m_valueSelection1; m_valueSelection1 = 0; delete m_valueSelection2; m_valueSelection2 = 0; qint64 field = m_filter.field(); if( field == Meta::valUrl ) makeFilenameSelection(); else if( field == Meta::valTitle ) // We,re not going to populate this. There tends to be too many titles. makeGenericComboSelection( true, 0 ); else if( field == Meta::valArtist || field == Meta::valAlbumArtist || field == Meta::valAlbum || field == Meta::valGenre || field == Meta::valComposer ) makeMetaComboSelection( field ); else if( field == Meta::valYear ) makeGenericNumberSelection( field ); else if( field == Meta::valComment ) makeGenericComboSelection( true, 0 ); else if( field == Meta::valTrackNr ) makeGenericNumberSelection( field ); else if( field == Meta::valDiscNr ) makeGenericNumberSelection( field ); else if( field == Meta::valBpm ) makeGenericNumberSelection( field ); else if( field == Meta::valLength ) makeLengthSelection(); else if( field == Meta::valBitrate ) makeGenericNumberSelection( field, i18nc("Unit for data rate kilo bit per seconds", "kbps") ); else if( field == Meta::valSamplerate ) makeGenericNumberSelection( field, i18nc("Unit for sample rate", "Hz") ); else if( field == Meta::valFilesize ) makeGenericNumberSelection( field, i18nc("Unit for file size in mega byte", "MiB") ); else if( field == Meta::valFormat ) makeFormatComboSelection(); else if( field == Meta::valCreateDate ) makeDateTimeSelection(); else if( field == Meta::valScore ) makeGenericNumberSelection( field ); else if( field == Meta::valRating ) makeRatingSelection(); else if( field == Meta::valFirstPlayed ) makeDateTimeSelection(); else if( field == Meta::valLastPlayed ) makeDateTimeSelection(); else if( field == Meta::valPlaycount ) makeGenericNumberSelection( field ); else if( field == Meta::valLabel ) makeGenericComboSelection( true, 0 ); else if( field == Meta::valModified ) makeDateTimeSelection(); else // e.g. the simple search makeGenericComboSelection( true, 0 ); } void MetaQueryWidget::makeGenericComboSelection( bool editable, Collections::QueryMaker* populateQuery ) { KComboBox* combo = new KComboBox( this ); combo->setEditable( editable ); if( populateQuery != 0 ) { m_runningQueries.insert(populateQuery, QWeakPointer(combo)); connect( populateQuery, SIGNAL(newResultReady(QStringList)), SLOT(populateComboBox(QStringList)) ); connect( populateQuery, SIGNAL(queryDone()), SLOT(comboBoxPopulated()) ); populateQuery->run(); } combo->setEditText( m_filter.value ); connect( combo, SIGNAL(editTextChanged(QString)), SLOT(valueChanged(QString)) ); combo->completionObject()->setIgnoreCase( true ); combo->setCompletionMode( KGlobalSettings::CompletionPopup ); combo->setInsertPolicy( QComboBox::InsertAtTop ); m_valueSelection1 = combo; } void MetaQueryWidget::makeMetaComboSelection( qint64 field ) { Collections::QueryMaker* qm = CollectionManager::instance()->queryMaker(); qm->setQueryType( Collections::QueryMaker::Custom ); qm->addReturnValue( field ); qm->setAutoDelete( true ); makeGenericComboSelection( true, qm ); } void MetaQueryWidget::populateComboBox( QStringList results ) { QObject* query = sender(); if( !query ) return; QWeakPointer combo = m_runningQueries.value(query); if( combo.isNull() ) return; // note: adding items seems to reset the edit text, so we have // to take care of that. disconnect( combo.data(), 0, this, 0 ); // want the results unique and sorted const QSet dataSet = results.toSet(); QStringList dataList = dataSet.toList(); dataList.sort(); combo.data()->addItems( dataList ); KCompletion* comp = combo.data()->completionObject(); comp->setItems( dataList ); // reset the text and re-enable the signal combo.data()->setEditText( m_filter.value ); connect( combo.data(), SIGNAL(editTextChanged(QString)), SLOT(valueChanged(QString)) ); } void MetaQueryWidget::makeFormatComboSelection() { KComboBox* combo = new KComboBox( this ); combo->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Preferred ); QStringList filetypes = Amarok::FileTypeSupport::possibleFileTypes(); for (int listpos=0;listposaddItem(filetypes.at(listpos),listpos); } int index = m_fieldSelection->findData( (int)m_filter.numValue ); combo->setCurrentIndex( index == -1 ? 0 : index ); connect( combo, SIGNAL(currentIndexChanged(int)), SLOT(numValueFormatChanged(int)) ); m_valueSelection1 = combo; } void MetaQueryWidget::comboBoxPopulated() { QObject* query = sender(); if( !query ) return; m_runningQueries.remove( query ); } void MetaQueryWidget::makeFilenameSelection() { // Don't populate the combobox. Too many urls. makeGenericComboSelection( true, 0 ); } void MetaQueryWidget::makeRatingSelection() { KRatingWidget* ratingWidget = new KRatingWidget(); ratingWidget->setRating( (int)m_filter.numValue ); connect( ratingWidget, SIGNAL(ratingChanged(int)), this, SLOT(numValueChanged(int)) ); m_valueSelection1 = ratingWidget; if( m_filter.condition != Between ) return; // second KRatingWidget for the between selection KRatingWidget* ratingWidget2 = new KRatingWidget(); ratingWidget2->setRating( (int)m_filter.numValue2 ); connect( ratingWidget2, SIGNAL(ratingChanged(int)), this, SLOT(numValue2Changed(int)) ); m_valueSelection2 = ratingWidget2; } void MetaQueryWidget::makeLengthSelection() { QString displayFormat = i18nc( "time format for specifying track length - hours, minutes, seconds", "h:m:ss" ); QTimeEdit* timeSpin = new QTimeEdit(); timeSpin->setDisplayFormat( displayFormat ); timeSpin->setMinimumTime( QTime( 0, 0, 0 ) ); timeSpin->setMaximumTime( QTime( maxHours - 1, 59, 59 ) ); timeSpin->setTime( QTime().addSecs( m_filter.numValue ) ); connect( timeSpin, SIGNAL(timeChanged(QTime)), SLOT(numValueChanged(QTime)) ); m_valueSelection1 = timeSpin; if( m_filter.condition != Between ) return; QTimeEdit* timeSpin2 = new QTimeEdit(); timeSpin2->setDisplayFormat( displayFormat ); timeSpin2->setMinimumTime( QTime( 0, 0, 0 ) ); timeSpin2->setMaximumTime( QTime( maxHours - 1, 59, 59 ) ); timeSpin2->setTime( QTime().addSecs( m_filter.numValue2 ) ); connect( timeSpin2, SIGNAL(timeChanged(QTime)), SLOT(numValue2Changed(QTime)) ); m_valueSelection2 = timeSpin2; } void MetaQueryWidget::makeGenericNumberSelection( qint64 field, const QString& unit ) { KIntSpinBox* spin = new KIntSpinBox(); spin->setMinimum( Filter::minimumValue( field ) ); spin->setMaximum( Filter::maximumValue( field ) ); if( !unit.isEmpty() ) spin->setSuffix( ' ' + unit ); spin->setValue( m_filter.numValue ); connect( spin, SIGNAL(valueChanged(int)), this, SLOT(numValueChanged(int)) ); m_valueSelection1 = spin; if( m_filter.condition != Between ) return; // second spin box for the between selection KIntSpinBox* spin2 = new KIntSpinBox(); spin2->setMinimum( Filter::minimumValue( field ) ); spin2->setMaximum( Filter::maximumValue( field ) ); if( !unit.isEmpty() ) spin2->setSuffix( ' ' + unit ); spin2->setValue( m_filter.numValue2 ); connect( spin2, SIGNAL(valueChanged(int)), this, SLOT(numValue2Changed(int)) ); m_valueSelection2 = spin2; } void MetaQueryWidget::makeDateTimeSelection() { if( m_filter.condition == OlderThan || m_filter.condition == NewerThan ) { TimeDistanceWidget* distanceSelection = new TimeDistanceWidget(); distanceSelection->setTimeDistance( m_filter.numValue ); distanceSelection->connectChanged( this, SLOT(numValueTimeDistanceChanged())); m_valueSelection1 = distanceSelection; } else { KDateCombo* dateSelection = new KDateCombo(); QDateTime dt; // if( m_filter.condition == Contains || m_filter.condition == Equals ) // dt = QDateTime::currentDateTime(); // else // dt.setTime_t( m_filter.numValue ); dt.setTime_t( m_filter.numValue ); dateSelection->setDate( dt.date() ); connect( dateSelection, SIGNAL(currentIndexChanged(int)), SLOT(numValueDateChanged()) ); m_valueSelection1 = dateSelection; if( m_filter.condition != Between ) return; // second KDateCombo for the between selection KDateCombo* dateSelection2 = new KDateCombo(); dt.setTime_t( m_filter.numValue2 ); dateSelection2->setDate( dt.date() ); connect( dateSelection2, SIGNAL(currentIndexChanged(int)), SLOT(numValue2DateChanged()) ); m_valueSelection2 = dateSelection2; } } bool MetaQueryWidget::isNumeric( qint64 field ) { switch( field ) { case Meta::valYear: case Meta::valTrackNr: case Meta::valDiscNr: case Meta::valBpm: case Meta::valLength: case Meta::valBitrate: case Meta::valSamplerate: case Meta::valFilesize: case Meta::valFormat: case Meta::valCreateDate: case Meta::valScore: case Meta::valRating: case Meta::valFirstPlayed: case Meta::valLastPlayed: case Meta::valPlaycount: case Meta::valModified: return true; default: return false; } } bool MetaQueryWidget::isDate( qint64 field ) { switch( field ) { case Meta::valCreateDate: case Meta::valFirstPlayed: case Meta::valLastPlayed: case Meta::valModified: return true; default: return false; } } QString MetaQueryWidget::conditionToString( FilterCondition condition, qint64 field ) { if( isDate(field) ) { switch( condition ) { case LessThan: return i18nc( "The date lies before the given fixed date", "before" ); case Equals: return i18nc( "The date is the same as the given fixed date", "on" ); case GreaterThan: return i18nc( "The date is after the given fixed date", "after" ); case Between: return i18nc( "The date is between the given fixed dates", "between" ); case OlderThan: return i18nc( "The date lies before the given time interval", "older than" ); case NewerThan: return i18nc( "The date lies after the given time interval", "newer than" ); default: ; // fall through } } else if( isNumeric(field) ) { switch( condition ) { case LessThan: return i18n("less than"); case Equals: return i18nc("a numerical tag (like year or track number) equals a value","equals"); case GreaterThan: return i18n("greater than"); case Between: return i18nc( "a numerical tag (like year or track number) is between two values", "between" ); default: ; // fall through } } else { switch( condition ) { case Equals: return i18nc("an alphabetical tag (like title or artist name) equals some string","equals"); case Contains: return i18nc("an alphabetical tag (like title or artist name) contains some string", "contains"); default: ; // fall through } } return QString( i18n("unknown comparison") ); } QString MetaQueryWidget::Filter::fieldToString() const { return Meta::shortI18nForField( m_field ); } QString MetaQueryWidget::Filter::toString( bool invert ) const { // this member is called when there is a keyword that needs numeric attributes QString strValue1 = value; QString strValue2 = value; if( m_field == Meta::valFormat ) { strValue1 = Amarok::FileTypeSupport::toString( Amarok::FileType( numValue )); } else if( m_field == Meta::valRating ) { strValue1 = QString::number( (float)numValue / 2 ); strValue2 = QString::number( (float)numValue2 / 2 ); } else if( isDate() ) { if( condition == OlderThan || condition == NewerThan ) { strValue1 = QString::number( numValue ); strValue2 = QString::number( numValue2 ); } else { KLocalizedDate localizedDate1( QDateTime::fromTime_t(numValue).date() ); strValue1 = localizedDate1.formatDate( KLocale::ShortDate ); KLocalizedDate localizedDate2( QDateTime::fromTime_t(numValue2).date() ); strValue2 = localizedDate2.formatDate( KLocale::ShortDate ); } } else if( isNumeric() ) { if ( condition != Between ) { strValue1 = QString::number( numValue ); } else if (numValue < numValue2) // two values are only used for "between". We want to order them by size { strValue1 = QString::number( numValue ); strValue2 = QString::number( numValue2 ); } else { strValue1 = QString::number( numValue2 ); strValue2 = QString::number( numValue ); } } QString result; if( m_field ) result = fieldToString() + ':'; switch( condition ) { case Equals: { if( isNumeric() ) result += strValue1; else result += '=' + QString( "\"%1\"" ).arg( value ); if( invert ) result.prepend( QChar('-') ); break; } case GreaterThan: { result += '>' + strValue1; if( invert ) result.prepend( QChar('-') ); break; } case LessThan: { result +='<' + strValue1; if( invert ) result.prepend( QChar('-') ); break; } case Between: { if( invert ) result = QString( "%1<%2 OR %1>%3" ).arg( result, strValue1, strValue2 ); else result = QString( "%1>%2 AND %1<%3" ).arg( result, strValue1, strValue2 ); break; } case OlderThan: case NewerThan: { // a human readable time.. QChar strUnit = 's'; qint64 value = numValue; if( !(value % 60) ) { strUnit = 'M'; value /= 60; if( !(value % 60) ) { strUnit = 'h'; value /= 60; if( !(value % 24) ) { strUnit = 'd'; value /= 24; if( !(value % 365) ) { strUnit = 'y'; value /= 365; } else if( !(value % 30) ) { strUnit = 'm'; value /= 30; } else if( !(value % 7) ) { strUnit = 'w'; value /= 7; } } } } if( condition == OlderThan ) result += '>' + QString::number(value) + strUnit; else result += '<' + QString::number(value) + strUnit; if( invert ) result.prepend( QChar('-') ); break; } case Contains: { result += QString( "\"%1\"" ).arg( value ); if( invert ) result.prepend( QChar('-') ); break; } } return result; } bool MetaQueryWidget::isFieldSelectorHidden() const { return m_fieldSelection->isHidden(); } void MetaQueryWidget::setFieldSelectorHidden( const bool hidden ) { m_fieldSelection->setVisible( !hidden ); } void MetaQueryWidget::setField( const qint64 field ) { int index = m_fieldSelection->findData( field ); m_fieldSelection->setCurrentIndex( index == -1 ? 0 : index ); } #include "MetaQueryWidget.moc"