Add global search suggestions

This commit is contained in:
David Sansome 2012-06-10 23:05:30 +01:00
parent fe4e214a78
commit 9d6b72b4ab
21 changed files with 395 additions and 113 deletions

View File

@ -151,6 +151,7 @@ set(SOURCES
globalsearch/searchproviderstatuswidget.cpp globalsearch/searchproviderstatuswidget.cpp
globalsearch/simplesearchprovider.cpp globalsearch/simplesearchprovider.cpp
globalsearch/somafmsearchprovider.cpp globalsearch/somafmsearchprovider.cpp
globalsearch/suggestionwidget.cpp
globalsearch/urlsearchprovider.cpp globalsearch/urlsearchprovider.cpp
internet/digitallyimportedclient.cpp internet/digitallyimportedclient.cpp
@ -421,6 +422,7 @@ set(HEADERS
globalsearch/groovesharksearchprovider.h globalsearch/groovesharksearchprovider.h
globalsearch/searchprovider.h globalsearch/searchprovider.h
globalsearch/simplesearchprovider.h globalsearch/simplesearchprovider.h
globalsearch/suggestionwidget.h
internet/digitallyimportedclient.h internet/digitallyimportedclient.h
internet/digitallyimportedservicebase.h internet/digitallyimportedservicebase.h
@ -608,6 +610,7 @@ set(UI
globalsearch/globalsearchsettingspage.ui globalsearch/globalsearchsettingspage.ui
globalsearch/globalsearchview.ui globalsearch/globalsearchview.ui
globalsearch/searchproviderstatuswidget.ui globalsearch/searchproviderstatuswidget.ui
globalsearch/suggestionwidget.ui
internet/digitallyimportedsettingspage.ui internet/digitallyimportedsettingspage.ui
internet/groovesharksettingspage.ui internet/groovesharksettingspage.ui

View File

@ -29,6 +29,7 @@ DigitallyImportedSearchProvider::DigitallyImportedSearchProvider(
set_safe_words(QStringList() << "sky.fm" << "skyfm" << "di.fm" << "difm" set_safe_words(QStringList() << "sky.fm" << "skyfm" << "di.fm" << "difm"
<< "digitallyimported"); << "digitallyimported");
set_max_suggestion_count(5);
connect(service_, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems())); connect(service_, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems()));

View File

@ -27,6 +27,8 @@
#include <QTimerEvent> #include <QTimerEvent>
#include <QUrl> #include <QUrl>
#include <algorithm>
const int GlobalSearch::kDelayedSearchTimeoutMs = 200; const int GlobalSearch::kDelayedSearchTimeoutMs = 200;
const char* GlobalSearch::kSettingsGroup = "GlobalSearch"; const char* GlobalSearch::kSettingsGroup = "GlobalSearch";
const int GlobalSearch::kMaxResultsPerEmission = 100; const int GlobalSearch::kMaxResultsPerEmission = 100;
@ -357,22 +359,28 @@ void GlobalSearch::SaveProvidersSettings() {
} }
} }
QStringList GlobalSearch::GetSuggestions(int max) { QStringList GlobalSearch::GetSuggestions(int count) {
QStringList ret; QStringList ret;
QList<SearchProvider*> eligible_providers;
// Get count suggestions from each provider
foreach (SearchProvider* provider, providers_.keys()) { foreach (SearchProvider* provider, providers_.keys()) {
if (is_provider_enabled(provider) && provider->can_give_suggestions()) { if (is_provider_enabled(provider) && provider->can_give_suggestions()) {
eligible_providers << provider; foreach (QString suggestion, provider->GetSuggestions(count)) {
suggestion = suggestion.trimmed().toLower();
if (!suggestion.isEmpty()) {
ret << suggestion;
}
}
} }
} }
while (ret.count() < max && !eligible_providers.isEmpty()) { // Randomize the suggestions
SearchProvider* provider = eligible_providers.takeAt(qrand() % eligible_providers.count()); std::random_shuffle(ret.begin(), ret.end());
QString suggestion = provider->GetSuggestion().trimmed();
if (!suggestion.isEmpty())
ret << suggestion;
}
// Only return the first count
while (ret.length() > count) {
ret.removeLast();
}
return ret; return ret;
} }

View File

