Merge branch 'globalsearch-tree'
Fixes issue 2238 Fixes issue 2241 Fixes issue 2368 Fixes issue 2375 Fixes issue 2843 Fixes issue 2872 Fixes issue 2938
This commit is contained in:
commit
b427fc8a24
|
@ -331,7 +331,6 @@
|
|||
<file>providers/skyfm.png</file>
|
||||
<file>providers/digitallyimported-32.png</file>
|
||||
<file>providers/grooveshark.png</file>
|
||||
<file>allthethings.png</file>
|
||||
<file>globalsearch.css</file>
|
||||
<file>clementine-spotify-public.pem</file>
|
||||
<file>icons/22x22/user-away.png</file>
|
||||
|
|
|
@ -136,23 +136,22 @@ set(SOURCES
|
|||
engines/gstenginepipeline.cpp
|
||||
engines/gstelementdeleter.cpp
|
||||
|
||||
globalsearch/common.cpp
|
||||
globalsearch/digitallyimportedsearchprovider.cpp
|
||||
globalsearch/globalsearch.cpp
|
||||
globalsearch/globalsearchitemdelegate.cpp
|
||||
globalsearch/globalsearchmodel.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
|
||||
globalsearch/savedradiosearchprovider.cpp
|
||||
globalsearch/searchprovider.cpp
|
||||
globalsearch/searchproviderstatuswidget.cpp
|
||||
globalsearch/simplesearchprovider.cpp
|
||||
globalsearch/somafmsearchprovider.cpp
|
||||
globalsearch/tooltipactionwidget.cpp
|
||||
globalsearch/tooltipresultwidget.cpp
|
||||
globalsearch/suggestionwidget.cpp
|
||||
globalsearch/urlsearchprovider.cpp
|
||||
|
||||
internet/digitallyimportedclient.cpp
|
||||
|
@ -417,14 +416,13 @@ set(HEADERS
|
|||
engines/gstelementdeleter.h
|
||||
|
||||
globalsearch/globalsearch.h
|
||||
globalsearch/globalsearchmodel.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
|
||||
globalsearch/suggestionwidget.h
|
||||
|
||||
internet/digitallyimportedclient.h
|
||||
internet/digitallyimportedservicebase.h
|
||||
|
@ -610,7 +608,9 @@ set(UI
|
|||
devices/deviceproperties.ui
|
||||
|
||||
globalsearch/globalsearchsettingspage.ui
|
||||
globalsearch/globalsearchwidget.ui
|
||||
globalsearch/globalsearchview.ui
|
||||
globalsearch/searchproviderstatuswidget.ui
|
||||
globalsearch/suggestionwidget.ui
|
||||
|
||||
internet/digitallyimportedsettingspage.ui
|
||||
internet/groovesharksettingspage.ui
|
||||
|
@ -866,12 +866,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 +879,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
|
||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -29,6 +29,7 @@ DigitallyImportedSearchProvider::DigitallyImportedSearchProvider(
|
|||
|
||||
set_safe_words(QStringList() << "sky.fm" << "skyfm" << "di.fm" << "difm"
|
||||
<< "digitallyimported");
|
||||
set_max_suggestion_count(5);
|
||||
|
||||
connect(service_, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems()));
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
#include <QTimerEvent>
|
||||
#include <QUrl>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
const int GlobalSearch::kDelayedSearchTimeoutMs = 200;
|
||||
const char* GlobalSearch::kSettingsGroup = "GlobalSearch";
|
||||
const int GlobalSearch::kMaxResultsPerEmission = 100;
|
||||
|
@ -56,8 +58,6 @@ void GlobalSearch::ConnectProvider(SearchProvider* provider) {
|
|||
SLOT(SearchFinishedSlot(int)));
|
||||
connect(provider, SIGNAL(ArtLoaded(int,QImage)),
|
||||
SLOT(ArtLoadedSlot(int,QImage)));
|
||||
connect(provider, SIGNAL(TracksLoaded(int,MimeData*)),
|
||||
SIGNAL(TracksLoaded(int,MimeData*)));
|
||||
connect(provider, SIGNAL(destroyed(QObject*)),
|
||||
SLOT(ProviderDestroyedSlot(QObject*)));
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -278,12 +278,23 @@ bool GlobalSearch::FindCachedPixmap(const SearchProvider::Result& result,
|
|||
return pixmap_cache_.find(result.pixmap_cache_key_, pixmap);
|
||||
}
|
||||
|
||||
int GlobalSearch::LoadTracksAsync(const SearchProvider::Result& result) {
|
||||
const int id = next_id_ ++;
|
||||
MimeData* GlobalSearch::LoadTracks(const SearchProvider::ResultList& results) {
|
||||
// Different providers might create MimeData in different ways, so it's not
|
||||
// possible to combine different providers. Just take the results from a
|
||||
// single provider.
|
||||
if (results.isEmpty()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
result.provider_->LoadTracksAsync(id, result);
|
||||
SearchProvider* first_provider = results[0].provider_;
|
||||
SearchProvider::ResultList results_copy;
|
||||
foreach (const SearchProvider::Result& result, results) {
|
||||
if (result.provider_ == first_provider) {
|
||||
results_copy << result;
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
return first_provider->LoadTracks(results);
|
||||
}
|
||||
|
||||
bool GlobalSearch::SetProviderEnabled(const SearchProvider* const_provider,
|
||||
|
@ -332,14 +343,6 @@ void GlobalSearch::ReloadSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
bool GlobalSearch::HideOtherSearchBoxes() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
||||
return s.value("show_globalsearch", true).toBool() &&
|
||||
s.value("hide_others", false).toBool();
|
||||
}
|
||||
|
||||
void GlobalSearch::SaveProvidersSettings() {
|
||||
QSettings s;
|
||||
s.beginGroup(kSettingsGroup);
|
||||
|
@ -348,22 +351,28 @@ void GlobalSearch::SaveProvidersSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
QStringList GlobalSearch::GetSuggestions(int max) {
|
||||
QStringList GlobalSearch::GetSuggestions(int count) {
|
||||
QStringList ret;
|
||||
QList<SearchProvider*> eligible_providers;
|
||||
|
||||
// Get count suggestions from each provider
|
||||
foreach (SearchProvider* provider, providers_.keys()) {
|
||||
if (is_provider_enabled(provider) && provider->can_give_suggestions()) {
|
||||
eligible_providers << provider;
|
||||
foreach (QString suggestion, provider->GetSuggestions(count)) {
|
||||
suggestion = suggestion.trimmed().toLower();
|
||||
|
||||
if (!suggestion.isEmpty()) {
|
||||
ret << suggestion;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (ret.count() < max && !eligible_providers.isEmpty()) {
|
||||
SearchProvider* provider = eligible_providers.takeAt(qrand() % eligible_providers.count());
|
||||
QString suggestion = provider->GetSuggestion().trimmed();
|
||||
if (!suggestion.isEmpty())
|
||||
ret << suggestion;
|
||||
}
|
||||
// Randomize the suggestions
|
||||
std::random_shuffle(ret.begin(), ret.end());
|
||||
|
||||
// Only return the first count
|
||||
while (ret.length() > count) {
|
||||
ret.removeLast();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ public:
|
|||
static const char* kSettingsGroup;
|
||||
static const int kMaxResultsPerEmission;
|
||||
|
||||
Application* application() const { return app_; }
|
||||
|
||||
void AddProvider(SearchProvider* provider);
|
||||
// Try to change provider state. Returns false if we can't (e.g. we can't
|
||||
// enable a provider because it requires the user to be logged-in)
|
||||
|
@ -45,8 +47,8 @@ public:
|
|||
|
||||
int SearchAsync(const QString& query);
|
||||
int LoadArtAsync(const SearchProvider::Result& result);
|
||||
int LoadTracksAsync(const SearchProvider::Result& result);
|
||||
QStringList GetSuggestions(int max);
|
||||
MimeData* LoadTracks(const SearchProvider::ResultList& results);
|
||||
QStringList GetSuggestions(int count);
|
||||
|
||||
void CancelSearch(int id);
|
||||
void CancelArt(int id);
|
||||
|
@ -58,8 +60,6 @@ public:
|
|||
bool is_provider_enabled(const SearchProvider* provider) const;
|
||||
bool is_provider_usable(SearchProvider* provider) const;
|
||||
|
||||
static bool HideOtherSearchBoxes();
|
||||
|
||||
public slots:
|
||||
void ReloadSettings();
|
||||
|
||||
|
@ -70,8 +70,6 @@ signals:
|
|||
|
||||
void ArtLoaded(int id, const QPixmap& pixmap);
|
||||
|
||||
void TracksLoaded(int id, MimeData* mime_data);
|
||||
|
||||
void ProviderAdded(const SearchProvider* provider);
|
||||
void ProviderRemoved(const SearchProvider* provider);
|
||||
|
||||
|
|
|
@ -1,192 +1,34 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
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 "globalsearchitemdelegate.h"
|
||||
#include "globalsearchwidget.h"
|
||||
#include "searchprovider.h"
|
||||
#include "core/logging.h"
|
||||
#include "globalsearchview.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)
|
||||
GlobalSearchItemDelegate::GlobalSearchItemDelegate(GlobalSearchView* view)
|
||||
: LibraryItemDelegate(view),
|
||||
view_(view)
|
||||
{
|
||||
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);
|
||||
void GlobalSearchItemDelegate::paint(
|
||||
QPainter* painter, const QStyleOptionViewItem& option,
|
||||
const QModelIndex& index) const {
|
||||
// Tell the view we painted this item so it can lazy load some art.
|
||||
const_cast<GlobalSearchView*>(view_)->LazyLoadArt(index);
|
||||
|
||||
LibraryItemDelegate::paint(painter, option, index);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2010, David Sansome <me@davidsansome.com>
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
@ -18,27 +18,19 @@
|
|||
#ifndef GLOBALSEARCHITEMDELEGATE_H
|
||||
#define GLOBALSEARCHITEMDELEGATE_H
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
#include "library/libraryview.h"
|
||||
|
||||
class GlobalSearchWidget;
|
||||
class GlobalSearchView;
|
||||
|
||||
|
||||
class GlobalSearchItemDelegate : public QStyledItemDelegate {
|
||||
class GlobalSearchItemDelegate : public LibraryItemDelegate {
|
||||
public:
|
||||
GlobalSearchItemDelegate(GlobalSearchWidget* widget);
|
||||
GlobalSearchItemDelegate(GlobalSearchView* view);
|
||||
|
||||
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_;
|
||||
GlobalSearchView* view_;
|
||||
};
|
||||
|
||||
#endif // GLOBALSEARCHITEMDELEGATE_H
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
/* 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 "globalsearchmodel.h"
|
||||
#include "core/mimedata.h"
|
||||
|
||||
GlobalSearchModel::GlobalSearchModel(GlobalSearch* engine, QObject* parent)
|
||||
: QStandardItemModel(parent),
|
||||
engine_(engine),
|
||||
use_pretty_covers_(true),
|
||||
artist_icon_(":/icons/22x22/x-clementine-artist.png"),
|
||||
album_icon_(":/icons/22x22/x-clementine-album.png")
|
||||
{
|
||||
group_by_[0] = LibraryModel::GroupBy_Artist;
|
||||
group_by_[1] = LibraryModel::GroupBy_Album;
|
||||
group_by_[2] = LibraryModel::GroupBy_None;
|
||||
|
||||
no_cover_icon_ = QPixmap(":nocover.png").scaled(
|
||||
LibraryModel::kPrettyCoverSize, LibraryModel::kPrettyCoverSize,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
void GlobalSearchModel::AddResults(const SearchProvider::ResultList& results) {
|
||||
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)) {
|
||||
// Use the user's preferred order if one was set
|
||||
int configured_index = provider_order_.indexOf(provider->id());
|
||||
if (configured_index != -1) {
|
||||
sort_index = configured_index;
|
||||
} else {
|
||||
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);
|
||||
divider->setFlags(Qt::ItemIsEnabled);
|
||||
appendRow(divider);
|
||||
|
||||
provider_sort_indices_[provider] = sort_index;
|
||||
} else {
|
||||
sort_index = provider_sort_indices_[provider];
|
||||
}
|
||||
|
||||
foreach (const SearchProvider::Result& result, results) {
|
||||
QStandardItem* parent = 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;
|
||||
item->setText(result.metadata_.TitleWithCompilationArtist());
|
||||
item->setData(QVariant::fromValue(result), Role_Result);
|
||||
item->setData(sort_index, Role_ProviderIndex);
|
||||
|
||||
parent->appendRow(item);
|
||||
}
|
||||
}
|
||||
|
||||
QStandardItem* GlobalSearchModel::BuildContainers(
|
||||
const Song& s, QStandardItem* parent, ContainerKey* key, int level) {
|
||||
if (level >= 3) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
bool has_artist_icon = false;
|
||||
bool has_album_icon = false;
|
||||
QString display_text;
|
||||
QString sort_text;
|
||||
int year = 0;
|
||||
|
||||
switch (group_by_[level]) {
|
||||
case LibraryModel::GroupBy_Artist:
|
||||
if (s.is_compilation()) {
|
||||
display_text = tr("Various artists");
|
||||
sort_text = "aaaaaa";
|
||||
} else {
|
||||
display_text = LibraryModel::TextOrUnknown(s.artist());
|
||||
sort_text = LibraryModel::SortTextForArtist(s.artist());
|
||||
}
|
||||
has_artist_icon = true;
|
||||
break;
|
||||
|
||||
case LibraryModel::GroupBy_YearAlbum:
|
||||
year = qMax(0, s.year());
|
||||
display_text = LibraryModel::PrettyYearAlbum(year, s.album());
|
||||
sort_text = LibraryModel::SortTextForYear(year) + s.album();
|
||||
has_album_icon = true;
|
||||
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);
|
||||
has_album_icon = true;
|
||||
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);
|
||||
|
||||
if (has_artist_icon) {
|
||||
container->setIcon(artist_icon_);
|
||||
} else if (has_album_icon) {
|
||||
if (use_pretty_covers_) {
|
||||
container->setData(no_cover_icon_, Qt::DecorationRole);
|
||||
} else {
|
||||
container->setIcon(album_icon_);
|
||||
}
|
||||
}
|
||||
|
||||
parent->appendRow(container);
|
||||
containers_[*key] = container;
|
||||
}
|
||||
|
||||
// Create the container for the next level.
|
||||
return BuildContainers(s, container, key, level + 1);
|
||||
}
|
||||
|
||||
void GlobalSearchModel::Clear() {
|
||||
provider_sort_indices_.clear();
|
||||
containers_.clear();
|
||||
next_provider_sort_index_ = 1000;
|
||||
clear();
|
||||
}
|
||||
|
||||
SearchProvider::ResultList GlobalSearchModel::GetChildResults(
|
||||
const QModelIndexList& indexes) const {
|
||||
QList<QStandardItem*> items;
|
||||
foreach (const QModelIndex& index, indexes) {
|
||||
items << itemFromIndex(index);
|
||||
}
|
||||
return GetChildResults(items);
|
||||
}
|
||||
|
||||
SearchProvider::ResultList GlobalSearchModel::GetChildResults(
|
||||
const QList<QStandardItem*>& items) const {
|
||||
SearchProvider::ResultList results;
|
||||
QSet<const QStandardItem*> visited;
|
||||
|
||||
foreach (QStandardItem* item, items) {
|
||||
GetChildResults(item, &results, &visited);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void GlobalSearchModel::GetChildResults(const QStandardItem* item,
|
||||
SearchProvider::ResultList* results,
|
||||
QSet<const QStandardItem*>* visited) const {
|
||||
if (visited->contains(item)) {
|
||||
return;
|
||||
}
|
||||
visited->insert(item);
|
||||
|
||||
// Does this item have children?
|
||||
if (item->rowCount()) {
|
||||
// Yes - visit all the children
|
||||
for (int i=0 ; i<item->rowCount() ; ++i) {
|
||||
GetChildResults(item->child(i), results, visited);
|
||||
}
|
||||
} else {
|
||||
// No - it's a song, add its result
|
||||
results->append(item->data(Role_Result).value<SearchProvider::Result>());
|
||||
}
|
||||
}
|
||||
|
||||
QMimeData* GlobalSearchModel::mimeData(const QModelIndexList& indexes) const {
|
||||
return engine_->LoadTracks(GetChildResults(indexes));
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/* 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 GLOBALSEARCHMODEL_H
|
||||
#define GLOBALSEARCHMODEL_H
|
||||
|
||||
#include "searchprovider.h"
|
||||
#include "library/librarymodel.h"
|
||||
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class GlobalSearch;
|
||||
|
||||
class GlobalSearchModel : public QStandardItemModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GlobalSearchModel(GlobalSearch* engine, QObject* parent = 0);
|
||||
|
||||
enum Role {
|
||||
Role_Result = LibraryModel::LastRole,
|
||||
Role_LazyLoadingArt,
|
||||
Role_ProviderIndex,
|
||||
|
||||
LastRole
|
||||
};
|
||||
|
||||
struct ContainerKey {
|
||||
int provider_index_;
|
||||
QString group_[3];
|
||||
};
|
||||
|
||||
void set_use_pretty_covers(bool pretty) { use_pretty_covers_ = pretty; }
|
||||
void set_provider_order(const QStringList& provider_order) { provider_order_ = provider_order; }
|
||||
|
||||
void Clear();
|
||||
|
||||
SearchProvider::ResultList GetChildResults(const QModelIndexList& indexes) const;
|
||||
SearchProvider::ResultList GetChildResults(const QList<QStandardItem*>& items) const;
|
||||
|
||||
// QAbstractItemModel
|
||||
QMimeData* mimeData(const QModelIndexList& indexes) const;
|
||||
|
||||
public slots:
|
||||
void AddResults(const SearchProvider::ResultList& results);
|
||||
|
||||
private:
|
||||
QStandardItem* BuildContainers(const Song& metadata, QStandardItem* parent,
|
||||
ContainerKey* key, int level = 0);
|
||||
void GetChildResults(const QStandardItem* item,
|
||||
SearchProvider::ResultList* results,
|
||||
QSet<const QStandardItem*>* visited) const;
|
||||
|
||||
private:
|
||||
GlobalSearch* engine_;
|
||||
|
||||
LibraryModel::Grouping group_by_;
|
||||
|
||||
QMap<SearchProvider*, int> provider_sort_indices_;
|
||||
int next_provider_sort_index_;
|
||||
QMap<ContainerKey, QStandardItem*> containers_;
|
||||
|
||||
QStringList provider_order_;
|
||||
bool use_pretty_covers_;
|
||||
QIcon artist_icon_;
|
||||
QIcon album_icon_;
|
||||
QPixmap no_cover_icon_;
|
||||
};
|
||||
|
||||
inline uint qHash(const GlobalSearchModel::ContainerKey& key) {
|
||||
return qHash(key.provider_index_)
|
||||
^ qHash(key.group_[0])
|
||||
^ qHash(key.group_[1])
|
||||
^ qHash(key.group_[2]);
|
||||
}
|
||||
|
||||
inline bool operator <(const GlobalSearchModel::ContainerKey& left,
|
||||
const GlobalSearchModel::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 // GLOBALSEARCHMODEL_H
|
|
@ -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());
|
||||
}
|
|
@ -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
|
|
@ -79,11 +79,8 @@ void GlobalSearchSettingsPage::Load() {
|
|||
AddProviderItem(engine, provider);
|
||||
}
|
||||
|
||||
ui_->show_globalsearch->setChecked(s.value("show_globalsearch", true).toBool());
|
||||
ui_->hide_others->setChecked(s.value("hide_others", false).toBool());
|
||||
ui_->combine->setChecked(s.value("combine_identical_results", true).toBool());
|
||||
ui_->tooltip->setChecked(s.value("tooltip", true).toBool());
|
||||
ui_->tooltip_help->setChecked(s.value("tooltip_help", true).toBool());
|
||||
ui_->show_providers->setChecked(s.value("show_providers", true).toBool());
|
||||
ui_->show_suggestions->setChecked(s.value("show_suggestions", true).toBool());
|
||||
}
|
||||
|
||||
void GlobalSearchSettingsPage::AddProviderItem(GlobalSearch* engine,
|
||||
|
@ -144,11 +141,8 @@ void GlobalSearchSettingsPage::Save() {
|
|||
}
|
||||
|
||||
s.setValue("provider_order", provider_order);
|
||||
s.setValue("show_globalsearch", ui_->show_globalsearch->isChecked());
|
||||
s.setValue("hide_others", ui_->hide_others->isChecked() && ui_->show_globalsearch->isChecked());
|
||||
s.setValue("combine_identical_results", ui_->combine->isChecked());
|
||||
s.setValue("tooltip", ui_->tooltip->isChecked());
|
||||
s.setValue("tooltip_help", ui_->tooltip_help->isChecked());
|
||||
s.setValue("show_providers", ui_->show_providers->isChecked());
|
||||
s.setValue("show_suggestions", ui_->show_suggestions->isChecked());
|
||||
}
|
||||
|
||||
void GlobalSearchSettingsPage::MoveUp() {
|
||||
|
|
|
@ -18,43 +18,16 @@
|
|||
<normaloff>:/icons/32x32/search.png</normaloff>:/icons/32x32/search.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="show_globalsearch">
|
||||
<property name="text">
|
||||
<string>Show the "Search for anything" box above the sidebar</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="hide_others">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Hide all other search boxes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="sources_group">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Sources</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="combine">
|
||||
<property name="text">
|
||||
<string>Combine identical results from different sources</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Enable sources below to include them in search results. When identical results are available from more than one source, ones at the top will take priority.</string>
|
||||
<string>Enable sources below to include them in search results. Results will be displayed in this order.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
@ -136,28 +109,22 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="results_group">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QGroupBox" name="empty_list_group">
|
||||
<property name="title">
|
||||
<string>Results</string>
|
||||
<string>When the list is empty...</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="tooltip">
|
||||
<widget class="QCheckBox" name="show_providers">
|
||||
<property name="text">
|
||||
<string>Show a tooltip with more information about each result</string>
|
||||
<string>Show which sources are enabled and disabled</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="tooltip_help">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QCheckBox" name="show_suggestions">
|
||||
<property name="text">
|
||||
<string>Include keyboard shortcut help in the tooltip</string>
|
||||
<string>Show search suggestions</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -166,73 +133,16 @@
|
|||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>sources</tabstop>
|
||||
<tabstop>up</tabstop>
|
||||
<tabstop>down</tabstop>
|
||||
<tabstop>configure</tabstop>
|
||||
<tabstop>show_providers</tabstop>
|
||||
<tabstop>show_suggestions</tabstop>
|
||||
</tabstops>
|
||||
<resources>
|
||||
<include location="../../data/data.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>show_globalsearch</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>hide_others</receiver>
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>86</x>
|
||||
<y>23</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>88</x>
|
||||
<y>53</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>show_globalsearch</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>sources_group</receiver>
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>171</x>
|
||||
<y>25</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>165</x>
|
||||
<y>77</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>show_globalsearch</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>results_group</receiver>
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>201</x>
|
||||
<y>18</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>240</x>
|
||||
<y>416</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>tooltip</sender>
|
||||
<signal>toggled(bool)</signal>
|
||||
<receiver>tooltip_help</receiver>
|
||||
<slot>setEnabled(bool)</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>121</x>
|
||||
<y>447</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>123</x>
|
||||
<y>469</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "globalsearchwidget.h"
|
||||
#include "globalsearchmodel.h"
|
||||
#include "globalsearchsortmodel.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(GlobalSearchModel::Role_ProviderIndex).toInt();
|
||||
const int index_right = right.data(GlobalSearchModel::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(GlobalSearchModel::Role_Result)
|
||||
.value<SearchProvider::Result>();
|
||||
const SearchProvider::Result r2 = right.data(GlobalSearchModel::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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,441 @@
|
|||
/* 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 "globalsearchitemdelegate.h"
|
||||
#include "globalsearchmodel.h"
|
||||
#include "globalsearchsortmodel.h"
|
||||
#include "globalsearchview.h"
|
||||
#include "searchprovider.h"
|
||||
#include "searchproviderstatuswidget.h"
|
||||
#include "suggestionwidget.h"
|
||||
#include "ui_globalsearchview.h"
|
||||
#include "core/application.h"
|
||||
#include "core/logging.h"
|
||||
#include "core/mimedata.h"
|
||||
#include "core/timeconstants.h"
|
||||
#include "library/librarymodel.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItem>
|
||||
#include <QTimer>
|
||||
|
||||
const int GlobalSearchView::kSwapModelsTimeoutMsec = 250;
|
||||
const int GlobalSearchView::kMaxSuggestions = 10;
|
||||
const int GlobalSearchView::kUpdateSuggestionsTimeoutMsec = 60 * kMsecPerSec;
|
||||
|
||||
GlobalSearchView::GlobalSearchView(Application* app, QWidget* parent)
|
||||
: QWidget(parent),
|
||||
app_(app),
|
||||
engine_(app_->global_search()),
|
||||
ui_(new Ui_GlobalSearchView),
|
||||
context_menu_(NULL),
|
||||
last_search_id_(0),
|
||||
front_model_(new GlobalSearchModel(engine_, this)),
|
||||
back_model_(new GlobalSearchModel(engine_, 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)),
|
||||
update_suggestions_timer_(new QTimer(this)),
|
||||
search_icon_(IconLoader::Load("search")),
|
||||
warning_icon_(IconLoader::Load("dialog-warning")),
|
||||
show_providers_(true),
|
||||
show_suggestions_(true)
|
||||
{
|
||||
ui_->setupUi(this);
|
||||
|
||||
ui_->search->installEventFilter(this);
|
||||
ui_->results->installEventFilter(this);
|
||||
|
||||
// Must be a queued connection to ensure the GlobalSearch handles it first.
|
||||
connect(app_, SIGNAL(SettingsChanged()), SLOT(ReloadSettings()), Qt::QueuedConnection);
|
||||
|
||||
connect(ui_->search, SIGNAL(textChanged(QString)), SLOT(TextEdited(QString)));
|
||||
connect(ui_->results, SIGNAL(AddToPlaylistSignal(QMimeData*)), SIGNAL(AddToPlaylist(QMimeData*)));
|
||||
connect(ui_->results, SIGNAL(FocusOnFilterSignal(QKeyEvent*)), SLOT(FocusOnFilter(QKeyEvent*)));
|
||||
|
||||
// Set the appearance of the results list
|
||||
ui_->results->setItemDelegate(new GlobalSearchItemDelegate(this));
|
||||
ui_->results->setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||
ui_->results->setStyleSheet("QTreeView::item{padding-top:1px;}");
|
||||
|
||||
// Show the help page initially
|
||||
ui_->stack->setCurrentWidget(ui_->help_page);
|
||||
ui_->help_frame->setBackgroundRole(QPalette::Base);
|
||||
QVBoxLayout* enabled_layout = new QVBoxLayout(ui_->enabled_list);
|
||||
QVBoxLayout* disabled_layout = new QVBoxLayout(ui_->disabled_list);
|
||||
QVBoxLayout* suggestions_layout = new QVBoxLayout(ui_->suggestions_list);
|
||||
enabled_layout->setContentsMargins(16, 0, 16, 6);
|
||||
disabled_layout->setContentsMargins(16, 0, 16, 32);
|
||||
suggestions_layout->setContentsMargins(16, 0, 16, 6);
|
||||
|
||||
// Set the colour of the help text to the disabled text colour
|
||||
QPalette help_palette = ui_->help_text->palette();
|
||||
const QColor help_color = help_palette.color(QPalette::Disabled, QPalette::Text);
|
||||
help_palette.setColor(QPalette::Normal, QPalette::Text, help_color);
|
||||
help_palette.setColor(QPalette::Inactive, QPalette::Text, help_color);
|
||||
ui_->help_text->setPalette(help_palette);
|
||||
|
||||
// Create suggestion widgets
|
||||
for (int i=0 ; i<kMaxSuggestions ; ++i) {
|
||||
SuggestionWidget* widget = new SuggestionWidget(search_icon_);
|
||||
connect(widget, SIGNAL(SuggestionClicked(QString)), SLOT(StartSearch(QString)));
|
||||
suggestions_layout->addWidget(widget);
|
||||
suggestion_widgets_ << widget;
|
||||
}
|
||||
|
||||
// Make it bold
|
||||
QFont help_font = ui_->help_text->font();
|
||||
help_font.setBold(true);
|
||||
ui_->help_text->setFont(help_font);
|
||||
|
||||
// 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()));
|
||||
|
||||
update_suggestions_timer_->setInterval(kUpdateSuggestionsTimeoutMsec);
|
||||
connect(update_suggestions_timer_, SIGNAL(timeout()), SLOT(UpdateSuggestions()));
|
||||
|
||||
// These have to be queued connections because they may get emitted before
|
||||
// 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);
|
||||
}
|
||||
|
||||
GlobalSearchView::~GlobalSearchView() {
|
||||
delete ui_;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool CompareProviderName(SearchProvider* left, SearchProvider* right) {
|
||||
return left->name() < right->name();
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalSearchView::ReloadSettings() {
|
||||
QSettings s;
|
||||
|
||||
// Library settings
|
||||
s.beginGroup(LibraryView::kSettingsGroup);
|
||||
const bool pretty = s.value("pretty_covers", true).toBool();
|
||||
front_model_->set_use_pretty_covers(pretty);
|
||||
back_model_->set_use_pretty_covers(pretty);
|
||||
s.endGroup();
|
||||
|
||||
// Global search settings
|
||||
s.beginGroup(GlobalSearch::kSettingsGroup);
|
||||
const QStringList provider_order =
|
||||
s.value("provider_order", QStringList() << "library").toStringList();
|
||||
front_model_->set_provider_order(provider_order);
|
||||
back_model_->set_provider_order(provider_order);
|
||||
show_providers_ = s.value("show_providers", true).toBool();
|
||||
show_suggestions_ = s.value("show_suggestions", true).toBool();
|
||||
s.endGroup();
|
||||
|
||||
// Delete any old status widgets
|
||||
qDeleteAll(provider_status_widgets_);
|
||||
provider_status_widgets_.clear();
|
||||
|
||||
// Toggle visibility of the providers group
|
||||
ui_->providers_group->setVisible(show_providers_);
|
||||
|
||||
if (show_providers_) {
|
||||
// Sort the list of providers alphabetically
|
||||
QList<SearchProvider*> providers = engine_->providers();
|
||||
qSort(providers.begin(), providers.end(), CompareProviderName);
|
||||
|
||||
bool any_disabled = false;
|
||||
|
||||
foreach (SearchProvider* provider, providers) {
|
||||
QWidget* parent = ui_->enabled_list;
|
||||
if (!engine_->is_provider_usable(provider)) {
|
||||
parent = ui_->disabled_list;
|
||||
any_disabled = true;
|
||||
}
|
||||
|
||||
SearchProviderStatusWidget* widget =
|
||||
new SearchProviderStatusWidget(warning_icon_, engine_, provider);
|
||||
|
||||
parent->layout()->addWidget(widget);
|
||||
provider_status_widgets_ << widget;
|
||||
}
|
||||
|
||||
ui_->disabled_label->setVisible(any_disabled);
|
||||
}
|
||||
|
||||
ui_->suggestions_group->setVisible(show_suggestions_);
|
||||
if (!show_suggestions_) {
|
||||
update_suggestions_timer_->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalSearchView::UpdateSuggestions() {
|
||||
const QStringList suggestions = engine_->GetSuggestions(kMaxSuggestions);
|
||||
|
||||
for (int i=0 ; i<suggestions.count() ; ++i) {
|
||||
suggestion_widgets_[i]->SetText(suggestions[i]);
|
||||
suggestion_widgets_[i]->show();
|
||||
}
|
||||
|
||||
for (int i=suggestions.count() ; i<kMaxSuggestions ; ++i) {
|
||||
suggestion_widgets_[i]->hide();
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalSearchView::StartSearch(const QString& query) {
|
||||
ui_->search->set_text(query);
|
||||
TextEdited(query);
|
||||
|
||||
// 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.
|
||||
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;
|
||||
} else {
|
||||
last_search_id_ = engine_->SearchAsync(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalSearchView::AddResults(int id, const SearchProvider::ResultList& results) {
|
||||
if (id != last_search_id_ || results.isEmpty())
|
||||
return;
|
||||
|
||||
current_model_->AddResults(results);
|
||||
}
|
||||
|
||||
void GlobalSearchView::SwapModels() {
|
||||
art_requests_.clear();
|
||||
|
||||
qSwap(front_model_, back_model_);
|
||||
qSwap(front_proxy_, back_proxy_);
|
||||
|
||||
ui_->results->setModel(front_proxy_);
|
||||
|
||||
if (ui_->search->text().trimmed().isEmpty()) {
|
||||
ui_->stack->setCurrentWidget(ui_->help_page);
|
||||
} else {
|
||||
ui_->stack->setCurrentWidget(ui_->results_page);
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalSearchView::LazyLoadArt(const QModelIndex& proxy_index) {
|
||||
if (!proxy_index.isValid() || proxy_index.model() != front_proxy_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already loading art for this item?
|
||||
if (proxy_index.data(GlobalSearchModel::Role_LazyLoadingArt).isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Should we even load art at all?
|
||||
if (!app_->library_model()->use_pretty_covers()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Is this an album?
|
||||
const LibraryModel::GroupBy container_type = LibraryModel::GroupBy(
|
||||
proxy_index.data(LibraryModel::Role_ContainerType).toInt());
|
||||
if (container_type != LibraryModel::GroupBy_Album &&
|
||||
container_type != LibraryModel::GroupBy_AlbumArtist &&
|
||||
container_type != LibraryModel::GroupBy_YearAlbum) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the item as loading art
|
||||
const QModelIndex source_index = front_proxy_->mapToSource(proxy_index);
|
||||
QStandardItem* item = front_model_->itemFromIndex(source_index);
|
||||
item->setData(true, GlobalSearchModel::Role_LazyLoadingArt);
|
||||
|
||||
// Walk down the item's children until we find a track
|
||||
while (item->rowCount()) {
|
||||
item = item->child(0);
|
||||
}
|
||||
|
||||
// Get the track's Result
|
||||
const SearchProvider::Result result =
|
||||
item->data(GlobalSearchModel::Role_Result).value<SearchProvider::Result>();
|
||||
|
||||
// Load the art.
|
||||
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);
|
||||
|
||||
if (!pixmap.isNull()) {
|
||||
front_model_->itemFromIndex(index)->setData(pixmap, Qt::DecorationRole);
|
||||
}
|
||||
}
|
||||
|
||||
MimeData* GlobalSearchView::SelectedMimeData() {
|
||||
// Get all selected model indexes
|
||||
QModelIndexList indexes = ui_->results->selectionModel()->selectedRows();
|
||||
if (indexes.isEmpty()) {
|
||||
// There's nothing selected - take the first thing in the model that isn't
|
||||
// a divider.
|
||||
for (int i=0 ; i<front_proxy_->rowCount() ; ++i) {
|
||||
QModelIndex index = front_proxy_->index(i, 0);
|
||||
if (!index.data(LibraryModel::Role_IsDivider).toBool()) {
|
||||
indexes << index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Still got nothing? Give up.
|
||||
if (indexes.isEmpty()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get items for these indexes
|
||||
QList<QStandardItem*> items;
|
||||
foreach (const QModelIndex& index, indexes) {
|
||||
items << (front_model_->itemFromIndex(front_proxy_->mapToSource(index)));
|
||||
}
|
||||
|
||||
// Get a MimeData for these items
|
||||
return engine_->LoadTracks(front_model_->GetChildResults(items));
|
||||
}
|
||||
|
||||
bool GlobalSearchView::eventFilter(QObject* object, QEvent* event) {
|
||||
if (object == ui_->search && event->type() == QEvent::KeyRelease) {
|
||||
if (SearchKeyEvent(static_cast<QKeyEvent*>(event))) {
|
||||
return true;
|
||||
}
|
||||
} else if (object == ui_->results && event->type() == QEvent::ContextMenu) {
|
||||
if (ResultsContextMenuEvent(static_cast<QContextMenuEvent*>(event))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return QWidget::eventFilter(object, event);
|
||||
}
|
||||
|
||||
bool GlobalSearchView::SearchKeyEvent(QKeyEvent* event) {
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Up:
|
||||
ui_->results->UpAndFocus();
|
||||
break;
|
||||
|
||||
case Qt::Key_Down:
|
||||
ui_->results->DownAndFocus();
|
||||
break;
|
||||
|
||||
case Qt::Key_Escape:
|
||||
static_cast<LineEditInterface*>(ui_->search)->clear();
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
event->accept();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GlobalSearchView::ResultsContextMenuEvent(QContextMenuEvent* event) {
|
||||
if (!context_menu_) {
|
||||
context_menu_ = new QMenu(this);
|
||||
context_menu_->addAction(IconLoader::Load("media-playback-start"),
|
||||
tr("Append to current playlist"), this, SLOT(AddSelectedToPlaylist()));
|
||||
context_menu_->addAction(IconLoader::Load("media-playback-start"),
|
||||
tr("Replace current playlist"), this, SLOT(LoadSelected()));
|
||||
context_menu_->addAction(IconLoader::Load("document-new"),
|
||||
tr("Open in new playlist"), this, SLOT(OpenSelectedInNewPlaylist()));
|
||||
|
||||
context_menu_->addSeparator();
|
||||
context_menu_->addAction(IconLoader::Load("go-next"),
|
||||
tr("Queue track"), this, SLOT(AddSelectedToPlaylistEnqueue()));
|
||||
}
|
||||
|
||||
context_menu_->popup(event->globalPos());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalSearchView::AddSelectedToPlaylist() {
|
||||
emit AddToPlaylist(SelectedMimeData());
|
||||
}
|
||||
|
||||
void GlobalSearchView::LoadSelected() {
|
||||
MimeData* data = SelectedMimeData();
|
||||
data->clear_first_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void GlobalSearchView::AddSelectedToPlaylistEnqueue() {
|
||||
MimeData* data = SelectedMimeData();
|
||||
data->enqueue_now_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void GlobalSearchView::OpenSelectedInNewPlaylist() {
|
||||
MimeData* data = SelectedMimeData();
|
||||
data->open_in_new_playlist_ = true;
|
||||
emit AddToPlaylist(data);
|
||||
}
|
||||
|
||||
void GlobalSearchView::showEvent(QShowEvent* e) {
|
||||
if (show_suggestions_) {
|
||||
UpdateSuggestions();
|
||||
update_suggestions_timer_->start();
|
||||
}
|
||||
QWidget::showEvent(e);
|
||||
}
|
||||
|
||||
void GlobalSearchView::hideEvent(QHideEvent* e) {
|
||||
update_suggestions_timer_->stop();
|
||||
QWidget::hideEvent(e);
|
||||
}
|
||||
|
||||
void GlobalSearchView::FocusOnFilter(QKeyEvent* event) {
|
||||
ui_->search->set_focus();
|
||||
QApplication::sendEvent(ui_->search, event);
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
/* 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 GlobalSearchModel;
|
||||
class SearchProviderStatusWidget;
|
||||
class SuggestionWidget;
|
||||
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;
|
||||
static const int kMaxSuggestions;
|
||||
static const int kUpdateSuggestionsTimeoutMsec;
|
||||
|
||||
// Called by the delegate
|
||||
void LazyLoadArt(const QModelIndex& index);
|
||||
|
||||
// QWidget
|
||||
void showEvent(QShowEvent* e);
|
||||
void hideEvent(QHideEvent* e);
|
||||
|
||||
// QObject
|
||||
bool eventFilter(QObject* object, QEvent* event);
|
||||
|
||||
public slots:
|
||||
void ReloadSettings();
|
||||
void StartSearch(const QString& query);
|
||||
|
||||
signals:
|
||||
void AddToPlaylist(QMimeData* data);
|
||||
void OpenSettingsAtPage(SettingsDialog::Page page);
|
||||
|
||||
private slots:
|
||||
void UpdateSuggestions();
|
||||
|
||||
void SwapModels();
|
||||
void TextEdited(const QString& text);
|
||||
void AddResults(int id, const SearchProvider::ResultList& results);
|
||||
void ArtLoaded(int id, const QPixmap& pixmap);
|
||||
|
||||
void FocusOnFilter(QKeyEvent* event);
|
||||
|
||||
void AddSelectedToPlaylist();
|
||||
void LoadSelected();
|
||||
void OpenSelectedInNewPlaylist();
|
||||
void AddSelectedToPlaylistEnqueue();
|
||||
|
||||
private:
|
||||
MimeData* SelectedMimeData();
|
||||
|
||||
bool SearchKeyEvent(QKeyEvent* event);
|
||||
bool ResultsContextMenuEvent(QContextMenuEvent* event);
|
||||
|
||||
private:
|
||||
Application* app_;
|
||||
GlobalSearch* engine_;
|
||||
Ui_GlobalSearchView* ui_;
|
||||
|
||||
QMenu* context_menu_;
|
||||
|
||||
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.
|
||||
GlobalSearchModel* front_model_;
|
||||
GlobalSearchModel* back_model_;
|
||||
GlobalSearchModel* current_model_;
|
||||
|
||||
QSortFilterProxyModel* front_proxy_;
|
||||
QSortFilterProxyModel* back_proxy_;
|
||||
QSortFilterProxyModel* current_proxy_;
|
||||
|
||||
QMap<int, QModelIndex> art_requests_;
|
||||
|
||||
QTimer* swap_models_timer_;
|
||||
QTimer* update_suggestions_timer_;
|
||||
|
||||
QList<SearchProviderStatusWidget*> provider_status_widgets_;
|
||||
QList<SuggestionWidget*> suggestion_widgets_;
|
||||
|
||||
QIcon search_icon_;
|
||||
QIcon warning_icon_;
|
||||
|
||||
bool show_providers_;
|
||||
bool show_suggestions_;
|
||||
};
|
||||
|
||||
#endif // GLOBALSEARCHVIEW_H
|
|
@ -0,0 +1,206 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GlobalSearchView</class>
|
||||
<widget class="QWidget" name="GlobalSearchView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>437</width>
|
||||
<height>633</height>
|
||||
</rect>
|
||||
</property>
|
||||
<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="hint" stdset="0">
|
||||
<string>Search for anything</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QStackedWidget" name="stack">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="results_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="AutoExpandingTreeView" name="results">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="dragEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="dragDropMode">
|
||||
<enum>QAbstractItemView::DragOnly</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="allColumnsShowFocus">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="help_page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="help_frame">
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="help_frame_contents">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>435</width>
|
||||
<height>604</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>32</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>16</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>32</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>64</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="help_text">
|
||||
<property name="text">
|
||||
<string>Enter search terms above to find music on your computer and on the internet</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="providers_group" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Clementine will find music in:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="enabled_list" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="disabled_label">
|
||||
<property name="text">
|
||||
<string>But these sources are disabled:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="disabled_list" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="suggestions_group" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Why not try...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="suggestions_list" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>widgets/lineedit.h</header>
|
||||
</customwidget>
|
||||
<customwidget>
|
||||
<class>AutoExpandingTreeView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>widgets/autoexpandingtreeview.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -1,51 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>GlobalSearchWidget</class>
|
||||
<widget class="QWidget" name="GlobalSearchWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>522</width>
|
||||
<height>53</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<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>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>LineEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>widgets/lineedit.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -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;
|
||||
}
|
||||
|
@ -118,34 +113,6 @@ void GroovesharkSearchProvider::AlbumArtLoaded(quint64 id, const QImage& image)
|
|||
emit ArtLoaded(original_id, 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool GroovesharkSearchProvider::IsLoggedIn() {
|
||||
return (service_ && service_->IsLoggedIn());
|
||||
}
|
||||
|
@ -158,24 +125,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);
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ class GroovesharkSearchProvider : public SearchProvider {
|
|||
// SearchProvider
|
||||
void SearchAsync(int id, const QString& query);
|
||||
void LoadArtAsync(int id, const Result& result);
|
||||
void LoadTracksAsync(int id, const Result& result);
|
||||
bool IsLoggedIn();
|
||||
void ShowConfig();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ LastFMSearchProvider::LastFMSearchProvider(LastFMService* service,
|
|||
icon_ = ScaleAndPad(QImage(":last.fm/as.png"));
|
||||
|
||||
set_safe_words(QStringList() << "lastfm" << "last.fm");
|
||||
set_max_suggestion_count(3);
|
||||
|
||||
connect(service, SIGNAL(SavedItemsChanged()), SLOT(MaybeRecreateItems()));
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
#include "library/sqlrow.h"
|
||||
#include "playlist/songmimedata.h"
|
||||
|
||||
#include <QStack>
|
||||
|
||||
|
||||
LibrarySearchProvider::LibrarySearchProvider(LibraryBackendInterface* backend,
|
||||
const QString& name,
|
||||
|
@ -55,108 +57,25 @@ 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;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void LibrarySearchProvider::LoadTracksAsync(int id, const Result& result) {
|
||||
SongList ret;
|
||||
MimeData* LibrarySearchProvider::LoadTracks(const ResultList& results) {
|
||||
MimeData* ret = SearchProvider::LoadTracks(results);
|
||||
static_cast<SongMimeData*>(ret)->backend = backend_;
|
||||
|
||||
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;
|
||||
|
||||
emit TracksLoaded(id, mime_data);
|
||||
return ret;
|
||||
}
|
||||
|
||||
QString LibrarySearchProvider::GetSuggestion() {
|
||||
QStringList LibrarySearchProvider::GetSuggestions(int count) {
|
||||
// We'd like to use order by random(), but that's O(n) in sqlite, so instead
|
||||
// get the largest ROWID and pick a couple of random numbers within that
|
||||
// range.
|
||||
|
@ -167,13 +86,19 @@ QString LibrarySearchProvider::GetSuggestion() {
|
|||
q.SetIncludeUnavailable(true);
|
||||
q.SetLimit(1);
|
||||
|
||||
QStringList ret;
|
||||
|
||||
if (!backend_->ExecQuery(&q) || !q.Next()) {
|
||||
return QString();
|
||||
return ret;
|
||||
}
|
||||
|
||||
const int largest_rowid = q.Value(0).toInt();
|
||||
|
||||
for (int attempt=0 ; attempt<10 ; ++attempt) {
|
||||
for (int attempt=0 ; attempt<count*5 ; ++attempt) {
|
||||
if (ret.count() >= count) {
|
||||
break;
|
||||
}
|
||||
|
||||
LibraryQuery q;
|
||||
q.SetColumnSpec("artist, album");
|
||||
q.SetIncludeUnavailable(true);
|
||||
|
@ -188,12 +113,12 @@ QString LibrarySearchProvider::GetSuggestion() {
|
|||
const QString album = q.Value(1).toString();
|
||||
|
||||
if (!artist.isEmpty() && !album.isEmpty())
|
||||
return (qrand() % 2 == 0) ? artist : album;
|
||||
ret << ((qrand() % 2 == 0) ? artist : album);
|
||||
else if (!artist.isEmpty())
|
||||
return artist;
|
||||
ret << artist;
|
||||
else if (!album.isEmpty())
|
||||
return album;
|
||||
ret << album;
|
||||
}
|
||||
|
||||
return QString();
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -31,8 +31,8 @@ public:
|
|||
Application* app, QObject* parent = 0);
|
||||
|
||||
ResultList Search(int id, const QString& query);
|
||||
void LoadTracksAsync(int id, const Result& result);
|
||||
QString GetSuggestion();
|
||||
MimeData* LoadTracks(const ResultList& results);
|
||||
QStringList GetSuggestions(int count);
|
||||
|
||||
private:
|
||||
LibraryBackendInterface* backend_;
|
||||
|
|
|
@ -25,7 +25,10 @@ SavedRadioSearchProvider::SavedRadioSearchProvider(SavedRadio* service,
|
|||
: SimpleSearchProvider(app, parent),
|
||||
service_(service)
|
||||
{
|
||||
Init(tr("Your radio streams"), "savedradio", IconLoader::Load("document-open-remote"));
|
||||
Init(tr("Your radio streams"), "savedradio", IconLoader::Load("document-open-remote"),
|
||||
MimeDataContainsUrlsOnly);
|
||||
|
||||
set_max_suggestion_count(3);
|
||||
|
||||
connect(service_, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems()));
|
||||
|
||||
|
@ -45,10 +48,3 @@ void SavedRadioSearchProvider::RecreateItems() {
|
|||
|
||||
SetItems(items);
|
||||
}
|
||||
|
||||
void SavedRadioSearchProvider::LoadTracksAsync(int id, const Result& result) {
|
||||
MimeData* mime_data = new MimeData;
|
||||
mime_data->setUrls(QList<QUrl>() << result.metadata_.url());
|
||||
|
||||
emit TracksLoaded(id, mime_data);
|
||||
}
|
||||
|
|
|
@ -26,8 +26,6 @@ class SavedRadioSearchProvider : public SimpleSearchProvider {
|
|||
public:
|
||||
SavedRadioSearchProvider(SavedRadio* service, Application* app, QObject* parent);
|
||||
|
||||
void LoadTracksAsync(int id, const Result& result);
|
||||
|
||||
protected:
|
||||
void RecreateItems();
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "playlist/songmimedata.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QUrl>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
const int SearchProvider::kArtHeight = 32;
|
||||
|
@ -57,20 +58,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,28 +120,29 @@ 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());
|
||||
}
|
||||
|
||||
void SearchProvider::LoadTracksAsync(int id, const Result& result) {
|
||||
SongMimeData* mime_data = new SongMimeData;
|
||||
mime_data->songs = SongList() << result.metadata_;
|
||||
MimeData* SearchProvider::LoadTracks(const ResultList& results) {
|
||||
MimeData* mime_data = NULL;
|
||||
|
||||
emit TracksLoaded(id, mime_data);
|
||||
if (mime_data_contains_urls_only()) {
|
||||
mime_data = new MimeData;
|
||||
} else {
|
||||
SongMimeData* song_mime_data = new SongMimeData;
|
||||
mime_data = song_mime_data;
|
||||
|
||||
foreach (const Result& result, results) {
|
||||
song_mime_data->songs << result.metadata_;
|
||||
}
|
||||
}
|
||||
|
||||
QList<QUrl> urls;
|
||||
foreach (const Result& result, results) {
|
||||
urls << result.metadata_.url();
|
||||
}
|
||||
mime_data->setUrls(urls);
|
||||
|
||||
return mime_data;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
@ -96,7 +91,13 @@ public:
|
|||
// Normally providers get enabled unless the user chooses otherwise.
|
||||
// Setting this flag indicates that this provider is disabled by default
|
||||
// instead.
|
||||
DisabledByDefault = 0x40
|
||||
DisabledByDefault = 0x40,
|
||||
|
||||
// The default implementation of LoadTracksAsync normally creates a
|
||||
// SongMimeData containing the entire metadata for each result being loaded.
|
||||
// Setting this flag will cause a plain MimeData to be created containing
|
||||
// only the URLs of the results.
|
||||
MimeDataContainsUrlsOnly = 0x80
|
||||
};
|
||||
Q_DECLARE_FLAGS(Hints, Hint)
|
||||
|
||||
|
@ -113,6 +114,7 @@ public:
|
|||
bool can_give_suggestions() const { return hints() & CanGiveSuggestions; }
|
||||
bool is_disabled_by_default() const { return hints() & DisabledByDefault; }
|
||||
bool is_enabled_by_default() const { return !is_disabled_by_default(); }
|
||||
bool mime_data_contains_urls_only() const { return hints() & MimeDataContainsUrlsOnly; }
|
||||
|
||||
// Starts a search. Must emit ResultsAvailable zero or more times and then
|
||||
// SearchFinished exactly once, using this ID.
|
||||
|
@ -122,14 +124,15 @@ public:
|
|||
// ResultsAvailable. Must emit ArtLoaded exactly once with this ID.
|
||||
virtual void LoadArtAsync(int id, const Result& result);
|
||||
|
||||
// Starts loading tracks for a result that was previously emitted by
|
||||
// ResultsAvailable. Must emit TracksLoaded exactly once with this ID.
|
||||
virtual void LoadTracksAsync(int id, const Result& result);
|
||||
// Loads tracks for results that were previously emitted by ResultsAvailable.
|
||||
// The default implementation creates a SongMimeData with one Song for each
|
||||
// Result, unless the MimeDataContainsUrlsOnly flag is set.
|
||||
virtual MimeData* LoadTracks(const ResultList& results);
|
||||
|
||||
// Returns an example search string to display in the UI. The provider should
|
||||
// pick one of its items at random. Remember to set the CanGiveSuggestions
|
||||
// hint.
|
||||
virtual QString GetSuggestion() { return QString(); }
|
||||
// Returns some example search strings to display in the UI. The provider
|
||||
// should pick some of its items at random and return between 0 and count
|
||||
// strings. Remember to set the CanGiveSuggestions hint.
|
||||
virtual QStringList GetSuggestions(int count) { return QStringList(); }
|
||||
|
||||
// If provider needs user login to search and play songs, this method should
|
||||
// be reimplemented
|
||||
|
@ -144,17 +147,12 @@ signals:
|
|||
|
||||
void ArtLoaded(int id, const QImage& image);
|
||||
|
||||
void TracksLoaded(int id, MimeData* mime_data);
|
||||
|
||||
protected:
|
||||
// These functions treat queries in the same way as LibraryQuery. They're
|
||||
// useful for figuring out whether you got a result because it matched in
|
||||
// the song title or the artist/album name.
|
||||
static QStringList TokenizeQuery(const QString& query);
|
||||
static 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,
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/* 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 "searchprovider.h"
|
||||
#include "searchproviderstatuswidget.h"
|
||||
#include "ui_searchproviderstatuswidget.h"
|
||||
#include "core/application.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
SearchProviderStatusWidget::SearchProviderStatusWidget(
|
||||
const QIcon& warning_icon,
|
||||
GlobalSearch* engine, SearchProvider* provider,
|
||||
QWidget* parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_SearchProviderStatusWidget),
|
||||
engine_(engine),
|
||||
provider_(provider)
|
||||
{
|
||||
ui_->setupUi(this);
|
||||
|
||||
ui_->icon->setPixmap(provider->icon().pixmap(16));
|
||||
ui_->name->setText(provider->name());
|
||||
|
||||
const bool enabled = engine->is_provider_enabled(provider);
|
||||
const bool logged_in = provider->IsLoggedIn();
|
||||
|
||||
if (enabled && logged_in) {
|
||||
ui_->disabled_group->hide();
|
||||
} else {
|
||||
const QString disabled_text = tr("Disabled");
|
||||
const QString not_logged_in_text = tr("Not logged in");
|
||||
const int disabled_width = fontMetrics().width(" ") + qMax(
|
||||
fontMetrics().width(disabled_text),
|
||||
fontMetrics().width(not_logged_in_text));
|
||||
|
||||
ui_->disabled_reason->setMinimumWidth(disabled_width);
|
||||
ui_->disabled_reason->setText(logged_in ? disabled_text : not_logged_in_text);
|
||||
ui_->disabled_icon->setPixmap(warning_icon.pixmap(16));
|
||||
|
||||
ui_->disabled_reason->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
|
||||
SearchProviderStatusWidget::~SearchProviderStatusWidget() {
|
||||
delete ui_;
|
||||
}
|
||||
|
||||
bool SearchProviderStatusWidget::eventFilter(QObject* object, QEvent* event) {
|
||||
if (object != ui_->disabled_reason) {
|
||||
return QWidget::eventFilter(object, event);
|
||||
}
|
||||
|
||||
QFont font(ui_->disabled_reason->font());
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::Enter:
|
||||
font.setUnderline(true);
|
||||
ui_->disabled_reason->setFont(font);
|
||||
break;
|
||||
|
||||
case QEvent::Leave:
|
||||
font.setUnderline(false);
|
||||
ui_->disabled_reason->setFont(font);
|
||||
break;
|
||||
|
||||
case QEvent::MouseButtonRelease: {
|
||||
QMouseEvent* e = static_cast<QMouseEvent*>(event);
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
if (!provider_->IsLoggedIn()) {
|
||||
provider_->ShowConfig();
|
||||
} else {
|
||||
engine_->application()->OpenSettingsDialogAtPage(SettingsDialog::Page_GlobalSearch);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/* 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 SEARCHPROVIDERSTATUSWIDGET_H
|
||||
#define SEARCHPROVIDERSTATUSWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class GlobalSearch;
|
||||
class SearchProvider;
|
||||
class Ui_SearchProviderStatusWidget;
|
||||
|
||||
class SearchProviderStatusWidget : public QWidget {
|
||||
public:
|
||||
SearchProviderStatusWidget(const QIcon& warning_icon,
|
||||
GlobalSearch* engine, SearchProvider* provider,
|
||||
QWidget* parent = 0);
|
||||
~SearchProviderStatusWidget();
|
||||
|
||||
bool eventFilter(QObject* object, QEvent* event);
|
||||
|
||||
private:
|
||||
Ui_SearchProviderStatusWidget* ui_;
|
||||
|
||||
GlobalSearch* engine_;
|
||||
SearchProvider* provider_;
|
||||
};
|
||||
|
||||
#endif // SEARCHPROVIDERSTATUSWIDGET_H
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SearchProviderStatusWidget</class>
|
||||
<widget class="QWidget" name="SearchProviderStatusWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>464</width>
|
||||
<height>110</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="icon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="name">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="disabled_group" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="disabled_icon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="disabled_reason">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="cursor">
|
||||
<cursorShape>PointingHandCursor</cursorShape>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -39,6 +39,7 @@ SimpleSearchProvider::Item::Item(const Song& song, const QString& keyword)
|
|||
SimpleSearchProvider::SimpleSearchProvider(Application* app, QObject* parent)
|
||||
: BlockingSearchProvider(app, parent),
|
||||
result_limit_(kDefaultResultLimit),
|
||||
max_suggestion_count_(-1),
|
||||
items_dirty_(true),
|
||||
has_searched_before_(false)
|
||||
{
|
||||
|
@ -67,38 +68,19 @@ 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) &&
|
||||
!item.metadata_.title().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 result(this);
|
||||
result.group_automatically_ = false;
|
||||
result.metadata_ = item.metadata_;
|
||||
ret << result;
|
||||
}
|
||||
|
@ -110,34 +92,36 @@ SearchProvider::ResultList SimpleSearchProvider::Search(int id, const QString& q
|
|||
return ret;
|
||||
}
|
||||
|
||||
void SimpleSearchProvider::LoadTracksAsync(int id, const Result& result) {
|
||||
Song metadata = result.metadata_;
|
||||
metadata.set_filetype(Song::Type_Stream);
|
||||
|
||||
SongMimeData* mime_data = new SongMimeData;
|
||||
mime_data->songs = SongList() << metadata;
|
||||
|
||||
emit TracksLoaded(id, mime_data);
|
||||
}
|
||||
|
||||
void SimpleSearchProvider::SetItems(const ItemList& items) {
|
||||
QMutexLocker l(&items_mutex_);
|
||||
items_ = items;
|
||||
for (ItemList::iterator it = items_.begin() ; it != items_.end() ; ++it) {
|
||||
it->metadata_.set_filetype(Song::Type_Stream);
|
||||
}
|
||||
}
|
||||
|
||||
QString SimpleSearchProvider::GetSuggestion() {
|
||||
QStringList SimpleSearchProvider::GetSuggestions(int count) {
|
||||
if (max_suggestion_count_ != -1) {
|
||||
count = qMin(max_suggestion_count_, count);
|
||||
}
|
||||
|
||||
QStringList ret;
|
||||
QMutexLocker l(&items_mutex_);
|
||||
|
||||
if (items_.isEmpty())
|
||||
return QString();
|
||||
return ret;
|
||||
|
||||
for (int attempt=0 ; attempt<count*5 ; ++attempt) {
|
||||
if (ret.count() >= count) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (int attempt=0 ; attempt<10 ; ++attempt) {
|
||||
const Item& item = items_[qrand() % items_.count()];
|
||||
if (!item.keyword_.isEmpty())
|
||||
return item.keyword_;
|
||||
ret << item.keyword_;
|
||||
if (!item.metadata_.title().isEmpty())
|
||||
return item.metadata_.title();
|
||||
ret << item.metadata_.title();
|
||||
}
|
||||
|
||||
return QString();
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -32,8 +32,7 @@ public:
|
|||
ResultList Search(int id, const QString& query);
|
||||
|
||||
// SearchProvider
|
||||
void LoadTracksAsync(int id, const Result& result);
|
||||
QString GetSuggestion();
|
||||
QStringList GetSuggestions(int count);
|
||||
|
||||
protected slots:
|
||||
// Calls RecreateItems now if the user has done a global search with this
|
||||
|
@ -55,6 +54,7 @@ protected:
|
|||
|
||||
int result_limit() const { return result_limit_; }
|
||||
void set_result_limit(int result_limit) { result_limit_ = result_limit; }
|
||||
void set_max_suggestion_count(int count) { max_suggestion_count_ = count; }
|
||||
QStringList safe_words() const { return safe_words_; }
|
||||
void set_safe_words(const QStringList& safe_words) { safe_words_ = safe_words; }
|
||||
|
||||
|
@ -67,6 +67,7 @@ protected:
|
|||
private:
|
||||
int result_limit_;
|
||||
QStringList safe_words_;
|
||||
int max_suggestion_count_;
|
||||
|
||||
QMutex items_mutex_;
|
||||
ItemList items_;
|
||||
|
|
|
@ -24,6 +24,7 @@ SomaFMSearchProvider::SomaFMSearchProvider(SomaFMService* service, Application*
|
|||
{
|
||||
Init("SomaFM", "somafm", QIcon(":/providers/somafm.png"), CanGiveSuggestions);
|
||||
set_result_limit(3);
|
||||
set_max_suggestion_count(3);
|
||||
icon_ = ScaleAndPad(QImage(":/providers/somafm.png"));
|
||||
|
||||
connect(service, SIGNAL(StreamsChanged()), SLOT(MaybeRecreateItems()));
|
||||
|
|
|
@ -47,8 +47,6 @@ SpotifyServer* SpotifySearchProvider::server() {
|
|||
SLOT(SearchFinishedSlot(pb::spotify::SearchResponse)));
|
||||
connect(server_, SIGNAL(ImageLoaded(QString,QImage)),
|
||||
SLOT(ArtLoadedSlot(QString,QImage)));
|
||||
connect(server_, SIGNAL(AlbumBrowseResults(pb::spotify::BrowseAlbumResponse)),
|
||||
SLOT(AlbumBrowseResponse(pb::spotify::BrowseAlbumResponse)));
|
||||
connect(server_, SIGNAL(destroyed()), SLOT(ServerDestroyed()));
|
||||
|
||||
return server_;
|
||||
|
@ -88,9 +86,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 +94,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);
|
||||
|
@ -145,54 +131,6 @@ void SpotifySearchProvider::ArtLoadedSlot(const QString& id, const QImage& image
|
|||
emit ArtLoaded(orig_id, ScaleAndPad(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;
|
||||
}
|
||||
}
|
||||
|
||||
void SpotifySearchProvider::AlbumBrowseResponse(const pb::spotify::BrowseAlbumResponse& response) {
|
||||
QString uri = QStringFromStdString(response.uri());
|
||||
QMap<QString, int>::iterator it = pending_tracks_.find(uri);
|
||||
if (it == pending_tracks_.end())
|
||||
return;
|
||||
|
||||
const int orig_id = it.value();
|
||||
pending_tracks_.erase(it);
|
||||
|
||||
SongMimeData* mime_data = new SongMimeData;
|
||||
|
||||
for (int i=0 ; i<response.track_size() ; ++i) {
|
||||
Song song;
|
||||
SpotifyService::SongFromProtobuf(response.track(i), &song);
|
||||
mime_data->songs << song;
|
||||
}
|
||||
|
||||
emit TracksLoaded(orig_id, mime_data);
|
||||
}
|
||||
|
||||
bool SpotifySearchProvider::IsLoggedIn() {
|
||||
if (server()) {
|
||||
return service_->IsLoggedIn();
|
||||
|
|
|
@ -33,7 +33,6 @@ public:
|
|||
|
||||
void SearchAsync(int id, const QString& query);
|
||||
void LoadArtAsync(int id, const Result& result);
|
||||
void LoadTracksAsync(int id, const Result& result);
|
||||
|
||||
bool IsLoggedIn();
|
||||
void ShowConfig();
|
||||
|
@ -43,8 +42,6 @@ private slots:
|
|||
void SearchFinishedSlot(const pb::spotify::SearchResponse& response);
|
||||
void ArtLoadedSlot(const QString& id, const QImage& image);
|
||||
|
||||
void AlbumBrowseResponse(const pb::spotify::BrowseAlbumResponse& response);
|
||||
|
||||
private:
|
||||
SpotifyServer* server();
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "suggestionwidget.h"
|
||||
#include "ui_suggestionwidget.h"
|
||||
#include "ui/iconloader.h"
|
||||
|
||||
#include <QMouseEvent>
|
||||
|
||||
SuggestionWidget::SuggestionWidget(const QIcon& search_icon, QWidget* parent)
|
||||
: QWidget(parent),
|
||||
ui_(new Ui_SuggestionWidget)
|
||||
{
|
||||
ui_->setupUi(this);
|
||||
|
||||
ui_->icon->setPixmap(search_icon.pixmap(16));
|
||||
ui_->name->installEventFilter(this);
|
||||
}
|
||||
|
||||
SuggestionWidget::~SuggestionWidget() {
|
||||
delete ui_;
|
||||
}
|
||||
|
||||
void SuggestionWidget::SetText(const QString& text) {
|
||||
ui_->name->setText(text);
|
||||
}
|
||||
|
||||
bool SuggestionWidget::eventFilter(QObject* object, QEvent* event) {
|
||||
if (object != ui_->name) {
|
||||
return QWidget::eventFilter(object, event);
|
||||
}
|
||||
|
||||
QFont font(ui_->name->font());
|
||||
|
||||
switch (event->type()) {
|
||||
case QEvent::Enter:
|
||||
font.setUnderline(true);
|
||||
ui_->name->setFont(font);
|
||||
break;
|
||||
|
||||
case QEvent::Leave:
|
||||
font.setUnderline(false);
|
||||
ui_->name->setFont(font);
|
||||
break;
|
||||
|
||||
case QEvent::MouseButtonRelease: {
|
||||
QMouseEvent* e = static_cast<QMouseEvent*>(event);
|
||||
if (e->button() == Qt::LeftButton) {
|
||||
QString text = ui_->name->text();
|
||||
text.replace(QRegExp("\\W"), " ");
|
||||
|
||||
emit SuggestionClicked(text.simplified());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/* This file is part of Clementine.
|
||||
Copyright 2012, David Sansome <me@davidsansome.com>
|
||||
|
||||
Clementine is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Clementine is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef SUGGESTIONWIDGET_H
|
||||
#define SUGGESTIONWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class Ui_SuggestionWidget;
|
||||
|
||||
class SuggestionWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SuggestionWidget(const QIcon& search_icon, QWidget* parent = 0);
|
||||
~SuggestionWidget();
|
||||
|
||||
bool eventFilter(QObject* object, QEvent* event);
|
||||
|
||||
public slots:
|
||||
void SetText(const QString& text);
|
||||
|
||||
signals:
|
||||
void SuggestionClicked(const QString& query);
|
||||
|
||||
private:
|
||||
Ui_SuggestionWidget* ui_;
|
||||
};
|
||||
|
||||
#endif // SUGGESTIONWIDGET_H
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SuggestionWidget</class>
|
||||
<widget class="QWidget" name="SuggestionWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>464</width>
|
||||
<height>110</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="icon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="name">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="cursor">
|
||||
<cursorShape>PointingHandCursor</cursorShape>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -31,14 +31,14 @@ UrlSearchProvider::UrlSearchProvider(Application* app, QObject* parent)
|
|||
QIcon icon = IconLoader::Load("applications-internet");
|
||||
image_ = ScaleAndPad(icon.pixmap(kArtHeight, kArtHeight).toImage());
|
||||
|
||||
Init("URL", "url", icon);
|
||||
Init("URL", "url", icon, MimeDataContainsUrlsOnly);
|
||||
}
|
||||
|
||||
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_title(result.metadata_.url().toString());
|
||||
result.metadata_.set_filetype(Song::Type_Stream);
|
||||
|
||||
emit ResultsAvailable(id, ResultList() << result);
|
||||
|
@ -48,13 +48,6 @@ void UrlSearchProvider::LoadArtAsync(int id, const Result&) {
|
|||
emit ArtLoaded(id, image_);
|
||||
}
|
||||
|
||||
void UrlSearchProvider::LoadTracksAsync(int id, const Result& result) {
|
||||
MimeData* mime_data = new MimeData;
|
||||
mime_data->setUrls(QList<QUrl>() << result.metadata_.url());
|
||||
|
||||
emit TracksLoaded(id, mime_data);
|
||||
}
|
||||
|
||||
bool UrlSearchProvider::LooksLikeUrl(const QString& query) const {
|
||||
return url_regex_.indexIn(query) == 0;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ public:
|
|||
|
||||
void SearchAsync(int id, const QString& query);
|
||||
void LoadArtAsync(int id, const Result& result);
|
||||
void LoadTracksAsync(int id, const Result& result);
|
||||
|
||||
private:
|
||||
static const char* kUrlRegex;
|
||||
|
|
|
@ -108,9 +108,6 @@ void InternetViewContainer::SetHeaderVisible(QWidget* header, bool visible) {
|
|||
if (!header)
|
||||
return;
|
||||
|
||||
if (visible && GlobalSearch::HideOtherSearchBoxes())
|
||||
return;
|
||||
|
||||
HeaderData& d = headers_[header];
|
||||
if (d.visible_ == visible)
|
||||
return;
|
||||
|
|
|
@ -71,7 +71,6 @@ LibraryModel::LibraryModel(LibraryBackend* backend, Application* app,
|
|||
total_song_count_(0),
|
||||
artist_icon_(":/icons/22x22/x-clementine-artist.png"),
|
||||
album_icon_(":/icons/22x22/x-clementine-album.png"),
|
||||
no_cover_icon_(":nocover.png"),
|
||||
playlists_dir_icon_(IconLoader::Load("folder-sound")),
|
||||
playlist_icon_(":/icons/22x22/x-clementine-albums.png"),
|
||||
init_task_id_(-1),
|
||||
|
@ -92,7 +91,7 @@ LibraryModel::LibraryModel(LibraryBackend* backend, Application* app,
|
|||
SIGNAL(ImageLoaded(quint64,QImage)),
|
||||
SLOT(AlbumArtLoaded(quint64,QImage)));
|
||||
|
||||
no_cover_icon_pretty_ = QPixmap(":nocover.png").scaled(
|
||||
no_cover_icon_ = QPixmap(":nocover.png").scaled(
|
||||
kPrettyCoverSize, kPrettyCoverSize,
|
||||
Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
|
||||
|
@ -405,7 +404,7 @@ QString LibraryModel::AlbumIconPixmapCacheKey(const QModelIndex& index) const {
|
|||
QVariant LibraryModel::AlbumIcon(const QModelIndex& index) {
|
||||
LibraryItem* item = IndexToItem(index);
|
||||
if (!item)
|
||||
return no_cover_icon_pretty_;
|
||||
return no_cover_icon_;
|
||||
|
||||
// Check the cache for a pixmap we already loaded.
|
||||
const QString cache_key = AlbumIconPixmapCacheKey(index);
|
||||
|
@ -416,7 +415,7 @@ QVariant LibraryModel::AlbumIcon(const QModelIndex& index) {
|
|||
|
||||
// Maybe we're loading a pixmap already?
|
||||
if (pending_cache_keys_.contains(cache_key)) {
|
||||
return no_cover_icon_pretty_;
|
||||
return no_cover_icon_;
|
||||
}
|
||||
|
||||
// No art is cached and we're not loading it already. Load art for the first
|
||||
|
@ -429,7 +428,7 @@ QVariant LibraryModel::AlbumIcon(const QModelIndex& index) {
|
|||
pending_cache_keys_.insert(cache_key);
|
||||
}
|
||||
|
||||
return no_cover_icon_pretty_;
|
||||
return no_cover_icon_;
|
||||
}
|
||||
|
||||
void LibraryModel::AlbumArtLoaded(quint64 id, const QImage& image) {
|
||||
|
@ -444,7 +443,7 @@ void LibraryModel::AlbumArtLoaded(quint64 id, const QImage& image) {
|
|||
// Insert this image in the cache.
|
||||
if (image.isNull()) {
|
||||
// Set the no_cover image so we don't continually try to load art.
|
||||
QPixmapCache::insert(cache_key, no_cover_icon_pretty_);
|
||||
QPixmapCache::insert(cache_key, no_cover_icon_);
|
||||
} else {
|
||||
QPixmapCache::insert(cache_key, QPixmap::fromImage(image));
|
||||
}
|
||||
|
@ -949,20 +948,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 +972,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 +982,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());
|
||||
|
|
|
@ -136,10 +136,19 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
|||
|
||||
// Whether or not to use album cover art, if it exists, in the library view
|
||||
void set_pretty_covers(bool use_pretty_covers);
|
||||
bool use_pretty_covers() const { return use_pretty_covers_; }
|
||||
|
||||
//Whether or not to show letters heading in the library view
|
||||
// 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 +219,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;
|
||||
|
||||
|
@ -260,8 +260,7 @@ class LibraryModel : public SimpleTreeModel<LibraryItem> {
|
|||
QIcon album_icon_;
|
||||
// used as a generic icon to show when no cover art is found,
|
||||
// fixed to the same size as the artwork (32x32)
|
||||
QPixmap no_cover_icon_pretty_;
|
||||
QIcon no_cover_icon_;
|
||||
QPixmap no_cover_icon_;
|
||||
QIcon playlists_dir_icon_;
|
||||
QIcon playlist_icon_;
|
||||
|
||||
|
|
|
@ -62,13 +62,39 @@ 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()) {
|
||||
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);
|
||||
|
|
|
@ -47,6 +47,5 @@ LibraryFilterWidget* LibraryViewContainer::filter() const {
|
|||
}
|
||||
|
||||
void LibraryViewContainer::ReloadSettings() {
|
||||
filter()->setVisible(!GlobalSearch::HideOtherSearchBoxes());
|
||||
view()->ReloadSettings();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
@ -420,8 +419,6 @@ int main(int argc, char *argv[]) {
|
|||
qDBusRegisterMetaType<QList<QByteArray> >();
|
||||
|
||||
mpris::Mpris mpris(&app);
|
||||
|
||||
GlobalSearchService global_search_service(app.global_search());
|
||||
#endif
|
||||
|
||||
// Window
|
||||
|
|
|
@ -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)),
|
||||
|
@ -217,12 +219,13 @@ MainWindow::MainWindow(Application* app,
|
|||
IconLoader::Load("folder-sound"), true, app_, this));
|
||||
|
||||
app_->global_search()->ReloadSettings();
|
||||
global_search_view_->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 +762,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 +2025,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) {
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Reference in New Issue