diff --git a/src/globalsearch/globalsearchitemdelegate.cpp b/src/globalsearch/globalsearchitemdelegate.cpp index b961805de..846bc9a20 100644 --- a/src/globalsearch/globalsearchitemdelegate.cpp +++ b/src/globalsearch/globalsearchitemdelegate.cpp @@ -47,7 +47,9 @@ void GlobalSearchItemDelegate::paint(QPainter* p, const QStyleOptionViewItem& option, const QModelIndex& index) const { const SearchProvider::Result result = - index.data(GlobalSearchWidget::Role_Result).value(); + index.data(GlobalSearchWidget::Role_PrimaryResult).value(); + const SearchProvider::ResultList all_results = + index.data(GlobalSearchWidget::Role_AllResults).value(); const Song& m = result.metadata_; const bool selected = option.state & QStyle::State_Selected; @@ -101,15 +103,21 @@ void GlobalSearchItemDelegate::paint(QPainter* p, p->setFont(big_font); p->drawText(count_rect, Qt::TextSingleLine | Qt::AlignCenter, count); - // Draw a provider icon on the right. - QRect icon_rect(rect.right() - kArtMargin - kProviderIconSize, + // Draw provider icons on the right. + const int icons_width = (kProviderIconSize + kArtMargin) * all_results.count(); + QRect icons_rect(rect.right() - icons_width, rect.top() + (rect.height() - kProviderIconSize) / 2, - kProviderIconSize, kProviderIconSize); - p->drawPixmap(icon_rect, result.provider_->icon().pixmap(kProviderIconSize)); + icons_width, kProviderIconSize); + + QRect icon_rect(icons_rect.topLeft(), QSize(kProviderIconSize, kProviderIconSize)); + foreach (const SearchProvider::Result& result, all_results) { + p->drawPixmap(icon_rect, result.provider_->icon().pixmap(kProviderIconSize)); + icon_rect.translate(kProviderIconSize + kArtMargin, 0); + } // Position text QRect text_rect(count_rect.right() + kArtMargin, count_rect.top(), - icon_rect.left() - count_rect.right() - kArtMargin*2, kHeight); + icons_rect.left() - count_rect.right() - kArtMargin*2, kHeight); QRect text_rect_1(text_rect.adjusted(0, 0, 0, -kHeight/2)); QRect text_rect_2(text_rect.adjusted(0, kHeight/2, 0, 0)); diff --git a/src/globalsearch/globalsearchsortmodel.cpp b/src/globalsearch/globalsearchsortmodel.cpp index 1be202f99..88fea56d2 100644 --- a/src/globalsearch/globalsearchsortmodel.cpp +++ b/src/globalsearch/globalsearchsortmodel.cpp @@ -26,9 +26,9 @@ GlobalSearchSortModel::GlobalSearchSortModel(QObject* parent) } bool GlobalSearchSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - const SearchProvider::Result r1 = left.data(GlobalSearchWidget::Role_Result) + const SearchProvider::Result r1 = left.data(GlobalSearchWidget::Role_PrimaryResult) .value(); - const SearchProvider::Result r2 = right.data(GlobalSearchWidget::Role_Result) + const SearchProvider::Result r2 = right.data(GlobalSearchWidget::Role_PrimaryResult) .value(); int ret = 0; diff --git a/src/globalsearch/globalsearchwidget.cpp b/src/globalsearch/globalsearchwidget.cpp index 2b940667b..1a8dd2d63 100644 --- a/src/globalsearch/globalsearchwidget.cpp +++ b/src/globalsearch/globalsearchwidget.cpp @@ -35,12 +35,14 @@ #include #include #include +#include #include #include const int GlobalSearchWidget::kMinVisibleItems = 3; const int GlobalSearchWidget::kMaxVisibleItems = 25; +const char* GlobalSearchWidget::kSettingsGroup = "GlobalSearch"; GlobalSearchWidget::GlobalSearchWidget(QWidget* parent) @@ -54,9 +56,11 @@ GlobalSearchWidget::GlobalSearchWidget(QWidget* parent) view_(new QListView), eat_focus_out_(false), background_(":allthethings.png"), - desktop_(qApp->desktop()) + desktop_(qApp->desktop()), + combine_identical_results_(true) { ui_->setupUi(this); + ReloadSettings(); // Set up the sorting proxy model proxy_->setSourceModel(model_); @@ -97,7 +101,8 @@ GlobalSearchWidget::~GlobalSearchWidget() { void GlobalSearchWidget::Init(LibraryBackendInterface* library) { // Add providers engine_->AddProvider(new LibrarySearchProvider( - library, tr("Library"), IconLoader::Load("folder-sound"), engine_)); + library, tr("Library"), "library", + IconLoader::Load("folder-sound"), engine_)); #ifdef HAVE_SPOTIFY engine_->AddProvider(new SpotifySearchProvider(engine_)); @@ -189,7 +194,8 @@ void GlobalSearchWidget::AddResults(int id, const SearchProvider::ResultList& re foreach (const SearchProvider::Result& result, results) { QStandardItem* item = new QStandardItem; - item->setData(QVariant::fromValue(result), Role_Result); + item->setData(QVariant::fromValue(result), Role_PrimaryResult); + item->setData(QVariant::fromValue(SearchProvider::ResultList() << result), Role_AllResults); QPixmap pixmap; if (engine_->FindCachedPixmap(result, &pixmap)) { @@ -197,6 +203,39 @@ void GlobalSearchWidget::AddResults(int id, const SearchProvider::ResultList& re } model_->appendRow(item); + + if (combine_identical_results_) { + // Maybe we can combine this result with an identical result from another + // provider. Only look at the results above and below this one in the + // sorted model. + QModelIndex my_proxy_index = proxy_->mapFromSource(item->index()); + QModelIndexList candidates; + candidates << my_proxy_index.sibling(my_proxy_index.row() - 1, 0) + << my_proxy_index.sibling(my_proxy_index.row() + 1, 0); + + foreach (const QModelIndex& index, candidates) { + if (!index.isValid()) + continue; + + CombineAction action = CanCombineResults(my_proxy_index, index); + + switch (action) { + case CannotCombine: + continue; + + case LeftPreferred: + CombineResults(my_proxy_index, index); + break; + + case RightPreferred: + CombineResults(index, my_proxy_index); + break; + } + + // We've just invalidated the indexes so we have to stop. + break; + } + } } RepositionPopup(); @@ -371,7 +410,7 @@ void GlobalSearchWidget::LazyLoadArt(const QModelIndex& proxy_index) { model_->itemFromIndex(source_index)->setData(true, Role_LazyLoadingArt); const SearchProvider::Result result = - source_index.data(Role_Result).value(); + source_index.data(Role_PrimaryResult).value(); int id = engine_->LoadArtAsync(result); art_requests_[id] = source_index; @@ -393,7 +432,7 @@ void GlobalSearchWidget::AddCurrent() { if (!index.isValid()) return; - engine_->LoadTracksAsync(index.data(Role_Result).value()); + engine_->LoadTracksAsync(index.data(Role_PrimaryResult).value()); } void GlobalSearchWidget::TracksLoaded(int id, MimeData* mime_data) { @@ -406,3 +445,59 @@ void GlobalSearchWidget::TracksLoaded(int id, MimeData* mime_data) { emit AddToPlaylist(mime_data); } +void GlobalSearchWidget::ReloadSettings() { + QSettings s; + s.beginGroup(kSettingsGroup); + + combine_identical_results_ = s.value("combine_identical_results", true).toBool(); + provider_order_ = s.value("provider_order", QStringList() << "library").toStringList(); +} + +GlobalSearchWidget::CombineAction GlobalSearchWidget::CanCombineResults( + const QModelIndex& left, const QModelIndex& right) const { + const SearchProvider::Result r1 = left.data(Role_PrimaryResult) + .value(); + const SearchProvider::Result r2 = right.data(Role_PrimaryResult) + .value(); + + if (r1.match_quality_ != r2.match_quality_ || r1.type_ != r2.type_) + return CannotCombine; + +#define StringsDiffer(field) \ + (QString::compare(r1.metadata_.field(), r2.metadata_.field(), Qt::CaseInsensitive) != 0) + + switch (r1.type_) { + case SearchProvider::Result::Type_Track: + if (StringsDiffer(title)) + return CannotCombine; + // fallthrough + case SearchProvider::Result::Type_Album: + if (StringsDiffer(album) || StringsDiffer(artist)) + return CannotCombine; + break; + } + +#undef StringsDiffer + + // They look the same - decide which provider we like best. + const int p1 = provider_order_.indexOf(r1.provider_->id()); + const int p2 = provider_order_.indexOf(r2.provider_->id()); + + return p2 > p1 ? RightPreferred : LeftPreferred; +} + +void GlobalSearchWidget::CombineResults(const QModelIndex& superior, const QModelIndex& inferior) { + QStandardItem* superior_item = model_->itemFromIndex(proxy_->mapToSource(superior)); + QStandardItem* inferior_item = model_->itemFromIndex(proxy_->mapToSource(inferior)); + + SearchProvider::ResultList superior_results = + superior_item->data(Role_AllResults).value(); + SearchProvider::ResultList inferior_results = + inferior_item->data(Role_AllResults).value(); + + superior_results.append(inferior_results); + superior_item->setData(QVariant::fromValue(superior_results), Role_AllResults); + + model_->invisibleRootItem()->removeRow(inferior_item->row()); +} + diff --git a/src/globalsearch/globalsearchwidget.h b/src/globalsearch/globalsearchwidget.h index 8ade50c78..f2c156101 100644 --- a/src/globalsearch/globalsearchwidget.h +++ b/src/globalsearch/globalsearchwidget.h @@ -43,9 +43,11 @@ public: static const int kMinVisibleItems; static const int kMaxVisibleItems; + static const char* kSettingsGroup; enum Role { - Role_Result = Qt::UserRole + 1, + Role_PrimaryResult = Qt::UserRole + 1, + Role_AllResults, Role_LazyLoadingArt }; @@ -58,6 +60,9 @@ public: bool eventFilter(QObject* o, QEvent* e); void setFocus(Qt::FocusReason reason); +public slots: + void ReloadSettings(); + signals: void AddToPlaylist(QMimeData* data); @@ -77,8 +82,17 @@ private slots: void AddCurrent(); private: + // Return values from CanCombineResults + enum CombineAction { + CannotCombine, // The two results are different and can't be combined + LeftPreferred, // The two results can be combined - the left one is better + RightPreferred // The two results can be combined - the right one is better + }; + void Reset(); void RepositionPopup(); + CombineAction CanCombineResults(const QModelIndex& left, const QModelIndex& right) const; + void CombineResults(const QModelIndex& superior, const QModelIndex& inferior); bool EventFilterSearchWidget(QObject* o, QEvent* e); bool EventFilterPopup(QObject* o, QEvent* e); @@ -101,6 +115,9 @@ private: QPixmap background_scaled_; QDesktopWidget* desktop_; + + bool combine_identical_results_; + QStringList provider_order_; }; #endif // GLOBALSEARCHWIDGET_H diff --git a/src/globalsearch/librarysearchprovider.cpp b/src/globalsearch/librarysearchprovider.cpp index c14bb618c..687d75645 100644 --- a/src/globalsearch/librarysearchprovider.cpp +++ b/src/globalsearch/librarysearchprovider.cpp @@ -26,13 +26,14 @@ LibrarySearchProvider::LibrarySearchProvider(LibraryBackendInterface* backend, const QString& name, + const QString& id, const QIcon& icon, QObject* parent) : BlockingSearchProvider(parent), backend_(backend), cover_loader_(new BackgroundThreadImplementation(this)) { - Init(name, icon, false, true); + Init(name, id, icon, false, true); cover_loader_->Start(true); cover_loader_->Worker()->SetDesiredHeight(kArtHeight); diff --git a/src/globalsearch/librarysearchprovider.h b/src/globalsearch/librarysearchprovider.h index 7f29c604a..706fb9b9d 100644 --- a/src/globalsearch/librarysearchprovider.h +++ b/src/globalsearch/librarysearchprovider.h @@ -30,7 +30,7 @@ class LibrarySearchProvider : public BlockingSearchProvider { public: LibrarySearchProvider(LibraryBackendInterface* backend, const QString& name, - const QIcon& icon, QObject* parent = 0); + const QString& id, const QIcon& icon, QObject* parent = 0); void LoadArtAsync(int id, const Result& result); void LoadTracksAsync(int id, const Result& result); diff --git a/src/globalsearch/searchprovider.cpp b/src/globalsearch/searchprovider.cpp index 4bd5d8845..f10cfc2c7 100644 --- a/src/globalsearch/searchprovider.cpp +++ b/src/globalsearch/searchprovider.cpp @@ -29,9 +29,10 @@ SearchProvider::SearchProvider(QObject* parent) { } -void SearchProvider::Init(const QString& name, const QIcon& icon, +void SearchProvider::Init(const QString& name, const QString& id, const QIcon& icon, bool delay_searches, bool serialised_art) { name_ = name; + id_ = id; icon_ = icon; delay_searches_ = delay_searches; serialised_art_ = serialised_art; diff --git a/src/globalsearch/searchprovider.h b/src/globalsearch/searchprovider.h index 2d7ea3e28..08b5ead74 100644 --- a/src/globalsearch/searchprovider.h +++ b/src/globalsearch/searchprovider.h @@ -72,6 +72,7 @@ public: typedef QList ResultList; const QString& name() const { return name_; } + const QString& id() const { return id_; } const QIcon& icon() const { return icon_; } const bool wants_delayed_queries() const { return delay_searches_; } const bool wants_serialised_art() const { return serialised_art_; } @@ -106,17 +107,19 @@ protected: static Result::MatchQuality MatchQuality(const QStringList& tokens, const QString& string); // Subclasses must call this from their constructor - void Init(const QString& name, const QIcon& icon, + void Init(const QString& name, const QString& id, const QIcon& icon, bool delay_searches, bool serialised_art); private: QString name_; + QString id_; QIcon icon_; bool delay_searches_; bool serialised_art_; }; Q_DECLARE_METATYPE(SearchProvider::Result) +Q_DECLARE_METATYPE(SearchProvider::ResultList) class BlockingSearchProvider : public SearchProvider { diff --git a/src/globalsearch/spotifysearchprovider.cpp b/src/globalsearch/spotifysearchprovider.cpp index 0dc1642b3..4ffbbea71 100644 --- a/src/globalsearch/spotifysearchprovider.cpp +++ b/src/globalsearch/spotifysearchprovider.cpp @@ -28,7 +28,7 @@ SpotifySearchProvider::SpotifySearchProvider(QObject* parent) server_(NULL), service_(NULL) { - Init("Spotify", QIcon(":icons/svg/spotify.svg"), true, true); + Init("Spotify", "spotify", QIcon(":icons/svg/spotify.svg"), true, true); } SpotifyServer* SpotifySearchProvider::server() {