Add a "tooltip" that shows the songs within each album in the global search results

This commit is contained in:
David Sansome 2011-09-18 00:06:07 +01:00
parent 02f2d5dc88
commit 4ac16f0dd4
14 changed files with 441 additions and 15 deletions

View File

@ -1,2 +1,2 @@
# Increment this whenever the user needs to download a new blob # Increment this whenever the user needs to download a new blob
set(SPOTIFY_BLOB_VERSION 5) set(SPOTIFY_BLOB_VERSION 6)

View File

@ -233,9 +233,17 @@ void SpotifyClient::SendSearchResponse(sp_search* result) {
QList<sp_albumbrowse*> browses = pending_search_album_browses_.take(result); QList<sp_albumbrowse*> browses = pending_search_album_browses_.take(result);
foreach (sp_albumbrowse* browse, browses) { foreach (sp_albumbrowse* browse, browses) {
sp_album* album = sp_albumbrowse_album(browse); sp_album* album = sp_albumbrowse_album(browse);
spotify_pb::Track* track = response->add_album(); spotify_pb::Album* msg = response->add_album();
ConvertAlbum(album, track);
ConvertAlbumBrowse(browse, track); ConvertAlbum(album, msg->mutable_metadata());
ConvertAlbumBrowse(browse, msg->mutable_metadata());
// Add all tracks
const int tracks = sp_albumbrowse_num_tracks(browse);
for (int i=0 ; i<tracks ; ++i) {
ConvertTrack(sp_albumbrowse_track(browse, i), msg->add_track());
}
sp_albumbrowse_release(browse); sp_albumbrowse_release(browse);
} }

View File

@ -68,6 +68,11 @@ message Track {
required string album_art_id = 11; required string album_art_id = 11;
} }
message Album {
required Track metadata = 1;
repeated Track track = 2;
}
enum PlaylistType { enum PlaylistType {
Starred = 1; Starred = 1;
Inbox = 2; Inbox = 2;
@ -116,7 +121,9 @@ message SearchResponse {
optional string did_you_mean = 4; optional string did_you_mean = 4;
optional string error = 5; optional string error = 5;
repeated Track album = 6; // field 6 is deprecated
repeated Album album = 7;
} }
message ImageRequest { message ImageRequest {

View File

@ -119,9 +119,11 @@ set(SOURCES
globalsearch/globalsearchitemdelegate.cpp globalsearch/globalsearchitemdelegate.cpp
globalsearch/globalsearchpopup.cpp globalsearch/globalsearchpopup.cpp
globalsearch/globalsearchsortmodel.cpp globalsearch/globalsearchsortmodel.cpp
globalsearch/globalsearchtooltip.cpp
globalsearch/globalsearchwidget.cpp globalsearch/globalsearchwidget.cpp
globalsearch/librarysearchprovider.cpp globalsearch/librarysearchprovider.cpp
globalsearch/searchprovider.cpp globalsearch/searchprovider.cpp
globalsearch/tooltipresultwidget.cpp
internet/digitallyimportedservice.cpp internet/digitallyimportedservice.cpp
internet/digitallyimportedservicebase.cpp internet/digitallyimportedservicebase.cpp
@ -358,8 +360,10 @@ set(HEADERS
globalsearch/librarysearchprovider.h globalsearch/librarysearchprovider.h
globalsearch/globalsearch.h globalsearch/globalsearch.h
globalsearch/globalsearchpopup.h globalsearch/globalsearchpopup.h
globalsearch/globalsearchtooltip.h
globalsearch/globalsearchwidget.h globalsearch/globalsearchwidget.h
globalsearch/searchprovider.h globalsearch/searchprovider.h
globalsearch/tooltipresultwidget.h
internet/digitallyimportedservicebase.h internet/digitallyimportedservicebase.h
internet/digitallyimportedsettingspage.h internet/digitallyimportedsettingspage.h

View File

@ -0,0 +1,83 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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 3 of the License, or
(at your option) any later version.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "globalsearchtooltip.h"
#include "tooltipresultwidget.h"
#include "core/logging.h"
#include <QCoreApplication>
#include <QKeyEvent>
#include <QLayoutItem>
#include <QPainter>
#include <QVBoxLayout>
GlobalSearchTooltip::GlobalSearchTooltip(QObject* event_target)
: QWidget(NULL),
event_target_(event_target)
{
setWindowFlags(Qt::Popup);
setFocusPolicy(Qt::NoFocus);
setAttribute(Qt::WA_OpaquePaintEvent);
}
void GlobalSearchTooltip::SetResults(const SearchProvider::ResultList& results) {
results_ = results;
qDeleteAll(widgets_);
widgets_.clear();
// Using a QVBoxLayout here made some weird flickering that I couldn't figure
// out how to fix, so do layout manually.
int y = 0;
int w = 0;
foreach (const SearchProvider::Result& result, results) {
QWidget* widget = new TooltipResultWidget(result, this);
widget->move(0, y);
widget->show();
widgets_ << widget;
QSize size_hint(widget->sizeHint());
y += size_hint.height();
w = qMax(w, size_hint.width());
}
resize(w, y);
}
void GlobalSearchTooltip::ShowAt(const QPoint& pointing_to) {
move(pointing_to);
if (!isVisible())
show();
}
void GlobalSearchTooltip::keyPressEvent(QKeyEvent* e) {
// Copy the event to send to the target
QKeyEvent e2(e->type(), e->key(), e->modifiers(), e->text(),
e->isAutoRepeat(), e->count());
qApp->sendEvent(event_target_, &e2);
e->accept();
}
void GlobalSearchTooltip::paintEvent(QPaintEvent*) {
QPainter p(this);
p.fillRect(rect(), palette().base());
}

View File

@ -0,0 +1,48 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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 3 of the License, or
(at your option) any later version.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef GLOBALSEARCHTOOLTIP_H
#define GLOBALSEARCHTOOLTIP_H
#include "searchprovider.h"
#include <QWidget>
class QVBoxLayout;
class GlobalSearchTooltip : public QWidget {
Q_OBJECT
public:
GlobalSearchTooltip(QObject* event_target);
void SetResults(const SearchProvider::ResultList& results);
void ShowAt(const QPoint& pointing_to);
protected:
void keyPressEvent(QKeyEvent* e);
void paintEvent(QPaintEvent*);
private:
SearchProvider::ResultList results_;
QObject* event_target_;
QWidgetList widgets_;
};
#endif // GLOBALSEARCHTOOLTIP_H

View File

@ -19,6 +19,7 @@
#include "globalsearch.h" #include "globalsearch.h"
#include "globalsearchitemdelegate.h" #include "globalsearchitemdelegate.h"
#include "globalsearchsortmodel.h" #include "globalsearchsortmodel.h"
#include "globalsearchtooltip.h"
#include "globalsearchwidget.h" #include "globalsearchwidget.h"
#include "librarysearchprovider.h" #include "librarysearchprovider.h"
#include "ui_globalsearchwidget.h" #include "ui_globalsearchwidget.h"
@ -92,6 +93,8 @@ GlobalSearchWidget::GlobalSearchWidget(QWidget* parent)
connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap))); connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap)));
connect(engine_, SIGNAL(TracksLoaded(int,MimeData*)), SLOT(TracksLoaded(int,MimeData*))); connect(engine_, SIGNAL(TracksLoaded(int,MimeData*)), SLOT(TracksLoaded(int,MimeData*)));
connect(view_, SIGNAL(doubleClicked(QModelIndex)), SLOT(AddCurrent())); connect(view_, SIGNAL(doubleClicked(QModelIndex)), SLOT(AddCurrent()));
connect(view_->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
SLOT(UpdateTooltip()));
} }
GlobalSearchWidget::~GlobalSearchWidget() { GlobalSearchWidget::~GlobalSearchWidget() {
@ -243,7 +246,7 @@ void GlobalSearchWidget::AddResults(int id, const SearchProvider::ResultList& re
void GlobalSearchWidget::RepositionPopup() { void GlobalSearchWidget::RepositionPopup() {
if (model_->rowCount() == 0) { if (model_->rowCount() == 0) {
view_->hide(); HidePopup();
return; return;
} }
@ -346,7 +349,7 @@ bool GlobalSearchWidget::EventFilterPopup(QObject*, QEvent* e) {
if (e->isAccepted() || !view_->isVisible()) { if (e->isAccepted() || !view_->isVisible()) {
// widget lost focus, hide the popup // widget lost focus, hide the popup
if (!ui_->search->hasFocus()) if (!ui_->search->hasFocus())
view_->hide(); HidePopup();
if (e->isAccepted()) if (e->isAccepted())
return true; return true;
} }
@ -356,18 +359,18 @@ bool GlobalSearchWidget::EventFilterPopup(QObject*, QEvent* e) {
case Qt::Key_Return: case Qt::Key_Return:
case Qt::Key_Enter: case Qt::Key_Enter:
case Qt::Key_Tab: case Qt::Key_Tab:
view_->hide(); HidePopup();
AddCurrent(); AddCurrent();
break; break;
case Qt::Key_F4: case Qt::Key_F4:
if (ke->modifiers() & Qt::AltModifier) if (ke->modifiers() & Qt::AltModifier)
view_->hide(); HidePopup();
break; break;
case Qt::Key_Backtab: case Qt::Key_Backtab:
case Qt::Key_Escape: case Qt::Key_Escape:
view_->hide(); HidePopup();
break; break;
default: default:
@ -379,7 +382,7 @@ bool GlobalSearchWidget::EventFilterPopup(QObject*, QEvent* e) {
case QEvent::MouseButtonPress: case QEvent::MouseButtonPress:
if (!view_->underMouse()) { if (!view_->underMouse()) {
view_->hide(); HidePopup();
return true; return true;
} }
return false; return false;
@ -501,3 +504,32 @@ void GlobalSearchWidget::CombineResults(const QModelIndex& superior, const QMode
model_->invisibleRootItem()->removeRow(inferior_item->row()); model_->invisibleRootItem()->removeRow(inferior_item->row());
} }
void GlobalSearchWidget::HidePopup() {
if (tooltip_)
tooltip_->hide();
view_->hide();
}
void GlobalSearchWidget::UpdateTooltip() {
if (!view_->isVisible()) {
if (tooltip_)
tooltip_->hide();
return;
}
const QModelIndex current = view_->selectionModel()->currentIndex();
if (!current.isValid())
return;
const SearchProvider::ResultList results = current.data(Role_AllResults)
.value<SearchProvider::ResultList>();
if (!tooltip_) {
tooltip_.reset(new GlobalSearchTooltip(view_));
tooltip_->setFont(view_->font());
tooltip_->setPalette(view_->palette());
}
tooltip_->SetResults(results);
tooltip_->ShowAt(view_->mapToGlobal(view_->visualRect(current).topRight()));
}

View File

@ -20,9 +20,11 @@
#include "searchprovider.h" #include "searchprovider.h"
#include <QScopedPointer>
#include <QWidget> #include <QWidget>
class GlobalSearch; class GlobalSearch;
class GlobalSearchTooltip;
class LibraryBackendInterface; class LibraryBackendInterface;
class Ui_GlobalSearchWidget; class Ui_GlobalSearchWidget;
@ -81,6 +83,9 @@ private slots:
void AddCurrent(); void AddCurrent();
void HidePopup();
void UpdateTooltip();
private: private:
// Return values from CanCombineResults // Return values from CanCombineResults
enum CombineAction { enum CombineAction {
@ -118,6 +123,8 @@ private:
bool combine_identical_results_; bool combine_identical_results_;
QStringList provider_order_; QStringList provider_order_;
QScopedPointer<GlobalSearchTooltip> tooltip_;
}; };
#endif // GLOBALSEARCHWIDGET_H #endif // GLOBALSEARCHWIDGET_H

View File

@ -104,6 +104,10 @@ SearchProvider::ResultList LibrarySearchProvider::Search(int id, const QString&
MatchQuality(tokens, result.metadata_.albumartist()), MatchQuality(tokens, result.metadata_.albumartist()),
qMin(MatchQuality(tokens, result.metadata_.artist()), qMin(MatchQuality(tokens, result.metadata_.artist()),
MatchQuality(tokens, result.metadata_.album()))); MatchQuality(tokens, result.metadata_.album())));
result.album_songs_ = albums.values(key);
SortSongs(&result.album_songs_);
ret << result; ret << result;
} }
@ -135,7 +139,6 @@ void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) {
case Result::Type_Album: { case Result::Type_Album: {
// Find all the songs in this album. // Find all the songs in this album.
LibraryQuery query; LibraryQuery query;
query.SetOrderBy("track");
query.SetColumnSpec("ROWID, " + Song::kColumnSpec); query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddCompilationRequirement(result.metadata_.is_compilation()); query.AddCompilationRequirement(result.metadata_.is_compilation());
query.AddWhere("album", result.metadata_.album()); query.AddWhere("album", result.metadata_.album());
@ -155,6 +158,8 @@ void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) {
} }
} }
SortSongs(&ret);
SongMimeData* mime_data = new SongMimeData; SongMimeData* mime_data = new SongMimeData;
mime_data->backend = backend_; mime_data->backend = backend_;
mime_data->songs = ret; mime_data->songs = ret;

View File

@ -122,3 +122,18 @@ QImage SearchProvider::ScaleAndPad(const QImage& image) {
return padded_image; return padded_image;
} }
namespace {
bool SortSongsCompare(const Song& left, const Song& right) {
if (left.disc() < right.disc())
return true;
if (left.disc() > right.disc())
return false;
return left.track() < right.track();
}
}
void SearchProvider::SortSongs(SongList* list) {
qStableSort(list->begin(), list->end(), SortSongsCompare);
}

View File

@ -67,6 +67,10 @@ public:
// How many songs in the album - valid only if type == Type_Album. // How many songs in the album - valid only if type == Type_Album.
int album_size_; int album_size_;
// Songs in the album - valid only if type == Type_Album. This is only
// used for display in the tooltip, so it's fine not to provide it.
SongList album_songs_;
QString pixmap_cache_key_; QString pixmap_cache_key_;
}; };
typedef QList<Result> ResultList; typedef QList<Result> ResultList;
@ -106,6 +110,9 @@ protected:
static QStringList TokenizeQuery(const QString& query); static QStringList TokenizeQuery(const QString& query);
static Result::MatchQuality MatchQuality(const QStringList& tokens, const QString& string); static Result::MatchQuality MatchQuality(const QStringList& tokens, const QString& string);
// Sorts a list of songs by disc, then by track.
static void SortSongs(SongList* list);
// Subclasses must call this from their constructor // Subclasses must call this from their constructor
void Init(const QString& name, const QString& id, const QIcon& icon, void Init(const QString& name, const QString& id, const QIcon& icon,
bool delay_searches, bool serialised_art); bool delay_searches, bool serialised_art);

