Combine similar search results

This commit is contained in:
David Sansome 2011-09-17 17:42:14 +01:00
parent 4065037aba
commit 02f2d5dc88
9 changed files with 144 additions and 19 deletions

View File

@ -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<SearchProvider::Result>();
index.data(GlobalSearchWidget::Role_PrimaryResult).value<SearchProvider::Result>();
const SearchProvider::ResultList all_results =
index.data(GlobalSearchWidget::Role_AllResults).value<SearchProvider::ResultList>();
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));

View File

@ -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<SearchProvider::Result>();
const SearchProvider::Result r2 = right.data(GlobalSearchWidget::Role_Result)
const SearchProvider::Result r2 = right.data(GlobalSearchWidget::Role_PrimaryResult)
.value<SearchProvider::Result>();
int ret = 0;

View File

@ -35,12 +35,14 @@
#include <QDesktopWidget>
#include <QListView>
#include <QPainter>
#include <QSettings>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
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<SearchProvider::Result>();
source_index.data(Role_PrimaryResult).value<SearchProvider::Result>();
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<SearchProvider::Result>());
engine_->LoadTracksAsync(index.data(Role_PrimaryResult).value<SearchProvider::Result>());
}
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<SearchProvider::Result>();
const SearchProvider::Result r2 = right.data(Role_PrimaryResult)
.value<SearchProvider::Result>();
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>();
SearchProvider::ResultList inferior_results =
inferior_item->data(Role_AllResults).value<SearchProvider::ResultList>();
superior_results.append(inferior_results);
superior_item->setData(QVariant::fromValue(superior_results), Role_AllResults);
model_->invisibleRootItem()->removeRow(inferior_item->row());
}

View File

@ -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

View File

@ -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<AlbumCoverLoader, AlbumCoverLoader>(this))
{
Init(name, icon, false, true);
Init(name, id, icon, false, true);
cover_loader_->Start(true);
cover_loader_->Worker()->SetDesiredHeight(kArtHeight);

View File

@ -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);

View File

@ -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;

View File

@ -72,6 +72,7 @@ public:
typedef QList<Result> 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 {

View File

@ -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() {