@ -48,7 +48,7 @@ public:
int SearchAsync(const QString& query); int SearchAsync(const QString& query);
int LoadArtAsync(const SearchProvider::Result& result); int LoadArtAsync(const SearchProvider::Result& result);
MimeData* LoadTracks(const SearchProvider::ResultList& results); MimeData* LoadTracks(const SearchProvider::ResultList& results);
QStringList GetSuggestions(int max); QStringList GetSuggestions(int count);
void CancelSearch(int id); void CancelSearch(int id);
void CancelArt(int id); void CancelArt(int id);

View File

@ -22,10 +22,12 @@
#include "globalsearchview.h" #include "globalsearchview.h"
#include "searchprovider.h" #include "searchprovider.h"
#include "searchproviderstatuswidget.h" #include "searchproviderstatuswidget.h"
#include "suggestionwidget.h"
#include "ui_globalsearchview.h" #include "ui_globalsearchview.h"
#include "core/application.h" #include "core/application.h"
#include "core/logging.h" #include "core/logging.h"
#include "core/mimedata.h" #include "core/mimedata.h"
#include "core/timeconstants.h"
#include "library/librarymodel.h" #include "library/librarymodel.h"
#include <QMenu> #include <QMenu>
@ -34,6 +36,8 @@
#include <QTimer> #include <QTimer>
const int GlobalSearchView::kSwapModelsTimeoutMsec = 250; const int GlobalSearchView::kSwapModelsTimeoutMsec = 250;
const int GlobalSearchView::kMaxSuggestions = 10;
const int GlobalSearchView::kUpdateSuggestionsTimeoutMsec = 60 * kMsecPerSec;
GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent) GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
: QWidget(parent), : QWidget(parent),
@ -48,7 +52,10 @@ GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
front_proxy_(new GlobalSearchSortModel(this)), front_proxy_(new GlobalSearchSortModel(this)),
back_proxy_(new GlobalSearchSortModel(this)), back_proxy_(new GlobalSearchSortModel(this)),
current_proxy_(front_proxy_), current_proxy_(front_proxy_),
swap_models_timer_(new QTimer(this)) swap_models_timer_(new QTimer(this)),
update_suggestions_timer_(new QTimer(this)),
search_icon_(IconLoader::Load("search")),
warning_icon_(IconLoader::Load("dialog-warning"))
{ {
ui_->setupUi(this); ui_->setupUi(this);
@ -71,8 +78,10 @@ GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
ui_->help_frame->setBackgroundRole(QPalette::Base); ui_->help_frame->setBackgroundRole(QPalette::Base);
QVBoxLayout* enabled_layout = new QVBoxLayout(ui_->enabled_list); QVBoxLayout* enabled_layout = new QVBoxLayout(ui_->enabled_list);
QVBoxLayout* disabled_layout = new QVBoxLayout(ui_->disabled_list); QVBoxLayout* disabled_layout = new QVBoxLayout(ui_->disabled_list);
QVBoxLayout* suggestions_layout = new QVBoxLayout(ui_->suggestions_list);
enabled_layout->setContentsMargins(16, 0, 16, 6); enabled_layout->setContentsMargins(16, 0, 16, 6);
disabled_layout->setContentsMargins(16, 0, 16, 6); disabled_layout->setContentsMargins(16, 0, 16, 32);
suggestions_layout->setContentsMargins(16, 0, 16, 6);
// Set the colour of the help text to the disabled text colour // Set the colour of the help text to the disabled text colour
QPalette help_palette = ui_->help_text->palette(); QPalette help_palette = ui_->help_text->palette();
@ -81,6 +90,14 @@ GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
help_palette.setColor(QPalette::Inactive, QPalette::Text, help_color); help_palette.setColor(QPalette::Inactive, QPalette::Text, help_color);
ui_->help_text->setPalette(help_palette); ui_->help_text->setPalette(help_palette);
// Create suggestion widgets
for (int i=0 ; i<kMaxSuggestions ; ++i) {
SuggestionWidget* widget = new SuggestionWidget(search_icon_);
connect(widget, SIGNAL(SuggestionClicked(QString)), SLOT(StartSearch(QString)));
suggestions_layout->addWidget(widget);
suggestion_widgets_ << widget;
}
// Make it bold // Make it bold
QFont help_font = ui_->help_text->font(); QFont help_font = ui_->help_text->font();
help_font.setBold(true); help_font.setBold(true);
@ -99,6 +116,9 @@ GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
swap_models_timer_->setInterval(kSwapModelsTimeoutMsec); swap_models_timer_->setInterval(kSwapModelsTimeoutMsec);
connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels())); connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels()));
update_suggestions_timer_->setInterval(kUpdateSuggestionsTimeoutMsec);
connect(update_suggestions_timer_, SIGNAL(timeout()), SLOT(UpdateSuggestions()));
// These have to be queued connections because they may get emitted before // These have to be queued connections because they may get emitted before
// our call to Search() (or whatever) returns and we add the ID to the map. // our call to Search() (or whatever) returns and we add the ID to the map.
connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)), connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)),
@ -106,8 +126,6 @@ GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
Qt::QueuedConnection); Qt::QueuedConnection);
connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap)), connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap)),
Qt::QueuedConnection); Qt::QueuedConnection);
ReloadSettings();
} }
GlobalSearchView::~GlobalSearchView() { GlobalSearchView::~GlobalSearchView() {
@ -139,7 +157,7 @@ void GlobalSearchView::ReloadSettings() {
} }
SearchProviderStatusWidget* widget = SearchProviderStatusWidget* widget =
new SearchProviderStatusWidget(engine_, provider); new SearchProviderStatusWidget(warning_icon_, engine_, provider);
parent->layout()->addWidget(widget); parent->layout()->addWidget(widget);
provider_status_widgets_ << widget; provider_status_widgets_ << widget;
@ -155,6 +173,19 @@ void GlobalSearchView::ReloadSettings() {
back_model_->set_use_pretty_covers(pretty); back_model_->set_use_pretty_covers(pretty);
} }
void GlobalSearchView::UpdateSuggestions() {
const QStringList suggestions = engine_->GetSuggestions(kMaxSuggestions);
for (int i=0 ; i<suggestions.count() ; ++i) {
suggestion_widgets_[i]->SetText(suggestions[i]);
suggestion_widgets_[i]->show();
}
for (int i=suggestions.count() ; i<kMaxSuggestions ; ++i) {
suggestion_widgets_[i]->hide();
}
}
void GlobalSearchView::StartSearch(const QString& query) { void GlobalSearchView::StartSearch(const QString& query) {
ui_->search->set_text(query); ui_->search->set_text(query);
TextEdited(query); TextEdited(query);
@ -365,3 +396,14 @@ void GlobalSearchView::OpenSelectedInNewPlaylist() {
data->open_in_new_playlist_ = true; data->open_in_new_playlist_ = true;
emit AddToPlaylist(data); emit AddToPlaylist(data);
} }
void GlobalSearchView::showEvent(QShowEvent* e) {
UpdateSuggestions();
update_suggestions_timer_->start();
QWidget::showEvent(e);
}
void GlobalSearchView::hideEvent(QHideEvent* e) {
update_suggestions_timer_->stop();
QWidget::hideEvent(e);
}

View File

@ -27,6 +27,7 @@
class Application; class Application;
class GlobalSearchModel; class GlobalSearchModel;
class SearchProviderStatusWidget; class SearchProviderStatusWidget;
class SuggestionWidget;
class Ui_GlobalSearchView; class Ui_GlobalSearchView;
class QMimeData; class QMimeData;
@ -42,14 +43,21 @@ public:
~GlobalSearchView(); ~GlobalSearchView();
static const int kSwapModelsTimeoutMsec; static const int kSwapModelsTimeoutMsec;
static const int kMaxSuggestions;
static const int kUpdateSuggestionsTimeoutMsec;
// Called by the delegate // Called by the delegate
void LazyLoadArt(const QModelIndex& index); void LazyLoadArt(const QModelIndex& index);
// QWidget
void showEvent(QShowEvent* e);
void hideEvent(QHideEvent* e);
// QObject // QObject
bool eventFilter(QObject* object, QEvent* event); bool eventFilter(QObject* object, QEvent* event);
public slots: public slots:
void ReloadSettings();
void StartSearch(const QString& query); void StartSearch(const QString& query);
signals: signals:
@ -57,7 +65,7 @@ signals:
void OpenSettingsAtPage(SettingsDialog::Page page); void OpenSettingsAtPage(SettingsDialog::Page page);
private slots: private slots:
void ReloadSettings(); void UpdateSuggestions();
void SwapModels(); void SwapModels();
void TextEdited(const QString& text); void TextEdited(const QString& text);
@ -99,8 +107,13 @@ private:
QMap<int, QModelIndex> art_requests_; QMap<int, QModelIndex> art_requests_;
QTimer* swap_models_timer_; QTimer* swap_models_timer_;
QTimer* update_suggestions_timer_;
QList<SearchProviderStatusWidget*> provider_status_widgets_; QList<SearchProviderStatusWidget*> provider_status_widgets_;
QList<SuggestionWidget*> suggestion_widgets_;
QIcon search_icon_;
QIcon warning_icon_;
}; };
#endif // GLOBALSEARCHVIEW_H #endif // GLOBALSEARCHVIEW_H

