From 9370657b5050b59cdbd42346c0752bc4d34e3e5e Mon Sep 17 00:00:00 2001 From: David Sansome Date: Sun, 28 Aug 2011 21:33:59 +0100 Subject: [PATCH] Started work on a global search for library, with album art. The idea is that there's a single place to search for music, and it doesn't matter where it is, Clementine will find something playable. --- data/allthethings.png | Bin 0 -> 12892 bytes data/data.qrc | 1 + src/CMakeLists.txt | 12 + src/globalsearch/globalsearch.cpp | 91 +++++ src/globalsearch/globalsearch.h | 59 +++ src/globalsearch/globalsearchwidget.cpp | 452 +++++++++++++++++++++ src/globalsearch/globalsearchwidget.h | 112 +++++ src/globalsearch/globalsearchwidget.ui | 119 ++++++ src/globalsearch/librarysearchprovider.cpp | 116 ++++++ src/globalsearch/librarysearchprovider.h | 51 +++ src/globalsearch/searchprovider.cpp | 83 ++++ src/globalsearch/searchprovider.h | 109 +++++ src/ui/mainwindow.cpp | 5 +- src/ui/mainwindow.ui | 9 + 14 files changed, 1218 insertions(+), 1 deletion(-) create mode 100644 data/allthethings.png create mode 100644 src/globalsearch/globalsearch.cpp create mode 100644 src/globalsearch/globalsearch.h create mode 100644 src/globalsearch/globalsearchwidget.cpp create mode 100644 src/globalsearch/globalsearchwidget.h create mode 100644 src/globalsearch/globalsearchwidget.ui create mode 100644 src/globalsearch/librarysearchprovider.cpp create mode 100644 src/globalsearch/librarysearchprovider.h create mode 100644 src/globalsearch/searchprovider.cpp create mode 100644 src/globalsearch/searchprovider.h diff --git a/data/allthethings.png b/data/allthethings.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5c3bf66f8e1e25d4e5d3b7acf3a9ef6bdf7dea GIT binary patch literal 12892 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/digitallyimported.png providers/skyfm.png providers/digitallyimported-32.png + allthethings.png diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aedbc9ab8..294a52021 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -115,6 +115,11 @@ set(SOURCES engines/gstenginepipeline.cpp engines/gstelementdeleter.cpp + globalsearch/globalsearch.cpp + globalsearch/globalsearchwidget.cpp + globalsearch/librarysearchprovider.cpp + globalsearch/searchprovider.cpp + internet/digitallyimportedservice.cpp internet/digitallyimportedservicebase.cpp internet/digitallyimportedsettingspage.cpp @@ -347,6 +352,11 @@ set(HEADERS engines/gstenginepipeline.h engines/gstelementdeleter.h + globalsearch/librarysearchprovider.h + globalsearch/globalsearch.h + globalsearch/globalsearchwidget.h + globalsearch/searchprovider.h + internet/digitallyimportedservicebase.h internet/digitallyimportedsettingspage.h internet/icecastbackend.h @@ -508,6 +518,8 @@ set(UI devices/deviceproperties.ui + globalsearch/globalsearchwidget.ui + internet/digitallyimportedsettingspage.ui internet/icecastfilterwidget.ui internet/internetviewcontainer.ui diff --git a/src/globalsearch/globalsearch.cpp b/src/globalsearch/globalsearch.cpp new file mode 100644 index 000000000..550e770fe --- /dev/null +++ b/src/globalsearch/globalsearch.cpp @@ -0,0 +1,91 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#include "librarysearchprovider.h" +#include "globalsearch.h" +#include "core/logging.h" + +GlobalSearch::GlobalSearch(QObject* parent) + : QObject(parent), + next_id_(1) +{ +} + +void GlobalSearch::AddProvider(SearchProvider* provider) { + connect(provider, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)), + SLOT(ResultsAvailableSlot(int,SearchProvider::ResultList))); + connect(provider, SIGNAL(SearchFinished(int)), + SLOT(SearchFinishedSlot(int))); + connect(provider, SIGNAL(ArtLoaded(int,QImage)), SIGNAL(ArtLoaded(int,QImage))); + connect(provider, SIGNAL(destroyed(QObject*)), + SLOT(ProviderDestroyedSlot(QObject*))); + + providers_ << provider; +} + +int GlobalSearch::SearchAsync(const QString& query) { + const int id = next_id_ ++; + + pending_search_providers_[id] = providers_.count(); + foreach (SearchProvider* provider, providers_) { + provider->SearchAsync(id, query); + } + + return id; +} + +void GlobalSearch::ResultsAvailableSlot(int id, const SearchProvider::ResultList& results) { + if (!results.isEmpty()) + emit ResultsAvailable(id, results); +} + +void GlobalSearch::SearchFinishedSlot(int id) { + if (!pending_search_providers_.contains(id)) + return; + + SearchProvider* provider = static_cast(sender()); + const int remaining = --pending_search_providers_[id]; + + emit ProviderSearchFinished(id, provider); + if (remaining == 0) { + emit SearchFinished(id); + pending_search_providers_.remove(id); + } +} + +void GlobalSearch::ProviderDestroyedSlot(QObject* object) { + SearchProvider* provider = static_cast(object); + if (!providers_.contains(provider)) + return; + + providers_.removeAll(provider); + emit ProviderDestroyed(provider); + + // We have to abort any pending searches since we can't tell whether they + // were on this provider. + foreach (int id, pending_search_providers_.keys()) { + emit SearchFinished(id); + } + pending_search_providers_.clear(); +} + +int GlobalSearch::LoadArtAsync(const SearchProvider::Result& result) { + const int id = next_id_ ++; + result.provider_->LoadArtAsync(id, result); + return id; +} + diff --git a/src/globalsearch/globalsearch.h b/src/globalsearch/globalsearch.h new file mode 100644 index 000000000..d3cdfc97a --- /dev/null +++ b/src/globalsearch/globalsearch.h @@ -0,0 +1,59 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#ifndef GLOBALSEARCH_H +#define GLOBALSEARCH_H + +#include + +#include "searchprovider.h" + + +class GlobalSearch : public QObject { + Q_OBJECT + +public: + GlobalSearch(QObject* parent = 0); + + void AddProvider(SearchProvider* provider); + + int SearchAsync(const QString& query); + int LoadArtAsync(const SearchProvider::Result& result); + +signals: + void ResultsAvailable(int id, const SearchProvider::ResultList& results); + void ProviderSearchFinished(int id, const SearchProvider* provider); + void SearchFinished(int id); + + void ArtLoaded(int id, const QImage& image); + + void ProviderDestroyed(SearchProvider* provider); + +private slots: + void ResultsAvailableSlot(int id, const SearchProvider::ResultList& results); + void SearchFinishedSlot(int id); + + void ProviderDestroyedSlot(QObject* object); + +private: + QList providers_; + + int next_id_; + QMap pending_search_providers_; +}; + +#endif // GLOBALSEARCH_H diff --git a/src/globalsearch/globalsearchwidget.cpp b/src/globalsearch/globalsearchwidget.cpp new file mode 100644 index 000000000..ac3ffbeb3 --- /dev/null +++ b/src/globalsearch/globalsearchwidget.cpp @@ -0,0 +1,452 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#include "globalsearch.h" +#include "globalsearchwidget.h" +#include "librarysearchprovider.h" +#include "ui_globalsearchwidget.h" +#include "core/logging.h" +#include "core/utilities.h" +#include "widgets/stylehelper.h" + +#include +#include +#include + +const int GlobalSearchItemDelegate::kHeight = SearchProvider::kArtHeight; +const int GlobalSearchItemDelegate::kMargin = 1; +const int GlobalSearchItemDelegate::kArtMargin = 6; +const int GlobalSearchItemDelegate::kWordPadding = 6; +const int GlobalSearchWidget::kMinVisibleItems = 3; +const int GlobalSearchWidget::kMaxVisibleItems = 12; + + +GlobalSearchItemDelegate::GlobalSearchItemDelegate(GlobalSearchWidget* widget) + : QStyledItemDelegate(widget), + widget_(widget) +{ + no_cover_ = ScaleAndPad(QImage(":nocover.png")); +} + +QPixmap GlobalSearchItemDelegate::ScaleAndPad(const QImage& image) { + if (image.isNull()) + return QPixmap(); + + if (image.size() == QSize(kHeight, kHeight)) + return QPixmap::fromImage(image); + + // Scale the image down + QImage copy; + copy = image.scaled(QSize(kHeight, kHeight), + Qt::KeepAspectRatio, Qt::SmoothTransformation); + + // Pad the image to kHeight x kHeight + QImage padded_image(kHeight, kHeight, QImage::Format_ARGB32); + padded_image.fill(0); + + QPainter p(&padded_image); + p.drawImage((kHeight - copy.width()) / 2, (kHeight - copy.height()) / 2, + copy); + p.end(); + + return QPixmap::fromImage(padded_image); +} + +QSize GlobalSearchItemDelegate::sizeHint(const QStyleOptionViewItem& option, + const QModelIndex& index) const { + QSize size = QStyledItemDelegate::sizeHint(option, index); + size.setHeight(kHeight + kMargin); + return size; +} + +void GlobalSearchItemDelegate::DrawAndShrink(QPainter* p, QRect* rect, + const QString& text) const { + QRect br; + p->drawText(*rect, Qt::TextSingleLine | Qt::AlignVCenter, text, &br); + rect->setLeft(br.right() + kWordPadding); +} + +void GlobalSearchItemDelegate::paint(QPainter* p, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + const SearchProvider::Result result = + index.data(GlobalSearchWidget::Role_Result).value(); + const Song& m = result.metadata_; + + widget_->LazyLoadArt(index); + + QFont bold_font = option.font; + bold_font.setBold(true); + + QColor pen = option.palette.color(QPalette::Text); + QColor light_pen = pen; + pen.setAlpha(200); + light_pen.setAlpha(128); + + // Draw the background + const QStyleOptionViewItemV3* vopt = qstyleoption_cast(&option); + const QWidget* widget = vopt->widget; + QStyle* style = widget->style() ? widget->style() : QApplication::style(); + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, p, widget); + + // Draw the album art. This will already be the correct size. + const QRect rect = option.rect; + const QRect art_rect(rect.left() + kMargin, rect.top(), kHeight, kHeight); + + QPixmap art = index.data(Qt::DecorationRole).value(); + if (art.isNull()) + art = no_cover_; + + p->drawPixmap(art_rect, art); + + // Position text + QRect text_rect(art_rect.right() + kArtMargin, art_rect.top(), + rect.right() - art_rect.right() - kArtMargin, 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)); + + // The text we draw depends on the type of result. + switch (result.type_) { + case SearchProvider::Result::Type_Track: { + // Line 1 is Title + p->setFont(bold_font); + + // Title + p->setPen(pen); + DrawAndShrink(p, &text_rect_1, m.title()); + + // Line 2 is Artist - Album + p->setFont(option.font); + + // Artist + p->setPen(pen); + if (!m.artist().isEmpty()) { + DrawAndShrink(p, &text_rect_2, m.artist()); + } else if (!m.albumartist().isEmpty()) { + DrawAndShrink(p, &text_rect_2, m.albumartist()); + } + + if (!m.album().isEmpty()) { + // Dash + p->setPen(light_pen); + DrawAndShrink(p, &text_rect_2, " - "); + + // Album + p->setPen(pen); + DrawAndShrink(p, &text_rect_2, m.album()); + } + + break; + } + + case SearchProvider::Result::Type_Album: { + // Line 1 is Artist - Album + p->setFont(bold_font); + + // Artist + p->setPen(pen); + if (!m.albumartist().isEmpty()) + DrawAndShrink(p, &text_rect_1, m.albumartist()); + else if (m.is_compilation()) + DrawAndShrink(p, &text_rect_1, tr("Various Artists")); + else if (!m.artist().isEmpty()) + DrawAndShrink(p, &text_rect_1, m.artist()); + else + DrawAndShrink(p, &text_rect_1, tr("Unknown")); + + // Dash + p->setPen(light_pen); + DrawAndShrink(p, &text_rect_1, " - "); + + // Album + p->setPen(pen); + if (m.album().isEmpty()) + DrawAndShrink(p, &text_rect_1, tr("Unknown")); + else + DrawAndShrink(p, &text_rect_1, m.album()); + + // Line 2 is tracks + p->setFont(option.font); + + p->setPen(pen); + DrawAndShrink(p, &text_rect_2, QString::number(result.album_size_)); + + p->setPen(light_pen); + DrawAndShrink(p, &text_rect_2, tr(result.album_size_ == 1 ? "track" : "tracks")); + break; + } + + default: + break; + } +} + + +GlobalSearchWidget::GlobalSearchWidget(QWidget* parent) + : QWidget(parent), + ui_(new Ui_GlobalSearchWidget), + engine_(new GlobalSearch(this)), + last_id_(0), + clear_model_on_next_result_(false), + model_(new QStandardItemModel(this)), + view_(new QListView), + eat_focus_out_(false), + background_(":allthethings.png") +{ + ui_->setupUi(this); + + view_->setWindowFlags(Qt::Popup); + view_->setFocusPolicy(Qt::NoFocus); + view_->setFocusProxy(ui_->search); + view_->installEventFilter(this); + + view_->setModel(model_); + view_->setItemDelegate(new GlobalSearchItemDelegate(this)); + view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + connect(ui_->search, SIGNAL(textEdited(QString)), SLOT(TextEdited(QString))); + connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)), + SLOT(AddResults(int,SearchProvider::ResultList))); + connect(engine_, SIGNAL(ArtLoaded(int,QImage)), SLOT(ArtLoaded(int,QImage))); +} + +GlobalSearchWidget::~GlobalSearchWidget() { + delete ui_; +} + +void GlobalSearchWidget::Init(LibraryBackendInterface* library) { + engine_->AddProvider(new LibrarySearchProvider( + library, tr("Library"), IconLoader::Load("folder-sound"), engine_)); + + // The style helper's base color doesn't get initialised until after the + // constructor. + QPalette view_palette = view_->palette(); + view_palette.setColor(QPalette::Text, Qt::white); + view_palette.setColor(QPalette::Base, Utils::StyleHelper::shadowColor().darker(109)); + + QFont view_font = view_->font(); + view_font.setPointSizeF(Utils::StyleHelper::sidebarFontSize()); + + view_->setFont(view_font); + view_->setPalette(view_palette); +} + +void GlobalSearchWidget::resizeEvent(QResizeEvent* e) { + background_scaled_ = background_.scaled(size(), Qt::KeepAspectRatio, + Qt::SmoothTransformation); + + QWidget::resizeEvent(e); +} + +void GlobalSearchWidget::paintEvent(QPaintEvent* e) { + QPainter p(this); + + QRect total_rect = rect().adjusted(0, 0, 1, 0); + total_rect = style()->visualRect(layoutDirection(), geometry(), total_rect); + Utils::StyleHelper::verticalGradient(&p, total_rect, total_rect); + + QRect background_rect = background_scaled_.rect(); + background_rect.moveLeft(total_rect.right() - background_rect.width()); + background_rect.moveTop(total_rect.top()); + + p.setOpacity(0.5); + p.drawPixmap(background_rect, background_scaled_); + p.setOpacity(1.0); + + p.setPen(Utils::StyleHelper::borderColor()); + p.drawLine(total_rect.topRight(), total_rect.bottomRight()); + + QColor light = Utils::StyleHelper::sidebarHighlight(); + p.setPen(light); + p.drawLine(total_rect.bottomLeft(), total_rect.bottomRight()); +} + +void GlobalSearchWidget::TextEdited(const QString& text) { + clear_model_on_next_result_ = true; + last_id_ = engine_->SearchAsync(text); +} + +void GlobalSearchWidget::AddResults(int id, const SearchProvider::ResultList& results) { + if (id != last_id_) + return; + + if (clear_model_on_next_result_) { + model_->clear(); + art_requests_.clear(); + clear_model_on_next_result_ = false; + } + + foreach (const SearchProvider::Result& result, results) { + QStandardItem* item = new QStandardItem; + item->setData(QVariant::fromValue(result), Role_Result); + + model_->appendRow(item); + } + + RepositionPopup(); +} + +void GlobalSearchWidget::RepositionPopup() { + if (model_->rowCount() == 0) { + view_->hide(); + return; + } + + int h = view_->sizeHintForRow(0) * float(0.5 + + qBound(kMinVisibleItems, model_->rowCount(), kMaxVisibleItems)); + int w = ui_->search->width(); + + QPoint pos = ui_->search->mapToGlobal(ui_->search->rect().bottomLeft()); + + view_->setGeometry(QRect(pos, QSize(w, h))); + + if (!view_->isVisible()) + view_->show(); +} + +bool GlobalSearchWidget::eventFilter(QObject* o, QEvent* e) { + // Most of this is borrowed from QCompleter::eventFilter + + if (eat_focus_out_ && o == ui_->search && e->type() == QEvent::FocusOut) { + if (view_->isVisible()) + return true; + } + + if (o != view_) + return QWidget::eventFilter(o, e); + + switch (e->type()) { + case QEvent::KeyPress: { + QKeyEvent* ke = static_cast(e); + + QModelIndex cur_index = view_->currentIndex(); + const int key = ke->key(); + + // Handle popup navigation keys. These are hardcoded because up/down might make the + // widget do something else (lineedit cursor moves to home/end on mac, for instance) + switch (key) { + case Qt::Key_End: + case Qt::Key_Home: + if (ke->modifiers() & Qt::ControlModifier) + return false; + break; + + case Qt::Key_Up: + if (!cur_index.isValid()) { + view_->setCurrentIndex(model_->index(model_->rowCount() - 1, 0)); + return true; + } else if (cur_index.row() == 0) { + return true; + } + return false; + + case Qt::Key_Down: + if (!cur_index.isValid()) { + view_->setCurrentIndex(model_->index(0, 0)); + return true; + } else if (cur_index.row() == model_->rowCount() - 1) { + return true; + } + return false; + + case Qt::Key_PageUp: + case Qt::Key_PageDown: + return false; + } + + // Send the event to the widget. If the widget accepted the event, do nothing + // If the widget did not accept the event, provide a default implementation + eat_focus_out_ = false; + (static_cast(ui_->search))->event(ke); + eat_focus_out_ = true; + + if (e->isAccepted() || !view_->isVisible()) { + // widget lost focus, hide the popup + if (!ui_->search->hasFocus()) + view_->hide(); + if (e->isAccepted()) + return true; + } + + // default implementation for keys not handled by the widget when popup is open + switch (key) { + case Qt::Key_Return: + case Qt::Key_Enter: + case Qt::Key_Tab: + view_->hide(); + // TODO: complete + break; + + case Qt::Key_F4: + if (ke->modifiers() & Qt::AltModifier) + view_->hide(); + break; + + case Qt::Key_Backtab: + case Qt::Key_Escape: + view_->hide(); + break; + + default: + break; + } + + return true; + } + + case QEvent::MouseButtonPress: + if (!view_->underMouse()) { + view_->hide(); + return true; + } + return false; + + case QEvent::InputMethod: + case QEvent::ShortcutOverride: + QApplication::sendEvent(ui_->search, e); + break; + + default: + return false; + } + + return false; +} + +void GlobalSearchWidget::LazyLoadArt(const QModelIndex& index) { + if (!index.isValid() || index.data(Role_LazyLoadingArt).isValid()) { + return; + } + + model_->itemFromIndex(index)->setData(true, Role_LazyLoadingArt); + + const SearchProvider::Result result = + index.data(Role_Result).value(); + + int id = engine_->LoadArtAsync(result); + art_requests_[id] = index; +} + +void GlobalSearchWidget::ArtLoaded(int id, const QImage& image) { + if (!art_requests_.contains(id)) + return; + QModelIndex index = art_requests_.take(id); + + model_->itemFromIndex(index)->setData( + GlobalSearchItemDelegate::ScaleAndPad(image), Qt::DecorationRole); +} + diff --git a/src/globalsearch/globalsearchwidget.h b/src/globalsearch/globalsearchwidget.h new file mode 100644 index 000000000..1c54a3199 --- /dev/null +++ b/src/globalsearch/globalsearchwidget.h @@ -0,0 +1,112 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#ifndef GLOBALSEARCHWIDGET_H +#define GLOBALSEARCHWIDGET_H + +#include "searchprovider.h" + +#include +#include + +class GlobalSearch; +class GlobalSearchWidget; +class LibraryBackendInterface; +class Ui_GlobalSearchWidget; + +class QListView; +class QStandardItemModel; + + +class GlobalSearchItemDelegate : public QStyledItemDelegate { +public: + GlobalSearchItemDelegate(GlobalSearchWidget* widget); + + static const int kHeight; + static const int kMargin; + static const int kArtMargin; + static const int kWordPadding; + + static QPixmap ScaleAndPad(const QImage& image); + + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paint(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index) const; + +private: + void DrawAndShrink(QPainter* p, QRect* rect, const QString& text) const; + +private: + GlobalSearchWidget* widget_; + QPixmap no_cover_; +}; + + +class GlobalSearchWidget : public QWidget { + Q_OBJECT + +public: + GlobalSearchWidget(QWidget *parent = 0); + ~GlobalSearchWidget(); + + static const int kMinVisibleItems; + static const int kMaxVisibleItems; + + enum Role { + Role_Result = Qt::UserRole + 1, + Role_LazyLoadingArt + }; + + void Init(LibraryBackendInterface* library); + + // Called by the delegate + void LazyLoadArt(const QModelIndex& index); + + // QWidget + bool eventFilter(QObject* o, QEvent* e); + +protected: + void resizeEvent(QResizeEvent* e); + void paintEvent(QPaintEvent* e); + +private slots: + void TextEdited(const QString& text); + void AddResults(int id, const SearchProvider::ResultList& results); + + void ArtLoaded(int id, const QImage& image); + +private: + void RepositionPopup(); + +private: + Ui_GlobalSearchWidget* ui_; + + GlobalSearch* engine_; + int last_id_; + bool clear_model_on_next_result_; + + QMap art_requests_; + + QStandardItemModel* model_; + QListView* view_; + bool eat_focus_out_; + + QPixmap background_; + QPixmap background_scaled_; +}; + +#endif // GLOBALSEARCHWIDGET_H diff --git a/src/globalsearch/globalsearchwidget.ui b/src/globalsearch/globalsearchwidget.ui new file mode 100644 index 000000000..050635e77 --- /dev/null +++ b/src/globalsearch/globalsearchwidget.ui @@ -0,0 +1,119 @@ + + + GlobalSearchWidget + + + + 0 + 0 + 522 + 101 + + + + Form + + + * { + color: white; + font-weight: bold; + font-size: 7.5pt; +} + +#search { + border: 1px groove rgba(128, 128, 128, 60%); + background-color: rgba(0, 0, 0, 20%) +} + +QToolButton { + font-weight: normal; + font-weight: bold; + color: rgba(255, 255, 255, 50%); +} + +QToolButton:hover { + background-color: black; + border: 2px solid rgba(255, 255, 255, 20%); + border-radius: 3px; +} + +QToolButton:pressed { + border: 2px solid rgba(255, 255, 255, 20%); + background-color: rgba(255, 255, 255, 10%); +} + + + + + + + + + Search <b>ALL THE THINGS</b> in your library, connected devices and on the Internet. + + + true + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Include: + + + + + + + Library + + + + + + + Spotify + + + + + + + Magnatune + + + + + + + + + + LineEdit + QLineEdit +
widgets/lineedit.h
+
+
+ + +
diff --git a/src/globalsearch/librarysearchprovider.cpp b/src/globalsearch/librarysearchprovider.cpp new file mode 100644 index 000000000..5e5379a96 --- /dev/null +++ b/src/globalsearch/librarysearchprovider.cpp @@ -0,0 +1,116 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#include "librarysearchprovider.h" +#include "core/logging.h" +#include "covers/albumcoverloader.h" +#include "library/librarybackend.h" +#include "library/libraryquery.h" +#include "library/sqlrow.h" + + +LibrarySearchProvider::LibrarySearchProvider(LibraryBackendInterface* backend, + const QString& name, + const QIcon& icon, + QObject* parent) + : BlockingSearchProvider(name, icon, parent), + backend_(backend), + cover_loader_(new BackgroundThreadImplementation(this)) +{ + cover_loader_->Start(true); + cover_loader_->Worker()->SetDesiredHeight(kArtHeight); + cover_loader_->Worker()->SetPadOutputImage(true); + cover_loader_->Worker()->SetScaleOutputImage(true); + + connect(cover_loader_->Worker().get(), + SIGNAL(ImageLoaded(quint64,QImage)), + SLOT(AlbumArtLoaded(quint64,QImage))); +} + +SearchProvider::ResultList LibrarySearchProvider::Search(int id, const QString& query) { + QueryOptions options; + options.set_filter(query); + + LibraryQuery q(options); + q.SetColumnSpec("%songs_table.ROWID, " + Song::kColumnSpec); + + if (!backend_->ExecQuery(&q)) { + return ResultList(); + } + + const QStringList tokens = TokenizeQuery(query); + + QMultiMap albums; + QSet albums_with_non_track_matches; + + ResultList ret; + + while (q.Next()) { + Song song; + song.InitFromQuery(q, true); + + QString album_key = song.album(); + if (song.is_compilation() && !song.albumartist().isEmpty()) { + album_key.prepend(song.albumartist() + " - "); + } else if (!song.is_compilation()) { + album_key.prepend(song.artist()); + } + + if (TokenMatches(tokens, song.title())) { + // If the query matched in the song title then we're interested in this + // as an individual song. + Result result(this); + result.type_ = Result::Type_Track; + result.metadata_ = song; + ret << result; + } else { + // Otherwise we record this as being an interesting album. + albums_with_non_track_matches.insert(album_key); + } + + albums.insertMulti(album_key, song); + } + + // Add any albums that contained least one song that wasn't matched on the + // song title. + foreach (const QString& key, albums_with_non_track_matches) { + Result result(this); + result.type_ = Result::Type_Album; + result.metadata_ = albums.value(key); + result.album_size_ = albums.count(key); + ret << result; + } + + return ret; +} + +void LibrarySearchProvider::LoadArtAsync(int id, const Result& result) { + quint64 loader_id = cover_loader_->Worker()->LoadImageAsync(result.metadata_); + cover_loader_tasks_[loader_id] = id; +} + +void LibrarySearchProvider::AlbumArtLoaded(quint64 id, const QImage& image) { + if (!cover_loader_tasks_.contains(id)) + return; + int orig_id = cover_loader_tasks_.take(id); + + emit ArtLoaded(orig_id, image); +} + +void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) { + emit TracksLoaded(id, SongList()); +} diff --git a/src/globalsearch/librarysearchprovider.h b/src/globalsearch/librarysearchprovider.h new file mode 100644 index 000000000..7f29c604a --- /dev/null +++ b/src/globalsearch/librarysearchprovider.h @@ -0,0 +1,51 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#ifndef LIBRARYSEARCHPROVIDER_H +#define LIBRARYSEARCHPROVIDER_H + +#include "searchprovider.h" +#include "core/backgroundthread.h" + +class AlbumCoverLoader; +class LibraryBackendInterface; + + +class LibrarySearchProvider : public BlockingSearchProvider { + Q_OBJECT + +public: + LibrarySearchProvider(LibraryBackendInterface* backend, const QString& name, + const QIcon& icon, QObject* parent = 0); + + void LoadArtAsync(int id, const Result& result); + void LoadTracksAsync(int id, const Result& result); + +protected: + ResultList Search(int id, const QString& query); + +private slots: + void AlbumArtLoaded(quint64 id, const QImage& image); + +private: + LibraryBackendInterface* backend_; + + BackgroundThread* cover_loader_; + QMap cover_loader_tasks_; +}; + +#endif // LIBRARYSEARCHPROVIDER_H diff --git a/src/globalsearch/searchprovider.cpp b/src/globalsearch/searchprovider.cpp new file mode 100644 index 000000000..7e9414aab --- /dev/null +++ b/src/globalsearch/searchprovider.cpp @@ -0,0 +1,83 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#include "searchprovider.h" +#include "core/boundfuturewatcher.h" + +#include + +const int SearchProvider::kArtHeight = 32; + + +SearchProvider::SearchProvider(const QString& name, const QIcon& icon, + QObject* parent) + : QObject(parent), + name_(name), + icon_(icon) +{ +} + +QStringList SearchProvider::TokenizeQuery(const QString& query) { + QStringList tokens(query.split(QRegExp("\\s+"))); + + for (QStringList::iterator it = tokens.begin() ; it != tokens.end() ; ++it) { + (*it).remove('('); + (*it).remove(')'); + (*it).remove('"'); + + const int colon = (*it).indexOf(":"); + if (colon != -1) { + (*it).remove(0, colon + 1); + } + } + + return tokens; +} + +int SearchProvider::TokenMatches(const QStringList& tokens, const QString& string) { + int ret = 0; + foreach (const QString& token, tokens) { + if (string.contains(token, Qt::CaseInsensitive)) { + ret ++; + } + } + return ret; +} + +BlockingSearchProvider::BlockingSearchProvider(const QString& name, const QIcon& icon, QObject* parent) + : SearchProvider(name, icon, parent) { +} + +void BlockingSearchProvider::SearchAsync(int id, const QString& query) { + QFuture future = QtConcurrent::run( + this, &BlockingSearchProvider::Search, id, query); + + BoundFutureWatcher* watcher = + new BoundFutureWatcher(id); + watcher->setFuture(future); + connect(watcher, SIGNAL(finished()), SLOT(BlockingSearchFinished())); +} + +void BlockingSearchProvider::BlockingSearchFinished() { + BoundFutureWatcher* watcher = + static_cast*>(sender()); + watcher->deleteLater(); + + const int id = watcher->data(); + emit ResultsAvailable(id, watcher->result()); + emit SearchFinished(id); +} diff --git a/src/globalsearch/searchprovider.h b/src/globalsearch/searchprovider.h new file mode 100644 index 000000000..1f5abdb72 --- /dev/null +++ b/src/globalsearch/searchprovider.h @@ -0,0 +1,109 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + + 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 . +*/ + +#ifndef SEARCHPROVIDER_H +#define SEARCHPROVIDER_H + +#include +#include +#include + +#include "core/song.h" + +class SearchProvider : public QObject { + Q_OBJECT + +public: + SearchProvider(const QString& name, const QIcon& icon, QObject* parent = 0); + + static const int kArtHeight; + + struct Result { + Result(SearchProvider* provider = 0) + : provider_(provider), album_size_(0) {} + + enum Type { + Type_Track, + Type_Album, + Type_Stream + }; + + SearchProvider* provider_; + + Type type_; + Song metadata_; + + // How many songs in the album - valid only if type == Type_Album. + int album_size_; + }; + typedef QList ResultList; + + const QString& name() const { return name_; } + const QIcon& icon() const { return icon_; } + + // Starts a search. Must emit ResultsAvailable zero or more times and then + // SearchFinished exactly once, using this ID. + virtual void SearchAsync(int id, const QString& query) = 0; + + // Starts loading an icon for a result that was previously emitted by + // ResultsAvailable. Must emit ArtLoaded exactly once with this ID. + virtual void LoadArtAsync(int id, const Result& result) = 0; + + // Starts loading tracks for a result that was previously emitted by + // ResultsAvailable. Must emit TracksLoaded exactly once with this ID. + virtual void LoadTracksAsync(int id, const Result& result) = 0; + +signals: + void ResultsAvailable(int id, const SearchProvider::ResultList& results); + void SearchFinished(int id); + + void ArtLoaded(int id, const QImage& image); + + void TracksLoaded(int id, const SongList& tracks); + +protected: + // These functions treat queries in the same way as LibraryQuery. They're + // useful for figuring out whether you got a result because it matched in + // the song title or the artist/album name. + static QStringList TokenizeQuery(const QString& query); + static int TokenMatches(const QStringList& tokens, const QString& string); + +private: + QString name_; + QIcon icon_; +}; + +Q_DECLARE_METATYPE(SearchProvider::Result) + + +class BlockingSearchProvider : public SearchProvider { + Q_OBJECT + +public: + BlockingSearchProvider(const QString& name, const QIcon& icon, + QObject* parent = 0); + + void SearchAsync(int id, const QString& query); + +protected: + virtual ResultList Search(int id, const QString& query) = 0; + +private slots: + void BlockingSearchFinished(); +}; + +#endif // SEARCHPROVIDER_H diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 319c2ade6..f060bc293 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -227,6 +227,10 @@ MainWindow::MainWindow( ui_->volume->setValue(volume); VolumeChanged(volume); + // Initialise the global search widget + StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker()); + ui_->global_search->Init(library_->backend()); + // Add tabs to the fancy tab widget ui_->tabs->AddTab(library_view_, IconLoader::Load("folder-sound"), tr("Library")); ui_->tabs->AddTab(file_view_, IconLoader::Load("document-open"), tr("Files")); @@ -240,7 +244,6 @@ MainWindow::MainWindow( ui_->tabs->AddBottomWidget(ui_->now_playing); ui_->tabs->SetBackgroundPixmap(QPixmap(":/sidebar_background.png")); - StyleHelper::setBaseColor(palette().color(QPalette::Highlight).darker()); track_position_timer_->setInterval(1000); connect(track_position_timer_, SIGNAL(timeout()), SLOT(UpdateTrackPosition())); diff --git a/src/ui/mainwindow.ui b/src/ui/mainwindow.ui index 035d6cb20..076079706 100644 --- a/src/ui/mainwindow.ui +++ b/src/ui/mainwindow.ui @@ -35,6 +35,9 @@ 0 + + + @@ -866,6 +869,12 @@ QWidget
widgets/fancytabwidget.h
+ + GlobalSearchWidget + QWidget +
globalsearch/globalsearchwidget.h
+ 1 +