Replace the global search widget with a "Search" view on the sidebar. Organise results in a tree automatically.

This commit is contained in:
David Sansome 2012-06-04 18:18:37 +01:00
parent 5984aecbb8
commit 9c36cfa199
36 changed files with 559 additions and 2740 deletions

View File

@ -136,14 +136,11 @@ set(SOURCES
engines/gstenginepipeline.cpp
engines/gstelementdeleter.cpp
globalsearch/common.cpp
globalsearch/digitallyimportedsearchprovider.cpp
globalsearch/globalsearch.cpp
globalsearch/globalsearchitemdelegate.cpp
globalsearch/globalsearchsettingspage.cpp
globalsearch/globalsearchsortmodel.cpp
globalsearch/globalsearchtooltip.cpp
globalsearch/globalsearchwidget.cpp
globalsearch/globalsearchview.cpp
globalsearch/groovesharksearchprovider.cpp
globalsearch/icecastsearchprovider.cpp
globalsearch/librarysearchprovider.cpp
@ -151,8 +148,6 @@ set(SOURCES
globalsearch/searchprovider.cpp
globalsearch/simplesearchprovider.cpp
globalsearch/somafmsearchprovider.cpp
globalsearch/tooltipactionwidget.cpp
globalsearch/tooltipresultwidget.cpp
globalsearch/urlsearchprovider.cpp
internet/digitallyimportedclient.cpp
@ -418,13 +413,10 @@ set(HEADERS
globalsearch/globalsearch.h
globalsearch/globalsearchsettingspage.h
globalsearch/globalsearchtooltip.h
globalsearch/globalsearchwidget.h
globalsearch/globalsearchview.h
globalsearch/groovesharksearchprovider.h
globalsearch/searchprovider.h
globalsearch/simplesearchprovider.h
globalsearch/tooltipactionwidget.h
globalsearch/tooltipresultwidget.h
internet/digitallyimportedclient.h
internet/digitallyimportedservicebase.h
@ -610,7 +602,7 @@ set(UI
devices/deviceproperties.ui
globalsearch/globalsearchsettingspage.ui
globalsearch/globalsearchwidget.ui
globalsearch/globalsearchview.ui
internet/digitallyimportedsettingspage.ui
internet/groovesharksettingspage.ui
@ -866,12 +858,6 @@ if(HAVE_DBUS)
dbus/udisksdevice)
endif(HAVE_DEVICEKIT)
# Global search interface
qt4_add_dbus_adaptor(SOURCES
dbus/org.clementineplayer.GlobalSearch.xml
globalsearch/globalsearchservice.h GlobalSearchService
globalsearch/globalsearchadaptor)
# Wiimotedev interface classes
if(ENABLE_WIIMOTEDEV)
qt4_add_dbus_interface(SOURCES
@ -885,13 +871,11 @@ optional_source(HAVE_DBUS
core/mpris.cpp
core/mpris1.cpp
core/mpris2.cpp
globalsearch/globalsearchservice.cpp
ui/dbusscreensaver.cpp
HEADERS
core/mpris.h
core/mpris1.h
core/mpris2.h
globalsearch/globalsearchservice.h
)
optional_source(HAVE_WIIMOTEDEV

View File

@ -1,32 +0,0 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name='org.clementineplayer.GlobalSearch'>
<method name='StartSearch'>
<arg type='i' name='id' direction='out' />
<arg type='s' name='query' direction='in' />
<arg type='b' name='prefetch_art' direction='in' />
</method>
<method name='CancelSearch'>
<arg type='i' name='id' direction='in' />
</method>
<signal name='ResultsAvailable'>
<arg type='i' name='id' />
<arg type='a(ibsiiissssbi)' name='results' />
<annotation name="com.trolltech.QtDBus.QtTypeName.In1" value="GlobalSearchServiceResultList" />
</signal>
<signal name='SearchFinished'>
<arg type='i' name='id' />
</signal>
<signal name='ArtLoaded'>
<arg type='i' name='result_id' />
<arg type='ay' name='image_data' />
<annotation name="com.trolltech.QtDBus.QtTypeName.Out1" value="QByteArray" />
</signal>
</interface>
</node>

View File

@ -1,66 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, 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 "common.h"
#ifdef HAVE_DBUS
QDBusArgument& operator <<(QDBusArgument& arg, const GlobalSearchServiceResult& result) {
arg.beginStructure();
arg << result.result_id_
<< result.art_on_the_way_
<< result.provider_name_
<< result.type_
<< result.match_quality_
<< result.album_size_
<< result.title_
<< result.artist_
<< result.album_
<< result.album_artist_
<< result.is_compilation_
<< result.track_;
arg.endStructure();
return arg;
}
const QDBusArgument& operator >>(const QDBusArgument& arg, GlobalSearchServiceResult& result) {
int type;
int match_quality;
arg.beginStructure();
arg >> result.result_id_
>> result.art_on_the_way_
>> result.provider_name_
>> type
>> match_quality
>> result.album_size_
>> result.title_
>> result.artist_
>> result.album_
>> result.album_artist_
>> result.is_compilation_
>> result.track_;
arg.endStructure();
result.type_ = static_cast<globalsearch::Type>(type);
result.match_quality_ = static_cast<globalsearch::MatchQuality>(match_quality);
return arg;
}
#endif // HAVE_DBUS

View File

@ -1,92 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, 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 GLOBALSEARCHSERVICERESULT_H
#define GLOBALSEARCHSERVICERESULT_H
// This file contains definitions that are shared between Clementine and any
// external applications that need to do global searches over DBus.
#include "config.h"
#ifdef HAVE_DBUS
# include <QDBusArgument>
#endif
#include <QIcon>
namespace globalsearch {
// The order of types here is the order they'll appear in the UI.
enum Type {
Type_Track = 0,
Type_Stream,
Type_Album
};
enum MatchQuality {
// A token in the search string matched at the beginning of the song
// metadata.
Quality_AtStart = 0,
// A token matched somewhere else.
Quality_Middle,
Quality_None
};
} // namespace globalsearch
#ifdef HAVE_DBUS
struct GlobalSearchServiceResult {
// When adding new fields to this struct remember to update the dbus signature
// which is duplicated in the xml specification and in clementinerunner.cpp
int result_id_;
bool art_on_the_way_;
QString provider_name_;
globalsearch::Type type_;
globalsearch::MatchQuality match_quality_;
int album_size_;
QString title_;
QString artist_;
QString album_;
QString album_artist_;
bool is_compilation_;
int track_;
// Not included in the dbus emission.
QIcon image_;
int provider_order_;
};
typedef QList<GlobalSearchServiceResult> GlobalSearchServiceResultList;
Q_DECLARE_METATYPE(GlobalSearchServiceResult)
Q_DECLARE_METATYPE(GlobalSearchServiceResultList)
QDBusArgument& operator <<(QDBusArgument& arg, const GlobalSearchServiceResult& result);
const QDBusArgument& operator >>(const QDBusArgument& arg, GlobalSearchServiceResult& result);
#endif // HAVE_DBUS
#endif // GLOBALSEARCHSERVICERESULT_H

View File

@ -140,8 +140,8 @@ void GlobalSearch::timerEvent(QTimerEvent* e) {
}
QString GlobalSearch::PixmapCacheKey(const SearchProvider::Result& result) const {
return QString::number(qulonglong(result.provider_))
% "," % QString::number(int(result.type_))
return "globalsearch:"
% QString::number(qulonglong(result.provider_))
% "," % result.metadata_.url().toString();
}

View File

@ -1,192 +0,0 @@
/* 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 "globalsearchitemdelegate.h"
#include "globalsearchwidget.h"
#include "searchprovider.h"
#include "core/logging.h"
#include <QApplication>
#include <QPainter>
const int GlobalSearchItemDelegate::kHeight = SearchProvider::kArtHeight;
const int GlobalSearchItemDelegate::kMargin = 2;
const int GlobalSearchItemDelegate::kArtMargin = 6;
const int GlobalSearchItemDelegate::kProviderIconSize = 16;
GlobalSearchItemDelegate::GlobalSearchItemDelegate(GlobalSearchWidget* widget)
: QStyledItemDelegate(widget),
widget_(widget)
{
no_cover_ = QPixmap::fromImage(SearchProvider::ScaleAndPad(QImage(":nocover.png")));
}
QSize GlobalSearchItemDelegate::sizeHint(const QStyleOptionViewItem& option,
const QModelIndex& index) const {
QSize size = QStyledItemDelegate::sizeHint(option, index);
size.setHeight(kHeight + kMargin*2);
return size;
}
void GlobalSearchItemDelegate::paint(QPainter* p,
const QStyleOptionViewItem& option,
const QModelIndex& index) const {
const SearchProvider::Result result =
index.data(GlobalSearchWidget::Role_PrimaryResult).value<SearchProvider::Result>();
const SearchProvider::ResultList all_results =
index.data(GlobalSearchWidget::Role_AllResults).value<SearchProvider::ResultList>();
const Song& m = result.metadata_;
const bool selected = option.state & QStyle::State_Selected;
widget_->LazyLoadArt(index);
QFont bold_font = option.font;
bold_font.setBold(true);
QFont big_font(bold_font);
big_font.setPointSizeF(big_font.pointSizeF() + 2);
QColor pen = option.palette.color(selected ? QPalette::HighlightedText : 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() + kMargin, kHeight, kHeight);
QPixmap art = index.data(Qt::DecorationRole).value<QPixmap>();
if (art.isNull())
art = no_cover_;
p->drawPixmap(art_rect, art);
// Draw a track count indicator next to the art.
QRect count_rect(art_rect.right() + kArtMargin, art_rect.top(),
kHeight, kHeight);
QString count;
switch (result.type_) {
case globalsearch::Type_Track:
break;
case globalsearch::Type_Stream:
count = QString::fromUtf8("");
break;
case globalsearch::Type_Album:
if (result.album_size_ <= 0)
count = "-";
else
count = QString::number(result.album_size_);
break;
}
p->setPen(light_pen);
p->setFont(big_font);
p->drawText(count_rect, Qt::TextSingleLine | Qt::AlignCenter, count);
// Draw provider icons on the right.
const int icons_width = (kProviderIconSize + kArtMargin) * all_results.count();
QRect icons_rect(rect.right() - icons_width,
rect.top() + (rect.height() - kProviderIconSize) / 2,
icons_width, kProviderIconSize);
QRect icon_rect(icons_rect.topLeft(), QSize(kProviderIconSize, kProviderIconSize));
foreach (const SearchProvider::Result& result, all_results) {
p->drawPixmap(icon_rect, result.provider_->icon().pixmap(kProviderIconSize));
icon_rect.translate(kProviderIconSize + kArtMargin, 0);
}
// Position text
QRect text_rect(count_rect.right() + kArtMargin, count_rect.top(),
icons_rect.left() - count_rect.right() - kArtMargin*2, kHeight);
QRect text_rect_1(text_rect.adjusted(0, 0, 0, -kHeight/2));
QRect text_rect_2(text_rect.adjusted(0, kHeight/2, 0, 0));
QString line_1;
QString line_2;
// The text we draw depends on the type of result.
switch (result.type_) {
case globalsearch::Type_Track:
case globalsearch::Type_Stream: {
// Title
line_1 += m.PrettyTitle() + " ";
// Artist - Album - Track n
if (!m.artist().isEmpty()) {
line_2 += m.artist();
} else if (!m.albumartist().isEmpty()) {
line_2 += m.albumartist();
}
if (!m.album().isEmpty()) {
line_2 += " - " + m.album();
}
if (m.track() > 0) {
line_2 += " - " + tr("track %1").arg(m.track());
}
break;
}
case globalsearch::Type_Album: {
// Line 1 is Artist - Album
// Artist
if (!m.albumartist().isEmpty())
line_1 += m.albumartist();
else if (m.is_compilation())
line_1 += tr("Various artists");
else if (!m.artist().isEmpty())
line_1 += m.artist();
else
line_1 += tr("Unknown");
// Dash
line_1 += " - ";
// Album
if (m.album().isEmpty())
line_1 += tr("Unknown");
else
line_1 += m.album();
break;
}
}
if (line_2.isEmpty()) {
text_rect_1 = text_rect;
}
p->setFont(bold_font);
p->setPen(pen);
p->drawText(text_rect_1, Qt::TextSingleLine | Qt::AlignVCenter, line_1);
p->setFont(option.font);
p->setPen(light_pen);
p->drawText(text_rect_2, Qt::TextSingleLine | Qt::AlignVCenter, line_2);
}

View File

@ -1,44 +0,0 @@
/* 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 GLOBALSEARCHITEMDELEGATE_H
#define GLOBALSEARCHITEMDELEGATE_H
#include <QStyledItemDelegate>
class GlobalSearchWidget;
class GlobalSearchItemDelegate : public QStyledItemDelegate {
public:
GlobalSearchItemDelegate(GlobalSearchWidget* widget);
static const int kHeight;
static const int kMargin;
static const int kArtMargin;
static const int kProviderIconSize;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const;
void paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const;
private:
GlobalSearchWidget* widget_;
QPixmap no_cover_;
};
#endif // GLOBALSEARCHITEMDELEGATE_H

View File

@ -1,129 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, 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 "globalsearchservice.h"
#include "globalsearch/globalsearchadaptor.h"
#include "core/logging.h"
#include "core/mpris_common.h"
GlobalSearchService::GlobalSearchService(GlobalSearch* engine, QObject* parent)
: QObject(parent),
engine_(engine)
{
qDBusRegisterMetaType<GlobalSearchServiceResult>();
qDBusRegisterMetaType<GlobalSearchServiceResultList>();
new GlobalSearchAdaptor(this);
QDBusConnection::sessionBus().registerObject("/GlobalSearch", this);
connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)),
this, SLOT(ResultsAvailableSlot(int,SearchProvider::ResultList)),
Qt::QueuedConnection);
connect(engine_, SIGNAL(SearchFinished(int)),
this, SLOT(SearchFinishedSlot(int)),
Qt::QueuedConnection);
connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)),
this, SLOT(ArtLoadedSlot(int,QPixmap)),
Qt::QueuedConnection);
}
int GlobalSearchService::StartSearch(const QString& query, bool prefetch_art) {
PendingSearch pending_search;
pending_search.prefetch_art_ = prefetch_art;
const int id = engine_->SearchAsync(query);
pending_searches_[id] = pending_search;
return id;
}
void GlobalSearchService::CancelSearch(int id) {
if (!pending_searches_.contains(id))
return;
engine_->CancelSearch(id);
pending_searches_.remove(id);
}
void GlobalSearchService::ResultsAvailableSlot(int id, const SearchProvider::ResultList& results) {
if (!pending_searches_.contains(id))
return;
const PendingSearch& pending = pending_searches_[id];
GlobalSearchServiceResultList ret;
foreach (const SearchProvider::Result& result, results) {
const int result_id = next_result_id_ ++;
RecentResult* recent = &recent_results_[result_id];
recent->result_.art_on_the_way_ = false;
// Prefetch art if it was requested
if (pending.prefetch_art_ && !result.provider_->art_is_probably_remote()) {
const int art_id = engine_->LoadArtAsync(result);
prefetching_art_[art_id] = result_id;
recent->result_.art_on_the_way_ = true;
}
// Build the result to send back
recent->result_.result_id_ = result_id;
recent->result_.provider_name_ = result.provider_->name();
recent->result_.type_ = result.type_;
recent->result_.match_quality_ = result.match_quality_;
recent->result_.album_size_ = result.album_size_;
recent->result_.title_ = result.metadata_.title();
recent->result_.artist_ = result.metadata_.artist();
recent->result_.album_ = result.metadata_.album();
recent->result_.album_artist_ = result.metadata_.albumartist();
recent->result_.is_compilation_ = result.metadata_.is_compilation();
recent->result_.track_ = result.metadata_.track();
ret << recent->result_;
}
emit ResultsAvailable(id, ret);
}
void GlobalSearchService::SearchFinishedSlot(int id) {
if (!pending_searches_.contains(id))
return;
emit SearchFinished(id);
pending_searches_.remove(id);
}
void GlobalSearchService::ArtLoadedSlot(int id, const QPixmap& pixmap) {
QMap<int, int>::iterator it = prefetching_art_.find(id);
if (it == prefetching_art_.end())
return;
const int result_id = prefetching_art_.take(id);
QMap<int, RecentResult>::iterator it2 = recent_results_.find(result_id);
if (it2 == recent_results_.end())
return;
// Encode the pixmap as a png
QBuffer buf;
buf.open(QIODevice::WriteOnly);
pixmap.toImage().save(&buf, "PNG");
buf.close();
emit ArtLoaded(result_id, buf.data());
}

View File

@ -1,74 +0,0 @@
/* This file is part of Clementine.
Copyright 2011, 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 GLOBALSEARCHSERVICE_H
#define GLOBALSEARCHSERVICE_H
#include <QDBusArgument>
#include <QObject>
#include <QSet>
#include "common.h"
#include "searchprovider.h"
class GlobalSearch;
class QEventLoop;
class GlobalSearchService : public QObject {
Q_OBJECT
public:
GlobalSearchService(GlobalSearch* engine, QObject* parent = 0);
public slots:
int StartSearch(const QString& query, bool prefetch_art);
void CancelSearch(int id);
signals:
void ResultsAvailable(int id, const GlobalSearchServiceResultList& results);
void SearchFinished(int id);
void ArtLoaded(int result_id, const QByteArray& image_data);
private slots:
void ResultsAvailableSlot(int id, const SearchProvider::ResultList& results);
void SearchFinishedSlot(int id);
void ArtLoadedSlot(int id, const QPixmap& pixmap);
private:
struct PendingSearch {
bool prefetch_art_;
};
struct RecentResult {
GlobalSearchServiceResult result_;
};
GlobalSearch* engine_;
// GlobalSearch request ids
QMap<int, PendingSearch> pending_searches_;
// Result ids
QMap<int, RecentResult> recent_results_;
QMap<int, int> prefetching_art_; // LoadArt id -> result id
int next_result_id_;
};
#endif // GLOBALSEARCHSERVICE_H

View File

@ -15,8 +15,8 @@
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "globalsearchwidget.h"
#include "globalsearchsortmodel.h"
#include "globalsearchview.h"
#include "searchprovider.h"
#include "core/logging.h"
@ -26,22 +26,38 @@ GlobalSearchSortModel::GlobalSearchSortModel(QObject* parent)
}
bool GlobalSearchSortModel::lessThan(const QModelIndex& left, const QModelIndex& right) const {
const SearchProvider::Result r1 = left.data(GlobalSearchWidget::Role_PrimaryResult)
.value<SearchProvider::Result>();
const SearchProvider::Result r2 = right.data(GlobalSearchWidget::Role_PrimaryResult)
.value<SearchProvider::Result>();
// Compare the provider sort index first.
const int index_left = left.data(GlobalSearchView::Role_ProviderIndex).toInt();
const int index_right = right.data(GlobalSearchView::Role_ProviderIndex).toInt();
if (index_left < index_right) return true;
if (index_left > index_right) return false;
// Order results that arrived first first, so that the results don't jump
// around while the user is trying to navigate through them.
const int order_left = left.data(GlobalSearchWidget::Role_OrderArrived).toInt();
const int order_right = right.data(GlobalSearchWidget::Role_OrderArrived).toInt();
// Dividers always go first
if (left.data(LibraryModel::Role_IsDivider).toBool()) return true;
if (right.data(LibraryModel::Role_IsDivider).toBool()) return false;
if (order_left < order_right) return true;
if (order_left > order_right) return false;
// Containers go before songs if they're at the same level
const bool left_is_container = left.data(LibraryModel::Role_ContainerType).isValid();
const bool right_is_container = right.data(LibraryModel::Role_ContainerType).isValid();
if (left_is_container && !right_is_container) return true;
if (right_is_container && !left_is_container) return false;
// Containers get sorted on their sort text.
if (left_is_container) {
return QString::localeAwareCompare(
left.data(LibraryModel::Role_SortText).toString(),
right.data(LibraryModel::Role_SortText).toString()) < 0;
}
// Otherwise we're comparing songs. Sort by disc, track, then title.
const SearchProvider::Result r1 = left.data(GlobalSearchView::Role_Result)
.value<SearchProvider::Result>();
const SearchProvider::Result r2 = right.data(GlobalSearchView::Role_Result)
.value<SearchProvider::Result>();
#define CompareInt(field) \
if (r1.field < r2.field) return true; \
if (r1.field > r2.field) return false
if (r1.metadata_.field() < r2.metadata_.field()) return true; \
if (r1.metadata_.field() > r2.metadata_.field()) return false
int ret = 0;
@ -50,21 +66,9 @@ bool GlobalSearchSortModel::lessThan(const QModelIndex& left, const QModelIndex&
if (ret < 0) return true; \
if (ret > 0) return false
// If they arrived at the same time then sort by quality and type.
CompareInt(match_quality_);
CompareInt(type_);
// Failing that, compare title, artist and album
switch (r1.type_) {
case globalsearch::Type_Track:
case globalsearch::Type_Stream:
CompareString(title);
// fallthrough
case globalsearch::Type_Album:
CompareString(artist);
CompareString(album);
break;
}
CompareInt(disc);
CompareInt(track);
CompareString(title);
return false;

View File

@ -1,261 +0,0 @@
/* 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 "globalsearchtooltip.h"
#include "tooltipactionwidget.h"
#include "tooltipresultwidget.h"
#include "core/logging.h"
#include "core/utilities.h"
#include <QAction>
#include <QApplication>
#include <QDesktopWidget>
#include <QKeyEvent>
#include <QLayoutItem>
#include <QPainter>
#include <QSettings>
#include <QVBoxLayout>
const qreal GlobalSearchTooltip::kBorderRadius = 8.0;
const qreal GlobalSearchTooltip::kTotalBorderWidth = 4.0;
const qreal GlobalSearchTooltip::kOuterBorderWidth = 0.5;
const qreal GlobalSearchTooltip::kArrowWidth = 10.0;
const qreal GlobalSearchTooltip::kArrowHeight = 10.0;
GlobalSearchTooltip::GlobalSearchTooltip(QWidget* event_target)
: QWidget(NULL),
desktop_(qApp->desktop()),
event_target_(event_target),
active_result_(0),
show_tooltip_help_(true)
{
setWindowFlags(Qt::FramelessWindowHint | Qt::Popup);
setFocusPolicy(Qt::NoFocus);
setAttribute(Qt::WA_NoSystemBackground);
setAttribute(Qt::WA_TranslucentBackground);
switch_action_ = new QAction(tr("Switch provider"), this);
switch_action_->setShortcut(QKeySequence(Qt::Key_Tab));
connect(switch_action_, SIGNAL(triggered()), SLOT(SwitchProvider()));
ReloadSettings();
}
void GlobalSearchTooltip::SetResults(const SearchProvider::ResultList& results) {
results_ = results;
qDeleteAll(widgets_);
widgets_.clear();
result_buttons_.clear();
active_result_ = 0;
// Using a QVBoxLayout here made some weird flickering that I couldn't figure
// out how to fix, so do layout manually.
int w = 0;
int y = 9;
// Add a widget for each result
foreach (const SearchProvider::Result& result, results) {
TooltipResultWidget* widget = new TooltipResultWidget(result, this);
if (widgets_.isEmpty()) {
// If this is the first widget then mark it as selected
widget->setChecked(true);
}
AddWidget(widget, &w, &y);
result_buttons_ << widget;
}
if (show_tooltip_help_) {
// Add the action widget
QList<QAction*> actions;
if (results_.count() > 1) {
actions.append(switch_action_);
}
actions.append(common_actions_);
action_widget_ = new TooltipActionWidget(this);
action_widget_->SetActions(actions);
AddWidget(action_widget_, &w, &y);
}
// Set the width of each widget
foreach (QWidget* widget, widgets_) {
widget->resize(w, widget->sizeHint().height());
}
// Resize this widget
y += 9;
resize(w, y);
inner_rect_ = rect().adjusted(
kArrowWidth + kTotalBorderWidth, kTotalBorderWidth,
-kTotalBorderWidth, -kTotalBorderWidth);
foreach (QWidget* widget, widgets_) {
widget->setMask(inner_rect_);
}
}
void GlobalSearchTooltip::AddWidget(QWidget* widget, int* w, int* y) {
widget->move(0, *y);
widget->show();
widgets_ << widget;
QSize size_hint(widget->sizeHint());
*y += size_hint.height();
*w = qMax(*w, size_hint.width());
}
void GlobalSearchTooltip::ShowAt(const QPoint& pointing_to) {
const qreal min_arrow_offset = kBorderRadius + kArrowHeight;
const QRect screen = desktop_->screenGeometry(this);
arrow_offset_ = min_arrow_offset +
qMax(0, pointing_to.y() + height() - screen.bottom());
move(pointing_to.x(), pointing_to.y() - arrow_offset_);
if (!isVisible())
show();
}
bool GlobalSearchTooltip::event(QEvent* e) {
switch (e->type()) {
case QEvent::KeyPress: {
QKeyEvent* ke = static_cast<QKeyEvent*>(e);
if (ke->key() == Qt::Key_Tab && ke->modifiers() == Qt::NoModifier) {
SwitchProvider();
e->accept();
return true;
}
// fallthrough
}
case QEvent::KeyRelease:
case QEvent::InputMethod:
case QEvent::Shortcut:
case QEvent::ShortcutOverride:
if (QApplication::sendEvent(event_target_, e)) {
return true;
}
break;
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
if (!underMouse()) {
const QMouseEvent* me = static_cast<QMouseEvent*>(e);
QWidget* child = event_target_->childAt(
event_target_->mapFromGlobal(me->globalPos()));
if (child)
child->setAttribute(Qt::WA_UnderMouse, true);
Utilities::ForwardMouseEvent(me, child ? child : event_target_);
return true;
}
break;
default:
break;
}
return QWidget::event(e);
}
void GlobalSearchTooltip::paintEvent(QPaintEvent*) {
QPainter p(this);
const QColor outer_color(0, 0, 0, 192);
const QColor inner_color = palette().color(QPalette::Highlight);
const QColor center_color = palette().color(QPalette::Base);
// Transparent background
p.fillRect(rect(), Qt::transparent);
QRect area(inner_rect_.adjusted(
-kTotalBorderWidth/2, -kTotalBorderWidth/2,
kTotalBorderWidth/2, kTotalBorderWidth/2));
// Draw the border
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setRenderHint(QPainter::Antialiasing);
p.setPen(QPen(outer_color, kTotalBorderWidth));
p.drawRoundedRect(area, kBorderRadius, kBorderRadius);
// Draw the arrow
QPolygonF arrow;
arrow << QPointF(kArrowWidth + 2, arrow_offset_ - kArrowHeight)
<< QPointF(0, arrow_offset_)
<< QPointF(kArrowWidth + 2, arrow_offset_ + kArrowHeight);
p.setBrush(outer_color);
p.setPen(outer_color);
p.drawPolygon(arrow);
// Now draw the inner shapes on top
const qreal inner_border_width = kTotalBorderWidth - kOuterBorderWidth;
QRect inner_area(inner_rect_.adjusted(
-inner_border_width/2, -inner_border_width/2,
inner_border_width/2, inner_border_width/2));
// Inner border
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
p.setBrush(center_color);
p.setPen(QPen(inner_color, inner_border_width));
p.drawRoundedRect(inner_area, kBorderRadius, kBorderRadius);
// Inner arrow
arrow[0].setY(arrow[0].y() + kOuterBorderWidth + 0.5);
arrow[1].setX(arrow[1].x() + kOuterBorderWidth + 1.5);
arrow[2].setY(arrow[2].y() - kOuterBorderWidth - 0.5);
p.setBrush(inner_color);
p.setPen(inner_color);
p.drawPolygon(arrow);
}
void GlobalSearchTooltip::SwitchProvider() {
// Find which one was checked before.
int old_index = -1;
for (int i=0 ; i<result_buttons_.count() ; ++i) {
if (result_buttons_[i]->isChecked()) {
old_index = i;
break;
}
}
if (old_index == -1)
return;
// Check the new one. The auto exclusive group will take care of unchecking
// the old one.
active_result_ = (old_index + 1) % result_buttons_.count();
result_buttons_[active_result_]->setChecked(true);
}
void GlobalSearchTooltip::ReloadSettings() {
QSettings s;
s.beginGroup(GlobalSearch::kSettingsGroup);
show_tooltip_help_ = s.value("tooltip_help", true).toBool();
}

View File

@ -1,83 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef GLOBALSEARCHTOOLTIP_H
#define GLOBALSEARCHTOOLTIP_H
#include "searchprovider.h"
#include <QWidget>
class QAbstractButton;
class QDesktopWidget;
class TooltipActionWidget;
class GlobalSearchTooltip : public QWidget {
Q_OBJECT
public:
GlobalSearchTooltip(QWidget* event_target);
static const qreal kBorderRadius;
static const qreal kTotalBorderWidth;
static const qreal kOuterBorderWidth;
static const qreal kArrowWidth;
static const qreal kArrowHeight;
void SetActions(const QList<QAction*>& actions) { common_actions_ = actions; }
void SetResults(const SearchProvider::ResultList& results);
void ShowAt(const QPoint& pointing_to);
int ActiveResultIndex() const { return active_result_; }
qreal ArrowOffset() const;
bool event(QEvent* e);
public slots:
void ReloadSettings();
protected:
void paintEvent(QPaintEvent*);
private slots:
void SwitchProvider();
private:
void AddWidget(QWidget* widget, int* w, int* y);
private:
QDesktopWidget* desktop_;
TooltipActionWidget* action_widget_;
QList<QAction*> common_actions_;
QAction* switch_action_;
SearchProvider::ResultList results_;
qreal arrow_offset_;
QRect inner_rect_;
QWidget* event_target_;
QWidgetList widgets_;
QList<QAbstractButton*> result_buttons_;
int active_result_;
bool show_tooltip_help_;
};
#endif // GLOBALSEARCHTOOLTIP_H

View File

@ -0,0 +1,278 @@
/* 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 "globalsearch.h"
#include "globalsearchsortmodel.h"
#include "globalsearchview.h"
#include "searchprovider.h"
#include "ui_globalsearchview.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/mimedata.h"
#include "library/librarymodel.h"
#include "library/libraryview.h"
#include <QSortFilterProxyModel>
#include <QStandardItem>
#include <QTimer>
const int GlobalSearchView::kSwapModelsTimeoutMsec = 250;
GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
: QWidget(parent),
app_(app),
engine_(app_->global_search()),
ui_(new Ui_GlobalSearchView),
last_search_id_(0),
front_model_(new QStandardItemModel(this)),
back_model_(new QStandardItemModel(this)),
current_model_(front_model_),
front_proxy_(new GlobalSearchSortModel(this)),
back_proxy_(new GlobalSearchSortModel(this)),
current_proxy_(front_proxy_),
swap_models_timer_(new QTimer(this))
{
ui_->setupUi(this);
connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString)));
ui_->results->setItemDelegate(new LibraryItemDelegate(this));
group_by_[0] = LibraryModel::GroupBy_Artist;
group_by_[1] = LibraryModel::GroupBy_Album;
group_by_[2] = LibraryModel::GroupBy_None;
// Set up the sorting proxy model
front_proxy_->setSourceModel(front_model_);
front_proxy_->setDynamicSortFilter(true);
front_proxy_->sort(0);
back_proxy_->setSourceModel(back_model_);
back_proxy_->setDynamicSortFilter(true);
back_proxy_->sort(0);
swap_models_timer_->setSingleShot(true);
swap_models_timer_->setInterval(kSwapModelsTimeoutMsec);
connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels()));
// 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.
connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)),
SLOT(AddResults(int,SearchProvider::ResultList)),
Qt::QueuedConnection);
connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap)),
Qt::QueuedConnection);
connect(engine_, SIGNAL(TracksLoaded(int,MimeData*)), SLOT(TracksLoaded(int,MimeData*)),
Qt::QueuedConnection);
}
GlobalSearchView::~GlobalSearchView() {
delete ui_;
}
void GlobalSearchView::StartSearch(const QString& query) {
ui_->search->set_text(query);
TextEdited(query.trimmed());
// Swap models immediately
swap_models_timer_->stop();
SwapModels();
}
void GlobalSearchView::TextEdited(const QString& text) {
const QString trimmed(text.trimmed());
// Add results to the back model, switch models after some delay.
provider_sort_indices_.clear();
containers_.clear();
next_provider_sort_index_ = 1000;
back_model_->clear();
current_model_ = back_model_;
current_proxy_ = back_proxy_;
swap_models_timer_->start();
// Cancel the last search (if any) and start the new one.
engine_->CancelSearch(last_search_id_);
// If text query is empty, don't start a new search
if (trimmed.isEmpty()) {
last_search_id_ = -1;
return;
}
last_search_id_ = engine_->SearchAsync(trimmed);
}
void GlobalSearchView::AddResults(int id, const SearchProvider::ResultList& results) {
if (id != last_search_id_ || results.isEmpty())
return;
int sort_index = 0;
// Create a divider for this provider if we haven't seen it before.
SearchProvider* provider = results.first().provider_;
if (!provider_sort_indices_.contains(provider)) {
// TODO: Check if the user has configured a sort order for this provider.
sort_index = next_provider_sort_index_ ++;
QStandardItem* divider = new QStandardItem(provider->icon(), provider->name());
divider->setData(true, LibraryModel::Role_IsDivider);
divider->setData(sort_index, Role_ProviderIndex);
current_model_->appendRow(divider);
provider_sort_indices_[provider] = sort_index;
} else {
sort_index = provider_sort_indices_[provider];
}
foreach (const SearchProvider::Result& result, results) {
QStandardItem* parent = current_model_->invisibleRootItem();
// Find (or create) the container nodes for this result if we can.
if (result.group_automatically_) {
ContainerKey key;
key.provider_index_ = sort_index;
parent = BuildContainers(result.metadata_, parent, &key);
}
// Create the item
QStandardItem* item = new QStandardItem(result.metadata_.title());
item->setData(QVariant::fromValue(result), Role_Result);
item->setData(sort_index, Role_ProviderIndex);
parent->appendRow(item);
}
}
QStandardItem* GlobalSearchView::BuildContainers(
const Song& s, QStandardItem* parent, ContainerKey* key, int level) {
if (level >= 3) {
return parent;
}
QString display_text;
QString sort_text;
int year = 0;
switch (group_by_[level]) {
case LibraryModel::GroupBy_Artist:
display_text = LibraryModel::TextOrUnknown(s.artist());
sort_text = LibraryModel::SortTextForArtist(s.artist());
break;
case LibraryModel::GroupBy_YearAlbum:
year = qMax(0, s.year());
display_text = LibraryModel::PrettyYearAlbum(year, s.album());
sort_text = LibraryModel::SortTextForYear(year) + s.album();
break;
case LibraryModel::GroupBy_Year:
year = qMax(0, s.year());
display_text = QString::number(year);
sort_text = LibraryModel::SortTextForYear(year) + " ";
break;
case LibraryModel::GroupBy_Composer: display_text = s.composer();
case LibraryModel::GroupBy_Genre: if (display_text.isNull()) display_text = s.genre();
case LibraryModel::GroupBy_Album: if (display_text.isNull()) display_text = s.album();
case LibraryModel::GroupBy_AlbumArtist: if (display_text.isNull()) display_text = s.effective_albumartist();
display_text = LibraryModel::TextOrUnknown(display_text);
sort_text = LibraryModel::SortTextForArtist(display_text);
break;
case LibraryModel::GroupBy_FileType:
display_text = s.TextForFiletype();
sort_text = display_text;
break;
case LibraryModel::GroupBy_None:
return parent;
}
// Find a container for this level
key->group_[level] = display_text;
QStandardItem* container = containers_[*key];
if (!container) {
container = new QStandardItem(display_text);
container->setData(key->provider_index_, Role_ProviderIndex);
container->setData(sort_text, LibraryModel::Role_SortText);
container->setData(group_by_[level], LibraryModel::Role_ContainerType);
parent->appendRow(container);
containers_[*key] = container;
}
// Create the container for the next level.
return BuildContainers(s, container, key, level + 1);
}
void GlobalSearchView::SwapModels() {
art_requests_.clear();
qSwap(front_model_, back_model_);
qSwap(front_proxy_, back_proxy_);
ui_->results->setModel(front_proxy_);
}
void GlobalSearchView::LazyLoadArt(const QModelIndex& proxy_index) {
if (!proxy_index.isValid() || proxy_index.data(Role_LazyLoadingArt).isValid()) {
return;
}
if (proxy_index.model() != front_proxy_) {
return;
}
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
front_model_->itemFromIndex(source_index)->setData(true, Role_LazyLoadingArt);
const SearchProvider::Result result =
source_index.data(Role_Result).value<SearchProvider::Result>();
int id = engine_->LoadArtAsync(result);
art_requests_[id] = source_index;
}
void GlobalSearchView::ArtLoaded(int id, const QPixmap& pixmap) {
if (!art_requests_.contains(id))
return;
QModelIndex index = art_requests_.take(id);
front_model_->itemFromIndex(index)->setData(pixmap, Qt::DecorationRole);
}
void GlobalSearchView::LoadTracks() {
QModelIndex index = ui_->results->currentIndex();
if (!index.isValid())
index = front_proxy_->index(0, 0);
if (!index.isValid())
return;
const SearchProvider::Result result =
index.data(Role_Result).value<SearchProvider::Result>();
engine_->LoadTracksAsync(result);
}
void GlobalSearchView::TracksLoaded(int id, MimeData* mime_data) {
if (!mime_data)
return;
mime_data->from_doubleclick_ = true;
emit AddToPlaylist(mime_data);
}

View File

@ -0,0 +1,133 @@
/* 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 GLOBALSEARCHVIEW_H
#define GLOBALSEARCHVIEW_H
#include "searchprovider.h"
#include "library/librarymodel.h"
#include "ui/settingsdialog.h"
#include <QWidget>
class Application;
class Ui_GlobalSearchView;
class QMimeData;
class QSortFilterProxyModel;
class QStandardItem;
class QStandardItemModel;
class GlobalSearchView : public QWidget {
Q_OBJECT
public:
GlobalSearchView(Application* app, QWidget* parent = 0);
~GlobalSearchView();
static const int kSwapModelsTimeoutMsec;
enum Role {
Role_Result = LibraryModel::LastRole,
Role_LazyLoadingArt,
Role_ProviderIndex,
LastRole
};
struct ContainerKey {
int provider_index_;
QString group_[3];
};
// Called by the delegate
void LazyLoadArt(const QModelIndex& index);
public slots:
void StartSearch(const QString& query);
signals:
void AddToPlaylist(QMimeData* data);
void OpenSettingsAtPage(SettingsDialog::Page page);
private slots:
void SwapModels();
void TextEdited(const QString& text);
void AddResults(int id, const SearchProvider::ResultList& results);
void ArtLoaded(int id, const QPixmap& pixmap);
void TracksLoaded(int id, MimeData* mime_data);
private:
void LoadTracks();
QStandardItem* BuildContainers(const Song& metadata, QStandardItem* parent,
ContainerKey* key, int level = 0);
private:
Application* app_;
GlobalSearch* engine_;
Ui_GlobalSearchView* ui_;
int last_search_id_;
// Like graphics APIs have a front buffer and a back buffer, there's a front
// model and a back model - the front model is the one that's shown in the
// UI and the back model is the one that lies in wait. current_model_ will
// point to either the front or the back model.
QStandardItemModel* front_model_;
QStandardItemModel* back_model_;
QStandardItemModel* current_model_;
QSortFilterProxyModel* front_proxy_;
QSortFilterProxyModel* back_proxy_;
QSortFilterProxyModel* current_proxy_;
QTimer* swap_models_timer_;
LibraryModel::Grouping group_by_;
QMap<SearchProvider*, int> provider_sort_indices_;
int next_provider_sort_index_;
QMap<ContainerKey, QStandardItem*> containers_;
QMap<int, QAction*> track_requests_;
QMap<int, QModelIndex> art_requests_;
};
inline uint qHash(const GlobalSearchView::ContainerKey& key) {
return qHash(key.provider_index_)
^ qHash(key.group_[0])
^ qHash(key.group_[1])
^ qHash(key.group_[2]);
}
inline bool operator <(const GlobalSearchView::ContainerKey& left,
const GlobalSearchView::ContainerKey& right) {
#define CMP(field) \
if (left.field < right.field) return true; \
if (left.field > right.field) return false
CMP(provider_index_);
CMP(group_[0]);
CMP(group_[1]);
CMP(group_[2]);
return false;
#undef CMP
}
#endif // GLOBALSEARCHVIEW_H

View File

@ -1,40 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GlobalSearchWidget</class>
<widget class="QWidget" name="GlobalSearchWidget">
<class>GlobalSearchView</class>
<widget class="QWidget" name="GlobalSearchView">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>522</width>
<height>53</height>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="LineEdit" name="search">
<property name="toolTip">
<string>Search around all your sources (library, internet services, ...)</string>
</property>
<property name="hint" stdset="0">
<string>Search for anything</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="settings">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
<widget class="QTreeView" name="results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="autoRaise">
<property name="dragEnabled">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>

View File

@ -1,818 +0,0 @@
/* 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 "config.h"
#include "globalsearch.h"
#include "globalsearchitemdelegate.h"
#include "globalsearchsortmodel.h"
#include "globalsearchtooltip.h"
#include "globalsearchwidget.h"
#include "ui_globalsearchwidget.h"
#include "core/logging.h"
#include "core/stylesheetloader.h"
#include "core/utilities.h"
#include "playlist/playlistview.h"
#include "playlist/songmimedata.h"
#include "ui/qt_blurimage.h"
#include "widgets/stylehelper.h"
#include <QDesktopWidget>
#include <QListView>
#include <QPainter>
#include <QSettings>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QTimer>
#include <QToolButton>
#include <QToolTip>
const int GlobalSearchWidget::kMinVisibleItems = 3;
const int GlobalSearchWidget::kMaxVisibleItems = 25;
const int GlobalSearchWidget::kSwapModelsTimeoutMsec = 250;
const int GlobalSearchWidget::kSuggestionTimeoutMsec = 60000; // 1 minute
const int GlobalSearchWidget::kSuggestionCount = 3;
GlobalSearchWidget::GlobalSearchWidget(QWidget* parent)
: QWidget(parent),
ui_(new Ui_GlobalSearchWidget),
engine_(NULL),
last_id_(0),
order_arrived_counter_(0),
closed_since_search_began_(false),
front_model_(new QStandardItemModel(this)),
back_model_(new QStandardItemModel(this)),
current_model_(front_model_),
front_proxy_(new GlobalSearchSortModel(this)),
back_proxy_(new GlobalSearchSortModel(this)),
current_proxy_(front_proxy_),
view_(new QListView),
consume_focus_out_(false),
swap_models_timer_(new QTimer(this)),
background_(":allthethings.png"),
desktop_(qApp->desktop()),
show_tooltip_(true),
combine_identical_results_(true),
next_suggestion_timer_(new QTimer(this))
{
ui_->setupUi(this);
ReloadSettings();
// Set up the sorting proxy model
front_proxy_->setSourceModel(front_model_);
front_proxy_->setDynamicSortFilter(true);
front_proxy_->sort(0);
back_proxy_->setSourceModel(back_model_);
back_proxy_->setDynamicSortFilter(true);
back_proxy_->sort(0);
combine_cache_[front_model_] = new CombineCache(front_model_);
combine_cache_[back_model_] = new CombineCache(back_model_);
// Set up the popup
view_->setObjectName("popup");
view_->setWindowFlags(Qt::Popup);
view_->setFocusPolicy(Qt::NoFocus);
view_->setFocusProxy(ui_->search);
view_->installEventFilter(this);
view_->setModel(front_proxy_);
view_->setItemDelegate(new GlobalSearchItemDelegate(this));
view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
view_->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui_->search->installEventFilter(this);
// Actions
add_ = new QAction(tr("Add to playlist"), this);
add_and_play_ = new QAction(tr("Add and play now"), this);
add_and_queue_ = new QAction(tr("Queue track"), this);
replace_ = new QAction(tr("Replace current playlist"), this);
replace_and_play_ = new QAction(tr("Replace and play now"), this);
add_->setShortcut(QKeySequence(Qt::Key_Return));
add_and_play_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return));
add_and_queue_->setShortcut(QKeySequence(Qt::SHIFT | Qt::Key_Return));
replace_->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Return));
replace_and_play_->setShortcut(QKeySequence(Qt::ALT | Qt::CTRL | Qt::Key_Return));
connect(add_, SIGNAL(triggered()), SLOT(AddCurrent()));
connect(add_and_play_, SIGNAL(triggered()), SLOT(AddAndPlayCurrent()));
connect(add_and_queue_, SIGNAL(triggered()), SLOT(AddAndQueueCurrent()));
connect(replace_, SIGNAL(triggered()), SLOT(ReplaceCurrent()));
connect(replace_and_play_, SIGNAL(triggered()), SLOT(ReplaceAndPlayCurrent()));
actions_ << add_ << add_and_play_ << add_and_queue_ << replace_
<< replace_and_play_;
// Load style sheets
StyleSheetLoader* style_loader = new StyleSheetLoader(this);
style_loader->SetStyleSheet(this, ":globalsearch.css");
// Icons
ui_->settings->setIcon(IconLoader::Load("configure"));
swap_models_timer_->setSingleShot(true);
swap_models_timer_->setInterval(kSwapModelsTimeoutMsec);
next_suggestion_timer_->setInterval(kSuggestionTimeoutMsec);
hint_text_ = ui_->search->hint();
connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString)));
connect(view_, SIGNAL(doubleClicked(QModelIndex)), SLOT(ResultDoubleClicked()));
connect(view_->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
SLOT(UpdateTooltip()));
connect(swap_models_timer_, SIGNAL(timeout()), SLOT(SwapModels()));
connect(ui_->settings, SIGNAL(clicked()), SLOT(SettingsClicked()));
connect(next_suggestion_timer_, SIGNAL(timeout()), SLOT(NextSuggestion()));
}
GlobalSearchWidget::~GlobalSearchWidget() {
delete ui_;
qDeleteAll(combine_cache_.values());
}
void GlobalSearchWidget::Init(GlobalSearch* engine) {
engine_ = engine;
// 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.
connect(engine_, SIGNAL(ResultsAvailable(int,SearchProvider::ResultList)),
SLOT(AddResults(int,SearchProvider::ResultList)),
Qt::QueuedConnection);
connect(engine_, SIGNAL(ArtLoaded(int,QPixmap)), SLOT(ArtLoaded(int,QPixmap)),
Qt::QueuedConnection);
connect(engine_, SIGNAL(TracksLoaded(int,MimeData*)), SLOT(TracksLoaded(int,MimeData*)),
Qt::QueuedConnection);
view_->setStyle(new PlaylistProxyStyle(style()));
// The style helper's base color doesn't get initialised until after the
// constructor.
QPalette view_palette = view_->palette();
view_palette.setColor(QPalette::Text, Utils::StyleHelper::panelTextColor());
view_palette.setColor(QPalette::HighlightedText, QColor(60, 60, 60));
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(ui_->settings->mapTo(this, ui_->settings->rect().center()).x() -
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::hideEvent(QHideEvent* e) {
QWidget::hideEvent(e);
next_suggestion_timer_->stop();
}
void GlobalSearchWidget::showEvent(QShowEvent* e) {
QWidget::showEvent(e);
next_suggestion_timer_->start();
NextSuggestion();
}
void GlobalSearchWidget::TextEdited(const QString& text) {
const QString trimmed_text = text.trimmed();
closed_since_search_began_ = false;
// Add results to the back model, switch models after some delay.
back_model_->clear();
combine_cache_[back_model_]->Clear();
current_model_ = back_model_;
current_proxy_ = back_proxy_;
order_arrived_counter_ = 0;
swap_models_timer_->start();
// Cancel the last search (if any) and start the new one.
engine_->CancelSearch(last_id_);
// If text query is empty, don't start a new search
if (trimmed_text.isEmpty()) {
last_id_ = -1;
return;
}
last_id_ = engine_->SearchAsync(trimmed_text);
}
void GlobalSearchWidget::SwapModels() {
art_requests_.clear();
qSwap(front_model_, back_model_);
qSwap(front_proxy_, back_proxy_);
view_->setModel(front_proxy_);
connect(view_->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
SLOT(UpdateTooltip()));
if (!closed_since_search_began_)
RepositionPopup();
}
void GlobalSearchWidget::StartSearch(const QString& query) {
ui_->search->setText(query);
TextEdited(query);
// Swap models immediately
swap_models_timer_->stop();
SwapModels();
}
void GlobalSearchWidget::AddResults(int id, const SearchProvider::ResultList& results) {
if (id != last_id_)
return;
foreach (const SearchProvider::Result& result, results) {
QStandardItem* item = new QStandardItem;
item->setData(QVariant::fromValue(result), Role_PrimaryResult);
item->setData(QVariant::fromValue(SearchProvider::ResultList() << result), Role_AllResults);
item->setData(order_arrived_counter_, Role_OrderArrived);
QPixmap pixmap;
if (engine_->FindCachedPixmap(result, &pixmap)) {
item->setData(pixmap, Qt::DecorationRole);
}
current_model_->appendRow(item);
QModelIndex index = item->index();
combine_cache_[current_model_]->Insert(index);
if (combine_identical_results_) {
// Maybe we can combine this result with an identical result from another
// provider.
QModelIndexList candidates = combine_cache_[current_model_]->FindCandidates(index);
foreach (const QModelIndex& candidate, candidates) {
if (!candidate.isValid())
continue;
CombineAction action = CanCombineResults(index, candidate);
switch (action) {
case CannotCombine:
continue;
case LeftPreferred:
CombineResults(index, candidate);
break;
case RightPreferred:
CombineResults(candidate, index);
break;
}
// We've just invalidated the indexes so we have to stop.
break;
}
}
}
order_arrived_counter_ ++;
if (!closed_since_search_began_) {
RepositionPopup();
UpdateTooltipPosition();
}
}
void GlobalSearchWidget::RepositionPopup() {
if (front_model_->rowCount() == 0) {
HidePopup(false);
return;
}
closed_since_search_began_ = false;
int h = view_->sizeHintForRow(0) * float(0.5 +
qBound(kMinVisibleItems, front_model_->rowCount(), kMaxVisibleItems));
int w = ui_->search->width();
const QPoint pos = ui_->search->mapToGlobal(ui_->search->rect().bottomLeft());
// Shrink the popup if it would otherwise go off the screen
const QRect screen = desktop_->availableGeometry(ui_->search);
h = qMin(h, screen.bottom() - pos.y());
view_->setGeometry(QRect(pos, QSize(w, h)));
if (!view_->isVisible()) {
view_->show();
ui_->search->setFocus();
}
}
bool GlobalSearchWidget::eventFilter(QObject* o, QEvent* e) {
if (o == ui_->search)
return EventFilterSearchWidget(o, e);
if (o == view_)
return EventFilterPopup(o, e);
return QWidget::eventFilter(o, e);
}
bool GlobalSearchWidget::EventFilterSearchWidget(QObject* o, QEvent* e) {
switch (e->type()) {
case QEvent::FocusOut:
if (consume_focus_out_ && view_->isVisible())
return true;
break;
case QEvent::FocusIn: {
QFocusEvent* fe = static_cast<QFocusEvent*>(e);
if (fe->reason() == Qt::MouseFocusReason) {
RepositionPopup();
}
break;
}
case QEvent::KeyPress: {
QKeyEvent* ke = static_cast<QKeyEvent*>(e);
const int key = ke->key();
switch (key) {
case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
// If we got one of these it means the popup wasn't visible, so show it
// now.
RepositionPopup();
return true;
case Qt::Key_Escape:
ui_->search->LineEditInterface::clear();
return true;
default:
break;
}
}
default:
break;
}
return QWidget::eventFilter(o, e);
}
bool GlobalSearchWidget::EventFilterPopup(QObject*, QEvent* e) {
// Most of this is borrowed from QCompleter::eventFilter
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(front_proxy_->index(front_proxy_->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(front_proxy_->index(0, 0));
return true;
} else if (cur_index.row() == front_proxy_->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
consume_focus_out_ = false;
(static_cast<QObject *>(ui_->search))->event(ke);
consume_focus_out_ = true;
if (e->isAccepted() || !view_->isVisible()) {
// widget lost focus, hide the popup
if (!ui_->search->hasFocus())
HidePopup(true);
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:
// Handle the QActions here - they don't activate when the tooltip is showing
if (ke->modifiers() & Qt::AltModifier && ke->modifiers() & Qt::ControlModifier)
replace_and_play_->trigger();
else if (ke->modifiers() & Qt::AltModifier)
replace_->trigger();
else if (ke->modifiers() & Qt::ControlModifier)
add_and_play_->trigger();
else if (ke->modifiers() & Qt::ShiftModifier)
add_and_queue_->trigger();
else
add_->trigger();
break;
case Qt::Key_F4:
if (ke->modifiers() & Qt::AltModifier)
HidePopup(true);
break;
case Qt::Key_Backtab:
case Qt::Key_Escape:
HidePopup(true);
break;
default:
break;
}
return true;
}
case QEvent::MouseButtonPress:
if (!view_->underMouse()) {
HidePopup(true);
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& proxy_index) {
if (!proxy_index.isValid() || proxy_index.data(Role_LazyLoadingArt).isValid()) {
return;
}
if (proxy_index.model() != front_proxy_) {
return;
}
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
front_model_->itemFromIndex(source_index)->setData(true, Role_LazyLoadingArt);
const SearchProvider::Result result =
source_index.data(Role_PrimaryResult).value<SearchProvider::Result>();
int id = engine_->LoadArtAsync(result);
art_requests_[id] = source_index;
}
void GlobalSearchWidget::ArtLoaded(int id, const QPixmap& pixmap) {
if (!art_requests_.contains(id))
return;
QModelIndex index = art_requests_.take(id);
front_model_->itemFromIndex(index)->setData(pixmap, Qt::DecorationRole);
}
void GlobalSearchWidget::ResultDoubleClicked() {
LoadTracks(NULL);
}
void GlobalSearchWidget::AddCurrent() {
LoadTracks(add_);
}
void GlobalSearchWidget::AddAndPlayCurrent() {
LoadTracks(add_and_play_);
}
void GlobalSearchWidget::AddAndQueueCurrent() {
LoadTracks(add_and_queue_);
}
void GlobalSearchWidget::ReplaceCurrent() {
LoadTracks(replace_);
}
void GlobalSearchWidget::ReplaceAndPlayCurrent() {
LoadTracks(replace_and_play_);
}
void GlobalSearchWidget::LoadTracks(QAction* trigger) {
QModelIndex index = view_->currentIndex();
if (!index.isValid())
index = front_proxy_->index(0, 0);
if (!index.isValid())
return;
int result_index = 0;
if (tooltip_ && tooltip_->isVisible()) {
result_index = tooltip_->ActiveResultIndex();
}
const SearchProvider::ResultList results =
index.data(Role_AllResults).value<SearchProvider::ResultList>();
if (result_index < 0 || result_index >= results.count())
return;
int id = engine_->LoadTracksAsync(results[result_index]);
track_requests_[id] = trigger;
}
void GlobalSearchWidget::TracksLoaded(int id, MimeData* mime_data) {
if (!track_requests_.contains(id))
return;
QAction* trigger = track_requests_.take(id);
if (!mime_data)
return;
if (trigger == NULL) {
mime_data->from_doubleclick_ = true;
} else {
if (trigger == add_and_play_) {
mime_data->override_user_settings_ = true;
mime_data->play_now_ = true;
} else if (trigger == add_and_queue_) {
mime_data->enqueue_now_ = true;
} else if (trigger == replace_) {
mime_data->clear_first_= true;
} else if (trigger == replace_and_play_) {
mime_data->clear_first_ = true;
mime_data->override_user_settings_ = true;
mime_data->play_now_ = true;
}
}
emit AddToPlaylist(mime_data);
}
void GlobalSearchWidget::ReloadSettings() {
QSettings s;
s.beginGroup(GlobalSearch::kSettingsGroup);
show_tooltip_ = s.value("tooltip", true).toBool();
combine_identical_results_ = s.value("combine_identical_results", true).toBool();
provider_order_ = s.value("provider_order", QStringList() << "library").toStringList();
setVisible(s.value("show_globalsearch", true).toBool());
if (tooltip_) {
tooltip_->ReloadSettings();
}
}
GlobalSearchWidget::CombineAction GlobalSearchWidget::CanCombineResults(
const QModelIndex& left, const QModelIndex& right) const {
const SearchProvider::Result r1 = left.data(Role_PrimaryResult)
.value<SearchProvider::Result>();
const SearchProvider::Result r2 = right.data(Role_PrimaryResult)
.value<SearchProvider::Result>();
// If you change the logic here remember to change CombineCache::Hash too.
if (r1.match_quality_ != r2.match_quality_ || r1.type_ != r2.type_)
return CannotCombine;
#define StringsDiffer(field) \
(QString::compare(r1.metadata_.field(), r2.metadata_.field(), Qt::CaseInsensitive) != 0)
switch (r1.type_) {
case globalsearch::Type_Track:
if (StringsDiffer(title))
return CannotCombine;
// fallthrough
case globalsearch::Type_Album:
if (StringsDiffer(album) || StringsDiffer(artist))
return CannotCombine;
break;
case globalsearch::Type_Stream:
if (StringsDiffer(url().toString))
return CannotCombine;
break;
}
#undef StringsDiffer
// They look the same - decide which provider we like best.
const int p1 = provider_order_.indexOf(r1.provider_->id());
const int p2 = provider_order_.indexOf(r2.provider_->id());
return p2 > p1 ? LeftPreferred : RightPreferred;
}
void GlobalSearchWidget::CombineResults(const QModelIndex& superior, const QModelIndex& inferior) {
QStandardItem* superior_item = current_model_->itemFromIndex(superior);
QStandardItem* inferior_item = current_model_->itemFromIndex(inferior);
SearchProvider::ResultList superior_results =
superior_item->data(Role_AllResults).value<SearchProvider::ResultList>();
SearchProvider::ResultList inferior_results =
inferior_item->data(Role_AllResults).value<SearchProvider::ResultList>();
superior_results.append(inferior_results);
superior_item->setData(QVariant::fromValue(superior_results), Role_AllResults);
combine_cache_[current_model_]->Remove(inferior_item->index());
current_model_->invisibleRootItem()->removeRow(inferior_item->row());
}
void GlobalSearchWidget::HidePopup(bool manual) {
if (manual) {
closed_since_search_began_ = true;
}
if (tooltip_)
tooltip_->hide();
view_->hide();
}
void GlobalSearchWidget::UpdateTooltip() {
if (!view_->isVisible() || !show_tooltip_) {
if (tooltip_)
tooltip_->hide();
return;
}
const QModelIndex current = view_->selectionModel()->currentIndex();
if (!current.isValid())
return;
const SearchProvider::ResultList results = current.data(Role_AllResults)
.value<SearchProvider::ResultList>();
if (!tooltip_) {
tooltip_.reset(new GlobalSearchTooltip(view_));
tooltip_->setFont(view_->font());
tooltip_->setPalette(view_->palette());
tooltip_->SetActions(actions_);
}
tooltip_->SetResults(results);
UpdateTooltipPosition();
}
void GlobalSearchWidget::UpdateTooltipPosition() {
if (!tooltip_ || !view_->isVisible())
return;
const QModelIndex current = view_->selectionModel()->currentIndex();
if (!current.isValid()) {
tooltip_->hide();
return;
}
const QRect item_rect = view_->visualRect(current);
const QPoint popup_pos = item_rect.topRight() +
QPoint(-GlobalSearchTooltip::kArrowWidth,
item_rect.height() / 2);
tooltip_->ShowAt(view_->mapToGlobal(popup_pos));
}
void GlobalSearchWidget::SettingsClicked() {
emit OpenSettingsAtPage(SettingsDialog::Page_GlobalSearch);
}
void GlobalSearchWidget::NextSuggestion() {
const QStringList suggestions = engine_->GetSuggestions(kSuggestionCount);
QString hint = hint_text_;
if (!suggestions.isEmpty()) {
hint += QString(", %1 ").arg(tr("e.g.")) + suggestions.join(", ");
}
ui_->search->set_hint(hint);
}
GlobalSearchWidget::CombineCache::CombineCache(QAbstractItemModel* model)
: model_(model)
{
}
uint GlobalSearchWidget::CombineCache::Hash(const QModelIndex& index) {
const SearchProvider::Result r = index.data(Role_PrimaryResult)
.value<SearchProvider::Result>();
uint ret = qHash(r.match_quality_) ^ qHash(r.type_);
switch (r.type_) {
case globalsearch::Type_Track:
ret ^= qHash(r.metadata_.title());
// fallthrough
case globalsearch::Type_Album:
ret ^= qHash(r.metadata_.album());
ret ^= qHash(r.metadata_.artist());
break;
case globalsearch::Type_Stream:
ret ^= qHash(r.metadata_.url().toString());
break;
}
return ret;
}
void GlobalSearchWidget::CombineCache::Insert(const QModelIndex& index) {
data_.insert(Hash(index), index.row());
}
void GlobalSearchWidget::CombineCache::Remove(const QModelIndex& index) {
// This is really inefficient but we're not doing it much - find any items
// with a row greater than this one and shuffle them down one.
for (QMultiMap<uint, int>::iterator it = data_.begin() ; it != data_.end() ; ++it) {
if (it.value() > index.row())
(*it) --;
}
// Now remove the row itself.
QMultiMap<uint, int>::iterator it = data_.find(Hash(index), index.row());
if (it != data_.end())
data_.erase(it);
}
QModelIndexList GlobalSearchWidget::CombineCache::FindCandidates(
const QModelIndex& result) const {
QModelIndexList ret;
foreach (int row, data_.values(Hash(result))) {
if (row != result.row()) {
ret << model_->index(row, 0);
}
}
return ret;
}
void GlobalSearchWidget::CombineCache::Clear() {
data_.clear();
}

View File

@ -1,194 +0,0 @@
/* 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 "ui/settingsdialog.h"
#include <QScopedPointer>
#include <QWidget>
#include <boost/bimap.hpp>
class GlobalSearch;
class GlobalSearchTooltip;
class LibraryBackendInterface;
class Ui_GlobalSearchWidget;
class QDesktopWidget;
class QListView;
class QMimeData;
class QModelIndex;
class QSortFilterProxyModel;
class QStandardItemModel;
class QToolButton;
class GlobalSearchWidget : public QWidget {
Q_OBJECT
public:
GlobalSearchWidget(QWidget* parent = 0);
~GlobalSearchWidget();
static const int kMinVisibleItems;
static const int kMaxVisibleItems;
static const int kSwapModelsTimeoutMsec;
static const int kSuggestionTimeoutMsec;
static const int kSuggestionCount;
enum Role {
Role_PrimaryResult = Qt::UserRole + 1,
Role_AllResults,
Role_LazyLoadingArt,
Role_OrderArrived
};
void Init(GlobalSearch* engine_);
// Called by the delegate
void LazyLoadArt(const QModelIndex& index);
// QObject
bool eventFilter(QObject* o, QEvent* e);
public slots:
void ReloadSettings();
void StartSearch(const QString& query);
signals:
void AddToPlaylist(QMimeData* data);
void OpenSettingsAtPage(SettingsDialog::Page page);
protected:
void resizeEvent(QResizeEvent* e);
void paintEvent(QPaintEvent* e);
void showEvent(QShowEvent* e);
void hideEvent(QHideEvent* e);
private slots:
void TextEdited(const QString& text);
void AddResults(int id, const SearchProvider::ResultList& results);
void ArtLoaded(int id, const QPixmap& pixmap);
void TracksLoaded(int id, MimeData* mime_data);
void ResultDoubleClicked();
void AddCurrent();
void AddAndPlayCurrent();
void AddAndQueueCurrent();
void ReplaceCurrent();
void ReplaceAndPlayCurrent();
void SettingsClicked();
void HidePopup(bool manual);
void UpdateTooltip();
void UpdateTooltipPosition();
void SwapModels();
void NextSuggestion();
private:
// Return values from CanCombineResults
enum CombineAction {
CannotCombine, // The two results are different and can't be combined
LeftPreferred, // The two results can be combined - the left one is better
RightPreferred // The two results can be combined - the right one is better
};
class CombineCache {
public:
CombineCache(QAbstractItemModel* model);
QModelIndexList FindCandidates(const QModelIndex& result) const;
void Insert(const QModelIndex& index);
void Remove(const QModelIndex& index);
void Clear();
static uint Hash(const QModelIndex& index);
private:
QAbstractItemModel* model_;
QMultiMap<uint, int> data_;
};
void RepositionPopup();
CombineAction CanCombineResults(const QModelIndex& left, const QModelIndex& right) const;
void CombineResults(const QModelIndex& superior, const QModelIndex& inferior);
bool EventFilterSearchWidget(QObject* o, QEvent* e);
bool EventFilterPopup(QObject* o, QEvent* e);
void LoadTracks(QAction* trigger);
private:
Ui_GlobalSearchWidget* ui_;
GlobalSearch* engine_;
int last_id_;
int order_arrived_counter_;
bool closed_since_search_began_;
QMap<int, QModelIndex> art_requests_;
QMap<int, QAction*> track_requests_;
// Like graphics APIs have a front buffer and a back buffer, there's a front
// model and a back model - the front model is the one that's shown in the
// UI and the back model is the one that lies in wait. current_model_ will
// point to either the front or the back model.
QStandardItemModel* front_model_;
QStandardItemModel* back_model_;
QStandardItemModel* current_model_;
QMap<QStandardItemModel*, CombineCache*> combine_cache_;
QSortFilterProxyModel* front_proxy_;
QSortFilterProxyModel* back_proxy_;
QSortFilterProxyModel* current_proxy_;
QListView* view_;
bool consume_focus_out_;
QTimer* swap_models_timer_;
QPixmap background_;
QPixmap background_scaled_;
QDesktopWidget* desktop_;
bool show_tooltip_;
bool combine_identical_results_;
QStringList provider_order_;
QScopedPointer<GlobalSearchTooltip> tooltip_;
QAction* add_;
QAction* add_and_play_;
QAction* add_and_queue_;
QAction* replace_;
QAction* replace_and_play_;
QList<QAction*> actions_;
QString hint_text_;
QTimer* next_suggestion_timer_;
};
#endif // GLOBALSEARCHWIDGET_H

View File

@ -66,15 +66,10 @@ void GroovesharkSearchProvider::SearchDone(int id, const SongList& songs) {
const PendingState state = pending_searches_.take(id);
const int global_search_id = state.orig_id_;
SongList songs_copy(songs);
SortSongs(&songs_copy);
ResultList ret;
foreach (const Song& song, songs_copy) {
foreach (const Song& song, songs) {
Result result(this);
result.type_ = globalsearch::Type_Track;
result.metadata_ = song;
result.match_quality_ = MatchQuality(state.tokens_, song.title());
ret << result;
}
@ -119,31 +114,10 @@ void GroovesharkSearchProvider::AlbumArtLoaded(quint64 id, const QImage& image)
}
void GroovesharkSearchProvider::LoadTracksAsync(int id, const Result& result) {
SongList ret;
switch (result.type_) {
case globalsearch::Type_Track: {
ret << result.metadata_;
SortSongs(&ret);
InternetSongMimeData* mime_data = new InternetSongMimeData(service_);
mime_data->songs = ret;
emit TracksLoaded(id, mime_data);
break;
}
case globalsearch::Type_Album: {
InternetSongMimeData* mime_data = new InternetSongMimeData(service_);
mime_data->songs = result.album_songs_;
emit TracksLoaded(id, mime_data);
break;
}
default:
Q_ASSERT(0);
}
InternetSongMimeData* mime_data = new InternetSongMimeData(service_);
mime_data->songs << result.metadata_;
emit TracksLoaded(id, mime_data);
}
bool GroovesharkSearchProvider::IsLoggedIn() {
@ -158,24 +132,12 @@ void GroovesharkSearchProvider::AlbumSongsLoaded(quint64 id, const SongList& son
const PendingState state = pending_searches_.take(id);
const int global_search_id = state.orig_id_;
ResultList ret;
if (!songs.isEmpty()) {
foreach (const Song& s, songs) {
Result result(this);
result.type_ = globalsearch::Type_Album;
const QString& artist = songs.last().artist();
const QString& album = songs.last().album();
result.metadata_.set_album(album);
result.metadata_.set_artist(artist);
result.metadata_.set_art_automatic(songs.last().art_automatic());
result.match_quality_ =
qMin(MatchQuality(state.tokens_, album),
MatchQuality(state.tokens_, artist));
foreach (const Song& s, songs) {
result.album_songs_ << s;
}
result.album_size_ = result.album_songs_.size();
result.metadata_ = s;
ret << result;
}
emit ResultsAvailable(global_search_id, ret);
MaybeSearchFinished(global_search_id);
}

View File

@ -30,16 +30,13 @@ SearchProvider::ResultList IcecastSearchProvider::Search(int id, const QString&
IcecastBackend::StationList stations = backend_->GetStations(query);
ResultList ret;
const QStringList tokens = TokenizeQuery(query);
foreach (const IcecastBackend::Station& station, stations) {
if (ret.count() > 3)
break;
Result result(this);
result.type_ = globalsearch::Type_Stream;
result.group_automatically_ = false;
result.metadata_ = station.ToSong();
result.match_quality_ = MatchQuality(tokens, station.name);
ret << result;
}

View File

@ -23,6 +23,8 @@
#include "library/sqlrow.h"
#include "playlist/songmimedata.h"
#include <QStack>
LibrarySearchProvider::LibrarySearchProvider(LibraryBackendInterface* backend,
const QString& name,
@ -55,58 +57,11 @@ SearchProvider::ResultList LibrarySearchProvider::Search(int id, const QString&
return ResultList();
}
const QStringList tokens = TokenizeQuery(query);
QMultiMap<QString, Song> albums;
QSet<QString> albums_with_non_track_matches;
// Build the result list
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());
}
globalsearch::MatchQuality quality = MatchQuality(tokens, song.title());
if (quality != globalsearch::Quality_None) {
// If the query matched in the song title then we're interested in this
// as an individual song.
Result result(this);
result.type_ = globalsearch::Type_Track;
result.metadata_ = song;
result.match_quality_ = quality;
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_ = globalsearch::Type_Album;
result.metadata_ = albums.value(key);
result.album_size_ = albums.count(key);
result.match_quality_ =
qMin(
MatchQuality(tokens, result.metadata_.albumartist()),
qMin(MatchQuality(tokens, result.metadata_.artist()),
MatchQuality(tokens, result.metadata_.album())));
result.album_songs_ = albums.values(key);
SortSongs(&result.album_songs_);
result.metadata_.InitFromQuery(q, true);
ret << result;
}
@ -114,44 +69,9 @@ SearchProvider::ResultList LibrarySearchProvider::Search(int id, const QString&
}
void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) {
SongList ret;
switch (result.type_) {
case globalsearch::Type_Track:
// This is really easy - we just emit the track again.
ret << result.metadata_;
break;
case globalsearch::Type_Album: {
// Find all the songs in this album.
LibraryQuery query;
query.SetColumnSpec("ROWID, " + Song::kColumnSpec);
query.AddCompilationRequirement(result.metadata_.is_compilation());
query.AddWhere("album", result.metadata_.album());
if (!result.metadata_.is_compilation())
query.AddWhere("artist", result.metadata_.artist());
if (!backend_->ExecQuery(&query)) {
break;
}
while (query.Next()) {
Song song;
song.InitFromQuery(query, true);
ret << song;
}
}
default:
break;
}
SortSongs(&ret);
SongMimeData* mime_data = new SongMimeData;
mime_data->backend = backend_;
mime_data->songs = ret;
mime_data->songs = SongList() << result.metadata_;
emit TracksLoaded(id, mime_data);
}

View File

@ -57,20 +57,14 @@ QStringList SearchProvider::TokenizeQuery(const QString& query) {
return tokens;
}
globalsearch::MatchQuality SearchProvider::MatchQuality(
const QStringList& tokens, const QString& string) {
globalsearch::MatchQuality ret = globalsearch::Quality_None;
bool SearchProvider::Matches(const QStringList& tokens, const QString& string) {
foreach (const QString& token, tokens) {
const int index = string.indexOf(token, 0, Qt::CaseInsensitive);
if (index == 0) {
return globalsearch::Quality_AtStart;
} else if (index != -1) {
ret = globalsearch::Quality_Middle;
if (!string.contains(token, Qt::CaseInsensitive)) {
return false;
}
}
return ret;
return true;
}
BlockingSearchProvider::BlockingSearchProvider(Application* app, QObject* parent)
@ -125,21 +119,6 @@ QImage SearchProvider::ScaleAndPad(const QImage& image) {
return padded_image;
}
namespace {
bool SortSongsCompare(const Song& left, const Song& right) {
if (left.disc() < right.disc())
return true;
if (left.disc() > right.disc())
return false;
return left.track() < right.track();
}
}
void SearchProvider::SortSongs(SongList* list) {
qStableSort(list->begin(), list->end(), SortSongsCompare);
}
void SearchProvider::LoadArtAsync(int id, const Result& result) {
emit ArtLoaded(id, QImage());
}

View File

@ -23,7 +23,6 @@
#include <QObject>
#include "core/song.h"
#include "globalsearch/common.h"
class Application;
class MimeData;
@ -39,23 +38,19 @@ public:
struct Result {
Result(SearchProvider* provider = 0)
: provider_(provider), album_size_(0) {}
: provider_(provider), group_automatically_(true) {}
// This must be set by the provider using the constructor.
SearchProvider* provider_;
// These must be set explicitly by the provider.
globalsearch::Type type_;
globalsearch::MatchQuality match_quality_;
// If this is set to true, the view will group this result into
// artist/album categories as appropriate.
bool group_automatically_;
// Must be set by the provider.
Song metadata_;
// How many songs in the album - valid only if type == Type_Album.
int album_size_;
// Songs in the album - valid only if type == Type_Album. This is only
// used for display in the tooltip, so it's fine not to provide it.
SongList album_songs_;
// This is set and used by the GlobalSearch engine itself.
QString pixmap_cache_key_;
};
typedef QList<Result> ResultList;
@ -85,7 +80,7 @@ public:
ArtIsInSongMetadata = 0x08,
// Indicates this provider has a config dialog that can be shown by calling
// CanShowConfig. If this is not set then the button will be greyed out
// ShowConfig. If this is not set then the button will be greyed out
// in the GUI.
CanShowConfig = 0x10,
@ -151,10 +146,7 @@ protected:
// 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 globalsearch::MatchQuality MatchQuality(const QStringList& tokens, const QString& string);
// Sorts a list of songs by disc, then by track.
static void SortSongs(SongList* list);
static bool Matches(const QStringList& tokens, const QString& string);
// Subclasses must call this from their constructors.
void Init(const QString& name, const QString& id, const QIcon& icon,

View File

@ -68,37 +68,18 @@ SearchProvider::ResultList SimpleSearchProvider::Search(int id, const QString& q
QMutexLocker l(&items_mutex_);
foreach (const Item& item, items_) {
Result result(this);
result.type_ = globalsearch::Type_Stream;
result.match_quality_ = globalsearch::Quality_None;
bool matched = true;
foreach (const QString& token, tokens) {
if (item.keyword_.startsWith(token, Qt::CaseInsensitive)) {
result.match_quality_ = globalsearch::Quality_AtStart;
continue;
}
if (!item.metadata_.title().contains(token, Qt::CaseInsensitive)) {
bool matched_safe_word = false;
foreach (const QString& safe_word, safe_words_) {
if (safe_word.startsWith(token, Qt::CaseInsensitive)) {
matched_safe_word = true;
break;
}
}
if (matched_safe_word)
continue;
result.match_quality_ = globalsearch::Quality_None;
if (!item.keyword_.contains(token, Qt::CaseInsensitive) &&
!safe_words_.contains(token, Qt::CaseInsensitive)) {
matched = false;
break;
}
result.match_quality_ = qMin(result.match_quality_, globalsearch::Quality_Middle);
}
if (result.match_quality_ == globalsearch::Quality_Middle) {
result.match_quality_ = MatchQuality(tokens, item.metadata_.title());
}
if (result.match_quality_ != globalsearch::Quality_None) {
if (matched) {
result.group_automatically_ = false;
result.metadata_ = item.metadata_;
ret << result;
}

View File

@ -88,9 +88,7 @@ void SpotifySearchProvider::SearchFinishedSlot(const pb::spotify::SearchResponse
const pb::spotify::Track& track = response.result(i);
Result result(this);
result.type_ = globalsearch::Type_Track;
SpotifyService::SongFromProtobuf(track, &result.metadata_);
result.match_quality_ = MatchQuality(state.tokens_, result.metadata_.title());
ret << result;
}
@ -98,21 +96,11 @@ void SpotifySearchProvider::SearchFinishedSlot(const pb::spotify::SearchResponse
for (int i=0 ; i<response.album_size() ; ++i) {
const pb::spotify::Album& album = response.album(i);
Result result(this);
result.type_ = globalsearch::Type_Album;
SpotifyService::SongFromProtobuf(album.metadata(), &result.metadata_);
result.match_quality_ =
qMin(MatchQuality(state.tokens_, result.metadata_.album()),
MatchQuality(state.tokens_, result.metadata_.artist()));
result.album_size_ = album.metadata().track();
for (int j=0; j < album.track_size() ; ++j) {
Song track_song;
SpotifyService::SongFromProtobuf(album.track(j), &track_song);
result.album_songs_ << track_song;
Result result(this);
SpotifyService::SongFromProtobuf(album.track(j), &result.metadata_);
ret << result;
}
ret << result;
}
emit ResultsAvailable(state.orig_id_, ret);
@ -146,31 +134,9 @@ void SpotifySearchProvider::ArtLoadedSlot(const QString& id, const QImage& image
}
void SpotifySearchProvider::LoadTracksAsync(int id, const Result& result) {
switch (result.type_) {
case globalsearch::Type_Track: {
SongMimeData* mime_data = new SongMimeData;
mime_data->songs = SongList() << result.metadata_;
emit TracksLoaded(id, mime_data);
break;
}
case globalsearch::Type_Album: {
SpotifyServer* s = server();
if (!s) {
emit TracksLoaded(id, NULL);
return;
}
QString uri = result.metadata_.url().toString();
pending_tracks_[uri] = id;
s->AlbumBrowse(uri);
break;
}
default:
break;
}
SongMimeData* mime_data = new SongMimeData;
mime_data->songs << result.metadata_;
emit TracksLoaded(id, mime_data);
}
void SpotifySearchProvider::AlbumBrowseResponse(const pb::spotify::BrowseAlbumResponse& response) {

View File

@ -1,130 +0,0 @@
/* 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 "tooltipactionwidget.h"
#include "core/logging.h"
#include <QAction>
#include <QMouseEvent>
#include <QPainter>
const int TooltipActionWidget::kBorder = 16;
const int TooltipActionWidget::kSpacing = 6;
const int TooltipActionWidget::kTopPadding = 3;
const int TooltipActionWidget::kFadeDurationMsec = 200;
TooltipActionWidget::TooltipActionWidget(QWidget* parent)
: QWidget(parent),
kTextHeight(fontMetrics().height()),
shortcut_width_(0),
description_width_(0)
{
setMouseTracking(true);
}
void TooltipActionWidget::SetActions(QList<QAction*> actions) {
actions_ = actions;
action_opacities_.clear();
int h = kTopPadding + kTextHeight * actions.count();
shortcut_width_ = 0;
description_width_ = 0;
foreach (const QAction* action, actions) {
shortcut_width_ =
qMax(shortcut_width_,
fontMetrics().width(action->shortcut().toString(QKeySequence::NativeText)));
description_width_ =
qMax(description_width_, fontMetrics().width(action->text()));
QTimeLine* timeline = new QTimeLine(kFadeDurationMsec, this);
connect(timeline, SIGNAL(valueChanged(qreal)), SLOT(update()));
action_opacities_ << timeline;
}
size_hint_ = QSize(
kBorder*2 + shortcut_width_ + kSpacing + description_width_, h);
updateGeometry();
update();
}
void TooltipActionWidget::paintEvent(QPaintEvent*) {
int y = kTopPadding;
QPainter p(this);
p.setPen(palette().color(QPalette::Text));
for (int i=0 ; i<actions_.count() ; ++i) {
const QAction* action = actions_[i];
const QTimeLine* timeline = action_opacities_[i];
const QRect shortcut_rect(kBorder, y, shortcut_width_, kTextHeight);
const QRect description_rect(shortcut_rect.right() + kSpacing, y,
description_width_, kTextHeight);
const qreal shortcut_opacity = 0.4 + 0.3 * timeline->currentValue();
const qreal description_opacity = 0.7 + 0.3 * timeline->currentValue();
p.setOpacity(shortcut_opacity);
p.drawText(shortcut_rect, Qt::AlignRight | Qt::AlignVCenter,
action->shortcut().toString(QKeySequence::NativeText));
p.setOpacity(description_opacity);
p.drawText(description_rect, Qt::AlignVCenter, action->text());
y += kTextHeight;
}
}
int TooltipActionWidget::ActionAt(const QPoint& pos) const {
return (pos.y() - kTopPadding) / kTextHeight;
}
void TooltipActionWidget::mouseMoveEvent(QMouseEvent* e) {
const int action = ActionAt(e->pos());
for (int i=0 ; i<actions_.count() ; ++i) {
if (i == action) {
StartAnimation(i, QTimeLine::Forward);
} else {
StartAnimation(i, QTimeLine::Backward);
}
}
}
void TooltipActionWidget::leaveEvent(QEvent* e) {
for (int i=0 ; i<actions_.count() ; ++i) {
StartAnimation(i, QTimeLine::Backward);
}
}
void TooltipActionWidget::StartAnimation(int i, QTimeLine::Direction direction) {
QTimeLine* timeline = action_opacities_[i];
if (timeline->direction() != direction) {
timeline->setDirection(direction);
if (timeline->state() != QTimeLine::Running)
timeline->resume();
}
}
void TooltipActionWidget::mousePressEvent(QMouseEvent* e) {
const int action = ActionAt(e->pos());
if (action >= 0 && action < actions_.count()) {
actions_[action]->trigger();
}
}

View File

@ -1,61 +0,0 @@
/* 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 TOOLTIPACTIONWIDGET_H
#define TOOLTIPACTIONWIDGET_H
#include <QTimeLine>
#include <QWidget>
class TooltipActionWidget : public QWidget {
Q_OBJECT
public:
TooltipActionWidget(QWidget* parent = 0);
static const int kBorder;
static const int kSpacing;
static const int kTopPadding;
static const int kFadeDurationMsec;
void SetActions(QList<QAction*> actions);
QSize sizeHint() const { return size_hint_; }
protected:
void paintEvent(QPaintEvent*);
void mouseMoveEvent(QMouseEvent* e);
void leaveEvent(QEvent* e);
void mousePressEvent(QMouseEvent* e);
private:
int ActionAt(const QPoint& pos) const;
void StartAnimation(int i, QTimeLine::Direction direction);
private:
const int kTextHeight;
QList<QAction*> actions_;
QList<QTimeLine*> action_opacities_;
QSize size_hint_;
int shortcut_width_;
int description_width_;
};
#endif // TOOLTIPACTIONWIDGET_H

View File

@ -1,163 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "tooltipresultwidget.h"
#include "core/logging.h"
#include <QPainter>
const int TooltipResultWidget::kBorder = 15;
const int TooltipResultWidget::kSpacing = 3;
const int TooltipResultWidget::kTrackNumSpacing = 6;
const int TooltipResultWidget::kLineHeight = 1;
const int TooltipResultWidget::kIconSize = 16;
TooltipResultWidget::TooltipResultWidget(const SearchProvider::Result& result,
QWidget* parent)
: QAbstractButton(parent),
result_(result),
kTextHeight(fontMetrics().height()),
kTrackNoWidth(fontMetrics().width("0000")),
bold_metrics_(fontMetrics())
{
bold_font_ = font();
bold_font_.setBold(true);
bold_metrics_ = QFontMetrics(bold_font_);
size_hint_ = CalculateSizeHint();
setCheckable(true);
setAutoExclusive(true);
}
QSize TooltipResultWidget::sizeHint() const {
return size_hint_;
}
QSize TooltipResultWidget::CalculateSizeHint() const {
int w = 0;
int h = 0;
// Title text
h += kSpacing + kIconSize + kSpacing + kLineHeight;
w = qMax(w, kBorder + kTrackNoWidth + kTrackNumSpacing +
bold_metrics_.width(TitleText()) + kBorder);
switch (result_.type_) {
case globalsearch::Type_Track:
case globalsearch::Type_Stream:
break;
case globalsearch::Type_Album:
if (result_.album_songs_.isEmpty())
break;
// Song list
h += kSpacing + kSpacing * (result_.album_songs_.count() - 1) +
kTextHeight * result_.album_songs_.count();
foreach (const Song& song, result_.album_songs_) {
w = qMax(w, kBorder + kTrackNoWidth + kTrackNumSpacing +
fontMetrics().width(song.TitleWithCompilationArtist()) +
kBorder);
}
h += kSpacing + kLineHeight;
break;
}
return QSize(w, h);
}
QString TooltipResultWidget::TitleText() const {
return result_.provider_->name();
}
void TooltipResultWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
const QColor text_color = palette().color(QPalette::BrightText);
const qreal line_opacity = 0.1 + (isChecked() ? 0.2 : 0.0);
const qreal track_opacity = 0.1 + (isChecked() ? 0.5 : 0.0);
const qreal text_opacity = 0.4 + (isChecked() ? 0.5 : 0.0);
int y = kSpacing;
// Title text
QRect text_rect(kBorder + kTrackNoWidth + kTrackNumSpacing, y,
width() - kBorder*2 - kTrackNoWidth - kTrackNumSpacing, kIconSize);
p.setFont(bold_font_);
p.setPen(text_color);
p.setOpacity(text_opacity);
p.drawText(text_rect, Qt::AlignVCenter, TitleText());
// Title icon
QRect icon_rect(text_rect.left() - kTrackNumSpacing - kIconSize, y,
kIconSize, kIconSize);
p.drawPixmap(icon_rect, result_.provider_->icon().pixmap(kIconSize));
// Line
y += kIconSize + kSpacing;
p.setOpacity(line_opacity);
p.setPen(text_color);
p.drawLine(0, y, width(), y);
y += kLineHeight;
switch (result_.type_) {
case globalsearch::Type_Track:
case globalsearch::Type_Stream:
break;
case globalsearch::Type_Album:
if (result_.album_songs_.isEmpty())
break;
// Song list
y += kSpacing;
p.setFont(font());
foreach (const Song& song, result_.album_songs_) {
QRect number_rect(kBorder, y, kTrackNoWidth, kTextHeight);
if (song.track() > 0) {
// Track number
p.setOpacity(track_opacity);
p.drawText(number_rect, Qt::AlignRight | Qt::AlignHCenter,
QString::number(song.track()));
}
// Song title
QRect title_rect(number_rect.right() + kTrackNumSpacing, y,
width() - number_rect.right() - kTrackNumSpacing - kBorder,
kTextHeight);
p.setOpacity(text_opacity);
p.drawText(title_rect, song.TitleWithCompilationArtist());
y += kTextHeight + kSpacing;
}
// Line
p.setOpacity(line_opacity);
p.drawLine(0, y, width(), y);
y += kLineHeight;
break;
}
y += kSpacing;
}

View File

@ -1,58 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TOOLTIPRESULTWIDGET_H
#define TOOLTIPRESULTWIDGET_H
#include "searchprovider.h"
#include <QAbstractButton>
class TooltipResultWidget : public QAbstractButton {
Q_OBJECT
public:
TooltipResultWidget(const SearchProvider::Result& result, QWidget* parent = 0);
static const int kBorder;
static const int kSpacing;
static const int kTrackNumSpacing;
static const int kLineHeight;
static const int kIconSize;
QSize sizeHint() const;
QString TitleText() const;
protected:
void paintEvent(QPaintEvent*);
private:
QSize CalculateSizeHint() const;
private:
SearchProvider::Result result_;
const int kTextHeight;
const int kTrackNoWidth;
QSize size_hint_;
QFont bold_font_;
QFontMetrics bold_metrics_;
};
#endif // TOOLTIPRESULTWIDGET_H

View File

@ -36,8 +36,7 @@ UrlSearchProvider::UrlSearchProvider(Application* app, QObject* parent)
void UrlSearchProvider::SearchAsync(int id, const QString& query) {
Result result(this);
result.match_quality_ = globalsearch::Quality_AtStart;
result.type_ = globalsearch::Type_Stream;
result.group_automatically_ = false;
result.metadata_.set_url(QUrl::fromUserInput(query));
result.metadata_.set_filetype(Song::Type_Stream);

View File

@ -949,20 +949,20 @@ void LibraryModel::FinishItem(GroupBy type,
}
}
QString LibraryModel::TextOrUnknown(const QString& text) const {
QString LibraryModel::TextOrUnknown(const QString& text) {
if (text.isEmpty()) {
return tr("Unknown");
}
return text;
}
QString LibraryModel::PrettyYearAlbum(int year, const QString& album) const {
QString LibraryModel::PrettyYearAlbum(int year, const QString& album) {
if (year <= 0)
return TextOrUnknown(album);
return QString::number(year) + " - " + TextOrUnknown(album);
}
QString LibraryModel::SortText(QString text) const {
QString LibraryModel::SortText(QString text) {
if (text.isEmpty()) {
text = " unknown";
} else {
@ -973,7 +973,7 @@ QString LibraryModel::SortText(QString text) const {
return text;
}
QString LibraryModel::SortTextForArtist(QString artist) const {
QString LibraryModel::SortTextForArtist(QString artist) {
artist = SortText(artist);
if (artist.startsWith("the ")) {
@ -983,12 +983,12 @@ QString LibraryModel::SortTextForArtist(QString artist) const {
return artist;
}
QString LibraryModel::SortTextForYear(int year) const {
QString LibraryModel::SortTextForYear(int year) {
QString str = QString::number(year);
return QString("0").repeated(qMax(0, 4 - str.length())) + str;
}
QString LibraryModel::SortTextForSong(const Song& song) const {
QString LibraryModel::SortTextForSong(const Song& song) {
QString ret = QString::number(qMax(0, song.disc()) * 1000 + qMax(0, song.track()));
ret.prepend(QString("0").repeated(6 - ret.length()));
ret.append(song.url().toString());

View File

@ -140,6 +140,14 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
//Whether or not to show letters heading in the library view
void set_show_dividers(bool show_dividers);
// Utility functions for manipulating text
static QString TextOrUnknown(const QString& text);
static QString PrettyYearAlbum(int year, const QString& album);
static QString SortText(QString text);
static QString SortTextForArtist(QString artist);
static QString SortTextForYear(int year);
static QString SortTextForSong(const Song& song);
signals:
void TotalSongCountUpdated(int count);
void GroupingChanged(const LibraryModel::Grouping& g);
@ -210,15 +218,6 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
void FinishItem(GroupBy type, bool signal, bool create_divider,
LibraryItem* parent, LibraryItem* item);
// Functions for manipulating text
QString TextOrUnknown(const QString& text) const;
QString PrettyYearAlbum(int year, const QString& album) const;
QString SortText(QString text) const;
QString SortTextForArtist(QString artist) const;
QString SortTextForYear(int year) const;
QString SortTextForSong(const Song& song) const;
QString DividerKey(GroupBy type, LibraryItem* item) const;
QString DividerDisplayText(GroupBy type, const QString& key) const;

View File

@ -62,13 +62,41 @@ void LibraryItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o
painter->save();
QRect text_rect(opt.rect);
// Does this item have an icon?
QPixmap pixmap;
QVariant decoration = index.data(Qt::DecorationRole);
if (!decoration.isNull()) {
if (decoration.canConvert<QPixmap>()) {
pixmap = decoration.value<QPixmap>();
} else if (decoration.canConvert<QIcon>()) {
pixmap = decoration.value<QIcon>().pixmap(opt.decorationSize);
}
}
// Draw the icon at the left of the text rectangle
if (!pixmap.isNull()) {
text_rect.setLeft(text_rect.left() + 15);
QRect icon_rect(text_rect.topLeft(), opt.decorationSize);
const int padding = (text_rect.height() - icon_rect.height()) / 2;
icon_rect.adjust(padding, padding, padding, padding);
text_rect.moveLeft(icon_rect.right() + padding + 6);
if (pixmap.size() != opt.decorationSize) {
pixmap = pixmap.scaled(opt.decorationSize, Qt::KeepAspectRatio);
}
painter->drawPixmap(icon_rect, pixmap);
} else {
text_rect.setLeft(text_rect.left() + 30);
}
// Draw the text
QFont bold_font(opt.font);
bold_font.setBold(true);
QRect text_rect(opt.rect);
text_rect.setLeft(text_rect.left() + 30);
painter->setPen(opt.palette.color(QPalette::Text));
painter->setFont(bold_font);
painter->drawText(text_rect, text);

View File

@ -99,7 +99,6 @@ using boost::scoped_ptr;
#include "core/mpris.h"
#include "core/mpris2.h"
#include "dbus/metatypes.h"
#include "globalsearch/globalsearchservice.h"
#include <QDBusArgument>
#include <QDBusConnection>
#include <QImage>
@ -418,8 +417,6 @@ int main(int argc, char *argv[]) {
qDBusRegisterMetaType<QList<QByteArray> >();
mpris::Mpris mpris(&app);
GlobalSearchService global_search_service(app.global_search());
#endif
// Window

View File

@ -42,6 +42,7 @@
#include "engines/enginebase.h"
#include "engines/gstengine.h"
#include "globalsearch/globalsearch.h"
#include "globalsearch/globalsearchview.h"
#include "globalsearch/librarysearchprovider.h"
#include "internet/jamendoservice.h"
#include "internet/magnatuneservice.h"
@ -162,6 +163,7 @@ MainWindow::MainWindow(Application* app,
osd_(osd),
global_shortcuts_(new GlobalShortcuts(this)),
remote_(NULL),
global_search_view_(new GlobalSearchView(app_, this)),
library_view_(new LibraryViewContainer(this)),
file_view_(new FileView(this)),
internet_view_(new InternetViewContainer(this)),
@ -218,11 +220,11 @@ MainWindow::MainWindow(Application* app,
app_->global_search()->ReloadSettings();
ui_->global_search->Init(app_->global_search());
connect(ui_->global_search, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
connect(ui_->global_search, SIGNAL(OpenSettingsAtPage(SettingsDialog::Page)), SLOT(OpenSettingsDialogAtPage(SettingsDialog::Page)));
connect(global_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*)));
connect(global_search_view_, SIGNAL(OpenSettingsAtPage(SettingsDialog::Page)), SLOT(OpenSettingsDialogAtPage(SettingsDialog::Page)));
// Add tabs to the fancy tab widget
ui_->tabs->AddTab(global_search_view_, IconLoader::Load("search"), tr("Search"));
ui_->tabs->AddTab(library_view_, IconLoader::Load("folder-sound"), tr("Library"));
ui_->tabs->AddTab(file_view_, IconLoader::Load("document-open"), tr("Files"));
ui_->tabs->AddTab(internet_view_, IconLoader::Load("applications-internet"), tr("Internet"));
@ -759,7 +761,6 @@ void MainWindow::ReloadAllSettings() {
// Other settings
app_->ReloadSettings();
app_->global_search()->ReloadSettings();
ui_->global_search->ReloadSettings();
app_->library()->ReloadSettings();
app_->player()->ReloadSettings();
osd_->ReloadSettings();
@ -2023,7 +2024,7 @@ void MainWindow::ConnectInfoView(SongInfoBase* view) {
connect(view, SIGNAL(ShowSettingsDialog()), SLOT(ShowSongInfoConfig()));
connect(view, SIGNAL(DoGlobalSearch(QString)),
ui_->global_search, SLOT(StartSearch(QString)));
global_search_view_, SLOT(StartSearch(QString)));
}
void MainWindow::AddSongInfoGenerator(smart_playlists::GeneratorPtr gen) {

View File

@ -49,6 +49,7 @@ class Equalizer;
class ErrorDialog;
class FileView;
class GlobalSearch;
class GlobalSearchView;
class GlobalShortcuts;
class GroupByDialog;
class Library;
@ -272,6 +273,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
GlobalShortcuts* global_shortcuts_;
Remote* remote_;
GlobalSearchView* global_search_view_;
LibraryViewContainer* library_view_;
FileView* file_view_;
InternetViewContainer* internet_view_;

View File

@ -35,9 +35,6 @@
<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>
@ -418,7 +415,7 @@
<x>0</x>
<y>0</y>
<width>1131</width>
<height>23</height>
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menu_music">
@ -900,12 +897,6 @@
<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"/>