View File

@ -67,85 +67,102 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QFrame" name="help_frame"> <widget class="QScrollArea" name="help_frame">
<property name="autoFillBackground"> <property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool> <bool>true</bool>
</property> </property>
<property name="frameShape"> <widget class="QWidget" name="help_frame_contents">
<enum>QFrame::StyledPanel</enum> <property name="geometry">
</property> <rect>
<property name="frameShadow"> <x>0</x>
<enum>QFrame::Sunken</enum> <y>0</y>
</property> <width>435</width>
<layout class="QVBoxLayout" name="verticalLayout_2"> <height>605</height>
<item> </rect>
<widget class="QWidget" name="widget" native="true"> </property>
<layout class="QVBoxLayout" name="verticalLayout_4"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin"> <item>
<number>32</number> <widget class="QWidget" name="widget" native="true">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>32</number>
</property>
<property name="topMargin">
<number>16</number>
</property>
<property name="rightMargin">
<number>32</number>
</property>
<property name="bottomMargin">
<number>64</number>
</property>
<item>
<widget class="QLabel" name="help_text">
<property name="text">
<string>Enter search terms above to find music on your computer and on the internet</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Clementine will find music in:</string>
</property> </property>
<property name="topMargin"> <property name="wordWrap">
<number>16</number> <bool>true</bool>
</property> </property>
<property name="rightMargin"> </widget>
<number>32</number> </item>
<item>
<widget class="QWidget" name="enabled_list" native="true"/>
</item>
<item>
<widget class="QLabel" name="disabled_label">
<property name="text">
<string>But these sources are disabled:</string>
</property> </property>
<property name="bottomMargin"> </widget>
<number>64</number> </item>
<item>
<widget class="QWidget" name="disabled_list" native="true"/>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Why not try...</string>
</property> </property>
<item> </widget>
<widget class="QLabel" name="help_text"> </item>
<property name="text"> <item>
<string>Enter search terms above to find music on your computer and on the internet</string> <widget class="QWidget" name="suggestions_list" native="true"/>
</property> </item>
<property name="alignment"> <item>
<set>Qt::AlignCenter</set> <spacer name="verticalSpacer">
</property> <property name="orientation">
<property name="wordWrap"> <enum>Qt::Vertical</enum>
<bool>true</bool> </property>
</property> <property name="sizeHint" stdset="0">
</widget> <size>
</item> <width>20</width>
</layout> <height>40</height>
</widget> </size>
</item> </property>
<item> </spacer>
<widget class="QLabel" name="label_2"> </item>
<property name="text"> </layout>
<string>Clementine will find music in:</string> </widget>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="enabled_list" native="true"/>
</item>
<item>
<widget class="QLabel" name="disabled_label">
<property name="text">
<string>But these sources are disabled:</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="disabled_list" native="true"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget> </widget>
</item> </item>
</layout> </layout>

