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.
This commit is contained in:
David Sansome 2011-08-28 21:33:59 +01:00
parent 2cbe90c2e5
commit 9370657b50
14 changed files with 1218 additions and 1 deletions

BIN
data/allthethings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -343,5 +343,6 @@
<file>providers/digitallyimported.png</file>
<file>providers/skyfm.png</file>
<file>providers/digitallyimported-32.png</file>
<file>allthethings.png</file>
</qresource>
</RCC>

View File

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

View File

@ -0,0 +1,91 @@
/* 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 "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<SearchProvider*>(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<SearchProvider*>(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;
}

View File

@ -0,0 +1,59 @@
/* 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 GLOBALSEARCH_H
#define GLOBALSEARCH_H
#include <QObject>
#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<SearchProvider*> providers_;
int next_id_;
QMap<int, int> pending_search_providers_;
};
#endif // GLOBALSEARCH_H

View File

@ -0,0 +1,452 @@
/* 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 "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 <QListView>
#include <QPainter>
#include <QStandardItemModel>
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<SearchProvider::Result>();
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<const QStyleOptionViewItemV3*>(&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<QPixmap>();
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 <n> 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<QKeyEvent*>(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<QObject *>(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<SearchProvider::Result>();
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);
}

View File

@ -0,0 +1,112 @@
/* 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 GLOBALSEARCHWIDGET_H
#define GLOBALSEARCHWIDGET_H
#include "searchprovider.h"
#include <QStyledItemDelegate>
#include <QWidget>
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<int, QModelIndex> art_requests_;
QStandardItemModel* model_;
QListView* view_;
bool eat_focus_out_;
QPixmap background_;
QPixmap background_scaled_;
};
#endif // GLOBALSEARCHWIDGET_H

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GlobalSearchWidget</class>
<widget class="QWidget" name="GlobalSearchWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>101</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">* {
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%);
}
</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Search &lt;b&gt;ALL THE THINGS&lt;/b&gt; in your library, connected devices and on the Internet.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LineEdit" name="search">
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Include:</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton">
<property name="text">
<string>Library</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_2">
<property name="text">
<string>Spotify</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton_3">
<property name="text">
<string>Magnatune</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEdit</class>
<extends>QLineEdit</extends>
<header>widgets/lineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,116 @@
/* 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 "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<AlbumCoverLoader, AlbumCoverLoader>(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<QString, Song> albums;
QSet<QString> 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());
}

View File

@ -0,0 +1,51 @@
/* 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 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<AlbumCoverLoader>* cover_loader_;
QMap<quint64, int> cover_loader_tasks_;
};
#endif // LIBRARYSEARCHPROVIDER_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 "searchprovider.h"
#include "core/boundfuturewatcher.h"
#include <QtConcurrentRun>
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<ResultList> future = QtConcurrent::run(
this, &BlockingSearchProvider::Search, id, query);
BoundFutureWatcher<ResultList, int>* watcher =
new BoundFutureWatcher<ResultList, int>(id);
watcher->setFuture(future);
connect(watcher, SIGNAL(finished()), SLOT(BlockingSearchFinished()));
}
void BlockingSearchProvider::BlockingSearchFinished() {
BoundFutureWatcher<ResultList, int>* watcher =
static_cast<BoundFutureWatcher<ResultList, int>*>(sender());
watcher->deleteLater();
const int id = watcher->data();
emit ResultsAvailable(id, watcher->result());
emit SearchFinished(id);
}

View File

@ -0,0 +1,109 @@
/* 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 SEARCHPROVIDER_H
#define SEARCHPROVIDER_H
#include <QIcon>
#include <QMetaType>
#include <QObject>
#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<Result> 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

View File

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

View File

@ -35,6 +35,9 @@
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="GlobalSearchWidget" name="global_search" native="true"/>
</item>
<item>
<widget class="FancyTabWidget" name="tabs" native="true"/>
</item>
@ -866,6 +869,12 @@
<extends>QWidget</extends>
<header>widgets/fancytabwidget.h</header>
</customwidget>
<customwidget>
<class>GlobalSearchWidget</class>
<extends>QWidget</extends>
<header>globalsearch/globalsearchwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../../data/data.qrc"/>