View File

@ -95,15 +95,22 @@ void SpotifySearchProvider::SearchFinishedSlot(const spotify_pb::SearchResponse&
} }
for (int i=0 ; i<response.album_size() ; ++i) { for (int i=0 ; i<response.album_size() ; ++i) {
const spotify_pb::Track& track = response.album(i); const spotify_pb::Album& album = response.album(i);
Result result(this); Result result(this);
result.type_ = Result::Type_Album; result.type_ = Result::Type_Album;
SpotifyService::SongFromProtobuf(track, &result.metadata_); SpotifyService::SongFromProtobuf(album.metadata(), &result.metadata_);
result.match_quality_ = result.match_quality_ =
qMin(MatchQuality(state.tokens_, result.metadata_.album()), qMin(MatchQuality(state.tokens_, result.metadata_.album()),
MatchQuality(state.tokens_, result.metadata_.artist())); MatchQuality(state.tokens_, result.metadata_.artist()));
result.album_size_ = track.track(); result.album_size_ = album.metadata().track();
for (int j=0; j < album.track_size() ; ++j) {
Song track_song;
SpotifyService::SongFromProtobuf(album.track(j), &track_song);
result.album_songs_ << track_song;
}
ret << result; ret << result;
} }

View File

@ -0,0 +1,145 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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 3 of the License, or
(at your option) any later version.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "tooltipresultwidget.h"
#include "core/logging.h"
#include <QPainter>
const int TooltipResultWidget::kBorder = 6;
const int TooltipResultWidget::kSpacing = 3;
const int TooltipResultWidget::kTrackNoSpacing = 6;
const int TooltipResultWidget::kLineHeight = 1;
const int TooltipResultWidget::kIconSize = 16;
TooltipResultWidget::TooltipResultWidget(const SearchProvider::Result& result,
QWidget* parent)
: QWidget(parent),
result_(result),
kTextHeight(fontMetrics().height()),
kTrackNoWidth(fontMetrics().width("0000")),
bold_metrics_(fontMetrics())
{
bold_font_ = font();
bold_font_.setBold(true);
bold_metrics_ = QFontMetrics(bold_font_);
size_hint_ = CalculateSizeHint();
}
QSize TooltipResultWidget::sizeHint() const {
return size_hint_;
}
QSize TooltipResultWidget::CalculateSizeHint() const {
int w = 0;
int h = 0;
// Title text
h += kBorder + kIconSize + kBorder + kLineHeight;
w = qMax(w, kBorder + kIconSize + kBorder + bold_metrics_.width(TitleText()) + kBorder);
switch (result_.type_) {
case SearchProvider::Result::Type_Track:
break;
case SearchProvider::Result::Type_Album:
if (result_.album_songs_.isEmpty())
break;
// Song list
h += kBorder + kSpacing * (result_.album_songs_.count() - 1) +
kTextHeight * result_.album_songs_.count();
foreach (const Song& song, result_.album_songs_) {
w = qMax(w, kTrackNoWidth + kTrackNoSpacing +
fontMetrics().width(song.TitleWithCompilationArtist()) +
kBorder);
}
h += kBorder + kLineHeight;
break;
}
return QSize(w, h);
}
QString TooltipResultWidget::TitleText() const {
return result_.provider_->name();
}
void TooltipResultWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setPen(palette().color(QPalette::Text));
int y = kBorder;
// Title icon
QRect icon_rect(kBorder, y, kIconSize, kIconSize);
p.drawPixmap(icon_rect, result_.provider_->icon().pixmap(kIconSize));
// Title text
QRect text_rect(icon_rect.right() + kBorder, y,
width() - kBorder*2 - icon_rect.right(), kIconSize);
p.setFont(bold_font_);
p.drawText(text_rect, Qt::AlignVCenter, TitleText());
// Line
y += kIconSize + kBorder;
p.drawLine(0, y, width(), y);
y += kLineHeight;
switch (result_.type_) {
case SearchProvider::Result::Type_Track:
break;
case SearchProvider::Result::Type_Album:
if (result_.album_songs_.isEmpty())
break;
// Song list
y += kBorder;
p.setFont(font());
foreach (const Song& song, result_.album_songs_) {
QRect number_rect(0, y, kTrackNoWidth, kTextHeight);
if (song.track() > 0) {
// Track number
p.drawText(number_rect, Qt::AlignRight | Qt::AlignHCenter,
QString::number(song.track()));
}
// Song title
QRect title_rect(number_rect.right() + kTrackNoSpacing, y,
width() - number_rect.right() - kTrackNoSpacing - kBorder,
kTextHeight);
p.drawText(title_rect, song.TitleWithCompilationArtist());
y += kTextHeight + kSpacing;
}
y -= kSpacing;
y += kBorder;
// Line
p.drawLine(0, y, width(), y);
y += kLineHeight;
break;
}
}

View File

@ -0,0 +1,58 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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 3 of the License, or
(at your option) any later version.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TOOLTIPRESULTWIDGET_H
#define TOOLTIPRESULTWIDGET_H
#include "searchprovider.h"
#include <QWidget>
class TooltipResultWidget : public QWidget {
Q_OBJECT
public:
TooltipResultWidget(const SearchProvider::Result& result, QWidget* parent = 0);
static const int kBorder;
static const int kSpacing;
static const int kTrackNoSpacing;
static const int kLineHeight;
static const int kIconSize;
QSize sizeHint() const;
QString TitleText() const;
protected:
void paintEvent(QPaintEvent*);
private:
QSize CalculateSizeHint() const;
private:
SearchProvider::Result result_;
const int kTextHeight;
const int kTrackNoWidth;
QSize size_hint_;
QFont bold_font_;
QFontMetrics bold_metrics_;
};
#endif // TOOLTIPRESULTWIDGET_H