View File

@ -29,6 +29,7 @@ LastFMSearchProvider::LastFMSearchProvider(LastFMService* service,
icon_ = ScaleAndPad(QImage(":last.fm/as.png")); icon_ = ScaleAndPad(QImage(":last.fm/as.png"));
set_safe_words(QStringList() << "lastfm" << "last.fm"); set_safe_words(QStringList() << "lastfm" << "last.fm");
set_max_suggestion_count(3);
connect(service, SIGNAL(SavedItemsChanged()), SLOT(MaybeRecreateItems())); connect(service, SIGNAL(SavedItemsChanged()), SLOT(MaybeRecreateItems()));

View File

@ -75,7 +75,7 @@ MimeData* LibrarySearchProvider::LoadTracks(const ResultList& results) {
return ret; return ret;
} }
QString LibrarySearchProvider::GetSuggestion() { QStringList LibrarySearchProvider::GetSuggestions(int count) {
// We'd like to use order by random(), but that's O(n) in sqlite, so instead // We'd like to use order by random(), but that's O(n) in sqlite, so instead
// get the largest ROWID and pick a couple of random numbers within that // get the largest ROWID and pick a couple of random numbers within that
// range. // range.
@ -86,13 +86,19 @@ QString LibrarySearchProvider::GetSuggestion() {
q.SetIncludeUnavailable(true); q.SetIncludeUnavailable(true);
q.SetLimit(1); q.SetLimit(1);
QStringList ret;
if (!backend_->ExecQuery(&q) || !q.Next()) { if (!backend_->ExecQuery(&q) || !q.Next()) {
return QString(); return ret;
} }
const int largest_rowid = q.Value(0).toInt(); const int largest_rowid = q.Value(0).toInt();
for (int attempt=0 ; attempt<10 ; ++attempt) { for (int attempt=0 ; attempt<count*5 ; ++attempt) {
if (ret.count() >= count) {
break;
}
LibraryQuery q; LibraryQuery q;
q.SetColumnSpec("artist, album"); q.SetColumnSpec("artist, album");
q.SetIncludeUnavailable(true); q.SetIncludeUnavailable(true);
@ -107,12 +113,12 @@ QString LibrarySearchProvider::GetSuggestion() {
const QString album = q.Value(1).toString(); const QString album = q.Value(1).toString();
if (!artist.isEmpty() && !album.isEmpty()) if (!artist.isEmpty() && !album.isEmpty())
return (qrand() % 2 == 0) ? artist : album; ret << ((qrand() % 2 == 0) ? artist : album);
else if (!artist.isEmpty()) else if (!artist.isEmpty())
return artist; ret << artist;
else if (!album.isEmpty()) else if (!album.isEmpty())
return album; ret << album;
} }
return QString(); return ret;
} }

View File

@ -32,7 +32,7 @@ public:
ResultList Search(int id, const QString& query); ResultList Search(int id, const QString& query);
MimeData* LoadTracks(const ResultList& results); MimeData* LoadTracks(const ResultList& results);
QString GetSuggestion(); QStringList GetSuggestions(int count);
private: private:
LibraryBackendInterface* backend_; LibraryBackendInterface* backend_;

View File

@ -28,6 +28,8 @@ SavedRadioSearchProvider::SavedRadioSearchProvider(SavedRadio* service,
Init(tr("Your radio streams"), "savedradio", IconLoader::Load("document-open-remote"), Init(tr("Your radio streams"), "savedradio", IconLoader::Load("document-open-remote"),
MimeDataContainsUrlsOnly); MimeDataContainsUrlsOnly);
set_max_suggestion_count(3);
connect(service_, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems())); connect(service_, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems()));
RecreateItems(); RecreateItems();

View File

@ -129,10 +129,10 @@ public:
// Result, unless the MimeDataContainsUrlsOnly flag is set. // Result, unless the MimeDataContainsUrlsOnly flag is set.
virtual MimeData* LoadTracks(const ResultList& results); virtual MimeData* LoadTracks(const ResultList& results);
// Returns an example search string to display in the UI. The provider should // Returns some example search strings to display in the UI. The provider
// pick one of its items at random. Remember to set the CanGiveSuggestions // should pick some of its items at random and return between 0 and count
// hint. // strings. Remember to set the CanGiveSuggestions hint.
virtual QString GetSuggestion() { return QString(); } virtual QStringList GetSuggestions(int count) { return QStringList(); }
// If provider needs user login to search and play songs, this method should // If provider needs user login to search and play songs, this method should
// be reimplemented // be reimplemented

View File

@ -24,9 +24,10 @@
#include <QMouseEvent> #include <QMouseEvent>
SearchProviderStatusWidget::SearchProviderStatusWidget(GlobalSearch* engine, SearchProviderStatusWidget::SearchProviderStatusWidget(
SearchProvider* provider, const QIcon& warning_icon,
QWidget* parent) GlobalSearch* engine, SearchProvider* provider,
QWidget* parent)
: QWidget(parent), : QWidget(parent),
ui_(new Ui_SearchProviderStatusWidget), ui_(new Ui_SearchProviderStatusWidget),
engine_(engine), engine_(engine),
@ -51,7 +52,7 @@ SearchProviderStatusWidget::SearchProviderStatusWidget(GlobalSearch* engine,
ui_->disabled_reason->setMinimumWidth(disabled_width); ui_->disabled_reason->setMinimumWidth(disabled_width);
ui_->disabled_reason->setText(logged_in ? disabled_text : not_logged_in_text); ui_->disabled_reason->setText(logged_in ? disabled_text : not_logged_in_text);
ui_->disabled_icon->setPixmap(IconLoader::Load("dialog-warning").pixmap(16)); ui_->disabled_icon->setPixmap(warning_icon.pixmap(16));
ui_->disabled_reason->installEventFilter(this); ui_->disabled_reason->installEventFilter(this);
} }

View File

@ -26,7 +26,8 @@ class Ui_SearchProviderStatusWidget;
class SearchProviderStatusWidget : public QWidget { class SearchProviderStatusWidget : public QWidget {
public: public:
SearchProviderStatusWidget(GlobalSearch* engine, SearchProvider* provider, SearchProviderStatusWidget(const QIcon& warning_icon,
GlobalSearch* engine, SearchProvider* provider,
QWidget* parent = 0); QWidget* parent = 0);
~SearchProviderStatusWidget(); ~SearchProviderStatusWidget();

View File

@ -39,6 +39,7 @@ SimpleSearchProvider::Item::Item(const Song& song, const QString& keyword)
SimpleSearchProvider::SimpleSearchProvider(Application* app, QObject* parent) SimpleSearchProvider::SimpleSearchProvider(Application* app, QObject* parent)
: BlockingSearchProvider(app, parent), : BlockingSearchProvider(app, parent),
result_limit_(kDefaultResultLimit), result_limit_(kDefaultResultLimit),
max_suggestion_count_(-1),
items_dirty_(true), items_dirty_(true),
has_searched_before_(false) has_searched_before_(false)
{ {
@ -99,19 +100,28 @@ void SimpleSearchProvider::SetItems(const ItemList& items) {
} }
} }
QString SimpleSearchProvider::GetSuggestion() { QStringList SimpleSearchProvider::GetSuggestions(int count) {
if (max_suggestion_count_ != -1) {
count = qMin(max_suggestion_count_, count);
}
QStringList ret;
QMutexLocker l(&items_mutex_); QMutexLocker l(&items_mutex_);
if (items_.isEmpty()) if (items_.isEmpty())
return QString(); return ret;
for (int attempt=0 ; attempt<count*5 ; ++attempt) {
if (ret.count() >= count) {
break;
}
for (int attempt=0 ; attempt<10 ; ++attempt) {
const Item& item = items_[qrand() % items_.count()]; const Item& item = items_[qrand() % items_.count()];
if (!item.keyword_.isEmpty()) if (!item.keyword_.isEmpty())
return item.keyword_; ret << item.keyword_;
if (!item.metadata_.title().isEmpty()) if (!item.metadata_.title().isEmpty())
return item.metadata_.title(); ret << item.metadata_.title();
} }
return QString(); return ret;
} }

View File

@ -32,7 +32,7 @@ public:
ResultList Search(int id, const QString& query); ResultList Search(int id, const QString& query);
// SearchProvider // SearchProvider
QString GetSuggestion(); QStringList GetSuggestions(int count);
protected slots: protected slots:
// Calls RecreateItems now if the user has done a global search with this // Calls RecreateItems now if the user has done a global search with this
@ -54,6 +54,7 @@ protected:
int result_limit() const { return result_limit_; } int result_limit() const { return result_limit_; }
void set_result_limit(int result_limit) { result_limit_ = result_limit; } void set_result_limit(int result_limit) { result_limit_ = result_limit; }
void set_max_suggestion_count(int count) { max_suggestion_count_ = count; }
QStringList safe_words() const { return safe_words_; } QStringList safe_words() const { return safe_words_; }
void set_safe_words(const QStringList& safe_words) { safe_words_ = safe_words; } void set_safe_words(const QStringList& safe_words) { safe_words_ = safe_words; }
@ -66,6 +67,7 @@ protected:
private: private:
int result_limit_; int result_limit_;
QStringList safe_words_; QStringList safe_words_;
int max_suggestion_count_;
QMutex items_mutex_; QMutex items_mutex_;
ItemList items_; ItemList items_;

View File

@ -24,6 +24,7 @@ SomaFMSearchProvider::SomaFMSearchProvider(SomaFMService* service, Application*
{ {
Init("SomaFM", "somafm", QIcon(":/providers/somafm.png"), CanGiveSuggestions); Init("SomaFM", "somafm", QIcon(":/providers/somafm.png"), CanGiveSuggestions);
set_result_limit(3); set_result_limit(3);
set_max_suggestion_count(3);
icon_ = ScaleAndPad(QImage(":/providers/somafm.png")); icon_ = ScaleAndPad(QImage(":/providers/somafm.png"));
connect(service, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems())); connect(service, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems()));

View File

@ -0,0 +1,76 @@
/* This file is part of Clementine.
Copyright 2012, 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 "suggestionwidget.h"
#include "ui_suggestionwidget.h"
#include "ui/iconloader.h"
#include <QMouseEvent>
SuggestionWidget::SuggestionWidget(const QIcon& search_icon, QWidget* parent)
: QWidget(parent),
ui_(new Ui_SuggestionWidget)
{
ui_->setupUi(this);
ui_->icon->setPixmap(search_icon.pixmap(16));
ui_->name->installEventFilter(this);
}
SuggestionWidget::~SuggestionWidget() {
delete ui_;
}
void SuggestionWidget::SetText(const QString& text) {
ui_->name->setText(text);
}
bool SuggestionWidget::eventFilter(QObject* object, QEvent* event) {
if (object != ui_->name) {
return QWidget::eventFilter(object, event);
}
QFont font(ui_->name->font());
switch (event->type()) {
case QEvent::Enter:
font.setUnderline(true);
ui_->name->setFont(font);
break;
case QEvent::Leave:
font.setUnderline(false);
ui_->name->setFont(font);
break;
case QEvent::MouseButtonRelease: {
QMouseEvent* e = static_cast<QMouseEvent*>(event);
if (e->button() == Qt::LeftButton) {
QString text = ui_->name->text();
text.replace(QRegExp("\\W"), " ");
emit SuggestionClicked(text.simplified());
}
break;
}
default:
return false;
}
return true;
}

View File

@ -0,0 +1,44 @@
/* This file is part of Clementine.
Copyright 2012, 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 SUGGESTIONWIDGET_H
#define SUGGESTIONWIDGET_H
#include <QWidget>
class Ui_SuggestionWidget;
class SuggestionWidget : public QWidget {
Q_OBJECT
public:
SuggestionWidget(const QIcon& search_icon, QWidget* parent = 0);
~SuggestionWidget();
bool eventFilter(QObject* object, QEvent* event);
public slots:
void SetText(const QString& text);
signals:
void SuggestionClicked(const QString& query);
private:
Ui_SuggestionWidget* ui_;
};
#endif // SUGGESTIONWIDGET_H

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SuggestionWidget</class>
<widget class="QWidget" name="SuggestionWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>464</width>
<height>110</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="icon">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -219,6 +219,7 @@ MainWindow::MainWindow(Application* app,
IconLoader::Load("folder-sound"), true, app_, this)); IconLoader::Load("folder-sound"), true, app_, this));
app_->global_search()->ReloadSettings(); app_->global_search()->ReloadSettings();
global_search_view_->ReloadSettings();
connect(global_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); connect(global_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
connect(global_search_view_, SIGNAL(OpenSettingsAtPage(SettingsDialog::Page)), SLOT(OpenSettingsDialogAtPage(SettingsDialog::Page))); connect(global_search_view_, SIGNAL(OpenSettingsAtPage(SettingsDialog::Page)), SLOT(OpenSettingsDialogAtPage(SettingsDialog::Page)));